Skip to content

Commit 4ac5af9

Browse files
committed
fix(bench): make v0.4.3 comparisons correctness-aware
- Adapt the shared benchmark harness across v0.4.3 API differences without changing measured operations - Exclude invalid balanced-range baselines while requiring current samples and reporting unavailable comparisons - Preserve benchmark provenance, suite-specific fallback commands, and publication rollback guarantees - Harden Windows Git input, changelog links, and version-reference parsing across platforms
1 parent 668daed commit 4ac5af9

22 files changed

Lines changed: 736 additions & 109 deletions

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ include = [
2525
"/docs/assets/**/*.csv",
2626
"/docs/assets/**/*.jpg",
2727
"/docs/assets/**/*.png",
28+
"/docs/assets/**/*.provenance.json",
2829
"/docs/assets/**/*.svg",
2930
"/examples/**/*.rs",
3031
"/src/**/*.rs",
@@ -88,6 +89,7 @@ unsafe_code = "forbid"
8889
missing_docs = { level = "deny", priority = 0 }
8990
dead_code = { level = "deny", priority = 0 }
9091
unreachable_pub = { level = "deny", priority = 0 }
92+
unexpected_cfgs = { level = "deny", priority = 0, check-cfg = [ 'cfg(la_stack_v0_4_3_api)' ] }
9193

9294
[lints.rustdoc]
9395
bare_urls = "deny"

benches/common/exact.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use core::cmp::Ordering;
77
use std::fmt::{self, Display};
88
use std::num::NonZeroU64;
99

10-
use la_stack::{DeterminantSign, LaError, Matrix, UnrepresentableReason, Vector};
10+
use la_stack::{LaError, Matrix, UnrepresentableReason, Vector};
1111
use num_bigint::BigInt;
1212
use num_rational::BigRational;
1313
use num_traits::{FromPrimitive, Signed, ToPrimitive, Zero};
@@ -19,8 +19,10 @@ pub const RANDOM_SEED: [u8; 32] = [0; 32];
1919

2020
/// Configuration errors for exact-arithmetic benchmark input generation.
2121
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22+
#[non_exhaustive]
2223
pub enum ExactBenchConfigError {
2324
/// The inclusive lower bound was greater than the inclusive upper bound.
25+
#[non_exhaustive]
2426
UnorderedRange {
2527
/// Inclusive lower bound.
2628
min: i16,
@@ -153,6 +155,29 @@ fn require_ok<T, E: Display>(result: Result<T, E>, operation: &str) -> T {
153155
}
154156
}
155157

158+
/// Return one matrix entry through the bounds-checked API shared by current and
159+
/// v0.4.3 releases.
160+
fn stored_matrix_entry<const D: usize>(matrix: &Matrix<D>, row: usize, col: usize) -> f64 {
161+
matrix
162+
.get(row, col)
163+
.unwrap_or_else(|| panic!("matrix entry ({row}, {col}) is outside dimension {D}"))
164+
}
165+
166+
/// Normalize the exact determinant-sign API across the v0.4.3 compatibility
167+
/// boundary used only by historical benchmark worktrees.
168+
#[cfg(not(la_stack_v0_4_3_api))]
169+
fn checked_det_sign<const D: usize>(matrix: &Matrix<D>) -> i8 {
170+
matrix.det_sign_exact().as_i8()
171+
}
172+
173+
#[cfg(la_stack_v0_4_3_api)]
174+
fn checked_det_sign<const D: usize>(matrix: &Matrix<D>) -> i8 {
175+
require_ok(
176+
matrix.det_sign_exact(),
177+
"exact determinant sign oracle check",
178+
)
179+
}
180+
156181
/// Return a deterministic, strictly diagonally-dominant matrix entry.
157182
#[inline]
158183
#[expect(
@@ -349,14 +374,13 @@ fn next_permutation(values: &mut [usize]) -> bool {
349374

350375
/// Compute a determinant with the independent factorial-time Leibniz formula.
351376
fn determinant_leibniz<const D: usize>(matrix: &Matrix<D>) -> BigRational {
352-
let rows = matrix.as_rows();
353377
let mut determinant = BigRational::zero();
354378
let mut permutation: [usize; D] = from_fn(|index| index);
355379

356380
loop {
357381
let mut term = BigRational::from_integer(BigInt::from(1));
358382
for (row, &col) in permutation.iter().enumerate() {
359-
term *= rational_from_f64(rows[row][col]);
383+
term *= rational_from_f64(stored_matrix_entry(matrix, row, col));
360384
}
361385
if permutation_is_even(&permutation) {
362386
determinant += term;
@@ -372,11 +396,11 @@ fn determinant_leibniz<const D: usize>(matrix: &Matrix<D>) -> BigRational {
372396
}
373397

374398
/// Return the exact determinant sign implied by an independent rational value.
375-
fn determinant_sign(exact: &BigRational) -> DeterminantSign {
399+
fn determinant_sign(exact: &BigRational) -> i8 {
376400
match exact.cmp(&BigRational::zero()) {
377-
Ordering::Less => DeterminantSign::Negative,
378-
Ordering::Equal => DeterminantSign::Zero,
379-
Ordering::Greater => DeterminantSign::Positive,
401+
Ordering::Less => -1,
402+
Ordering::Equal => 0,
403+
Ordering::Greater => 1,
380404
}
381405
}
382406

@@ -474,7 +498,7 @@ fn assert_exact_residual<const D: usize>(input: &ExactInput<D>, solution: &[BigR
474498
for row in 0..D {
475499
let mut observed = BigRational::zero();
476500
for (col, value) in solution.iter().enumerate() {
477-
observed += rational_from_f64(input.matrix.as_rows()[row][col]) * value;
501+
observed += rational_from_f64(stored_matrix_entry(&input.matrix, row, col)) * value;
478502
}
479503
assert_eq!(observed, rational_from_f64(input.rhs.as_array()[row]));
480504
}
@@ -499,7 +523,7 @@ pub fn validate_exact_fixture<const D: usize>(input: ExactInput<D>) -> Validated
499523
determinant
500524
);
501525
assert_eq!(
502-
input.matrix.det_sign_exact(),
526+
checked_det_sign(&input.matrix),
503527
determinant_sign(&determinant)
504528
);
505529
assert_strict_scalar(input.matrix.det_exact_f64(), &determinant, None);

benches/common/vs_linalg.rs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,58 @@
44
55
use faer::linalg::solvers::{Ldlt as FaerLdlt, PartialPivLu};
66
use faer::perm::PermRef;
7+
use la_stack::{LaError, Tolerance, Vector};
78
use nalgebra::SMatrix;
89

10+
/// Evaluate la-stack's dot product through the ownership contract used by the
11+
/// selected library revision.
12+
///
13+
/// # Errors
14+
///
15+
/// Returns the selected revision's typed error if finite inputs overflow during
16+
/// dot-product accumulation.
17+
#[cfg(not(la_stack_v0_4_3_api))]
18+
#[inline]
19+
pub fn la_stack_dot<const D: usize>(left: &Vector<D>, right: &Vector<D>) -> Result<f64, LaError> {
20+
left.dot(right)
21+
}
22+
23+
/// Evaluate the v0.4.3 by-value dot-product API without changing benchmark
24+
/// inputs or the mathematical operation.
25+
///
26+
/// # Errors
27+
///
28+
/// Returns v0.4.3's typed error if finite inputs overflow during dot-product
29+
/// accumulation.
30+
#[cfg(la_stack_v0_4_3_api)]
31+
#[inline]
32+
pub fn la_stack_dot<const D: usize>(left: &Vector<D>, right: &Vector<D>) -> Result<f64, LaError> {
33+
(*left).dot(*right)
34+
}
35+
36+
/// Parse a tolerance through the constructor exposed by the selected library
37+
/// revision.
38+
///
39+
/// # Errors
40+
///
41+
/// Returns a typed error when `value` is negative or non-finite.
42+
#[cfg(not(la_stack_v0_4_3_api))]
43+
#[inline]
44+
pub const fn la_stack_tolerance(value: f64) -> Result<Tolerance, LaError> {
45+
Tolerance::try_new(value)
46+
}
47+
48+
/// Parse a tolerance through v0.4.3's pre-`try_` constructor name.
49+
///
50+
/// # Errors
51+
///
52+
/// Returns a typed error when `value` is negative or non-finite.
53+
#[cfg(la_stack_v0_4_3_api)]
54+
#[inline]
55+
pub const fn la_stack_tolerance(value: f64) -> Result<Tolerance, LaError> {
56+
Tolerance::new(value)
57+
}
58+
959
/// Return `det(P)` for faer's permutation representation.
1060
///
1161
/// Sign(det(P)) is +1 for even permutations and -1 for odd. Parity is computed
@@ -127,11 +177,13 @@ pub fn make_pivoting_matrix_rows<const D: usize>() -> [[f64; D]; D] {
127177
/// Build a positive-definite diagonal matrix spanning 112 binary exponents at D=8.
128178
///
129179
/// Each successive pivot is `2^-16` times the previous one. Benchmarks use a
130-
/// zero tolerance so the complete, finite factorization remains in scope.
180+
/// zero tolerance so the complete, finite factorization remains in scope. The
181+
/// fixed return dimension prevents extending the progression until a diagonal
182+
/// entry underflows to zero and destroys positive-definiteness.
131183
#[inline]
132184
#[must_use]
133-
pub fn make_ill_conditioned_matrix_rows<const D: usize>() -> [[f64; D]; D] {
134-
let mut rows = [[0.0; D]; D];
185+
pub fn make_ill_conditioned_matrix_rows() -> [[f64; 8]; 8] {
186+
let mut rows = [[0.0; 8]; 8];
135187
let mut diagonal = 1.0;
136188
for (index, row) in rows.iter_mut().enumerate() {
137189
row[index] = diagonal;

benches/vs_linalg.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ use faer::mat::AsMatRef;
1919
use faer::{Mat, Side};
2020
use nalgebra::{Const, DimMin, SMatrix, SVector};
2121

22-
use la_stack::{DEFAULT_SINGULAR_TOL, Matrix, Tolerance, Vector};
22+
use la_stack::{DEFAULT_SINGULAR_TOL, Matrix, Vector};
2323

2424
#[path = "common/vs_linalg.rs"]
2525
pub mod vs_linalg_common;
2626

2727
use vs_linalg_common::{
28-
faer_det_from_ldlt, faer_det_from_partial_piv_lu, make_balanced_dynamic_range_rows,
29-
make_ill_conditioned_matrix_rows, make_matrix_rows, make_pivoting_matrix_rows,
30-
make_vector_array, matrix_entry, nalgebra_inf_norm, vector_entry,
28+
faer_det_from_ldlt, faer_det_from_partial_piv_lu, la_stack_dot, la_stack_tolerance,
29+
make_balanced_dynamic_range_rows, make_ill_conditioned_matrix_rows, make_matrix_rows,
30+
make_pivoting_matrix_rows, make_vector_array, matrix_entry, nalgebra_inf_norm, vector_entry,
3131
};
3232

3333
/// Return a successful benchmark operation result or panic with the named operation.
@@ -489,7 +489,7 @@ fn register_vector_benchmarks<const D: usize>(group: &mut BenchmarkGroup<'_, Wal
489489

490490
group.bench_function("la_stack_dot", |bencher| {
491491
bencher.iter(|| {
492-
let result = require_ok(black_box(&v1).dot(black_box(&v2)), "la_stack dot");
492+
let result = require_ok(la_stack_dot(black_box(&v1), black_box(&v2)), "la_stack dot");
493493
black_box(result);
494494
});
495495
});
@@ -577,22 +577,19 @@ fn register_matrix_norm_benchmarks<const D: usize>(group: &mut BenchmarkGroup<'_
577577
}
578578

579579
/// Register D=8 stress cases that exercise pivoting, conditioning, and scaled products.
580-
fn register_stress_benchmarks<const D: usize>(group: &mut BenchmarkGroup<'_, WallTime>) {
581-
if D != 8 {
582-
return;
583-
}
584-
585-
let zero_tolerance = require_ok(Tolerance::try_new(0.0), "zero benchmark tolerance");
580+
fn register_stress_benchmarks(group: &mut BenchmarkGroup<'_, WallTime>) {
581+
let zero_tolerance = require_ok(la_stack_tolerance(0.0), "zero benchmark tolerance");
586582
let pivoting = require_ok(
587-
Matrix::<D>::try_from_rows(make_pivoting_matrix_rows()),
583+
Matrix::<8>::try_from_rows(make_pivoting_matrix_rows()),
588584
"pivoting benchmark matrix construction",
589585
);
590586
let ill_conditioned = require_ok(
591-
Matrix::<D>::try_from_rows(make_ill_conditioned_matrix_rows()),
587+
Matrix::<8>::try_from_rows(make_ill_conditioned_matrix_rows()),
592588
"ill-conditioned benchmark matrix construction",
593589
);
590+
#[cfg(not(la_stack_v0_4_3_api))]
594591
let balanced = require_ok(
595-
Matrix::<D>::try_from_rows(make_balanced_dynamic_range_rows()),
592+
Matrix::<8>::try_from_rows(make_balanced_dynamic_range_rows()),
596593
"balanced-range benchmark matrix construction",
597594
);
598595

@@ -638,15 +635,18 @@ fn register_stress_benchmarks<const D: usize>(group: &mut BenchmarkGroup<'_, Wal
638635
);
639636
});
640637

638+
#[cfg(not(la_stack_v0_4_3_api))]
641639
let balanced_lu = require_ok(
642640
balanced.lu(zero_tolerance),
643641
"balanced-range LU factorization",
644642
);
643+
#[cfg(not(la_stack_v0_4_3_api))]
645644
let balanced_ldlt = require_ok(
646645
balanced.ldlt(zero_tolerance),
647646
"balanced-range LDLT factorization",
648647
);
649648

649+
#[cfg(not(la_stack_v0_4_3_api))]
650650
group.bench_function("la_stack_det_from_lu_balanced_range", |bencher| {
651651
bencher.iter(|| {
652652
let det = require_ok(
@@ -657,6 +657,7 @@ fn register_stress_benchmarks<const D: usize>(group: &mut BenchmarkGroup<'_, Wal
657657
});
658658
});
659659

660+
#[cfg(not(la_stack_v0_4_3_api))]
660661
group.bench_function("la_stack_det_from_ldlt_balanced_range", |bencher| {
661662
bencher.iter(|| {
662663
let det = require_ok(
@@ -669,7 +670,7 @@ fn register_stress_benchmarks<const D: usize>(group: &mut BenchmarkGroup<'_, Wal
669670
}
670671

671672
macro_rules! define_vs_linalg_benches_for_dim {
672-
($fn_name:ident, $d:literal) => {
673+
($fn_name:ident, $d:literal $(, $register_stress:ident)?) => {
673674
fn $fn_name(c: &mut Criterion) {
674675
let mut group = c.benchmark_group(concat!("d", stringify!($d)));
675676
register_determinant_benchmarks::<$d>(&mut group);
@@ -682,7 +683,9 @@ macro_rules! define_vs_linalg_benches_for_dim {
682683
register_precomputed_ldlt_determinant_benchmarks::<$d>(&mut group);
683684
register_vector_benchmarks::<$d>(&mut group);
684685
register_matrix_norm_benchmarks::<$d>(&mut group);
685-
register_stress_benchmarks::<$d>(&mut group);
686+
$(
687+
$register_stress(&mut group);
688+
)?
686689
group.finish();
687690
}
688691
};
@@ -692,7 +695,7 @@ define_vs_linalg_benches_for_dim!(bench_d2, 2);
692695
define_vs_linalg_benches_for_dim!(bench_d3, 3);
693696
define_vs_linalg_benches_for_dim!(bench_d4, 4);
694697
define_vs_linalg_benches_for_dim!(bench_d5, 5);
695-
define_vs_linalg_benches_for_dim!(bench_d8, 8);
698+
define_vs_linalg_benches_for_dim!(bench_d8, 8, register_stress_benchmarks);
696699
define_vs_linalg_benches_for_dim!(bench_d16, 16);
697700
define_vs_linalg_benches_for_dim!(bench_d32, 32);
698701
define_vs_linalg_benches_for_dim!(bench_d64, 64);

docs/BENCHMARKING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ Criterion selection/commands, and both correctness-gate results. The report
8181
reader rejects malformed or mismatched provenance and incomplete selected-suite
8282
coverage.
8383

84+
The shared harness carries an explicit v0.4.3-only API adapter for renamed or
85+
ownership-adjusted calls (`det_sign_exact`, `Tolerance`, and vector dot
86+
products). The adapter changes only how the same operation is invoked; it does
87+
not patch either library implementation. Comparison builds cap lint diagnostics
88+
at warning for both revisions because the current manifest's lint policy may
89+
reject historical source that predates a lint, even though that source remains
90+
valid benchmark input.
91+
92+
The v0.4.3 LU/LDLT balanced-range determinant paths return an incorrect zero,
93+
so their two D=8 stress rows are deliberately not timed as baselines. Reports
94+
leave those baselines explicitly unavailable rather than presenting invalid
95+
performance evidence. The other v0.4.3 D=8 pivoting and ill-conditioned rows
96+
remain in the comparison.
97+
8498
This command does not depend on existing local `target/criterion/` baselines.
8599
It is slower than reusing a saved baseline, but less sensitive to stale local
86100
benchmark state.

scripts/archive_changelog.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,14 +320,8 @@ def _archive_dir_link_prefix(archive_dir: Path, changelog_parent: Path) -> str:
320320
try:
321321
archive_dir_rel = Path(os.path.relpath(archive_dir, changelog_parent)).as_posix()
322322
except ValueError as err:
323-
archive_dir_rel = archive_dir.as_posix()
324-
LOGGER.warning(
325-
"Could not compute relative archive directory: %s; archive_dir=%s changelog_parent=%s; generated Markdown links use %s",
326-
err,
327-
archive_dir,
328-
changelog_parent,
329-
archive_dir_rel,
330-
)
323+
msg = "cannot compute relative archive links because the archive and changelog directories are on different filesystem roots"
324+
raise ValueError(msg) from err
331325
if archive_dir_rel == ".." or archive_dir_rel.startswith("../") or Path(archive_dir_rel).is_absolute():
332326
LOGGER.warning(
333327
"Archive directory %s is outside changelog directory %s; generated Markdown links use %s",

0 commit comments

Comments
 (0)