Skip to content

Commit 159d04e

Browse files
committed
docs: add det_direct example, hoist 4×4 minors, update docs
- Add examples/const_det_4x4.rs showing compile-time 4×4 determinant - Hoist 6 unique 2×2 minors in D=4 path (was computing 12, 6 duplicated) - Fix det_direct doc to include D=0 (empty product → 1.0) with doctest - Add const fn design goal and compile-time determinants section to README - Update AGENTS.md with det_direct and new example - Add const_det_4x4 to justfile examples recipe
1 parent 40cc2b7 commit 159d04e

5 files changed

Lines changed: 87 additions & 30 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ When making changes in this repo, prioritize (in order):
2828
- Pre-commit validation: `just ci`
2929
- Python tests: `just test-python`
3030
- Run a single test (by name filter): `cargo test solve_2x2_basic` (or the full path: `cargo test lu::tests::solve_2x2_basic`)
31-
- Run examples: `just examples` (or `cargo run --example det_5x5` / `cargo run --example solve_5x5`)
31+
- Run examples: `just examples` (or `cargo run --example det_5x5` / `cargo run --example solve_5x5` / `cargo run --example const_det_4x4`)
3232
- Spell check: `just spell-check` (uses `typos.toml` at repo root; add false positives to `[default.extend-words]`)
3333

3434
## Code structure (big picture)
@@ -37,7 +37,7 @@ When making changes in this repo, prioritize (in order):
3737
- The linear algebra implementation is split across:
3838
- `src/lib.rs`: crate root + shared items (`LaError`, `DEFAULT_SINGULAR_TOL`, `DEFAULT_PIVOT_TOL`) + re-exports
3939
- `src/vector.rs`: `Vector<const D: usize>` (`[f64; D]`)
40-
- `src/matrix.rs`: `Matrix<const D: usize>` (`[[f64; D]; D]`) + helpers (`get`, `set`, `inf_norm`, `det`)
40+
- `src/matrix.rs`: `Matrix<const D: usize>` (`[[f64; D]; D]`) + helpers (`get`, `set`, `inf_norm`, `det`, `det_direct`)
4141
- `src/lu.rs`: `Lu<const D: usize>` factorization with partial pivoting (`solve_vec`, `det`)
4242
- `src/ldlt.rs`: `Ldlt<const D: usize>` factorization without pivoting for symmetric SPD/PSD matrices (`solve_vec`, `det`)
4343
- A minimal `justfile` exists for common workflows (see `just --list`).

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ while keeping the API intentionally small and explicit.
2929

3030
-`Copy` types where possible
3131
- ✅ Const-generic dimensions (no dynamic sizes)
32+
-`const fn` where possible (compile-time evaluation of determinants, dot products, etc.)
3233
- ✅ Explicit algorithms (LU, solve, determinant)
3334
- ✅ No runtime dependencies (dev-dependencies are for contributors only)
3435
- ✅ Stack storage only (no heap allocation in core types)
@@ -103,12 +104,36 @@ let det = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap().det();
103104
assert!((det - 1.0).abs() <= 1e-12);
104105
```
105106

107+
## ⚡ Compile-time determinants (D ≤ 4)
108+
109+
`det_direct()` is a `const fn` that computes closed-form determinants for D=1–4
110+
using fused multiply-add, bypassing LU factorization entirely. This enables
111+
compile-time evaluation when inputs are known at compile time:
112+
113+
```rust
114+
use la_stack::prelude::*;
115+
116+
// Evaluated entirely at compile time — no runtime cost.
117+
const DET: Option<f64> = {
118+
let m = Matrix::<3>::from_rows([
119+
[2.0, 0.0, 0.0],
120+
[0.0, 3.0, 0.0],
121+
[0.0, 0.0, 5.0],
122+
]);
123+
m.det_direct()
124+
};
125+
assert_eq!(DET, Some(30.0));
126+
```
127+
128+
The public `det()` method automatically dispatches through the closed-form path
129+
for D ≤ 4 and falls back to LU for D ≥ 5 — no API change needed.
130+
106131
## 🧩 API at a glance
107132

108133
| Type | Storage | Purpose | Key methods |
109134
|---|---|---|---|
110135
| `Vector<D>` | `[f64; D]` | Fixed-length vector | `new`, `zero`, `dot`, `norm2_sq` |
111-
| `Matrix<D>` | `[[f64; D]; D]` | Fixed-size square matrix | `from_rows`, `zero`, `identity`, `lu`, `ldlt`, `det` |
136+
| `Matrix<D>` | `[[f64; D]; D]` | Fixed-size square matrix | `from_rows`, `zero`, `identity`, `lu`, `ldlt`, `det`, `det_direct` |
112137
| `Lu<D>` | `Matrix<D>` + pivot array | Factorization for solves/det | `solve_vec`, `det` |
113138
| `Ldlt<D>` | `Matrix<D>` | Factorization for symmetric SPD/PSD solves/det | `solve_vec`, `det` |
114139

@@ -123,6 +148,7 @@ just examples
123148
# or:
124149
cargo run --example solve_5x5
125150
cargo run --example det_5x5
151+
cargo run --example const_det_4x4
126152
```
127153

