//! Integration tests for Inverse Calibration (Story 5.5). //! //! Tests cover: //! - AC: Components can dynamically read calibration factors (e.g. f_m, f_ua) from SystemState. //! - AC: The solver successfully optimizes these calibration factors to meet constraints. use entropyk_components::{Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState}; use entropyk_core::CalibIndices; use entropyk_solver::{ System, NewtonConfig, Solver, inverse::{ BoundedVariable, BoundedVariableId, Constraint, ConstraintId, ComponentOutput, }, }; /// A mock component that simulates a heat exchanger whose capacity depends on `f_ua`. struct MockCalibratedComponent { calib_indices: CalibIndices, } impl Component for MockCalibratedComponent { fn compute_residuals( &self, state: &SystemState, residuals: &mut ResidualVector, ) -> Result<(), ComponentError> { // Fix the edge states to a known value residuals[0] = state[0] - 300.0; residuals[1] = state[1] - 400.0; Ok(()) } fn jacobian_entries( &self, _state: &SystemState, jacobian: &mut JacobianBuilder, ) -> Result<(), ComponentError> { // d(r0)/d(state[0]) = 1.0 jacobian.add_entry(0, 0, 1.0); // d(r1)/d(state[1]) = 1.0 jacobian.add_entry(1, 1, 1.0); // No dependence of physical equations on f_ua Ok(()) } fn n_equations(&self) -> usize { 2 // balances 2 edge variables } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn set_calib_indices(&mut self, indices: CalibIndices) { self.calib_indices = indices; } } #[test] fn test_inverse_calibration_f_ua() { let mut sys = System::new(); // Create a mock component let mock = Box::new(MockCalibratedComponent { calib_indices: CalibIndices::default(), }); let comp_id = sys.add_component(mock); sys.register_component_name("evaporator", comp_id); // Add a self-edge just to simulate some connections sys.add_edge(comp_id, comp_id).unwrap(); // We want the capacity to be exactly 4015 W. // The mocked math in System::extract_constraint_values_with_controls: // Capacity = state[1] * 10.0 + f_ua * 10.0 (primary effect) // We fixed state[1] to 400.0, so: // 400.0 * 10.0 + f_ua * 10.0 = 4015 // 4000.0 + 10.0 * f_ua = 4015 // 10.0 * f_ua = 15.0 // f_ua = 1.5 sys.add_constraint(Constraint::new( ConstraintId::new("capacity_control"), ComponentOutput::Capacity { component_id: "evaporator".to_string(), }, 4015.0, )).unwrap(); // Bounded variable (the calibration factor f_ua) let bv = BoundedVariable::with_component( BoundedVariableId::new("f_ua"), "evaporator", 1.0, // initial 0.1, // min 10.0 // max ).unwrap(); sys.add_bounded_variable(bv).unwrap(); // Link constraint to control sys.link_constraint_to_control( &ConstraintId::new("capacity_control"), &BoundedVariableId::new("f_ua") ).unwrap(); sys.finalize().unwrap(); // Verify that the validation passes assert!(sys.validate_inverse_control_dof().is_ok()); let initial_state = vec![0.0; sys.full_state_vector_len()]; // Use NewtonRaphson let mut solver = NewtonConfig::default().with_initial_state(initial_state); let result = solver.solve(&mut sys); // Should converge quickly assert!(dbg!(&result).is_ok()); let converged = result.unwrap(); // The control variable `f_ua` is at the end of the state vector let f_ua_idx = sys.full_state_vector_len() - 1; let final_f_ua: f64 = converged.state[f_ua_idx]; // Target f_ua = 1.5 let abs_diff = (final_f_ua - 1.5_f64).abs(); assert!(abs_diff < 1e-4, "f_ua should converge to 1.5, got {}", final_f_ua); }