Skip to content

Commit 97b5126

Browse files
authored
Merge pull request #48 from acgetchell/feat/solve-exact
feat: exact linear system solve (solve_exact, solve_exact_f64)
2 parents 92ce476 + 2bddccf commit 97b5126

7 files changed

Lines changed: 630 additions & 21 deletions

File tree

AGENTS.md

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ testable.
9393
#### Reference examples
9494

9595
- `src/matrix.rs``gen_public_api_matrix_tests!`
96-
- `src/exact.rs``gen_det_exact_tests!`, `gen_det_exact_f64_tests!`
96+
- `src/lu.rs``gen_public_api_pivoting_solve_vec_and_det_tests!`, `gen_public_api_tridiagonal_smoke_solve_vec_and_det_tests!`
97+
- `src/ldlt.rs``gen_public_api_ldlt_identity_tests!`, `gen_public_api_ldlt_diagonal_tests!`
98+
- `src/exact.rs``gen_det_exact_tests!`, `gen_det_exact_f64_tests!`, `gen_solve_exact_tests!`, `gen_solve_exact_f64_tests!`
9799

98100
#### When single-dimension tests are acceptable
99101

@@ -143,8 +145,10 @@ just examples # Run all examples
143145
- Run a single test (by name filter): `cargo test solve_2x2_basic` (or the full path: `cargo test lu::tests::solve_2x2_basic`)
144146
- Run exact-feature tests: `cargo test --features exact --verbose` (or `just test-exact`)
145147
- Run examples: `just examples` (or `cargo run --example det_5x5` / `cargo run --example solve_5x5` /
146-
`cargo run --example const_det_4x4` / `cargo run --features exact --example exact_det_3x3` /
147-
`cargo run --features exact --example exact_sign_3x3`)
148+
`cargo run --example ldlt_solve_3x3` / `cargo run --example const_det_4x4` /
149+
`cargo run --features exact --example exact_det_3x3` /
150+
`cargo run --features exact --example exact_sign_3x3` /
151+
`cargo run --features exact --example exact_solve_3x3`)
148152
- Spell check: `just spell-check` (uses `typos.toml` at repo root; add false positives to `[default.extend-words]`)
149153

150154
### Changelog
@@ -155,6 +159,15 @@ just examples # Run all examples
155159

156160
### GitHub Issues
157161

162+
Use the `gh` CLI to read, create, and edit issues:
163+
164+
- **Read**: `gh issue view <number>` (or `--json title,body,labels,milestone` for structured data)
165+
- **List**: `gh issue list` (add `--label enhancement`, `--milestone v0.3.0`, etc. to filter)
166+
- **Create**: `gh issue create --title "..." --body "..." --label enhancement --label rust`
167+
- **Edit**: `gh issue edit <number> --add-label "..."`, `--milestone "..."`, `--title "..."`
168+
- **Comment**: `gh issue comment <number> --body "..."`
169+
- **Close**: `gh issue close <number>` (with optional `--reason completed` or `--reason "not planned"`)
170+
158171
When creating or updating issues:
159172

160173
- **Labels**: Use appropriate labels: `enhancement`, `bug`, `performance`, `documentation`, `rust`, `python`, etc.
@@ -173,10 +186,10 @@ When creating or updating issues:
173186

174187
## Feature flags
175188

176-
- `exact` — enables exact determinant methods via `BigRational` arithmetic:
177-
`det_exact()`, `det_exact_f64()`, and `det_sign_exact()`.
189+
- `exact` — enables exact arithmetic methods via `BigRational`:
190+
`det_exact()`, `det_exact_f64()`, `det_sign_exact()`, `solve_exact()`, and `solve_exact_f64()`.
178191
Also re-exports `BigRational` from the crate root and prelude.
179-
Gates `src/exact.rs`, additional tests, and the `exact_det_3x3`/`exact_sign_3x3` examples.
192+
Gates `src/exact.rs`, additional tests, and the `exact_det_3x3`/`exact_sign_3x3`/`exact_solve_3x3` examples.
180193
Clippy, doc builds, and test commands have dedicated `--features exact` variants.
181194

