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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,4 @@ jobs:
cargo test --lib --verbose
cargo test --doc --verbose
cargo test --tests --verbose
cargo test --features exact --verbose
28 changes: 23 additions & 5 deletions .github/workflows/codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
# only required for a private repository by
# github/codeql-action/upload-sarif to get the Action run status
actions: read
env:
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
name: Codacy Security Scan
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down Expand Up @@ -77,13 +79,13 @@ jobs:
# Execute Codacy Analysis CLI and generate a SARIF output with
# the security issues identified during the analysis
- name: Run Codacy Analysis CLI
if: ${{ env.CODACY_PROJECT_TOKEN != '' }}
id: codacy_analysis
uses: codacy/codacy-analysis-cli-action@562ee3e92b8e92df8b67e0a5ff8aa8e261919c08
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token
# to get your project token from your Codacy repository.
# You can also omit the token and run the tools that support
# default configurations
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
project-token: ${{ env.CODACY_PROJECT_TOKEN }}
verbose: true
directory: ${{ env.CODACY_WORKDIR }}
output: ${{ env.CODACY_SARIF }}
Expand All @@ -94,9 +96,24 @@ jobs:
# Force 0 exit code to allow SARIF file generation
# This will handover control about PR rejection to the GitHub side
max-allowed-issues: 2147483647
# Codacy can fail transiently on PRs (e.g. remote config/tools service outages).
# Keep PR checks non-blocking and continue to SARIF fallback/upload.
continue-on-error: ${{ github.event_name == 'pull_request' }}

# Process SARIF file to split by tool
- name: Split SARIF by tool
- name: Warn when Codacy token is unavailable on PR
if: ${{ github.event_name == 'pull_request' && env.CODACY_PROJECT_TOKEN == '' }}
run: |
echo "::warning::CODACY_PROJECT_TOKEN is unavailable for this pull_request."
echo "::warning::Skipping Codacy Analysis CLI and using SARIF fallback."

- name: Warn when Codacy analysis fails on PR
if: ${{ always() && github.event_name == 'pull_request' && steps.codacy_analysis.outcome == 'failure' }}
run: |
echo "::warning::Codacy Analysis CLI failed on this pull_request run; continuing with SARIF fallback."

# Validate SARIF output or create an empty fallback for upload
- name: Validate or create SARIF
if: always()
run: |
# Fail fast and surface errors clearly
set -euo pipefail
Expand Down Expand Up @@ -124,6 +141,7 @@ jobs:

# Select SARIF file for upload
- name: Select SARIF file for upload
if: always()
run: |
set -euo pipefail
# Honor preselected SARIF_FILE from earlier steps (e.g., empty SARIF case)
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ When making changes in this repo, prioritize (in order):
- Format: `cargo fmt` (or `just fmt`)
- Integration tests: `just test-integration`
- Lint (Clippy): `cargo clippy --all-targets --all-features -- -D warnings` (or `just clippy`)
- Lint (Clippy, exact feature): `cargo clippy --features exact --all-targets -- -D warnings` (or `just clippy-exact`)
- Lint/validate: `just check`
- Pre-commit validation: `just ci`
- Python tests: `just test-python`
- Run a single test (by name filter): `cargo test solve_2x2_basic` (or the full path: `cargo test lu::tests::solve_2x2_basic`)
- Run exact-feature tests: `cargo test --features exact --verbose` (or `just test-exact`)
- Run examples: `just examples` (or `cargo run --example det_5x5` / `cargo run --example solve_5x5` / `cargo run --example const_det_4x4`)
- Spell check: `just spell-check` (uses `typos.toml` at repo root; add false positives to `[default.extend-words]`)

Expand All @@ -40,6 +42,8 @@ When making changes in this repo, prioritize (in order):
- `src/matrix.rs`: `Matrix<const D: usize>` (`[[f64; D]; D]`) + helpers (`get`, `set`, `inf_norm`, `det`, `det_direct`)
- `src/lu.rs`: `Lu<const D: usize>` factorization with partial pivoting (`solve_vec`, `det`)
- `src/ldlt.rs`: `Ldlt<const D: usize>` factorization without pivoting for symmetric SPD/PSD matrices (`solve_vec`, `det`)
- `src/exact.rs`: `det_sign_exact()` — adaptive-precision determinant sign
(Shewchuk-style f64 filter + Bareiss in `BigRational`); `features = ["exact"]`
- A minimal `justfile` exists for common workflows (see `just --list`).
- The public API re-exports these items from `src/lib.rs`.
- Dev-only benchmarks live in `benches/vs_linalg.rs` (Criterion + nalgebra/faer comparison).
Expand Down
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "la-stack"
version = "0.1.3"
version = "0.2.0"
edition = "2024"
rust-version = "1.94"
license = "BSD-3-Clause"
Expand All @@ -13,10 +13,12 @@ categories = [ "mathematics", "science" ]
keywords = [ "linear-algebra", "geometry", "const-generics" ]

