Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### ⚠️ Breaking Changes

- Make exact f64 conversions strict

### Added

- Guard README dependency snippets [`7137fee`](https://github.com/acgetchell/la-stack/commit/7137fee16ab33e08f4dc6a60e02417e3e7c4e020)
Expand All @@ -17,6 +21,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Refresh README determinant examples with explicit fallible handling and
hidden doctest mirrors
- Update CI uv pins to 0.11.19
- Report determinant scale overflow precisely [`928f62b`](https://github.com/acgetchell/la-stack/commit/928f62bba0d837afe04cb8ccb3fbfed6b095d8f7)
- Add a typed LaError::DeterminantScaleOverflow path for exact determinant scale exponent failures
- Convert det_exact_f64 directly from the shared Bareiss integer/exponent pair while preserving Overflow for finite-f64 conversion failures
- Reuse vector finiteness scanning across raw and proof-bearing constructors
- Harden docs version sync checks for reordered inline-table dependency snippets and pruned Markdown traversal
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- Add release performance comparison workflow [`53b5fde`](https://github.com/acgetchell/la-stack/commit/53b5fde13f3e4afbd3db80d324185a091203cb75)
- Extend vs_linalg with LDLT/Cholesky benchmark rows and shared deterministic inputs.
- Add smoke coverage that checks la-stack, nalgebra, and faer agree on benchmark inputs.
- Expand bench-compare to support latest-vs-last reports, suite/scope selection, peer baseline context, and clearer malformed Criterion diagnostics.
- Document the benchmark methodology, release baseline workflow, roadmap direction, and contributor guidance.
- Publish release benchmark baselines [`9497ca5`](https://github.com/acgetchell/la-stack/commit/9497ca5f88dd7800bdf3823123e2a295fcb5ced1)
- Add a release-only benchmark workflow that saves full Criterion baselines for published releases and attaches the archived baseline to the GitHub Release.
- Keep the regular benchmark workflow focused on PR and main-branch comparison runs.
- Document how to restore archived release baselines for future performance comparisons.
- Feat!(api): make Matrix and Vector finite by construction [`1fa2f55`](https://github.com/acgetchell/la-stack/commit/1fa2f55cfac6f249a7e2bf30922901539e580dd8)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- [**breaking**] Make exact f64 conversions strict [`8e33f1a`](https://github.com/acgetchell/la-stack/commit/8e33f1a8ec291bfcb6312375969efce076421e96)
- Add explicit rounded exact-to-f64 APIs for determinant and solve results
- Report exact conversion failures with typed Unrepresentable reasons
- Remove finite proof wrapper APIs now that Matrix and Vector carry finiteness directly
- Move error and tolerance contracts into first-class modules with prelude exports
- Update exact benchmarks to distinguish strict Result paths from rounded f64 paths
- Document and exercise the rounded fallback pattern for RequiresRounding errors

### Changed

- Cover determinant scale overflow boundaries [`532093a`](https://github.com/acgetchell/la-stack/commit/532093a1ed9f65ace159f80aac290907d570ea8a)

- Extract determinant scale exponent calculation into a private helper
- Assert typed DeterminantScaleOverflow errors for dimension conversion and exponent product overflow
- Harden support script parsing [`87e1d00`](https://github.com/acgetchell/la-stack/commit/87e1d0042ad2de7888c1a065ab78524a63f4c045)
- Require Python 3.13 for support-script tooling and align Ruff/Ty with that baseline.
- Replace mypy with strict Ty checking in the Python workflow.
- Parse TOML, JSON, argparse, and Semgrep inputs into typed boundary objects before downstream use.
- Reject malformed Criterion estimates, non-finite timings, invalid confidence intervals, and malformed Semgrep result shapes.

### Documentation

Expand All @@ -27,6 +65,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add citation metadata validation to the release checklist and config lint flow.
- Include CITATION.cff in YAML/CFF formatting checks.

### Fixed

- Escape path regex in benchmark parser test [`1222c93`](https://github.com/acgetchell/la-stack/commit/1222c9325c7b7ea0e1a174e4ee6f0f08e1f7e94b)

Use a literal regex pattern for the malformed Criterion JSON diagnostic so
Windows paths with backslashes do not break pytest's match expression.
- Align ty with Python 3.13 [`b9e0ba0`](https://github.com/acgetchell/la-stack/commit/b9e0ba08e54a15d8eddd5c5c53edc37bbc03939a)

## [0.4.2] - 2026-06-04

### Added
Expand Down Expand Up @@ -378,7 +424,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump rust-version to 1.95 in Cargo.toml
- Bump channel to 1.95.0 in rust-toolchain.toml
- Add core::hint::cold_path() hints at cold/error branches:

- src/exact.rs: validate_finite, validate_finite_vec, gauss_solve
singular return, det_exact_f64 / solve_exact_f64 overflow returns,
det_sign_exact Stage 2 Bareiss fallback
Expand Down
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ while keeping the API intentionally small and explicit.
- ✅ `const fn` where possible (compile-time evaluation of determinants, dot products, etc.)
- ✅ Explicit algorithms (LU, solve, determinant)
- ✅ Robust geometric predicates via optional exact arithmetic (`det_sign_exact`, `det_errbound`)
- ✅ Exact linear system solve via optional arbitrary-precision arithmetic (`solve_exact`, `solve_exact_f64`)
- ✅ Exact linear system solve via optional arbitrary-precision arithmetic (`solve_exact`, strict/rounded f64 conversions)
- ✅ No runtime dependencies by default (optional features may add deps)
- ✅ Stack storage only (no heap allocation in core types)
- ✅ `unsafe` forbidden
Expand Down Expand Up @@ -213,14 +213,19 @@ la-stack = { version = "0.4.2", features = ["exact"] }
**Determinants:**

- **`det_exact()`** — returns the exact determinant as a `BigRational`
- **`det_exact_f64()`** — returns the exact determinant converted to the nearest `f64`
(or `LaError::Overflow` when the exact value is unrepresentable)
- **`det_exact_f64()`** — returns the exact determinant as `f64` only when
it is exactly representable (or `LaError::Unrepresentable` otherwise)
- **`det_exact_rounded_f64()`** — returns the exact determinant rounded to a
finite `f64`
- **`det_sign_exact()`** — returns the provably correct sign (−1, 0, or +1)

**Linear system solve:**

- **`solve_exact(b)`** — solves `Ax = b` exactly, returning `[BigRational; D]`
- **`solve_exact_f64(b)`** — solves `Ax = b` exactly, converting the result to `Vector<D>` (f64)
- **`solve_exact_f64(b)`** — solves `Ax = b` exactly, returning `Vector<D>` only when
every component is exactly representable as `f64`
- **`solve_exact_rounded_f64(b)`** — solves `Ax = b` exactly, returning each
component rounded to finite `f64`

```rust,ignore
use la_stack::prelude::*;
Expand All @@ -239,6 +244,19 @@ fn main() -> Result<(), LaError> {
let det_f64 = m.det_exact_f64()?;
assert_eq!(det_f64, 0.0);

// If strict exact-to-f64 conversion would require rounding, opt in
// explicitly with the rounded API.
let inexact = Matrix::<2>::try_from_rows([
[1.0 + f64::EPSILON, 0.0],
[0.0, 1.0 - f64::EPSILON],
])?;
let rounded_det = match inexact.det_exact_f64() {
Ok(det) => det,
Err(err) if err.requires_rounding() => inexact.det_exact_rounded_f64()?,
Err(err) => return Err(err),
};
assert_eq!(rounded_det.to_bits(), 1.0f64.to_bits());

// If the exact determinant cannot fit in f64, keep the BigRational value.
let big = f64::MAX / 2.0;
let huge = Matrix::<3>::try_from_rows([
Expand All @@ -247,7 +265,12 @@ fn main() -> Result<(), LaError> {
[0.0, big, 1.0],
])?;
let huge_det = huge.det_exact()?;
assert_eq!(huge.det_exact_f64(), Err(LaError::Overflow { index: None }));
assert_eq!(
huge.det_exact_f64()
.err()
.and_then(|err| err.unrepresentable_reason()),
Some(UnrepresentableReason::NotFinite)
);
println!("exact determinant = {huge_det}");

// Exact linear system solve
Expand Down Expand Up @@ -338,7 +361,8 @@ compose the same bound themselves.
Storage shown above reflects the intentional `f64` scalar model.

`Matrix<D>` key methods: `lu`, `ldlt`, `det`, `det_direct`, `det_errbound`,
`det_exact`¹, `det_exact_f64`¹, `det_sign_exact`¹, `solve_exact`¹, `solve_exact_f64`¹.
`det_exact`¹, `det_exact_f64`¹, `det_exact_rounded_f64`¹, `det_sign_exact`¹,
`solve_exact`¹, `solve_exact_f64`¹, `solve_exact_rounded_f64`¹.
Matrix and vector constructors validate non-finite inputs at public API
boundaries. After construction, `Matrix<D>` and `Vector<D>` carry that
finite-storage invariant directly, so kernels do not revalidate stored entries.
Expand Down
82 changes: 61 additions & 21 deletions benches/exact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
//! a fixed-seed corpus of diagonally-dominant random matrices per
//! dimension. Each operation is pre-timed across the corpus to select
//! p50/p95/p99 cumulative input subsets, then measured with Criterion.
//!
//! Fallible exact-to-f64 conversions use a `_result` suffix. Those rows measure
//! the full `Result` path, including valid `Err(Unrepresentable)` outcomes for
//! inputs whose exact answer cannot be represented as finite binary64.

use std::array;
use std::cell::Cell;
Expand Down Expand Up @@ -202,7 +206,8 @@ enum ExactRandomOperation {
DetSignExact,
DetExact,
SolveExact,
SolveExactF64,
SolveExactF64Result,
SolveExactRoundedF64,
}

impl ExactRandomOperation {
Expand All @@ -212,7 +217,8 @@ impl ExactRandomOperation {
Self::DetSignExact => "det_sign_exact",
Self::DetExact => "det_exact",
Self::SolveExact => "solve_exact",
Self::SolveExactF64 => "solve_exact_f64",
Self::SolveExactF64Result => "solve_exact_f64_result",
Self::SolveExactRoundedF64 => "solve_exact_rounded_f64",
}
}
}
Expand Down Expand Up @@ -323,10 +329,14 @@ fn run_random_operation<const D: usize>(
);
let _ = black_box(x);
}
ExactRandomOperation::SolveExactF64 => {
ExactRandomOperation::SolveExactF64Result => {
let x = black_box(input.matrix).solve_exact_f64(black_box(input.rhs));
let _ = black_box(x);
}
ExactRandomOperation::SolveExactRoundedF64 => {
let x = require_ok(
black_box(input.matrix).solve_exact_f64(black_box(input.rhs)),
"exact linear solve converted to f64",
black_box(input.matrix).solve_exact_rounded_f64(black_box(input.rhs)),
"exact linear solve rounded to f64",
);
let _ = black_box(x);
}
Expand Down Expand Up @@ -488,9 +498,10 @@ fn hilbert<const D: usize>() -> Matrix<D> {
)
}

/// Populate a Criterion group with the four headline exact-arithmetic
/// Populate a Criterion group with the five headline exact-arithmetic
/// benches on a single `(matrix, rhs)` pair: `det_sign_exact`,
/// `det_exact`, `solve_exact`, `solve_exact_f64`.
/// `det_exact`, `solve_exact`, `solve_exact_f64_result`, and
/// `solve_exact_rounded_f64`.
///
/// Used by every adversarial-input group so each one measures the same
/// operations, making the resulting tables directly comparable.
Expand Down Expand Up @@ -523,11 +534,18 @@ fn bench_extreme_group<const D: usize>(
});
});

group.bench_function("solve_exact_f64", |bencher| {
group.bench_function("solve_exact_f64_result", |bencher| {
bencher.iter(|| {
let x = black_box(m).solve_exact_f64(black_box(rhs));
let _ = black_box(x);
});
});

group.bench_function("solve_exact_rounded_f64", |bencher| {
bencher.iter(|| {
let x = require_ok(
black_box(m).solve_exact_f64(black_box(rhs)),
"exact linear solve converted to f64",
black_box(m).solve_exact_rounded_f64(black_box(rhs)),
"exact linear solve rounded to f64",
);
let _ = black_box(x);
});
Expand Down Expand Up @@ -571,12 +589,20 @@ macro_rules! gen_exact_benches_for_dim {
});
});

// === det_exact_f64 (exact → f64) ===
[<group_d $d>].bench_function("det_exact_f64", |bencher| {
// === det_exact_f64 (fallible exact → f64 Result) ===
[<group_d $d>].bench_function("det_exact_f64_result", |bencher| {
bencher.iter(|| {
let det = black_box(a).det_exact_f64();
black_box(det);
});
});

// === det_exact_rounded_f64 (lossy exact → f64) ===
[<group_d $d>].bench_function("det_exact_rounded_f64", |bencher| {
bencher.iter(|| {
let det = require_ok(
black_box(a).det_exact_f64(),
"exact determinant converted to f64",
black_box(a).det_exact_rounded_f64(),
"exact determinant rounded to f64",
);
black_box(det);
});
Expand All @@ -601,12 +627,20 @@ macro_rules! gen_exact_benches_for_dim {
});
});

// === solve_exact_f64 (exact → f64) ===
[<group_d $d>].bench_function("solve_exact_f64", |bencher| {
// === solve_exact_f64 (fallible exact → f64 Result) ===
[<group_d $d>].bench_function("solve_exact_f64_result", |bencher| {
bencher.iter(|| {
let x = black_box(a).solve_exact_f64(black_box(rhs));
black_box(x);
});
});

// === solve_exact_rounded_f64 (lossy exact → f64) ===
[<group_d $d>].bench_function("solve_exact_rounded_f64", |bencher| {
bencher.iter(|| {
let x = require_ok(
black_box(a).solve_exact_f64(black_box(rhs)),
"exact linear solve converted to f64",
black_box(a).solve_exact_rounded_f64(black_box(rhs)),
"exact linear solve rounded to f64",
);
black_box(x);
});
Expand Down Expand Up @@ -642,7 +676,12 @@ macro_rules! gen_random_percentile_benches_for_dim {
bench_random_percentile_operation(
&mut [<group_random_percentile_d $d>],
&corpus,
ExactRandomOperation::SolveExactF64,
ExactRandomOperation::SolveExactF64Result,
);
bench_random_percentile_operation(
&mut [<group_random_percentile_d $d>],
&corpus,
ExactRandomOperation::SolveExactRoundedF64,
);

[<group_random_percentile_d $d>].finish();
Expand Down Expand Up @@ -677,8 +716,9 @@ fn main() {

// === Adversarial / extreme-input groups ===
//
// Each group runs the same four exact-arithmetic benches
// (`det_sign_exact`, `det_exact`, `solve_exact`, `solve_exact_f64`)
// Each group runs the same five exact-arithmetic benches
// (`det_sign_exact`, `det_exact`, `solve_exact`, `solve_exact_f64_result`,
// `solve_exact_rounded_f64`)
// via `bench_extreme_group`, so the resulting tables are directly
// comparable across input classes.

Expand Down
30 changes: 26 additions & 4 deletions docs/BENCHMARKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ la-stack has two Criterion benchmark suites:
dependency version used here.

- **`exact`** (`benches/exact.rs`) — measures exact-arithmetic methods
(`det_exact`, `solve_exact`, `det_sign_exact`, etc.) alongside f64
(`det_exact`, `solve_exact`, `det_sign_exact`, strict `*_result`
conversions, and lossy `*_rounded_f64` conversions) alongside f64
baselines (`det`, `det_direct`) across D=2–5. Use this to understand
the cost of exact arithmetic and track optimization progress.
In addition to the fixed per-dimension groups (`exact_d{2..5}`), the
Expand All @@ -35,10 +36,14 @@ la-stack has two Criterion benchmark suites:
ill-conditioned matrices whose non-terminating-in-binary entries
stress the `f64_decompose → BigInt` scaling path.

Each random percentile and adversarial group runs the same four
Each random percentile and adversarial group runs the same five
exact-arithmetic benches (`det_sign_exact`, `det_exact`, `solve_exact`,
`solve_exact_f64`) so the resulting tables are directly comparable
across input classes.
`solve_exact_f64_result`, `solve_exact_rounded_f64`) so the resulting tables
are directly comparable across input classes. Rows with a `_result` suffix
measure the strict fallible conversion path, including valid
`Err(Unrepresentable)` outcomes when the exact answer is not
finite-binary64 representable. Rows with a `_rounded_f64` suffix measure the
intentionally lossy finite-binary64 conversion path.

## `vs_linalg` methodology

Expand Down Expand Up @@ -188,6 +193,23 @@ local. The report includes per-dimension tables showing median times,
percent change, speedup, and last-release nalgebra/faer context where a
matching `vs_linalg` peer exists.

For exact-arithmetic comparisons against v0.4.2 or older baselines, rows such
as `det_exact_rounded_f64 (vs det_exact_f64)` mean the current rounded API is
being compared to the historical lossy `*_exact_f64` benchmark. Rows such as
`det_exact_f64_result (vs det_exact_f64)` intentionally show the overhead of
the new strict conversion contract against that same historical baseline.

The default `release-signal` scope reports exact-arithmetic rows whose inputs
are fixed across versions: deterministic D=2..=5 cases plus adversarial fixed
matrices. Random percentile groups are exploratory tail probes; each benchmark
run selects p50/p95/p99 input sets by timing the implementation under test, so
those rows can measure different corpus subsets across versions. Include them
when investigating tails with:

```bash
uv run bench-compare v0.4.2 --suite exact --scope all-benches
```

To generate a current snapshot without a saved baseline:

```bash
Expand Down
Loading
Loading