From 3f9f1befe42dec3df1cc209d0dd76dda3fc7550d Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Wed, 28 Oct 2026 16:38:43 -0700 Subject: [PATCH] feat: add bounded polynomial determinants and release benchmarks - Add canonical sparse rational polynomials and exact determinant evaluation through order eight with caller-selected work budgets and typed failures. - Capture all 1,403 public benchmark cases in parallel default/exact release suites, then verify and archive their raw samples and original provenance. - Adopt research-repo-tools v0.2.4 for shared updater and release commands; keep Semgrep setup and fixture checks in the standard Python 3.15 environment until the tracked v0.2.5 cleanup. - Validate with the full Rust CI pass, final check gate, and 303 Python consumer tests after shared-package adoption. Closes #308 Closes #316 Refs #304 Refs #317 --- .github/actions/setup-tools/action.yml | 2 +- .github/workflows/release-benchmarks.yml | 183 +++++--- CONTRIBUTING.md | 10 +- Cargo.toml | 9 + README.md | 10 + REFERENCES.md | 15 + benches/common/polynomial.rs | 103 +++++ benches/polynomial.rs | 40 ++ docs/BENCHMARKING.md | 124 ++++-- docs/RELEASING.md | 39 +- docs/code_organization.md | 33 +- docs/dev/performance-tooling-adoption.md | 32 +- docs/mathematical_basis.md | 78 +++- examples/polynomial_determinant.rs | 43 ++ justfile | 17 +- pyproject.toml | 6 +- src/error.rs | 144 +++++- src/lib.rs | 24 +- src/polynomial.rs | 232 ++++++++++ src/rational.rs | 2 +- tests/polynomial_bench_inputs.rs | 58 +++ tests/polynomial_determinant.rs | 419 ++++++++++++++++++ tests/proptest_polynomial.rs | 66 +++ .../tooling/test_justfile_discoverability.py | 6 +- tests/tooling/test_maintenance_integration.py | 2 +- tests/tooling/test_performance_workflow.py | 39 +- tests/tooling/test_release_baseline.py | 258 ++++++++--- tests/tooling/test_release_publishing.py | 7 +- tests/tooling/test_shared_tooling.py | 73 +++ tests/tooling/test_toolchain_integration.py | 8 +- tooling/README.md | 39 +- tooling/criterion-inventory.toml | 161 ++++++- tooling/criterion-saved.toml | 310 ++++++++++++- tooling/examples.toml | 3 + tooling/measurements/full.toml | 334 +++++++++++++- tooling/performance-policy.toml | 155 ++++++- tooling/performance.just | 7 +- tooling/release_performance.py | 4 +- tooling/shared_tooling.py | 49 ++ uv.lock | 10 +- 40 files changed, 2875 insertions(+), 279 deletions(-) create mode 100644 benches/common/polynomial.rs create mode 100644 benches/polynomial.rs create mode 100644 examples/polynomial_determinant.rs create mode 100644 src/polynomial.rs create mode 100644 tests/polynomial_bench_inputs.rs create mode 100644 tests/polynomial_determinant.rs create mode 100644 tests/proptest_polynomial.rs create mode 100644 tests/tooling/test_shared_tooling.py create mode 100644 tooling/shared_tooling.py diff --git a/.github/actions/setup-tools/action.yml b/.github/actions/setup-tools/action.yml index c54b5e6..f3fc6d6 100644 --- a/.github/actions/setup-tools/action.yml +++ b/.github/actions/setup-tools/action.yml @@ -29,7 +29,7 @@ runs: shell: bash env: RESEARCH_REPO_TOOLS_HOME: ${{ runner.temp }}/rrt - run: uv run --locked --managed-python --only-group tooling research-repo-tools setup + run: uv run --locked --managed-python --only-group tooling python tooling/shared_tooling.py setup - name: Export verified managed paths shell: bash diff --git a/.github/workflows/release-benchmarks.yml b/.github/workflows/release-benchmarks.yml index 1517c73..c2f6156 100644 --- a/.github/workflows/release-benchmarks.yml +++ b/.github/workflows/release-benchmarks.yml @@ -37,7 +37,7 @@ jobs: - name: Install trusted package runner uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.13.1" + version: "0.13.6" enable-cache: false cache-python: false @@ -54,114 +54,171 @@ jobs: echo "::error::Dispatch with --ref $RELEASE_TAG and the matching tag input" exit 1 fi - target="$(uvx --python 3.15 --from research-repo-tools==0.2.3 research-repo-tools \ + target="$(uvx --python 3.15 --from research-repo-tools==0.2.4 research-repo-tools \ --root "$RUNNER_TEMP" performance release-draft "$GH_REPO" "$RELEASE_TAG" \ --commit "$GITHUB_SHA" --expected-title "$RELEASE_TAG" | jq -c .)" echo "target=$target" >> "$GITHUB_OUTPUT" - release-baseline: + measure-baseline: needs: validate-release runs-on: ubuntu-latest - # 2 min checkout + 60 min shared setup + 140 min default + 130 min exact - # + 18 min for the tail and runner overhead; below the hosted 360 min limit. - timeout-minutes: 350 + strategy: + fail-fast: false + matrix: + suite: [public-default, public-exact] + # Per runner: 2 min checkout + 60 min preparation + 2 min host capture + # + 240 min measurement + 26 min packaging, upload and overhead. + timeout-minutes: 330 env: RELEASE_TAG: ${{ inputs.tag }} + BENCH_SUITE: ${{ matrix.suite }} CRITERION_HOME: ${{ github.workspace }}/target/criterion - outputs: - release-asset: ${{ steps.package-baseline.outputs.asset }} - artifact-name: ${{ steps.package-baseline.outputs.artifact-name }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # Use the workflow's own commit and tag cache scope, never a separate ref. ref: ${{ github.sha }} persist-credentials: false timeout-minutes: 2 - name: Prepare release benchmarks - # One parent timeout bounds all nested installs, validation, and inventory. timeout-minutes: 60 uses: ./.github/actions/prepare-release-benchmarks # zizmor: ignore[self-repository] actionlint 1.7.12 does not accept $/... with: validated-attempt: ${{ needs.validate-release.outputs.validated-attempt }} - - name: Save default-feature Criterion baseline - id: comparative - timeout-minutes: 140 - run: | - set -euo pipefail - echo "[release-baseline] public-default started $(date -u +%FT%TZ); budget 140 min" - date +%s > target/public-default-started - just bench-save-baseline "$RELEASE_TAG" public-default - date +%s > target/public-default-finished - - - name: Save exact Criterion baseline - id: exact - timeout-minutes: 130 + - name: Capture runner host + timeout-minutes: 2 + run: uv run --locked --no-sync research-repo-tools performance host --output target/release-host.json + + - name: Save Criterion suite + id: measurement + timeout-minutes: 240 run: | set -euo pipefail - echo "[release-baseline] public-exact started $(date -u +%FT%TZ); budget 130 min" - date +%s > target/public-exact-started - just bench-save-baseline "$RELEASE_TAG" public-exact - date +%s > target/public-exact-finished + echo "[release-baseline] $BENCH_SUITE started $(date -u +%FT%TZ); budget 240 min" + date +%s > "target/$BENCH_SUITE-started" + just bench-save-baseline "$RELEASE_TAG" "$BENCH_SUITE" + date +%s > "target/$BENCH_SUITE-finished" - - name: Validate complete release dataset - run: just bench-release-check "$RELEASE_TAG" - - - name: Package release Criterion baseline - id: package-baseline + - name: Package Criterion suite run: | set -euo pipefail + output="target/release-suites/$BENCH_SUITE" + features=bench + if [[ "$BENCH_SUITE" == public-exact ]]; then + features=bench,exact + fi + uvx --python 3.15 --from research-repo-tools==0.2.4 research-repo-tools performance pack-suite \ + tooling/criterion-saved.toml "$BENCH_SUITE" --saved-sample "$RELEASE_TAG" \ + --output "$output/criterion.tar.gz" --revision "$GITHUB_SHA" \ + --session "$GITHUB_REPOSITORY:$GITHUB_RUN_ID:$GITHUB_RUN_ATTEMPT" \ + --host target/release-host.json \ + --started-at "$(cat "target/$BENCH_SUITE-started")" \ + --finished-at "$(cat "target/$BENCH_SUITE-finished")" \ + --context "command=just bench-save-baseline $RELEASE_TAG $BENCH_SUITE" \ + --context "features=$features" --context "runner=$RUNNER_NAME" \ + --context "image=$ImageOS" --context "image_version=$ImageVersion" \ + --context "rustc=$(rustc -vV | paste -sd ';' -)" \ + --context "cargo=$(cargo --version)" \ + --context "cargo_lock_sha256=$(sha256sum Cargo.lock | cut -d ' ' -f 1)" + # Transport the native inventory separately for the assembly plan. + # Both archives must match it and the checked-in required case policy. + if [[ "$BENCH_SUITE" == public-default ]]; then + cp target/release-benchmark-inventory.json "$output/inventory.json" + fi - asset="la-stack-${RELEASE_TAG}-criterion-baseline.tar.gz" - cp target/release-benchmark-inventory.json target/criterion/release-benchmark-inventory.json - tar -C target -czf "$asset" criterion - echo "asset=$asset" >> "$GITHUB_OUTPUT" - echo "artifact-name=bench-baseline-${RELEASE_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" - - - name: Upload temporary baseline artifact + - name: Upload temporary suite artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: ${{ steps.package-baseline.outputs.artifact-name }} - path: ${{ steps.package-baseline.outputs.asset }} + name: bench-suite-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.suite }} + path: target/release-suites/${{ matrix.suite }} retention-days: 30 if-no-files-found: error - name: Suite timing summary if: ${{ always() }} env: - COMPARATIVE_OUTCOME: ${{ steps.comparative.outcome }} - EXACT_OUTCOME: ${{ steps.exact.outcome }} + SUITE_OUTCOME: ${{ steps.measurement.outcome }} run: | set -euo pipefail + elapsed="not started" + if [[ -f "target/$BENCH_SUITE-started" ]]; then + end="$(date +%s)" + if [[ -f "target/$BENCH_SUITE-finished" ]]; then + end="$(cat "target/$BENCH_SUITE-finished")" + fi + elapsed="$((end - $(cat "target/$BENCH_SUITE-started")))" + fi { echo "### Release benchmark timing" echo "" echo "| Suite | Outcome | Elapsed seconds | Budget minutes |" echo "| --- | --- | --- | --- |" - for suite in public-default public-exact; do - elapsed="not started" - if [[ -f "target/$suite-started" ]]; then - end="$(date +%s)" - if [[ -f "target/$suite-finished" ]]; then - end="$(cat "target/$suite-finished")" - fi - elapsed="$((end - $(cat "target/$suite-started")))" - fi - outcome="$COMPARATIVE_OUTCOME" - budget=140 - if [[ "$suite" == public-exact ]]; then - outcome="$EXACT_OUTCOME" - budget=130 - fi - echo "| $suite | $outcome | $elapsed | $budget |" - done + echo "| $BENCH_SUITE | $SUITE_OUTCOME | $elapsed | 240 |" echo "" echo "An unfinished suite reports elapsed time through this summary." } | tee -a "$GITHUB_STEP_SUMMARY" + release-baseline: + needs: [validate-release, measure-baseline] + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + RELEASE_TAG: ${{ inputs.tag }} + outputs: + release-asset: ${{ steps.package-baseline.outputs.asset }} + artifact-name: ${{ steps.package-baseline.outputs.artifact-name }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Install trusted package runner + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: "0.13.6" + enable-cache: false + cache-python: false + + - name: Download default suite + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: bench-suite-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}-public-default + path: target/release-suites/public-default + + - name: Download exact suite + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: bench-suite-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}-public-exact + path: target/release-suites/public-exact + + - name: Assemble release Criterion baseline + id: package-baseline + run: | + set -euo pipefail + + asset="la-stack-${RELEASE_TAG}-criterion-baseline.tar.gz" + cp target/release-suites/public-default/inventory.json target/release-benchmark-inventory.json + uvx --python 3.15 --from research-repo-tools==0.2.4 research-repo-tools \ + --root "$GITHUB_WORKSPACE" performance assemble-suites tooling/criterion-saved.toml \ + target/release-suites/public-default/criterion.tar.gz \ + target/release-suites/public-exact/criterion.tar.gz \ + --saved-sample "$RELEASE_TAG" --output "$asset" --revision "$GITHUB_SHA" \ + --session "$GITHUB_REPOSITORY:$GITHUB_RUN_ID:$GITHUB_RUN_ATTEMPT" + echo "asset=$asset" >> "$GITHUB_OUTPUT" + echo "artifact-name=bench-baseline-${RELEASE_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" + + - name: Upload temporary baseline artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: ${{ steps.package-baseline.outputs.artifact-name }} + path: ${{ steps.package-baseline.outputs.asset }} + retention-days: 30 + if-no-files-found: error + publish-baseline: needs: [validate-release, release-baseline] permissions: @@ -173,7 +230,7 @@ jobs: - name: Install trusted package runner uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.13.1" + version: "0.13.6" enable-cache: false cache-python: false @@ -194,7 +251,7 @@ jobs: [[ "$RELEASE_ASSET" == "la-stack-${RELEASE_TAG}-criterion-baseline.tar.gz" ]] || exit 1 printf '%s\n' "$RELEASE_TARGET" > "$RUNNER_TEMP/release-target.json" - uvx --python 3.15 --from research-repo-tools==0.2.3 research-repo-tools \ + uvx --python 3.15 --from research-repo-tools==0.2.4 research-repo-tools \ --root "$RUNNER_TEMP" performance release-upload "$GH_REPO" "$RELEASE_TAG" \ "$GITHUB_WORKSPACE/$RELEASE_ASSET" --target "$RUNNER_TEMP/release-target.json" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8056b3..ce19b70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ exact version declared in `pyproject.toml`, and `jq`. Authenticate the GitHub CL the declared Python, Rust, Cargo tools, and Just through the locked PyPI package: ```bash -uv run --locked --managed-python --only-group tooling research-repo-tools setup +uv run --locked --managed-python --only-group tooling python tooling/shared_tooling.py setup ``` Set up the remaining development tools and validate the checkout: @@ -39,7 +39,7 @@ change files. Run bare `just` for the complete generated command list, including arguments and descriptions. The [command policy](docs/dev/docs.md#command-discovery-and-placement) owns recipe naming and documentation placement. -Setup, tool checks, updates, changelog, and opt-in CodeRabbit review use the published `research-repo-tools==0.2.3` package, +Setup, tool checks, updates, changelog, and opt-in CodeRabbit review use the published `research-repo-tools==0.2.4` package, locked in the `tooling` dependency group and included by `dev`. Its `python-tools` extra owns the pytest, Ruff and Ty versions. `toolchain.inherit-python-tools` checks those versions against the lock and @@ -71,12 +71,14 @@ Then `just update-dependencies` upgrades Cargo requirements and refreshes Cargo' lock, resolves exact direct Python `dev` pins, refreshes the complete `uv.lock`, and explicitly synchronizes `dev` with managed Rust available for native builds. Included groups retain their constraints, including the exact shared-package pin. -Semgrep shares the root Python 3.15 environment and lockfile. Preserve its exact +Semgrep scans and fixture checks share the root Python 3.15 environment and lockfile. Preserve its exact pin, native-wheel requirement and compatibility overrides for MCP, PyJWT, OpenTelemetry and Python 3.15 Windows wheels. Scanner commands retain the Linux stack-limit workaround required by Semgrep on Python 3.15. The hosted dependency audit checks the root Python lockfile with the shared OSV adapter; its existing -Cargo audit retains the native Rust advisory policy. +Cargo audit retains the native Rust advisory policy. The shared fixture checker +uses that same installed scanner; see the +[tooling guide](tooling/README.md#setup-and-validation). `just update-cargo-dependencies` excludes `num-bigint` and `num-rational` from incompatible requirement upgrades because their public types must advance together; Cargo can still refresh locked versions within those requirements. diff --git a/Cargo.toml b/Cargo.toml index 43502ee..e38c7c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,10 @@ required-features = [ "exact" ] name = "rational_input_5x5" required-features = [ "exact" ] +[[example]] +name = "polynomial_determinant" +required-features = [ "exact" ] + [[bench]] name = "public_api" harness = false @@ -121,6 +125,11 @@ name = "rational_dot" harness = false required-features = [ "bench", "exact" ] +[[bench]] +name = "polynomial" +harness = false +required-features = [ "bench", "exact" ] + [profile.release] lto = "fat" codegen-units = 1 diff --git a/README.md b/README.md index 75e94d6..c8bc573 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,11 @@ encloses exact-real values during a small set of outward-rounded operations and feeds `IntervalMatrix` determinant proofs; it does not make `Matrix` generic over alternate scalars or provide a general interval package. +With `exact`, `SparsePolynomial` supplies rational coefficients keyed by `u128` +exponents for bounded polynomial determinants through order eight. Its first +nonzero term uses increasing exponent order; callers own perturbation encoding +and work limits. This focused kernel retains cancellation exactly. + This is not a generic scalar-parameterized API. Exact support intentionally covers the robustness-sensitive operations that require it: determinant sign, determinant value, and linear solve, followed by explicit strict or rounded @@ -198,6 +203,7 @@ to combine operations. | Certified dot, affine-difference, and determinant estimates | [`ScalarWithErrorBound`][api-scalar-bound], [`DeterminantWithErrorBound`][api-det-bound] | | Exact rational dot products¹ | [`RationalVector::dot`][api-rational-dot], [`rational_dot`][api-rational-dot-slice] | | Exact signs, determinants, solves, and output conversion¹ | [Exact arithmetic examples][api-exact] | +| Exact sparse polynomial determinants¹ | [`polynomial_determinant`][api-polynomial], [`SparsePolynomial`][api-sparse-polynomial] | | Floating-point determinants and solves | [`Matrix`][api-matrix], [`Lu`][api-lu], [`Ldlt`][api-ldlt] | | Gram matrix construction | [`gram_matrix`][api-gram] | | Interval expressions, determinant signs, and solution enclosures | [`Interval`][api-interval], [`IntervalMatrix`][api-interval-matrix] | @@ -431,6 +437,7 @@ The `examples/` directory contains small, runnable programs: - **[`exact_sign_3x3`][example-exact-sign]** — exact determinant sign of a near-singular 3×3 matrix (requires `exact` feature) - **[`exact_solve_3x3`][example-exact-solve]** — exact solve of a near-singular 3×3 system vs f64 LU (requires `exact` feature) - **[`ldlt_solve_3x3`][example-ldlt-solve]** — solve a 3×3 symmetric positive definite system via LDLT +- **[`polynomial_determinant`][example-polynomial]** — exact cancellation, leading coefficients, and explicit work limits (requires `exact` feature) - **[`rational_input_5x5`][example-rational-input]** — exact rational solve of a 5×5 system that becomes singular as f64 (requires `exact` feature) - **[`solve_5x5`][example-solve]** — solve a 5×5 system via LU with partial pivoting - **[`vector_angles`][example-vector-angles]** — unsigned angles for fixed vectors and borrowed slices, including extreme magnitudes and subnormal separations @@ -441,6 +448,7 @@ The `examples/` directory contains small, runnable programs: [example-exact-sign]: https://github.com/acgetchell/la-stack/blob/main/examples/exact_sign_3x3.rs [example-exact-solve]: https://github.com/acgetchell/la-stack/blob/main/examples/exact_solve_3x3.rs [example-ldlt-solve]: https://github.com/acgetchell/la-stack/blob/main/examples/ldlt_solve_3x3.rs +[example-polynomial]: https://github.com/acgetchell/la-stack/blob/main/examples/polynomial_determinant.rs [example-rational-input]: https://github.com/acgetchell/la-stack/blob/main/examples/rational_input_5x5.rs [example-solve]: https://github.com/acgetchell/la-stack/blob/main/examples/solve_5x5.rs [example-vector-angles]: https://github.com/acgetchell/la-stack/blob/main/examples/vector_angles.rs @@ -572,6 +580,8 @@ BSD 3-Clause License. See [LICENSE](https://github.com/acgetchell/la-stack/blob/ [audit-badge]: https://github.com/acgetchell/la-stack/actions/workflows/audit.yml/badge.svg [api-column-basis]: https://docs.rs/la-stack/latest/la_stack/fn.column_basis.html +[api-polynomial]: https://docs.rs/la-stack/latest/la_stack/fn.polynomial_determinant.html +[api-sparse-polynomial]: https://docs.rs/la-stack/latest/la_stack/struct.SparsePolynomial.html [api-rational-dot]: https://docs.rs/la-stack/latest/la_stack/struct.RationalVector.html#method.dot [api-rational-dot-slice]: https://docs.rs/la-stack/latest/la_stack/fn.rational_dot.html [audit-workflow]: https://github.com/acgetchell/la-stack/actions/workflows/audit.yml diff --git a/REFERENCES.md b/REFERENCES.md index 133b031..1ebceb2 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -10,6 +10,7 @@ - [Exact determinant sign](#exact-determinant-sign-adaptive-precision-integer-arithmetic) - [Exact linear system solve](#exact-linear-system-solve-hybrid-bareiss--bigrational) - [Exact rational inputs](#exact-rational-inputs-and-row-denominator-clearing) + - [Exact sparse polynomial determinants](#exact-sparse-polynomial-determinants) - [Exact-to-binary64 conversion](#exact-to-binary64-conversion) - [f64 → integer decomposition](#f64--integer-decomposition-decompose_proven_finite_f64) - [Gram matrices and geometric measures](#gram-matrices-and-geometric-measures) @@ -127,6 +128,20 @@ See the [rational-input construction](docs/mathematical_basis.md#exact-arithmeti quotients. Integer arithmetic introduces no rounding; the slice adapter validates shape and denominators before reduction. +### Exact sparse polynomial determinants + +`polynomial_determinant` reorganizes the Leibniz determinant identity \[[12]\] +over the commutative ring of univariate rational polynomials. Its column-subset +recurrence is adapted from Delaunay's +[`polynomial_determinant_leading_sign`][delaunay-polynomial], +with canonical inputs, caller-selected budgets, typed failures and a complete +polynomial result. The [local recurrence proof](docs/mathematical_basis.md#exact-sparse-polynomial-determinants) +defines the permutation sign and cancellation invariant. This is a division-free +exact expansion, not an approximate determinant or a general symbolic algebra +system; perturbation encoding and geometric sign interpretation remain downstream. + +[delaunay-polynomial]: https://github.com/acgetchell/delaunay/blob/3fa8a9907101c523574a0d17991a1f652b5419dc/src/geometry/sos.rs + ### Exact-to-binary64 conversion Strict conversion accepts only dyadic rationals whose reduced significand and diff --git a/benches/common/polynomial.rs b/benches/common/polynomial.rs new file mode 100644 index 0000000..09c74b8 --- /dev/null +++ b/benches/common/polynomial.rs @@ -0,0 +1,103 @@ +#![forbid(unsafe_code)] + +//! A consumer-shaped, maximally coincident lifted matrix and factorial oracle. + +use std::array::from_fn; +use std::cmp::Ordering; +use std::collections::BTreeMap; + +use la_stack::{ + BigRational, PolynomialDeterminantBudget, SparsePolynomial, polynomial_determinant, +}; + +use crate::bench_utils::OrAbort; + +pub(crate) const BUDGET: PolynomialDeterminantBudget = PolynomialDeterminantBudget { + max_subset_transitions: 1_024, + max_term_products: 55_631_284, +}; + +/// A fixture checked against independent permutation enumeration before timing. +pub(crate) struct ValidatedPolynomialInput { + pub(crate) matrix: [[SparsePolynomial; N]; N], +} + +/// All unperturbed points are at the origin. A coordinate is `t^(3^variable)`, +/// its lift is the sum of coordinate squares, and the last column is one. +/// Each permutation chooses one squared coordinate in its lifted row. Base-three +/// digits (0, 1, 2) identify that row, its axis and every other coordinate choice +/// uniquely, so the `N! * (N-2)` monomials are distinct and have coefficient ±1. +pub(crate) fn coincident_lifted() -> ValidatedPolynomialInput { + assert!((3..=8).contains(&N)); + let axes = N - 2; + let powers: [[u128; N]; N] = from_fn(|row| { + from_fn(|col| { + if col < axes { + 3_u128.pow(u32::try_from(row * axes + col).or_abort("bounded variable index")) + } else { + 0 + } + }) + }); + let matrix = from_fn(|row| { + from_fn(|col| { + let exponents = match col.cmp(&axes) { + Ordering::Less => vec![powers[row][col]], + Ordering::Equal => (0..axes).map(|axis| 2 * powers[row][axis]).collect(), + Ordering::Greater => vec![0], + }; + SparsePolynomial::try_from_terms( + exponents + .into_iter() + .map(|e| (e, BigRational::from_integer(1.into()))), + ) + .or_abort("integer polynomial coefficients") + }) + }); + let mut expected = BTreeMap::new(); + enumerate(&powers, &mut from_fn(|i| i), 0, &mut expected); + assert_eq!(expected.len(), (1..=N).product::() * axes); + let determinant = + polynomial_determinant(&matrix, BUDGET).or_abort("lifted determinant validation"); + assert_eq!(determinant.terms(), &expected); + ValidatedPolynomialInput { matrix } +} + +fn enumerate( + powers: &[[u128; N]; N], + columns: &mut [usize; N], + row: usize, + result: &mut BTreeMap, +) { + if row < N { + for col in row..N { + columns.swap(row, col); + enumerate(powers, columns, row + 1, result); + columns.swap(row, col); + } + return; + } + let inversions: usize = (0..N) + .map(|i| (i + 1..N).filter(|&j| columns[i] > columns[j]).count()) + .sum(); + for axis in 0..N - 2 { + let exponent = columns + .iter() + .enumerate() + .map(|(i, &j)| { + if j == N - 2 { + 2 * powers[i][axis] + } else { + powers[i][j] + } + }) + .sum(); + let coefficient = + BigRational::from_integer(if inversions.is_multiple_of(2) { 1 } else { -1 }.into()); + assert_eq!( + result.insert(exponent, coefficient), + None, + "unique base-three encoding" + ); + } +} diff --git a/benches/polynomial.rs b/benches/polynomial.rs new file mode 100644 index 0000000..bd54a5c --- /dev/null +++ b/benches/polynomial.rs @@ -0,0 +1,40 @@ +#![forbid(unsafe_code)] + +//! Exact determinant and leading-term extraction for coincident lifted points. + +use std::hint::black_box; + +use criterion::Criterion; +use la_stack::polynomial_determinant; + +#[path = "common/bench_utils.rs"] +mod bench_utils; +use bench_utils::OrAbort; + +#[path = "common/polynomial.rs"] +mod polynomial; +use polynomial::{BUDGET, coincident_lifted}; + +fn register(criterion: &mut Criterion) { + let input = coincident_lifted::(); + let mut group = criterion.benchmark_group(format!("polynomial/coincident_lifted/order{N}")); + group.bench_function("determinant", |bencher| { + bencher.iter(|| { + let determinant = polynomial_determinant(black_box(&input.matrix), black_box(BUDGET)) + .or_abort("validated fixture fits the consumer budget"); + black_box(determinant.leading_term()); + black_box(determinant) + }); + }); + group.finish(); +} + +fn main() { + let mut criterion = Criterion::default().configure_from_args(); + register::<4>(&mut criterion); + register::<5>(&mut criterion); + register::<6>(&mut criterion); + register::<7>(&mut criterion); + register::<8>(&mut criterion); + criterion.final_summary(); +} diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index 119d2e5..7f82f3c 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -60,16 +60,17 @@ evidence being validated. ## Benchmark Suites -`la-stack` has eleven Criterion benchmark targets. Routine release acquisition +`la-stack` has twelve Criterion benchmark targets. Routine release acquisition selects public workloads from these targets; unfiltered targets remain available for diagnostics. ### Release acquisition policy -The #301 policy `public-release-and-lu-peers-v1` applies to new measurements. -The full la-stack selection has 646 default-feature and 608 exact-feature cases. +The `public-release-and-lu-peers-v2` policy extends #301 with column bases, +interval division/solves, rational dot products, and polynomial determinants. +The full la-stack selection has 736 default-feature and 667 exact-feature cases. Local `performance-release`, `performance-local`, and full `performance-measure` -measure these same 1,254 cases at both revisions under one captured harness. +measure these same 1,403 cases at both revisions under one captured harness. Only the baseline phase also measures the 16 native nalgebra/faer LU factorization-plus-one-solve cases at D=2, 3, 4, 5, 8, 16, 32, and 64. The matching eight current la-stack timings already belong to the full suite. @@ -87,14 +88,18 @@ The installed engine rejects duplicate semantic IDs and incomplete raw samples. | --- | --- | | Angles: vector, checked slice, construction followed by angle | `angle`, default, vetted dense/sparse/endpoint fixtures at D=3–6; excludes the control kernel | | Certified dot products, affine differences, certificate endpoints | `linear_form`, default, prepared and projection-batch regimes at D=2–6 | +| Column bases | `column_basis`, default, 55 square/rectangular, rank-deficient, rescaled and affine cases | | Construction, storage access, mutation, symmetry and tolerances | `public_api`, default, D=2–5; accessor rows combine trivial queries; mutations reset a local copy | | Exact determinant/sign/solve and strict/rounded binary64 APIs | `exact`, exact, fixed/corpus/adversarial inputs at D=2–5 | | Exact scalar, array and canonical-vector conversions | `public_api` scalar and `exact` conversions, exact, D=2–5 where applicable, with strict rejection and range boundaries | | Gram construction | `gram`, default, square and embedded inputs through coordinate dimension 8; excludes hand-written controls | | Interval arithmetic, assembly, determinant and sign | `interval` and `public_api`, default; scalar edge regimes, storage at D=2–5, sign filters at D=3–7 | +| Interval division, reciprocal and solve enclosures | `interval_solve`, default, 5 scalar cases and 30 batches at D=1–6, including typed fallback outcomes | | LU/LDLT, factorization-inclusive and prepared solves/determinants, norms and dot products | `vs_linalg` la-stack rows, default, D=2–64, with norm scenarios and D=8 stress inputs | | Direct determinant, error bound and paired result access | `public_api`, default, D=2–5; `exact` retains its existing exact-feature baseline rows | | Rational construction, storage, mutation, determinant/sign/solve and dispatch | `public_api`, `exact`, `rational_solve`, exact; small/wide inputs and structured dyadic/non-dyadic systems through D=8 | +| Rational dot products | `rational_dot`, exact, 54 prepared/slice cases at lengths 2–8, 14 and 22 | +| Sparse polynomial determinants | `polynomial`, exact, 5 coincident lifted fixtures at orders 4–8, including borrowed leading-term extraction | Default implementations share their constructor's workload. Error factories, formatting, enum queries, indexing/dispatch macros and re-exported third-party @@ -111,7 +116,7 @@ Neither routine release path acquires them. The README retains its existing eight-dimension LU graph; no additional performance page or graph is produced. The local driver prints the case count and nominal timing budget before samples -are acquired. The current plan has 2,524 measurements and a nominal 336.5-minute +are acquired. The current plan has 2,822 measurements and a nominal 376.3-minute warmup-plus-measurement budget, before compilation, gates, and analysis. `performance-release` records measurement-and-promotion elapsed seconds and exit status in ignored `target/bench-reports/full/runtime.txt`, including failures. @@ -121,7 +126,13 @@ the terminal log with the exact command, revisions, toolchain, dependencies and host when comparing actual before/after runtime. The former 1,422-case run had a nominal 189.6-minute budget; that calculation supplies no observed elapsed time. Expanded public coverage means removing peers alone establishes no overall -speedup. No new end-to-end acquisition has been timed for #301. +speedup. No end-to-end acquisition has been timed for the expanded policy. + +Full comparisons require both revisions to implement every selected API. A +baseline predating these additions fails discovery; it cannot supply polynomial, +rational-dot, column-basis or interval-solve ratios. Hosted tagged acquisition +still records absolute costs. Existing retained reports remain historical and +do not satisfy the expanded inventory; changing this plan does not rewrite them. **`public_api`** (`benches/public_api.rs`) owns construction, access, mutation, symmetry, direct determinant certificates, interval determinant queries, and @@ -175,6 +186,20 @@ exponent differences within each row. Both measure exact values and signs; all four determinant APIs are checked against rational Gaussian elimination outside timing. These focused diagnostics are separate from release metrics. +**`polynomial`** (`benches/polynomial.rs`) measures complete exact determinant +polynomials and borrowed leading-term extraction for coincident lifted points +at matrix orders 4–8 (geometric dimensions 2–6). Independent permutation +enumeration checks every coefficient before timing. Inputs and their construction +are outside timing; output allocation and destruction are included. The +order-eight fixture has 241,920 output terms, so expect substantially more work +than scalar determinant benchmarks. All five rows belong to the exact release +selection. For a shorter standalone diagnostic run: + +```bash +cargo bench --locked --features bench,exact --bench polynomial -- \ + --sample-size 10 --warm-up-time 1 --measurement-time 3 --noplot +``` + **`rational_solve`** (`benches/rational_solve.rs`) measures diagonal, upper/lower triangular, row-permuted triangular, general sparse, and dense rational systems through D=8, with both dyadic and non-dyadic coefficients. @@ -226,9 +251,9 @@ cargo bench --locked --features bench --bench interval_solve -- \ --sample-size 30 --warm-up-time 1 --measurement-time 2 --noplot ``` -The new rows remain diagnostics outside the retained release catalog, like the -new rectangular and rational-reduction targets. They establish current cost and -success rates; there is no previous la-stack solve API for a speedup comparison. +All 35 rows belong to the default-feature release selection. They establish +current cost and corpus certification counts; revisions predating this API +cannot provide a speedup comparison. **`linear_form`** (`benches/linear_form.rs`) measures the default-feature certified dot-product and affine-difference filters through D=2–6. Prepared @@ -241,9 +266,9 @@ construction-inclusive measurements. The original D=4 plain-dot control remains. Fixture expectations are validated outside the timed closures. Release acquisition includes these public certified reduction operations. -The `column_basis` and `rational_dot` targets cover the APIs added for #305/#306 -as separate diagnostic workloads. They do not modify the retained release -catalog or imply before/after speedups for previously unavailable APIs. +The `column_basis` and `rational_dot` targets cover the APIs added for #305/#306. +Their 55 default-feature and 54 exact-feature rows belong to release acquisition; +they do not imply before/after speedups for previously unavailable APIs. Column selection covers square, tall, wide, deficient, nearly dependent, severely rescaled, and shared-face affine inputs through `8 × 14` and `6 × 7`. Exact dot products cover lengths 2–8, 14, and 22, with non-dyadic, cancelling, @@ -280,7 +305,10 @@ a different measurement environment. The shared engine discovers complete native inventories on both sources, checks independent required scientific IDs, and runs both correctness gates before any -timing. The gates cover `vs_linalg_inputs` and `exact_bench_config`. +timing. The full gate covers `vs_linalg_inputs`, `exact_bench_config`, +`column_basis`, `rational_dot`, `interval_solve` (including its rational oracle), +and `polynomial_bench_inputs`. The non-exact diagnostic retains its original +comparison and exact-configuration gate. Baseline measurements include nalgebra and faer; current measurements reuse those baseline-phase peers and measure la-stack and the exact suite. Diagnostic cases remain in the complete samples even when omitted from selected report rows. @@ -570,7 +598,9 @@ The integration smoke test `benches/comparison/tests/vs_linalg_inputs.rs` reuses input helpers and verifies that la-stack, nalgebra, and faer agree on the determinant, solve, dot, Euclidean-norm, squared-norm, and infinity-norm results for every measured dimension: D=2, 3, 4, 5, 8, 16, 32, and 64. The same focused recipe -also tests exact-benchmark range and deterministic-generator configuration: +also tests exact-benchmark range and deterministic-generator configuration, +column bases, rational dot products, interval solve corpora against a rational +oracle, and polynomial coefficients against independent permutation enumeration: ```bash just test-bench-inputs @@ -772,7 +802,7 @@ or `det_exact_f64_result (vs det_exact_f64)`. Those used the historical lossy `*_exact_f64` baseline. New comparisons require matching benchmark identifiers; the pre-v0.4.3 name substitutions have been removed. -The default `public-release-and-lu-peers-v1` scope includes canonical exact-arithmetic +The default `public-release-and-lu-peers-v2` scope includes canonical exact-arithmetic groups because their inputs and execution order are fixed across revisions. Historical baselines created before the `exact_random_corpus_d*` names were introduced do not have comparable full-corpus rows, so those rows appear once both sides of a @@ -811,28 +841,37 @@ benchmarking. Publication makes the attached evidence immutable. ### Hosted Release Runtime Budget -The producer runs default and exact public workloads sequentially on one -`ubuntu-latest` runner. The steps allow 140 and 130 minutes, within a 350-minute -job. Checkout allows 2 minutes; preparation allows 60 minutes for uncached tools, -two 10-minute discovery deadlines and independent input validation. Preparation -reserves 35 minutes for tools and 5 for the input gate. The remaining 18 minutes -cover dataset validation, packaging, upload, diagnostics and runner overhead. -Discovery compiles all selected targets before timing. Dependency caches remain -disabled; these deadlines remain capacity estimates until the new hosted policy -has completed on a runner. +The producer runs default and exact public workloads in parallel on separate +`ubuntu-latest` runners. Each job allows 330 minutes: 2 for checkout, 60 for +preparation, 2 for host capture, 240 for measurement, and 26 for packaging, upload and +overhead. Each preparation runs full discovery and independent input validation, +reserving 35 minutes for uncached tools, two 10-minute discovery deadlines and +5 minutes for the input gate. Both runners compile the selected targets before +timing; neither restores dependency caches. Each measurement step has at least +50% headroom beyond its planning estimate. These remain capacity estimates until +the expanded hosted policy completes. | Selection | Cases | Nominal warmup + measurement | Planning estimate at 12 s/case | Step limit | | --- | --- | --- | --- | --- | -| `public-default` | 646 | 86.1 min | 129.2 min | 140 min | -| `public-exact` | 608 | 81.1 min | 121.6 min | 130 min | -| Hosted total | 1,254 | 167.2 min | 250.8 min | 350 min including setup and tail | -| Local total: both revisions and 16 peers | 2,524 | 336.5 min | 504.8 min | Separate per-phase deadlines | +| `public-default` | 736 | 98.1 min | 147.2 min | 240 min; 330 min job | +| `public-exact` | 667 | 88.9 min | 133.4 min | 240 min; 330 min job | +| Hosted aggregate work | 1,403 | 187.1 min | 280.6 min | Concurrent jobs, then 30 min for assembly | +| Local total: both revisions and 16 peers | 2,822 | 376.3 min | 564.4 min | Separate per-phase deadlines | These estimates are not measured runtimes or upper bounds. Criterion can extend measurement for expensive iterations. Sampling remains unchanged: 100 samples, 100,000 resamples, 95% confidence, 3-second warmup and 5-second measurement target. Native filters select public workloads without reducing their sampling. +Hosted wall time follows the slower suite plus preparation and archive assembly, +subject to runner availability. Parallel execution increases setup work because +each runner installs and validates its own environment. The archive records each +suite's runner, CPU, toolchain, lockfile hash, command, source commit, workflow +attempt and measurement timestamps under `release-suites/`. These are +absolute timings from two hosts; do not treat cross-suite values as a common-host +comparison. Local baseline/current and peer comparisons still use one captured +environment. + The workflow logs discovered counts and budgets before timing. Its final summary records each feature selection's outcome, elapsed seconds and budget, including failed steps. A runner-level termination can prevent the summary. For comparable @@ -848,12 +887,35 @@ la-stack catalog; hosted completeness deliberately does not require peer rows. estimates with 95% intervals and 100 finite positive samples. Each named baseline's four raw JSON files must match its fresh `new` measurement, including the native sanitized storage paths. Missing public cases, failed writes, stale -baselines and malformed measurements stop publication. Only validation permits packaging -the single `criterion/` archive, including the inventory manifest, and uploading -the temporary Actions artifact. The separate publisher attaches that archive +baselines and malformed measurements stop publication. For one suite, use +`just bench-release-check-suite ` with the runner's output directory. +The shared v0.2.4 `pack-suite` command already performs that validation before +packaging, so the workflow needs no additional verification pass. It retains +original host metadata captured before timing and digests of every raw file. +Assembly uses `assemble-suites` with the checked-in policy and separately +transported native inventory. It requires both suites from the same commit and +repository/run/attempt, matching complete contracts, disjoint portable paths, +and unchanged raw bytes. Only successful assembly emits the final archive and +Actions artifact output. The separate publisher attaches that archive as `la-stack-$TAG-criterion-baseline.tar.gz` to the draft, verifies its uploaded state, size, and SHA-256 digest, and leaves the draft ready for review. +Starting with v0.5.2, the shared archive contains raw Criterion paths directly +at its root, `criterion-inventory.json`, and original manifests at +`release-suites/0.json` (default) and `release-suites/1.json` (exact). It has no +outer `criterion/` directory. Historical assets retain that wrapper and their +original bytes. Extract a new archive into an absent directory, then verify it: + +```bash +uv run --locked research-repo-tools performance extract "$ASSET" target/release-criterion +just bench-release-check "$TAG" target/release-criterion +``` + +The verification command also needs the independently discovered +`target/release-benchmark-inventory.json`; the archived inventory alone does not +replace the checked-in required scientific coverage. Original per-suite host +records remain authoritative when reading the archive on another machine. + ### Validate The Release Workflow Run the applicable local gates for workflow changes: diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 60791c4..c504227 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -34,7 +34,7 @@ including when a rerun happens after UTC midnight. ## Reusable CI evidence -The publishing process uses the installed `research-repo-tools==0.2.3` +The publishing process uses the installed `research-repo-tools==0.2.4` validation helpers. `tooling/release_evidence.py` supplies the Cargo-specific policy; it does not use the shared package's wheel/sdist release commands. Every successful protected `main` push produces signed evidence after all three @@ -447,13 +447,24 @@ release ID and tag commit before attaching the archive. Do not edit the draft, move the tag, or publish manually while the workflow is running. -The privileged jobs install the exact published `research-repo-tools==0.2.3` +The privileged jobs install the exact published `research-repo-tools==0.2.4` package on Python 3.15 without repository checkout. Preflight emits versioned target JSON with the captured ID, commit and title assertion. Its trusted job output passes directly to the publisher, separately from the producer's inert archive. The shared writer verifies downloaded asset bytes as well as provider metadata. It leaves publication to the authenticated maintainer. +The measurement jobs capture their own host metadata before timing and use the +shared `pack-suite` command to verify and archive their full samples. Assembly +uses `assemble-suites` with the checked-in policy, separately transported native +inventory, tagged commit and repository/run/attempt identity. It emits the final +asset only after validating both suites, raw-file digests and original provenance. +These suite commands use the published `research-repo-tools==0.2.4` through +`uvx --python 3.15`, matching the standard development dependency pin. +New v0.5.2 archives use the shared layout described in +[Benchmarking](BENCHMARKING.md#hosted-release-runtime-budget); historical assets +keep their original layout and bytes. + Runs for the same tag are serialized without cancelling an active run. ### 7. Publish the reviewed draft and approve Cargo publication @@ -524,20 +535,22 @@ commit, benchmark run, publishing run, registry URL/checksum and consumer result in the current release's tracking issue or merged release PR. Keep any release tracking issue open until publication and consumer verification succeed. -The benchmark producer has read-only repository permissions and restores no -dependency caches, including tool binaries. It invokes `setup-tools` with +The benchmark producers have read-only repository permissions and restore no +dependency caches, including tool binaries. Each invokes `setup-tools` with `cache: false`, installs declared managed Rust and Cargo tools through the locked shared package, and exports verified paths. The short preflight job receives `contents: write` for draft visibility; the separate asset writer receives it to attach the archive. Neither privileged benchmark job checks out or executes repository code. -The producer budgets 140 minutes for default-feature public workloads and -130 minutes for exact-feature public workloads within a 350-minute job, -including a 60-minute preparation budget for uncached -tool installation, compilation, inventory and input validation. -Both suites retain full release sampling. Inventory -and raw-data validation must succeed before the single complete archive is -packaged and uploaded; inspect the suite timing summary when a run fails. +Default-feature and exact-feature workloads run on separate runners in parallel. +Each job allows 330 minutes, including 60 minutes for uncached tool installation, +compilation, inventory and input validation, and 240 minutes for measurement. +The 1,403 cases include column bases, interval division/solves, rational dot +products, and polynomial determinants. Both suites retain full release sampling. Inventory +and raw-data validation must succeed for both suites. A separate 30-minute job +checks their commit/attempt identities and disjoint files, validates the complete +dataset, and packages one archive with provenance for each runner. Inspect the +suite timing summaries when a run fails. See the [hosted runtime budget](BENCHMARKING.md#hosted-release-runtime-budget) for measured history, capacity estimates, and headroom, and [workflow validation](BENCHMARKING.md#validate-the-release-workflow) for local @@ -555,8 +568,8 @@ or substitute manual-dispatch results. Local builds are useful diagnostics but cannot renew hosted attestations. The following recovery rules apply separately to `Release Benchmarks`. -If preflight or the producer failed, rerun all jobs with `gh run rerun ` -or dispatch again. The producer requires a preflight from the current attempt; +If preflight, either producer, or archive assembly failed, rerun all jobs with +`gh run rerun ` or dispatch again. Each producer requires a preflight from the current attempt; rerunning failed benchmark jobs alone stops before tool setup or measurement because the earlier draft check is stale. diff --git a/docs/code_organization.md b/docs/code_organization.md index 0d23c89..5306b59 100644 --- a/docs/code_organization.md +++ b/docs/code_organization.md @@ -31,6 +31,7 @@ mirrors, and the public prelude. There is no `src/main.rs`. | [`src/lu.rs`](../src/lu.rs) | Partially pivoted `Lu` factorization, solves, and determinants | | [`src/matrix.rs`](../src/matrix.rs) | `Matrix`, finite storage, accessors, matrix operations, norms, and direct determinant APIs | | [`src/norm.rs`](../src/norm.rs) | Exact range-boundary fallback for the approximate Euclidean norm | +| [`src/polynomial.rs`](../src/polynomial.rs) | Canonical `SparsePolynomial` coefficients, explicit determinant budgets, and the exact column-subset kernel through order eight | | [`src/rational.rs`](../src/rational.rs) | `RationalMatrix` and `RationalVector`, exact dot products and the checked `rational_dot` slice adapter, canonical rational inputs, and denominator clearing | | [`src/rounding.rs`](../src/rounding.rs) | Shared binary64 rounding primitives for certified arithmetic | | [`src/scaled_product.rs`](../src/scaled_product.rs) | Allocation-free scaled products for floating-point factor diagonals | @@ -46,7 +47,7 @@ belong in [Mathematical basis](mathematical_basis.md) and The [Cargo manifest](../Cargo.toml) owns feature and dependency declarations. -- **`exact`** gates `src/exact.rs`, `src/rational.rs`, their additional tests, +- **`exact`** gates `src/exact.rs`, `src/polynomial.rs`, `src/rational.rs`, their additional tests, exact guides, and exact-arithmetic examples. It enables `det_exact()`, strict `det_exact_f64()`, rounded `det_exact_rounded_f64()`, `det_sign_exact()`, `solve_exact()`, strict `solve_exact_f64()`, and @@ -95,6 +96,11 @@ The [Cargo manifest](../Cargo.toml) owns feature and dependency declarations. through D=8, including arbitrary row permutations and wide rational inputs. - `tests/rational_dot.rs` checks exact dot identities, shape and denominator rejection, and basis/objective vector sizes through 22 and beyond. +- `tests/polynomial_determinant.rs` checks canonical terms, independent dense + permutation expansions, cancellation, row swaps, budgets and exponent limits. + `tests/polynomial_bench_inputs.rs` verifies lifted coincident-point fixtures + and repeated rows through order eight against a sparse permutation oracle. + `tests/proptest_polynomial.rs` checks exact determinant row scaling. - `tests/column_basis.rs` checks rectangular numerical rank, scaling, pivot ordering, typed failures, and shared-face affine constraints through 6D; exact-feature checks independently certify the selected square minors. @@ -112,6 +118,8 @@ The [Cargo manifest](../Cargo.toml) owns feature and dependency declarations. executable numerical assertions stay in `examples/`. - `examples/vector_angles.rs` demonstrates fixed-vector and borrowed-slice angles, independent magnitude scaling, and subnormal separations. +- `examples/polynomial_determinant.rs` demonstrates exact cancellation, borrowed + leading coefficients and typed work-budget exhaustion. Use [Testing guidance](dev/testing.md) for dimension coverage and test design, and [Documentation guidance](dev/docs.md) for executable-example ownership. @@ -139,11 +147,15 @@ construction, prepared solves, and full runtime-dispatch adapter costs; `tests/exact_bench_config.rs` includes its independent fixture gates. `benches/common/interval_solve.rs` owns the deterministic D=1–6 solve corpora. `benches/interval_solve.rs` measures 32-system batches and reports certification counts -outside timing. New division and solve rows are diagnostics pending release -catalog adoption; existing interval release workloads retain their selection. +outside timing. Division and solve rows belong to default-feature release +acquisition, with `tests/interval_solve.rs` providing the independent rational gate. `benches/column_basis.rs` and `benches/rational_dot.rs` own the new rectangular selection and exact reduction benchmarks, with analytical fixture checks before -timing. These diagnostic targets are separate from the retained release catalog. +timing. Both targets belong to release acquisition in their respective feature selections. +`benches/polynomial.rs` measures exact sparse determinants for coincident lifted +points through order eight. `benches/common/polynomial.rs` owns the independently +validated fixtures. These rows belong to exact-feature release acquisition, +with `tests/polynomial_bench_inputs.rs` checking every coefficient independently. The root package is the default workspace member. `benches/comparison/Cargo.toml` owns the `vs_linalg` target at `benches/vs_linalg.rs` and its input test. Select @@ -170,7 +182,11 @@ The [justfile](../justfile) owns executable development workflows. Shared complete-inventory Python and file runners own discovery and bounded execution; repository lint policy and archive exclusions stay in the recipes and native tool configuration. The [capability inventory](../tooling/README.md#shared-capability-adoption) -records the v0.2.3 adoption boundary. +records the v0.2.4 development and release-command dependency. +`tooling/shared_tooling.py` temporarily selects the scanner from the standard +Python 3.15 environment before delegating to the installed fixture checker. +[#317](https://github.com/acgetchell/la-stack/issues/317) owns its removal after +the shared Python baseline also governs Semgrep in v0.2.5. `osv-scanner.toml` owns temporary advisory-specific dependency exceptions, and `.gitleaks.toml` owns narrow secret-scan false-positive exceptions. Their rationale @@ -222,9 +238,12 @@ owns settings and deployment verification. `tooling/criterion-inventory.toml` declares hosted la-stack-only list commands and independent public API coverage. `tooling/criterion-saved.toml` declares complete saved-sample requirements. Shared discovery and saved-sample APIs own -inventory parsing and exact raw-file verification. The release workflow packages only datasets that pass -that gate; its regression and archive tests live in +inventory parsing and exact raw-file verification. Published v0.2.4 also owns +suite-scoped verification, provenance, packaging and complete archive assembly. +The release workflow retains the feature matrix, native commands, job budgets +and artifact handoff; its regression and archive tests live in `tests/tooling/test_release_baseline.py`. +There is no repository-specific suite verifier or archive merge implementation. `tooling/performance.just` declares native Cargo discovery, correctness gates and measurements. `tooling/measurements/{full,non-exact}.toml` declares the diff --git a/docs/dev/performance-tooling-adoption.md b/docs/dev/performance-tooling-adoption.md index 3c98eab..d254f8f 100644 --- a/docs/dev/performance-tooling-adoption.md +++ b/docs/dev/performance-tooling-adoption.md @@ -8,8 +8,14 @@ belong to research-repo-tools. ## Publication boundary -The dependency-only environment pins the published `research-repo-tools==0.2.3` -package, including the release workflow's isolated installations. +The dependency-only environment pins the published `research-repo-tools==0.2.4` +package. Release target capture and publication use the same exact version. +The suite verification, packaging and assembly commands adopted for +[#316](https://github.com/acgetchell/la-stack/issues/316), targeting v0.5.2, +use that release as well. Its verified tool updater is adopted without local +updater logic. A temporary scanner-selection override preserves the standard +Python 3.15 Semgrep environment until the shared fix ships in v0.2.5; +[#317](https://github.com/acgetchell/la-stack/issues/317) tracks its removal. Upstream [#123](https://github.com/acgetchell/research-repo-tools/pull/123) adds trusted main-validation artifact reuse for release promotion; v0.2.3 was published from commit `4085125526e21b2855eb7c61a52ee312782ae31a`. @@ -18,18 +24,20 @@ preserving their selectors, exact pins, overrides, and separate environments. The minimum-release acquisition contract introduced by v0.2.2 remains in place: v0.2.1 could not express the consumer's v0.5.0 measurement floor. -[PyPI v0.2.3](https://pypi.org/project/research-repo-tools/0.2.3/) lists both -distributions as unyanked. Their SHA-256 hashes match the signed -[GitHub release assets](https://github.com/acgetchell/research-repo-tools/releases/tag/v0.2.3) +[PyPI v0.2.4](https://pypi.org/project/research-repo-tools/0.2.4/) supplies both +distributions. Their SHA-256 hashes match the published +[GitHub release assets](https://github.com/acgetchell/research-repo-tools/releases/tag/v0.2.4) and the exact registry lock entries: | Distribution | SHA-256 | | --- | --- | -| Wheel | `da5f0259027662e7706b7f2b8a38d8e1f1d8a5dce314e2c996177b2947514fc6` | -| Source distribution | `13700b174656ffad265ecb926dd66013949853b7a39c088ab6d3852aba72df95` | +| Wheel | `00aaab2b7efd680d79324c6debb64db4d8d1ff076a460ff46e24e0ef1c24bd7d` | +| Source distribution | `30b6d4d2f8d0cb98d5311656928dd65800088b651dd6c0c29f9a02526e9e902a` | Runtime imports and acceptance checks use this installed package. No sibling -checkout, private API, or release-candidate installation supplies final evidence. +checkout or release-candidate installation supplies final evidence. The temporary +Semgrep adapter overrides only the installed scanner-selection function; all +benchmark execution, validation and publication use the public shared APIs. The maintainer approved freezing earlier evidence instead of extending the shared policy gate to accept missing acquisition declarations. This deliberately @@ -55,6 +63,8 @@ or shell implementations of the shared workflow. | `performance-readme` | `performance publish` with `performance-readme.toml`, independent identities and transactional formatting | | `plot-vs-linalg` | `performance publish --artifacts-only` with named full scratch, retained, or non-exact scratch views | | `bench-release-check` | `performance verify-saved` with saved name, Criterion directory and `criterion-saved.toml` | +| `bench-release-check-suite` | `performance verify-saved --suite` with the complete independent policy and selected feature suite | +| Hosted suite packaging and assembly | `performance host`, `pack-suite` and `assemble-suites`; la-stack retains the native commands, matrix, artifact selection and budgets | | `bench-compare` | Existing installed saved-Criterion comparison CLI | Paths in the table are relative to `tooling/`. @@ -147,15 +157,17 @@ The dependency-only environment remains Python 3.15. Python supports shared tools and consumer tests; deleting production scripts does not remove it. Semgrep initially used a separately locked Python 3.14 environment. Following [MCMC #214](https://github.com/acgetchell/markov-chain-monte-carlo/pull/214), -Semgrep 1.182.1 now shares the root Python 3.15 environment and lockfile. +Semgrep 1.182.1 scans and fixture checks share the root Python 3.15 environment and lockfile. The root manifest retains its native-wheel requirement, compatibility overrides and Python 3.15 Windows wheels. Local and hosted scans retain the Linux stack-limit workaround. Consumer tests now live under `tests/tooling/`. -Native discovery listed 533 comparison and 354 exact cases while verifying +Initial migration discovery listed 533 comparison and 354 exact cases while verifying the independent 417 required IDs. The scientific gates passed 26 exact-input tests and 42 comparison-input tests. These checks compiled/listed the native benchmarks without collecting timing measurements. +The expanded v0.5.2 selection and its current correctness gates are documented +in [Benchmarking](../BENCHMARKING.md#release-acquisition-policy). Final acceptance also requires focused installed-package tests, `just check`, `just ci`, Markdown/spelling/docs checks, and Linux/macOS/Windows CI for the diff --git a/docs/mathematical_basis.md b/docs/mathematical_basis.md index 90aa0dc..b7a4e79 100644 --- a/docs/mathematical_basis.md +++ b/docs/mathematical_basis.md @@ -13,6 +13,7 @@ - [Derivation of the returned determinant bound](#derivation-of-the-returned-determinant-bound) - [Exact arithmetic over binary64 inputs](#exact-arithmetic-over-binary64-inputs) - [Exact arithmetic over rational inputs](#exact-arithmetic-over-rational-inputs) + - [Exact sparse polynomial determinants](#exact-sparse-polynomial-determinants) - [Exact-to-binary64 conversion](#exact-to-binary64-conversion) - [Gram matrices and geometric measures](#gram-matrices-and-geometric-measures) - [Interval linear solves](#interval-linear-solves) @@ -59,6 +60,7 @@ Reference numbers point to [REFERENCES.md](../REFERENCES.md). | Exact determinant value or solve | `det_exact`, `solve_exact` | Exact for represented inputs | | Exact operations over preassembled rationals | `RationalMatrix::det_sign`, `det`, `solve` | No intermediate binary64 reconstruction | | Exact rational dot products | `RationalVector::dot`, `rational_dot` | Exact canonical result; checked slices have no dimension cap | +| Exact polynomial determinant and first nonzero coefficient | `polynomial_determinant`, `SparsePolynomial::leading_term` | Orders 0–8; increasing `u128` exponents; explicit work limits | | Rectangular column selection | `column_basis` | Explicit absolute tolerance; provisional numerical rank, requiring independent certification | | Binary64 output from an exact result | Strict or rounded conversions | Strict conversion forbids rounding | | Unsigned vector angle in radians | `VectorAngle::angle`, `Vector::angle` | Finite nonzero vectors; rounded, without a certified bound | @@ -77,13 +79,19 @@ without changing the const-generic storage model. Spherical callers retain radius validation and angle-to-arc-length conversion. The deliberate anti-goals are general dynamically sized matrix storage, -sparse storage, broad decomposition coverage, alternate floating scalar +sparse matrix storage, broad decomposition coverage, alternate floating scalar families, and GPU-, parallel-, BLAS-, or LAPACK-scale throughput. Those problems need different storage models and numerical policies rather than extensions to this small fixed-dimension design. The rectangular column selector operates on borrowed const-generic arrays; it does not introduce a general rectangular matrix framework. +The polynomial determinant accepts a borrowed const-generic square array of +canonical sparse polynomials. Its exponent encoding, monomial order and work +budgets are explicit. Simulation of Simplicity variable assignment, base-three +encoding proofs, geometric tie-breaking and consumer budget selection remain +outside this library. + ## Represented values and arithmetic model Public matrix and vector construction rejects NaN and infinity. Because the @@ -498,6 +506,74 @@ Delaunay's basis lengths through 8 and full objective lengths 14 and 22 in 6D are ordinary inputs, as are larger lengths. Both APIs perform a linear number of rational operations; bit complexity depends on numerator/denominator growth. +### Exact sparse polynomial determinants + +`SparsePolynomial` represents `Σ c_e t^e`, with `e ∈ [0, u128::MAX]` and +canonical rational `c_e`. Construction rejects zero denominators before any +arithmetic, reduces quotients, combines duplicate exponents, and removes exactly +zero coefficients. Numeric increasing exponent order is fixed. The leading term +is the least nonzero exponent, as appropriate to `t → 0+`; it is not the +conventional highest-degree term. Arbitrary multivariate orderings are not +supported directly. A caller encoding them into `u128` must ensure injectivity, +order preservation and compatibility with addition throughout the calculation. + +For a column subset `S` with `k = |S|`, maintain the determinant `p[S]` of the +first `k` rows restricted to the sorted columns in `S`. Initialize `p[∅] = 1`. +For every `j ∉ S`, add + +```text +p[S ∪ {j}] += (-1)^(number of columns in S greater than j) * p[S] * A[k,j]. +``` + +Appending column `j` adds exactly that many inversions to each existing +permutation. Each permutation has exactly one predecessor, so the recurrence +sums the Leibniz terms once with their correct signs \[12\]. All predecessors +have smaller numeric masks, allowing a completed partial to be consumed during +increasing-mask traversal. No division or pivot is needed, even when many +coefficients cancel or entire partial determinants vanish. + +Every product and sum uses exact rational arithmetic. No term is discarded +based on its exponent or sign. Only after all contributions are complete can +the first nonzero term be read. A successful empty term map proves the zero +polynomial; `LaError::PolynomialBudgetExceeded` and +`LaError::PolynomialExponentOverflow` provide no sign or zero classification. +Overflowing intermediate exponents are rejected even if subsequent cancellation +could have removed them. There is no rounding error or numerical conditioning +restriction; combinatorial growth and integer bit complexity limit resources. + +The caller supplies `PolynomialDeterminantBudget`. For positive order `D`, the +complete structural envelope `D × 2^(D-1)` is checked against +`max_subset_transitions` before allocation, even for zero matrices. Order zero +needs no transitions and returns one. The hard order-eight cap limits the +state table to 256 maps and the structural envelope to 1,024 transitions. +`max_term_products` counts each nonzero coefficient pair before exponent +addition and multiplication. Zero entries/partials cost no products; exact +cancellation never refunds completed work. Traversal order is documented in +the public API, and each call starts a fresh budget. + +If `P` products are permitted, at most `P + 1` term insertions can occur, +including the initial one. Consuming states releases their storage, and inputs +are borrowed without deep cloning. Neither budget bounds integer bit lengths, +allocation bytes, elapsed time or the construction of caller-owned polynomials. +Allocator failure retains Rust's normal behavior. A term product may be costly +when coefficients have large numerators or denominators. + +For a consumer whose column term maxima are `m_j`, the no-cancellation bound for +a subset is `|S|! × Π_{j∈S} m_j`. Therefore a conservative product envelope is + +```text +Σ over S ⊊ {0,..,D-1}: |S|! × (Π_{j∈S} m_j) × (Σ_{j∉S} m_j). +``` + +The Delaunay order-eight lifted shape `[2, 2, 2, 2, 2, 2, 13, 1]` gives +55,631,284 products. This is a consumer policy example, not a library default +or a practical memory guarantee. A fully coincident origin fixture with +base-three coordinate perturbations has 241,920 distinct final monomials at +order eight; even this sparse input can create a large result. The release +benchmark and independent permutation tests exercise that shape through order +eight. Dense small-degree tests separately verify coefficient cancellation, +row swaps, repeated rows and non-dyadic coefficients. + ### Exact-to-binary64 conversion For strict conversion, a canonical nonzero rational must have a power-of-two diff --git a/examples/polynomial_determinant.rs b/examples/polynomial_determinant.rs new file mode 100644 index 0000000..21a1632 --- /dev/null +++ b/examples/polynomial_determinant.rs @@ -0,0 +1,43 @@ +#![forbid(unsafe_code)] + +//! Preserve cancellation and handle bounded exact-polynomial evaluation. + +use la_stack::prelude::*; + +fn main() -> Result<(), LaError> { + let one = SparsePolynomial::try_from_terms([(0, BigRational::from_integer(1.into()))])?; + let perturbed = SparsePolynomial::try_from_terms([ + (0, BigRational::from_integer(1.into())), + (3, BigRational::new(1.into(), 2.into())), + ])?; + let matrix = [[one.clone(), one.clone()], [one, perturbed]]; + let budget = PolynomialDeterminantBudget { + max_subset_transitions: 4, + max_term_products: 5, + }; + let determinant = polynomial_determinant(&matrix, budget)?; + assert_eq!( + determinant.leading_term(), + Some((3, &BigRational::new(1.into(), 2.into()))) + ); + println!("Exact determinant terms: {:?}", determinant.terms()); + + let exhausted = polynomial_determinant( + &matrix, + PolynomialDeterminantBudget { + max_term_products: 4, + ..budget + }, + ); + match exhausted { + Err(LaError::PolynomialBudgetExceeded { + work: PolynomialWork::TermProducts, + limit: 4, + completed: 4, + requested: 1, + .. + }) => {} + other => panic!("expected the explicit budget failure, got {other:?}"), + } + Ok(()) +} diff --git a/justfile b/justfile index 7f2d1bc..23b2c44 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,6 @@ # shellcheck disable=SC2148 # Justfile for la-stack development workflow -# Install tools: uv run --locked --managed-python --only-group tooling research-repo-tools setup +# Install tools: uv run --locked --managed-python --only-group tooling python tooling/shared_tooling.py setup # Usage: just or just --list # Use bash with strict error handling for all recipes @@ -86,6 +86,7 @@ bench-compile: CARGO_BUILD_WARNINGS=deny {{ _run }} cargo bench --locked --no-run --features bench,exact --bench exact CARGO_BUILD_WARNINGS=deny {{ _run }} cargo bench --locked --no-run --features bench,exact --bench rational_solve CARGO_BUILD_WARNINGS=deny {{ _run }} cargo bench --locked --no-run --features bench,exact --bench rational_dot + CARGO_BUILD_WARNINGS=deny {{ _run }} cargo bench --locked --no-run --features bench,exact --bench polynomial CARGO_BUILD_WARNINGS=deny {{ _run }} cargo bench --locked --no-run --features bench,exact --bench public_api # Run the exact-arithmetic benchmark suite. @@ -111,6 +112,10 @@ bench-linear-form: bench-release-check tag criterion_dir="target/criterion": _ensure-uv {{ _run }} uv run --locked research-repo-tools performance verify-saved tooling/criterion-saved.toml --saved-sample {{ quote(tag) }} --criterion-dir {{ quote(criterion_dir) }} +# Verify one parallel release runner's exact feature selection before transport. +bench-release-check-suite tag suite criterion_dir="target/criterion": _ensure-uv + {{ _run }} uv run --locked research-repo-tools performance verify-saved tooling/criterion-saved.toml --saved-sample {{ quote(tag) }} --suite {{ quote(suite) }} --criterion-dir {{ quote(criterion_dir) }} + # Discover all release benchmarks and report their expected measurement budget. bench-release-inventory: _ensure-uv #!/usr/bin/env bash @@ -467,16 +472,16 @@ semgrep: _ensure-uv # Fixture tests for repository-owned Semgrep rules. semgrep-test: python-sync - {{ semgrep_stack_limit }} uv run --locked --group dev research-repo-tools semgrep check-fixtures + {{ semgrep_stack_limit }} uv run --locked --group dev python tooling/shared_tooling.py semgrep check-fixtures # Install declared managed tools and explicitly synchronize dev. setup: _ensure-gh _ensure-jq - uv run --locked --managed-python --only-group tooling research-repo-tools setup + uv run --locked --managed-python --only-group tooling python tooling/shared_tooling.py setup {{ _run }} cargo build # Tool-only setup; compilation belongs to setup. setup-tools: _ensure-gh _ensure-jq - uv run --locked --managed-python --only-group tooling research-repo-tools setup + uv run --locked --managed-python --only-group tooling python tooling/shared_tooling.py setup # Check shell script lint and formatting. shell-check: _ensure-shellcheck _ensure-shfmt @@ -508,7 +513,7 @@ test-all: test-rust test-python # Smoke-test deterministic inputs and configuration shared with benchmark suites. test-bench-inputs: tools-check - {{ _run }} cargo nextest run --workspace --profile ci --features bench,exact --test vs_linalg_inputs --test exact_bench_config --verbose + {{ _run }} cargo nextest run --workspace --profile ci --features bench,exact --test vs_linalg_inputs --test exact_bench_config --test column_basis --test rational_dot --test interval_solve --test polynomial_bench_inputs --verbose # Run default-feature Rust doctests. test-doc: @@ -629,7 +634,7 @@ validate-json: _ensure-jq # Validate external workflow and composite-action dependencies against the repository allowlist. workflow-allowlist-check: python-sync uv run --locked research-repo-tools actions allowlist --policy .github/actions-allowlist.json .github/workflows - # The v0.2.3 parser accepts workflows only; Semgrep checks composite actions. + # Keep this allowlist scoped to workflows; Semgrep checks composite actions. {{ semgrep_stack_limit }} uv run --locked --group dev semgrep --metrics off --error --strict --timeout 30 --config semgrep.yaml .github/actions # Check YAML and CFF formatting and lint. diff --git a/pyproject.toml b/pyproject.toml index fd5fa01..3da84b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,7 @@ override-dependencies = [ ] [dependency-groups] -tooling = [ "research-repo-tools[python-tools]==0.2.3" ] +tooling = [ "research-repo-tools[python-tools]==0.2.4" ] dev = [ { include-group = "tooling" }, "actionlint-py==1.7.12.25", @@ -130,7 +130,7 @@ cargo-nextest = "0.9.150" clippy-sarif = "0.8.0" dprint = "0.64.3" git-cliff = "2.14.2" -rumdl = "0.3.0" +rumdl = "0.3.2" sarif-fmt = "0.8.0" taplo-cli = "0.10.0" typos-cli = "1.51.1" @@ -171,7 +171,7 @@ exclude = [ "docs/archive/**", "docs/performance/**", "docs/performance-runs/**" path = "README.md" pattern = 'https://(?:github\.com/acgetchell/la-stack/(?:blob|raw|tree)/|raw\.githubusercontent\.com/acgetchell/la-stack/)(?Pmain|v[0-9]+\.[0-9]+\.[0-9]+|[0-9a-f]{7,40})/[^\s)\]>"?]+' value = "main" -count = 41 +count = 42 exclude = '/docs/(?:assets/bench|performance-runs)/' [[tool.research-repo-tools.release.rules]] diff --git a/src/error.rs b/src/error.rs index f650c64..1dc4185 100644 --- a/src/error.rs +++ b/src/error.rs @@ -215,6 +215,14 @@ impl fmt::Display for VectorOperand { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum NonFiniteLocation { + /// Raw coefficient supplied to a sparse polynomial constructor. + #[non_exhaustive] + PolynomialTerm { + /// Zero-based position in the supplied term iterator. + index: usize, + /// Exponent supplied alongside the invalid coefficient. + exponent: u128, + }, /// Cell `(row, col)` in matrix-shaped storage or computation. #[non_exhaustive] MatrixCell { @@ -259,6 +267,25 @@ pub enum NonFiniteLocation { Scalar, } +/// Work category limited by a polynomial determinant budget. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PolynomialWork { + /// Complete structural column-subset transition envelope. + SubsetTransitions, + /// Individual nonzero rational coefficient-pair products. + TermProducts, +} + +impl fmt::Display for PolynomialWork { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::SubsetTransitions => "subset transitions", + Self::TermProducts => "coefficient-pair products", + }) + } +} + /// Provenance of a non-finite value. /// /// # Examples @@ -404,6 +431,30 @@ pub enum UnrepresentableReason { #[derive(Clone, Copy, Debug, PartialEq)] #[non_exhaustive] pub enum LaError { + /// An exact polynomial determinant exceeded a caller-selected work limit. + #[non_exhaustive] + PolynomialBudgetExceeded { + /// Work category that exhausted its limit. + work: PolynomialWork, + /// Caller-selected maximum for this category. + limit: usize, + /// Completed units of this category before the rejected request. + completed: usize, + /// Additional units rejected before performing their arithmetic. + requested: usize, + }, + /// A polynomial product needs an exponent greater than `u128::MAX`. + #[non_exhaustive] + PolynomialExponentOverflow { + /// Row of the matrix entry being multiplied into a partial determinant. + row: usize, + /// Column of that entry. + col: usize, + /// Partial determinant term's exponent. + left: u128, + /// Matrix entry term's exponent. + right: u128, + }, /// Two vector operands have unequal ambient lengths. #[non_exhaustive] DimensionMismatch { @@ -815,6 +866,9 @@ fn write_non_finite_location( location: NonFiniteLocation, ) -> fmt::Result { match location { + NonFiniteLocation::PolynomialTerm { index, exponent } => { + write!(f, "polynomial term {index} (exponent {exponent})") + } NonFiniteLocation::MatrixCell { row, col } => { write!(f, "matrix cell ({row}, {col})") } @@ -884,9 +938,47 @@ fn write_unrepresentable( Ok(()) } +fn write_singularity( + f: &mut fmt::Formatter<'_>, + pivot_col: usize, + reason: SingularityReason, +) -> fmt::Result { + match reason { + SingularityReason::Exact => { + write!(f, "matrix is exactly singular at pivot column {pivot_col}") + } + SingularityReason::Numerical { + factorization, + pivot_magnitude, + tolerance, + } => write!( + f, + "matrix is numerically singular during {factorization} factorization at pivot column {pivot_col}: pivot magnitude {pivot_magnitude} <= tolerance {tolerance}" + ), + } +} + impl fmt::Display for LaError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { + Self::PolynomialBudgetExceeded { + work, + limit, + completed, + requested, + } => write!( + f, + "polynomial determinant budget exceeded for {work}: limit {limit}, completed {completed}, requested {requested} more" + ), + Self::PolynomialExponentOverflow { + row, + col, + left, + right, + } => write!( + f, + "polynomial exponent overflow at matrix cell ({row}, {col}): {left} + {right} exceeds u128::MAX" + ), Self::DimensionMismatch { left, right } => { write!( f, @@ -895,22 +987,7 @@ impl fmt::Display for LaError { } Self::EmptyVector => f.write_str("an empty vector has no direction"), Self::ZeroVector { operand } => write!(f, "the {operand} zero vector has no direction"), - Self::Singular { - pivot_col, - reason: SingularityReason::Exact, - } => write!(f, "matrix is exactly singular at pivot column {pivot_col}"), - Self::Singular { - pivot_col, - reason: - SingularityReason::Numerical { - factorization, - pivot_magnitude, - tolerance, - }, - } => write!( - f, - "matrix is numerically singular during {factorization} factorization at pivot column {pivot_col}: pivot magnitude {pivot_magnitude} <= tolerance {tolerance}" - ), + Self::Singular { pivot_col, reason } => write_singularity(f, pivot_col, reason), Self::NonFinite { location, origin } => write_non_finite(f, location, origin), Self::IntervalRangeExhausted { operation } => write!( f, @@ -986,6 +1063,41 @@ mod tests { use super::*; use crate::MAX_STACK_MATRIX_DISPATCH_DIM; + #[test] + fn polynomial_errors_display_work_and_location_without_exact_feature() { + assert_eq!( + PolynomialWork::SubsetTransitions.to_string(), + "subset transitions" + ); + assert_eq!( + PolynomialWork::TermProducts.to_string(), + "coefficient-pair products" + ); + assert_eq!( + LaError::PolynomialBudgetExceeded { + work: PolynomialWork::TermProducts, + limit: 4, + completed: 4, + requested: 1, + } + .to_string(), + "polynomial determinant budget exceeded for coefficient-pair products: limit 4, completed 4, requested 1 more" + ); + assert_eq!( + LaError::PolynomialExponentOverflow { + row: 1, + col: 2, + left: u128::MAX, + right: 1, + } + .to_string(), + format!( + "polynomial exponent overflow at matrix cell (1, 2): {} + 1 exceeds u128::MAX", + u128::MAX + ) + ); + } + #[test] fn category_displays_are_concise() { assert_eq!(FactorizationKind::Lu.to_string(), "LU"); diff --git a/src/lib.rs b/src/lib.rs index abf3020..33d0e1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -995,6 +995,8 @@ mod lu; mod matrix; mod norm; #[cfg(feature = "exact")] +mod polynomial; +#[cfg(feature = "exact")] mod rational; mod rounding; mod scaled_product; @@ -1015,6 +1017,12 @@ pub use num_rational::BigRational; pub use num_traits::{FromPrimitive, Signed, ToPrimitive}; #[cfg(feature = "exact")] #[cfg_attr(docsrs, doc(cfg(feature = "exact")))] +pub use polynomial::{ + MAX_POLYNOMIAL_MATRIX_DIM, PolynomialDeterminantBudget, SparsePolynomial, + polynomial_determinant, +}; +#[cfg(feature = "exact")] +#[cfg_attr(docsrs, doc(cfg(feature = "exact")))] pub use rational::{RationalMatrix, RationalVector, rational_dot}; pub use column_basis::{ColumnBasis, column_basis}; @@ -1184,8 +1192,8 @@ pub const MAX_RATIONAL_MATRIX_DISPATCH_DIM: usize = 8; pub use angle::VectorAngle; pub use error::{ ArithmeticOperation, FactorizationKind, IntervalBound, IntervalOperand, InvalidToleranceReason, - LaError, NonFiniteLocation, NonFiniteOrigin, PositiveSemidefiniteViolation, SingularityReason, - UnrepresentableReason, VectorOperand, + LaError, NonFiniteLocation, NonFiniteOrigin, PolynomialWork, PositiveSemidefiniteViolation, + SingularityReason, UnrepresentableReason, VectorOperand, }; pub use gram::gram_matrix; pub use interval::{Interval, IntervalDeterminantSign, IntervalMatrix, MAX_INTERVAL_MATRIX_DIM}; @@ -1516,12 +1524,17 @@ macro_rules! try_with_rational_matrix { doc = "`BigRational::to_f64` / `to_i64`, and [`Signed`] for `is_positive` / `is_negative` /" )] #[cfg_attr(feature = "exact", doc = "`abs`.")] +#[cfg_attr(feature = "exact", doc = "")] +#[cfg_attr( + feature = "exact", + doc = "[`SparsePolynomial`], [`PolynomialDeterminantBudget`], [`polynomial_determinant`] and [`MAX_POLYNOMIAL_MATRIX_DIM`] support bounded exact polynomial determinants. [`PolynomialWork`] identifies exhausted budget categories." +)] pub mod prelude { pub use crate::{ ArithmeticOperation, ColumnBasis, DEFAULT_SINGULAR_TOL, DeterminantWithErrorBound, FactorizationKind, Interval, IntervalBound, IntervalDeterminantSign, IntervalMatrix, IntervalOperand, InvalidToleranceReason, LaError, Ldlt, Lu, MAX_INTERVAL_MATRIX_DIM, - MAX_STACK_MATRIX_DISPATCH_DIM, Matrix, NonFiniteLocation, NonFiniteOrigin, + MAX_STACK_MATRIX_DISPATCH_DIM, Matrix, NonFiniteLocation, NonFiniteOrigin, PolynomialWork, PositiveSemidefiniteViolation, ScalarWithErrorBound, SingularityReason, Tolerance, UnrepresentableReason, Vector, VectorAngle, VectorOperand, column_basis, gram_matrix, try_with_interval_matrix, try_with_stack_matrix, @@ -1531,8 +1544,9 @@ pub mod prelude { #[cfg_attr(docsrs, doc(cfg(feature = "exact")))] pub use crate::{ BigInt, BigRational, DeterminantSign, ExactF64Conversion, FromPrimitive, - MAX_RATIONAL_MATRIX_DISPATCH_DIM, RationalMatrix, RationalVector, Signed, ToPrimitive, - rational_dot, try_with_rational_matrix, + MAX_POLYNOMIAL_MATRIX_DIM, MAX_RATIONAL_MATRIX_DISPATCH_DIM, PolynomialDeterminantBudget, + RationalMatrix, RationalVector, Signed, SparsePolynomial, ToPrimitive, + polynomial_determinant, rational_dot, try_with_rational_matrix, }; } diff --git a/src/polynomial.rs b/src/polynomial.rs new file mode 100644 index 0000000..d22a8d8 --- /dev/null +++ b/src/polynomial.rs @@ -0,0 +1,232 @@ +#![forbid(unsafe_code)] + +//! Bounded exact determinants over sparse univariate rational polynomials. +//! +//! The subset recurrence reorganizes the Leibniz identity (REFERENCES.md \[12\]). +//! See the [invariant and resource contract](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#exact-sparse-polynomial-determinants). + +use std::collections::{BTreeMap, btree_map::Entry}; + +use num_rational::BigRational; +use num_traits::{One, Zero}; + +use crate::rational::canonicalize_rational; +use crate::{LaError, NonFiniteLocation, NonFiniteOrigin, PolynomialWork}; + +/// Largest supported order of [`polynomial_determinant`]. +pub const MAX_POLYNOMIAL_MATRIX_DIM: usize = 8; + +/// An exact polynomial `Σ coefficient[e] * t^e` with nonnegative `u128` exponents. +/// +/// Terms are stored in increasing exponent order. Coefficients are reduced +/// rational numbers with positive denominators; zero coefficients are absent. +/// Duplicate input exponents are summed exactly, including cancellation. +/// [`Self::leading_term`] means the *least* exponent with a nonzero coefficient, +/// appropriate to an infinitesimal `t → 0+`, rather than the highest degree. +/// +/// Storage and arbitrary-precision coefficients allocate on the heap. Construction +/// consumes owned terms; queries borrow them; cloning makes a deep copy. The +/// determinant budget does not cover construction. Multivariate encodings, +/// perturbation-variable assignment and interpretation of coefficient signs +/// belong to callers, who must prove their encoding preserves the desired order +/// and exponent addition. No multivariate ordering is inferred here. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[must_use] +pub struct SparsePolynomial { + terms: BTreeMap, +} + +impl SparsePolynomial { + /// Construct the zero polynomial without allocating. + pub const fn zero() -> Self { + Self { + terms: BTreeMap::new(), + } + } + + /// Consume terms, normalize rational coefficients and combine equal exponents. + /// + /// Input order is immaterial to a successful result. Raw unreduced or + /// negative-denominator rationals are accepted. Validation precedes zero + /// removal, so even `0/0` is rejected. The iterator is consumed only up to + /// the first invalid coefficient; this constructor has no work budget. + /// + /// # Errors + /// Returns [`LaError::NonFinite`] with input origin and a + /// [`NonFiniteLocation::PolynomialTerm`] for a zero denominator. + pub fn try_from_terms( + terms: impl IntoIterator, + ) -> Result { + let mut polynomial = Self::zero(); + for (index, (exponent, coefficient)) in terms.into_iter().enumerate() { + if coefficient.denom().is_zero() { + return Err(LaError::NonFinite { + location: NonFiniteLocation::PolynomialTerm { index, exponent }, + origin: NonFiniteOrigin::Input, + }); + } + polynomial.add_term(exponent, canonicalize_rational(coefficient)); + } + Ok(polynomial) + } + + /// Borrow all nonzero terms in increasing exponent order. + #[must_use] + pub const fn terms(&self) -> &BTreeMap { + &self.terms + } + + /// Whether every coefficient is exactly zero. + #[must_use] + pub fn is_zero(&self) -> bool { + self.terms.is_empty() + } + + /// Borrow the first nonzero coefficient in increasing exponent order. + /// + /// `None` proves this is the zero polynomial. A failed determinant evaluation + /// never returns a polynomial, so it cannot be confused with this result. + #[must_use] + pub fn leading_term(&self) -> Option<(u128, &BigRational)> { + self.terms + .first_key_value() + .map(|(&exponent, coefficient)| (exponent, coefficient)) + } + + fn add_term(&mut self, exponent: u128, coefficient: BigRational) { + if coefficient.is_zero() { + return; + } + match self.terms.entry(exponent) { + Entry::Vacant(entry) => { + entry.insert(coefficient); + } + Entry::Occupied(mut entry) => { + *entry.get_mut() += coefficient; + if entry.get().is_zero() { + entry.remove(); + } + } + } + } +} + +/// Caller-selected work limits for one [`polynomial_determinant`] invocation. +/// +/// No default policy is supplied. All limits, including zero, are valid. +/// These count combinatorial work, not elapsed time, bytes or integer bit +/// operations: a single rational product can be expensive for wide inputs. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[must_use] +pub struct PolynomialDeterminantBudget { + /// Maximum structural subset transitions. The complete `D * 2^(D-1)` + /// envelope is checked before allocation, even for an all-zero matrix. + /// Order zero requires zero transitions; order eight requires 1,024. + pub max_subset_transitions: usize, + /// Maximum individual nonzero coefficient-pair multiplications. Each is + /// charged before exponent addition or rational arithmetic; zero partials + /// and zero entries cost no products. Cancellation does not refund work. + pub max_term_products: usize, +} + +/// Compute the complete exact determinant polynomial of a borrowed square array. +/// +/// Supports orders `0..=8`; the empty determinant is the constant one. Singular +/// polynomial matrices return the zero polynomial successfully. All cancellation +/// terms are retained, so [`SparsePolynomial::leading_term`] is valid only after +/// the complete calculation succeeds. There is no early sign fallback, truncation, +/// division, rounding or conditioning-dependent numerical error. +/// +/// The kernel allocates `2^D` sparse states (at most 256) and consumes each partial +/// once all its predecessors have contributed. Inputs are neither cloned nor +/// changed. At most one new stored term is created per charged product, plus the +/// initial constant one. Rational sizes, allocator overhead, and caller-owned +/// inputs are outside these combinatorial bounds; allocation failure follows +/// Rust's allocator behavior. Limits do not promise a byte or wall-clock bound. +/// +/// # Errors +/// In order: [`LaError::UnsupportedDimension`] for `D > 8`, +/// [`LaError::PolynomialBudgetExceeded`] for insufficient structural transitions, +/// then product-budget or [`LaError::PolynomialExponentOverflow`] failures during +/// evaluation. An overflowing intermediate exponent is an error even if later +/// contributions could cancel it. Errors discard partial results and leave the +/// input reusable. Traversal is increasing subset mask, column, left exponent, +/// then right exponent, making budget accounting deterministic. +/// +/// # Examples +/// ``` +/// use la_stack::prelude::*; +/// # fn main() -> Result<(), LaError> { +/// let one = SparsePolynomial::try_from_terms([(0, BigRational::from_integer(1.into()))])?; +/// let perturbed = SparsePolynomial::try_from_terms([ +/// (0, BigRational::from_integer(1.into())), +/// (3, BigRational::new(1.into(), 2.into())), +/// ])?; +/// let matrix = [[one.clone(), one.clone()], [one, perturbed]]; +/// let determinant = polynomial_determinant(&matrix, PolynomialDeterminantBudget { +/// max_subset_transitions: 4, +/// max_term_products: 5, +/// })?; +/// assert_eq!(determinant.leading_term(), Some((3, &BigRational::new(1.into(), 2.into())))); +/// # Ok(()) +/// # } +/// ``` +pub fn polynomial_determinant( + matrix: &[[SparsePolynomial; D]; D], + budget: PolynomialDeterminantBudget, +) -> Result { + if D > MAX_POLYNOMIAL_MATRIX_DIM { + return Err(LaError::unsupported_dimension(D, MAX_POLYNOMIAL_MATRIX_DIM)); + } + let state_count = 1usize << D; + let transitions = D * (state_count / 2); + if transitions > budget.max_subset_transitions { + return Err(LaError::PolynomialBudgetExceeded { + work: PolynomialWork::SubsetTransitions, + limit: budget.max_subset_transitions, + completed: 0, + requested: transitions, + }); + } + let mut partials = vec![SparsePolynomial::zero(); state_count]; + partials[0].add_term(0, BigRational::one()); + let mut products = 0; + for mask in 0..state_count - 1 { + let row = mask.count_ones() as usize; + let partial = std::mem::take(&mut partials[mask]); + if partial.is_zero() { + continue; + } + for (col, entry) in matrix[row].iter().enumerate() { + if mask & (1 << col) != 0 { + continue; + } + let negative = (mask >> (col + 1)).count_ones() % 2 != 0; + let next = &mut partials[mask | (1 << col)]; + for (&left, left_coefficient) in &partial.terms { + for (&right, right_coefficient) in &entry.terms { + if products == budget.max_term_products { + return Err(LaError::PolynomialBudgetExceeded { + work: PolynomialWork::TermProducts, + limit: budget.max_term_products, + completed: products, + requested: 1, + }); + } + products += 1; + let exponent = + left.checked_add(right) + .ok_or(LaError::PolynomialExponentOverflow { + row, + col, + left, + right, + })?; + let coefficient = left_coefficient * right_coefficient; + next.add_term(exponent, if negative { -coefficient } else { coefficient }); + } + } + } + } + Ok(std::mem::take(&mut partials[state_count - 1])) +} diff --git a/src/rational.rs b/src/rational.rs index 33f91a0..c10ea14 100644 --- a/src/rational.rs +++ b/src/rational.rs @@ -579,7 +579,7 @@ pub fn rational_dot(left: &[BigRational], right: &[BigRational]) -> Result BigRational { +pub(crate) fn canonicalize_rational(value: BigRational) -> BigRational { let (numerator, denominator) = value.into_raw(); BigRational::new(numerator, denominator) } diff --git a/tests/polynomial_bench_inputs.rs b/tests/polynomial_bench_inputs.rs new file mode 100644 index 0000000..fcc1d93 --- /dev/null +++ b/tests/polynomial_bench_inputs.rs @@ -0,0 +1,58 @@ +#![forbid(unsafe_code)] + +//! Exercise the full lifted consumer shape through geometric dimension six. + +#![cfg(feature = "exact")] + +#[path = "../benches/common/bench_utils.rs"] +mod bench_utils; +#[path = "../benches/common/polynomial.rs"] +mod polynomial; + +use la_stack::polynomial_determinant; +use pastey::paste; +use polynomial::{BUDGET, coincident_lifted}; + +macro_rules! lifted_test { + ($n:literal) => { + paste! { + #[test] + fn []() { + let mut input = coincident_lifted::<$n>(); + // Repeating a complete polynomial row must cancel every term. + let (first, rest) = input.matrix.split_at_mut(1); + rest[0].clone_from(&first[0]); + assert!(polynomial_determinant(&input.matrix, BUDGET).unwrap().is_zero()); + } + } + }; +} +lifted_test!(4); +lifted_test!(5); +lifted_test!(6); +lifted_test!(7); +lifted_test!(8); + +#[test] +fn consumer_product_envelope_matches_documented_policy() { + let maxima = [2_usize, 2, 2, 2, 2, 2, 13, 1]; + let mut total = 0; + for mask in 0_usize..255 { + let size = mask.count_ones() as usize; + let partial = (1..=size).product::() + * maxima + .iter() + .enumerate() + .filter(|(i, _)| mask & (1 << i) != 0) + .map(|(_, &count)| count) + .product::(); + let choices: usize = maxima + .iter() + .enumerate() + .filter(|(i, _)| mask & (1 << i) == 0) + .map(|(_, &count)| count) + .sum(); + total += partial * choices; + } + assert_eq!(total, BUDGET.max_term_products); +} diff --git a/tests/polynomial_determinant.rs b/tests/polynomial_determinant.rs new file mode 100644 index 0000000..dc4639b --- /dev/null +++ b/tests/polynomial_determinant.rs @@ -0,0 +1,419 @@ +#![forbid(unsafe_code)] + +//! Independent permutation expansions and public polynomial contracts. + +#![cfg(feature = "exact")] + +use core::assert_matches; +use std::array::from_fn; +use std::collections::BTreeMap; + +use la_stack::prelude::*; +use num_traits::{One, Zero}; +use pastey::paste; + +const BUDGET: PolynomialDeterminantBudget = PolynomialDeterminantBudget { + max_subset_transitions: 1_024, + max_term_products: 1_000_000, +}; + +fn rational(numerator: i64, denominator: i64) -> BigRational { + BigRational::new(numerator.into(), denominator.into()) +} + +fn polynomial(terms: &[(u128, i64)]) -> SparsePolynomial { + SparsePolynomial::try_from_terms(terms.iter().map(|&(e, c)| (e, rational(c, 1)))) + .expect("integer coefficients") +} + +// A dense coefficient vector and factorial permutation enumeration deliberately +// avoid the production sparse convolution and column-subset recurrence. +fn permutation_determinant( + matrix: &[[Vec; D]; D], +) -> Vec { + fn visit( + matrix: &[[Vec; D]; D], + columns: &mut [usize; D], + row: usize, + result: &mut Vec, + ) { + if row < D { + for next in row..D { + columns.swap(row, next); + visit(matrix, columns, row + 1, result); + columns.swap(row, next); + } + return; + } + let inversions: usize = (0..D) + .map(|i| (i + 1..D).filter(|&j| columns[i] > columns[j]).count()) + .sum(); + let mut product = vec![rational( + if inversions.is_multiple_of(2) { 1 } else { -1 }, + 1, + )]; + for (i, &j) in columns.iter().enumerate() { + let entry = &matrix[i][j]; + let mut next = vec![BigRational::zero(); product.len() + entry.len() - 1]; + for (a, x) in product.iter().enumerate() { + for (b, y) in entry.iter().enumerate() { + next[a + b] += x * y; + } + } + product = next; + } + result.resize(result.len().max(product.len()), BigRational::zero()); + for (target, coefficient) in result.iter_mut().zip(product) { + *target += coefficient; + } + } + let mut result = Vec::new(); + visit(matrix, &mut from_fn(|i| i), 0, &mut result); + result +} + +fn dense_terms(coefficients: &[BigRational]) -> BTreeMap { + coefficients + .iter() + .enumerate() + .filter(|(_, coefficient)| !coefficient.is_zero()) + .map(|(exponent, coefficient)| (exponent as u128, coefficient.clone())) + .collect() +} + +fn check_permutations() { + for seed in 0..6 { + let dense: [[Vec; D]; D] = from_fn(|row| { + from_fn(|col| { + (0..3) + .map(|e| { + let value = + (row * 7 + col * 11 + e * 13 + row * col * (seed + 1) + seed) % 9; + rational( + i64::try_from(value).unwrap() - 4, + i64::try_from(e + col + 1).unwrap(), + ) + }) + .collect() + }) + }); + let mut matrix = dense.clone().map(|row| { + row.map(|coefficients| { + SparsePolynomial::try_from_terms( + coefficients + .into_iter() + .enumerate() + .map(|(e, c)| (e as u128, c)), + ) + .unwrap() + }) + }); + let expected = dense_terms(&permutation_determinant(&dense)); + let result = polynomial_determinant(&matrix, BUDGET).unwrap(); + assert_eq!(result.terms(), &expected, "order {D}, seed {seed}"); + matrix.swap(0, D - 1); + let negative: BTreeMap<_, _> = expected.iter().map(|(&e, c)| (e, -c)).collect(); + assert_eq!( + polynomial_determinant(&matrix, BUDGET).unwrap().terms(), + &negative + ); + let (first, rest) = matrix.split_at_mut(1); + rest[0].clone_from(&first[0]); + assert_eq!( + polynomial_determinant(&matrix, BUDGET).unwrap(), + SparsePolynomial::zero() + ); + } +} + +// det(J + diag(t^e_i)) = product(t^e_i) + sum_i product_{j != i}(t^e_j). +// Multilinearity leaves either all diagonal columns or exactly one J column; +// two J columns are identical and contribute zero. This checks every coefficient +// with independent analytical evidence through the largest supported order. +fn check_degenerate() { + let exponents: [u128; D] = from_fn(|i| 3_u128.pow(u32::try_from(i).unwrap())); + let matrix: [[SparsePolynomial; D]; D] = from_fn(|row| { + from_fn(|col| { + if row == col { + polynomial(&[(0, 1), (exponents[row], 1)]) + } else { + polynomial(&[(0, 1)]) + } + }) + }); + let total: u128 = exponents.iter().sum(); + let expected: BTreeMap<_, _> = std::iter::once((total, BigRational::one())) + .chain(exponents.iter().map(|e| (total - e, BigRational::one()))) + .collect(); + let result = polynomial_determinant(&matrix, BUDGET).unwrap(); + assert_eq!(result.terms(), &expected); + assert_eq!(result.leading_term().unwrap().0, total - exponents[D - 1]); +} + +macro_rules! dimension_tests { + ($d:literal) => { + paste! { + #[test] + fn []() { check_permutations::<$d>(); } + #[test] + fn []() { check_degenerate::<$d>(); } + } + }; +} +dimension_tests!(2); +dimension_tests!(3); +dimension_tests!(4); +dimension_tests!(5); + +#[test] +fn degenerate_largest_orders() { + check_degenerate::<6>(); + check_degenerate::<7>(); + check_degenerate::<8>(); +} + +#[test] +fn canonicalization_merges_cancels_and_orders() { + const ZERO: SparsePolynomial = SparsePolynomial::zero(); + let result = SparsePolynomial::try_from_terms([ + (7, BigRational::new_raw(2.into(), (-4).into())), + (2, rational(3, 5)), + (7, rational(1, 2)), + (1, BigRational::new_raw(0.into(), (-3).into())), + (2, rational(-1, 10)), + (9, BigRational::new_raw((-6).into(), (-8).into())), + ]) + .unwrap(); + assert_eq!( + result.terms(), + &BTreeMap::from([(2, rational(1, 2)), (9, rational(3, 4))]) + ); + assert_eq!(result.leading_term(), Some((2, &rational(1, 2)))); + assert!(!result.is_zero()); + assert_eq!(ZERO, SparsePolynomial::default()); + assert!(ZERO.is_zero()); + assert_eq!(ZERO.leading_term(), None); +} + +#[test] +fn invalid_raw_coefficient_is_not_hidden_by_zero_removal() { + for numerator in [0, 1] { + let result = SparsePolynomial::try_from_terms([ + (4, rational(1, 1)), + (7, BigRational::new_raw(numerator.into(), 0.into())), + ]); + assert_matches!( + result, + Err(LaError::NonFinite { + location: NonFiniteLocation::PolynomialTerm { + index: 1, + exponent: 7, + .. + }, + origin: NonFiniteOrigin::Input, + .. + }) + ); + assert_eq!( + result.unwrap_err().to_string(), + "non-finite input value at polynomial term 1 (exponent 7)" + ); + } +} + +#[test] +fn empty_zero_and_unsupported_orders_are_distinct() { + let none = PolynomialDeterminantBudget { + max_subset_transitions: 0, + max_term_products: 0, + }; + assert_eq!( + polynomial_determinant::<0>(&[], none).unwrap(), + polynomial(&[(0, 1)]) + ); + let zero = [[SparsePolynomial::zero()]]; + assert_eq!( + polynomial_determinant( + &zero, + PolynomialDeterminantBudget { + max_subset_transitions: 1, + ..none + } + ) + .unwrap() + .leading_term(), + None + ); + let too_large: [[SparsePolynomial; 9]; 9] = from_fn(|_| from_fn(|_| SparsePolynomial::zero())); + assert_eq!( + polynomial_determinant(&too_large, none), + Err(LaError::unsupported_dimension(9, MAX_POLYNOMIAL_MATRIX_DIM)) + ); +} + +#[test] +fn early_coefficients_cancel_before_leading_term_is_selected() { + // (1+t)(1-t+t^2+t^5) - 1 = t^3+t^5+t^6. + let matrix = [ + [polynomial(&[(0, 1), (1, 1)]), polynomial(&[(0, 1)])], + [ + polynomial(&[(0, 1)]), + polynomial(&[(0, 1), (1, -1), (2, 1), (5, 1)]), + ], + ]; + assert_eq!( + polynomial_determinant(&matrix, BUDGET).unwrap(), + polynomial(&[(3, 1), (5, 1), (6, 1)]) + ); +} + +#[test] +fn budgets_are_inclusive_deterministic_and_do_not_mutate_inputs() { + let one = polynomial(&[(0, 1)]); + let matrix = [ + [one.clone(), one.clone()], + [one, polynomial(&[(0, 1), (3, 1)])], + ]; + let original = matrix.clone(); + let exact = PolynomialDeterminantBudget { + max_subset_transitions: 4, + max_term_products: 5, + }; + for (budget, expected) in [ + ( + PolynomialDeterminantBudget { + max_subset_transitions: 3, + ..exact + }, + (PolynomialWork::SubsetTransitions, 3, 0, 4), + ), + ( + PolynomialDeterminantBudget { + max_term_products: 4, + ..exact + }, + (PolynomialWork::TermProducts, 4, 4, 1), + ), + ( + PolynomialDeterminantBudget { + max_term_products: 0, + ..exact + }, + (PolynomialWork::TermProducts, 0, 0, 1), + ), + ] { + match polynomial_determinant(&matrix, budget) { + Err(LaError::PolynomialBudgetExceeded { + work, + limit, + completed, + requested, + .. + }) => { + assert_eq!((work, limit, completed, requested), expected); + } + other => panic!("expected budget exhaustion, got {other:?}"), + } + assert_eq!(matrix, original); + } + assert_eq!( + polynomial_determinant(&matrix, exact).unwrap(), + polynomial(&[(3, 1)]) + ); + let zero: [[SparsePolynomial; 8]; 8] = from_fn(|_| from_fn(|_| SparsePolynomial::zero())); + assert_matches!( + polynomial_determinant( + &zero, + PolynomialDeterminantBudget { + max_subset_transitions: 1_023, + max_term_products: usize::MAX, + } + ), + Err(LaError::PolynomialBudgetExceeded { + work: PolynomialWork::SubsetTransitions, + limit: 1_023, + completed: 0, + requested: 1_024, + .. + }) + ); + assert!( + polynomial_determinant( + &zero, + PolynomialDeterminantBudget { + max_subset_transitions: 1_024, + max_term_products: 0, + } + ) + .unwrap() + .is_zero() + ); +} + +#[test] +fn exponent_boundaries_and_error_precedence() { + let largest = polynomial(&[(u128::MAX, 1)]); + assert_eq!( + polynomial_determinant(&[[largest.clone()]], BUDGET).unwrap(), + largest + ); + let matrix = [ + [largest, SparsePolynomial::zero()], + [SparsePolynomial::zero(), polynomial(&[(1, 1)])], + ]; + assert_matches!( + polynomial_determinant(&matrix, BUDGET), + Err(LaError::PolynomialExponentOverflow { + row: 1, + col: 1, + left: u128::MAX, + right: 1, + .. + }) + ); + assert_matches!( + polynomial_determinant( + &matrix, + PolynomialDeterminantBudget { + max_term_products: 1, + ..BUDGET + } + ), + Err(LaError::PolynomialBudgetExceeded { + work: PolynomialWork::TermProducts, + limit: 1, + completed: 1, + requested: 1, + .. + }) + ); + let boundary = [ + [polynomial(&[(u128::MAX - 1, 1)]), SparsePolynomial::zero()], + [SparsePolynomial::zero(), polynomial(&[(1, 1)])], + ]; + assert_eq!( + polynomial_determinant(&boundary, BUDGET).unwrap(), + polynomial(&[(u128::MAX, 1)]) + ); +} + +#[test] +fn coefficients_remain_exact_beyond_binary64_range() { + let wide = BigInt::one() << 2048; + let a = BigRational::new(&wide + 1, 3.into()); + let b = BigRational::new(5.into(), &wide - 1); + let matrix = [ + [ + SparsePolynomial::try_from_terms([(0, a.clone()), (7, b.clone())]).unwrap(), + SparsePolynomial::zero(), + ], + [ + SparsePolynomial::zero(), + SparsePolynomial::try_from_terms([(0, a.clone()), (7, -b.clone())]).unwrap(), + ], + ]; + assert_eq!( + polynomial_determinant(&matrix, BUDGET).unwrap().terms(), + &BTreeMap::from([(0, &a * &a), (14, -(&b * &b))]) + ); +} diff --git a/tests/proptest_polynomial.rs b/tests/proptest_polynomial.rs new file mode 100644 index 0000000..dc563fe --- /dev/null +++ b/tests/proptest_polynomial.rs @@ -0,0 +1,66 @@ +#![forbid(unsafe_code)] + +//! Determinant multilinearity for arbitrary canonical polynomial entries. + +#![cfg(feature = "exact")] + +use std::array::from_fn; + +use la_stack::prelude::*; +use pastey::paste; +use proptest::prelude::*; + +#[path = "common/proptest_config.rs"] +mod proptest_config; + +const BUDGET: PolynomialDeterminantBudget = PolynomialDeterminantBudget { + max_subset_transitions: 1_024, + max_term_products: 1_000_000, +}; + +fn row_scaling(terms: &[(u8, i16, i16)], scale: i16) { + let mut matrix: [[SparsePolynomial; D]; D] = from_fn(|row| { + from_fn(|col| { + let start = 2 * (row * D + col); + SparsePolynomial::try_from_terms( + terms[start..start + 2] + .iter() + .map(|&(e, n, d)| (u128::from(e), BigRational::new(n.into(), d.into()))), + ) + .unwrap() + }) + }); + let determinant = polynomial_determinant(&matrix, BUDGET).unwrap(); + let factor = BigRational::from_integer(scale.into()); + for entry in &mut matrix[0] { + *entry = + SparsePolynomial::try_from_terms(entry.terms().iter().map(|(&e, c)| (e, c * &factor))) + .unwrap(); + } + let expected = SparsePolynomial::try_from_terms( + determinant.terms().iter().map(|(&e, c)| (e, c * &factor)), + ) + .unwrap(); + assert_eq!(polynomial_determinant(&matrix, BUDGET).unwrap(), expected); +} + +macro_rules! dimension_properties { + ($d:literal) => { + paste! { + proptest! { + #![proptest_config(proptest_config::with_default_cases(32))] + #[test] + fn []( + terms in prop::collection::vec((0_u8..6, -4_i16..5, 1_i16..7), 2 * $d * $d), + scale in -3_i16..4, + ) { + row_scaling::<$d>(&terms, scale); + } + } + } + }; +} +dimension_properties!(2); +dimension_properties!(3); +dimension_properties!(4); +dimension_properties!(5); diff --git a/tests/tooling/test_justfile_discoverability.py b/tests/tooling/test_justfile_discoverability.py index 00d144f..d05c841 100644 --- a/tests/tooling/test_justfile_discoverability.py +++ b/tests/tooling/test_justfile_discoverability.py @@ -66,8 +66,10 @@ def test_saved_release_baseline_selects_only_public_workloads() -> None: baseline_commands = [shlex.split(line) for line in baseline_run.stdout.splitlines()] assert baseline_commands == [["--justfile", "tooling/performance.just", "--working-directory", ".", "la-stack", "measure", "v0.4.5"]] native = dry_run(REPO_ROOT, "la-stack", ["measure", "v0.4.5"], justfile=Path("tooling/performance.just")).stderr - assert native.count("cargo bench --locked") == 9 - assert native.count("args+=(--save-baseline") == 9 + assert native.count("cargo bench --locked") == 13 + assert native.count("args+=(--save-baseline") == 13 + for target in ("column_basis", "interval_solve", "polynomial", "rational_dot"): + assert f"--bench '{target}'" in native assert "^d[0-9]+/la_stack_" in native assert "(nalgebra|faer)_lu_solve$" not in native diff --git a/tests/tooling/test_maintenance_integration.py b/tests/tooling/test_maintenance_integration.py index ad39b05..571736e 100644 --- a/tests/tooling/test_maintenance_integration.py +++ b/tests/tooling/test_maintenance_integration.py @@ -161,7 +161,7 @@ def test_semgrep_adapter_uses_real_consumer_rules_and_fixtures() -> None: assert config["fixtures"] == "tests/semgrep" assert config["namespace"] == "la-stack." result = dry_run(REPO_ROOT, "semgrep-test") - assert "research-repo-tools semgrep check-fixtures" in result.stderr + assert "uv run --locked --group dev python tooling/shared_tooling.py semgrep check-fixtures" in result.stderr def test_semgrep_runs_in_the_root_python_315_environment_with_compatible_dependencies() -> None: diff --git a/tests/tooling/test_performance_workflow.py b/tests/tooling/test_performance_workflow.py index 0475fee..82f70dd 100644 --- a/tests/tooling/test_performance_workflow.py +++ b/tests/tooling/test_performance_workflow.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, cast import pytest -from research_repo_tools.common_measurement import CommonHarnessPlan, DiscoveredSeries, DiscoveryPolicy, measure_prepared_pair, validate_run_policy +from research_repo_tools.common_measurement import CompleteMeasurementPlan, DiscoveredSeries, DiscoveryPolicy, measure_prepared_pair, validate_run_policy from research_repo_tools.complete_runs import RunSeries from research_repo_tools.evidence import load_evidence, publish_evidence from research_repo_tools.just_inspect import dry_run @@ -58,9 +58,9 @@ def estimate(value): """ -def declaration(root: Path, view: str = "full") -> CommonHarnessPlan: +def declaration(root: Path, view: str = "full") -> CompleteMeasurementPlan: plan = load_measurement(root, f"tooling/measurements/{view}.toml") - assert isinstance(plan, CommonHarnessPlan) + assert isinstance(plan, CompleteMeasurementPlan) return plan @@ -108,7 +108,7 @@ def test_declared_scientific_sampling_coverage_and_original_peer_phases(view: st assert phase.policy.statistics == ("mean", "median") assert phase.policy.confidence_level == 0.95 assert phase.policy.unit == "ns" - assert phase.gate[-1] == "gate" + assert phase.gate[-1] == ("gate-public" if view == "full" else "gate") environment = dict(phase.environment) assert environment["RUSTFLAGS"] == environment["CARGO_ENCODED_RUSTFLAGS"] == "--cap-lints=warn" assert environment["RUSTUP_TOOLCHAIN"] == tomllib.loads((ROOT / "rust-toolchain.toml").read_text(encoding="utf-8"))["toolchain"]["channel"] @@ -139,6 +139,15 @@ def test_declared_scientific_sampling_coverage_and_original_peer_phases(view: st } assert {"context.os", "context.architecture", "context.cpu", "harness_sha256"} <= set(plan.measurement.compatible) assert {"benches/**/*.rs", "tests/exact_bench_config.rs", "tooling/performance.just", "tooling/measurements/*.toml"} <= set(plan.harness) + if view == "full": + assert { + "tests/column_basis.rs", + "tests/rational_dot.rs", + "tests/interval_solve.rs", + "tests/common/interval_solve_oracle.rs", + "tests/common/proptest_config.rs", + "tests/polynomial_bench_inputs.rs", + } <= set(plan.harness) def test_named_views_keep_every_metric_statistic_axis_and_explicit_input_choice() -> None: @@ -202,14 +211,29 @@ def test_native_recipes_preserve_scientific_gates_and_peer_reuse() -> None: justfile = Path("tooling/performance.just") baseline = dry_run(ROOT, "baseline-all", justfile=justfile).stderr current = dry_run(ROOT, "current-all", justfile=justfile).stderr - assert baseline.count("cargo bench --locked") == 10 - assert current.count("cargo bench --locked") == 9 + assert baseline.count("cargo bench --locked") == 14 + assert current.count("cargo bench --locked") == 13 assert "(nalgebra|faer)_lu_solve$" in baseline assert "(nalgebra|faer)_lu_solve$" not in current assert "--target" in baseline assert "--target" in current - assert dry_run(ROOT, "baseline-all", ["list"], justfile=justfile).stderr.count("args+=(--list)") == 10 + assert dry_run(ROOT, "baseline-all", ["list"], justfile=justfile).stderr.count("args+=(--list)") == 14 assert "--test vs_linalg_inputs --test exact_bench_config" in dry_run(ROOT, "gate", justfile=justfile).stderr + gate = dry_run(ROOT, "gate-public", justfile=justfile).stderr + hosted_gate = dry_run(ROOT, "test-bench-inputs").stderr + for target in ("vs_linalg_inputs", "exact_bench_config", "column_basis", "rational_dot", "interval_solve", "polynomial_bench_inputs"): + assert f"--test {target}" in gate + assert f"--test {target}" in hosted_gate + # New native targets must be considered for release coverage. Diagnostic + # controls are filtered within targets rather than omitting public targets. + targets = { + bench["name"] + for manifest in (ROOT / "Cargo.toml", ROOT / "benches/comparison/Cargo.toml") + for bench in tomllib.loads(manifest.read_text(encoding="utf-8"))["bench"] + } + for target in targets: + assert f"--bench '{target}'" in baseline + assert f"--bench '{target}'" in current for name in ("performance-local", "performance-local-non-exact"): command = dry_run(ROOT, name).stderr assert "research-repo-tools performance measure" in command @@ -266,6 +290,7 @@ def prepared(tmp_path: Path) -> tuple[Path, Path]: native += f'\n{side}-{suite} mode="measure":\n {driver} {side} {suite} {{{{mode}}}}\n' native += f'\n{side}-vs_linalg mode="measure":\n {driver} {side} vs_linalg {{{{mode}}}}\n' native += f"\ngate:\n {driver} current all gate\n" + native += "\ngate-public: gate\n" (root / "tooling/performance.just").write_text(native, encoding="utf-8", newline="\n") for config in (root / "tooling/measurements").glob("*.toml"): config.write_text( diff --git a/tests/tooling/test_release_baseline.py b/tests/tooling/test_release_baseline.py index 922400a..6fbd472 100644 --- a/tests/tooling/test_release_baseline.py +++ b/tests/tooling/test_release_baseline.py @@ -16,6 +16,7 @@ import pytest from packaging.requirements import Requirement from research_repo_tools.criterion_inventory import CriterionInventory, load_discovery, serialize_inventory +from research_repo_tools.host_metadata import HostMetadata, serialize_host from research_repo_tools.process import run_command REPO_ROOT = Path(__file__).resolve().parents[2] @@ -38,6 +39,7 @@ STORAGE = tomllib.loads((REPO_ROOT / "tooling/criterion-saved.toml").read_text(encoding="utf-8"))["storage-paths"] BASELINE = "v1.2.3" RAW_FILES = ("benchmark.json", "estimates.json", "sample.json", "tukey.json") +SUITE_ENV = {"RELEASE_TAG": BASELINE, "GITHUB_SHA": "a" * 40, "GITHUB_REPOSITORY": "owner/repo", "GITHUB_RUN_ID": "123", "GITHUB_RUN_ATTEMPT": "2"} def write_json(path: Path, data: object) -> None: @@ -71,11 +73,12 @@ def dataset(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, Path return criterion, manifest -def verify_saved(root: Path) -> subprocess.CompletedProcess[str]: +def verify_saved(root: Path, *, suite: str | None = None, criterion_dir: str = "target/criterion") -> subprocess.CompletedProcess[str]: """Exercise the installed verifier through the actual release-check recipe.""" + recipe = ["bench-release-check", BASELINE, criterion_dir] if suite is None else ["bench-release-check-suite", BASELINE, suite, criterion_dir] return run_command( "just", - ["--no-deps", "--justfile", str(REPO_ROOT / "justfile"), "--working-directory", str(root), "bench-release-check", BASELINE], + ["--no-deps", "--justfile", str(REPO_ROOT / "justfile"), "--working-directory", str(root), *recipe], env={**os.environ, "UV_NO_SYNC": "1", "UV_PROJECT_ENVIRONMENT": sys.prefix}, check=False, ) @@ -94,7 +97,7 @@ def shell_step(name: str, source: Path = WORKFLOW) -> str: return textwrap.dedent("\n".join(lines)).rstrip() -def run_shell(script: str, cwd: Path, extra_env: dict[str, str]) -> subprocess.CompletedProcess[str]: +def run_shell(script: str, cwd: Path, extra_env: dict[str, str], *, timeout: int = 30) -> subprocess.CompletedProcess[str]: bash = shutil.which("bash") assert bash is not None return subprocess.run( # noqa: S603 - run only checked-in workflow shell in an isolated fixture. @@ -104,41 +107,141 @@ def run_shell(script: str, cwd: Path, extra_env: dict[str, str]) -> subprocess.C capture_output=True, encoding="utf-8", check=False, - timeout=30, + timeout=timeout, ) +def installed_runner() -> str: + """Keep workflow package assertions while using the installed locked CLI.""" + command = f"{shlex.quote(sys.executable)} -I -X utf8 -m research_repo_tools" + return f'uvx() {{ [[ "$1 $2 $3 $4 $5" == "--python 3.15 --from {TOOLING_REQUIREMENT} research-repo-tools" ]] || return 90; shift 5; {command} "$@"; }}\n' + + +def package_suites(root: Path, manifest: Path, criterion: Path) -> None: + """Execute the workflow's installed CLI with all independently required cases.""" + destination = root / "target/release-suites" + destination.mkdir(parents=True) + for suite, ids in IDS.items(): + shard = root / "shards" / suite + output = shard / "target/criterion" + output.mkdir(parents=True) + (shard / "tooling").mkdir() + shutil.copyfile(REPO_ROOT / "tooling/criterion-saved.toml", shard / "tooling/criterion-saved.toml") + shutil.copyfile(manifest, shard / "target/release-benchmark-inventory.json") + host = HostMetadata("fixture OS", "fixture architecture", f"CPU for {suite}", 2, 4, None) + (shard / "target/release-host.json").write_bytes(serialize_host(host)) + for event, timestamp in (("started", "100"), ("finished", "200")): + (shard / f"target/{suite}-{event}").write_text(timestamp, encoding="utf-8", newline="\n") + for benchmark in ids: + storage = STORAGE[benchmark] + shutil.copytree(criterion / storage, output / storage) + # Substitute only native version/hash observations. The real uvx command + # installs the exact published suite package independently of root tooling. + probes = ( + f'rustc() {{ printf "rustc fixture\\nLLVM fixture\\n"; }}\ncargo() {{ echo "cargo fixture"; }}\nsha256sum() {{ echo "{"f" * 64} Cargo.lock"; }}\n' + ) + result = run_shell( + probes + shell_step("Package Criterion suite"), + shard, + {**SUITE_ENV, "BENCH_SUITE": suite, "RUNNER_NAME": suite, "ImageOS": "ubuntu24", "ImageVersion": "fixture-image"}, + timeout=180, + ) + assert result.returncode == 0, result.stdout + result.stderr + shutil.move(shard / "target/release-suites" / suite, destination / suite) + + def test_complete_dataset_round_trips_through_workflow_archive(dataset: tuple[Path, Path], tmp_path: Path) -> None: - criterion, _manifest = dataset - verified = verify_saved(tmp_path) - assert verified.returncode == 0, verified.stdout + verified.stderr + criterion, manifest = dataset + criterion = criterion.rename(tmp_path / "original-criterion") + package_suites(tmp_path, manifest, criterion) + for suite, ids in IDS.items(): + verified = verify_saved(tmp_path, suite=suite, criterion_dir=f"shards/{suite}/target/criterion") + assert verified.returncode == 0, verified.stdout + verified.stderr + assert f"Verified {len(ids)} complete saved Criterion benchmarks" in verified.stdout + # Assembly must use the downloaded inventory, not an accidentally retained + # fixture copy or an inventory inferred from the supplied archives. + manifest.unlink() output = tmp_path / "step outputs" - # Git Bash accepts forward-slash drive paths on Windows as well as POSIX paths. - result = run_shell( - shell_step("Package release Criterion baseline"), - tmp_path, - {"RELEASE_TAG": BASELINE, "GITHUB_OUTPUT": output.as_posix(), "GITHUB_RUN_ID": "123", "GITHUB_RUN_ATTEMPT": "2"}, - ) - assert result.returncode == 0, result.stderr asset = f"la-stack-{BASELINE}-criterion-baseline.tar.gz" + script = shell_step("Assemble release Criterion baseline") + env = {**SUITE_ENV, "GITHUB_WORKSPACE": tmp_path.as_posix(), "GITHUB_OUTPUT": output.as_posix()} + for assertion in ({"GITHUB_SHA": "b" * 40}, {"GITHUB_RUN_ATTEMPT": "3"}): + rejected = run_shell(script, tmp_path, {**env, **assertion}, timeout=180) + assert rejected.returncode != 0 + assert "source revision or collection session differs" in rejected.stderr + assert not (tmp_path / asset).exists() + assert not output.exists() + # Git Bash accepts forward-slash drive paths on Windows as well as POSIX paths. + result = run_shell(script, tmp_path, env, timeout=180) + assert result.returncode == 0, result.stdout + result.stderr assert output.read_text(encoding="utf-8") == f"asset={asset}\nartifact-name=bench-baseline-{BASELINE}-123-2\n" - with tarfile.open(tmp_path / asset) as archive: + assert_archive_contents(tmp_path / asset, criterion) + # Release readers keep the standard tooling environment. Exercise its + # documented extraction and full-dataset verification against the new archive. + run_command( + "uv", + ["run", "--locked", "--no-sync", "research-repo-tools", "performance", "extract", str(tmp_path / asset), str(tmp_path / "restored-criterion")], + cwd=REPO_ROOT, + ) + verified = verify_saved(tmp_path, criterion_dir="restored-criterion") + assert verified.returncode == 0, verified.stdout + verified.stderr + assert f"Verified {sum(map(len, IDS.values()))} complete saved Criterion benchmarks" in verified.stdout + + +def assert_archive_contents(archive_path: Path, criterion: Path) -> None: + """Require every raw byte and original runner observation in the final asset.""" + with tarfile.open(archive_path) as archive: for ids in IDS.values(): for benchmark in ids: for sample in ("new", BASELINE): for filename in RAW_FILES: - member = archive.extractfile(f"criterion/{STORAGE.get(benchmark, benchmark)}/{sample}/{filename}") + member = archive.extractfile(f"{STORAGE.get(benchmark, benchmark)}/{sample}/{filename}") assert member is not None assert member.read() == (criterion / STORAGE.get(benchmark, benchmark) / sample / filename).read_bytes() - member = archive.extractfile("criterion/release-benchmark-inventory.json") + member = archive.extractfile("criterion-inventory.json") assert member is not None assert json.loads(member.read())["suites"] == IDS - - -def test_saved_baseline_must_match_fresh_samples(dataset: tuple[Path, Path]) -> None: + for index, suite in enumerate(sorted(IDS)): + member = archive.extractfile(f"release-suites/{index}.json") + assert member is not None + retained = member.read() + record = json.loads(retained) + assert record["suite"] == suite + run = record["run"] + assert run["provenance"]["revision"] == SUITE_ENV["GITHUB_SHA"] + assert run["session"] == "owner/repo:123:2" + assert (run["started_at"], run["finished_at"]) == (100, 200) + assert run["host"]["cpu"] == f"CPU for {suite}" + context = run["provenance"]["context"] + assert context["command"] == f"just bench-save-baseline {BASELINE} {suite}" + assert context["features"] == ("bench,exact" if suite == "public-exact" else "bench") + assert context["runner"] == suite + assert context["rustc"] == "rustc fixture;LLVM fixture" + assert context["cargo"] == "cargo fixture" + assert context["cargo_lock_sha256"] == "f" * 64 + assert (context["image"], context["image_version"]) == ("ubuntu24", "fixture-image") + with tarfile.open(archive_path.parent / "target/release-suites" / suite / "criterion.tar.gz") as original: + original_manifest = original.extractfile("suite.json") + assert original_manifest is not None + assert retained == original_manifest.read() + + +@pytest.mark.parametrize( + "benchmark", + [ + "d2/la_stack_dot", + "column_basis/8x14/near_dependent", + "interval_solve_d6/uncertain_dense", + "polynomial/coincident_lifted/order8/determinant", + "rational_dot/large/d22/slice", + ], +) +def test_saved_baseline_must_match_fresh_samples(dataset: tuple[Path, Path], benchmark: str) -> None: criterion, _manifest = dataset - write_json(criterion / "d2/la_stack_dot" / BASELINE / "sample.json", {"iters": [1.0] * 100, "times": [3.0] * 100}) - assert verify_saved(criterion.parent.parent).returncode != 0 + write_json(criterion / STORAGE[benchmark] / BASELINE / "sample.json", {"iters": [1.0] * 100, "times": [3.0] * 100}) + result = verify_saved(criterion.parent.parent) + assert result.returncode != 0 + assert "saved baseline differs from fresh measurement" in result.stderr @pytest.mark.parametrize("relocation", ["moved", "swapped"]) @@ -175,12 +278,25 @@ def assert_scientific_inventory(root: Path) -> None: def test_configured_inventory_includes_public_workloads_without_peers() -> None: assert_scientific_inventory(REPO_ROOT) - assert len(REQUIRED) == 1254 + assert len(CATALOG["release"]["default"]) == 736 + assert len(CATALOG["release"]["exact"]) == 667 + assert len(REQUIRED) == 1403 assert not any("/nalgebra_" in name or "/faer_" in name for name in REQUIRED) @pytest.mark.parametrize("declaration", ["criterion-inventory.toml", "criterion-saved.toml", "both"]) -@pytest.mark.parametrize("removed", ["d3/la_stack_norm2_sq", "public_d4/interval_det", "canonical_conversion_dyadic_d3/strict_array"]) +@pytest.mark.parametrize( + "removed", + [ + "d3/la_stack_norm2_sq", + "public_d4/interval_det", + "canonical_conversion_dyadic_d3/strict_array", + "column_basis/8x14/near_dependent", + "interval_solve_d6/uncertain_dense", + "polynomial/coincident_lifted/order8/determinant", + "rational_dot/large/d22/slice", + ], +) def test_scientific_inventory_detects_a_nonsentinel_omission(tmp_path: Path, declaration: str, removed: str) -> None: tooling = tmp_path / "tooling" tooling.mkdir() @@ -207,23 +323,27 @@ def test_workflow_requires_preflight_and_isolates_publication_permissions() -> N assert "cache: false" in preparation steps = [ "Prepare release benchmarks", - "Save default-feature Criterion baseline", - "Save exact Criterion baseline", - "Validate complete release dataset", - "Package release Criterion baseline", + "Capture runner host", + "Save Criterion suite", + "Package Criterion suite", + "Upload temporary suite artifact", + "Assemble release Criterion baseline", "Upload temporary baseline artifact", ] positions = [workflow.index(f"- name: {step}") for step in steps] assert positions == sorted(positions) - assert "\n if:" not in workflow[positions[0] : positions[-1]] + for step in steps: + body = workflow.split(f" - name: {step}\n", 1)[1].split("\n - ", 1)[0] + assert "\n if:" not in body assert "continue-on-error" not in workflow assert "workflow_dispatch:" in workflow assert "required: true" in workflow assert "\n release:" not in workflow assert "group: release-benchmarks-${{ inputs.tag }}" in workflow assert "cancel-in-progress: false" in workflow - preflight, remainder = workflow.split(" release-baseline:\n", 1) - producer, publisher = remainder.split(" publish-baseline:\n", 1) + preflight, remainder = workflow.split(" measure-baseline:\n", 1) + producer, remainder = remainder.split(" release-baseline:\n", 1) + _assembler, publisher = remainder.split(" publish-baseline:\n", 1) assert "needs: validate-release" in producer assert "validated-attempt: ${{ github.run_attempt }}" in preflight assert "validated-attempt: ${{ needs.validate-release.outputs.validated-attempt }}" in producer @@ -245,14 +365,47 @@ def test_workflow_requires_preflight_and_isolates_publication_permissions() -> N assert "GH_REPO: ${{ github.repository }}" in publisher assert "contents: write" in publisher assert "name: ${{ needs.release-baseline.outputs.artifact-name }}" in publisher + assert "performance host --output target/release-host.json" in producer + assert "--host target/release-host.json" in producer + assert "path: target/release-suites/${{ matrix.suite }}" in producer + assert "tar -" not in workflow + assert "tooling.release_benchmarks" not in (REPO_ROOT / "justfile").read_text(encoding="utf-8") + + +def test_parallel_suites_require_both_current_attempts_before_assembly() -> None: + workflow = WORKFLOW.read_text(encoding="utf-8") + producer, remainder = workflow.split(" measure-baseline:\n", 1)[1].split(" release-baseline:\n", 1) + assembler = remainder.split(" publish-baseline:\n", 1)[0] + assert "fail-fast: false" in producer + assert "suite: [public-default, public-exact]" in producer + assert "max-parallel: 1" not in producer + assert "BENCH_SUITE: ${{ matrix.suite }}" in producer + assert 'just bench-save-baseline "$RELEASE_TAG" "$BENCH_SUITE"' in producer + assert "performance pack-suite" in producer + assert 'tooling/criterion-saved.toml "$BENCH_SUITE" --saved-sample "$RELEASE_TAG"' in producer + assert "ref: ${{ github.sha }}" in assembler + assert "persist-credentials: false" in assembler + assert "contents: write" not in assembler + assert "needs: [validate-release, measure-baseline]" in assembler + assert "always()" not in assembler + for suite in IDS: + assert f"name: bench-suite-${{{{ inputs.tag }}}}-${{{{ github.run_id }}}}-${{{{ github.run_attempt }}}}-{suite}" in assembler + assert f"path: target/release-suites/{suite}" in assembler + assert f"target/release-suites/{suite}/criterion.tar.gz" in assembler + assert "performance assemble-suites tooling/criterion-saved.toml" in assembler + for job in (producer, assembler): + assert '--session "$GITHUB_REPOSITORY:$GITHUB_RUN_ID:$GITHUB_RUN_ATTEMPT"' in job + assert '--revision "$GITHUB_SHA"' in job + for job in (producer, assembler): + assert f"--from {TOOLING_REQUIREMENT}" in job def test_setup_limits_preserve_benchmark_and_tail_budgets() -> None: workflow = WORKFLOW.read_text(encoding="utf-8") - producer = workflow.split(" release-baseline:\n", 1)[1].split(" publish-baseline:\n", 1)[0] + producer = workflow.split(" measure-baseline:\n", 1)[1].split(" release-baseline:\n", 1)[0] job_limit = re.search(r"(?m)^ timeout-minutes: (\d+)$", producer) assert job_limit is not None - setup = producer.split(" steps:\n", 1)[1].split(" - name: Save default-feature Criterion baseline", 1)[0] + setup = producer.split(" steps:\n", 1)[1].split(" - name: Save Criterion suite", 1)[0] setup_steps = re.findall(r"(?ms)^ - .*?(?=^ - |\Z)", setup) assert setup_steps setup_limits: list[int] = [] @@ -265,25 +418,28 @@ def test_setup_limits_preserve_benchmark_and_tail_budgets() -> None: discovery_minutes = sum(suite.timeout for suite in discovery.suites) / 60 # The failed hosted run spent 27 minutes on uncached tool installation. # Reserve 35 for tools, each declared cold discovery deadline, and 5 for inputs. - assert setup_limits[-1] >= 35 + discovery_minutes + 5 + preparation = re.search(r"(?ms)^ - name: Prepare release benchmarks\n.*?^ timeout-minutes: (\d+)$", setup) + assert preparation is not None + assert int(preparation[1]) >= 35 + discovery_minutes + 5 assert "uses: ./.github/actions/prepare-release-benchmarks" in setup assert "using: composite" in PREPARATION.read_text(encoding="utf-8") - benchmark_limits = [ - int(limit) - for limit in re.findall(r"(?ms)^ - name: Save (?:default-feature|exact) Criterion baseline\n.*?^ timeout-minutes: (\d+)$", producer) - ] - assert benchmark_limits == [140, 130] - assert int(job_limit[1]) - sum(setup_limits) - sum(benchmark_limits) >= 15 + benchmark_limit = re.search(r"(?ms)^ - name: Save Criterion suite\n.*?^ timeout-minutes: (\d+)$", producer) + assert benchmark_limit is not None + assert int(benchmark_limit[1]) == 240 + for suite in ("default", "exact"): + # Keep at least 50% headroom beyond the documented planning rate. + assert int(benchmark_limit[1]) * 60 >= len(CATALOG["release"][suite]) * 12 * 1.5 + assert int(job_limit[1]) - sum(setup_limits) - int(benchmark_limit[1]) >= 20 assert int(job_limit[1]) <= 360 -@pytest.mark.parametrize(("suite", "step"), [("public-default", "Save default-feature Criterion baseline"), ("public-exact", "Save exact Criterion baseline")]) -def test_failed_suite_retains_timing_and_does_not_mark_completion(tmp_path: Path, suite: str, step: str) -> None: +@pytest.mark.parametrize("suite", ["public-default", "public-exact"]) +def test_failed_suite_retains_timing_and_does_not_mark_completion(tmp_path: Path, suite: str) -> None: (tmp_path / "target").mkdir() # A shell function substitutes only the benchmark command; timestamps and # failure handling execute exactly as checked into the workflow. - script = "just() { return 7; }\n" + shell_step(step) - result = run_shell(script, tmp_path, {"RELEASE_TAG": BASELINE}) + script = "just() { return 7; }\n" + shell_step("Save Criterion suite") + result = run_shell(script, tmp_path, {"RELEASE_TAG": BASELINE, "BENCH_SUITE": suite}) assert result.returncode == 7 assert (tmp_path / f"target/{suite}-started").is_file() assert not (tmp_path / f"target/{suite}-finished").exists() @@ -293,13 +449,13 @@ def test_failed_suite_retains_timing_and_does_not_mark_completion(tmp_path: Path tmp_path, { "GITHUB_STEP_SUMMARY": summary.as_posix(), - "COMPARATIVE_OUTCOME": "failure", - "EXACT_OUTCOME": "skipped", + "BENCH_SUITE": suite, + "SUITE_OUTCOME": "failure", }, ) assert result.returncode == 0, result.stderr - assert "| public-default | failure |" in summary.read_text(encoding="utf-8") - assert "| public-exact | skipped |" in summary.read_text(encoding="utf-8") + assert f"| {suite} | failure |" in summary.read_text(encoding="utf-8") + assert "| 240 |" in summary.read_text(encoding="utf-8") @pytest.mark.parametrize("validated_attempt", ["", "1", "2"]) @@ -388,11 +544,7 @@ def test_installed_cli_binds_trusted_target_and_leaves_draft_for_review(tmp_path } # Substitute only uvx's environment acquisition; retain its exact package # arguments and execute the already locked package in a real child process. - runner = ( - 'uvx() { [[ "$1 $2 $3 $4 $5" == "--python 3.15 --from ' - f'{TOOLING_REQUIREMENT} research-repo-tools" ]] || return 90; ' - f'shift 5; {shlex.quote(sys.executable)} -m research_repo_tools "$@"; }}\n' - ) + runner = installed_runner() result = run_shell(runner + shell_step("Validate draft release target"), fixture, env) assert result.returncode == 0, result.stderr target = output.read_text(encoding="utf-8").strip().removeprefix("target=") diff --git a/tests/tooling/test_release_publishing.py b/tests/tooling/test_release_publishing.py index 7d657c2..ba05379 100644 --- a/tests/tooling/test_release_publishing.py +++ b/tests/tooling/test_release_publishing.py @@ -95,7 +95,9 @@ def test_each_configured_native_check_is_required(release_event: dict[str, Any], check = next(item for item in release_event["checks"] if item["name"] == name) check["name"] = "unrelated check" assert main(["--root", str(REPO_ROOT), "release", "gate", TAG]) == 1 - assert f"required check {name!r} lacks one unambiguous successful result on {SHA}" in capsys.readouterr().err + diagnostic = capsys.readouterr().err + assert f"Unmet release checks on exact commit {SHA}:" in diagnostic + assert f"required check {name!r} (app ID 15368): missing" in diagnostic @pytest.mark.parametrize("immutable", [True, False, None]) @@ -163,8 +165,9 @@ def test_release_notes_stdout_excludes_setup_diagnostics() -> None: def test_publish_recipe_passes_explicit_approval(monkeypatch: pytest.MonkeyPatch) -> None: requested: list[str] = [] - def publish(settings: config.Config, tag: str) -> None: + def publish(settings: config.Config, tag: str, *, reuse_evidence: object = None) -> None: assert settings.root == REPO_ROOT + assert reuse_evidence is None requested.append(tag) # Exercise the installed argument parser and dispatch, replacing only the diff --git a/tests/tooling/test_shared_tooling.py b/tests/tooling/test_shared_tooling.py new file mode 100644 index 0000000..783fc8c --- /dev/null +++ b/tests/tooling/test_shared_tooling.py @@ -0,0 +1,73 @@ +"""Temporary scanner selection preserves the installed fixture checker's gates.""" + +import json +import os +import sys +import sysconfig +from importlib.metadata import version +from pathlib import Path + +import pytest +from research_repo_tools import semgrep_tool + +from tooling import shared_tooling as adapter + +ROOT = Path(__file__).resolve().parents[2] + + +def test_scanner_setup_uses_standard_environment_and_restores_override(capsys: pytest.CaptureFixture[str]) -> None: + """The shared setup path cannot acquire a scanner in another environment.""" + previous_setup, previous_version, previous_path = semgrep_tool.setup, semgrep_tool.VERSION, os.environ.get("PATH") + assert sys.version_info[:2] == (3, 15) + assert adapter.main(["--root", str(ROOT), "semgrep", "setup"]) == 0 + expected = Path(sysconfig.get_path("scripts")) / ("semgrep.exe" if os.name == "nt" else "semgrep") + output = capsys.readouterr().out + assert f"Semgrep {version('semgrep')}: {expected}" in output + assert semgrep_tool.setup is previous_setup + assert previous_version == semgrep_tool.VERSION + assert os.environ.get("PATH") == previous_path + + +@pytest.mark.parametrize( + ("source", "diagnostic"), + [("print('blocked')", ""), ("print('allowed')", "expected finding not reported"), ("print('blocked')\nprint('blocked')", "unexpected finding")], +) +def test_real_fixture_checker_keeps_positive_negative_gates( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, source: str, diagnostic: str, capsys: pytest.CaptureFixture[str] +) -> None: + """Actual native scans accept a match and reject missing or extra findings.""" + (tmp_path / "pyproject.toml").write_text( + '[tool.research-repo-tools.semgrep]\nconfig = "rules.yaml"\nfixtures = "fixture.py"\nnamespace = "la-stack."\ncwd = "."\n', + encoding="utf-8", + ) + (tmp_path / "rules.yaml").write_text( + json.dumps( + {"rules": [{"id": "la-stack.fixture", "languages": ["python"], "message": "fixture violation", "severity": "ERROR", "pattern": "print('blocked')"}]} + ), + encoding="utf-8", + ) + (tmp_path / "fixture.py").write_text(f"# ruleid: la-stack.fixture\n{source}\n# ok: la-stack.fixture\nprint('allowed')\n", encoding="utf-8") + # A broken ambient scanner must not be selected by either native launcher. + ambient = tmp_path / "ambient" + ambient.mkdir() + for name in ("semgrep", "pysemgrep"): + binary = ambient / name + binary.write_text("#!/bin/sh\nexit 97\n", encoding="utf-8") + binary.chmod(0o755) + monkeypatch.setenv("PATH", os.pathsep.join([str(ambient), os.environ["PATH"]])) + previous_path = os.environ["PATH"] + assert adapter.main(["--root", str(tmp_path), "semgrep", "check-fixtures"]) == (1 if diagnostic else 0) + errors = capsys.readouterr().err + if diagnostic: + assert "la-stack.fixture" in errors + assert diagnostic in errors + else: + assert not errors + assert os.environ["PATH"] == previous_path + + +def test_override_requires_review_when_shared_package_advances(monkeypatch: pytest.MonkeyPatch) -> None: + """Do not silently keep the temporary internal override after its fixed release.""" + monkeypatch.setattr(adapter, "version", lambda _name: "0.2.5") + with pytest.raises(RuntimeError, match="la-stack#317"): + adapter.main(["semgrep", "setup"]) diff --git a/tests/tooling/test_toolchain_integration.py b/tests/tooling/test_toolchain_integration.py index 5423bba..65340f0 100644 --- a/tests/tooling/test_toolchain_integration.py +++ b/tests/tooling/test_toolchain_integration.py @@ -25,7 +25,7 @@ UPDATER = ["run", "--locked", "--only-group", "tooling", "--inexact", "research-repo-tools"] UV_UPDATE = ["run", "--no-config", "--no-sync", "--no-python-downloads", "research-repo-tools", "deps", "update-uv"] TOOL_UPDATE = [*UPDATER, "toolchain", "upgrade"] -SETUP = ["run", "--locked", "--managed-python", "--only-group", "tooling", "research-repo-tools", "setup"] +SETUP = ["run", "--locked", "--managed-python", "--only-group", "tooling", "python", "tooling/shared_tooling.py", "setup"] CARGO_UPDATE = [ [*UPDATER, "toolchain", "run", "--", "cargo", "upgrade", "--incompatible", "allow", "--exclude", "num-bigint", "--exclude", "num-rational"], [*UPDATER, "toolchain", "run", "--", "cargo", "update"], @@ -47,7 +47,7 @@ def test_published_tooling_package_matches_manifest_lock_and_installation() -> None: """Keep the exact published-package contract in one consumer test.""" - expected_version = "0.2.3" + expected_version = "0.2.4" manifest = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) assert manifest["project"]["dependencies"] == [] assert manifest["tool"]["uv"]["package"] is False @@ -61,8 +61,8 @@ def test_published_tooling_package_matches_manifest_lock_and_installation() -> N package = next(item for item in lock["package"] if item["name"] == "research-repo-tools") assert package["version"] == expected_version assert package["source"] == {"registry": "https://pypi.org/simple"} - assert package["wheels"][0]["hash"] == "sha256:da5f0259027662e7706b7f2b8a38d8e1f1d8a5dce314e2c996177b2947514fc6" - assert package["sdist"]["hash"] == "sha256:13700b174656ffad265ecb926dd66013949853b7a39c088ab6d3852aba72df95" + assert package["wheels"][0]["hash"] == "sha256:00aaab2b7efd680d79324c6debb64db4d8d1ff076a460ff46e24e0ef1c24bd7d" + assert package["sdist"]["hash"] == "sha256:30b6d4d2f8d0cb98d5311656928dd65800088b651dd6c0c29f9a02526e9e902a" locked = {item["name"]: item["version"] for item in lock["package"]} for name, expected in shared_tool_versions().items(): assert locked[name] == version(name) == expected diff --git a/tooling/README.md b/tooling/README.md index 52653d2..e61df71 100644 --- a/tooling/README.md +++ b/tooling/README.md @@ -41,17 +41,29 @@ fixture keeps the real Just, uv and Python launch and substitutes only the entry point's provider responses. It checks the exact received identity and rejects an explicitly empty pin rather than selecting fresh evidence. -Semgrep shares the root Python 3.15 environment and lockfile. Preserve its +Semgrep scans and fixture checks share the root Python 3.15 environment and lockfile. Preserve its native-wheel requirement and compatibility overrides in `pyproject.toml`. Local scans, fixture checks and SARIF retain the Linux stack-limit workaround for [Semgrep #11979](https://github.com/semgrep/semgrep/issues/11979). Ordinary Rust and Python tests remain scan targets; deliberate `tests/semgrep/` fixtures are checked separately by `just semgrep-test`. +Published v0.2.4 hard-codes Python 3.14 for those fixture checks. The temporary +`shared_tooling.py` adapter selects the scanner installed beside the active +interpreter and delegates all fixture validation to the shared CLI. It also puts +that environment's scripts first on `PATH` for the native scanner launcher. +Setup uses the same adapter so it does not provision a separate scanner runtime. +The shared Python baseline must apply to Semgrep too; the fix is tracked in +[research-repo-tools#165](https://github.com/acgetchell/research-repo-tools/issues/165) +for v0.2.5. Remove the version-guarded override through +[#317](https://github.com/acgetchell/la-stack/issues/317) after adopting that fix. ## Shared capability adoption -The capability inventory below was checked against the installed v0.2.3 CLI and -its [versioned contracts](https://github.com/acgetchell/research-repo-tools/blob/v0.2.3/README.md#common-workflows). +The capability inventory below uses the installed v0.2.4 CLI and +its [versioned contracts](https://github.com/acgetchell/research-repo-tools/blob/v0.2.4/README.md#common-workflows). +Trusted release jobs use the same exact package pin through `uvx --python 3.15`. +The shared updater installs and verifies candidate Cargo tools before publishing +new pins; no local updater implementation is needed. Native Rust algorithms, assertions, benchmark selection and release decisions remain owned here. @@ -61,9 +73,9 @@ remain owned here. | Changelog and release | Shared generation, archives, metadata rules, registry gates and verification; the Cargo adapter reuses signed native/package evidence | | CI environment and toolchain | Shared setup/check/run/export and verified cached installations; `tools-clean` previews obsolete managed tools separately from Cargo `clean` | | Coverage | Native `cargo llvm-cov` acquisition; `coverage-report` summarizes its existing Cobertura output | -| Dependencies | Shared uv/tool upgrades and `deps update-python --all-projects`; all Python tools share the root Python 3.15 environment and lock | +| Dependencies | Shared uv/tool upgrades and `deps update-python --all-projects`; consumer Python dependencies share the root environment and lock | | Documentation and files | Shared tracked/nonignored selection and bounded argument batches for formatting, lint, spelling and syntax checks; one active Markdown policy preserves archives | -| Performance | Declarative discovery, measurement, saved-sample validation, immutable evidence, offline rendering, report/README publication and release assets | +| Performance | Declarative discovery, measurement, suite verification/packaging/assembly, immutable evidence, offline rendering, report/README publication and release assets | | Python | Shared complete-inventory check/fix/typecheck with repository Ruff exceptions and strict ty diagnostics | | Review | Shared opt-in CodeRabbit branch and uncommitted review | | Security and workflow audit | Shared OSV, full-history Gitleaks and zizmor policy; consumer advisory exceptions and workflow permissions remain local | @@ -91,8 +103,8 @@ validation and the updater. Workflows retain full commit pins. This source polic is separate from the historical GitHub settings payloads under `.github/settings/`, which also constrain the publishing action to a specific commit. Reconcile live settings before deploying a pin update; the local updater does not change them. -The v0.2.3 Actions parser accepts workflow documents only. Semgrep rules -check composite actions under `.github/actions/`, including their +The allowlist recipe remains scoped to workflows. Semgrep rules check +composite actions under `.github/actions/`, including their external dependencies and commit pins. ## Performance ownership @@ -121,10 +133,12 @@ There is no runtime configuration generator. Both labels of a new measurement must be v0.5.0 or newer. The full and non-exact declarations require 100 samples, mean and median estimates, -95% intervals and nanoseconds. Both native correctness gates run before -timing. Reference libraries originate in the baseline phase. +95% intervals and nanoseconds. Each phase runs its native correctness gate before +timing. The full `gate-public` adds column bases, rational dot products, interval +solve oracles and polynomial coefficient checks to the diagnostic `gate`. +Reference libraries originate in the baseline phase. -The full #301 policy measures all public la-stack workflows at both revisions, +The expanded #301 policy measures all public la-stack workflows at both revisions, plus only 16 baseline-phase peer LU-factorization-and-solve cases. Hosted acquisition omits peers entirely. The full scratch/retained plot selections therefore expose only LU solve; broad peer plots remain in the explicitly @@ -167,6 +181,11 @@ report and plot rendering after scratch cleanup. Shared implementation matrices live upstream, including `tests/performance/public_complete_consumer.py` and `public_criterion_consumer.py`, exercised from wheel and sdist installations. +Published v0.2.4 also owns `public_suites_consumer.py`: suite selection, raw-file +digests, archive paths, original host retention and complete assembly failures. +The release workflow calls `performance pack-suite` and `assemble-suites` +directly; `bench-release-check-suite` is a diagnostic wrapper around +`performance verify-saved --suite`. No local suite verifier or merge engine remains. Consumer tests retain independent scientific selections, actual recipe wiring, saved archive round trips, publication boundaries and historical hashes. diff --git a/tooling/criterion-inventory.toml b/tooling/criterion-inventory.toml index 52bf3a2..d53a0ce 100644 --- a/tooling/criterion-inventory.toml +++ b/tooling/criterion-inventory.toml @@ -297,6 +297,61 @@ required = [ "canonical_conversion_wide256_d5/rounded_result", "canonical_conversion_wide256_d5/strict_array", "canonical_conversion_wide256_d5/strict_result", + "column_basis/2x2/full_rank", + "column_basis/2x2/large", + "column_basis/2x2/near_dependent", + "column_basis/2x2/rank_one", + "column_basis/2x2/small", + "column_basis/2x3/shared_face", + "column_basis/3x3/full_rank", + "column_basis/3x3/large", + "column_basis/3x3/near_dependent", + "column_basis/3x3/rank_one", + "column_basis/3x3/small", + "column_basis/3x4/shared_face", + "column_basis/4x4/full_rank", + "column_basis/4x4/large", + "column_basis/4x4/near_dependent", + "column_basis/4x4/rank_one", + "column_basis/4x4/small", + "column_basis/4x5/shared_face", + "column_basis/4x6/full_rank", + "column_basis/4x6/large", + "column_basis/4x6/near_dependent", + "column_basis/4x6/rank_one", + "column_basis/4x6/small", + "column_basis/5x2/full_rank", + "column_basis/5x2/large", + "column_basis/5x2/near_dependent", + "column_basis/5x2/rank_one", + "column_basis/5x2/small", + "column_basis/5x5/full_rank", + "column_basis/5x5/large", + "column_basis/5x5/near_dependent", + "column_basis/5x5/rank_one", + "column_basis/5x5/small", + "column_basis/5x6/shared_face", + "column_basis/5x8/full_rank", + "column_basis/5x8/large", + "column_basis/5x8/near_dependent", + "column_basis/5x8/rank_one", + "column_basis/5x8/small", + "column_basis/6x10/full_rank", + "column_basis/6x10/large", + "column_basis/6x10/near_dependent", + "column_basis/6x10/rank_one", + "column_basis/6x10/small", + "column_basis/6x7/shared_face", + "column_basis/7x12/full_rank", + "column_basis/7x12/large", + "column_basis/7x12/near_dependent", + "column_basis/7x12/rank_one", + "column_basis/7x12/small", + "column_basis/8x14/full_rank", + "column_basis/8x14/large", + "column_basis/8x14/near_dependent", + "column_basis/8x14/rank_one", + "column_basis/8x14/small", "d16/la_stack_det", "d16/la_stack_det_from_ldlt", "d16/la_stack_det_from_lu", @@ -611,6 +666,11 @@ required = [ "interval_det_sign/d4_conclusive_lifted", "interval_det_sign/d4_inconclusive_lifted", "interval_det_sign/d7_conclusive_lifted", + "interval_division/negative", + "interval_division/positive", + "interval_division/reciprocal", + "interval_division/subnormal", + "interval_division/tiny_ratio", "interval_scalar/cancellation/add", "interval_scalar/cancellation/multiply", "interval_scalar/cancellation/square", @@ -626,6 +686,36 @@ required = [ "interval_scalar/zero/add", "interval_scalar/zero/multiply", "interval_scalar/zero/square", + "interval_solve_d1/dense", + "interval_solve_d1/near_singular", + "interval_solve_d1/range_exhausted", + "interval_solve_d1/singular", + "interval_solve_d1/uncertain_dense", + "interval_solve_d2/dense", + "interval_solve_d2/near_singular", + "interval_solve_d2/range_exhausted", + "interval_solve_d2/singular", + "interval_solve_d2/uncertain_dense", + "interval_solve_d3/dense", + "interval_solve_d3/near_singular", + "interval_solve_d3/range_exhausted", + "interval_solve_d3/singular", + "interval_solve_d3/uncertain_dense", + "interval_solve_d4/dense", + "interval_solve_d4/near_singular", + "interval_solve_d4/range_exhausted", + "interval_solve_d4/singular", + "interval_solve_d4/uncertain_dense", + "interval_solve_d5/dense", + "interval_solve_d5/near_singular", + "interval_solve_d5/range_exhausted", + "interval_solve_d5/singular", + "interval_solve_d5/uncertain_dense", + "interval_solve_d6/dense", + "interval_solve_d6/near_singular", + "interval_solve_d6/range_exhausted", + "interval_solve_d6/singular", + "interval_solve_d6/uncertain_dense", "linear_form_d2_batch/origin/construction", "linear_form_d2_batch/origin/prepared", "linear_form_d2_batch/origin/with_construction", @@ -766,6 +856,11 @@ required = [ "linear_form_d6_prepared/underflow/difference_endpoints", "linear_form_d6_prepared/underflow/dot", "linear_form_d6_prepared/underflow/dot_endpoints", + "polynomial/coincident_lifted/order4/determinant", + "polynomial/coincident_lifted/order5/determinant", + "polynomial/coincident_lifted/order6/determinant", + "polynomial/coincident_lifted/order7/determinant", + "polynomial/coincident_lifted/order8/determinant", "public_d2/det_bound", "public_d2/det_direct", "public_d2/det_with_bound", @@ -850,12 +945,6 @@ required = [ "public_d5/vector_access", "public_d5/vector_new", "public_d5/vector_zero", - "public_scalar/tolerance", - "public_scalar/interval_new", - "public_scalar/interval_point", - "public_scalar/interval_subtraction", - "public_scalar/interval_access", - "public_scalar/interval_negate", "public_exact_d2/matrix_access", "public_exact_d2/matrix_from_fn", "public_exact_d2/matrix_from_rows", @@ -898,6 +987,66 @@ required = [ "public_exact_d5/vector_zero", "public_exact_scalar/rounded", "public_exact_scalar/strict", + "public_scalar/interval_access", + "public_scalar/interval_negate", + "public_scalar/interval_new", + "public_scalar/interval_point", + "public_scalar/interval_subtraction", + "public_scalar/tolerance", + "rational_dot/cancellation/d14/prepared", + "rational_dot/cancellation/d14/slice", + "rational_dot/cancellation/d2/prepared", + "rational_dot/cancellation/d2/slice", + "rational_dot/cancellation/d22/prepared", + "rational_dot/cancellation/d22/slice", + "rational_dot/cancellation/d3/prepared", + "rational_dot/cancellation/d3/slice", + "rational_dot/cancellation/d4/prepared", + "rational_dot/cancellation/d4/slice", + "rational_dot/cancellation/d5/prepared", + "rational_dot/cancellation/d5/slice", + "rational_dot/cancellation/d6/prepared", + "rational_dot/cancellation/d6/slice", + "rational_dot/cancellation/d7/prepared", + "rational_dot/cancellation/d7/slice", + "rational_dot/cancellation/d8/prepared", + "rational_dot/cancellation/d8/slice", + "rational_dot/large/d14/prepared", + "rational_dot/large/d14/slice", + "rational_dot/large/d2/prepared", + "rational_dot/large/d2/slice", + "rational_dot/large/d22/prepared", + "rational_dot/large/d22/slice", + "rational_dot/large/d3/prepared", + "rational_dot/large/d3/slice", + "rational_dot/large/d4/prepared", + "rational_dot/large/d4/slice", + "rational_dot/large/d5/prepared", + "rational_dot/large/d5/slice", + "rational_dot/large/d6/prepared", + "rational_dot/large/d6/slice", + "rational_dot/large/d7/prepared", + "rational_dot/large/d7/slice", + "rational_dot/large/d8/prepared", + "rational_dot/large/d8/slice", + "rational_dot/non_dyadic/d14/prepared", + "rational_dot/non_dyadic/d14/slice", + "rational_dot/non_dyadic/d2/prepared", + "rational_dot/non_dyadic/d2/slice", + "rational_dot/non_dyadic/d22/prepared", + "rational_dot/non_dyadic/d22/slice", + "rational_dot/non_dyadic/d3/prepared", + "rational_dot/non_dyadic/d3/slice", + "rational_dot/non_dyadic/d4/prepared", + "rational_dot/non_dyadic/d4/slice", + "rational_dot/non_dyadic/d5/prepared", + "rational_dot/non_dyadic/d5/slice", + "rational_dot/non_dyadic/d6/prepared", + "rational_dot/non_dyadic/d6/slice", + "rational_dot/non_dyadic/d7/prepared", + "rational_dot/non_dyadic/d7/slice", + "rational_dot/non_dyadic/d8/prepared", + "rational_dot/non_dyadic/d8/slice", "rational_input_d2/det_row_cleared_bareiss", "rational_input_d2/det_sign_row_cleared_bareiss", "rational_input_d2/solve_row_cleared_bareiss", diff --git a/tooling/criterion-saved.toml b/tooling/criterion-saved.toml index bdea80c..f65c536 100644 --- a/tooling/criterion-saved.toml +++ b/tooling/criterion-saved.toml @@ -299,6 +299,61 @@ required = [ "canonical_conversion_wide256_d5/rounded_result", "canonical_conversion_wide256_d5/strict_array", "canonical_conversion_wide256_d5/strict_result", + "column_basis/2x2/full_rank", + "column_basis/2x2/large", + "column_basis/2x2/near_dependent", + "column_basis/2x2/rank_one", + "column_basis/2x2/small", + "column_basis/2x3/shared_face", + "column_basis/3x3/full_rank", + "column_basis/3x3/large", + "column_basis/3x3/near_dependent", + "column_basis/3x3/rank_one", + "column_basis/3x3/small", + "column_basis/3x4/shared_face", + "column_basis/4x4/full_rank", + "column_basis/4x4/large", + "column_basis/4x4/near_dependent", + "column_basis/4x4/rank_one", + "column_basis/4x4/small", + "column_basis/4x5/shared_face", + "column_basis/4x6/full_rank", + "column_basis/4x6/large", + "column_basis/4x6/near_dependent", + "column_basis/4x6/rank_one", + "column_basis/4x6/small", + "column_basis/5x2/full_rank", + "column_basis/5x2/large", + "column_basis/5x2/near_dependent", + "column_basis/5x2/rank_one", + "column_basis/5x2/small", + "column_basis/5x5/full_rank", + "column_basis/5x5/large", + "column_basis/5x5/near_dependent", + "column_basis/5x5/rank_one", + "column_basis/5x5/small", + "column_basis/5x6/shared_face", + "column_basis/5x8/full_rank", + "column_basis/5x8/large", + "column_basis/5x8/near_dependent", + "column_basis/5x8/rank_one", + "column_basis/5x8/small", + "column_basis/6x10/full_rank", + "column_basis/6x10/large", + "column_basis/6x10/near_dependent", + "column_basis/6x10/rank_one", + "column_basis/6x10/small", + "column_basis/6x7/shared_face", + "column_basis/7x12/full_rank", + "column_basis/7x12/large", + "column_basis/7x12/near_dependent", + "column_basis/7x12/rank_one", + "column_basis/7x12/small", + "column_basis/8x14/full_rank", + "column_basis/8x14/large", + "column_basis/8x14/near_dependent", + "column_basis/8x14/rank_one", + "column_basis/8x14/small", "d16/la_stack_det", "d16/la_stack_det_from_ldlt", "d16/la_stack_det_from_lu", @@ -613,6 +668,11 @@ required = [ "interval_det_sign/d4_conclusive_lifted", "interval_det_sign/d4_inconclusive_lifted", "interval_det_sign/d7_conclusive_lifted", + "interval_division/negative", + "interval_division/positive", + "interval_division/reciprocal", + "interval_division/subnormal", + "interval_division/tiny_ratio", "interval_scalar/cancellation/add", "interval_scalar/cancellation/multiply", "interval_scalar/cancellation/square", @@ -628,6 +688,36 @@ required = [ "interval_scalar/zero/add", "interval_scalar/zero/multiply", "interval_scalar/zero/square", + "interval_solve_d1/dense", + "interval_solve_d1/near_singular", + "interval_solve_d1/range_exhausted", + "interval_solve_d1/singular", + "interval_solve_d1/uncertain_dense", + "interval_solve_d2/dense", + "interval_solve_d2/near_singular", + "interval_solve_d2/range_exhausted", + "interval_solve_d2/singular", + "interval_solve_d2/uncertain_dense", + "interval_solve_d3/dense", + "interval_solve_d3/near_singular", + "interval_solve_d3/range_exhausted", + "interval_solve_d3/singular", + "interval_solve_d3/uncertain_dense", + "interval_solve_d4/dense", + "interval_solve_d4/near_singular", + "interval_solve_d4/range_exhausted", + "interval_solve_d4/singular", + "interval_solve_d4/uncertain_dense", + "interval_solve_d5/dense", + "interval_solve_d5/near_singular", + "interval_solve_d5/range_exhausted", + "interval_solve_d5/singular", + "interval_solve_d5/uncertain_dense", + "interval_solve_d6/dense", + "interval_solve_d6/near_singular", + "interval_solve_d6/range_exhausted", + "interval_solve_d6/singular", + "interval_solve_d6/uncertain_dense", "linear_form_d2_batch/origin/construction", "linear_form_d2_batch/origin/prepared", "linear_form_d2_batch/origin/with_construction", @@ -768,6 +858,11 @@ required = [ "linear_form_d6_prepared/underflow/difference_endpoints", "linear_form_d6_prepared/underflow/dot", "linear_form_d6_prepared/underflow/dot_endpoints", + "polynomial/coincident_lifted/order4/determinant", + "polynomial/coincident_lifted/order5/determinant", + "polynomial/coincident_lifted/order6/determinant", + "polynomial/coincident_lifted/order7/determinant", + "polynomial/coincident_lifted/order8/determinant", "public_d2/det_bound", "public_d2/det_direct", "public_d2/det_with_bound", @@ -852,12 +947,6 @@ required = [ "public_d5/vector_access", "public_d5/vector_new", "public_d5/vector_zero", - "public_scalar/tolerance", - "public_scalar/interval_new", - "public_scalar/interval_point", - "public_scalar/interval_subtraction", - "public_scalar/interval_access", - "public_scalar/interval_negate", "public_exact_d2/matrix_access", "public_exact_d2/matrix_from_fn", "public_exact_d2/matrix_from_rows", @@ -900,6 +989,66 @@ required = [ "public_exact_d5/vector_zero", "public_exact_scalar/rounded", "public_exact_scalar/strict", + "public_scalar/interval_access", + "public_scalar/interval_negate", + "public_scalar/interval_new", + "public_scalar/interval_point", + "public_scalar/interval_subtraction", + "public_scalar/tolerance", + "rational_dot/cancellation/d14/prepared", + "rational_dot/cancellation/d14/slice", + "rational_dot/cancellation/d2/prepared", + "rational_dot/cancellation/d2/slice", + "rational_dot/cancellation/d22/prepared", + "rational_dot/cancellation/d22/slice", + "rational_dot/cancellation/d3/prepared", + "rational_dot/cancellation/d3/slice", + "rational_dot/cancellation/d4/prepared", + "rational_dot/cancellation/d4/slice", + "rational_dot/cancellation/d5/prepared", + "rational_dot/cancellation/d5/slice", + "rational_dot/cancellation/d6/prepared", + "rational_dot/cancellation/d6/slice", + "rational_dot/cancellation/d7/prepared", + "rational_dot/cancellation/d7/slice", + "rational_dot/cancellation/d8/prepared", + "rational_dot/cancellation/d8/slice", + "rational_dot/large/d14/prepared", + "rational_dot/large/d14/slice", + "rational_dot/large/d2/prepared", + "rational_dot/large/d2/slice", + "rational_dot/large/d22/prepared", + "rational_dot/large/d22/slice", + "rational_dot/large/d3/prepared", + "rational_dot/large/d3/slice", + "rational_dot/large/d4/prepared", + "rational_dot/large/d4/slice", + "rational_dot/large/d5/prepared", + "rational_dot/large/d5/slice", + "rational_dot/large/d6/prepared", + "rational_dot/large/d6/slice", + "rational_dot/large/d7/prepared", + "rational_dot/large/d7/slice", + "rational_dot/large/d8/prepared", + "rational_dot/large/d8/slice", + "rational_dot/non_dyadic/d14/prepared", + "rational_dot/non_dyadic/d14/slice", + "rational_dot/non_dyadic/d2/prepared", + "rational_dot/non_dyadic/d2/slice", + "rational_dot/non_dyadic/d22/prepared", + "rational_dot/non_dyadic/d22/slice", + "rational_dot/non_dyadic/d3/prepared", + "rational_dot/non_dyadic/d3/slice", + "rational_dot/non_dyadic/d4/prepared", + "rational_dot/non_dyadic/d4/slice", + "rational_dot/non_dyadic/d5/prepared", + "rational_dot/non_dyadic/d5/slice", + "rational_dot/non_dyadic/d6/prepared", + "rational_dot/non_dyadic/d6/slice", + "rational_dot/non_dyadic/d7/prepared", + "rational_dot/non_dyadic/d7/slice", + "rational_dot/non_dyadic/d8/prepared", + "rational_dot/non_dyadic/d8/slice", "rational_input_d2/det_row_cleared_bareiss", "rational_input_d2/det_sign_row_cleared_bareiss", "rational_input_d2/solve_row_cleared_bareiss", @@ -1265,6 +1414,155 @@ confidence-level = 0.95 unit = "ns" [storage-paths] +"column_basis/2x2/full_rank" = "column_basis_2x2_full_rank" +"column_basis/2x2/large" = "column_basis_2x2_large" +"column_basis/2x2/near_dependent" = "column_basis_2x2_near_dependent" +"column_basis/2x2/rank_one" = "column_basis_2x2_rank_one" +"column_basis/2x2/small" = "column_basis_2x2_small" +"column_basis/2x3/shared_face" = "column_basis_2x3_shared_face" +"column_basis/3x3/full_rank" = "column_basis_3x3_full_rank" +"column_basis/3x3/large" = "column_basis_3x3_large" +"column_basis/3x3/near_dependent" = "column_basis_3x3_near_dependent" +"column_basis/3x3/rank_one" = "column_basis_3x3_rank_one" +"column_basis/3x3/small" = "column_basis_3x3_small" +"column_basis/3x4/shared_face" = "column_basis_3x4_shared_face" +"column_basis/4x4/full_rank" = "column_basis_4x4_full_rank" +"column_basis/4x4/large" = "column_basis_4x4_large" +"column_basis/4x4/near_dependent" = "column_basis_4x4_near_dependent" +"column_basis/4x4/rank_one" = "column_basis_4x4_rank_one" +"column_basis/4x4/small" = "column_basis_4x4_small" +"column_basis/4x5/shared_face" = "column_basis_4x5_shared_face" +"column_basis/4x6/full_rank" = "column_basis_4x6_full_rank" +"column_basis/4x6/large" = "column_basis_4x6_large" +"column_basis/4x6/near_dependent" = "column_basis_4x6_near_dependent" +"column_basis/4x6/rank_one" = "column_basis_4x6_rank_one" +"column_basis/4x6/small" = "column_basis_4x6_small" +"column_basis/5x2/full_rank" = "column_basis_5x2_full_rank" +"column_basis/5x2/large" = "column_basis_5x2_large" +"column_basis/5x2/near_dependent" = "column_basis_5x2_near_dependent" +"column_basis/5x2/rank_one" = "column_basis_5x2_rank_one" +"column_basis/5x2/small" = "column_basis_5x2_small" +"column_basis/5x5/full_rank" = "column_basis_5x5_full_rank" +"column_basis/5x5/large" = "column_basis_5x5_large" +"column_basis/5x5/near_dependent" = "column_basis_5x5_near_dependent" +"column_basis/5x5/rank_one" = "column_basis_5x5_rank_one" +"column_basis/5x5/small" = "column_basis_5x5_small" +"column_basis/5x6/shared_face" = "column_basis_5x6_shared_face" +"column_basis/5x8/full_rank" = "column_basis_5x8_full_rank" +"column_basis/5x8/large" = "column_basis_5x8_large" +"column_basis/5x8/near_dependent" = "column_basis_5x8_near_dependent" +"column_basis/5x8/rank_one" = "column_basis_5x8_rank_one" +"column_basis/5x8/small" = "column_basis_5x8_small" +"column_basis/6x10/full_rank" = "column_basis_6x10_full_rank" +"column_basis/6x10/large" = "column_basis_6x10_large" +"column_basis/6x10/near_dependent" = "column_basis_6x10_near_dependent" +"column_basis/6x10/rank_one" = "column_basis_6x10_rank_one" +"column_basis/6x10/small" = "column_basis_6x10_small" +"column_basis/6x7/shared_face" = "column_basis_6x7_shared_face" +"column_basis/7x12/full_rank" = "column_basis_7x12_full_rank" +"column_basis/7x12/large" = "column_basis_7x12_large" +"column_basis/7x12/near_dependent" = "column_basis_7x12_near_dependent" +"column_basis/7x12/rank_one" = "column_basis_7x12_rank_one" +"column_basis/7x12/small" = "column_basis_7x12_small" +"column_basis/8x14/full_rank" = "column_basis_8x14_full_rank" +"column_basis/8x14/large" = "column_basis_8x14_large" +"column_basis/8x14/near_dependent" = "column_basis_8x14_near_dependent" +"column_basis/8x14/rank_one" = "column_basis_8x14_rank_one" +"column_basis/8x14/small" = "column_basis_8x14_small" +"interval_division/negative" = "interval_division/negative" +"interval_division/positive" = "interval_division/positive" +"interval_division/reciprocal" = "interval_division/reciprocal" +"interval_division/subnormal" = "interval_division/subnormal" +"interval_division/tiny_ratio" = "interval_division/tiny_ratio" +"interval_solve_d1/dense" = "interval_solve_d1/dense" +"interval_solve_d1/near_singular" = "interval_solve_d1/near_singular" +"interval_solve_d1/range_exhausted" = "interval_solve_d1/range_exhausted" +"interval_solve_d1/singular" = "interval_solve_d1/singular" +"interval_solve_d1/uncertain_dense" = "interval_solve_d1/uncertain_dense" +"interval_solve_d2/dense" = "interval_solve_d2/dense" +"interval_solve_d2/near_singular" = "interval_solve_d2/near_singular" +"interval_solve_d2/range_exhausted" = "interval_solve_d2/range_exhausted" +"interval_solve_d2/singular" = "interval_solve_d2/singular" +"interval_solve_d2/uncertain_dense" = "interval_solve_d2/uncertain_dense" +"interval_solve_d3/dense" = "interval_solve_d3/dense" +"interval_solve_d3/near_singular" = "interval_solve_d3/near_singular" +"interval_solve_d3/range_exhausted" = "interval_solve_d3/range_exhausted" +"interval_solve_d3/singular" = "interval_solve_d3/singular" +"interval_solve_d3/uncertain_dense" = "interval_solve_d3/uncertain_dense" +"interval_solve_d4/dense" = "interval_solve_d4/dense" +"interval_solve_d4/near_singular" = "interval_solve_d4/near_singular" +"interval_solve_d4/range_exhausted" = "interval_solve_d4/range_exhausted" +"interval_solve_d4/singular" = "interval_solve_d4/singular" +"interval_solve_d4/uncertain_dense" = "interval_solve_d4/uncertain_dense" +"interval_solve_d5/dense" = "interval_solve_d5/dense" +"interval_solve_d5/near_singular" = "interval_solve_d5/near_singular" +"interval_solve_d5/range_exhausted" = "interval_solve_d5/range_exhausted" +"interval_solve_d5/singular" = "interval_solve_d5/singular" +"interval_solve_d5/uncertain_dense" = "interval_solve_d5/uncertain_dense" +"interval_solve_d6/dense" = "interval_solve_d6/dense" +"interval_solve_d6/near_singular" = "interval_solve_d6/near_singular" +"interval_solve_d6/range_exhausted" = "interval_solve_d6/range_exhausted" +"interval_solve_d6/singular" = "interval_solve_d6/singular" +"interval_solve_d6/uncertain_dense" = "interval_solve_d6/uncertain_dense" +"polynomial/coincident_lifted/order4/determinant" = "polynomial_coincident_lifted_order4/determinant" +"polynomial/coincident_lifted/order5/determinant" = "polynomial_coincident_lifted_order5/determinant" +"polynomial/coincident_lifted/order6/determinant" = "polynomial_coincident_lifted_order6/determinant" +"polynomial/coincident_lifted/order7/determinant" = "polynomial_coincident_lifted_order7/determinant" +"polynomial/coincident_lifted/order8/determinant" = "polynomial_coincident_lifted_order8/determinant" +"rational_dot/cancellation/d14/prepared" = "rational_dot_cancellation_d14/prepared" +"rational_dot/cancellation/d14/slice" = "rational_dot_cancellation_d14/slice" +"rational_dot/cancellation/d2/prepared" = "rational_dot_cancellation_d2/prepared" +"rational_dot/cancellation/d2/slice" = "rational_dot_cancellation_d2/slice" +"rational_dot/cancellation/d22/prepared" = "rational_dot_cancellation_d22/prepared" +"rational_dot/cancellation/d22/slice" = "rational_dot_cancellation_d22/slice" +"rational_dot/cancellation/d3/prepared" = "rational_dot_cancellation_d3/prepared" +"rational_dot/cancellation/d3/slice" = "rational_dot_cancellation_d3/slice" +"rational_dot/cancellation/d4/prepared" = "rational_dot_cancellation_d4/prepared" +"rational_dot/cancellation/d4/slice" = "rational_dot_cancellation_d4/slice" +"rational_dot/cancellation/d5/prepared" = "rational_dot_cancellation_d5/prepared" +"rational_dot/cancellation/d5/slice" = "rational_dot_cancellation_d5/slice" +"rational_dot/cancellation/d6/prepared" = "rational_dot_cancellation_d6/prepared" +"rational_dot/cancellation/d6/slice" = "rational_dot_cancellation_d6/slice" +"rational_dot/cancellation/d7/prepared" = "rational_dot_cancellation_d7/prepared" +"rational_dot/cancellation/d7/slice" = "rational_dot_cancellation_d7/slice" +"rational_dot/cancellation/d8/prepared" = "rational_dot_cancellation_d8/prepared" +"rational_dot/cancellation/d8/slice" = "rational_dot_cancellation_d8/slice" +"rational_dot/large/d14/prepared" = "rational_dot_large_d14/prepared" +"rational_dot/large/d14/slice" = "rational_dot_large_d14/slice" +"rational_dot/large/d2/prepared" = "rational_dot_large_d2/prepared" +"rational_dot/large/d2/slice" = "rational_dot_large_d2/slice" +"rational_dot/large/d22/prepared" = "rational_dot_large_d22/prepared" +"rational_dot/large/d22/slice" = "rational_dot_large_d22/slice" +"rational_dot/large/d3/prepared" = "rational_dot_large_d3/prepared" +"rational_dot/large/d3/slice" = "rational_dot_large_d3/slice" +"rational_dot/large/d4/prepared" = "rational_dot_large_d4/prepared" +"rational_dot/large/d4/slice" = "rational_dot_large_d4/slice" +"rational_dot/large/d5/prepared" = "rational_dot_large_d5/prepared" +"rational_dot/large/d5/slice" = "rational_dot_large_d5/slice" +"rational_dot/large/d6/prepared" = "rational_dot_large_d6/prepared" +"rational_dot/large/d6/slice" = "rational_dot_large_d6/slice" +"rational_dot/large/d7/prepared" = "rational_dot_large_d7/prepared" +"rational_dot/large/d7/slice" = "rational_dot_large_d7/slice" +"rational_dot/large/d8/prepared" = "rational_dot_large_d8/prepared" +"rational_dot/large/d8/slice" = "rational_dot_large_d8/slice" +"rational_dot/non_dyadic/d14/prepared" = "rational_dot_non_dyadic_d14/prepared" +"rational_dot/non_dyadic/d14/slice" = "rational_dot_non_dyadic_d14/slice" +"rational_dot/non_dyadic/d2/prepared" = "rational_dot_non_dyadic_d2/prepared" +"rational_dot/non_dyadic/d2/slice" = "rational_dot_non_dyadic_d2/slice" +"rational_dot/non_dyadic/d22/prepared" = "rational_dot_non_dyadic_d22/prepared" +"rational_dot/non_dyadic/d22/slice" = "rational_dot_non_dyadic_d22/slice" +"rational_dot/non_dyadic/d3/prepared" = "rational_dot_non_dyadic_d3/prepared" +"rational_dot/non_dyadic/d3/slice" = "rational_dot_non_dyadic_d3/slice" +"rational_dot/non_dyadic/d4/prepared" = "rational_dot_non_dyadic_d4/prepared" +"rational_dot/non_dyadic/d4/slice" = "rational_dot_non_dyadic_d4/slice" +"rational_dot/non_dyadic/d5/prepared" = "rational_dot_non_dyadic_d5/prepared" +"rational_dot/non_dyadic/d5/slice" = "rational_dot_non_dyadic_d5/slice" +"rational_dot/non_dyadic/d6/prepared" = "rational_dot_non_dyadic_d6/prepared" +"rational_dot/non_dyadic/d6/slice" = "rational_dot_non_dyadic_d6/slice" +"rational_dot/non_dyadic/d7/prepared" = "rational_dot_non_dyadic_d7/prepared" +"rational_dot/non_dyadic/d7/slice" = "rational_dot_non_dyadic_d7/slice" +"rational_dot/non_dyadic/d8/prepared" = "rational_dot_non_dyadic_d8/prepared" +"rational_dot/non_dyadic/d8/slice" = "rational_dot_non_dyadic_d8/slice" "angle_d3/dense/borrowed_slice" = "angle_d3_dense/borrowed_slice" "angle_d3/dense/construct_then_angle" = "angle_d3_dense/construct_then_angle" "angle_d3/dense/prepared_vector" = "angle_d3_dense/prepared_vector" diff --git a/tooling/examples.toml b/tooling/examples.toml index fd7d209..b1b59a1 100644 --- a/tooling/examples.toml +++ b/tooling/examples.toml @@ -17,3 +17,6 @@ features = [ "exact" ] [examples.rational_input_5x5] features = [ "exact" ] + +[examples.polynomial_determinant] +features = [ "exact" ] diff --git a/tooling/measurements/full.toml b/tooling/measurements/full.toml index 36fc3df..0d00eed 100644 --- a/tooling/measurements/full.toml +++ b/tooling/measurements/full.toml @@ -10,6 +10,12 @@ harness = [ "Cargo.lock", "rust-toolchain.toml", "tests/exact_bench_config.rs", + "tests/column_basis.rs", + "tests/rational_dot.rs", + "tests/interval_solve.rs", + "tests/common/interval_solve_oracle.rs", + "tests/common/proptest_config.rs", + "tests/polynomial_bench_inputs.rs", "tooling/performance.just", "tooling/criterion-inventory.toml", "tooling/performance-policy.toml", @@ -36,7 +42,7 @@ compatible = [ [context] phase-driver = "just-v1" suite = "all" -scope = "public-release-and-lu-peers-v1" +scope = "public-release-and-lu-peers-v2" baseline-api-compatibility = "none" [probes] @@ -51,7 +57,7 @@ faer = "Cargo.lock" [phases.baseline] command = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "baseline-all" ] -gate = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "gate" ] +gate = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "gate-public" ] environment = { CARGO_TARGET_DIR = "target", CRITERION_HOME = "target/criterion", RUSTUP_TOOLCHAIN = "1.99.1", RUSTFLAGS = "--cap-lints=warn", CARGO_ENCODED_RUSTFLAGS = "--cap-lints=warn", RUSTC = "rustc", RUSTC_WRAPPER = "", RUSTC_WORKSPACE_WRAPPER = "" } [phases.baseline.discovery] @@ -352,6 +358,61 @@ required = [ "canonical_conversion_wide256_d5/rounded_result", "canonical_conversion_wide256_d5/strict_array", "canonical_conversion_wide256_d5/strict_result", + "column_basis/2x2/full_rank", + "column_basis/2x2/large", + "column_basis/2x2/near_dependent", + "column_basis/2x2/rank_one", + "column_basis/2x2/small", + "column_basis/2x3/shared_face", + "column_basis/3x3/full_rank", + "column_basis/3x3/large", + "column_basis/3x3/near_dependent", + "column_basis/3x3/rank_one", + "column_basis/3x3/small", + "column_basis/3x4/shared_face", + "column_basis/4x4/full_rank", + "column_basis/4x4/large", + "column_basis/4x4/near_dependent", + "column_basis/4x4/rank_one", + "column_basis/4x4/small", + "column_basis/4x5/shared_face", + "column_basis/4x6/full_rank", + "column_basis/4x6/large", + "column_basis/4x6/near_dependent", + "column_basis/4x6/rank_one", + "column_basis/4x6/small", + "column_basis/5x2/full_rank", + "column_basis/5x2/large", + "column_basis/5x2/near_dependent", + "column_basis/5x2/rank_one", + "column_basis/5x2/small", + "column_basis/5x5/full_rank", + "column_basis/5x5/large", + "column_basis/5x5/near_dependent", + "column_basis/5x5/rank_one", + "column_basis/5x5/small", + "column_basis/5x6/shared_face", + "column_basis/5x8/full_rank", + "column_basis/5x8/large", + "column_basis/5x8/near_dependent", + "column_basis/5x8/rank_one", + "column_basis/5x8/small", + "column_basis/6x10/full_rank", + "column_basis/6x10/large", + "column_basis/6x10/near_dependent", + "column_basis/6x10/rank_one", + "column_basis/6x10/small", + "column_basis/6x7/shared_face", + "column_basis/7x12/full_rank", + "column_basis/7x12/large", + "column_basis/7x12/near_dependent", + "column_basis/7x12/rank_one", + "column_basis/7x12/small", + "column_basis/8x14/full_rank", + "column_basis/8x14/large", + "column_basis/8x14/near_dependent", + "column_basis/8x14/rank_one", + "column_basis/8x14/small", "d16/faer_lu_solve", "d16/la_stack_det", "d16/la_stack_det_from_ldlt", @@ -682,6 +743,11 @@ required = [ "interval_det_sign/d4_conclusive_lifted", "interval_det_sign/d4_inconclusive_lifted", "interval_det_sign/d7_conclusive_lifted", + "interval_division/negative", + "interval_division/positive", + "interval_division/reciprocal", + "interval_division/subnormal", + "interval_division/tiny_ratio", "interval_scalar/cancellation/add", "interval_scalar/cancellation/multiply", "interval_scalar/cancellation/square", @@ -697,6 +763,36 @@ required = [ "interval_scalar/zero/add", "interval_scalar/zero/multiply", "interval_scalar/zero/square", + "interval_solve_d1/dense", + "interval_solve_d1/near_singular", + "interval_solve_d1/range_exhausted", + "interval_solve_d1/singular", + "interval_solve_d1/uncertain_dense", + "interval_solve_d2/dense", + "interval_solve_d2/near_singular", + "interval_solve_d2/range_exhausted", + "interval_solve_d2/singular", + "interval_solve_d2/uncertain_dense", + "interval_solve_d3/dense", + "interval_solve_d3/near_singular", + "interval_solve_d3/range_exhausted", + "interval_solve_d3/singular", + "interval_solve_d3/uncertain_dense", + "interval_solve_d4/dense", + "interval_solve_d4/near_singular", + "interval_solve_d4/range_exhausted", + "interval_solve_d4/singular", + "interval_solve_d4/uncertain_dense", + "interval_solve_d5/dense", + "interval_solve_d5/near_singular", + "interval_solve_d5/range_exhausted", + "interval_solve_d5/singular", + "interval_solve_d5/uncertain_dense", + "interval_solve_d6/dense", + "interval_solve_d6/near_singular", + "interval_solve_d6/range_exhausted", + "interval_solve_d6/singular", + "interval_solve_d6/uncertain_dense", "linear_form_d2_batch/origin/construction", "linear_form_d2_batch/origin/prepared", "linear_form_d2_batch/origin/with_construction", @@ -837,6 +933,11 @@ required = [ "linear_form_d6_prepared/underflow/difference_endpoints", "linear_form_d6_prepared/underflow/dot", "linear_form_d6_prepared/underflow/dot_endpoints", + "polynomial/coincident_lifted/order4/determinant", + "polynomial/coincident_lifted/order5/determinant", + "polynomial/coincident_lifted/order6/determinant", + "polynomial/coincident_lifted/order7/determinant", + "polynomial/coincident_lifted/order8/determinant", "public_d2/det_bound", "public_d2/det_direct", "public_d2/det_with_bound", @@ -921,12 +1022,6 @@ required = [ "public_d5/vector_access", "public_d5/vector_new", "public_d5/vector_zero", - "public_scalar/tolerance", - "public_scalar/interval_new", - "public_scalar/interval_point", - "public_scalar/interval_subtraction", - "public_scalar/interval_access", - "public_scalar/interval_negate", "public_exact_d2/matrix_access", "public_exact_d2/matrix_from_fn", "public_exact_d2/matrix_from_rows", @@ -969,6 +1064,66 @@ required = [ "public_exact_d5/vector_zero", "public_exact_scalar/rounded", "public_exact_scalar/strict", + "public_scalar/interval_access", + "public_scalar/interval_negate", + "public_scalar/interval_new", + "public_scalar/interval_point", + "public_scalar/interval_subtraction", + "public_scalar/tolerance", + "rational_dot/cancellation/d14/prepared", + "rational_dot/cancellation/d14/slice", + "rational_dot/cancellation/d2/prepared", + "rational_dot/cancellation/d2/slice", + "rational_dot/cancellation/d22/prepared", + "rational_dot/cancellation/d22/slice", + "rational_dot/cancellation/d3/prepared", + "rational_dot/cancellation/d3/slice", + "rational_dot/cancellation/d4/prepared", + "rational_dot/cancellation/d4/slice", + "rational_dot/cancellation/d5/prepared", + "rational_dot/cancellation/d5/slice", + "rational_dot/cancellation/d6/prepared", + "rational_dot/cancellation/d6/slice", + "rational_dot/cancellation/d7/prepared", + "rational_dot/cancellation/d7/slice", + "rational_dot/cancellation/d8/prepared", + "rational_dot/cancellation/d8/slice", + "rational_dot/large/d14/prepared", + "rational_dot/large/d14/slice", + "rational_dot/large/d2/prepared", + "rational_dot/large/d2/slice", + "rational_dot/large/d22/prepared", + "rational_dot/large/d22/slice", + "rational_dot/large/d3/prepared", + "rational_dot/large/d3/slice", + "rational_dot/large/d4/prepared", + "rational_dot/large/d4/slice", + "rational_dot/large/d5/prepared", + "rational_dot/large/d5/slice", + "rational_dot/large/d6/prepared", + "rational_dot/large/d6/slice", + "rational_dot/large/d7/prepared", + "rational_dot/large/d7/slice", + "rational_dot/large/d8/prepared", + "rational_dot/large/d8/slice", + "rational_dot/non_dyadic/d14/prepared", + "rational_dot/non_dyadic/d14/slice", + "rational_dot/non_dyadic/d2/prepared", + "rational_dot/non_dyadic/d2/slice", + "rational_dot/non_dyadic/d22/prepared", + "rational_dot/non_dyadic/d22/slice", + "rational_dot/non_dyadic/d3/prepared", + "rational_dot/non_dyadic/d3/slice", + "rational_dot/non_dyadic/d4/prepared", + "rational_dot/non_dyadic/d4/slice", + "rational_dot/non_dyadic/d5/prepared", + "rational_dot/non_dyadic/d5/slice", + "rational_dot/non_dyadic/d6/prepared", + "rational_dot/non_dyadic/d6/slice", + "rational_dot/non_dyadic/d7/prepared", + "rational_dot/non_dyadic/d7/slice", + "rational_dot/non_dyadic/d8/prepared", + "rational_dot/non_dyadic/d8/slice", "rational_input_d2/det_row_cleared_bareiss", "rational_input_d2/det_sign_row_cleared_bareiss", "rational_input_d2/solve_row_cleared_bareiss", @@ -1345,7 +1500,7 @@ timeout = 14400 [phases.current] command = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "current-all" ] -gate = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "gate" ] +gate = [ "just", "--justfile", "tooling/performance.just", "--working-directory", ".", "gate-public" ] environment = { CARGO_TARGET_DIR = "target", CRITERION_HOME = "target/criterion", RUSTUP_TOOLCHAIN = "1.99.1", RUSTFLAGS = "--cap-lints=warn", CARGO_ENCODED_RUSTFLAGS = "--cap-lints=warn", RUSTC = "rustc", RUSTC_WRAPPER = "", RUSTC_WORKSPACE_WRAPPER = "" } [phases.current.discovery] @@ -1646,6 +1801,61 @@ required = [ "canonical_conversion_wide256_d5/rounded_result", "canonical_conversion_wide256_d5/strict_array", "canonical_conversion_wide256_d5/strict_result", + "column_basis/2x2/full_rank", + "column_basis/2x2/large", + "column_basis/2x2/near_dependent", + "column_basis/2x2/rank_one", + "column_basis/2x2/small", + "column_basis/2x3/shared_face", + "column_basis/3x3/full_rank", + "column_basis/3x3/large", + "column_basis/3x3/near_dependent", + "column_basis/3x3/rank_one", + "column_basis/3x3/small", + "column_basis/3x4/shared_face", + "column_basis/4x4/full_rank", + "column_basis/4x4/large", + "column_basis/4x4/near_dependent", + "column_basis/4x4/rank_one", + "column_basis/4x4/small", + "column_basis/4x5/shared_face", + "column_basis/4x6/full_rank", + "column_basis/4x6/large", + "column_basis/4x6/near_dependent", + "column_basis/4x6/rank_one", + "column_basis/4x6/small", + "column_basis/5x2/full_rank", + "column_basis/5x2/large", + "column_basis/5x2/near_dependent", + "column_basis/5x2/rank_one", + "column_basis/5x2/small", + "column_basis/5x5/full_rank", + "column_basis/5x5/large", + "column_basis/5x5/near_dependent", + "column_basis/5x5/rank_one", + "column_basis/5x5/small", + "column_basis/5x6/shared_face", + "column_basis/5x8/full_rank", + "column_basis/5x8/large", + "column_basis/5x8/near_dependent", + "column_basis/5x8/rank_one", + "column_basis/5x8/small", + "column_basis/6x10/full_rank", + "column_basis/6x10/large", + "column_basis/6x10/near_dependent", + "column_basis/6x10/rank_one", + "column_basis/6x10/small", + "column_basis/6x7/shared_face", + "column_basis/7x12/full_rank", + "column_basis/7x12/large", + "column_basis/7x12/near_dependent", + "column_basis/7x12/rank_one", + "column_basis/7x12/small", + "column_basis/8x14/full_rank", + "column_basis/8x14/large", + "column_basis/8x14/near_dependent", + "column_basis/8x14/rank_one", + "column_basis/8x14/small", "d16/la_stack_det", "d16/la_stack_det_from_ldlt", "d16/la_stack_det_from_lu", @@ -1960,6 +2170,11 @@ required = [ "interval_det_sign/d4_conclusive_lifted", "interval_det_sign/d4_inconclusive_lifted", "interval_det_sign/d7_conclusive_lifted", + "interval_division/negative", + "interval_division/positive", + "interval_division/reciprocal", + "interval_division/subnormal", + "interval_division/tiny_ratio", "interval_scalar/cancellation/add", "interval_scalar/cancellation/multiply", "interval_scalar/cancellation/square", @@ -1975,6 +2190,36 @@ required = [ "interval_scalar/zero/add", "interval_scalar/zero/multiply", "interval_scalar/zero/square", + "interval_solve_d1/dense", + "interval_solve_d1/near_singular", + "interval_solve_d1/range_exhausted", + "interval_solve_d1/singular", + "interval_solve_d1/uncertain_dense", + "interval_solve_d2/dense", + "interval_solve_d2/near_singular", + "interval_solve_d2/range_exhausted", + "interval_solve_d2/singular", + "interval_solve_d2/uncertain_dense", + "interval_solve_d3/dense", + "interval_solve_d3/near_singular", + "interval_solve_d3/range_exhausted", + "interval_solve_d3/singular", + "interval_solve_d3/uncertain_dense", + "interval_solve_d4/dense", + "interval_solve_d4/near_singular", + "interval_solve_d4/range_exhausted", + "interval_solve_d4/singular", + "interval_solve_d4/uncertain_dense", + "interval_solve_d5/dense", + "interval_solve_d5/near_singular", + "interval_solve_d5/range_exhausted", + "interval_solve_d5/singular", + "interval_solve_d5/uncertain_dense", + "interval_solve_d6/dense", + "interval_solve_d6/near_singular", + "interval_solve_d6/range_exhausted", + "interval_solve_d6/singular", + "interval_solve_d6/uncertain_dense", "linear_form_d2_batch/origin/construction", "linear_form_d2_batch/origin/prepared", "linear_form_d2_batch/origin/with_construction", @@ -2115,6 +2360,11 @@ required = [ "linear_form_d6_prepared/underflow/difference_endpoints", "linear_form_d6_prepared/underflow/dot", "linear_form_d6_prepared/underflow/dot_endpoints", + "polynomial/coincident_lifted/order4/determinant", + "polynomial/coincident_lifted/order5/determinant", + "polynomial/coincident_lifted/order6/determinant", + "polynomial/coincident_lifted/order7/determinant", + "polynomial/coincident_lifted/order8/determinant", "public_d2/det_bound", "public_d2/det_direct", "public_d2/det_with_bound", @@ -2199,12 +2449,6 @@ required = [ "public_d5/vector_access", "public_d5/vector_new", "public_d5/vector_zero", - "public_scalar/tolerance", - "public_scalar/interval_new", - "public_scalar/interval_point", - "public_scalar/interval_subtraction", - "public_scalar/interval_access", - "public_scalar/interval_negate", "public_exact_d2/matrix_access", "public_exact_d2/matrix_from_fn", "public_exact_d2/matrix_from_rows", @@ -2247,6 +2491,66 @@ required = [ "public_exact_d5/vector_zero", "public_exact_scalar/rounded", "public_exact_scalar/strict", + "public_scalar/interval_access", + "public_scalar/interval_negate", + "public_scalar/interval_new", + "public_scalar/interval_point", + "public_scalar/interval_subtraction", + "public_scalar/tolerance", + "rational_dot/cancellation/d14/prepared", + "rational_dot/cancellation/d14/slice", + "rational_dot/cancellation/d2/prepared", + "rational_dot/cancellation/d2/slice", + "rational_dot/cancellation/d22/prepared", + "rational_dot/cancellation/d22/slice", + "rational_dot/cancellation/d3/prepared", + "rational_dot/cancellation/d3/slice", + "rational_dot/cancellation/d4/prepared", + "rational_dot/cancellation/d4/slice", + "rational_dot/cancellation/d5/prepared", + "rational_dot/cancellation/d5/slice", + "rational_dot/cancellation/d6/prepared", + "rational_dot/cancellation/d6/slice", + "rational_dot/cancellation/d7/prepared", + "rational_dot/cancellation/d7/slice", + "rational_dot/cancellation/d8/prepared", + "rational_dot/cancellation/d8/slice", + "rational_dot/large/d14/prepared", + "rational_dot/large/d14/slice", + "rational_dot/large/d2/prepared", + "rational_dot/large/d2/slice", + "rational_dot/large/d22/prepared", + "rational_dot/large/d22/slice", + "rational_dot/large/d3/prepared", + "rational_dot/large/d3/slice", + "rational_dot/large/d4/prepared", + "rational_dot/large/d4/slice", + "rational_dot/large/d5/prepared", + "rational_dot/large/d5/slice", + "rational_dot/large/d6/prepared", + "rational_dot/large/d6/slice", + "rational_dot/large/d7/prepared", + "rational_dot/large/d7/slice", + "rational_dot/large/d8/prepared", + "rational_dot/large/d8/slice", + "rational_dot/non_dyadic/d14/prepared", + "rational_dot/non_dyadic/d14/slice", + "rational_dot/non_dyadic/d2/prepared", + "rational_dot/non_dyadic/d2/slice", + "rational_dot/non_dyadic/d22/prepared", + "rational_dot/non_dyadic/d22/slice", + "rational_dot/non_dyadic/d3/prepared", + "rational_dot/non_dyadic/d3/slice", + "rational_dot/non_dyadic/d4/prepared", + "rational_dot/non_dyadic/d4/slice", + "rational_dot/non_dyadic/d5/prepared", + "rational_dot/non_dyadic/d5/slice", + "rational_dot/non_dyadic/d6/prepared", + "rational_dot/non_dyadic/d6/slice", + "rational_dot/non_dyadic/d7/prepared", + "rational_dot/non_dyadic/d7/slice", + "rational_dot/non_dyadic/d8/prepared", + "rational_dot/non_dyadic/d8/slice", "rational_input_d2/det_row_cleared_bareiss", "rational_input_d2/det_sign_row_cleared_bareiss", "rational_input_d2/solve_row_cleared_bareiss", diff --git a/tooling/performance-policy.toml b/tooling/performance-policy.toml index 52fc879..acc5de1 100644 --- a/tooling/performance-policy.toml +++ b/tooling/performance-policy.toml @@ -245,6 +245,61 @@ default = [ "angle_d6/subnormal_axes/borrowed_slice", "angle_d6/subnormal_axes/construct_then_angle", "angle_d6/subnormal_axes/prepared_vector", + "column_basis/2x2/full_rank", + "column_basis/2x2/large", + "column_basis/2x2/near_dependent", + "column_basis/2x2/rank_one", + "column_basis/2x2/small", + "column_basis/2x3/shared_face", + "column_basis/3x3/full_rank", + "column_basis/3x3/large", + "column_basis/3x3/near_dependent", + "column_basis/3x3/rank_one", + "column_basis/3x3/small", + "column_basis/3x4/shared_face", + "column_basis/4x4/full_rank", + "column_basis/4x4/large", + "column_basis/4x4/near_dependent", + "column_basis/4x4/rank_one", + "column_basis/4x4/small", + "column_basis/4x5/shared_face", + "column_basis/4x6/full_rank", + "column_basis/4x6/large", + "column_basis/4x6/near_dependent", + "column_basis/4x6/rank_one", + "column_basis/4x6/small", + "column_basis/5x2/full_rank", + "column_basis/5x2/large", + "column_basis/5x2/near_dependent", + "column_basis/5x2/rank_one", + "column_basis/5x2/small", + "column_basis/5x5/full_rank", + "column_basis/5x5/large", + "column_basis/5x5/near_dependent", + "column_basis/5x5/rank_one", + "column_basis/5x5/small", + "column_basis/5x6/shared_face", + "column_basis/5x8/full_rank", + "column_basis/5x8/large", + "column_basis/5x8/near_dependent", + "column_basis/5x8/rank_one", + "column_basis/5x8/small", + "column_basis/6x10/full_rank", + "column_basis/6x10/large", + "column_basis/6x10/near_dependent", + "column_basis/6x10/rank_one", + "column_basis/6x10/small", + "column_basis/6x7/shared_face", + "column_basis/7x12/full_rank", + "column_basis/7x12/large", + "column_basis/7x12/near_dependent", + "column_basis/7x12/rank_one", + "column_basis/7x12/small", + "column_basis/8x14/full_rank", + "column_basis/8x14/large", + "column_basis/8x14/near_dependent", + "column_basis/8x14/rank_one", + "column_basis/8x14/small", "d16/la_stack_det", "d16/la_stack_det_from_ldlt", "d16/la_stack_det_from_lu", @@ -478,6 +533,11 @@ default = [ "interval_det_sign/d4_conclusive_lifted", "interval_det_sign/d4_inconclusive_lifted", "interval_det_sign/d7_conclusive_lifted", + "interval_division/negative", + "interval_division/positive", + "interval_division/reciprocal", + "interval_division/subnormal", + "interval_division/tiny_ratio", "interval_scalar/cancellation/add", "interval_scalar/cancellation/multiply", "interval_scalar/cancellation/square", @@ -493,6 +553,36 @@ default = [ "interval_scalar/zero/add", "interval_scalar/zero/multiply", "interval_scalar/zero/square", + "interval_solve_d1/dense", + "interval_solve_d1/near_singular", + "interval_solve_d1/range_exhausted", + "interval_solve_d1/singular", + "interval_solve_d1/uncertain_dense", + "interval_solve_d2/dense", + "interval_solve_d2/near_singular", + "interval_solve_d2/range_exhausted", + "interval_solve_d2/singular", + "interval_solve_d2/uncertain_dense", + "interval_solve_d3/dense", + "interval_solve_d3/near_singular", + "interval_solve_d3/range_exhausted", + "interval_solve_d3/singular", + "interval_solve_d3/uncertain_dense", + "interval_solve_d4/dense", + "interval_solve_d4/near_singular", + "interval_solve_d4/range_exhausted", + "interval_solve_d4/singular", + "interval_solve_d4/uncertain_dense", + "interval_solve_d5/dense", + "interval_solve_d5/near_singular", + "interval_solve_d5/range_exhausted", + "interval_solve_d5/singular", + "interval_solve_d5/uncertain_dense", + "interval_solve_d6/dense", + "interval_solve_d6/near_singular", + "interval_solve_d6/range_exhausted", + "interval_solve_d6/singular", + "interval_solve_d6/uncertain_dense", "linear_form_d2_batch/origin/construction", "linear_form_d2_batch/origin/prepared", "linear_form_d2_batch/origin/with_construction", @@ -717,12 +807,12 @@ default = [ "public_d5/vector_access", "public_d5/vector_new", "public_d5/vector_zero", - "public_scalar/tolerance", + "public_scalar/interval_access", + "public_scalar/interval_negate", "public_scalar/interval_new", "public_scalar/interval_point", "public_scalar/interval_subtraction", - "public_scalar/interval_access", - "public_scalar/interval_negate", + "public_scalar/tolerance", ] exact = [ "canonical_conversion_below_overflow_d2/rounded_array", @@ -934,6 +1024,11 @@ exact = [ "exact_random_corpus_d5/solve_exact", "exact_random_corpus_d5/solve_exact_f64_result", "exact_random_corpus_d5/solve_exact_rounded_f64", + "polynomial/coincident_lifted/order4/determinant", + "polynomial/coincident_lifted/order5/determinant", + "polynomial/coincident_lifted/order6/determinant", + "polynomial/coincident_lifted/order7/determinant", + "polynomial/coincident_lifted/order8/determinant", "public_exact_d2/matrix_access", "public_exact_d2/matrix_from_fn", "public_exact_d2/matrix_from_rows", @@ -976,6 +1071,60 @@ exact = [ "public_exact_d5/vector_zero", "public_exact_scalar/rounded", "public_exact_scalar/strict", + "rational_dot/cancellation/d14/prepared", + "rational_dot/cancellation/d14/slice", + "rational_dot/cancellation/d2/prepared", + "rational_dot/cancellation/d2/slice", + "rational_dot/cancellation/d22/prepared", + "rational_dot/cancellation/d22/slice", + "rational_dot/cancellation/d3/prepared", + "rational_dot/cancellation/d3/slice", + "rational_dot/cancellation/d4/prepared", + "rational_dot/cancellation/d4/slice", + "rational_dot/cancellation/d5/prepared", + "rational_dot/cancellation/d5/slice", + "rational_dot/cancellation/d6/prepared", + "rational_dot/cancellation/d6/slice", + "rational_dot/cancellation/d7/prepared", + "rational_dot/cancellation/d7/slice", + "rational_dot/cancellation/d8/prepared", + "rational_dot/cancellation/d8/slice", + "rational_dot/large/d14/prepared", + "rational_dot/large/d14/slice", + "rational_dot/large/d2/prepared", + "rational_dot/large/d2/slice", + "rational_dot/large/d22/prepared", + "rational_dot/large/d22/slice", + "rational_dot/large/d3/prepared", + "rational_dot/large/d3/slice", + "rational_dot/large/d4/prepared", + "rational_dot/large/d4/slice", + "rational_dot/large/d5/prepared", + "rational_dot/large/d5/slice", + "rational_dot/large/d6/prepared", + "rational_dot/large/d6/slice", + "rational_dot/large/d7/prepared", + "rational_dot/large/d7/slice", + "rational_dot/large/d8/prepared", + "rational_dot/large/d8/slice", + "rational_dot/non_dyadic/d14/prepared", + "rational_dot/non_dyadic/d14/slice", + "rational_dot/non_dyadic/d2/prepared", + "rational_dot/non_dyadic/d2/slice", + "rational_dot/non_dyadic/d22/prepared", + "rational_dot/non_dyadic/d22/slice", + "rational_dot/non_dyadic/d3/prepared", + "rational_dot/non_dyadic/d3/slice", + "rational_dot/non_dyadic/d4/prepared", + "rational_dot/non_dyadic/d4/slice", + "rational_dot/non_dyadic/d5/prepared", + "rational_dot/non_dyadic/d5/slice", + "rational_dot/non_dyadic/d6/prepared", + "rational_dot/non_dyadic/d6/slice", + "rational_dot/non_dyadic/d7/prepared", + "rational_dot/non_dyadic/d7/slice", + "rational_dot/non_dyadic/d8/prepared", + "rational_dot/non_dyadic/d8/slice", "rational_input_d2/det_row_cleared_bareiss", "rational_input_d2/det_sign_row_cleared_bareiss", "rational_input_d2/solve_row_cleared_bareiss", diff --git a/tooling/performance.just b/tooling/performance.just index 7fb7fd3..41cadef 100644 --- a/tooling/performance.just +++ b/tooling/performance.just @@ -36,6 +36,9 @@ current-vs_linalg mode="measure": gate: cargo test --locked --target {{ host_target }} --workspace --features bench,exact --test vs_linalg_inputs --test exact_bench_config +gate-public: gate + cargo test --locked --target {{ host_target }} --features bench,exact --test column_basis --test rational_dot --test interval_solve --test polynomial_bench_inputs + la-stack mode="measure" sample="": (public-default mode sample) (public-exact mode sample) # Shared native argument construction keeps discovery and measurement identical. @@ -56,6 +59,6 @@ native suite filter features mode="measure" sample="": peers mode="measure" sample="": (native "vs_linalg" '^d(2|3|4|5|8|16|32|64)/(nalgebra|faer)_lu_solve$' "bench" mode sample) -public-default mode="measure" sample="": (native "vs_linalg" '^d[0-9]+/la_stack_' "bench" mode sample) (native "angle" '/(prepared_vector|borrowed_slice|construct_then_angle)$' "bench" mode sample) (native "gram" '/la_stack$' "bench" mode sample) (native "interval" "" "bench" mode sample) (native "linear_form" "" "bench" mode sample) (native "public_api" "" "bench" mode sample) +public-default mode="measure" sample="": (native "vs_linalg" '^d[0-9]+/la_stack_' "bench" mode sample) (native "angle" '/(prepared_vector|borrowed_slice|construct_then_angle)$' "bench" mode sample) (native "column_basis" "" "bench" mode sample) (native "gram" '/la_stack$' "bench" mode sample) (native "interval" "" "bench" mode sample) (native "interval_solve" "" "bench" mode sample) (native "linear_form" "" "bench" mode sample) (native "public_api" "" "bench" mode sample) -public-exact mode="measure" sample="": (native "exact" '^(canonical_conversion_|exact_(d[2-5]|random_corpus_d[2-5]|near_singular_3x3|large_entries_3x3|hilbert_[45]x[45])/|rational_input_.*d[2-8]/(det_sign_row_cleared_bareiss|det_row_cleared_bareiss|solve_row_cleared_bareiss)$)' "bench,exact" mode sample) (native "rational_solve" '/(construct|prepared|adapter)$' "bench,exact" mode sample) (native "public_api" '^public_exact_' "bench,exact" mode sample) +public-exact mode="measure" sample="": (native "exact" '^(canonical_conversion_|exact_(d[2-5]|random_corpus_d[2-5]|near_singular_3x3|large_entries_3x3|hilbert_[45]x[45])/|rational_input_.*d[2-8]/(det_sign_row_cleared_bareiss|det_row_cleared_bareiss|solve_row_cleared_bareiss)$)' "bench,exact" mode sample) (native "polynomial" "" "bench,exact" mode sample) (native "rational_dot" "" "bench,exact" mode sample) (native "rational_solve" '/(construct|prepared|adapter)$' "bench,exact" mode sample) (native "public_api" '^public_exact_' "bench,exact" mode sample) diff --git a/tooling/release_performance.py b/tooling/release_performance.py index 68e943e..fb94901 100644 --- a/tooling/release_performance.py +++ b/tooling/release_performance.py @@ -13,7 +13,7 @@ from typing import cast from research_repo_tools.cli import main as tooling_main -from research_repo_tools.common_measurement import CommonHarnessPlan +from research_repo_tools.common_measurement import CompleteMeasurementPlan from research_repo_tools.files import replace_many from research_repo_tools.measurement import load_measurement from research_repo_tools.process import ExecutableNotFoundError, format_exception_diagnostics, run_git_command @@ -40,7 +40,7 @@ def capture(root: Path, current: str, baseline: str) -> tuple[ReleasePair, str, baseline=baseline or None, ) configuration = load_measurement(root, "tooling/measurements/full.toml") - policy = configuration.measurement if isinstance(configuration, CommonHarnessPlan) else configuration + policy = configuration.measurement if isinstance(configuration, CompleteMeasurementPlan) else configuration policy.validate_release(pair.current) policy.validate_release(pair.baseline) tagged = bool(current) or any(item.tag == pair.current for item in releases) diff --git a/tooling/shared_tooling.py b/tooling/shared_tooling.py new file mode 100644 index 0000000..5217cb3 --- /dev/null +++ b/tooling/shared_tooling.py @@ -0,0 +1,49 @@ +"""Keep v0.2.4 Semgrep setup/scans in the standard environment until #317. + +Only scanner selection is overridden; the installed shared CLI owns all fixture +validation. Remove this adapter after research-repo-tools#165 ships in v0.2.5. +""" + +import os +import sys +import sysconfig +from importlib.metadata import version +from pathlib import Path + +from research_repo_tools import semgrep_tool +from research_repo_tools.cli import main as tooling_main +from research_repo_tools.process import resolve_executable + + +def main(argv: list[str] | None = None) -> int: + """Run the shared CLI with the scanner installed beside this interpreter.""" + if version("research-repo-tools") != "0.2.4": + msg = "Review/remove the v0.2.4 Semgrep override before upgrading (la-stack#317)." + raise RuntimeError(msg) + scripts = Path(sysconfig.get_path("scripts")) + previous_setup = semgrep_tool.setup + previous_version = semgrep_tool.VERSION + previous_path = os.environ.get("PATH") + + def scanner(root: Path) -> Path: + # Shared setup synchronizes dev before selecting its scanner. + # The shared CLI prints this module constant after selecting the binary. + vars(semgrep_tool)["VERSION"] = version("semgrep") + return resolve_executable(scripts / ("semgrep.exe" if os.name == "nt" else "semgrep")) + + try: + # The native launcher also resolves pysemgrep through PATH. + os.environ["PATH"] = os.pathsep.join([str(scripts), *([previous_path] if previous_path else [])]) + vars(semgrep_tool)["setup"] = scanner + return tooling_main(argv) + finally: + vars(semgrep_tool)["setup"] = previous_setup + vars(semgrep_tool)["VERSION"] = previous_version + if previous_path is None: + os.environ.pop("PATH", None) + else: + os.environ["PATH"] = previous_path + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/uv.lock b/uv.lock index 433be67..a47c3bf 100644 --- a/uv.lock +++ b/uv.lock @@ -413,13 +413,13 @@ tooling = [ [package.metadata.requires-dev] dev = [ { name = "actionlint-py", specifier = "==1.7.12.25" }, - { name = "research-repo-tools", extras = ["python-tools"], specifier = "==0.2.3" }, + { name = "research-repo-tools", extras = ["python-tools"], specifier = "==0.2.4" }, { name = "semgrep", specifier = "==1.182.1" }, { name = "shellcheck-py", specifier = "==0.11.0.1" }, { name = "shfmt-py", specifier = "==4.2.0" }, { name = "yamllint", specifier = "==1.38.0" }, ] -tooling = [{ name = "research-repo-tools", extras = ["python-tools"], specifier = "==0.2.3" }] +tooling = [{ name = "research-repo-tools", extras = ["python-tools"], specifier = "==0.2.4" }] [[package]] name = "markdown-it-py" @@ -866,16 +866,16 @@ wheels = [ [[package]] name = "research-repo-tools" -version = "0.2.3" +version = "0.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pyyaml" }, { name = "rust-just" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/0f/13f1c4eac70a510a452fcad67c95eaa05ab93ae143428e5e2e625a9abb3e/research_repo_tools-0.2.3.tar.gz", hash = "sha256:13700b174656ffad265ecb926dd66013949853b7a39c088ab6d3852aba72df95", size = 894191, upload-time = "2026-10-21T23:18:17.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/e9/576fd80eb942edbdfe5d8be0e15cf0a6a6af3d30d4ad0000830d876740f5/research_repo_tools-0.2.4.tar.gz", hash = "sha256:30b6d4d2f8d0cb98d5311656928dd65800088b651dd6c0c29f9a02526e9e902a", size = 1042746, upload-time = "2026-10-28T20:12:31.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/ad/a522413628887cc54c06fe2186ad5490a25e6a098057a71df47a99bb9c63/research_repo_tools-0.2.3-py3-none-any.whl", hash = "sha256:da5f0259027662e7706b7f2b8a38d8e1f1d8a5dce314e2c996177b2947514fc6", size = 391281, upload-time = "2026-10-21T23:18:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/53/1d/8f7eed9320adca20787a4b10d323a72e1259a7718fc8fd53805c82e509c2/research_repo_tools-0.2.4-py3-none-any.whl", hash = "sha256:00aaab2b7efd680d79324c6debb64db4d8d1ff076a460ff46e24e0ef1c24bd7d", size = 431258, upload-time = "2026-10-28T20:12:29.604Z" }, ] [package.optional-dependencies]