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>
This commit is contained in:
2026-07-17 22:46:46 +02:00
parent 62efea0646
commit 3358b74342
275 changed files with 70187 additions and 5230 deletions

View File

@@ -0,0 +1,109 @@
//! 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());
}