[dependencies]
# Intentionally empty (bench-only deps are optional features below)
# All runtime deps are optional; see [features] below.
criterion = { version = "0.8.2", features = [ "html_reports" ], optional = true }
faer = { version = "0.24.0", default-features = false, features = [ "std", "linalg" ], optional = true }
nalgebra = { version = "0.34.1", optional = true }
num-bigint = { version = "0.4", optional = true }
num-rational = { version = "0.4", features = [ "num-bigint-std" ], optional = true }

[dev-dependencies]
approx = "0.5.1"
Expand All @@ -26,6 +28,11 @@ proptest = "1.10.0"
[features]
default = [ ]
bench = [ "criterion", "faer", "nalgebra" ]
exact = [ "num-bigint", "num-rational" ]

[[example]]
name = "exact_sign_3x3"
required-features = [ "exact" ]

[[bench]]
name = "vs_linalg"
Expand All @@ -36,6 +43,9 @@ required-features = [ "bench" ]
lto = "fat"
codegen-units = 1

[package.metadata.docs.rs]
features = [ "exact" ]

[lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
Expand Down
50 changes: 43 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ while keeping the API intentionally small and explicit.
- ✅ Const-generic dimensions (no dynamic sizes)
- ✅ `const fn` where possible (compile-time evaluation of determinants, dot products, etc.)
- ✅ Explicit algorithms (LU, solve, determinant)
- ✅ No runtime dependencies (dev-dependencies are for contributors only)
- ✅ Robust geometric predicates via optional exact arithmetic (`det_sign_exact`)
- ✅ No runtime dependencies by default (optional features may add deps)
- ✅ Stack storage only (no heap allocation in core types)
- ✅ `unsafe` forbidden

Expand All @@ -44,16 +45,16 @@ while keeping the API intentionally small and explicit.
## 🔢 Scalar types

Today, the core types are implemented for `f64`. The intent is to support `f32` and `f64`
(and `f128` if/when Rust gains a stable primitive for it). Longer term, we may add optional
arbitrary-precision support (e.g. via `rug`) depending on performance.
(and `f128` if/when Rust gains a stable primitive for it). Arbitrary-precision arithmetic
is available via the optional `"exact"` feature (see below).

## 🚀 Quickstart

Add this to your `Cargo.toml`:

```toml
[dependencies]
la-stack = "0.1"
la-stack = "0.2"
```

Solve a 5×5 system via LU:
Expand Down Expand Up @@ -130,27 +131,62 @@ assert_eq!(DET, Some(30.0));
The public `det()` method automatically dispatches through the closed-form path
for D ≤ 4 and falls back to LU for D ≥ 5 — no API change needed.

## 🔬 Exact determinant sign (`"exact"` feature)

The default build has **zero runtime dependencies**. Enable the optional
`exact` Cargo feature to add `det_sign_exact()`, which returns the provably
correct sign (−1, 0, or +1) of the determinant using adaptive-precision
arithmetic (this pulls in `num-bigint` and `num-rational` for `BigRational`):

```toml
[dependencies]
la-stack = { version = "0.2", features = ["exact"] }
```

```rust,ignore
use la_stack::prelude::*;

let m = Matrix::<3>::from_rows([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]);
assert_eq!(m.det_sign_exact(), 0); // exactly singular
```

For D ≤ 4, a fast f64 filter (error-bounded `det_direct()`) resolves the sign
without allocating. Only near-degenerate or large (D ≥ 5) matrices fall through
to the exact Bareiss algorithm in `BigRational`.

## 🧩 API at a glance

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

Storage shown above reflects the current `f64` implementation.

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

## 📋 Examples

The `examples/` directory contains small, runnable programs:

- **`solve_5x5`** — solve a 5×5 system via LU with partial pivoting
- **`det_5x5`** — determinant of a 5×5 matrix via LU
- **`const_det_4x4`** — compile-time 4×4 determinant via `det_direct()`
- **`exact_sign_3x3`** — exact determinant sign of a near-singular 3×3 matrix (requires `exact` feature)

```bash
just examples
# or:
# or individually:
cargo run --example solve_5x5
cargo run --example det_5x5
cargo run --example const_det_4x4
cargo run --features exact --example exact_sign_3x3
```

## 🤝 Contributing
Expand All @@ -174,7 +210,7 @@ If you use this library in academic work, please cite it using [CITATION.cff](CI

## 📚 References

For canonical references to LU / `LDL^T` algorithms used by this crate, see [REFERENCES.md](REFERENCES.md).
For canonical references to the algorithms used by this crate, see [REFERENCES.md](REFERENCES.md).

## 📊 Benchmarks (vs nalgebra/faer)

Expand Down
17 changes: 17 additions & 0 deletions REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ pivoting.
For background on the SPD/PSD setting, see [4-5]. For pivoted variants used for symmetric *indefinite*
matrices, see [6].

### Exact determinant sign (adaptive-precision Bareiss)

`det_sign_exact()` uses a Shewchuk-style f64 error-bound filter [8] backed by exact Bareiss
elimination [7] in `BigRational`. See `src/exact.rs` for the full architecture description.

## References

### LU / Gaussian elimination
Expand Down Expand Up @@ -53,3 +58,15 @@ matrices, see [6].
6. Bunch, J. R., L. Kaufman, and B. N. Parlett. "Decomposition of a Symmetric Matrix."
*Numerische Mathematik* 27 (1976/1977): 95–110.
[Full text](https://eudml.org/doc/132435)

### Exact determinant sign (`det_sign_exact`)

7. Bareiss, Erwin H. "Sylvester's Identity and Multistep Integer-Preserving Gaussian
Elimination." *Mathematics of Computation* 22.103 (1968): 565–578.
[DOI](https://doi.org/10.1090/S0025-5718-1968-0226829-0) ·
[PDF](https://www.ams.org/journals/mcom/1968-22-103/S0025-5718-1968-0226829-0/S0025-5718-1968-0226829-0.pdf)
8. Shewchuk, Jonathan Richard. "Adaptive Precision Floating-Point Arithmetic and Fast
Robust Geometric Predicates." *Discrete & Computational Geometry* 18.3 (1997): 305–363.
[DOI](https://doi.org/10.1007/PL00009321) ·
[PDF](https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf)
Also: Technical Report CMU-CS-96-140, Carnegie Mellon University, May 1996.
45 changes: 45 additions & 0 deletions examples/exact_sign_3x3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Exact determinant sign for a near-singular 3×3 matrix.
//!
//! This example demonstrates `det_sign_exact()`, which uses adaptive-precision
//! arithmetic to return the provably correct sign of the determinant — even when
//! the matrix is so close to singular that f64 rounding could give the wrong answer.
//!
//! Run with: `cargo run --features exact --example exact_sign_3x3`

use la_stack::prelude::*;

fn main() {
// Base matrix: rows in arithmetic progression → exactly singular (det = 0).
// [[1, 2, 3],
// [4, 5, 6],
// [7, 8, 9]]
//
// Perturb entry (0,0) by 2^-50 ≈ 8.9e-16.
// Exact det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0.
let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
let m = Matrix::<3>::from_rows([
[1.0 + perturbation, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]);

let sign = m.det_sign_exact();
let det_f64 = m.det(DEFAULT_PIVOT_TOL).unwrap();

println!("Near-singular 3×3 matrix (perturbation = 2^-50 ≈ {perturbation:.2e}):");
for r in 0..3 {
print!(" [");
for c in 0..3 {
if c > 0 {
print!(", ");
}
print!("{:22.18}", m.get(r, c).unwrap());
}
println!("]");
}
println!();
println!("f64 det() = {det_f64:+.6e}");
println!("det_sign_exact() = {sign}");
println!();
println!("The exact sign is −1 (negative), matching the analytical result.");
}
16 changes: 13 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ check-fast:
ci: check bench-compile test-all examples
@echo "🎯 CI checks complete!"

# Clippy for the "exact" feature (catches feature-gated lint issues)
clippy-exact:
cargo clippy --features exact --all-targets -- -D warnings -W clippy::pedantic

# Clean build artifacts
clean:
cargo clean
Expand All @@ -145,6 +149,7 @@ clippy:
# Common tarpaulin arguments for all coverage runs
# Note: -t 300 sets per-test timeout to 5 minutes (needed for slow CI environments)
_coverage_base_args := '''--exclude-files 'benches/*' --exclude-files 'examples/*' \
--features exact \
--workspace --lib --tests \
-t 300 --verbose --implicit-test-threads'''

Expand Down Expand Up @@ -179,15 +184,16 @@ coverage-ci:
default:
@just --list

# Documentation build check
# Documentation build check (includes exact feature for full API coverage)
doc-check:
RUSTDOCFLAGS='-D warnings' cargo doc --no-deps
RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --features exact

# Examples
examples:
cargo run --quiet --example det_5x5
cargo run --quiet --example solve_5x5
cargo run --quiet --example const_det_4x4
cargo run --quiet --features exact --example exact_sign_3x3

# Fix (mutating): apply formatters/auto-fixes
fix: toml-fmt fmt python-fix shell-fmt markdown-fix yaml-fix
Expand Down Expand Up @@ -500,9 +506,13 @@ test:
cargo test --lib --verbose
cargo test --doc --verbose

test-all: test test-integration test-python
test-all: test test-integration test-exact test-python
@echo "✅ All tests passed"

# Tests for the "exact" feature (det_sign_exact + BigRational Bareiss)
test-exact:
cargo test --features exact --verbose

test-integration:
cargo test --tests --verbose

Expand Down
Loading
Loading