182195
## Code structure (big picture)
@@ -188,9 +201,11 @@ When creating or updating issues:
188201
- `src/matrix.rs`: `Matrix<const D: usize>` (`[[f64; D]; D]`) + helpers (`get`, `set`, `inf_norm`, `det`, `det_direct`)
189202
- `src/lu.rs`: `Lu<const D: usize>` factorization with partial pivoting (`solve_vec`, `det`)
190203
- `src/ldlt.rs`: `Ldlt<const D: usize>` factorization without pivoting for symmetric SPD/PSD matrices (`solve_vec`, `det`)
191-
- `src/exact.rs`: `det_exact()`, `det_exact_f64()`, `det_sign_exact()` — exact determinant
192-
value and sign via Bareiss in `BigRational`; `det_sign_exact()` adds a Shewchuk-style f64
193-
filter for fast sign resolution; `features = ["exact"]`
204+
- `src/exact.rs`: exact arithmetic behind `features = ["exact"]`:
205+
- Determinants: `det_exact()`, `det_exact_f64()`, `det_sign_exact()` via Bareiss in
206+
`BigRational`; `det_sign_exact()` adds a Shewchuk-style f64 filter for fast sign resolution
207+
- Linear system solve: `solve_exact()`, `solve_exact_f64()` via Gaussian elimination
208+
with first-non-zero pivoting in `BigRational`
194209
- Rust tests are inline `#[cfg(test)]` modules in each `src/*.rs` file.
195210
- Python tests live in `scripts/tests/` and run via `just test-python` (`uv run pytest`).
196211
- The public API re-exports these items from `src/lib.rs`.

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ version = "0.2.2"
44
edition = "2024"
55
rust-version = "1.94"
66
license = "BSD-3-Clause"
7-
description = "Small, stack-allocated linear algebra for fixed dimensions"
7+
description = "Fast, stack-allocated linear algebra for fixed dimensions"
88
readme = "README.md"
99
documentation = "https://docs.rs/la-stack"
1010
repository = "https://github.com/acgetchell/la-stack"
1111
homepage = "https://github.com/acgetchell/la-stack"
1212
categories = [ "mathematics", "science" ]
13-
keywords = [ "linear-algebra", "geometry", "const-generics" ]
13+
keywords = [ "linear-algebra", "geometry", "const-generics", "exact-arithmetic", "robust-predicates" ]
1414

1515
[dependencies]
1616
# All runtime deps are optional; see [features] below.
@@ -39,6 +39,10 @@ required-features = [ "exact" ]
3939
name = "exact_sign_3x3"
4040
required-features = [ "exact" ]
4141

42+
[[example]]
43+
name = "exact_solve_3x3"
44+
required-features = [ "exact" ]
45+
4246
[[bench]]
4347
name = "vs_linalg"
4448
harness = false

README.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ while keeping the API intentionally small and explicit.
3232
-`const fn` where possible (compile-time evaluation of determinants, dot products, etc.)
3333
- ✅ Explicit algorithms (LU, solve, determinant)
3434
- ✅ Robust geometric predicates via optional exact arithmetic (`det_sign_exact`, `det_errbound`)
35+
- ✅ Exact linear system solve via optional arbitrary-precision arithmetic (`solve_exact`, `solve_exact_f64`)
3536
- ✅ No runtime dependencies by default (optional features may add deps)
3637
- ✅ Stack storage only (no heap allocation in core types)
3738
-`unsafe` forbidden
@@ -46,9 +47,9 @@ See [CHANGELOG.md](CHANGELOG.md) for details.
4647

4748
## 🔢 Scalar types
4849

49-
Today, the core types are implemented for `f64`. The intent is to support `f32` and `f64`
50-
(and `f128` if/when Rust gains a stable primitive for it). Arbitrary-precision arithmetic
51-
is available via the optional `"exact"` feature (see below).
50+
The core types use `f64`. When f64 precision is insufficient (e.g. near-degenerate
51+
geometric configurations), the optional `"exact"` feature provides arbitrary-precision
52+
arithmetic via `BigRational` (see below).
5253

5354
## 🚀 Quickstart
5455

@@ -136,22 +137,30 @@ for D ≤ 4 and falls back to LU for D ≥ 5 — no API change needed.
136137
## 🔬 Exact arithmetic (`"exact"` feature)
137138

