From 7019b821a28b5a4d32f506f8bd575184eef8e4cf Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sun, 12 Jul 2026 21:34:46 -0700 Subject: [PATCH 1/2] chore(release): release v0.4.4 - Bump crate, utility package, citation, and documentation metadata - Publish the v0.4.4 changelog and release performance comparison - Refresh reproducible LU-solve benchmarks and archived performance reports - Include exact-conversion and LDLT performance improvements - Harden version-sync checks and generated Markdown formatting --- AGENTS.md | 5 + CHANGELOG.md | 104 +++++ CITATION.cff | 4 +- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 68 ++- benches/vs_linalg.rs | 46 +- docs/BENCHMARKING.md | 35 +- docs/PERFORMANCE.md | 407 ++++++++++++++---- docs/RELEASING.md | 12 +- docs/archive/performance/README.md | 1 + docs/archive/performance/v0.4.3-vs-v0.4.2.md | 148 +++++++ .../bench/vs_linalg_lu_solve_median.csv | 16 +- .../vs_linalg_lu_solve_median.provenance.json | 48 ++- .../bench/vs_linalg_lu_solve_median.svg | 148 +++---- justfile | 11 +- pyproject.toml | 2 +- scripts/bench_compare.py | 53 +-- scripts/check_docs_version_sync.py | 29 +- scripts/criterion_dim_plot.py | 22 +- scripts/postprocess_changelog.py | 41 ++ scripts/tests/test_bench_compare.py | 2 + scripts/tests/test_check_docs_version_sync.py | 43 +- scripts/tests/test_criterion_dim_plot.py | 45 +- scripts/tests/test_postprocess_changelog.py | 25 ++ src/exact.rs | 54 ++- src/ldlt.rs | 57 ++- tests/vs_linalg_inputs.rs | 14 +- uv.lock | 2 +- 29 files changed, 1084 insertions(+), 362 deletions(-) create mode 100644 docs/archive/performance/v0.4.3-vs-v0.4.2.md diff --git a/AGENTS.md b/AGENTS.md index 74f8399..ad42436 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,6 +124,11 @@ invariant over the convenient edit. from the same representative benchmark command, inputs, features, and environment. Use `bench-vs-linalg` (vs nalgebra / faer) or `bench-exact` (exact arithmetic), as appropriate. +- For nanosecond-scale fixed-size kernels, prefer Criterion `bencher.iter` so + the complete public operation is measured symmetrically across implementations. + Use `iter_batched` only when setup is explicitly outside the estimand, the + exclusion is applied comparably to every implementation, and a same-binary + comparison shows that batching does not materially distort the result. - Preserve benchmark provenance and distinguish descriptive point-estimate ratios from statistically supported performance claims. Marginal Criterion interval separation is not a paired confidence interval for the change. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d09cb9..3a8e159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,108 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.4] - 2026-07-12 + +### ⚠️ Breaking Changes + +- Make numerical invariants and errors explicit +- Require Rust 1.97.0 + +### Added + +- [**breaking**] Make numerical invariants and errors explicit + [`668daed`](https://github.com/acgetchell/la-stack/commit/668daed6f5d83a03fc95a7872a0a302d097e3fc4) + + - add structured error reasons, locations, origins, and factorization context + - make determinant, exact-conversion, LU, and LDLT paths range-safe and mathematically explicit + - validate benchmark inputs independently and require reproducible, provenance-backed performance evidence + - centralize tool versions, adopt nextest profiles, and replace Codacy with repository-owned checks and SARIF reporting + +### Changed + +- Consolidate setup abort handling [`b9e453e`](https://github.com/acgetchell/la-stack/commit/b9e453e540da3c63b0a14ca94b4bcfbc9c8ff6bd) + + - Share `OrAbort` across exact and comparison benchmarks while preserving contextual panic behavior. + - Centralize Clippy policy in Cargo.toml for consistent local and CI checks. + - Keep `unreachable_pub` enforced while allowing intentional crate-visible helpers. + - Enable CodeRabbit's request-changes review workflow. + +### Dependencies + +- Bump python-multipart in the uv group across 1 directory [`6bb4fb7`](https://github.com/acgetchell/la-stack/commit/6bb4fb7ce48e49d4d2a5bfe4ff5d55b09e6300e2) +- Bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7 [`4a999a6`](https://github.com/acgetchell/la-stack/commit/4a999a664211126e581da47713f6d2426d6db698) +- Bump github/codeql-action/upload-sarif [`85c0a7b`](https://github.com/acgetchell/la-stack/commit/85c0a7bd6a1b3d3e0e00f4e82c6d544c2ca0f22b) +- Bump actions/checkout from 6.0.2 to 7.0.0 [`0f0f956`](https://github.com/acgetchell/la-stack/commit/0f0f956c64ddbc5700f3c2f96b90242703e0a410) +- Bump astral-sh/setup-uv from 8.1.0 to 8.2.0 [`259acaa`](https://github.com/acgetchell/la-stack/commit/259acaa06cc2d6b50f5c024c60cbdf9fe3172f7f) +- Bump codecov/codecov-action from 6.0.1 to 7.0.0 [`035807c`](https://github.com/acgetchell/la-stack/commit/035807c9312ce1ff818dcab9dd986980dfcb37d8) +- Bump vulnerable Python tooling dependencies [`4990260`](https://github.com/acgetchell/la-stack/commit/49902604db70de7c6831f559aee967bf3c376bc1) + +### Documentation + +- Add mathematical basis and clarify numerical guarantees [`31b7b1e`](https://github.com/acgetchell/la-stack/commit/31b7b1e7c547fc69edbb601e68b2f29e83d5cd24) + + - define the binary64, factorization, determinant-filter, exact-arithmetic, tolerance, and error contracts + - align README, API documentation, examples, and LDLT diagnostics with guarantees over stored values + - audit academic references and extend Criterion coverage for determinant error-bound paths +- Clarify determinant and LDLT failure contracts [`e257343`](https://github.com/acgetchell/la-stack/commit/e2573434c541a8115750b3a965882f7b484b6be0) + - State the determinant sign condition without ambiguous Markdown delimiters. + - Distinguish singular zero pivots from coupled zero-pivot LDLT rejections. + - Align citation and contributor guidance with the positive-definite domain. + +### Fixed + +- Make v0.4.3 comparisons correctness-aware [`4ac5af9`](https://github.com/acgetchell/la-stack/commit/4ac5af9ce104c5126bfd6217414977cca269b62e) + + - Adapt the shared benchmark harness across v0.4.3 API differences without changing measured operations + - Exclude invalid balanced-range baselines while requiring current samples and reporting unavailable comparisons + - Preserve benchmark provenance, suite-specific fallback commands, and publication rollback guarantees + - Harden Windows Git input, changelog links, and version-reference parsing across platforms +- Harden exact arithmetic and benchmark publication [`db2fad5`](https://github.com/acgetchell/la-stack/commit/db2fad50087c773f7ae37f181f720da2a9ff4a23) + - add `DeterminantWithErrorBound` for paired determinant estimates and certified bounds + - scale exact systems independently and round exact values directly to IEEE-754 + - fail benchmark publication closed on invalid samples or mismatched provenance + - make release and changelog tooling transactional, path-safe, and Windows-portable + - align benchmark CI with pinned local tools and least-privilege publishing + +### Maintenance + +- Update setuptools requirement in the dependencies group [`6a6c339`](https://github.com/acgetchell/la-stack/commit/6a6c339e7fbec1c9ca4226005794e8ab87f0c2d6) + + Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version. + + Updates `setuptools` to 83.0.0 + + - [Release notes](https://github.com/pypa/setuptools/releases) + - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) + - [Commits](https://github.com/pypa/setuptools/compare/v65.0.0...v83.0.0) +- Avoid duplicate review checks [`5e15d92`](https://github.com/acgetchell/la-stack/commit/5e15d9250455fd1467e47a735412f65449f30b56) + - Use the legacy required status while preserving automatic approvals. + - Add explicit dense D3 determinant coverage and document the D4 error bound. +- [**breaking**] Require Rust 1.97.0 [`4033f1f`](https://github.com/acgetchell/la-stack/commit/4033f1f39e411ed066fec808805371979995964b) + - Align Cargo, the pinned toolchain, Clippy, dependency-audit triggers, and maintainer guidance with the new baseline. + - Audit Rust 1.97 integer bit helpers against exact-arithmetic hot paths and retain existing operations where alternatives do not preserve performance. + - Correct v0.4.3 citation metadata and document exact decomposition and solve scaling accurately. + +### Performance + +- Restore det_direct throughput for D=2..4 [`f6d6885`](https://github.com/acgetchell/la-stack/commit/f6d68855a314fc4802c559080d9faca85db81bb9) + + - Reintroduce branch-free dense expansions for D=2 and D=3. + - Share D=4 minors while retaining guarded sparse evaluation. + - Preserve non-finite handling for mathematically inactive terms. +- Restore small-matrix det_sign_exact throughput [`b8bfa9f`](https://github.com/acgetchell/la-stack/commit/b8bfa9fa0c908ea034d294ceb123c8191eff3395) + - Reuse proof-bearing shared minors for D4 determinant and permanent evaluation. + - Restore the dense D3 filter while preserving sparse, overflow, and underflow fallbacks. + - Require headline benchmarks to exercise the intended filter and document historical harness overhead. +- Improve inf_norm throughput [`dc28f98`](https://github.com/acgetchell/la-stack/commit/dc28f9828a1e90b5604e4a55284175e50f84d7e2) + - Check row-sum finiteness once per completed row on the success path. + - Replay only overflowing rows to preserve the first failing matrix coordinate. + - Preserve left-to-right accumulation and const evaluation across dimensions. +- Improve dot and norm2_sq throughput [`ece54d7`](https://github.com/acgetchell/la-stack/commit/ece54d7b2a73111c2bc378a417189e2951a39cbc) + - Check accumulator finiteness once after the success-path reduction. + - Replay only non-finite reductions to preserve the first failing step. + - Preserve left-to-right fused accumulation and const evaluation. + ## [0.4.3] - 2026-06-09 ### ⚠️ Breaking Changes @@ -93,6 +195,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Align the Python utility package metadata and lockfile with the crate release. - Add citation metadata validation to the release checklist and config lint flow. - Include CITATION.cff in YAML/CFF formatting checks. +- Make release documentation links portable [`dd4ad19`](https://github.com/acgetchell/la-stack/commit/dd4ad192a42e28d9cc72b336b40802fe65cea4f0) ### Fixed @@ -672,6 +775,7 @@ Older releases are archived by minor series: - [0.2.x](docs/archive/changelog/0.2.md) - [0.1.x](docs/archive/changelog/0.1.md) +[0.4.4]: https://github.com/acgetchell/la-stack/compare/v0.4.3...v0.4.4 [0.4.3]: https://github.com/acgetchell/la-stack/compare/v0.4.2...v0.4.3 [0.4.2]: https://github.com/acgetchell/la-stack/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/acgetchell/la-stack/compare/v0.4.0...v0.4.1 diff --git a/CITATION.cff b/CITATION.cff index da83df8..0e79206 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." type: software title: "la-stack: Fast, stack-allocated linear algebra for fixed dimensions in Rust" -version: 0.4.3 -date-released: 2026-06-09 +version: 0.4.4 +date-released: 2026-07-12 url: "https://github.com/acgetchell/la-stack" repository-code: "https://github.com/acgetchell/la-stack" identifiers: diff --git a/Cargo.lock b/Cargo.lock index ed5ce58..1e3d832 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -639,7 +639,7 @@ dependencies = [ [[package]] name = "la-stack" -version = "0.4.3" +version = "0.4.4" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index ba84c60..099946d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "la-stack" -version = "0.4.3" +version = "0.4.4" edition = "2024" rust-version = "1.97.0" license = "BSD-3-Clause" diff --git a/README.md b/README.md index 89f44b0..42b7c3b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18158926.svg)](https://doi.org/10.5281/zenodo.18158926) [![Crates.io](https://badgen.net/crates/v/la-stack)](https://crates.io/crates/la-stack) [![Downloads](https://badgen.net/crates/d/la-stack)](https://crates.io/crates/la-stack) -[![License](https://badgen.net/github/license/acgetchell/la-stack)](https://github.com/acgetchell/la-stack/blob/v0.4.3/LICENSE) +[![License](https://badgen.net/github/license/acgetchell/la-stack)](https://github.com/acgetchell/la-stack/blob/v0.4.4/LICENSE) [![Docs.rs](https://docs.rs/la-stack/badge.svg)](https://docs.rs/la-stack) [![CI](https://github.com/acgetchell/la-stack/actions/workflows/ci.yml/badge.svg)](https://github.com/acgetchell/la-stack/actions/workflows/ci.yml) [![rust-clippy analyze][clippy-badge]][clippy-workflow] @@ -60,9 +60,9 @@ for the algorithms, validity boundaries, and supporting references. exact values allocate as required - ✅ `unsafe` forbidden -See [CHANGELOG.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/CHANGELOG.md) +See [CHANGELOG.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/CHANGELOG.md) for release history and -[docs/roadmap.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/docs/roadmap.md) +[docs/roadmap.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/roadmap.md) for current release planning. ## 🚫 Anti-goals @@ -102,7 +102,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -la-stack = "0.4.3" +la-stack = "0.4.4" ``` ### Feature flags @@ -256,7 +256,7 @@ rationals (this pulls in `num-bigint`, `num-rational`, and `num-traits` for ```toml [dependencies] -la-stack = { version = "0.4.3", features = ["exact"] } +la-stack = { version = "0.4.4", features = ["exact"] } ``` These routines are exact with respect to the finite binary64 values stored in @@ -501,8 +501,8 @@ breaking callers. ![LU solve (factor + solve): median time vs dimension][lu-solve-benchmark] Raw data: -[docs/assets/bench/vs_linalg_lu_solve_median.csv](https://github.com/acgetchell/la-stack/blob/v0.4.3/docs/assets/bench/vs_linalg_lu_solve_median.csv) -Historical provenance status: +[docs/assets/bench/vs_linalg_lu_solve_median.csv](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/assets/bench/vs_linalg_lu_solve_median.csv) +Measurement provenance: [docs/assets/bench/vs_linalg_lu_solve_median.provenance.json][benchmark-provenance] Representative benchmark: `lu_solve` factors the matrix and solves one @@ -515,34 +515,30 @@ across operations. Timings count only when the implementation preserves the documented correctness guarantees and invariants. Performance claims require comparable before-and-after evidence using the same inputs, configuration, and environment. -This v0.4.3 snapshot predates deterministic measurement-provenance capture, so -its CPU, operating system, Rust toolchain, exact measured source state, -dependency lock digest, and Criterion configuration are unavailable. The CSV -preserves confidence bounds, but without the missing configuration and -environment they do not make the result reproducible across environments. Treat -it as a historical snapshot, not reproducible cross-environment evidence. Future -`just plot-vs-linalg-readme` publications run the benchmark-input correctness -gate, require complete canonical-dimension coverage, and write deterministic -JSON provenance beside the CSV and SVG. +This snapshot records the measured source state, CPU, operating system, Rust +toolchain, dependency lock and harness digests, Criterion command, and +correctness-gate result in the adjacent JSON sidecar. The publication workflow +requires complete canonical-dimension coverage and regenerates the CSV, SVG, +README table, and provenance together. For the full per-kernel comparison methodology, input construction, and release-comparison workflow details, see -[docs/BENCHMARKING.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/docs/BENCHMARKING.md). +[docs/BENCHMARKING.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/BENCHMARKING.md). For the current release-to-release performance snapshot, see -[docs/PERFORMANCE.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/docs/PERFORMANCE.md). +[docs/PERFORMANCE.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/PERFORMANCE.md). -| D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | la-stack vs nalgebra | la-stack vs faer | +| D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | reduction vs nalgebra (point est.) | reduction vs faer (point est.) | |---:|--------------------:|--------------------:|----------------:|---------------------:|----------------:| -| 2 | 2.044 | 4.542 | 143.958 | +55.0% | +98.6% | -| 3 | 9.596 | 23.599 | 185.466 | +59.3% | +94.8% | -| 4 | 23.338 | 50.717 | 210.976 | +54.0% | +88.9% | -| 5 | 45.368 | 69.065 | 277.564 | +34.3% | +83.7% | -| 8 | 127.861 | 164.412 | 364.864 | +22.2% | +65.0% | -| 16 | 631.997 | 663.822 | 882.674 | +4.8% | +28.4% | -| 32 | 2,745.604 | 2,424.540 | 2,867.431 | -13.2% | +4.2% | -| 64 | 17,543.034 | 14,747.731 | 12,266.271 | -19.0% | -43.0% | +| 2 | 2.051 | 4.609 | 149.537 | +55.5% | +98.6% | +| 3 | 10.032 | 23.094 | 185.935 | +56.6% | +94.6% | +| 4 | 21.806 | 53.542 | 218.921 | +59.3% | +90.0% | +| 5 | 43.787 | 70.437 | 282.262 | +37.8% | +84.5% | +| 8 | 128.337 | 167.505 | 414.791 | +23.4% | +69.1% | +| 16 | 672.680 | 581.273 | 875.411 | -15.7% | +23.2% | +| 32 | 2,873.720 | 2,470.435 | 2,861.209 | -16.3% | -0.4% | +| 64 | 18,165.369 | 15,021.737 | 12,225.703 | -20.9% | -48.6% | @@ -597,14 +593,14 @@ CI runs `just ci` on Ubuntu, macOS, and Windows to keep platform coverage aligned with the local comprehensive validation path. For coverage commands and report locations, see -[`docs/COVERAGE.md`](https://github.com/acgetchell/la-stack/blob/v0.4.3/docs/COVERAGE.md). +[`docs/COVERAGE.md`](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/COVERAGE.md). For the full contributor workflow, see -[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/CONTRIBUTING.md). +[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/CONTRIBUTING.md). ## 📝 Citation If you use this library in academic work, please cite it using -[CITATION.cff](https://github.com/acgetchell/la-stack/blob/v0.4.3/CITATION.cff) +[CITATION.cff](https://github.com/acgetchell/la-stack/blob/v0.4.4/CITATION.cff) (or GitHub's "Cite this repository" feature). Tagged releases are archived on Zenodo under the [all-versions concept DOI](https://doi.org/10.5281/zenodo.18158926). @@ -612,23 +608,23 @@ Zenodo under the ## 📚 References For canonical references to the algorithms used by this crate, see -[REFERENCES.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/REFERENCES.md). +[REFERENCES.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/REFERENCES.md). ## 🤖 AI Agents AI coding assistants should read -[AGENTS.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/AGENTS.md) +[AGENTS.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/AGENTS.md) before proposing or applying changes. See -[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.3/CONTRIBUTING.md) +[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/CONTRIBUTING.md) for the repository's AI-assisted development note. ## 📄 License -BSD 3-Clause License. See [LICENSE](https://github.com/acgetchell/la-stack/blob/v0.4.3/LICENSE). +BSD 3-Clause License. See [LICENSE](https://github.com/acgetchell/la-stack/blob/v0.4.4/LICENSE). [audit-badge]: https://github.com/acgetchell/la-stack/actions/workflows/audit.yml/badge.svg [audit-workflow]: https://github.com/acgetchell/la-stack/actions/workflows/audit.yml -[benchmark-provenance]: https://github.com/acgetchell/la-stack/blob/668daed6/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json +[benchmark-provenance]: https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json [clippy-badge]: https://github.com/acgetchell/la-stack/actions/workflows/rust-clippy.yml/badge.svg [clippy-workflow]: https://github.com/acgetchell/la-stack/actions/workflows/rust-clippy.yml -[lu-solve-benchmark]: https://raw.githubusercontent.com/acgetchell/la-stack/v0.4.3/docs/assets/bench/vs_linalg_lu_solve_median.svg +[lu-solve-benchmark]: https://raw.githubusercontent.com/acgetchell/la-stack/v0.4.4/docs/assets/bench/vs_linalg_lu_solve_median.svg diff --git a/benches/vs_linalg.rs b/benches/vs_linalg.rs index da484f2..6274fc3 100644 --- a/benches/vs_linalg.rs +++ b/benches/vs_linalg.rs @@ -219,41 +219,29 @@ where let frhs = faer_vector::(0.0); group.bench_function("la_stack_lu_solve", |bencher| { - bencher.iter_batched( - || (a, rhs), - |(a, rhs)| { - let lu = black_box(a) - .lu(DEFAULT_SINGULAR_TOL) - .or_abort("la_stack LU factorization"); - let x = lu.solve(black_box(rhs)).or_abort("la_stack LU solve"); - let _ = black_box(x); - }, - BatchSize::SmallInput, - ); + bencher.iter(|| { + let lu = black_box(a) + .lu(DEFAULT_SINGULAR_TOL) + .or_abort("la_stack LU factorization"); + let x = lu.solve(black_box(rhs)).or_abort("la_stack LU solve"); + let _ = black_box(x); + }); }); group.bench_function("nalgebra_lu_solve", |bencher| { - bencher.iter_batched( - || (na, nrhs), - |(na, nrhs)| { - let lu = black_box(na).lu(); - let x = lu.solve(black_box(&nrhs)).or_abort("nalgebra LU solve"); - black_box(x); - }, - BatchSize::SmallInput, - ); + bencher.iter(|| { + let lu = black_box(na).lu(); + let x = lu.solve(black_box(&nrhs)).or_abort("nalgebra LU solve"); + black_box(x); + }); }); group.bench_function("faer_lu_solve", |bencher| { - bencher.iter_batched( - || (&fa, &frhs), - |(fa, rhs)| { - let lu = black_box(fa).partial_piv_lu(); - let x = lu.solve(black_box(rhs)); - black_box(x); - }, - BatchSize::SmallInput, - ); + bencher.iter(|| { + let lu = black_box(&fa).partial_piv_lu(); + let x = lu.solve(black_box(&frhs)); + black_box(x); + }); }); } diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index 162852d..db15e5a 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -25,12 +25,12 @@ the commands measure and where their outputs go. | Goal | Recipe | |------|--------| | Latest-release local audit | `just performance-local` | -| Release-signal check against tags | `just performance-local-vs-linalg v0.4.3 v0.4.2` | +| Release-signal check against tags | `just performance-local-vs-linalg v0.4.4 v0.4.3` | | Fast saved-baseline loop | `just bench-save-baseline ` then `just bench-compare all-benches` | | Full crate comparison | `just bench-vs-linalg` | | README table and plot | `just plot-vs-linalg-readme` | -| Release report | `just performance-release v0.4.3 v0.4.2` | -| Published-asset comparison | `just performance-github-assets v0.4.3 v0.4.2` | +| Release report | `just performance-release v0.4.4 v0.4.3` | +| Published-asset comparison | `just performance-github-assets v0.4.4 v0.4.3` | Rule of thumb: @@ -112,10 +112,10 @@ benchmark state. For a narrower non-exact check against a known release pair, run: ```bash -just performance-local-vs-linalg v0.4.3 v0.4.2 +just performance-local-vs-linalg v0.4.4 v0.4.3 ``` -This generates a local `v0.4.2` `vs_linalg` baseline, measures the current +This generates a local `v0.4.3` `vs_linalg` baseline, measures the current la-stack `vs_linalg` rows, and renders a `vs_linalg` report. The report includes saved baseline nalgebra/faer timings as context where matching peer rows exist, without rerunning current peer crates. @@ -169,9 +169,9 @@ current checkout: just plot-vs-linalg-readme ``` -This publication recipe validates the benchmark fixtures, runs a fresh complete -`vs_linalg` benchmark, and requires la-stack, nalgebra, and faer results for every -canonical dimension (D=2, 3, 4, 5, 8, 16, 32, and 64) before updating: +This publication recipe validates the benchmark fixtures, runs a fresh benchmark +for the selected metric only, and requires la-stack, nalgebra, and faer results +for every canonical dimension (D=2, 3, 4, 5, 8, 16, 32, and 64) before updating: - `README.md` - `docs/assets/bench/vs_linalg_lu_solve_median.csv` @@ -193,7 +193,7 @@ Release PRs promote one curated release-to-release comparison into committed docs: ```bash -just performance-release v0.4.3 v0.4.2 +just performance-release v0.4.4 v0.4.3 ``` With no arguments, `just performance-release` infers the current release tag @@ -216,7 +216,7 @@ requirement applies even when both release tags are supplied explicitly because the recipe still downloads their GitHub Release assets: ```bash -just performance-github-assets v0.4.3 v0.4.2 +just performance-github-assets v0.4.4 v0.4.3 ``` With no arguments, the recipe discovers the latest and previous stable @@ -265,15 +265,24 @@ All three crates receive equivalent deterministic inputs for a given dimension: generator - each benchmark uses `black_box` around inputs and outputs to keep the measured operation visible to the optimizer +- the `lu_solve` factor-plus-solve comparison uses direct Criterion + `bencher.iter` measurement; batching overhead is large enough at D=2 to + distort both the absolute timing and cross-crate ratio - precomputed-factor benchmarks pass the factor itself through `black_box` before each solve or determinant query, preventing invariant captured factors from being hoisted out of the measured closure -- consuming stack-matrix inputs are copied in Criterion batch setup, outside - the measured closure, so factorization rows measure kernels rather than the - harness's need to reuse one input +- `lu_solve` receives owned fixed-size `black_box` inputs inside the measured + closure, applying the same complete-operation protocol to la-stack and + nalgebra - borrowed operations receive references through `black_box`; in particular, `inf_norm` does not copy the matrix inside the measured closure +Use `iter_batched` only when fixture construction is explicitly outside the +scientific quantity being measured. The exclusion must be symmetric across the +compared implementations, documented beside the benchmark, and checked against +direct `iter` in the same binary to show that batching does not materially alter +the reported kernel time or cross-crate ratio. + The integration smoke test `tests/vs_linalg_inputs.rs` reuses the benchmark input helpers and verifies that la-stack, nalgebra, and faer agree on the determinant, solve, dot, squared-norm, and infinity-norm results for every diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index db6e35f..eb601ee 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -1,126 +1,353 @@ # Benchmark Performance -**la-stack** v0.4.3 · `45affa8` (HEAD) · 2026-06-09 08:41:32 UTC +**la-stack** v0.4.4 · `e736c5f` (HEAD) +**Source revision timestamp**: 2026-07-12 17:11:09 UTC (deterministic report metadata; not the benchmark measurement time) +**Benchmark measurement timestamp**: not recorded by Criterion; use the provenance below to identify the measured revisions and environment. **Statistic**: median -**Suite**: exact +**Suite**: all **Scope**: release-signal ## Benchmark Results -Comparison against baseline **v0.4.2**: - -**Harness provenance**: Historical per-release harnesses. These measurements -predate shared-harness enforcement, so differences may include benchmark-harness -changes as well as library changes. - -**Measurement provenance**: Unavailable. CPU, operating system, Rust toolchain, -full measured source states (including the baseline commit), dependency lock -digest, Criterion version/configuration, and confidence intervals were not -preserved with this historical report. The header's short hash identifies the -reporting checkout only; it does not establish both measured revisions. - -The change and speedup columns are descriptive ratios of median point estimates -only. A negative change or ratio above 1.00× means the recorded latest median was -lower; it does not establish a statistically supported speedup. Do not attribute -these differences to library code because the harnesses and environments were -not controlled or recorded comparably. +Comparison against baseline **v0.4.3**: + +Negative point-estimate change means the current point estimate is smaller; a baseline/current point-estimate ratio above 1.00 has the same meaning. +The CI-relation column reports only whether the two marginal Criterion intervals overlap. These are not paired confidence intervals +for the change, so the report makes no statistical-significance or performance-improvement claim from interval separation. + +### Reproducibility Provenance + +**Measurement environment**: recorded for both samples under one shared current harness. + +- CPU: `arm` +- OS: `Darwin 25.5.0 arm64` +- rustc: `rustc 1.97.0 (2d8144b78 2026-07-07)` +- Current commit: `e736c5fda155ef23c8712f89ae15bf5369ff3787` +- Current Git clean: `false` +- Current source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2` +- Baseline commit: `dd4ad192a42e28d9cc72b336b40802fe65cea4f0` +- Baseline Git clean: `false` +- Baseline source-state SHA-256: `1d77b9640c3fe8dce45b486052177fe4d9a44b840605661da0717b8cb0cec9b8` +- Cargo.lock SHA-256: `0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd` +- Benchmark harness SHA-256: `c2cc116cf2d77d415f12f30a60bcf3e1b9e0d553c884bc3c57ce6c953124bc08` + +**Publication and validation environment**: + +- Publication CPU: `arm` +- Publication OS: `Darwin 25.5.0 arm64` +- Publication rustc: `rustc 1.97.0 (2d8144b78 2026-07-07)` +- Publication commit: `e736c5fda155ef23c8712f89ae15bf5369ff3787` +- Publication Git clean: `false` +- Publication source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2` +- Publication Cargo.lock SHA-256: `0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd` +- Publication harness SHA-256: `c2cc116cf2d77d415f12f30a60bcf3e1b9e0d553c884bc3c57ce6c953124bc08` +- Criterion suite/scope: `all` / `release-signal` +- Criterion statistic/sample: `median` / `new` +- Criterion dependency version: `0.8.2` +- Baseline command: `just bench-save-baseline v0.4.3` +- Current command: `just bench-latest` +- Correctness gate: `just test-bench-inputs` passed against both the current and baseline revisions using the shared current fixture harness. +- Validated current revision: `e736c5fda155ef23c8712f89ae15bf5369ff3787` (Git clean: `false`; + source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2`) +- Validated baseline revision: `dd4ad192a42e28d9cc72b336b40802fe65cea4f0` (Git clean: `false`; + source-state SHA-256: `1d77b9640c3fe8dce45b486052177fe4d9a44b840605661da0717b8cb0cec9b8`) +- Baseline API compatibility: `la_stack_v0_4_3_api` selects only source-compatible benchmark calls; + rows outside the baseline's correctness domain remain explicitly unavailable. +- Baseline-unavailable rows: `d8/la_stack_det_from_lu_balanced_range` and + `d8/la_stack_det_from_ldlt_balanced_range` were not timed because v0.4.3 returns zero for a + fixture whose exact determinant is one; current samples remain required, but no speedup is claimed. +- Baseline-unavailable rows: `exact_d2/det_direct_with_errbound`, + `exact_d3/det_direct_with_errbound`, and `exact_d4/det_direct_with_errbound` were not timed + because v0.4.3 predates the paired API; the comparable `det_errbound` baselines remain required. ## Exact arithmetic ### D=2 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det | 0.9 ns | 0.7 ns | -24.0% | 1.32x | -| det_direct | 1.0 ns | 1.0 ns | +2.1% | 0.98x | -| det_exact | 248.9 ns | 195.6 ns | -21.4% | 1.27x | -| det_exact_f64_result (vs det_exact_f64) | 429.1 ns | 167.6 ns | -60.9% | 2.56x | -| det_exact_rounded_f64 (vs det_exact_f64) | 429.1 ns | 375.2 ns | -12.6% | 1.14x | -| det_sign_exact | 1.5 ns | 3.2 ns | +115.9% | 0.46x | -| solve_exact | 6.53 µs | 6.45 µs | -1.1% | 1.01x | -| solve_exact_f64_result (vs solve_exact_f64) | 6.90 µs | 6.60 µs | -4.4% | 1.05x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 6.90 µs | 7.02 µs | +1.7% | 0.98x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -11.0% | faster point estimate; marginal CIs separated | 1.12x | +| det_direct | 0.6 ns [0.6 ns, 0.6 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -28.6% | faster point estimate; marginal CIs separated | 1.40x | +| det_errbound | 0.7 ns [0.7 ns, 0.7 ns] | 1.6 ns [1.6 ns, 1.6 ns] | +128.0% | slower point estimate; marginal CIs separated | 0.44x | +| det_exact | 95.7 ns [95.5 ns, 95.8 ns] | 89.3 ns [89.2 ns, 89.4 ns] | -6.7% | faster point estimate; marginal CIs separated | 1.07x | +| det_exact_f64_result | 77.0 ns [76.9 ns, 77.1 ns] | 65.5 ns [65.4 ns, 65.5 ns] | -15.0% | faster point estimate; marginal CIs separated | 1.18x | +| det_exact_rounded_f64 | 245.4 ns [244.9 ns, 245.9 ns] | 66.5 ns [66.5 ns, 66.6 ns] | -72.9% | faster point estimate; marginal CIs separated | 3.69x | +| det_sign_exact | 2.0 ns [2.0 ns, 2.0 ns] | 2.8 ns [2.8 ns, 2.8 ns] | +41.2% | slower point estimate; marginal CIs separated | 0.71x | +| solve_exact | 6.73 µs [6.72 µs, 6.74 µs] | 7.33 µs [7.32 µs, 7.34 µs] | +8.9% | slower point estimate; marginal CIs separated | 0.92x | +| solve_exact_f64_result | 6.89 µs [6.88 µs, 6.91 µs] | 8.38 µs [8.37 µs, 8.39 µs] | +21.6% | slower point estimate; marginal CIs separated | 0.82x | +| solve_exact_rounded_f64 | 7.08 µs [7.07 µs, 7.10 µs] | 7.66 µs [7.64 µs, 7.68 µs] | +8.1% | slower point estimate; marginal CIs separated | 0.93x | ### D=3 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det | 1.8 ns | 1.5 ns | -19.4% | 1.24x | -| det_direct | 2.0 ns | 2.0 ns | +2.4% | 0.98x | -| det_exact | 739.0 ns | 468.6 ns | -36.6% | 1.58x | -| det_exact_f64_result (vs det_exact_f64) | 913.1 ns | 435.6 ns | -52.3% | 2.10x | -| det_exact_rounded_f64 (vs det_exact_f64) | 913.1 ns | 648.1 ns | -29.0% | 1.41x | -| det_sign_exact | 4.2 ns | 5.5 ns | +30.9% | 0.76x | -| solve_exact | 25.69 µs | 25.16 µs | -2.1% | 1.02x | -| solve_exact_f64_result (vs solve_exact_f64) | 26.16 µs | 25.42 µs | -2.8% | 1.03x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 26.16 µs | 25.67 µs | -1.9% | 1.02x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 1.0 ns [1.0 ns, 1.0 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -17.2% | faster point estimate; marginal CIs separated | 1.21x | +| det_direct | 1.0 ns [1.0 ns, 1.0 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -16.6% | faster point estimate; marginal CIs separated | 1.20x | +| det_errbound | 1.6 ns [1.6 ns, 1.6 ns] | 3.4 ns [3.4 ns, 3.4 ns] | +112.5% | slower point estimate; marginal CIs separated | 0.47x | +| det_exact | 334.2 ns [333.8 ns, 335.3 ns] | 320.7 ns [320.2 ns, 321.0 ns] | -4.1% | faster point estimate; marginal CIs separated | 1.04x | +| det_exact_f64_result | 311.4 ns [310.4 ns, 313.1 ns] | 294.9 ns [294.0 ns, 295.3 ns] | -5.3% | faster point estimate; marginal CIs separated | 1.06x | +| det_exact_rounded_f64 | 505.9 ns [503.2 ns, 507.1 ns] | 294.9 ns [294.2 ns, 295.5 ns] | -41.7% | faster point estimate; marginal CIs separated | 1.72x | +| det_sign_exact | 3.7 ns [3.7 ns, 3.7 ns] | 4.7 ns [4.7 ns, 4.7 ns] | +26.9% | slower point estimate; marginal CIs separated | 0.79x | +| solve_exact | 29.70 µs [29.65 µs, 29.77 µs] | 31.46 µs [31.37 µs, 31.51 µs] | +5.9% | slower point estimate; marginal CIs separated | 0.94x | +| solve_exact_f64_result | 29.88 µs [29.80 µs, 30.01 µs] | 33.01 µs [32.96 µs, 33.11 µs] | +10.5% | slower point estimate; marginal CIs separated | 0.90x | +| solve_exact_rounded_f64 | 30.48 µs [30.41 µs, 30.58 µs] | 31.82 µs [31.76 µs, 31.84 µs] | +4.4% | slower point estimate; marginal CIs separated | 0.96x | ### D=4 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det | 3.3 ns | 4.5 ns | +38.1% | 0.72x | -| det_direct | 3.7 ns | 4.3 ns | +17.6% | 0.85x | -| det_exact | 1.87 µs | 1.47 µs | -21.8% | 1.28x | -| det_exact_f64_result (vs det_exact_f64) | 2.04 µs | 1.47 µs | -27.9% | 1.39x | -| det_exact_rounded_f64 (vs det_exact_f64) | 2.04 µs | 1.63 µs | -19.8% | 1.25x | -| det_sign_exact | 6.9 ns | 11.5 ns | +67.1% | 0.60x | -| solve_exact | 64.95 µs | 61.67 µs | -5.1% | 1.05x | -| solve_exact_f64_result (vs solve_exact_f64) | 66.35 µs | 62.37 µs | -6.0% | 1.06x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 66.35 µs | 63.59 µs | -4.2% | 1.04x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 4.5 ns [4.5 ns, 4.5 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -47.7% | faster point estimate; marginal CIs separated | 1.91x | +| det_direct | 4.3 ns [4.3 ns, 4.3 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -47.0% | faster point estimate; marginal CIs separated | 1.89x | +| det_errbound | 6.9 ns [6.9 ns, 6.9 ns] | 6.7 ns [6.7 ns, 6.7 ns] | -2.4% | faster point estimate; marginal CIs separated | 1.02x | +| det_exact | 1.10 µs [1.10 µs, 1.10 µs] | 994.3 ns [990.6 ns, 998.2 ns] | -9.5% | faster point estimate; marginal CIs separated | 1.11x | +| det_exact_f64_result | 1.09 µs [1.09 µs, 1.10 µs] | 959.7 ns [955.9 ns, 961.8 ns] | -12.1% | faster point estimate; marginal CIs separated | 1.14x | +| det_exact_rounded_f64 | 1.28 µs [1.27 µs, 1.29 µs] | 963.6 ns [962.2 ns, 964.4 ns] | -24.6% | faster point estimate; marginal CIs separated | 1.33x | +| det_sign_exact | 11.3 ns [11.3 ns, 11.3 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -31.5% | faster point estimate; marginal CIs separated | 1.46x | +| solve_exact | 77.25 µs [77.10 µs, 77.40 µs] | 79.77 µs [79.57 µs, 79.95 µs] | +3.3% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact_f64_result | 76.96 µs [76.80 µs, 77.13 µs] | 83.27 µs [83.05 µs, 83.39 µs] | +8.2% | slower point estimate; marginal CIs separated | 0.92x | +| solve_exact_rounded_f64 | 77.73 µs [77.58 µs, 77.82 µs] | 81.50 µs [81.39 µs, 81.62 µs] | +4.8% | slower point estimate; marginal CIs separated | 0.95x | ### D=5 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det | 26.0 ns | 23.3 ns | -10.6% | 1.12x | -| det_exact | 4.10 µs | 4.05 µs | -1.3% | 1.01x | -| det_exact_f64_result (vs det_exact_f64) | 4.21 µs | 4.02 µs | -4.4% | 1.05x | -| det_exact_rounded_f64 (vs det_exact_f64) | 4.21 µs | 4.33 µs | +2.8% | 0.97x | -| det_sign_exact | 3.94 µs | 3.96 µs | +0.6% | 0.99x | -| solve_exact | 130.82 µs | 126.75 µs | -3.1% | 1.03x | -| solve_exact_f64_result (vs solve_exact_f64) | 132.70 µs | 127.37 µs | -4.0% | 1.04x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 132.70 µs | 128.15 µs | -3.4% | 1.04x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 22.0 ns [22.0 ns, 22.1 ns] | 25.5 ns [25.4 ns, 25.6 ns] | +16.0% | slower point estimate; marginal CIs separated | 0.86x | +| det_exact | 3.08 µs [3.08 µs, 3.09 µs] | 2.93 µs [2.93 µs, 2.94 µs] | -5.0% | faster point estimate; marginal CIs separated | 1.05x | +| det_exact_f64_result | 3.07 µs [3.07 µs, 3.07 µs] | 2.88 µs [2.88 µs, 2.89 µs] | -6.2% | faster point estimate; marginal CIs separated | 1.07x | +| det_exact_rounded_f64 | 3.30 µs [3.29 µs, 3.30 µs] | 2.90 µs [2.89 µs, 2.90 µs] | -12.1% | faster point estimate; marginal CIs separated | 1.14x | +| det_sign_exact | 3.06 µs [3.05 µs, 3.07 µs] | 2.99 µs [2.98 µs, 3.00 µs] | -2.4% | faster point estimate; marginal CIs separated | 1.03x | +| solve_exact | 151.99 µs [151.61 µs, 152.39 µs] | 159.24 µs [158.84 µs, 159.40 µs] | +4.8% | slower point estimate; marginal CIs separated | 0.95x | +| solve_exact_f64_result | 151.95 µs [151.73 µs, 152.26 µs] | 162.98 µs [162.62 µs, 163.59 µs] | +7.3% | slower point estimate; marginal CIs separated | 0.93x | +| solve_exact_rounded_f64 | 153.08 µs [152.82 µs, 153.19 µs] | 159.84 µs [159.58 µs, 160.22 µs] | +4.4% | slower point estimate; marginal CIs separated | 0.96x | + +### Random corpus D=2 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 99.8 ns [99.2 ns, 100.4 ns] | 136.1 ns [136.0 ns, 136.7 ns] | +36.4% | slower point estimate; marginal CIs separated | 0.73x | +| det_exact | 3.22 µs [3.22 µs, 3.23 µs] | 3.06 µs [3.05 µs, 3.06 µs] | -5.2% | faster point estimate; marginal CIs separated | 1.05x | +| solve_exact | 65.74 µs [65.66 µs, 65.83 µs] | 67.56 µs [67.42 µs, 67.79 µs] | +2.8% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact_f64_result | 66.67 µs [66.58 µs, 66.75 µs] | 77.80 µs [77.55 µs, 77.91 µs] | +16.7% | slower point estimate; marginal CIs separated | 0.86x | +| solve_exact_rounded_f64 | 66.67 µs [66.58 µs, 66.69 µs] | 69.23 µs [69.09 µs, 69.35 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | + +### Random corpus D=3 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 174.3 ns [174.2 ns, 174.5 ns] | 225.1 ns [225.0 ns, 225.3 ns] | +29.1% | slower point estimate; marginal CIs separated | 0.77x | +| det_exact | 8.43 µs [8.39 µs, 8.47 µs] | 7.76 µs [7.72 µs, 7.81 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | +| solve_exact | 212.03 µs [211.85 µs, 212.21 µs] | 216.24 µs [216.02 µs, 216.47 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | +| solve_exact_f64_result | 213.65 µs [213.11 µs, 214.14 µs] | 229.74 µs [229.51 µs, 230.18 µs] | +7.5% | slower point estimate; marginal CIs separated | 0.93x | +| solve_exact_rounded_f64 | 213.73 µs [213.55 µs, 214.10 µs] | 218.02 µs [217.72 µs, 218.45 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | + +### Random corpus D=4 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 534.8 ns [534.5 ns, 535.0 ns] | 426.5 ns [425.8 ns, 427.4 ns] | -20.3% | faster point estimate; marginal CIs separated | 1.25x | +| det_exact | 28.60 µs [28.34 µs, 28.91 µs] | 24.12 µs [24.09 µs, 24.16 µs] | -15.7% | faster point estimate; marginal CIs separated | 1.19x | +| solve_exact | 486.14 µs [485.47 µs, 487.12 µs] | 495.70 µs [494.68 µs, 496.46 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | +| solve_exact_f64_result | 487.15 µs [486.57 µs, 487.66 µs] | 512.02 µs [511.01 µs, 513.36 µs] | +5.1% | slower point estimate; marginal CIs separated | 0.95x | +| solve_exact_rounded_f64 | 489.02 µs [487.59 µs, 489.93 µs] | 497.20 µs [495.79 µs, 498.14 µs] | +1.7% | slower point estimate; marginal CIs separated | 0.98x | + +### Random corpus D=5 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 51.48 µs [51.17 µs, 51.83 µs] | 48.07 µs [47.59 µs, 48.89 µs] | -6.6% | faster point estimate; marginal CIs separated | 1.07x | +| det_exact | 52.09 µs [51.90 µs, 52.27 µs] | 47.94 µs [47.67 µs, 48.20 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | +| solve_exact | 963.73 µs [961.57 µs, 964.57 µs] | 978.88 µs [977.33 µs, 979.71 µs] | +1.6% | slower point estimate; marginal CIs separated | 0.98x | +| solve_exact_f64_result | 960.79 µs [958.85 µs, 962.50 µs] | 995.01 µs [993.26 µs, 997.47 µs] | +3.6% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact_rounded_f64 | 964.92 µs [963.08 µs, 966.28 µs] | 982.14 µs [980.67 µs, 983.66 µs] | +1.8% | slower point estimate; marginal CIs separated | 0.98x | ### Near-singular 3x3 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det_sign_exact | 705.2 ns | 444.2 ns | -37.0% | 1.59x | -| det_exact | 724.0 ns | 478.9 ns | -33.9% | 1.51x | -| solve_exact | 3.44 µs | 3.39 µs | -1.6% | 1.02x | -| solve_exact_f64_result (vs solve_exact_f64) | 3.47 µs | 3.36 µs | -3.2% | 1.03x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 3.47 µs | 3.39 µs | -2.5% | 1.03x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 350.2 ns [348.8 ns, 352.5 ns] | 349.4 ns [348.4 ns, 350.5 ns] | -0.2% | marginal CIs overlap | 1.00x | +| det_exact | 306.5 ns [305.5 ns, 307.4 ns] | 304.5 ns [303.9 ns, 305.1 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | +| solve_exact | 2.30 µs [2.29 µs, 2.30 µs] | 2.32 µs [2.31 µs, 2.32 µs] | +0.9% | slower point estimate; marginal CIs separated | 0.99x | +| solve_exact_f64_result | 2.30 µs [2.30 µs, 2.31 µs] | 2.42 µs [2.41 µs, 2.42 µs] | +5.0% | slower point estimate; marginal CIs separated | 0.95x | +| solve_exact_rounded_f64 | 2.32 µs [2.31 µs, 2.32 µs] | 2.32 µs [2.32 µs, 2.33 µs] | +0.3% | marginal CIs overlap | 1.00x | ### Large entries 3x3 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det_sign_exact | 2.91 µs | 402.4 ns | -86.2% | 7.23x | -| det_exact | 2.94 µs | 434.0 ns | -85.2% | 6.76x | -| solve_exact | 82.81 µs | 81.57 µs | -1.5% | 1.02x | -| solve_exact_f64_result (vs solve_exact_f64) | 84.32 µs | 81.66 µs | -3.1% | 1.03x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 84.32 µs | 82.04 µs | -2.7% | 1.03x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 283.6 ns [283.2 ns, 286.8 ns] | 281.7 ns [280.8 ns, 282.9 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | +| det_exact | 286.3 ns [285.5 ns, 288.1 ns] | 295.7 ns [294.0 ns, 296.9 ns] | +3.3% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact | 90.64 µs [90.57 µs, 90.79 µs] | 94.07 µs [94.01 µs, 94.20 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_f64_result | 91.18 µs [91.03 µs, 91.25 µs] | 95.07 µs [94.88 µs, 95.21 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_rounded_f64 | 91.51 µs [91.41 µs, 91.63 µs] | 94.47 µs [94.34 µs, 94.60 µs] | +3.2% | slower point estimate; marginal CIs separated | 0.97x | ### Hilbert 4x4 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det_sign_exact | 6.9 ns | 11.5 ns | +66.4% | 0.60x | -| det_exact | 1.91 µs | 1.50 µs | -21.7% | 1.28x | -| solve_exact | 49.42 µs | 47.77 µs | -3.3% | 1.03x | -| solve_exact_f64_result (vs solve_exact_f64) | 50.38 µs | 47.67 µs | -5.4% | 1.06x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 50.38 µs | 48.17 µs | -4.4% | 1.05x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 11.3 ns [11.3 ns, 11.3 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -31.5% | faster point estimate; marginal CIs separated | 1.46x | +| det_exact | 1.24 µs [1.24 µs, 1.25 µs] | 1.09 µs [1.09 µs, 1.09 µs] | -12.3% | faster point estimate; marginal CIs separated | 1.14x | +| solve_exact | 57.23 µs [57.13 µs, 57.41 µs] | 59.81 µs [59.71 µs, 59.94 µs] | +4.5% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_f64_result | 57.31 µs [57.18 µs, 57.62 µs] | 61.72 µs [61.51 µs, 61.85 µs] | +7.7% | slower point estimate; marginal CIs separated | 0.93x | +| solve_exact_rounded_f64 | 58.02 µs [57.92 µs, 58.14 µs] | 60.30 µs [60.18 µs, 60.43 µs] | +3.9% | slower point estimate; marginal CIs separated | 0.96x | ### Hilbert 5x5 -| Benchmark | v0.4.2 | Latest | Change | Speedup | -|-----------|-------:|-------:|-------:|--------:| -| det_sign_exact | 4.09 µs | 3.91 µs | -4.6% | 1.05x | -| det_exact | 4.00 µs | 4.02 µs | +0.6% | 0.99x | -| solve_exact | 98.71 µs | 95.41 µs | -3.4% | 1.03x | -| solve_exact_f64_result (vs solve_exact_f64) | 99.88 µs | 98.14 µs | -1.7% | 1.02x | -| solve_exact_rounded_f64 (vs solve_exact_f64) | 99.88 µs | 97.50 µs | -2.4% | 1.02x | +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 3.23 µs [3.22 µs, 3.24 µs] | 3.23 µs [3.23 µs, 3.24 µs] | +0.1% | marginal CIs overlap | 1.00x | +| det_exact | 3.26 µs [3.25 µs, 3.27 µs] | 3.00 µs [2.99 µs, 3.01 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | +| solve_exact | 119.87 µs [119.55 µs, 120.21 µs] | 124.37 µs [123.95 µs, 124.57 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_f64_result | 119.44 µs [119.30 µs, 119.59 µs] | 127.21 µs [126.96 µs, 127.34 µs] | +6.5% | slower point estimate; marginal CIs separated | 0.94x | +| solve_exact_rounded_f64 | 120.33 µs [120.13 µs, 120.48 µs] | 125.50 µs [125.24 µs, 125.78 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | + +## vs_linalg + +### D=2 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 1.6 ns [1.6 ns, 1.6 ns] | 2.7 ns [2.7 ns, 2.7 ns] | +67.9% | slower point estimate; marginal CIs separated | 0.60x | 0.8 ns [0.8 ns, 0.8 ns] | 99.0 ns [97.7 ns, 99.4 ns] | +| la_stack_det | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -16.0% | faster point estimate; marginal CIs separated | 1.19x | — | — | +| la_stack_lu | 1.3 ns [1.3 ns, 1.4 ns] | 1.9 ns [1.9 ns, 1.9 ns] | +40.5% | slower point estimate; marginal CIs separated | 0.71x | 1.6 ns [1.6 ns, 1.6 ns] | 87.9 ns [87.6 ns, 88.2 ns] | +| la_stack_ldlt | 2.1 ns [2.1 ns, 2.1 ns] | 6.7 ns [6.6 ns, 6.8 ns] | +225.3% | slower point estimate; marginal CIs separated | 0.31x | 1.7 ns [1.7 ns, 1.7 ns] | 79.3 ns [78.8 ns, 79.7 ns] | +| la_stack_lu_solve | 2.0 ns [2.0 ns, 2.0 ns] | 2.0 ns [2.0 ns, 2.0 ns] | -0.6% | faster point estimate; marginal CIs separated | 1.01x | 4.5 ns [4.5 ns, 4.5 ns] | 147.8 ns [147.2 ns, 148.8 ns] | +| la_stack_ldlt_solve | 4.0 ns [4.0 ns, 4.0 ns] | 10.0 ns [9.9 ns, 10.0 ns] | +146.2% | slower point estimate; marginal CIs separated | 0.41x | 2.8 ns [2.8 ns, 2.8 ns] | 122.8 ns [122.2 ns, 123.1 ns] | +| la_stack_solve_from_lu | 1.2 ns [1.2 ns, 1.2 ns] | 1.3 ns [1.2 ns, 1.3 ns] | +0.7% | slower point estimate; marginal CIs separated | 0.99x | 2.7 ns [2.7 ns, 2.7 ns] | 47.2 ns [47.1 ns, 47.4 ns] | +| la_stack_solve_from_ldlt | 1.2 ns [1.2 ns, 1.2 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +3.4% | slower point estimate; marginal CIs separated | 0.97x | 1.3 ns [1.3 ns, 1.3 ns] | 38.6 ns [38.0 ns, 39.0 ns] | +| la_stack_det_from_lu | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | +4.7% | slower point estimate; marginal CIs separated | 0.95x | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| la_stack_det_from_ldlt | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | +0.4% | slower point estimate; marginal CIs separated | 1.00x | 0.4 ns [0.4 ns, 0.4 ns] | 0.6 ns [0.6 ns, 0.6 ns] | +| la_stack_dot | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -14.0% | faster point estimate; marginal CIs separated | 1.16x | 0.6 ns [0.6 ns, 0.6 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -20.2% | faster point estimate; marginal CIs separated | 1.25x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| la_stack_inf_norm | 0.8 ns [0.8 ns, 0.8 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -24.2% | faster point estimate; marginal CIs separated | 1.32x | 0.5 ns [0.5 ns, 0.5 ns] | 0.8 ns [0.8 ns, 0.8 ns] | + +### D=3 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 7.5 ns [7.5 ns, 7.5 ns] | 10.6 ns [10.6 ns, 10.6 ns] | +41.0% | slower point estimate; marginal CIs separated | 0.71x | 16.7 ns [16.6 ns, 16.8 ns] | 137.9 ns [137.1 ns, 138.9 ns] | +| la_stack_det | 1.5 ns [1.5 ns, 1.5 ns] | 2.2 ns [2.2 ns, 2.2 ns] | +46.4% | slower point estimate; marginal CIs separated | 0.68x | — | — | +| la_stack_lu | 9.1 ns [9.1 ns, 9.1 ns] | 8.5 ns [8.4 ns, 8.5 ns] | -7.0% | faster point estimate; marginal CIs separated | 1.08x | 15.2 ns [15.1 ns, 15.4 ns] | 127.3 ns [126.3 ns, 128.1 ns] | +| la_stack_ldlt | 7.1 ns [7.1 ns, 7.1 ns] | 14.0 ns [14.0 ns, 14.1 ns] | +97.2% | slower point estimate; marginal CIs separated | 0.51x | 4.1 ns [4.1 ns, 4.1 ns] | 93.6 ns [93.2 ns, 94.0 ns] | +| la_stack_lu_solve | 9.6 ns [9.6 ns, 9.7 ns] | 9.9 ns [9.9 ns, 9.9 ns] | +2.8% | slower point estimate; marginal CIs separated | 0.97x | 23.0 ns [22.9 ns, 23.2 ns] | 191.2 ns [190.5 ns, 191.8 ns] | +| la_stack_ldlt_solve | 8.6 ns [8.6 ns, 8.7 ns] | 15.4 ns [15.3 ns, 15.4 ns] | +78.5% | slower point estimate; marginal CIs separated | 0.56x | 8.2 ns [8.2 ns, 8.2 ns] | 137.6 ns [137.3 ns, 138.2 ns] | +| la_stack_solve_from_lu | 2.1 ns [2.1 ns, 2.1 ns] | 2.1 ns [2.1 ns, 2.1 ns] | -0.3% | faster point estimate; marginal CIs separated | 1.00x | 4.4 ns [4.4 ns, 4.4 ns] | 49.5 ns [49.3 ns, 49.7 ns] | +| la_stack_solve_from_ldlt | 1.8 ns [1.8 ns, 1.8 ns] | 1.8 ns [1.8 ns, 1.8 ns] | +0.0% | marginal CIs overlap | 1.00x | 2.9 ns [2.9 ns, 2.9 ns] | 38.2 ns [37.8 ns, 38.5 ns] | +| la_stack_det_from_lu | 0.6 ns [0.6 ns, 0.6 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +28.1% | slower point estimate; marginal CIs separated | 0.78x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | +| la_stack_det_from_ldlt | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +40.3% | slower point estimate; marginal CIs separated | 0.71x | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| la_stack_dot | 0.8 ns [0.8 ns, 0.8 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -12.8% | faster point estimate; marginal CIs separated | 1.15x | 0.7 ns [0.7 ns, 0.7 ns] | 0.9 ns [0.9 ns, 0.9 ns] | +| la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -18.4% | faster point estimate; marginal CIs separated | 1.22x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.1 ns, 4.2 ns] | +| la_stack_inf_norm | 1.7 ns [1.7 ns, 1.7 ns] | 1.3 ns [1.3 ns, 1.3 ns] | -24.1% | faster point estimate; marginal CIs separated | 1.32x | 1.1 ns [1.1 ns, 1.1 ns] | 1.3 ns [1.3 ns, 1.3 ns] | + +### D=4 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 12.6 ns [12.6 ns, 12.7 ns] | 17.0 ns [16.9 ns, 17.0 ns] | +34.3% | slower point estimate; marginal CIs separated | 0.74x | 30.6 ns [30.5 ns, 30.6 ns] | 162.0 ns [161.2 ns, 164.0 ns] | +| la_stack_det | 6.1 ns [6.1 ns, 6.1 ns] | 4.9 ns [4.9 ns, 4.9 ns] | -20.1% | faster point estimate; marginal CIs separated | 1.25x | — | — | +| la_stack_lu | 13.7 ns [13.6 ns, 13.7 ns] | 13.8 ns [13.8 ns, 13.8 ns] | +0.9% | slower point estimate; marginal CIs separated | 0.99x | 29.6 ns [29.5 ns, 29.7 ns] | 148.2 ns [147.6 ns, 148.9 ns] | +| la_stack_ldlt | 12.6 ns [12.6 ns, 12.7 ns] | 21.5 ns [21.4 ns, 21.5 ns] | +69.8% | slower point estimate; marginal CIs separated | 0.59x | 10.0 ns [10.0 ns, 10.1 ns] | 115.8 ns [115.2 ns, 116.2 ns] | +| la_stack_lu_solve | 22.2 ns [22.1 ns, 22.2 ns] | 21.6 ns [21.6 ns, 21.6 ns] | -2.8% | faster point estimate; marginal CIs separated | 1.03x | 51.9 ns [51.8 ns, 52.0 ns] | 217.5 ns [216.4 ns, 218.8 ns] | +| la_stack_ldlt_solve | 17.5 ns [17.5 ns, 17.6 ns] | 26.8 ns [26.8 ns, 26.8 ns] | +52.7% | slower point estimate; marginal CIs separated | 0.65x | 14.9 ns [14.8 ns, 14.9 ns] | 159.4 ns [159.2 ns, 159.8 ns] | +| la_stack_solve_from_lu | 4.0 ns [4.0 ns, 4.0 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -0.2% | faster point estimate; marginal CIs separated | 1.00x | 5.1 ns [5.1 ns, 5.1 ns] | 51.8 ns [51.7 ns, 51.9 ns] | +| la_stack_solve_from_ldlt | 2.5 ns [2.5 ns, 2.5 ns] | 2.5 ns [2.5 ns, 2.5 ns] | +0.1% | marginal CIs overlap | 1.00x | 5.5 ns [5.5 ns, 5.5 ns] | 39.0 ns [38.7 ns, 39.5 ns] | +| la_stack_det_from_lu | 0.7 ns [0.7 ns, 0.7 ns] | 0.9 ns [0.9 ns, 0.9 ns] | +21.1% | slower point estimate; marginal CIs separated | 0.83x | 0.6 ns [0.6 ns, 0.6 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +| la_stack_det_from_ldlt | 0.7 ns [0.7 ns, 0.7 ns] | 1.0 ns [0.9 ns, 1.0 ns] | +47.9% | slower point estimate; marginal CIs separated | 0.68x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | +| la_stack_dot | 1.0 ns [1.0 ns, 1.0 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -29.3% | faster point estimate; marginal CIs separated | 1.41x | 0.7 ns [0.7 ns, 0.7 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +| la_stack_norm2_sq | 0.7 ns [0.7 ns, 0.7 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -32.7% | faster point estimate; marginal CIs separated | 1.49x | 0.5 ns [0.5 ns, 0.5 ns] | 4.2 ns [4.1 ns, 4.2 ns] | +| la_stack_inf_norm | 3.0 ns [3.0 ns, 3.0 ns] | 2.2 ns [2.2 ns, 2.2 ns] | -25.4% | faster point estimate; marginal CIs separated | 1.34x | 2.0 ns [2.0 ns, 2.0 ns] | 2.0 ns [2.0 ns, 2.0 ns] | + +### D=5 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 26.1 ns [26.0 ns, 26.1 ns] | 37.8 ns [37.8 ns, 38.0 ns] | +45.0% | slower point estimate; marginal CIs separated | 0.69x | 55.6 ns [55.4 ns, 55.7 ns] | 203.2 ns [201.2 ns, 206.1 ns] | +| la_stack_det | 26.5 ns [26.4 ns, 26.5 ns] | 39.6 ns [39.6 ns, 39.9 ns] | +49.6% | slower point estimate; marginal CIs separated | 0.67x | — | — | +| la_stack_lu | 26.8 ns [26.7 ns, 26.9 ns] | 32.3 ns [32.2 ns, 32.5 ns] | +20.6% | slower point estimate; marginal CIs separated | 0.83x | 54.7 ns [54.6 ns, 54.9 ns] | 194.4 ns [191.2 ns, 196.0 ns] | +| la_stack_ldlt | 19.8 ns [19.7 ns, 19.9 ns] | 42.2 ns [42.1 ns, 42.2 ns] | +113.0% | slower point estimate; marginal CIs separated | 0.47x | 14.7 ns [14.6 ns, 14.9 ns] | 137.3 ns [136.8 ns, 137.7 ns] | +| la_stack_lu_solve | 45.7 ns [45.6 ns, 46.1 ns] | 45.6 ns [44.9 ns, 46.1 ns] | -0.2% | marginal CIs overlap | 1.00x | 68.9 ns [68.8 ns, 69.0 ns] | 323.2 ns [302.8 ns, 329.6 ns] | +| la_stack_ldlt_solve | 24.7 ns [24.7 ns, 24.8 ns] | 54.9 ns [46.3 ns, 55.5 ns] | +121.7% | slower point estimate; marginal CIs separated | 0.45x | 61.5 ns [61.0 ns, 61.9 ns] | 219.5 ns [215.2 ns, 229.6 ns] | +| la_stack_solve_from_lu | 6.2 ns [6.2 ns, 6.3 ns] | 6.4 ns [6.4 ns, 6.4 ns] | +2.5% | slower point estimate; marginal CIs separated | 0.98x | 8.0 ns [8.0 ns, 8.1 ns] | 89.3 ns [88.2 ns, 89.9 ns] | +| la_stack_solve_from_ldlt | 3.9 ns [3.9 ns, 3.9 ns] | 3.9 ns [3.9 ns, 3.9 ns] | +1.4% | slower point estimate; marginal CIs separated | 0.99x | 9.2 ns [9.1 ns, 9.3 ns] | 66.4 ns [65.7 ns, 66.8 ns] | +| la_stack_det_from_lu | 0.9 ns [0.9 ns, 0.9 ns] | 1.7 ns [1.7 ns, 1.7 ns] | +98.3% | slower point estimate; marginal CIs separated | 0.50x | 0.7 ns [0.7 ns, 0.7 ns] | 1.5 ns [1.5 ns, 1.5 ns] | +| la_stack_det_from_ldlt | 0.8 ns [0.8 ns, 0.8 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +51.4% | slower point estimate; marginal CIs separated | 0.66x | 0.6 ns [0.6 ns, 0.6 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +| la_stack_dot | 1.2 ns [1.2 ns, 1.2 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -32.9% | faster point estimate; marginal CIs separated | 1.49x | 0.7 ns [0.7 ns, 0.7 ns] | 1.4 ns [1.4 ns, 1.4 ns] | +| la_stack_norm2_sq | 0.9 ns [0.9 ns, 0.9 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -38.6% | faster point estimate; marginal CIs separated | 1.63x | 0.6 ns [0.6 ns, 0.6 ns] | 4.3 ns [4.2 ns, 4.3 ns] | +| la_stack_inf_norm | 4.7 ns [4.7 ns, 4.7 ns] | 3.4 ns [3.4 ns, 3.4 ns] | -26.9% | faster point estimate; marginal CIs separated | 1.37x | 3.2 ns [3.2 ns, 3.2 ns] | 3.2 ns [3.2 ns, 3.2 ns] | + +### D=8 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 83.5 ns [83.1 ns, 83.7 ns] | 88.3 ns [88.1 ns, 88.7 ns] | +5.7% | slower point estimate; marginal CIs separated | 0.95x | 137.9 ns [137.5 ns, 138.5 ns] | 280.8 ns [279.8 ns, 281.8 ns] | +| la_stack_det | 81.9 ns [81.6 ns, 82.1 ns] | 90.3 ns [90.1 ns, 91.1 ns] | +10.3% | slower point estimate; marginal CIs separated | 0.91x | — | — | +| la_stack_lu | 83.7 ns [82.9 ns, 84.6 ns] | 82.5 ns [82.2 ns, 82.6 ns] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | 123.5 ns [123.2 ns, 124.5 ns] | 265.6 ns [263.8 ns, 266.7 ns] | +| la_stack_ldlt | 100.9 ns [100.7 ns, 101.1 ns] | 92.0 ns [90.4 ns, 92.3 ns] | -8.8% | faster point estimate; marginal CIs separated | 1.10x | 97.7 ns [97.4 ns, 97.8 ns] | 207.9 ns [205.9 ns, 209.1 ns] | +| la_stack_lu_solve | 127.2 ns [126.9 ns, 127.5 ns] | 142.9 ns [141.7 ns, 143.7 ns] | +12.3% | slower point estimate; marginal CIs separated | 0.89x | 165.3 ns [164.7 ns, 166.2 ns] | 367.7 ns [367.0 ns, 368.7 ns] | +| la_stack_ldlt_solve | 119.7 ns [119.5 ns, 119.8 ns] | 105.1 ns [104.8 ns, 105.4 ns] | -12.2% | faster point estimate; marginal CIs separated | 1.14x | 148.2 ns [147.6 ns, 148.3 ns] | 272.8 ns [272.0 ns, 273.8 ns] | +| la_stack_solve_from_lu | 13.5 ns [13.5 ns, 13.5 ns] | 13.5 ns [13.5 ns, 13.5 ns] | -0.2% | faster point estimate; marginal CIs separated | 1.00x | 13.4 ns [13.3 ns, 13.4 ns] | 94.9 ns [94.7 ns, 95.6 ns] | +| la_stack_solve_from_ldlt | 8.3 ns [8.3 ns, 8.3 ns] | 8.2 ns [8.2 ns, 8.2 ns] | -0.5% | faster point estimate; marginal CIs separated | 1.00x | 21.9 ns [21.9 ns, 22.0 ns] | 69.5 ns [69.4 ns, 69.7 ns] | +| la_stack_det_from_lu | 1.3 ns [1.3 ns, 1.3 ns] | 2.2 ns [2.2 ns, 2.5 ns] | +69.6% | slower point estimate; marginal CIs separated | 0.59x | 1.0 ns [1.0 ns, 1.0 ns] | 2.2 ns [2.2 ns, 2.2 ns] | +| la_stack_det_from_ldlt | 1.3 ns [1.3 ns, 1.3 ns] | 2.5 ns [2.4 ns, 2.5 ns] | +94.9% | slower point estimate; marginal CIs separated | 0.51x | 0.9 ns [0.9 ns, 0.9 ns] | 2.1 ns [2.0 ns, 2.1 ns] | +| la_stack_dot | 1.5 ns [1.5 ns, 1.6 ns] | 1.0 ns [1.0 ns, 1.0 ns] | -33.3% | faster point estimate; marginal CIs separated | 1.50x | 1.1 ns [1.1 ns, 1.1 ns] | 2.3 ns [2.3 ns, 2.3 ns] | +| la_stack_norm2_sq | 1.3 ns [1.3 ns, 1.3 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -48.8% | faster point estimate; marginal CIs separated | 1.95x | 0.7 ns [0.7 ns, 0.7 ns] | 4.1 ns [4.1 ns, 4.1 ns] | +| la_stack_inf_norm | 12.4 ns [12.4 ns, 12.5 ns] | 8.4 ns [8.4 ns, 8.4 ns] | -32.2% | faster point estimate; marginal CIs separated | 1.48x | 8.0 ns [8.0 ns, 8.1 ns] | 8.1 ns [8.1 ns, 8.1 ns] | +| la_stack_lu_pivoting | 103.7 ns [103.4 ns, 104.0 ns] | 91.3 ns [90.9 ns, 91.6 ns] | -12.0% | faster point estimate; marginal CIs separated | 1.14x | — | — | +| la_stack_lu_ill_conditioned | 96.7 ns [96.5 ns, 96.9 ns] | 82.5 ns [82.4 ns, 82.7 ns] | -14.7% | faster point estimate; marginal CIs separated | 1.17x | — | — | +| la_stack_ldlt_ill_conditioned | 100.4 ns [100.2 ns, 100.5 ns] | 92.0 ns [90.2 ns, 92.5 ns] | -8.4% | faster point estimate; marginal CIs separated | 1.09x | — | — | + +### D=16 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 398.9 ns [396.4 ns, 402.7 ns] | 427.7 ns [426.4 ns, 428.5 ns] | +7.2% | slower point estimate; marginal CIs separated | 0.93x | 460.8 ns [458.2 ns, 466.5 ns] | 662.5 ns [659.9 ns, 664.7 ns] | +| la_stack_det | 393.5 ns [392.8 ns, 395.5 ns] | 457.5 ns [444.9 ns, 461.4 ns] | +16.3% | slower point estimate; marginal CIs separated | 0.86x | — | — | +| la_stack_lu | 367.8 ns [367.3 ns, 368.7 ns] | 399.6 ns [398.4 ns, 404.0 ns] | +8.6% | slower point estimate; marginal CIs separated | 0.92x | 468.6 ns [468.4 ns, 469.5 ns] | 642.3 ns [637.6 ns, 646.4 ns] | +| la_stack_ldlt | 449.6 ns [448.6 ns, 450.4 ns] | 393.0 ns [392.5 ns, 393.5 ns] | -12.6% | faster point estimate; marginal CIs separated | 1.14x | 405.6 ns [405.2 ns, 405.8 ns] | 416.8 ns [415.5 ns, 418.1 ns] | +| la_stack_lu_solve | 638.4 ns [636.2 ns, 639.9 ns] | 654.7 ns [653.6 ns, 656.0 ns] | +2.6% | slower point estimate; marginal CIs separated | 0.98x | 577.2 ns [576.7 ns, 577.9 ns] | 894.6 ns [891.7 ns, 896.5 ns] | +| la_stack_ldlt_solve | 518.9 ns [517.5 ns, 520.2 ns] | 451.7 ns [451.4 ns, 452.3 ns] | -12.9% | faster point estimate; marginal CIs separated | 1.15x | 649.4 ns [647.5 ns, 651.8 ns] | 590.8 ns [588.9 ns, 593.7 ns] | +| la_stack_solve_from_lu | 196.9 ns [196.4 ns, 197.5 ns] | 197.2 ns [196.3 ns, 198.0 ns] | +0.2% | marginal CIs overlap | 1.00x | 93.4 ns [93.3 ns, 93.5 ns] | 239.7 ns [239.3 ns, 240.2 ns] | +| la_stack_solve_from_ldlt | 28.2 ns [28.1 ns, 28.3 ns] | 27.7 ns [27.7 ns, 27.7 ns] | -1.7% | faster point estimate; marginal CIs separated | 1.02x | 123.6 ns [123.3 ns, 123.8 ns] | 177.5 ns [177.1 ns, 177.8 ns] | +| la_stack_det_from_lu | 2.6 ns [2.6 ns, 2.6 ns] | 3.7 ns [3.7 ns, 3.7 ns] | +42.5% | slower point estimate; marginal CIs separated | 0.70x | 1.8 ns [1.8 ns, 1.8 ns] | 4.8 ns [4.7 ns, 4.9 ns] | +| la_stack_det_from_ldlt | 2.5 ns [2.5 ns, 2.6 ns] | 3.3 ns [3.2 ns, 3.5 ns] | +30.0% | slower point estimate; marginal CIs separated | 0.77x | 1.7 ns [1.7 ns, 1.7 ns] | 4.4 ns [4.4 ns, 4.4 ns] | +| la_stack_dot | 3.1 ns [3.0 ns, 3.3 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -26.3% | faster point estimate; marginal CIs separated | 1.36x | 1.9 ns [1.9 ns, 1.9 ns] | 4.6 ns [4.5 ns, 4.6 ns] | +| la_stack_norm2_sq | 2.7 ns [2.6 ns, 2.7 ns] | 2.1 ns [2.0 ns, 2.1 ns] | -23.1% | faster point estimate; marginal CIs separated | 1.30x | 1.5 ns [1.5 ns, 1.5 ns] | 4.1 ns [4.1 ns, 4.1 ns] | +| la_stack_inf_norm | 49.8 ns [49.5 ns, 50.0 ns] | 32.7 ns [32.6 ns, 32.7 ns] | -34.3% | faster point estimate; marginal CIs separated | 1.52x | 31.7 ns [31.7 ns, 31.7 ns] | 32.6 ns [32.5 ns, 32.6 ns] | + +### D=32 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 2.11 µs [2.10 µs, 2.11 µs] | 2.21 µs [2.21 µs, 2.22 µs] | +5.0% | slower point estimate; marginal CIs separated | 0.95x | 2.51 µs [2.50 µs, 2.52 µs] | 2.30 µs [2.28 µs, 2.31 µs] | +| la_stack_det | 2.01 µs [2.00 µs, 2.02 µs] | 2.60 µs [2.60 µs, 2.61 µs] | +29.6% | slower point estimate; marginal CIs separated | 0.77x | — | — | +| la_stack_lu | 2.20 µs [2.18 µs, 2.21 µs] | 2.07 µs [2.06 µs, 2.07 µs] | -6.1% | faster point estimate; marginal CIs separated | 1.07x | 2.16 µs [2.16 µs, 2.16 µs] | 2.26 µs [2.25 µs, 2.27 µs] | +| la_stack_ldlt | 2.81 µs [2.81 µs, 2.82 µs] | 2.53 µs [2.52 µs, 2.54 µs] | -9.9% | faster point estimate; marginal CIs separated | 1.11x | 2.11 µs [2.10 µs, 2.11 µs] | 1.42 µs [1.42 µs, 1.42 µs] | +| la_stack_lu_solve | 2.68 µs [2.68 µs, 2.68 µs] | 2.79 µs [2.79 µs, 2.80 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | 2.45 µs [2.45 µs, 2.46 µs] | 2.86 µs [2.86 µs, 2.87 µs] | +| la_stack_ldlt_solve | 3.27 µs [3.26 µs, 3.27 µs] | 2.94 µs [2.92 µs, 2.95 µs] | -10.1% | faster point estimate; marginal CIs separated | 1.11x | 2.79 µs [2.79 µs, 2.79 µs] | 1.91 µs [1.91 µs, 1.91 µs] | +| la_stack_solve_from_lu | 688.9 ns [686.2 ns, 692.0 ns] | 674.0 ns [672.6 ns, 674.9 ns] | -2.2% | faster point estimate; marginal CIs separated | 1.02x | 331.4 ns [331.1 ns, 331.8 ns] | 619.2 ns [617.2 ns, 622.7 ns] | +| la_stack_solve_from_ldlt | 303.6 ns [302.9 ns, 304.0 ns] | 307.3 ns [306.8 ns, 307.7 ns] | +1.2% | slower point estimate; marginal CIs separated | 0.99x | 566.9 ns [564.8 ns, 570.2 ns] | 463.7 ns [463.0 ns, 464.2 ns] | +| la_stack_det_from_lu | 7.0 ns [7.0 ns, 7.0 ns] | 13.9 ns [13.4 ns, 14.0 ns] | +98.2% | slower point estimate; marginal CIs separated | 0.50x | 3.1 ns [3.1 ns, 3.1 ns] | 8.8 ns [8.8 ns, 8.8 ns] | +| la_stack_det_from_ldlt | 6.3 ns [6.3 ns, 6.4 ns] | 12.9 ns [12.9 ns, 13.0 ns] | +105.4% | slower point estimate; marginal CIs separated | 0.49x | 3.0 ns [3.0 ns, 3.0 ns] | 8.5 ns [8.5 ns, 8.5 ns] | +| la_stack_dot | 7.5 ns [7.5 ns, 7.6 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -46.4% | faster point estimate; marginal CIs separated | 1.87x | 4.7 ns [4.7 ns, 4.7 ns] | 15.1 ns [15.1 ns, 15.1 ns] | +| la_stack_norm2_sq | 7.5 ns [7.5 ns, 7.5 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -46.9% | faster point estimate; marginal CIs separated | 1.88x | 3.9 ns [3.8 ns, 3.9 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| la_stack_inf_norm | 202.4 ns [202.2 ns, 202.7 ns] | 128.9 ns [128.6 ns, 129.1 ns] | -36.3% | faster point estimate; marginal CIs separated | 1.57x | 159.6 ns [157.6 ns, 161.3 ns] | 163.8 ns [163.5 ns, 164.1 ns] | + +### D=64 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 15.18 µs [15.16 µs, 15.21 µs] | 15.60 µs [15.58 µs, 15.65 µs] | +2.7% | slower point estimate; marginal CIs separated | 0.97x | 15.01 µs [14.78 µs, 15.10 µs] | 10.70 µs [10.68 µs, 10.71 µs] | +| la_stack_det | 15.15 µs [15.13 µs, 15.17 µs] | 15.48 µs [15.44 µs, 15.51 µs] | +2.2% | slower point estimate; marginal CIs separated | 0.98x | — | — | +| la_stack_lu | 15.34 µs [15.31 µs, 15.37 µs] | 14.66 µs [14.64 µs, 14.69 µs] | -4.4% | faster point estimate; marginal CIs separated | 1.05x | 13.67 µs [13.65 µs, 13.70 µs] | 10.50 µs [10.48 µs, 10.56 µs] | +| la_stack_ldlt | 21.53 µs [21.49 µs, 21.58 µs] | 20.42 µs [20.39 µs, 20.49 µs] | -5.2% | faster point estimate; marginal CIs separated | 1.05x | 11.41 µs [10.97 µs, 11.50 µs] | 8.86 µs [8.85 µs, 8.87 µs] | +| la_stack_lu_solve | 17.48 µs [17.44 µs, 17.50 µs] | 17.24 µs [17.21 µs, 17.41 µs] | -1.3% | faster point estimate; marginal CIs separated | 1.01x | 14.84 µs [14.80 µs, 14.86 µs] | 12.20 µs [12.18 µs, 12.23 µs] | +| la_stack_ldlt_solve | 24.60 µs [24.49 µs, 24.76 µs] | 22.70 µs [22.63 µs, 22.84 µs] | -7.7% | faster point estimate; marginal CIs separated | 1.08x | 14.06 µs [14.02 µs, 14.10 µs] | 10.12 µs [10.11 µs, 10.12 µs] | +| la_stack_solve_from_lu | 2.64 µs [2.63 µs, 2.65 µs] | 2.55 µs [2.54 µs, 2.56 µs] | -3.4% | faster point estimate; marginal CIs separated | 1.04x | 786.0 ns [778.1 ns, 788.8 ns] | 1.66 µs [1.66 µs, 1.66 µs] | +| la_stack_solve_from_ldlt | 1.10 µs [1.10 µs, 1.10 µs] | 1.07 µs [1.07 µs, 1.07 µs] | -2.7% | faster point estimate; marginal CIs separated | 1.03x | 1.29 µs [1.29 µs, 1.29 µs] | 1.25 µs [1.24 µs, 1.25 µs] | +| la_stack_det_from_lu | 27.0 ns [27.0 ns, 27.1 ns] | 31.9 ns [31.8 ns, 32.5 ns] | +17.9% | slower point estimate; marginal CIs separated | 0.85x | 8.7 ns [8.7 ns, 8.7 ns] | 21.6 ns [21.6 ns, 21.7 ns] | +| la_stack_det_from_ldlt | 27.5 ns [27.0 ns, 28.7 ns] | 31.6 ns [31.6 ns, 31.6 ns] | +14.8% | slower point estimate; marginal CIs separated | 0.87x | 8.5 ns [8.5 ns, 8.5 ns] | 21.0 ns [20.9 ns, 21.1 ns] | +| la_stack_dot | 38.4 ns [38.4 ns, 38.5 ns] | 11.0 ns [10.9 ns, 11.0 ns] | -71.5% | faster point estimate; marginal CIs separated | 3.51x | 9.0 ns [9.0 ns, 9.0 ns] | 36.6 ns [36.6 ns, 36.7 ns] | +| la_stack_norm2_sq | 38.2 ns [38.2 ns, 38.3 ns] | 10.8 ns [10.8 ns, 10.8 ns] | -71.8% | faster point estimate; marginal CIs separated | 3.54x | 7.4 ns [7.4 ns, 7.4 ns] | 6.2 ns [6.2 ns, 6.2 ns] | +| la_stack_inf_norm | 1.92 µs [1.92 µs, 1.92 µs] | 621.3 ns [620.2 ns, 621.7 ns] | -67.6% | faster point estimate; marginal CIs separated | 3.09x | 1.14 µs [1.13 µs, 1.15 µs] | 1.57 µs [1.57 µs, 1.57 µs] | ## How to Update diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 60fb305..6efc765 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -92,16 +92,14 @@ uv lock Review version references in documentation: ```bash -uv run --locked check-docs-version-sync +just docs-version-check ``` The automated check covers package metadata, lockfiles, README dependency -snippets, and release-pinned README links. Then review historical references -that intentionally remain on older versions: - -```bash -rg -n "\bv?[0-9]+\.[0-9]+\.[0-9]+\b" README.md docs/ CITATION.cff pyproject.toml || true -``` +snippets, release-pinned README links, and current-tag arguments in the active +benchmark workflow examples. Historical prose, compatibility notes, archived +reports, baseline arguments, and tool versions intentionally remain independent +of the current package version. 3. Generate the release changelog diff --git a/docs/archive/performance/README.md b/docs/archive/performance/README.md index 087b380..51696c5 100644 --- a/docs/archive/performance/README.md +++ b/docs/archive/performance/README.md @@ -5,3 +5,4 @@ Older release-to-release benchmark comparisons are archived here. - [v0.4.1-vs-v0.4.0](v0.4.1-vs-v0.4.0.md) - [v0.4.2-vs-v0.4.1](v0.4.2-vs-v0.4.1.md) +- [v0.4.3-vs-v0.4.2](v0.4.3-vs-v0.4.2.md) diff --git a/docs/archive/performance/v0.4.3-vs-v0.4.2.md b/docs/archive/performance/v0.4.3-vs-v0.4.2.md new file mode 100644 index 0000000..db6e35f --- /dev/null +++ b/docs/archive/performance/v0.4.3-vs-v0.4.2.md @@ -0,0 +1,148 @@ +# Benchmark Performance + +**la-stack** v0.4.3 · `45affa8` (HEAD) · 2026-06-09 08:41:32 UTC +**Statistic**: median +**Suite**: exact +**Scope**: release-signal + +## Benchmark Results + +Comparison against baseline **v0.4.2**: + +**Harness provenance**: Historical per-release harnesses. These measurements +predate shared-harness enforcement, so differences may include benchmark-harness +changes as well as library changes. + +**Measurement provenance**: Unavailable. CPU, operating system, Rust toolchain, +full measured source states (including the baseline commit), dependency lock +digest, Criterion version/configuration, and confidence intervals were not +preserved with this historical report. The header's short hash identifies the +reporting checkout only; it does not establish both measured revisions. + +The change and speedup columns are descriptive ratios of median point estimates +only. A negative change or ratio above 1.00× means the recorded latest median was +lower; it does not establish a statistically supported speedup. Do not attribute +these differences to library code because the harnesses and environments were +not controlled or recorded comparably. + +## Exact arithmetic + +### D=2 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det | 0.9 ns | 0.7 ns | -24.0% | 1.32x | +| det_direct | 1.0 ns | 1.0 ns | +2.1% | 0.98x | +| det_exact | 248.9 ns | 195.6 ns | -21.4% | 1.27x | +| det_exact_f64_result (vs det_exact_f64) | 429.1 ns | 167.6 ns | -60.9% | 2.56x | +| det_exact_rounded_f64 (vs det_exact_f64) | 429.1 ns | 375.2 ns | -12.6% | 1.14x | +| det_sign_exact | 1.5 ns | 3.2 ns | +115.9% | 0.46x | +| solve_exact | 6.53 µs | 6.45 µs | -1.1% | 1.01x | +| solve_exact_f64_result (vs solve_exact_f64) | 6.90 µs | 6.60 µs | -4.4% | 1.05x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 6.90 µs | 7.02 µs | +1.7% | 0.98x | + +### D=3 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det | 1.8 ns | 1.5 ns | -19.4% | 1.24x | +| det_direct | 2.0 ns | 2.0 ns | +2.4% | 0.98x | +| det_exact | 739.0 ns | 468.6 ns | -36.6% | 1.58x | +| det_exact_f64_result (vs det_exact_f64) | 913.1 ns | 435.6 ns | -52.3% | 2.10x | +| det_exact_rounded_f64 (vs det_exact_f64) | 913.1 ns | 648.1 ns | -29.0% | 1.41x | +| det_sign_exact | 4.2 ns | 5.5 ns | +30.9% | 0.76x | +| solve_exact | 25.69 µs | 25.16 µs | -2.1% | 1.02x | +| solve_exact_f64_result (vs solve_exact_f64) | 26.16 µs | 25.42 µs | -2.8% | 1.03x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 26.16 µs | 25.67 µs | -1.9% | 1.02x | + +### D=4 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det | 3.3 ns | 4.5 ns | +38.1% | 0.72x | +| det_direct | 3.7 ns | 4.3 ns | +17.6% | 0.85x | +| det_exact | 1.87 µs | 1.47 µs | -21.8% | 1.28x | +| det_exact_f64_result (vs det_exact_f64) | 2.04 µs | 1.47 µs | -27.9% | 1.39x | +| det_exact_rounded_f64 (vs det_exact_f64) | 2.04 µs | 1.63 µs | -19.8% | 1.25x | +| det_sign_exact | 6.9 ns | 11.5 ns | +67.1% | 0.60x | +| solve_exact | 64.95 µs | 61.67 µs | -5.1% | 1.05x | +| solve_exact_f64_result (vs solve_exact_f64) | 66.35 µs | 62.37 µs | -6.0% | 1.06x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 66.35 µs | 63.59 µs | -4.2% | 1.04x | + +### D=5 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det | 26.0 ns | 23.3 ns | -10.6% | 1.12x | +| det_exact | 4.10 µs | 4.05 µs | -1.3% | 1.01x | +| det_exact_f64_result (vs det_exact_f64) | 4.21 µs | 4.02 µs | -4.4% | 1.05x | +| det_exact_rounded_f64 (vs det_exact_f64) | 4.21 µs | 4.33 µs | +2.8% | 0.97x | +| det_sign_exact | 3.94 µs | 3.96 µs | +0.6% | 0.99x | +| solve_exact | 130.82 µs | 126.75 µs | -3.1% | 1.03x | +| solve_exact_f64_result (vs solve_exact_f64) | 132.70 µs | 127.37 µs | -4.0% | 1.04x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 132.70 µs | 128.15 µs | -3.4% | 1.04x | + +### Near-singular 3x3 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det_sign_exact | 705.2 ns | 444.2 ns | -37.0% | 1.59x | +| det_exact | 724.0 ns | 478.9 ns | -33.9% | 1.51x | +| solve_exact | 3.44 µs | 3.39 µs | -1.6% | 1.02x | +| solve_exact_f64_result (vs solve_exact_f64) | 3.47 µs | 3.36 µs | -3.2% | 1.03x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 3.47 µs | 3.39 µs | -2.5% | 1.03x | + +### Large entries 3x3 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det_sign_exact | 2.91 µs | 402.4 ns | -86.2% | 7.23x | +| det_exact | 2.94 µs | 434.0 ns | -85.2% | 6.76x | +| solve_exact | 82.81 µs | 81.57 µs | -1.5% | 1.02x | +| solve_exact_f64_result (vs solve_exact_f64) | 84.32 µs | 81.66 µs | -3.1% | 1.03x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 84.32 µs | 82.04 µs | -2.7% | 1.03x | + +### Hilbert 4x4 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det_sign_exact | 6.9 ns | 11.5 ns | +66.4% | 0.60x | +| det_exact | 1.91 µs | 1.50 µs | -21.7% | 1.28x | +| solve_exact | 49.42 µs | 47.77 µs | -3.3% | 1.03x | +| solve_exact_f64_result (vs solve_exact_f64) | 50.38 µs | 47.67 µs | -5.4% | 1.06x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 50.38 µs | 48.17 µs | -4.4% | 1.05x | + +### Hilbert 5x5 + +| Benchmark | v0.4.2 | Latest | Change | Speedup | +|-----------|-------:|-------:|-------:|--------:| +| det_sign_exact | 4.09 µs | 3.91 µs | -4.6% | 1.05x | +| det_exact | 4.00 µs | 4.02 µs | +0.6% | 0.99x | +| solve_exact | 98.71 µs | 95.41 µs | -3.4% | 1.03x | +| solve_exact_f64_result (vs solve_exact_f64) | 99.88 µs | 98.14 µs | -1.7% | 1.02x | +| solve_exact_rounded_f64 (vs solve_exact_f64) | 99.88 µs | 97.50 µs | -2.4% | 1.02x | + +## How to Update + +Local performance reports are generated in isolated temporary worktrees: + +```bash +# Local development: compare the current tree with the latest release +just performance-local + +# Release PR: update docs/PERFORMANCE.md and archive the previous report +just performance-release + +# GitHub Actions release assets +just performance-github-assets + +# Explicit repair +just performance-release +``` + +`just performance-local` writes `target/bench-reports/performance.md`. +`just performance-github-assets` writes `target/bench-reports/github-assets-performance.md`. + +Older curated release-to-release reports are archived in `docs/archive/performance/`. + +See `docs/BENCHMARKING.md` for the full comparison workflow. diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.csv b/docs/assets/bench/vs_linalg_lu_solve_median.csv index 55a3e34..9da270f 100644 --- a/docs/assets/bench/vs_linalg_lu_solve_median.csv +++ b/docs/assets/bench/vs_linalg_lu_solve_median.csv @@ -1,9 +1,9 @@ D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi -2,2.0437070181606085,2.0425631264853448,2.04607019228461,4.542175209414122,4.538937481352276,4.545658680820327,143.95823454981254,143.35650513307058,144.55261268043853 -3,9.595660662683073,9.588797748121927,9.613326467320519,23.59900523298633,23.193334672880862,23.89156203624332,185.46552169511642,184.30770285322595,186.6833373684646 -4,23.338056895226114,23.241755833807627,23.450110414132816,50.71697549754131,50.61114270941055,50.84226416178329,210.97635296260154,210.09558569349872,211.8172518966618 -5,45.36802780858268,45.33368856824674,45.45857791494792,69.06519772701617,68.91705833943838,69.22990310070215,277.56412002670754,276.40090137857896,278.9175994658497 -8,127.86115337657483,127.67784037443938,127.97259027100068,164.41175356549803,163.98918099473198,164.89947719932118,364.864449238967,363.9575332348597,365.67189807976365 -16,631.9974053918763,630.5943769720783,634.117961205346,663.82156895873,608.9343639517663,684.2256402276,882.673648246476,880.5187023299551,885.2058986456484 -32,2745.604342979343,2733.882221204255,2755.327066196631,2424.5398969497583,2422.7844615775653,2425.665479115479,2867.431290847727,2862.8613304608957,2879.1905179193345 -64,17543.03432206594,17378.64912280702,17669.562753036436,14747.730784813924,14732.373598480488,14759.246031746032,12266.27068329904,12250.220156695157,12279.630177029401 +2,2.0511167827781702,2.0508010652319073,2.0514799503238637,4.608511834479354,4.588541113174011,4.679159823823008,149.53700192152104,149.28626396895226,149.86909100717443 +3,10.031816164052708,10.012870984386646,10.042108916260883,23.094065508706453,23.050967573914953,23.12253053765827,185.93474278358173,185.4274152626928,186.52338345968678 +4,21.806265246500374,21.78844796496967,21.840814592483063,53.54247145040914,53.343918684571506,53.583225701852626,218.92100858066146,217.53978259902593,220.91673823920112 +5,43.78667308084254,43.725648916200925,43.8375145276915,70.43721993909745,70.40138994288745,70.46838014608551,282.26162962383944,281.1318190503487,283.00841946602355 +8,128.33741381271173,128.16537417133148,128.44380372544757,167.50543379849552,167.2076897070214,167.94225473214073,414.79144610503465,385.1675329566855,418.10038292529816 +16,672.6803584206948,669.32087973114,675.0099138907773,581.2733321011165,581.0081202884301,581.4932153392331,875.4114698625715,871.4328720696911,877.1246563118169 +32,2873.719688592829,2864.5208484738373,2883.4553890876564,2470.43527521921,2465.696974484398,2475.3848495644006,2861.2088220918868,2857.038328349756,2865.5225755003344 +64,18165.369153911564,18049.00228937729,18230.65507518797,15021.7373313609,15007.658290233809,15057.123188405796,12225.703395061728,12218.95586419753,12243.655761316872 diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json b/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json index d43cd72..bb74219 100644 --- a/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json +++ b/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json @@ -1,13 +1,18 @@ { "artifact": "README vs_linalg dimension plot", - "artifact_files": { - "csv_sha256": "9422bc1f7000b8c7d4c009306c7d5f1e7ac73011189e5611ca7446054ad948b9", - "svg_sha256": "00a08cf79a7193d88563c6f07f6d5c351d3426020b26a358355393f2b9f47894" - }, "criterion": { - "benchmark_command": "unavailable", - "confidence_interval_configuration": "unavailable; bounds are preserved in the CSV", - "criterion_dependency": "unavailable", + "benchmark_command": [ + "cargo", + "bench", + "--locked", + "--features", + "bench", + "--bench", + "vs_linalg", + "--", + "(la_stack_lu_solve|nalgebra_lu_solve|faer_lu_solve)$" + ], + "criterion_dependency": "0.8.2", "dimensions": [ 2, 3, @@ -24,13 +29,32 @@ "statistic": "median" }, "measurement": { - "reason": "the v0.4.3 assets predate deterministic measurement-provenance capture", - "status": "unavailable" + "cargo_lock_sha256": "0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd", + "commit": "e736c5fda155ef23c8712f89ae15bf5369ff3787", + "cpu": "arm", + "git_clean": false, + "git_status_sha256": "a367ed1608bcdea33eb781794fde643f01a6a280f154ffacbd02e25506cf9f8b", + "harness_sha256": "7915a36e12d9895170323aee46910b688492dc6aca704ac7f1daaefae644a9ea", + "missing_harness_files": [], + "os": "Darwin 25.5.0 arm64", + "rustc": "rustc 1.97.0 (2d8144b78 2026-07-07)", + "source_missing": false, + "source_state_sha256": "fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2", + "status": "recorded" }, "publication": { - "artifact_commit": "925cbb72be101aa10e612f80b07c1cad8e298434", - "release": "v0.4.3", - "status": "historical" + "cargo_lock_sha256": "0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd", + "commit": "e736c5fda155ef23c8712f89ae15bf5369ff3787", + "correctness_gate": "passed", + "cpu": "arm", + "git_clean": false, + "git_status_sha256": "a367ed1608bcdea33eb781794fde643f01a6a280f154ffacbd02e25506cf9f8b", + "harness_sha256": "7915a36e12d9895170323aee46910b688492dc6aca704ac7f1daaefae644a9ea", + "missing_harness_files": [], + "os": "Darwin 25.5.0 arm64", + "rustc": "rustc 1.97.0 (2d8144b78 2026-07-07)", + "source_missing": false, + "source_state_sha256": "fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2" }, "schema": 1 } diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.svg b/docs/assets/bench/vs_linalg_lu_solve_median.svg index deb7b3e..890ce89 100644 --- a/docs/assets/bench/vs_linalg_lu_solve_median.svg +++ b/docs/assets/bench/vs_linalg_lu_solve_median.svg @@ -244,51 +244,51 @@ - la-stack v0.4.3 + la-stack v0.4.4 - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + @@ -302,47 +302,47 @@ - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + @@ -352,51 +352,51 @@ gnuplot_plot_3 - faer v0.24.0 + faer v0.24.4 - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + diff --git a/justfile b/justfile index 7793571..b5baa71 100644 --- a/justfile +++ b/justfile @@ -393,6 +393,9 @@ doc-check: RUSTDOCFLAGS='-D warnings' cargo doc --no-deps RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --features exact +docs-version-check: _ensure-uv + uv run --locked check-docs-version-sync + # Examples examples: #!/usr/bin/env bash @@ -522,6 +525,9 @@ markdown-check: _ensure-rumdl line_number=0 while IFS= read -r line || [[ -n "$line" ]]; do line_number=$((line_number + 1)) + case "$line" in + '|'*) continue ;; + esac if [ "${#line}" -gt 160 ]; then printf '%s:%d: line length %d exceeds 160\n' "$file" "$line_number" "${#line}" >&2 violations=$((violations + 1)) @@ -626,7 +632,7 @@ plot-vs-linalg metric="lu_solve" stat="median" sample="new" log_y="false" allow_ fi uv run --locked criterion-dim-plot "${args[@]}" -# Validate fixtures, rerun the full comparison, and atomically publish the canonical README assets/table. +# Validate fixtures, rerun the selected metric across all peers and dimensions, and atomically publish the canonical README assets/table. plot-vs-linalg-readme metric="lu_solve" stat="median" sample="new" log_y="true": python-sync #!/usr/bin/env bash set -euo pipefail @@ -654,9 +660,8 @@ python-typecheck: python-sync uv run --locked ty check scripts/ --error all # Repository-owned Semgrep rules for project-specific diagnostics. -semgrep: _ensure-uv +semgrep: docs-version-check uv run --locked semgrep --metrics off --error --strict --timeout 30 --config semgrep.yaml . - uv run --locked check-docs-version-sync # Fixture tests for repository-owned Semgrep rules. semgrep-test: _ensure-uv diff --git a/pyproject.toml b/pyproject.toml index a89d60b..9aa8f5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "la-stack-scripts" -version = "0.4.3" +version = "0.4.4" description = "Python utility scripts for the la-stack Rust library" readme = "README.md" requires-python = ">=3.14" diff --git a/scripts/bench_compare.py b/scripts/bench_compare.py index 44975a5..f4b0028 100644 --- a/scripts/bench_compare.py +++ b/scripts/bench_compare.py @@ -1381,10 +1381,11 @@ def _generate_markdown( "Negative point-estimate change means the current point estimate is smaller; " "a baseline/current point-estimate ratio above 1.00 has the same meaning." ) - lines.append( - "The CI-relation column reports only whether the two marginal Criterion intervals overlap. " - "These are not paired confidence intervals for the change, so the report makes no " - "statistical-significance or performance-improvement claim from interval separation." + lines.extend( + [ + "The CI-relation column reports only whether the two marginal Criterion intervals overlap. These are not paired confidence intervals", + "for the change, so the report makes no statistical-significance or performance-improvement claim from interval separation.", + ] ) lines.append("") if settings.harness_provenance is None: @@ -1488,6 +1489,8 @@ def _provenance_markdown(provenance: HarnessProvenance) -> list[str]: lines.extend( [ + "", + "**Publication and validation environment**:", "", f"- Publication CPU: `{publication['cpu']}`", f"- Publication OS: `{publication['os']}`", @@ -1503,35 +1506,35 @@ def _provenance_markdown(provenance: HarnessProvenance) -> list[str]: f"- Baseline command: `{' '.join(criterion.baseline_command)}`", f"- Current command: `{' '.join(criterion.current_command)}`", correctness_gate, - ( - f"- Validated current revision: `{validation['current_commit']}` " - f"(Git clean: `{str(validation['current_git_clean']).lower()}`, " - f"source-state SHA-256: `{validation['current_source_state_sha256']}`)" - ), - ( - f"- Validated baseline revision: `{validation['baseline_commit']}` " - f"(Git clean: `{str(validation['baseline_git_clean']).lower()}`, " - f"source-state SHA-256: `{validation['baseline_source_state_sha256']}`)" - ), + f"- Validated current revision: `{validation['current_commit']}` (Git clean: `{str(validation['current_git_clean']).lower()}`;", + f" source-state SHA-256: `{validation['current_source_state_sha256']}`)", + f"- Validated baseline revision: `{validation['baseline_commit']}` (Git clean: `{str(validation['baseline_git_clean']).lower()}`;", + f" source-state SHA-256: `{validation['baseline_source_state_sha256']}`)", ] ) compatibility = validation.get("baseline_api_compatibility") if isinstance(compatibility, str) and compatibility != "none": - lines.append( - f"- Baseline API compatibility: `{compatibility}` selects only source-compatible benchmark calls; " - "rows outside the baseline's correctness domain remain explicitly unavailable." + lines.extend( + [ + f"- Baseline API compatibility: `{compatibility}` selects only source-compatible benchmark calls;", + " rows outside the baseline's correctness domain remain explicitly unavailable.", + ] ) if compatibility == _V0_4_3_API_COMPATIBILITY and criterion.suite in {"all", "vs_linalg"}: - lines.append( - "- Baseline-unavailable rows: `d8/la_stack_det_from_lu_balanced_range` and " - "`d8/la_stack_det_from_ldlt_balanced_range` were not timed because v0.4.3 returns zero for a fixture " - "whose exact determinant is one; current samples remain required, but no speedup is claimed." + lines.extend( + [ + "- Baseline-unavailable rows: `d8/la_stack_det_from_lu_balanced_range` and", + " `d8/la_stack_det_from_ldlt_balanced_range` were not timed because v0.4.3 returns zero for a", + " fixture whose exact determinant is one; current samples remain required, but no speedup is claimed.", + ] ) if compatibility == _V0_4_3_API_COMPATIBILITY and criterion.suite in {"all", "exact"}: - lines.append( - "- Baseline-unavailable rows: `exact_d2/det_direct_with_errbound`, " - "`exact_d3/det_direct_with_errbound`, and `exact_d4/det_direct_with_errbound` were not timed because " - "v0.4.3 predates the paired API; the comparable `det_errbound` baselines remain required." + lines.extend( + [ + "- Baseline-unavailable rows: `exact_d2/det_direct_with_errbound`,", + " `exact_d3/det_direct_with_errbound`, and `exact_d4/det_direct_with_errbound` were not timed", + " because v0.4.3 predates the paired API; the comparable `det_errbound` baselines remain required.", + ] ) return lines diff --git a/scripts/check_docs_version_sync.py b/scripts/check_docs_version_sync.py index f46a4b3..347156d 100644 --- a/scripts/check_docs_version_sync.py +++ b/scripts/check_docs_version_sync.py @@ -83,6 +83,7 @@ class ReferenceKind(StrEnum): """A release surface whose version must match Cargo.toml.""" CARGO_LOCK = "Cargo.lock root package" + BENCHMARK_CURRENT_TAG = "benchmark workflow current tag" CITATION = "CITATION.cff version" DEPENDENCY_SNIPPET = "documentation dependency snippet" PYPROJECT = "pyproject.toml project" @@ -259,7 +260,8 @@ def _dependency_references(path: Path, package_name: str) -> list[VersionReferen _README_TAG_LINK_RE = re.compile( r"https://(?:github\.com/acgetchell/la-stack/(?:blob|raw|tree)/|raw\.githubusercontent\.com/acgetchell/la-stack/)" - r"v(?P[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?=/|\b)" + r"(?:v(?P[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)" + r"|(?P[0-9a-f]{7,40}))(?=/|\b)" ) @@ -267,12 +269,34 @@ def _readme_tag_references(path: Path) -> list[VersionReference]: references: list[VersionReference] = [] for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): references.extend( - VersionReference(path, line_number, match.group("version"), ReferenceKind.README_TAG_LINK, line.strip()) + VersionReference( + path, + line_number, + match.group("version") or match.group("revision"), + ReferenceKind.README_TAG_LINK, + line.strip(), + ) for match in _README_TAG_LINK_RE.finditer(line) ) return references +_BENCHMARK_CURRENT_TAG_RE = re.compile( + r"just performance-(?:github-assets|local-vs-linalg|release)\s+v" + r"(?P[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?=\s|`)" +) + + +def _benchmark_current_tag_references(path: Path) -> list[VersionReference]: + references: list[VersionReference] = [] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + references.extend( + VersionReference(path, line_number, match.group("version"), ReferenceKind.BENCHMARK_CURRENT_TAG, line.strip()) + for match in _BENCHMARK_CURRENT_TAG_RE.finditer(line) + ) + return references + + def _version_references(root: Path, package: PackageInfo) -> list[VersionReference]: pyproject_path = root / "pyproject.toml" project = _read_python_project_info(pyproject_path) @@ -284,6 +308,7 @@ def _version_references(root: Path, package: PackageInfo) -> list[VersionReferen ] for path in _iter_markdown_files(root): references.extend(_dependency_references(path, package.name)) + references.extend(_benchmark_current_tag_references(path)) references.extend(_readme_tag_references(root / "README.md")) return references diff --git a/scripts/criterion_dim_plot.py b/scripts/criterion_dim_plot.py index a3eb692..164cb85 100644 --- a/scripts/criterion_dim_plot.py +++ b/scripts/criterion_dim_plot.py @@ -251,7 +251,7 @@ def no_plot(self) -> bool: ... CANONICAL_DIMS: Final[tuple[int, ...]] = (2, 3, 4, 5, 8, 16, 32, 64) _PUBLICATION_GATE: Final[tuple[str, ...]] = ("just", "test-bench-inputs") -_PUBLICATION_BENCHMARK: Final[tuple[str, ...]] = ( +_PUBLICATION_BENCHMARK_BASE: Final[tuple[str, ...]] = ( "cargo", "bench", "--locked", @@ -455,8 +455,7 @@ def _pct_reduction(baseline: float, value: float) -> str: def _markdown_table(rows: list[Row], stat: str) -> str: lines = [ - f"| D | la-stack {stat} (ns) | nalgebra {stat} (ns) | faer {stat} (ns) | " - "la-stack point-estimate reduction vs nalgebra | la-stack point-estimate reduction vs faer |", + f"| D | la-stack {stat} (ns) | nalgebra {stat} (ns) | faer {stat} (ns) | reduction vs nalgebra (point est.) | reduction vs faer (point est.) |", "|---:|--------------------:|--------------------:|----------------:|---------------------:|----------------:|", ] @@ -713,8 +712,15 @@ def _collect_rows(criterion_dir: Path, dims: list[int], metric: Metric, stat: st return (rows, skipped) -def _run_publication_benchmarks(root: Path) -> None: - """Validate fixtures, then produce fresh README publication measurements.""" +def _publication_benchmark_command(metric_name: str) -> tuple[str, ...]: + """Return the focused Criterion command for one published metric.""" + metric = METRICS[metric_name] + benchmark_filter = "(" + "|".join(re.escape(name) for name in (metric.la_bench, metric.na_bench, metric.fa_bench)) + ")$" + return (*_PUBLICATION_BENCHMARK_BASE, "--", benchmark_filter) + + +def _run_publication_benchmarks(root: Path, metric_name: str) -> None: + """Validate fixtures, then measure one complete README metric.""" _run_publication_command(root, _PUBLICATION_GATE) criterion_dir = root / "target" / "criterion" criterion_dir.parent.mkdir(parents=True, exist_ok=True) @@ -735,7 +741,7 @@ def _run_publication_benchmarks(root: Path) -> None: raise RuntimeError(msg) from primary try: - _run_publication_command(root, _PUBLICATION_BENCHMARK) + _run_publication_command(root, _publication_benchmark_command(metric_name)) except RuntimeError as primary: try: transaction.rollback(remove_fresh=True) @@ -946,7 +952,7 @@ def _capture_provenance( return { "artifact": "README vs_linalg dimension plot" if args.update_readme else "exploratory vs_linalg dimension plot", "criterion": { - "benchmark_command": list(_PUBLICATION_BENCHMARK) if measurement_recorded else "unavailable", + "benchmark_command": list(_publication_benchmark_command(args.metric)) if measurement_recorded else "unavailable", "criterion_dependency": criterion_version, "dimensions": dims, "log_y": args.log_y, @@ -1205,7 +1211,7 @@ def main(argv: list[str] | None = None) -> int: # noqa: C901, PLR0911, PLR0912, return rc if args.update_readme: try: - _run_publication_benchmarks(root) + _run_publication_benchmarks(root, args.metric) except (FileNotFoundError, RuntimeError) as exc: print(str(exc), file=sys.stderr) return 2 diff --git a/scripts/postprocess_changelog.py b/scripts/postprocess_changelog.py index cd47cef..f52c291 100644 --- a/scripts/postprocess_changelog.py +++ b/scripts/postprocess_changelog.py @@ -764,8 +764,49 @@ def _normalize_body_line(line: str, lines: list[str], idx: int, result: list[str return _reflow_line(line) if len(line) > MAX_LINE_WIDTH else line +def _strip_dependabot_metadata(text: str) -> str: + """Remove Dependabot's YAML metadata footer from rendered commit bodies.""" + lines = text.split("\n") + result: list[str] = [] + idx = 0 + + while idx < len(lines): + if lines[idx].strip() != "---": + result.append(lines[idx]) + idx += 1 + continue + + metadata_start = idx + 1 + while metadata_start < len(lines) and not lines[metadata_start].strip(): + metadata_start += 1 + if metadata_start >= len(lines) or lines[metadata_start].strip() != "updated-dependencies:": + result.append(lines[idx]) + idx += 1 + continue + + metadata_end = metadata_start + 1 + while metadata_end < len(lines) and lines[metadata_end].strip() != "...": + metadata_end += 1 + if metadata_end >= len(lines): + result.append(lines[idx]) + idx += 1 + continue + + while result and not result[-1].strip(): + result.pop() + idx = metadata_end + 1 + while idx < len(lines) and not lines[idx].strip(): + idx += 1 + if result and idx < len(lines): + result.append("") + + return "\n".join(result) + + def postprocess_text(text: str) -> str: """Apply changelog markdown hygiene transforms to *text*.""" + text = _strip_dependabot_metadata(text) + # Inject PR / breaking-change summary sections before reflow. text = _inject_summary_sections(text) diff --git a/scripts/tests/test_bench_compare.py b/scripts/tests/test_bench_compare.py index 498eb84..0c5cba3 100644 --- a/scripts/tests/test_bench_compare.py +++ b/scripts/tests/test_bench_compare.py @@ -821,6 +821,8 @@ def test_read_schema2_provenance_records_versions_dirty_source_and_both_gates(tm assert "exact_d2/det_direct_with_errbound" in rendered assert "exact_d3/det_direct_with_errbound" in rendered assert "exact_d4/det_direct_with_errbound" in rendered + assert "**Publication and validation environment**:" in rendered + assert all(len(line) <= 160 for line in markdown) assert "the comparable `det_errbound` baselines remain required" in rendered assert bench_compare._comparison_policy("release-signal", provenance) == bench_compare.ComparisonPolicy( scope="release-signal", diff --git a/scripts/tests/test_check_docs_version_sync.py b/scripts/tests/test_check_docs_version_sync.py index 8cba949..bd503d3 100644 --- a/scripts/tests/test_check_docs_version_sync.py +++ b/scripts/tests/test_check_docs_version_sync.py @@ -109,15 +109,50 @@ def test_find_version_mismatches_reports_readme_tag_links(tmp_path: Path) -> Non readme=( "[doc](https://github.com/acgetchell/la-stack/blob/v1.2.2/README.md)\n" "[raw](https://raw.githubusercontent.com/acgetchell/la-stack/v1.2.1/README.md)\n" + "[stale-commit](https://github.com/acgetchell/la-stack/blob/abc1234/README.md)\n" "[moving](https://github.com/acgetchell/la-stack/blob/main/README.md)\n" ), ) mismatches = check_docs_version_sync.find_version_mismatches(tmp_path) - assert [mismatch.reference.kind for mismatch in mismatches] == [check_docs_version_sync.ReferenceKind.README_TAG_LINK] * 2 - assert [mismatch.reference.line for mismatch in mismatches] == [1, 2] - assert [mismatch.reference.version for mismatch in mismatches] == ["1.2.2", "1.2.1"] + assert [mismatch.reference.kind for mismatch in mismatches] == [check_docs_version_sync.ReferenceKind.README_TAG_LINK] * 3 + assert [mismatch.reference.line for mismatch in mismatches] == [1, 2, 3] + assert [mismatch.reference.version for mismatch in mismatches] == ["1.2.2", "1.2.1", "abc1234"] + + +@pytest.mark.parametrize("recipe", ["performance-github-assets", "performance-local-vs-linalg", "performance-release"]) +def test_find_version_mismatches_reports_stale_benchmark_current_tags(tmp_path: Path, recipe: str) -> None: + _write_project(tmp_path) + docs = tmp_path / "docs" + docs.mkdir() + workflows = docs / "workflows.md" + workflows.write_text( + f"| Release workflow | `just {recipe} v1.2.2 v1.2.1` |\n" + "```bash\njust performance-local-vs-linalg v1.2.3 v1.2.2\n```\n" + "Historical v1.2.1 behavior remains documented.\n", + encoding="utf-8", + ) + + mismatches = check_docs_version_sync.find_version_mismatches(tmp_path) + + assert len(mismatches) == 1 + assert mismatches[0].reference.kind is check_docs_version_sync.ReferenceKind.BENCHMARK_CURRENT_TAG + assert mismatches[0].reference.path == workflows + assert mismatches[0].reference.line == 1 + assert mismatches[0].reference.version == "1.2.2" + + +def test_benchmark_current_tag_references_ignore_baselines_and_historical_prose(tmp_path: Path) -> None: + benchmarking = tmp_path / "BENCHMARKING.md" + benchmarking.write_text( + "just performance-release v1.2.3 v1.2.2\nThe v1.2.2 harness compares against v1.2.1.\n", + encoding="utf-8", + ) + + references = check_docs_version_sync._benchmark_current_tag_references(benchmarking) + + assert [(reference.line, reference.version) for reference in references] == [(1, "1.2.3")] @pytest.mark.parametrize( @@ -142,7 +177,7 @@ def test_find_version_mismatches_ignores_historical_docs_and_test_fixtures(tmp_p archive.mkdir(parents=True) fixtures = tmp_path / "tests" / "fixtures" fixtures.mkdir(parents=True) - stale_snippet = 'other-crate = "0.1.0"\n' + stale_snippet = 'other-crate = "0.1.0"\njust performance-release v0.1.0 v0.0.9\n' (tmp_path / "CHANGELOG.md").write_text(stale_snippet, encoding="utf-8") (archive / "old.md").write_text(stale_snippet, encoding="utf-8") (fixtures / "example.md").write_text(stale_snippet, encoding="utf-8") diff --git a/scripts/tests/test_criterion_dim_plot.py b/scripts/tests/test_criterion_dim_plot.py index e3ef01b..6ac5758 100644 --- a/scripts/tests/test_criterion_dim_plot.py +++ b/scripts/tests/test_criterion_dim_plot.py @@ -101,14 +101,31 @@ def test_markdown_table_formats_values_and_pct() -> None: table = criterion_dim_plot._markdown_table(rows, stat="median") assert ( - "| D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | " - "la-stack point-estimate reduction vs nalgebra | la-stack point-estimate reduction vs faer |" in table + "| D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | reduction vs nalgebra (point est.) | reduction vs faer (point est.) |" in table ) assert "| 2 | 50.000 | 100.000 | 200.000 | +50.0% | +75.0% |" in table # thousand separator and sign assert "| 64 | 1,000.000 | 900.000 | 800.000 | -11.1% | -25.0% |" in table +@pytest.mark.parametrize( + ("metric_name", "expected_filter"), + [ + ("lu_solve", "(la_stack_lu_solve|nalgebra_lu_solve|faer_lu_solve)$"), + ("dot", "(la_stack_dot|nalgebra_dot|faer_dot)$"), + ("inf_norm", "(la_stack_inf_norm|nalgebra_inf_norm|faer_inf_norm)$"), + ], +) +def test_publication_benchmark_command_selects_only_requested_metric( + metric_name: str, + expected_filter: str, +) -> None: + command = criterion_dim_plot._publication_benchmark_command(metric_name) + + assert command[:-2] == criterion_dim_plot._PUBLICATION_BENCHMARK_BASE + assert command[-2:] == ("--", expected_filter) + + def test_row_rejects_zero_peer_time_before_markdown_rendering() -> None: with pytest.raises(ValueError, match="na_time must be finite and positive"): criterion_dim_plot.Row( @@ -396,7 +413,19 @@ def fake_render(request: criterion_dim_plot.PlotRequest) -> None: assert out_svg.read_text(encoding="utf-8") == "\n" assert calls[:2] == [ ("just", ("test-bench-inputs",)), - ("cargo", ("bench", "--locked", "--features", "bench", "--bench", "vs_linalg")), + ( + "cargo", + ( + "bench", + "--locked", + "--features", + "bench", + "--bench", + "vs_linalg", + "--", + "(la_stack_lu_solve|nalgebra_lu_solve|faer_lu_solve)$", + ), + ), ] # CSV written @@ -973,7 +1002,7 @@ def test_main_publication_fails_closed_when_provenance_tool_is_unavailable( row = criterion_dim_plot.Row(2, 1.0, 0.9, 1.1, 2.0, 1.9, 2.1, 3.0, 2.9, 3.1) monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - monkeypatch.setattr(criterion_dim_plot, "_run_publication_benchmarks", lambda _root: None) + monkeypatch.setattr(criterion_dim_plot, "_run_publication_benchmarks", lambda _root, _metric: None) monkeypatch.setattr(criterion_dim_plot, "_detect_versions", lambda _root: {}) monkeypatch.setattr(criterion_dim_plot, "_discover_dims", lambda _criterion_dir: [2]) monkeypatch.setattr(criterion_dim_plot, "_collect_rows", lambda *_args: ([row], [])) @@ -1024,7 +1053,7 @@ def fail_gate(command: str, args: list[str], **_kwargs: object) -> SimpleNamespa monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fail_gate) with pytest.raises(RuntimeError, match="just test-bench-inputs"): - criterion_dim_plot._run_publication_benchmarks(tmp_path) + criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") assert calls == [("just", ("test-bench-inputs",))] @@ -1067,7 +1096,7 @@ def fail_timing(command: str, args: list[str], **_kwargs: object) -> SimpleNames monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fail_timing) with pytest.raises(RuntimeError, match="cargo bench") as exc_info: - criterion_dim_plot._run_publication_benchmarks(tmp_path) + criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") assert old_estimate.read_text(encoding="utf-8") == "old\n" assert all(detail in str(exc_info.value) for detail in expected_details) @@ -1105,7 +1134,7 @@ def fake_mkdtemp(*, prefix: str, **kwargs: object) -> str: monkeypatch.setattr(criterion_dim_plot, "run_safe_command", lambda *_args, **_kwargs: SimpleNamespace(stdout="")) with pytest.raises(RuntimeError, match="could not stage existing Criterion samples"): - criterion_dim_plot._run_publication_benchmarks(tmp_path) + criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") assert first.read_text(encoding="utf-8") == "first\n" assert second.read_text(encoding="utf-8") == "second\n" @@ -1142,7 +1171,7 @@ def fail_fresh_removal(path: Path) -> None: monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fail_timing) with pytest.raises(RuntimeError, match="backups preserved") as exc_info: - criterion_dim_plot._run_publication_benchmarks(tmp_path) + criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") assert str(backup_root) in str(exc_info.value) preserved = backup_root / "d2" / "la_stack_lu" / "new" / "estimates.json" diff --git a/scripts/tests/test_postprocess_changelog.py b/scripts/tests/test_postprocess_changelog.py index 8970923..5ab2ed4 100644 --- a/scripts/tests/test_postprocess_changelog.py +++ b/scripts/tests/test_postprocess_changelog.py @@ -20,6 +20,7 @@ _process_code_fence, _reflow_line, _squash_heading_parts, + _strip_dependabot_metadata, normalize_entry_headings_text, postprocess, postprocess_text, @@ -86,6 +87,30 @@ def test_empty_file(self, tmp_path: Path) -> None: assert f.read_text(encoding="utf-8") == "\n" +class TestDependabotMetadata: + def test_strips_yaml_footer_from_commit_body(self) -> None: + content = ( + "- Update setuptools [`abcdef0`](https://example.com/commit/abcdef0)\n\n" + " Updates setuptools to 83.0.0\n\n" + "---\n\n" + " updated-dependencies:\n\n" + "- dependency-name: setuptools\n" + " dependency-version: 83.0.0\n" + " dependency-type: direct:development\n" + " ...\n" + "- Next entry\n" + ) + + assert _strip_dependabot_metadata(content) == ( + "- Update setuptools [`abcdef0`](https://example.com/commit/abcdef0)\n\n Updates setuptools to 83.0.0\n\n- Next entry\n" + ) + + def test_preserves_unrelated_thematic_break(self) -> None: + content = "- Entry\n\n---\n\nAdditional context\n" + + assert _strip_dependabot_metadata(content) == content + + class TestListContinuationIndent: def test_normalizes_over_indented_top_level_continuation(self) -> None: lines = [ diff --git a/src/exact.rs b/src/exact.rs index 2f0a5ff..809c924 100644 --- a/src/exact.rs +++ b/src/exact.rs @@ -553,6 +553,20 @@ fn rounded_shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option { retained.checked_add(u64::from(increment)) } +/// Classify an inexact integer conversion, evaluating rounding only in the +/// maximum exponent bin where it can overflow to infinity. +#[inline] +fn inexact_big_int_reason( + top_bit_exp: i64, + rounded_reason: impl FnOnce() -> UnrepresentableReason, +) -> UnrepresentableReason { + if top_bit_exp < F64_MAX_BINARY_EXPONENT { + UnrepresentableReason::RequiresRounding + } else { + rounded_reason() + } +} + /// Round a `BigInt × 2^exp` pair directly to finite binary64. /// /// The implementation reads only the magnitude bits needed for the binary64 @@ -719,9 +733,21 @@ fn big_int_exp_ref_to_finite_f64( UnrepresentableReason::NotFinite, )); } - if exp < F64_MIN_BINARY_EXPONENT || bit_len > F64_SIGNIFICAND_BITS { + if exp < F64_MIN_BINARY_EXPONENT { + cold_path(); + // A low least-significant exponent normally rounds to a finite value, + // but a very wide integer in the maximum exponent bin can round up to + // infinity. + let reason = inexact_big_int_reason(top_bit_exp, rounded_reason); + return Err(LaError::unrepresentable(index, reason)); + } + if bit_len > F64_SIGNIFICAND_BITS { cold_path(); - return Err(LaError::unrepresentable(index, rounded_reason())); + // Rounding can overflow only when the exact value already occupies the + // maximum binary64 exponent bin. Avoid the full rounding calculation + // for every ordinary inexact conversion. + let reason = inexact_big_int_reason(top_bit_exp, rounded_reason); + return Err(LaError::unrepresentable(index, reason)); } let Some(mantissa) = shifted_magnitude_to_u64(value, trailing_zeros) else { @@ -897,6 +923,15 @@ mod decomposition { Self { value } } + /// Select the lower of two already-derived collection scales. + pub(super) const fn min(self, other: Self) -> Self { + if self.value < other.value { + self + } else { + other + } + } + /// Return the proven collection exponent. pub(super) const fn get(self) -> i32 { self.value @@ -1260,8 +1295,19 @@ fn bareiss_solve_components( matrix: &Decomposed<[[Component; D]; D]>, rhs: &Decomposed<[Component; D]>, ) -> Result<[BigRational; D], LaError> { - let matrix_scale = ScaleExponent::for_decomposed(matrix); - let rhs_scale = ScaleExponent::for_decomposed(rhs); + const MAX_SHARED_SCALE_GAP_BITS: u32 = 64; + + let independent_matrix_scale = ScaleExponent::for_decomposed(matrix); + let independent_rhs_scale = ScaleExponent::for_decomposed(rhs); + let scale_gap = independent_matrix_scale + .get() + .abs_diff(independent_rhs_scale.get()); + let (matrix_scale, rhs_scale) = if scale_gap <= MAX_SHARED_SCALE_GAP_BITS { + let shared = independent_matrix_scale.min(independent_rhs_scale); + (shared, shared) + } else { + (independent_matrix_scale, independent_rhs_scale) + }; let mut a = build_big_int_matrix(matrix.components(), matrix_scale); let mut rhs = build_big_int_vec(rhs.components(), rhs_scale); diff --git a/src/ldlt.rs b/src/ldlt.rs index 3fb9f04..ae4afa1 100644 --- a/src/ldlt.rs +++ b/src/ldlt.rs @@ -110,36 +110,9 @@ impl Ldlt { // LDLT via symmetric rank-1 updates, using only the lower triangle. for j in 0..D { let d = rows[j][j]; - if !d.is_finite() { + if !(d.is_finite() && d > tolerance) { cold_path(); - return Err(LaError::non_finite_computation_matrix( - ArithmeticOperation::LdltFactorization, - j, - j, - )); - } - if d < 0.0 { - cold_path(); - if let Some(error) = Self::non_finite_factor_error(rows) { - return Err(error); - } - return Err(LaError::not_positive_semidefinite_negative(j, d)); - } - if d == 0.0 { - cold_path(); - return Err(Self::zero_pivot_failure(rows, j, tolerance)); - } - if d <= tolerance { - cold_path(); - if let Some(error) = Self::non_finite_factor_error(rows) { - return Err(error); - } - return Err(LaError::singular_numerical( - j, - FactorizationKind::Ldlt, - d, - tolerance, - )); + return Err(Self::pivot_failure(rows, j, d, tolerance)); } if D <= 5 { // Tiny matrices benchmark better when column normalization stays @@ -229,6 +202,32 @@ impl Ldlt { None } + /// Classify a failed diagonal check outside the successful factorization path. + fn pivot_failure( + rows: &[[f64; D]; D], + pivot_col: usize, + pivot: f64, + tolerance: f64, + ) -> LaError { + if !pivot.is_finite() { + return LaError::non_finite_computation_matrix( + ArithmeticOperation::LdltFactorization, + pivot_col, + pivot_col, + ); + } + if pivot < 0.0 { + return Self::non_finite_factor_error(rows) + .unwrap_or_else(|| LaError::not_positive_semidefinite_negative(pivot_col, pivot)); + } + if pivot == 0.0 { + return Self::zero_pivot_failure(rows, pivot_col, tolerance); + } + Self::non_finite_factor_error(rows).unwrap_or_else(|| { + LaError::singular_numerical(pivot_col, FactorizationKind::Ldlt, pivot, tolerance) + }) + } + /// Classify a zero pivot after checking factor storage and every coupling. fn zero_pivot_failure(rows: &[[f64; D]; D], pivot_col: usize, tolerance: f64) -> LaError { if let Some(error) = Self::non_finite_factor_error(rows) { diff --git a/tests/vs_linalg_inputs.rs b/tests/vs_linalg_inputs.rs index f484583..3848771 100644 --- a/tests/vs_linalg_inputs.rs +++ b/tests/vs_linalg_inputs.rs @@ -10,17 +10,19 @@ use faer::perm::PermRef; use faer::{Mat, Side}; use nalgebra::{Const, DimMin, SMatrix, SVector}; -#[cfg(feature = "exact")] +#[cfg(all(feature = "exact", not(la_stack_v0_4_3_api)))] use la_stack::ExactF64Conversion; use la_stack::{DEFAULT_SINGULAR_TOL, Matrix, Vector}; #[path = "../benches/common/vs_linalg.rs"] pub mod vs_linalg_common; +#[cfg(not(la_stack_v0_4_3_api))] +use vs_linalg_common::make_balanced_dynamic_range_rows; use vs_linalg_common::{ PreparedFaerLuDet, faer_det_from_ldlt, faer_perm_sign, la_stack_dot, la_stack_tolerance, - make_balanced_dynamic_range_rows, make_ill_conditioned_matrix_rows, make_matrix_rows, - make_pivoting_matrix_rows, make_vector_array, matrix_entry, nalgebra_inf_norm, vector_entry, + make_ill_conditioned_matrix_rows, make_matrix_rows, make_pivoting_matrix_rows, + make_vector_array, matrix_entry, nalgebra_inf_norm, vector_entry, }; /// Assert scalar agreement with a tolerance that scales for larger magnitudes. @@ -297,10 +299,14 @@ fn stress_inputs_exercise_pivoting_conditioning_and_scaled_products() { // Bareiss-backed oracle is independent of the timed floating LU path. let baseline = Matrix::<8>::try_from_rows(make_matrix_rows::<8>()).unwrap(); let expected = -baseline.det_exact().unwrap(); + #[cfg(la_stack_v0_4_3_api)] + let expected_f64 = num_traits::ToPrimitive::to_f64(&expected).unwrap(); + #[cfg(not(la_stack_v0_4_3_api))] + let expected_f64 = expected.to_rounded_f64().unwrap(); assert_close( "pivoting LU determinant against exact row-swap oracle", pivoting_det, - expected.to_rounded_f64().unwrap(), + expected_f64, ); } diff --git a/uv.lock b/uv.lock index e713d72..cd22b6f 100644 --- a/uv.lock +++ b/uv.lock @@ -374,7 +374,7 @@ wheels = [ [[package]] name = "la-stack-scripts" -version = "0.4.3" +version = "0.4.4" source = { editable = "." } [package.dev-dependencies] From 2e1eb2e965333fab51934c2b605fe3465e4b57e6 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sun, 12 Jul 2026 21:54:03 -0700 Subject: [PATCH 2/2] fix(release): harden changelog and version generation - Reject tag prefixes embedded in longer non-semver revisions - Preserve fenced examples when stripping Dependabot metadata - Document the conditional exact-solve scaling invariant - Regenerate the v0.4.4 changelog through the release workflow --- CHANGELOG.md | 2 +- scripts/check_docs_version_sync.py | 2 +- scripts/postprocess_changelog.py | 33 ++++++++++++++----- scripts/tests/test_check_docs_version_sync.py | 11 +++++++ scripts/tests/test_postprocess_changelog.py | 15 +++++++++ src/exact.rs | 19 ++++++----- 6 files changed, 63 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8e159..038d39a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.4.4] - 2026-07-12 +## [0.4.4] - 2026-07-13 ### ⚠️ Breaking Changes diff --git a/scripts/check_docs_version_sync.py b/scripts/check_docs_version_sync.py index 347156d..3f1b35a 100644 --- a/scripts/check_docs_version_sync.py +++ b/scripts/check_docs_version_sync.py @@ -261,7 +261,7 @@ def _dependency_references(path: Path, package_name: str) -> list[VersionReferen _README_TAG_LINK_RE = re.compile( r"https://(?:github\.com/acgetchell/la-stack/(?:blob|raw|tree)/|raw\.githubusercontent\.com/acgetchell/la-stack/)" r"(?:v(?P[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)" - r"|(?P[0-9a-f]{7,40}))(?=/|\b)" + r"|(?P[0-9a-f]{7,40}))(?=/|$|[^0-9A-Za-z._+-])" ) diff --git a/scripts/postprocess_changelog.py b/scripts/postprocess_changelog.py index f52c291..781cbf2 100644 --- a/scripts/postprocess_changelog.py +++ b/scripts/postprocess_changelog.py @@ -764,30 +764,45 @@ def _normalize_body_line(line: str, lines: list[str], idx: int, result: list[str return _reflow_line(line) if len(line) > MAX_LINE_WIDTH else line +def _dependabot_metadata_end(lines: list[str], separator_index: int) -> int | None: + """Return the closing marker index for an unfenced Dependabot footer.""" + metadata_start = separator_index + 1 + while metadata_start < len(lines) and not lines[metadata_start].strip(): + metadata_start += 1 + if metadata_start >= len(lines) or lines[metadata_start].strip() != "updated-dependencies:": + return None + + metadata_end = metadata_start + 1 + while metadata_end < len(lines) and lines[metadata_end].strip() != "..." and _opening_code_fence(lines[metadata_end]) is None: + metadata_end += 1 + if metadata_end >= len(lines) or lines[metadata_end].strip() != "...": + return None + return metadata_end + + def _strip_dependabot_metadata(text: str) -> str: """Remove Dependabot's YAML metadata footer from rendered commit bodies.""" lines = text.split("\n") result: list[str] = [] + active_fence: _CodeFence | None = None idx = 0 while idx < len(lines): - if lines[idx].strip() != "---": + if active_fence is not None: result.append(lines[idx]) + if _closes_code_fence(lines[idx], active_fence): + active_fence = None idx += 1 continue - metadata_start = idx + 1 - while metadata_start < len(lines) and not lines[metadata_start].strip(): - metadata_start += 1 - if metadata_start >= len(lines) or lines[metadata_start].strip() != "updated-dependencies:": + active_fence = _opening_code_fence(lines[idx]) + if active_fence is not None: result.append(lines[idx]) idx += 1 continue - metadata_end = metadata_start + 1 - while metadata_end < len(lines) and lines[metadata_end].strip() != "...": - metadata_end += 1 - if metadata_end >= len(lines): + metadata_end = _dependabot_metadata_end(lines, idx) if lines[idx].strip() == "---" else None + if metadata_end is None: result.append(lines[idx]) idx += 1 continue diff --git a/scripts/tests/test_check_docs_version_sync.py b/scripts/tests/test_check_docs_version_sync.py index bd503d3..d3d9724 100644 --- a/scripts/tests/test_check_docs_version_sync.py +++ b/scripts/tests/test_check_docs_version_sync.py @@ -121,6 +121,17 @@ def test_find_version_mismatches_reports_readme_tag_links(tmp_path: Path) -> Non assert [mismatch.reference.version for mismatch in mismatches] == ["1.2.2", "1.2.1", "abc1234"] +@pytest.mark.parametrize("tag", ["v1.2.3.4", "v1.2.3.extra", "v1.2.3_suffix"]) +def test_readme_tag_references_reject_longer_non_semver_tags(tmp_path: Path, tag: str) -> None: + readme = tmp_path / "README.md" + readme.write_text( + f"[invalid](https://github.com/acgetchell/la-stack/blob/{tag}/README.md)\n", + encoding="utf-8", + ) + + assert check_docs_version_sync._readme_tag_references(readme) == [] + + @pytest.mark.parametrize("recipe", ["performance-github-assets", "performance-local-vs-linalg", "performance-release"]) def test_find_version_mismatches_reports_stale_benchmark_current_tags(tmp_path: Path, recipe: str) -> None: _write_project(tmp_path) diff --git a/scripts/tests/test_postprocess_changelog.py b/scripts/tests/test_postprocess_changelog.py index 5ab2ed4..8761aff 100644 --- a/scripts/tests/test_postprocess_changelog.py +++ b/scripts/tests/test_postprocess_changelog.py @@ -110,6 +110,21 @@ def test_preserves_unrelated_thematic_break(self) -> None: assert _strip_dependabot_metadata(content) == content + def test_postprocess_text_strips_dependabot_footer(self) -> None: + content = "- Update setuptools\n\n---\n\nupdated-dependencies:\n- dependency-name: setuptools\n dependency-version: 83.0.0\n...\n\n- Next entry\n" + + result = postprocess_text(content) + + assert "updated-dependencies:" not in result + assert result == "- Update setuptools\n- Next entry\n" + + def test_postprocess_text_preserves_dependabot_markers_inside_yaml_fence(self) -> None: + fenced_example = "```yaml\n---\nupdated-dependencies:\n- dependency-name: setuptools\n...\n```" + + result = postprocess_text(f"Example metadata:\n\n{fenced_example}\n") + + assert fenced_example in result + class TestListContinuationIndent: def test_normalizes_over_indented_top_level_continuation(self) -> None: diff --git a/src/exact.rs b/src/exact.rs index 809c924..9becc9b 100644 --- a/src/exact.rs +++ b/src/exact.rs @@ -42,11 +42,13 @@ //! `A x = b` with a hybrid algorithm that shares the determinant path's exact //! integer scaling and then applies Bareiss elimination to the augmented //! system. Matrix and RHS entries are decomposed via -//! `decompose_proven_finite_f64` into `mantissa × 2^exponent`. Each side is -//! independently scaled to its own minimum exponent before assembly into a -//! `BigInt` augmented system `(A | b)`; the resulting solution is adjusted by -//! the exact power-of-two ratio between those scales. This avoids inflating one -//! side's integers merely because the other side has much smaller entries. +//! `decompose_proven_finite_f64` into `mantissa × 2^exponent`. Each side first +//! derives an independent scale from its minimum exponent. When those scales +//! differ by at most `MAX_SHARED_SCALE_GAP_BITS` (64), both sides use the lower +//! scale; larger gaps retain the independent scales. The resulting solution is +//! adjusted by the exact power-of-two ratio between the selected scales. This +//! shares common factors when inexpensive without inflating one side's integers +//! across a large exponent gap. //! Forward elimination runs entirely in `BigInt` with //! fraction-free Bareiss updates \[7\] — no `BigRational`, no GCD //! normalisation in the `O(D³)` phase. Once the system is upper @@ -816,9 +818,10 @@ fn big_int_exp_to_rounded_f64(value: &BigInt, exp: i32) -> Result // systems) parse every f64 entry into a proof-bearing component, track the // minimum exponent across non-zero entries, and scale each entry by // `2^(exp − e_min)`. Determinants then use direct expansions for D≤4 and -// fraction-free Bareiss elimination for D≥5; solves scale the matrix and RHS -// independently, use Bareiss elimination on the augmented system, and restore -// their exact power-of-two scale ratio after rational back-substitution. +// fraction-free Bareiss elimination for D≥5. Solves derive matrix and RHS scales +// independently, share the lower scale when their gap is at most 64 bits, use +// Bareiss elimination on the augmented system, and restore the selected scales' +// exact power-of-two ratio after rational back-substitution. /// Decomposed finite f64 in the form `(-1)^is_negative · mantissa · 2^exponent`. ///