//! 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)"), } }