Files
Entropyk/crates/solver/tests/dof_balance.rs
sepehr 3358b74342 Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.
Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 22:46:46 +02:00

110 lines
3.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! System-wide DoF balance tests.
//!
//! Verifies that the ledger counts equations and unknowns consistently and that
//! `finalize` hard-fails on square-system violations.
use entropyk_components::{Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice};
use entropyk_solver::dof::SystemDofBalance;
use entropyk_solver::system::System;
use entropyk_solver::TopologyError;
/// Minimal mock: N residuals that are identically zero (topology bookkeeping only).
struct MockEq {
n: usize,
}
impl Component for MockEq {
fn compute_residuals(
&self,
_state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut().take(self.n) {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &StateSlice,
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n
}
fn get_ports(&self) -> &[ConnectedPort] {
&[]
}
fn flow_paths(&self) -> Vec<(usize, usize)> {
// Single-stream pass-through so the edge pair shares one ṁ branch.
vec![(0, 1)]
}
}
/// Two-node cycle with matching residual count → square after CM1.4.
///
/// Unknowns: 1 ṁ + 2 edges × (P,h) = 5
/// Equations: 3 + 2 = 5
#[test]
fn two_node_cycle_is_balanced() {
let mut sys = System::new();
let a = sys.add_component(Box::new(MockEq { n: 3 }));
let b = sys.add_component(Box::new(MockEq { n: 2 }));
sys.add_edge(a, b).unwrap();
sys.add_edge(b, a).unwrap();
sys.finalize().expect("balanced system must finalize");
let report = sys.dof_report();
assert_eq!(report.n_unknowns, 5);
assert_eq!(report.n_equations, 5);
assert_eq!(report.balance, SystemDofBalance::Balanced);
assert!(sys.validate_system_dof().is_ok());
}
/// One extra residual without a free unknown → over-constrained, finalize fails.
#[test]
fn overconstrained_finalize_fails() {
let mut sys = System::new();
let a = sys.add_component(Box::new(MockEq { n: 4 })); // +1 excess
let b = sys.add_component(Box::new(MockEq { n: 2 }));
sys.add_edge(a, b).unwrap();
sys.add_edge(b, a).unwrap();
// unknowns = 5, equations = 6
let err = sys.finalize().expect_err("must reject over-constrained system");
match err {
TopologyError::DofImbalance { message } => {
assert!(
message.contains("over-constrained") || message.contains("equations"),
"unexpected message: {message}"
);
}
other => panic!("expected DofImbalance, got {other:?}"),
}
}
/// Missing residual → under-constrained: finalize warns but allows topology tests;
/// hard `validate_system_dof` still rejects (production path).
#[test]
fn underconstrained_detected_by_validate_system_dof() {
let mut sys = System::new();
let a = sys.add_component(Box::new(MockEq { n: 2 }));
let b = sys.add_component(Box::new(MockEq { n: 2 }));
sys.add_edge(a, b).unwrap();
sys.add_edge(b, a).unwrap();
// unknowns = 5, equations = 4
sys.finalize()
.expect("under-constrained allowed at finalize for topology mocks");
let report = sys.dof_report();
assert!(matches!(
report.balance,
SystemDofBalance::UnderConstrained { free_dofs: 1 }
));
assert!(sys.validate_system_dof().is_err());
}