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