Files
Entropyk/crates/entropyk/tests/constraints_api.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

142 lines
4.5 KiB
Rust

//! Integration tests for SystemBuilder constraints API (Story 13-3).
//!
//! Verifies that constraints and bounded variables can be added via the builder,
//! linked for inverse control, and that the built system passes DoF validation.
use entropyk::{
BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintId, SystemBuilder,
ThermoError, TopologyError,
};
use entropyk_components::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector,
};
struct MockComponent {
n_eqs: usize,
}
impl Component for MockComponent {
fn compute_residuals(
&self,
_state: &[f64],
_residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
Ok(())
}
fn jacobian_entries(
&self,
_state: &[f64],
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n_eqs
}
fn get_ports(&self) -> &[ConnectedPort] {
&[]
}
}
#[test]
fn test_builder_constraints_link_and_validate_dof() {
let constraint = Constraint::new(
ConstraintId::new("superheat"),
ComponentOutput::Superheat {
component_id: "evap".to_string(),
},
5.0,
);
let valve =
BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).expect("valid bounds");
// Minimal topology: 2 nodes, 1 edge → 3 edge unknowns (ṁ,P,h). With 1 constraint and 1 control
// we need 4 equations total: 3 component eqs + 1 constraint = 4 = 3 + 1 unknowns.
let system = SystemBuilder::new()
.component("evap", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.component("other", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.edge("evap", "other")
.unwrap()
.with_constraint(constraint)
.unwrap()
.with_bounded_variable(valve)
.unwrap()
.link_constraint_to_control(
&ConstraintId::new("superheat"),
&BoundedVariableId::new("valve"),
)
.unwrap()
.build()
.expect("build should succeed");
// DoF validation should pass: 1 constraint, 1 control variable (balanced).
let dof_result = system.validate_inverse_control_dof();
assert!(
dof_result.is_ok(),
"validate_inverse_control_dof should pass when constraint and control are linked: {:?}",
dof_result.err()
);
}
#[test]
fn test_builder_dof_imbalance_two_constraints_one_control() {
let c1 = Constraint::new(
ConstraintId::new("superheat"),
ComponentOutput::Superheat {
component_id: "evap".to_string(),
},
5.0,
);
let c2 = Constraint::new(
ConstraintId::new("subcooling"),
ComponentOutput::Superheat {
component_id: "evap".to_string(),
},
3.0,
);
let valve =
BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).expect("valid bounds");
// Same topology as test 1 (3 component eqs total), but 2 constraints and 1 control →
// 3+2=5 equations vs 3+1=4 unknowns → over-constrained. Since `System::finalize()`
// enforces the DoF gate, `build()` itself now rejects the system with
// `TopologyError::DofImbalance` (the post-build `validate_inverse_control_dof()`
// check is unreachable for over-constrained systems).
let result = SystemBuilder::new()
.component("evap", Box::new(MockComponent { n_eqs: 2 }))
.unwrap()
.component("other", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.edge("evap", "other")
.unwrap()
.with_constraint(c1)
.unwrap()
.with_constraint(c2)
.unwrap()
.with_bounded_variable(valve)
.unwrap()
.link_constraint_to_control(
&ConstraintId::new("superheat"),
&BoundedVariableId::new("valve"),
)
.unwrap()
.build();
// DoF gate should reject the build: 2 constraints but only 1 control (unbalanced).
match result {
Err(ThermoError::Topology(TopologyError::DofImbalance { message })) => {
assert!(
message.contains("over-constrained"),
"expected an over-constrained DoF report, got: {message}"
);
}
Err(other) => panic!("expected DofImbalance error from build(), got: {other}"),
Ok(_) => panic!("build() should fail with DofImbalance (2 constraints, 1 control)"),
}
}