Files
Entropyk/crates/solver/tests/faer_backend_parity.rs
sepehr 3808e0f11b
Some checks failed
CI / check (push) Has been cancelled
Snapshot WIP: Probe calibration path, faer LU backend, and BPHX phase-change duty.
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>
2026-07-19 21:44:01 +02:00

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(_)
));
}