138139
The default build has **zero runtime dependencies**. Enable the optional
139-
`exact` Cargo feature to add exact determinant methods using adaptive-precision
140-
arithmetic (this pulls in `num-bigint`, `num-rational`, and `num-traits` for
140+
`exact` Cargo feature to add exact arithmetic methods using arbitrary-precision
141+
rationals (this pulls in `num-bigint`, `num-rational`, and `num-traits` for
141142
`BigRational`):
142143

143144
```toml
144145
[dependencies]
145146
la-stack = { version = "0.2.2", features = ["exact"] }
146147
```
147148

149+
**Determinants:**
150+
148151
- **`det_exact()`** — returns the exact determinant as a `BigRational`
149152
- **`det_exact_f64()`** — returns the exact determinant converted to the nearest `f64`
150153
- **`det_sign_exact()`** — returns the provably correct sign (−1, 0, or +1)
151154

155+
**Linear system solve:**
156+
157+
- **`solve_exact(b)`** — solves `Ax = b` exactly, returning `[BigRational; D]`
158+
- **`solve_exact_f64(b)`** — solves `Ax = b` exactly, converting the result to `Vector<D>` (f64)
159+
152160
```rust,ignore
153161
use la_stack::prelude::*;
154162
163+
// Exact determinant
155164
let m = Matrix::<3>::from_rows([
156165
[1.0, 2.0, 3.0],
157166
[4.0, 5.0, 6.0],
@@ -161,6 +170,13 @@ assert_eq!(m.det_sign_exact().unwrap(), 0); // exactly singular
161170
162171
let det = m.det_exact().unwrap();
163172
assert_eq!(det, BigRational::from_integer(0.into())); // exact zero
173+
174+
// Exact linear system solve
175+
let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
176+
let b = Vector::<2>::new([5.0, 11.0]);
177+
let x = a.solve_exact_f64(b).unwrap().into_array();
178+
assert!((x[0] - 1.0).abs() <= f64::EPSILON);
179+
assert!((x[1] - 2.0).abs() <= f64::EPSILON);
164180
```
165181

166182
`BigRational` is re-exported from the crate root and prelude when the `exact`
@@ -204,11 +220,14 @@ exposed for advanced use cases.
204220
| Type | Storage | Purpose | Key methods |
205221
|---|---|---|---|
206222
| `Vector<D>` | `[f64; D]` | Fixed-length vector | `new`, `zero`, `dot`, `norm2_sq` |
207-
| `Matrix<D>` | `[[f64; D]; D]` | Square matrix | `lu`, `ldlt`, `det`, `det_direct`, `det_errbound`, `det_exact`¹, `det_exact_f64`¹, `det_sign_exact`¹ |
223+
| `Matrix<D>` | `[[f64; D]; D]` | Square matrix | See below |
208224
| `Lu<D>` | `Matrix<D>` + pivot array | Factorization for solves/det | `solve_vec`, `det` |
209225
| `Ldlt<D>` | `Matrix<D>` | Factorization for symmetric SPD/PSD solves/det | `solve_vec`, `det` |
210226

211-
Storage shown above reflects the current `f64` implementation.
227+
Storage shown above reflects the `f64` implementation.
228+
229+
`Matrix<D>` key methods: `lu`, `ldlt`, `det`, `det_direct`, `det_errbound`,
230+
`det_exact`¹, `det_exact_f64`¹, `det_sign_exact`¹, `solve_exact`¹, `solve_exact_f64`¹.
212231

213232
¹ Requires `features = ["exact"]`.
214233

@@ -218,18 +237,22 @@ The `examples/` directory contains small, runnable programs:
218237

219238
- **`solve_5x5`** — solve a 5×5 system via LU with partial pivoting
220239
- **`det_5x5`** — determinant of a 5×5 matrix via LU
240+
- **`ldlt_solve_3x3`** — solve a 3×3 symmetric positive definite system via LDLT
221241
- **`const_det_4x4`** — compile-time 4×4 determinant via `det_direct()`
222242
- **`exact_det_3x3`** — exact determinant value of a near-singular 3×3 matrix (requires `exact` feature)
223243
- **`exact_sign_3x3`** — exact determinant sign of a near-singular 3×3 matrix (requires `exact` feature)
244+
- **`exact_solve_3x3`** — exact solve of a near-singular 3×3 system vs f64 LU (requires `exact` feature)
224245

225246
```bash
226247
just examples
227248
# or individually:
228249
cargo run --example solve_5x5
229250
cargo run --example det_5x5
251+
cargo run --example ldlt_solve_3x3
230252
cargo run --example const_det_4x4
231253
cargo run --features exact --example exact_det_3x3
232254
cargo run --features exact --example exact_sign_3x3
255+
cargo run --features exact --example exact_solve_3x3
233256
```
234257

235258
## 🤝 Contributing

examples/exact_solve_3x3.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Exact linear system solve for a near-singular 3×3 system.
2+
//!
3+
//! This example demonstrates `solve_exact()` and `solve_exact_f64()`, which use
4+
//! arbitrary-precision rational arithmetic to compute a provably correct
5+
//! solution — even when the matrix is so close to singular that the f64 LU
6+
//! solve produces a wildly inaccurate result.
7+
//!
8+
//! Run with: `cargo run --features exact --example exact_solve_3x3`
9+
10+
use la_stack::prelude::*;
11+
12+
fn main() {
13+
// Near-singular 3×3 system.
14+
//
15+
// The base matrix [[1,2,3],[4,5,6],[7,8,9]] is exactly singular (rows in
16+
// arithmetic progression). Perturbing entry (0,0) by 2^-50 ≈ 8.9e-16
17+
// makes it invertible but extremely ill-conditioned.
18+
let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
19+
let a = Matrix::<3>::from_rows([
20+
[1.0 + perturbation, 2.0, 3.0],
21+
[4.0, 5.0, 6.0],
22+
[7.0, 8.0, 9.0],
23+
]);
24+
25+
let b = Vector::<3>::new([1.0, 2.0, 3.0]);
26+
27+
// f64 LU solve (using zero pivot tolerance since the matrix is nearly singular
28+
// and would be rejected by DEFAULT_PIVOT_TOL).
29+
let lu_x = a.lu(0.0).unwrap().solve_vec(b).unwrap().into_array();
30+
31+
// Exact solve.
32+
let exact_x = a.solve_exact(b).unwrap();
33+
let exact_x_f64 = a.solve_exact_f64(b).unwrap().into_array();
34+
35+
println!("Near-singular 3×3 system (perturbation = 2^-50 ≈ {perturbation:.2e}):");
36+
for r in 0..3 {
37+
print!(" [");
38+
for c in 0..3 {
39+
if c > 0 {
40+
print!(", ");
41+
}
42+
print!("{:22.18}", a.get(r, c).unwrap());
43+
}
44+
println!("]");
45+
}
46+
println!(
47+
"b = [{}, {}, {}]",
48+
b.as_array()[0],
49+
b.as_array()[1],
50+
b.as_array()[2]
51+
);
52+
println!();
53+
println!(
54+
"f64 LU solve: x = [{:+.6e}, {:+.6e}, {:+.6e}]",
55+
lu_x[0], lu_x[1], lu_x[2]
56+
);
57+
println!(
58+
"solve_exact(): x = [{}, {}, {}]",
59+
exact_x[0], exact_x[1], exact_x[2]
60+
);
61+
println!(
62+
"solve_exact_f64(): x = [{:+.6e}, {:+.6e}, {:+.6e}]",
63+
exact_x_f64[0], exact_x_f64[1], exact_x_f64[2]
64+
);
65+
}

examples/ldlt_solve_3x3.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! Solve a 3×3 symmetric positive definite system via LDLT factorization.
2+
//!
3+
//! LDLT is the natural choice for SPD matrices (e.g. Gram matrices, covariance
4+
//! matrices, stiffness matrices). It avoids the row swaps of LU and exploits
5+
//! symmetry, using roughly half the work.
6+
//!
7+
//! Run with: `cargo run --example ldlt_solve_3x3`
8+
9+
use la_stack::prelude::*;
10+
11+
fn main() -> Result<(), LaError> {
12+
// Symmetric positive definite 3×3 matrix (classic SPD tridiagonal).
13+
let a = Matrix::<3>::from_rows([[4.0, -1.0, 0.0], [-1.0, 4.0, -1.0], [0.0, -1.0, 4.0]]);
14+
15+
// Choose x = [1, 2, 3]. Then b = A x = [2, 4, 10].
16+
let b = Vector::<3>::new([2.0, 4.0, 10.0]);
17+
18+
let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL)?;
19+
let x = ldlt.solve_vec(b)?.into_array();
20+
let det = ldlt.det();
21+
22+
println!("A (3×3 SPD tridiagonal):");
23+
for r in 0..3 {
24+
print!(" [");
25+
for c in 0..3 {
26+
if c > 0 {
27+
print!(", ");
28+
}
29+
print!("{:5.1}", a.get(r, c).unwrap());
30+
}
31+
println!("]");
32+
}
33+
println!(
34+
"b = [{}, {}, {}]",
35+
b.as_array()[0],
36+
b.as_array()[1],
37+
b.as_array()[2]
38+
);
39+
println!();
40+
println!("x = [{:.6}, {:.6}, {:.6}]", x[0], x[1], x[2]);
41+
println!("det = {det}");
42+
43+
Ok(())
44+
}

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,11 @@ doc-check:
211211
examples:
212212
cargo run --quiet --example det_5x5
213213
cargo run --quiet --example solve_5x5
214+
cargo run --quiet --example ldlt_solve_3x3
214215
cargo run --quiet --example const_det_4x4
216+
cargo run --quiet --features exact --example exact_det_3x3
215217
cargo run --quiet --features exact --example exact_sign_3x3
218+
cargo run --quiet --features exact --example exact_solve_3x3
216219

217220
# Fix (mutating): apply formatters/auto-fixes
218221
fix: toml-fmt fmt python-fix shell-fmt markdown-fix yaml-fix

0 commit comments

Comments
 (0)