//! 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, }; 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 → 2 edge unknowns (P,h). With 1 constraint and 1 control // we need 3 equations total: 2 component eqs + 1 constraint = 3 = 2 + 1 unknowns. let system = SystemBuilder::new() .component("evap", Box::new(MockComponent { n_eqs: 1 })) .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"); let system = SystemBuilder::new() .component("evap", Box::new(MockComponent { n_eqs: 1 })) .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() .expect("build should succeed"); // DoF validation should fail: 2 constraints but only 1 control (unbalanced). let dof_result = system.validate_inverse_control_dof(); assert!( dof_result.is_err(), "validate_inverse_control_dof should fail with 2 constraints and 1 control, got: {:?}", dof_result ); }