diff --git a/AGENTS.md b/AGENTS.md index 80184d9..285cf25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,98 @@ When making changes in this repo, prioritize (in order): - Speed - Coverage (but keep the code idiomatic Rust) +## Design Principles + +This is a scientific linear-algebra library. Design decisions trade off in +roughly this priority: mathematical correctness → API stability → +composability → idiomatic Rust → performance within scope. The sections +below spell out what each means in practice; when in doubt, favour the +invariant over the convenient edit. + +### Mathematical correctness as an invariant + +- Exact paths (`*_exact`) never silently lose precision. When f64 output + is required, a separate `*_exact_f64` method returns + [`LaError::Overflow`] on unrepresentability — not a truncation. +- Any f64 operation that can accumulate rounding error either documents + its absolute bound (`det_errbound`, `ERR_COEFF_*`) or explicitly states + that no bound is provided. +- Non-finite values (NaN, ±∞) always surface as + `LaError::NonFinite { row, col }` with source-location metadata. No + silent NaN propagation, no `unwrap_or(f64::NAN)`. +- Algorithms cite their source (Shewchuk, Bareiss, Goldberg, …) via + `REFERENCES.md` and document their conditioning behaviour. + +### Public-API stability + +- Error enums are `#[non_exhaustive]`; public wrapper types are + `#[must_use]`. +- New functionality is additive: use the prelude for ergonomic re-exports; + never silently rename or remove a public item. +- Pre-1.0 semver: `0.x.Y` is a patch-level additive bump, `0.X.y` is a + minor bump that may include breaking changes. Conventional-commit + types (`feat`, `fix`, `refactor`, …) mirror this convention. + +### Composability + +- Const-generic `D` for every core type (`Matrix`, `Vector`, + `Lu`, `Ldlt`). No runtime dimension. +- Stack allocation by default; heap only behind a feature flag or where + exact arithmetic inherently requires it (`BigInt` / `BigRational`). +- Feature flags isolate optional dependency weight; default builds stay + dep-minimal. + +### Idiomatic Rust as a proxy for mathematical clarity + +- `const fn` wherever possible — not for micro-optimisation, but because + compile-time evaluation forces a pure function of inputs. +- `Result<_, LaError>` for all fallible operations. Panics are reserved + for debug-only precondition violations (e.g. LDLT symmetry check) and + documented on the method. +- Borrow by default (`&T`, `&[T]`); return borrowed views when possible. +- Type and function names match textbook vocabulary (`Matrix`, `Vector`, + `Lu`, `Ldlt`, `solve_vec`, `det`, `inf_norm`). Avoid Rust-ecosystem + abstractions that obscure the math. + +### Scientific notation in docs + +- Unicode math (×, ≤, ≥, ∈, Σ, ², `2^-50`, …) is welcome in doc + comments — readability trumps ASCII-only preference. +- Reference literature via `REFERENCES.md` numbered citations (e.g. + `\[8\]`, `\[9-10\]`). +- State invariants mathematically where possible + (`|A[i][i]| > Σ_{j≠i} |A[i][j]|`) rather than prose-only. + +### Performance within scope + +- Performance is a design goal, but strictly subordinate to the + principles above. Never trade correctness, stability, or clarity for + speed; if the two conflict, re-scope the problem rather than + compromise the invariant. +- The library earns its speed through *deliberate scope restriction*: + fixed small dimensions via const generics, stack-allocated storage, + and closed-form algorithms where available (D ≤ 4 for `det_direct` / + `det_errbound`). Problems outside this scope — large or dynamic + dimensions, sparse matrices, parallelism — belong to `nalgebra` or + `faer` (see anti-goals in `README.md`). +- Within scope, prefer allocation-free paths, `const fn` wherever the + inputs allow, and FMA where applicable. Validate any performance + claim against the `bench-vs-linalg` (vs nalgebra / faer) or + `bench-exact` (exact-arithmetic) suites before relying on it. + +### Testing mirrors the principles + +- Unit tests cover known values, error paths, and dimension-generic + correctness across D=2..=5 (see **Dimension Coverage** below). +- Proptests under `tests/proptest_*.rs` cover algebraic invariants + (round-trip, residual, sign agreement) — not just "does it not panic". +- Adversarial inputs (near-singular, large-entry, Hilbert-style + ill-conditioning) accompany well-conditioned inputs in both tests and + benchmarks. +- When a public API has two paths for the same question (fast filter + + exact fallback), a proptest verifies they agree on the domain where + both are defined. + ## Core Rules ### Git Operations @@ -124,17 +216,19 @@ just examples # Run all examples ### Detailed Command Reference - All tests (Rust + Python): `just test-all` -- Benchmark comparison (generate `docs/PERFORMANCE.md`): `just bench-compare` (snapshot) or `just bench-compare v0.3.0` (vs baseline) +- Benchmark comparison (generate `docs/PERFORMANCE.md`): `just bench-compare` (snapshot) or `just bench-compare v0.4.1` (vs baseline) - Benchmarks: `cargo bench` (or `just bench`) - Benchmarks (exact arithmetic): `just bench-exact` -- Benchmarks (save baseline): `just bench-save-baseline v0.3.0` +- Benchmarks (la-stack vs nalgebra/faer): `just bench-vs-linalg [filter]` (full run) or `just bench-vs-linalg-quick [filter]` (reduced) +- Benchmarks (plot vs_linalg CSV/SVG): `just plot-vs-linalg [metric] [stat] [sample] [update_readme]` / `just plot-vs-linalg-readme [metric] [stat] [sample] [update_readme]` +- Benchmarks (save baseline): `just bench-save-baseline v0.4.1` - Build (debug): `cargo build` (or `just build`) - Build (release): `cargo build --release` (or `just build-release`) - Changelog (generate full): `just changelog` (runs `git-cliff -o CHANGELOG.md` + post-processing) -- Changelog (prepend unreleased): `just changelog-unreleased v0.3.0` +- Changelog (prepend unreleased): `just changelog-unreleased v0.4.1` - Coverage (CI XML): `just coverage-ci` - Coverage (HTML): `just coverage` -- Create release tag: `just tag v0.3.0` (creates annotated tag from CHANGELOG.md section) +- Create release tag: `just tag v0.4.1` (creates annotated tag from CHANGELOG.md section) / `just tag-force v0.4.1` (recreate if the tag already exists) - Fast compile check (no binary produced): `cargo check` (or `just check-fast`) - Fast Rust tests (lib + doc): `just test` - Format: `cargo fmt` (or `just fmt`) @@ -195,7 +289,7 @@ When using `gh` to view issues, PRs, or other GitHub objects: Use the `gh` CLI to read, create, and edit issues: - **Read**: `gh issue view --json title,body,labels,milestone | cat` -- **List**: `gh issue list --json number,title,labels --jq '.[] | "#\(.number) \(.title)"' | cat` (add `--label enhancement`, `--milestone v0.4.0`, etc. to filter) +- **List**: `gh issue list --json number,title,labels --jq '.[] | "#\(.number) \(.title)"' | cat` (add `--label enhancement`, `--milestone v0.4.1`, etc. to filter) - **Create**: `gh issue create --title "..." --body "..." --label enhancement --label rust` - **Edit**: `gh issue edit --add-label "..."`, `--milestone "..."`, `--title "..."` - **Comment**: `gh issue comment --body "..."` @@ -204,7 +298,7 @@ Use the `gh` CLI to read, create, and edit issues: When creating or updating issues: - **Labels**: Use appropriate labels: `enhancement`, `bug`, `performance`, `documentation`, `rust`, `python`, etc. -- **Milestones**: Assign to the appropriate milestone (e.g., `v0.3.0`, `v0.4.0`) +- **Milestones**: Assign to the appropriate milestone (e.g., `v0.4.1`, `v0.5.0`) - **Dependencies**: Document relationships in issue body and comments: - "Depends on: #XXX" - this issue cannot start until #XXX is complete - "Blocks: #YYY" - #YYY cannot start until this issue is complete @@ -243,7 +337,12 @@ When creating or updating issues: f64 filter for fast sign resolution - Linear system solve: `solve_exact()`, `solve_exact_f64()` via Gaussian elimination with first-non-zero pivoting in `BigRational` -- Rust tests are inline `#[cfg(test)]` modules in each `src/*.rs` file. +- Rust unit tests are inline `#[cfg(test)]` modules in each `src/*.rs` file. +- Property-based tests live under `tests/proptest_*.rs` (uses the `proptest` + dev-dependency): `proptest_matrix.rs`, `proptest_vector.rs`, + `proptest_factorizations.rs`, and `proptest_exact.rs` (the last gated on + the `exact` feature). They run as integration tests via + `just test-integration` or `just test-all`. - Python tests live in `scripts/tests/` and run via `just test-python` (`uv run pytest`). - The public API re-exports these items from `src/lib.rs`. - The `justfile` defines all dev workflows (see `just --list`). diff --git a/CHANGELOG.md b/CHANGELOG.md index b4f5dd9..d34e109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,25 @@ 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.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.4.1] - 2026-04-21 ### Added - Regression tests for solver and determinant overflow handling [`f763b11`](https://github.com/acgetchell/la-stack/commit/f763b119bcc57276b83f370b0bf7abce654c7eb8) - Defensive-path test coverage for LU and LDLT solve_vec [`87d426f`](https://github.com/acgetchell/la-stack/commit/87d426fca1627445b804fd26b62fc7d9d4f0ae48) - Const-ify Lu/Ldlt det + solve_vec and Matrix inf_norm + det_errbound [`81ecb35`](https://github.com/acgetchell/la-stack/commit/81ecb35bdaf159f1f44d1eb24274ecf82c6567d5) +- Fast-filter boundary proptests for exact determinant sign [`6357db3`](https://github.com/acgetchell/la-stack/commit/6357db35c70bca1b93e6bbf9a4fd231913631950) ### Changed - Report infinite vs finite off-diagonal pairs as asymmetric [`1805779`](https://github.com/acgetchell/la-stack/commit/1805779dbca49183fbfa95c68ec00984966aa551) +- Finalize documentation, benchmarks, and error handling [`0b98d3f`](https://github.com/acgetchell/la-stack/commit/0b98d3f6dbdd74699c318c4744a2b2f9a1b78481) +- Consolidate and expand const-evaluability tests via macros [`f8d80a0`](https://github.com/acgetchell/la-stack/commit/f8d80a01e9913f87e1b19b2ad5ffbc0994e2bfdb) +- Refactor solve_exact to use hybrid Bareiss forward elimination [`ecbbe8a`](https://github.com/acgetchell/la-stack/commit/ecbbe8a571ccaeb9cfedbf0269b8db44d43a5773) +- Polish exact module (Component struct, errors, perf) [`53a5be6`](https://github.com/acgetchell/la-stack/commit/53a5be6abecc0af332398236ed6803ed75564b03) +- Add adversarial-input coverage for exact arithmetic [#80](https://github.com/acgetchell/la-stack/pull/80) [`5bf5815`](https://github.com/acgetchell/la-stack/commit/5bf5815cb165c3b6145c5592420a58085a66efaa) +- Expand exact-arithmetic re-exports and adversarial benchmarks [`b1a491d`](https://github.com/acgetchell/la-stack/commit/b1a491d902ccdaba6f9cd2e6f8e05514b6dfa3de) +- Update AGENTS.md [`1e0648d`](https://github.com/acgetchell/la-stack/commit/1e0648dad3147ef127c775d8969b7cd214a2a6ed) ### Documentation @@ -38,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump taiki-e/install-action from 2.75.7 to 2.75.18 [`433cfc1`](https://github.com/acgetchell/la-stack/commit/433cfc1b01c9128e93c82cb553aa63d4091bace3) - Bump MSRV to Rust 1.95 and adopt new stable features [`0ab3c33`](https://github.com/acgetchell/la-stack/commit/0ab3c336074f2b866256fbe5db8a8ec5306d580a) +### Performance + +- Integer-only forward elimination for gauss_solve [#72](https://github.com/acgetchell/la-stack/pull/72) [`a1d3bdb`](https://github.com/acgetchell/la-stack/commit/a1d3bdbdb6fcb778a78a2a3d0cb66b79484e1472) + ## [0.4.0] - 2026-04-11 ### Added @@ -262,7 +274,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add tarpaulin coverage upload [`7486dfd`](https://github.com/acgetchell/la-stack/commit/7486dfd54e16a6dbde41575c3f35a1acb65f57d2) -[unreleased]: https://github.com/acgetchell/la-stack/compare/v0.4.0..HEAD +[0.4.1]: https://github.com/acgetchell/la-stack/compare/v0.4.0..v0.4.1 [0.4.0]: https://github.com/acgetchell/la-stack/compare/v0.3.0..v0.4.0 [0.3.0]: https://github.com/acgetchell/la-stack/compare/v0.2.2..v0.3.0 [0.2.2]: https://github.com/acgetchell/la-stack/compare/v0.2.1..v0.2.2 diff --git a/CITATION.cff b/CITATION.cff index 4e554c8..db71c31 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,7 +2,7 @@ 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.0 +version: 0.4.1 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 97f6274..def5bd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "la-stack" -version = "0.4.0" +version = "0.4.1" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 33b52e7..7158f8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "la-stack" -version = "0.4.0" +version = "0.4.1" edition = "2024" rust-version = "1.95" license = "BSD-3-Clause" diff --git a/README.md b/README.md index 3e37f39..faa751f 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -la-stack = "0.4.0" +la-stack = "0.4.1" ``` Solve a 5×5 system via LU: @@ -154,7 +154,7 @@ rationals (this pulls in `num-bigint`, `num-rational`, and `num-traits` for ```toml [dependencies] -la-stack = { version = "0.4.0", features = ["exact"] } +la-stack = { version = "0.4.1", features = ["exact"] } ``` **Determinants:** @@ -258,14 +258,14 @@ Summary (median time; lower is better). The “la-stack vs nalgebra/faer” colu | D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | la-stack vs nalgebra | la-stack vs faer | |---:|--------------------:|--------------------:|----------------:|---------------------:|----------------:| -| 2 | 2.309 | 4.365 | 140.156 | +47.1% | +98.4% | -| 3 | 18.331 | 22.706 | 181.074 | +19.3% | +89.9% | -| 4 | 27.430 | 51.372 | 210.451 | +46.6% | +87.0% | -| 5 | 53.819 | 70.722 | 276.064 | +23.9% | +80.5% | -| 8 | 143.611 | 160.309 | 356.960 | +10.4% | +59.8% | -| 16 | 611.393 | 580.793 | 871.704 | -5.3% | +29.9% | -| 32 | 2,631.241 | 2,733.946 | 2,832.816 | +3.8% | +7.1% | -| 64 | 17,233.345 | 14,112.678 | 12,164.571 | -22.1% | -41.7% | +| 2 | 2.173 | 4.448 | 139.923 | +51.2% | +98.4% | +| 3 | 13.989 | 34.607 | 180.026 | +59.6% | +92.2% | +| 4 | 27.580 | 48.435 | 203.163 | +43.1% | +86.4% | +| 5 | 53.517 | 75.935 | 274.375 | +29.5% | +80.5% | +| 8 | 134.859 | 162.859 | 371.463 | +17.2% | +63.7% | +| 16 | 635.775 | 576.171 | 846.189 | -10.3% | +24.9% | +| 32 | 2,704.570 | 2,731.740 | 2,589.494 | +1.0% | -4.4% | +| 64 | 17,381.460 | 13,744.505 | 11,276.642 | -26.5% | -54.1% | ## 📋 Examples diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index 94bc1ea..30969bd 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -39,10 +39,10 @@ just bench-vs-linalg just bench-exact # Save an exact baseline (e.g., before optimising) -just bench-save-baseline v0.4.0 +just bench-save-baseline v0.4.1 # Compare current code against a saved baseline -just bench-compare v0.4.0 +just bench-compare v0.4.1 # Generate a snapshot without comparison just bench-compare diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.csv b/docs/assets/bench/vs_linalg_lu_solve_median.csv index 21d8f0c..aba861c 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.309084094214292,2.303508072051772,2.320556696513248,4.364518872374484,4.358154936085015,4.36964789592874,140.15575262034162,139.53334588117198,140.4495844508752 -3,18.33097673394614,18.273359518783604,18.361403406211608,22.705897034612143,22.564093728463128,22.91072188569287,181.0737171931479,180.38682460200042,181.70129206459293 -4,27.430461775038097,27.41326480258085,27.462758331001304,51.37175309721522,51.29468685146327,51.477167992692316,210.45129217481627,210.11591462124417,211.12191423331922 -5,53.8187502446859,53.70850909928479,53.85944288329581,70.72209914126024,70.58461080060759,70.80642561266758,276.0642099442515,275.26718428973214,277.08908382769255 -8,143.61107101616628,143.08566824669077,144.23973111184426,160.3091383596412,157.74277868119285,162.4765631219522,356.9597107252403,356.09333193008604,357.7579293922214 -16,611.393474121212,607.0554516008083,614.0988895347266,580.7933953429945,578.7449824562907,581.0957836912207,871.7039621195331,869.4837470449172,873.6488504064271 -32,2631.241121426466,2626.669908635426,2635.1795337149238,2733.9455793946936,2732.3185346416067,2737.926103179753,2832.8157108843534,2829.2559523809523,2837.7406015037595 -64,17233.344659711875,17191.217013729976,17341.914285714287,14112.678414368063,13965.669014084508,14162.695618153364,12164.571092017737,12137.462174452254,12181.512446567765 +2,2.172623282439358,2.165444772092298,2.185506739324025,4.448408979470227,4.439782928520427,4.459719277058991,139.9226022608816,139.06474375246668,140.51212151197308 +3,13.989121746672993,13.955023991167852,14.013050521832692,34.606732435908626,33.499470537742226,34.761521044572746,180.02609852620088,179.59708359326262,180.8791974100286 +4,27.58037208037632,27.526517442670773,27.627054765320764,48.435129151777026,48.360737894391505,48.549664199225454,203.162599789916,202.79373021936047,203.32348086504715 +5,53.5167292278733,53.2079241828104,53.645503456564526,75.93470878300764,75.4146378575776,76.19896610821027,274.374828180443,272.8463967468175,275.1594136908799 +8,134.8587910191369,134.33353258195461,135.23630002147613,162.85903692402766,162.5110280122886,163.16671953470214,371.4632572777341,369.98617150369097,373.2859170186846 +16,635.7754523403698,630.758052532669,638.095305070158,576.1714016094243,575.8381924198251,577.8533527696793,846.1888291695698,843.6908420833043,847.8866115184559 +32,2704.5696137095592,2696.7101307744565,2715.9278532608696,2731.7404900064475,2726.3126052188554,2735.1429635561008,2589.4939744827334,2587.8085641182724,2594.9184784361632 +64,17381.45994623656,17346.594885060753,17418.868905879215,13744.504879275653,13711.504555743551,13761.18247949234,11276.642192271553,11263.305944055945,11293.149440751858 diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.svg b/docs/assets/bench/vs_linalg_lu_solve_median.svg index 5a7f890..d7a40c5 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.0 + la-stack v0.4.1 - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + @@ -302,47 +302,47 @@ - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + @@ -356,47 +356,47 @@ - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - +