Some checks failed
CI / check (push) Has been cancelled
Checkpoint incomplete calibration work (cond SDT green, evap SST failing) plus related solver/UI changes so the next pass can fix and extend safely. Co-authored-by: Cursor <cursoragent@cursor.com>
84 lines
2.9 KiB
Rust
84 lines
2.9 KiB
Rust
//! Story 1.5 AC#2/#3: the faer dense backend solves the reference cycles with
|
|
//! results matching the nalgebra baseline within tolerance (not bitwise —
|
|
//! different factorization engines), via the strangler dispatch.
|
|
|
|
mod common;
|
|
|
|
use entropyk_solver::linear::{make_linear_backend, set_linear_backend_override, DenseLu};
|
|
use entropyk_solver::{LinearSolver, Solver};
|
|
|
|
/// Per-value tolerance matching the golden-snapshot comparison policy.
|
|
const STATE_RTOL: f64 = 1e-6;
|
|
const STATE_ATOL: f64 = 1.0; // Pa / J/kg absolute floor
|
|
|
|
fn solve_all_cycles() -> Vec<Vec<f64>> {
|
|
[
|
|
common::build_reference_cycle_a(),
|
|
common::build_reference_cycle_b(),
|
|
common::build_reference_cycle_c(),
|
|
]
|
|
.into_iter()
|
|
.map(|mut system| {
|
|
common::solve_reference_system_with_state(&mut system)
|
|
.state
|
|
.clone()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn faer_path_dispatch_and_reference_cycle_parity() {
|
|
// 1. Unknown override must fail the solve with the dispatch error (the
|
|
// dispatch is on the strategy path, not dead code).
|
|
set_linear_backend_override(Some("definitely-not-a-backend"));
|
|
let mut system = common::build_reference_cycle_a();
|
|
let result = entropyk_solver::FallbackSolver::default_solver().solve(&mut system);
|
|
assert!(result.is_err(), "unknown backend must fail the solve");
|
|
|
|
// 2. Baseline (nalgebra) on the three reference cycles.
|
|
set_linear_backend_override(Some("nalgebra"));
|
|
let baseline = solve_all_cycles();
|
|
|
|
// 3. Same cycles on faer; converged states must match within tolerance.
|
|
set_linear_backend_override(Some("faer"));
|
|
let faer = solve_all_cycles();
|
|
|
|
set_linear_backend_override(None);
|
|
|
|
assert_eq!(baseline.len(), faer.len());
|
|
for (cycle_idx, (base, alt)) in baseline.iter().zip(faer.iter()).enumerate() {
|
|
assert_eq!(
|
|
base.len(),
|
|
alt.len(),
|
|
"cycle {cycle_idx}: state length differs"
|
|
);
|
|
for (i, (b, a)) in base.iter().zip(alt.iter()).enumerate() {
|
|
let scale = b.abs().max(a.abs()).max(STATE_ATOL);
|
|
assert!(
|
|
(b - a).abs() <= STATE_RTOL * scale,
|
|
"cycle {cycle_idx} state[{i}]: nalgebra={b} faer={a} (rel diff {})",
|
|
(b - a).abs() / scale
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dense_lu_enum_dispatches_both_engines() {
|
|
for name in ["nalgebra", "faer"] {
|
|
let mut backend = make_linear_backend(Some(name)).unwrap();
|
|
backend.set_problem(2, 2).unwrap();
|
|
backend
|
|
.set_linearisation(&[(0, 0, 2.0), (1, 1, 1.0)])
|
|
.unwrap();
|
|
let mut rhs = vec![4.0, 3.0];
|
|
backend.solve_in_place(&mut rhs).unwrap();
|
|
assert!((rhs[0] - 2.0).abs() < 1e-9, "{name}: {}", rhs[0]);
|
|
assert!((rhs[1] - 3.0).abs() < 1e-9, "{name}: {}", rhs[1]);
|
|
}
|
|
assert!(matches!(
|
|
make_linear_backend(Some("faer")).unwrap(),
|
|
DenseLu::Faer(_)
|
|
));
|
|
}
|