Skip to content

Commit 52e6a34

Browse files
authored
perf(lambda-rs): Compute the determinant via Gaussian elimination for smaller matrices (#194)
## Summary Optimize determinant computation in `lambda-rs` matrix math by replacing recursive Laplace expansion with Gaussian elimination plus partial pivoting. This reduces determinant evaluation from factorial-time growth to cubic-time growth for square matrices and adds stack-allocated fast paths for common 3x3 and 4x4 cases. ## Related Issues None linked. ## Changes - Replace recursive determinant expansion in `crates/lambda-rs/src/math/matrix.rs` with Gaussian elimination and partial pivoting. - Add fixed-size stack fast paths for 3x3 and 4x4 matrices to avoid heap allocation in common transform-sized matrices. - Change the larger-matrix fallback scratch space from `Vec<Vec<f32>>` to a contiguous row-major `Vec<f32>` for better cache locality and fewer allocations. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] Feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation (updates to docs, specs, tutorials, or comments) - [ ] Refactor (code change that neither fixes a bug nor adds a feature) - [x] Performance (change that improves performance) - [ ] Test (adding or updating tests) - [ ] Build/CI (changes to build process or CI configuration) ## Affected Crates - [x] `lambda-rs` - [ ] `lambda-rs-platform` - [ ] `lambda-rs-args` - [ ] `lambda-rs-logging` - [ ] Other: ## Checklist - [ ] Code follows the repository style guidelines (`cargo +nightly fmt --all`) - [ ] Code passes clippy (`cargo clippy --workspace --all-targets -- -D warnings`) - [ ] Tests pass (`cargo test --workspace`) - [x] New code includes appropriate documentation - [ ] Public API changes are documented - [ ] Breaking changes are noted in this PR description ## Testing **Commands run:** ```bash cargo test -p lambda-rs math::matrix ``` **Manual verification steps (if applicable):** 1. Confirm `determinant()` uses stack fast paths for 3x3 and 4x4 matrices. 2. Confirm larger matrices use a contiguous flat scratch buffer instead of nested vectors. ## Screenshots/Recordings Not applicable. ## Platform Testing - [x] macOS - [ ] Windows - [ ] Linux ## Additional Notes Asymptotic runtime for determinant computation improves from `O(n!)` under recursive Laplace expansion to `O(n^3)` with Gaussian elimination. Space remains `O(n^2)`, with lower constant overhead for both fixed-size and larger-matrix paths.
2 parents c38f004 + 9bdf71e commit 52e6a34

File tree

1 file changed

+194
-27
lines changed

1 file changed

+194
-27
lines changed

crates/lambda-rs/src/math/matrix.rs

Lines changed: 194 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,63 @@ pub fn identity_matrix<
259259
return result;
260260
}
261261

262+
/// Compute determinant via Gaussian elimination on fixed-size stack storage.
263+
///
264+
/// This avoids heap allocations in hot fixed-size paths (for example 4x4
265+
/// transform matrices).
266+
///
267+
/// # Arguments
268+
///
269+
/// - `data`: Square matrix values stored in stack-allocated fixed-size arrays.
270+
///
271+
/// # Returns
272+
///
273+
/// - Determinant of `data`.
274+
/// - `0.0` when elimination detects an exact zero pivot after pivoting
275+
/// (singular matrix).
276+
fn determinant_gaussian_stack<const N: usize>(mut data: [[f32; N]; N]) -> f32 {
277+
let mut sign = 1.0_f32;
278+
for pivot in 0..N {
279+
let mut pivot_row = pivot;
280+
let mut pivot_abs = data[pivot][pivot].abs();
281+
for (candidate, row) in data.iter().enumerate().skip(pivot + 1) {
282+
let candidate_abs = row[pivot].abs();
283+
if candidate_abs > pivot_abs {
284+
pivot_abs = candidate_abs;
285+
pivot_row = candidate;
286+
}
287+
}
288+
289+
if pivot_abs == 0.0 {
290+
return 0.0;
291+
}
292+
293+
if pivot_row != pivot {
294+
data.swap(pivot, pivot_row);
295+
sign = -sign;
296+
}
297+
298+
let pivot_value = data[pivot][pivot];
299+
let pivot_tail = data[pivot];
300+
for row in data.iter_mut().skip(pivot + 1) {
301+
let factor = row[pivot] / pivot_value;
302+
if factor == 0.0 {
303+
continue;
304+
}
305+
for (dst, src) in row[pivot..].iter_mut().zip(pivot_tail[pivot..].iter())
306+
{
307+
*dst -= factor * *src;
308+
}
309+
}
310+
}
311+
312+
let mut det = sign;
313+
for (i, row) in data.iter().enumerate().take(N) {
314+
det *= row[i];
315+
}
316+
return det;
317+
}
318+
262319
// -------------------------- ARRAY IMPLEMENTATION -----------------------------
263320

