Skip to content

Commit b1a491d

Browse files
committed
Changed: expand exact-arithmetic re-exports and adversarial benchmarks
Update the `exact` feature to re-export `BigInt` and essential `num-traits` (FromPrimitive, ToPrimitive, Signed) alongside `BigRational`. This simplifies downstream usage by removing the need for direct dependencies on the `num` ecosystem for common operations. Additionally, formalize adversarial-input benchmarking for near-singular, large-entry, and Hilbert matrices to better track performance on ill-conditioned data. CI benchmark comparisons are now more resilient to newly added benchmarks via `--baseline-lenient`. Refs: #80
1 parent 5bf5815 commit b1a491d

6 files changed

Lines changed: 72 additions & 8 deletions

File tree

.github/workflows/benchmarks.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,11 @@ jobs:
114114
115115
if [ -d target/criterion/exact_d2/det/main ]; then
116116
echo "::notice::Baseline found — comparing against main"
117+
# --baseline-lenient rather than --baseline: benches added on the
118+
# PR branch that don't yet exist in the main baseline get a
119+
# "no baseline data" notice instead of aborting the whole run.
117120
cargo bench --features bench,exact --bench exact \
118-
-- --baseline main 2>&1 | tee bench-output.txt
121+
-- --baseline-lenient main 2>&1 | tee bench-output.txt
119122
else
120123
echo "::notice::No baseline found — running without comparison"
121124
cargo bench --features bench,exact --bench exact \

AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ When creating or updating issues:
221221
222222
- `exact` — enables exact arithmetic methods via `BigRational`:
223223
`det_exact()`, `det_exact_f64()`, `det_sign_exact()`, `solve_exact()`, and `solve_exact_f64()`.
224-
Also re-exports `BigRational` from the crate root and prelude.
224+
Re-exports `BigInt` and `BigRational` from the crate root and prelude, plus
225+
`FromPrimitive`, `ToPrimitive`, and `Signed` from `num-traits` in the prelude
226+
(so consumers get usable `from_f64` / `to_f64` / `is_positive` etc. without adding
227+
`num-bigint` / `num-rational` / `num-traits` as their own deps).
225228
Gates `src/exact.rs`, additional tests, and the `exact_det_3x3`/`exact_sign_3x3`/`exact_solve_3x3` examples.
226229
Clippy, doc builds, and test commands have dedicated `--features exact` variants.
227230
@@ -245,7 +248,8 @@ When creating or updating issues:
245248
- The public API re-exports these items from `src/lib.rs`.
246249
- The `justfile` defines all dev workflows (see `just --list`).
247250
- Dev-only benchmarks live in `benches/vs_linalg.rs` (Criterion + nalgebra/faer comparison)
248-
and `benches/exact.rs` (exact arithmetic across D=2–5).
251+
and `benches/exact.rs` (exact arithmetic across D=2–5, plus adversarial-input groups
252+
`exact_near_singular_3x3`, `exact_large_entries_3x3`, `exact_hilbert_4x4`, `exact_hilbert_5x5`).
249253
- Python scripts under `scripts/`:
250254
- `bench_compare.py`: exact-arithmetic benchmark comparison across releases (generates `docs/PERFORMANCE.md`)
251255
- `criterion_dim_plot.py`: benchmark plotting (CSV + SVG + README table update)

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,13 @@ assert!((x[0] - 1.0).abs() <= f64::EPSILON);
190190
assert!((x[1] - 2.0).abs() <= f64::EPSILON);
191191
```
192192

193-
`BigRational` is re-exported from the crate root and prelude when the `exact`
194-
feature is enabled, so consumers don't need to depend on `num-rational` directly.
193+
With the `exact` feature enabled, `BigInt` and `BigRational` are re-exported
194+
from the crate root and prelude, alongside the most commonly needed
195+
`num-traits` items (`FromPrimitive`, `ToPrimitive`, `Signed`). This lets
196+
consumers construct exact values (`BigRational::from_f64`, `from_i64`), query
197+
sign (`is_positive` / `is_negative`), and convert back to `f64` (`to_f64`)
198+
with a single `use la_stack::prelude::*;` — no need to add `num-bigint`,
199+
`num-rational`, or `num-traits` to their own `Cargo.toml`.
195200

196201
For `det_sign_exact()`, D ≤ 4 matrices use a fast f64 filter (error-bounded
197202
`det_direct()`) that resolves the sign without allocating. Only near-degenerate

docs/BENCHMARKING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ la-stack has two Criterion benchmark suites:
1414
(`det_exact`, `solve_exact`, `det_sign_exact`, etc.) alongside f64
1515
baselines (`det`, `det_direct`) across D=2–5. Use this to understand
1616
the cost of exact arithmetic and track optimization progress.
17+
In addition to the per-dimension groups (`exact_d{2..5}`), the suite
18+
includes four adversarial-input groups designed to stress specific
19+
corners of the pipeline:
20+
- `exact_near_singular_3x3` — a 2^-50 perturbation of a singular base
21+
matrix; forces the Bareiss fallback in `det_sign_exact` and
22+
exercises the largest intermediate `BigInt` values in `solve_exact`.
23+
- `exact_large_entries_3x3` — diagonal entries near `f64::MAX / 2`
24+
stress `BigInt` growth during Bareiss forward elimination.
25+
- `exact_hilbert_4x4` / `exact_hilbert_5x5` — classically
26+
ill-conditioned matrices whose non-terminating-in-binary entries
27+
stress the `f64_decompose → BigInt` scaling path.
28+
Each adversarial group runs the same four benches (`det_sign_exact`,
29+
`det_exact`, `solve_exact`, `solve_exact_f64`) so the resulting tables
30+
are directly comparable across input classes.
1731

1832
## Quick reference
1933

src/exact.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,9 +2091,12 @@ mod tests {
20912091
}
20922092

20932093
/// Hilbert matrices are symmetric positive-definite, so
2094-
/// `det_sign_exact` must return `1` for every D. For D ≥ 5 the fast
2095-
/// filter is skipped entirely, so this exercises the Bareiss path on
2096-
/// inputs whose `(mantissa, exponent)` pairs all differ.
2094+
/// `det_sign_exact` must return `1` for every D. For D=2..=4 the
2095+
/// fast f64 filter resolves the positive sign without falling
2096+
/// through (Hilbert's determinant is tiny but still well above the
2097+
/// `det_errbound` cushion); for D=5 the filter is skipped entirely
2098+
/// and the Bareiss path handles inputs whose `(mantissa, exponent)`
2099+
/// pairs all differ.
20972100
macro_rules! gen_det_sign_exact_hilbert_positive_tests {
20982101
($d:literal) => {
20992102
paste! {
@@ -2113,6 +2116,7 @@ mod tests {
21132116
};
21142117
}
21152118

2119+
gen_det_sign_exact_hilbert_positive_tests!(2);
21162120
gen_det_sign_exact_hilbert_positive_tests!(3);
21172121
gen_det_sign_exact_hilbert_positive_tests!(4);
21182122
gen_det_sign_exact_hilbert_positive_tests!(5);
@@ -2154,6 +2158,7 @@ mod tests {
21542158
};
21552159
}
21562160

2161+
gen_solve_exact_hilbert_residual_tests!(2);
21572162
gen_solve_exact_hilbert_residual_tests!(3);
21582163
gen_solve_exact_hilbert_residual_tests!(4);
21592164
gen_solve_exact_hilbert_residual_tests!(5);

src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,37 @@ mod tests {
387387
let _ = m.lu(DEFAULT_PIVOT_TOL).unwrap().solve_vec(v).unwrap();
388388
let _ = m.ldlt(DEFAULT_SINGULAR_TOL).unwrap().solve_vec(v).unwrap();
389389
}
390+
391+
/// Exercise every exact-feature re-export via the prelude so a future
392+
/// refactor that drops one (e.g. removing `Signed` from the prelude
393+
/// list) fails to compile rather than silently breaking downstream.
394+
#[cfg(feature = "exact")]
395+
#[test]
396+
fn prelude_exact_reexports_compile_and_work() {
397+
use crate::prelude::*;
398+
399+
// `BigInt` and `BigRational` constructors.
400+
let n = BigInt::from(7);
401+
let r = BigRational::from_integer(n.clone());
402+
assert_eq!(*r.numer(), n);
403+
404+
// `FromPrimitive::from_f64` / `from_i64` on `BigRational`.
405+
let half = BigRational::from_f64(0.5).unwrap();
406+
let two = BigRational::from_i64(2).unwrap();
407+
assert_eq!(
408+
half.clone() + half.clone(),
409+
BigRational::from_integer(BigInt::from(1))
410+
);
411+
412+
// `Signed::is_positive` / `is_negative` / `abs`.
413+
assert!(half.is_positive());
414+
assert!(!half.is_negative());
415+
let neg = -half.clone();
416+
assert!(neg.is_negative());
417+
assert_eq!(neg.abs(), half);
418+
419+
// `ToPrimitive::to_f64` / `to_i64`.
420+
assert!((half.to_f64().unwrap() - 0.5).abs() <= f64::EPSILON);
421+
assert_eq!(two.to_i64().unwrap(), 2);
422+
}
390423
}

0 commit comments

Comments
 (0)