128154
## 🤝 Contributing

examples/const_det_4x4.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//! Compile-time 4×4 determinant via `det_direct()`.
2+
//!
3+
//! Because `det_direct` is a `const fn` (Rust 1.94+), the determinant is
4+
//! evaluated entirely at compile time — zero runtime cost.
5+
6+
use la_stack::prelude::*;
7+
8+
/// A 4×4 Hilbert-like matrix with exact rational entries scaled to integers.
9+
const MAT: Matrix<4> = Matrix::<4>::from_rows([
10+
[1.0, 2.0, 3.0, 4.0],
11+
[5.0, 6.0, 7.0, 8.0],
12+
[2.0, 6.0, 1.0, 5.0],
13+
[3.0, 8.0, 2.0, 9.0],
14+
]);
15+
16+
/// Determinant computed at compile time.
17+
const DET: f64 = match MAT.det_direct() {
18+
Some(d) => d,
19+
None => panic!("det_direct only supports D <= 4"),
20+
};
21+
22+
fn main() {
23+
println!("4×4 matrix:");
24+
for r in 0..4 {
25+
print!(" [");
26+
for c in 0..4 {
27+
if c > 0 {
28+
print!(", ");
29+
}
30+
print!("{:5.1}", MAT.get(r, c).unwrap());
31+
}
32+
println!("]");
33+
}
34+
println!();
35+
println!("det (computed at compile time) = {DET}");
36+
}

justfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ doc-check:
187187
examples:
188188
cargo run --quiet --example det_5x5
189189
cargo run --quiet --example solve_5x5
190+
cargo run --quiet --example const_det_4x4
190191

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

src/matrix.rs

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,10 @@ impl<const D: usize> Matrix<D> {
196196
Ldlt::factor(self, tol)
197197
}
198198