264321
/// Matrix implementations for arrays of f32 arrays. Including the trait Matrix into
@@ -325,7 +382,15 @@ where
325382
todo!()
326383
}
327384

328-
/// Computes the determinant of any square matrix using Laplace expansion.
385+
/// Computes the determinant of any square matrix using Gaussian elimination
386+
/// with partial pivoting.
387+
///
388+
/// # Behavior
389+
///
390+
/// - Uses stack-allocated elimination for 3x3 and 4x4 matrices to avoid heap
391+
/// allocation on common transform sizes.
392+
/// - Uses a generic dense fallback for larger matrices.
393+
/// - Overall asymptotic runtime is `O(n^3)` for square `n x n` matrices.
329394
fn determinant(&self) -> Result<f32, MathError> {
330395
let rows = self.as_ref().len();
331396
if rows == 0 {
@@ -341,34 +406,88 @@ where
341406
return Err(MathError::NonSquareMatrix { rows, cols });
342407
}
343408

344-
return match rows {
345-
1 => Ok(self.as_ref()[0].as_ref()[0]),
346-
2 => {
347-
let a = self.at(0, 0);
348-
let b = self.at(0, 1);
349-
let c = self.at(1, 0);
350-
let d = self.at(1, 1);
351-
return Ok(a * d - b * c);
409+
if rows == 1 {
410+
return Ok(self.as_ref()[0].as_ref()[0]);
411+
}
412+
413+
if rows == 2 {
414+
let a = self.at(0, 0);
415+
let b = self.at(0, 1);
416+
let c = self.at(1, 0);
417+
let d = self.at(1, 1);
418+
return Ok(a * d - b * c);
419+
}
420+
421+
if rows == 3 {
422+
let mut data = [[0.0_f32; 3]; 3];
423+
for (i, row) in data.iter_mut().enumerate() {
424+
for (j, value) in row.iter_mut().enumerate() {
425+
*value = self.at(i, j);
426+
}
427+
}
428+
return Ok(determinant_gaussian_stack::<3>(data));
429+
}
430+
431+
if rows == 4 {
432+
let mut data = [[0.0_f32; 4]; 4];
433+
for (i, row) in data.iter_mut().enumerate() {
434+
for (j, value) in row.iter_mut().enumerate() {
435+
*value = self.at(i, j);
436+
}
437+
}
438+
return Ok(determinant_gaussian_stack::<4>(data));
439+
}
440+
441+
// Use a contiguous row-major buffer for larger matrices to keep the
442+
// fallback cache-friendlier than `Vec<Vec<f32>>`.
443+
let mut working = vec![0.0_f32; rows * rows];
444+
for i in 0..rows {
445+
for j in 0..rows {
446+
working[i * rows + j] = self.at(i, j);
352447
}
353-
_ => {
354-
let mut result = 0.0;
355-
for i in 0..rows {
356-
let mut submatrix: Vec<Vec<f32>> = Vec::with_capacity(rows - 1);
357-
for j in 1..rows {
358-
let mut row = Vec::new();
359-
for k in 0..rows {
360-
if k != i {
361-
row.push(self.at(j, k));
362-
}
363-
}
364-
submatrix.push(row);
365-
}
366-
let sub_determinant = submatrix.determinant()?;
367-
result += self.at(0, i) * sub_determinant * (-1.0_f32).powi(i as i32);
448+
}
449+
450+
let mut sign = 1.0_f32;
451+
for pivot in 0..rows {
452+
// Partial pivoting improves numerical stability and avoids division by 0.
453+
let mut pivot_row = pivot;
454+
let mut pivot_abs = working[pivot * rows + pivot].abs();
455+
for candidate in (pivot + 1)..rows {
456+
let candidate_abs = working[candidate * rows + pivot].abs();
457+
if candidate_abs > pivot_abs {
458+
pivot_abs = candidate_abs;
459+
pivot_row = candidate;
368460
}
369-
return Ok(result);
370461
}
371-
};
462+
463+
if pivot_abs == 0.0 {
464+
return Ok(0.0);
465+
}
466+
467+
if pivot_row != pivot {
468+
for column in 0..rows {
469+
working.swap(pivot * rows + column, pivot_row * rows + column);
470+
}
471+
sign = -sign;
472+
}
473+
474+
let pivot_value = working[pivot * rows + pivot];
475+
for row in (pivot + 1)..rows {
476+
let factor = working[row * rows + pivot] / pivot_value;
477+
if factor == 0.0 {
478+
continue;
479+
}
480+
for column in pivot..rows {
481+
let row_idx = row * rows + column;
482+
let pivot_idx = pivot * rows + column;
483+
working[row_idx] -= factor * working[pivot_idx];
484+
}
485+
}
486+
}
487+
488+
let diagonal_product =
489+
(0..rows).map(|i| working[i * rows + i]).product::<f32>();
490+
return Ok(sign * diagonal_product);
372491
}
373492

374493
/// Return the size as a (rows, columns).
@@ -394,7 +513,6 @@ where
394513

395514
#[cfg(test)]
396515
mod tests {
397-
398516
use super::{
399517
filled_matrix,
400518
identity_matrix,
@@ -454,6 +572,55 @@ mod tests {
454572
assert_eq!(m2.determinant(), Ok(-306.0));
455573
}
456574

575+
#[test]
576+
fn determinant_4x4_stack_path_handles_row_swap_sign() {
577+
let m = [
578+
[0.0, 2.0, 0.0, 0.0],
579+
[1.0, 0.0, 0.0, 0.0],
580+
[0.0, 0.0, 3.0, 0.0],
581+
[0.0, 0.0, 0.0, 4.0],
582+
];
583+
assert_eq!(m.determinant(), Ok(-24.0));
584+
}
585+
586+
#[test]
587+
fn determinant_5x5_dynamic_path_handles_row_swap_sign() {
588+
let m = [
589+
[0.0, 2.0, 0.0, 0.0, 0.0],
590+
[1.0, 0.0, 0.0, 0.0, 0.0],
591+
[0.0, 0.0, 3.0, 0.0, 0.0],
592+
[0.0, 0.0, 0.0, 4.0, 0.0],
593+
[0.0, 0.0, 0.0, 0.0, 5.0],
594+
];
595+
assert_eq!(m.determinant(), Ok(-120.0));
596+
}
597+
598+
#[test]
599+
fn determinant_preserves_small_non_zero_pivot_in_stack_path() {
600+
let m = [[1.0e-8, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]];
601+
crate::assert_approximately_equal!(
602+
m.determinant().expect("determinant should succeed"),
603+
6.0e-8,
604+
1.0e-12
605+
);
606+
}
607+
608+
#[test]
609+
fn determinant_preserves_small_non_zero_pivot_in_dynamic_path() {
610+
let m = [
611+
[1.0e-8, 0.0, 0.0, 0.0, 0.0],
612+
[0.0, 2.0, 0.0, 0.0, 0.0],
613+
[0.0, 0.0, 3.0, 0.0, 0.0],
614+
[0.0, 0.0, 0.0, 4.0, 0.0],
615+
[0.0, 0.0, 0.0, 0.0, 5.0],
616+
];
617+
crate::assert_approximately_equal!(
618+
m.determinant().expect("determinant should succeed"),
619+
1.2e-6,
620+
1.0e-10
621+
);
622+
}
623+
457624
#[test]
458625
fn non_square_matrix_determinant() {
459626
let m = [[3.0, 8.0], [4.0, 6.0], [0.0, 1.0]];

0 commit comments

Comments
 (0)