199-
/// Closed-form determinant for dimensions 1–4, bypassing LU factorization.
199+
/// Closed-form determinant for dimensions 0–4, bypassing LU factorization.
200200
///
201-
/// Returns `Some(det)` for `D` ∈ {1, 2, 3, 4}, `None` for larger matrices.
201+
/// Returns `Some(det)` for `D` ∈ {0, 1, 2, 3, 4}, `None` for D ≥ 5.
202+
/// `D = 0` returns `Some(1.0)` (empty product).
202203
/// This is a `const fn` (Rust 1.94+) and uses fused multiply-add (`mul_add`)
203204
/// for improved accuracy and performance.
204205
///
@@ -212,6 +213,9 @@ impl<const D: usize> Matrix<D> {
212213
/// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
213214
/// assert!((m.det_direct().unwrap() - (-2.0)).abs() <= 1e-12);
214215
///
216+
/// // D = 0 is the empty product.
217+
/// assert_eq!(Matrix::<0>::zero().det_direct(), Some(1.0));
218+
///
215219
/// // D ≥ 5 returns None.
216220
/// assert!(Matrix::<5>::identity().det_direct().is_none());
217221
/// ```
@@ -239,33 +243,23 @@ impl<const D: usize> Matrix<D> {
239243
)
240244
}
241245
4 => {
242-
// Cofactor expansion on first row → four 3×3 sub-determinants,
243-
// each computed inline (closures are not const-compatible).
246+
// Cofactor expansion on first row → four 3×3 sub-determinants.
247+
// Hoist the 6 unique 2×2 minors from rows 2–3 (each used twice).
244248
let r = &self.rows;
245249

246-
// Minor M00: rows 1-3, cols 1-3
247-
let m00_0 = r[2][2].mul_add(r[3][3], -(r[2][3] * r[3][2]));
248-
let m00_1 = r[2][1].mul_add(r[3][3], -(r[2][3] * r[3][1]));
249-
let m00_2 = r[2][1].mul_add(r[3][2], -(r[2][2] * r[3][1]));
250-
let c00 = r[1][1].mul_add(m00_0, (-r[1][2]).mul_add(m00_1, r[1][3] * m00_2));
251-
252-
// Minor M01: rows 1-3, cols 0,2,3
253-
let m01_0 = r[2][2].mul_add(r[3][3], -(r[2][3] * r[3][2]));
254-
let m01_1 = r[2][0].mul_add(r[3][3], -(r[2][3] * r[3][0]));
255-
let m01_2 = r[2][0].mul_add(r[3][2], -(r[2][2] * r[3][0]));
256-
let c01 = r[1][0].mul_add(m01_0, (-r[1][2]).mul_add(m01_1, r[1][3] * m01_2));
257-
258-
// Minor M02: rows 1-3, cols 0,1,3
259-
let m02_0 = r[2][1].mul_add(r[3][3], -(r[2][3] * r[3][1]));
260-
let m02_1 = r[2][0].mul_add(r[3][3], -(r[2][3] * r[3][0]));
261-
let m02_2 = r[2][0].mul_add(r[3][1], -(r[2][1] * r[3][0]));
262-
let c02 = r[1][0].mul_add(m02_0, (-r[1][1]).mul_add(m02_1, r[1][3] * m02_2));
263-
264-
// Minor M03: rows 1-3, cols 0,1,2
265-
let m03_0 = r[2][1].mul_add(r[3][2], -(r[2][2] * r[3][1]));
266-
let m03_1 = r[2][0].mul_add(r[3][2], -(r[2][2] * r[3][0]));
267-
let m03_2 = r[2][0].mul_add(r[3][1], -(r[2][1] * r[3][0]));
268-
let c03 = r[1][0].mul_add(m03_0, (-r[1][1]).mul_add(m03_1, r[1][2] * m03_2));
250+
// 2×2 minors: s_ij = r[2][i]*r[3][j] - r[2][j]*r[3][i]
251+
let s23 = r[2][2].mul_add(r[3][3], -(r[2][3] * r[3][2])); // cols 2,3
252+
let s13 = r[2][1].mul_add(r[3][3], -(r[2][3] * r[3][1])); // cols 1,3
253+
let s12 = r[2][1].mul_add(r[3][2], -(r[2][2] * r[3][1])); // cols 1,2
254+
let s03 = r[2][0].mul_add(r[3][3], -(r[2][3] * r[3][0])); // cols 0,3
255+
let s02 = r[2][0].mul_add(r[3][2], -(r[2][2] * r[3][0])); // cols 0,2
256+
let s01 = r[2][0].mul_add(r[3][1], -(r[2][1] * r[3][0])); // cols 0,1
257+
258+
// 3×3 cofactors via row 1 expansion using hoisted minors.
259+
let c00 = r[1][1].mul_add(s23, (-r[1][2]).mul_add(s13, r[1][3] * s12));
260+
let c01 = r[1][0].mul_add(s23, (-r[1][2]).mul_add(s03, r[1][3] * s02));
261+
let c02 = r[1][0].mul_add(s13, (-r[1][1]).mul_add(s03, r[1][3] * s01));
262+
let c03 = r[1][0].mul_add(s12, (-r[1][1]).mul_add(s02, r[1][2] * s01));
269263

270264
Some(r[0][0].mul_add(
271265
c00,

0 commit comments

Comments
 (0)