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>
193 lines
5.9 KiB
Rust
193 lines
5.9 KiB
Rust
//! End-to-end saturated PI control integration test.
|
|
//!
|
|
//! The loop is co-solved with the emergent-pressure refrigeration cycle: the
|
|
//! saturated controller contributes `(u, x)` unknowns, wires compressor `f_m`
|
|
//! through `CalibIndices`, and measures real evaporator capacity from component
|
|
//! thermodynamics.
|
|
#![cfg(feature = "coolprop")]
|
|
|
|
use std::sync::Arc;
|
|
|
|
use entropyk_components::isentropic_compressor::VolumetricEfficiency;
|
|
use entropyk_components::{Condenser, Evaporator, IsenthalpicExpansionValve, IsentropicCompressor};
|
|
use entropyk_fluids::{CoolPropBackend, FluidBackend};
|
|
use entropyk_solver::inverse::{
|
|
BoundedVariable, BoundedVariableId, ComponentOutput, ConstraintId, SaturatedController,
|
|
Saturation,
|
|
};
|
|
use entropyk_solver::solver::Solver;
|
|
use entropyk_solver::system::System;
|
|
use entropyk_solver::{FallbackSolver, NewtonConfig};
|
|
|
|
const N_BASE: usize = 9;
|
|
|
|
fn build_system(controller: Option<SaturatedController>) -> System {
|
|
let backend: Arc<dyn FluidBackend> = Arc::new(CoolPropBackend::new());
|
|
let fluid = "R134a";
|
|
|
|
let comp = Box::new(
|
|
IsentropicCompressor::new(0.70, 318.15, 278.15, 5.0)
|
|
.with_refrigerant(fluid)
|
|
.with_fluid_backend(backend.clone())
|
|
.with_displacement(6.5e-5, 50.0, VolumetricEfficiency::Constant(0.92)),
|
|
);
|
|
let cond = Box::new(
|
|
Condenser::new(766.0)
|
|
.with_refrigerant(fluid)
|
|
.with_fluid_backend(backend.clone())
|
|
.with_secondary_stream(303.15, 1500.0)
|
|
.with_emergent_pressure(5.0),
|
|
);
|
|
let exv = Box::new(
|
|
IsenthalpicExpansionValve::new(278.15)
|
|
.with_refrigerant(fluid)
|
|
.with_fluid_backend(backend.clone())
|
|
.with_emergent_pressure(),
|
|
);
|
|
let evap = Box::new(
|
|
Evaporator::new(1468.0)
|
|
.with_refrigerant(fluid)
|
|
.with_fluid_backend(backend.clone())
|
|
.with_secondary_stream(285.15, 2000.0)
|
|
.with_emergent_pressure(),
|
|
);
|
|
|
|
let mut system = System::new();
|
|
let n_comp = system.add_component(comp);
|
|
let n_cond = system.add_component(cond);
|
|
let n_exv = system.add_component(exv);
|
|
let n_evap = system.add_component(evap);
|
|
|
|
system.register_component_name("compressor", n_comp);
|
|
system.register_component_name("evaporator", n_evap);
|
|
|
|
system.add_edge(n_comp, n_cond).unwrap();
|
|
system.add_edge(n_cond, n_exv).unwrap();
|
|
system.add_edge(n_exv, n_evap).unwrap();
|
|
system.add_edge(n_evap, n_comp).unwrap();
|
|
|
|
if let Some(ctrl) = controller {
|
|
let bv = BoundedVariable::with_component(
|
|
BoundedVariableId::new("compressor_f_m"),
|
|
"compressor",
|
|
1.0,
|
|
ctrl.u_min(),
|
|
ctrl.u_max(),
|
|
)
|
|
.unwrap();
|
|
system.add_bounded_variable(bv).unwrap();
|
|
system.add_saturated_controller(ctrl);
|
|
}
|
|
|
|
system.finalize().unwrap();
|
|
system
|
|
}
|
|
|
|
fn seed_state(system: &System) -> Vec<f64> {
|
|
let mut initial_state = vec![
|
|
0.05, 11.6e5, 445e3, 11.6e5, 262e3, 3.50e5, 262e3, 3.50e5, 405e3,
|
|
];
|
|
debug_assert_eq!(initial_state.len(), N_BASE);
|
|
while initial_state.len() < system.full_state_vector_len() {
|
|
initial_state.push(if initial_state.len() == N_BASE {
|
|
1.0
|
|
} else {
|
|
0.0
|
|
});
|
|
}
|
|
initial_state
|
|
}
|
|
|
|
fn solve_capacity(controller: Option<SaturatedController>) -> (f64, f64, f64, f64) {
|
|
let mut system = build_system(controller);
|
|
let initial_state = seed_state(&system);
|
|
|
|
let config = NewtonConfig {
|
|
max_iterations: 300,
|
|
tolerance: 1e-6,
|
|
line_search: true,
|
|
use_numerical_jacobian: false,
|
|
initial_state: Some(initial_state.clone()),
|
|
..NewtonConfig::default()
|
|
};
|
|
|
|
let mut solver = FallbackSolver::default_solver()
|
|
.with_newton_config(config)
|
|
.with_initial_state(initial_state);
|
|
|
|
let converged = solver
|
|
.solve(&mut system)
|
|
.unwrap_or_else(|e| panic!("saturated capacity solve must converge: {e:?}"));
|
|
|
|
let state = &converged.state;
|
|
let q_evap = state[0] * (state[8] - state[6]);
|
|
let u = if system.saturated_controller_count() > 0 {
|
|
state[N_BASE]
|
|
} else {
|
|
1.0
|
|
};
|
|
let x = if system.saturated_controller_count() > 0 {
|
|
state[N_BASE + 1]
|
|
} else {
|
|
0.0
|
|
};
|
|
(state[0], q_evap, u, x)
|
|
}
|
|
|
|
fn capacity_controller(setpoint: f64, u_min: f64, u_max: f64) -> SaturatedController {
|
|
SaturatedController::new(
|
|
ConstraintId::new("capacity_sat_loop"),
|
|
ComponentOutput::Capacity {
|
|
component_id: "evaporator".to_string(),
|
|
},
|
|
BoundedVariableId::new("compressor_f_m"),
|
|
setpoint,
|
|
u_min,
|
|
u_max,
|
|
)
|
|
.unwrap()
|
|
.with_gain(1.0e-2)
|
|
.unwrap()
|
|
.with_band(1.0)
|
|
.unwrap()
|
|
.with_saturation(Saturation::Hard)
|
|
}
|
|
|
|
#[test]
|
|
fn saturated_lwt_control_tracks_when_unsaturated() {
|
|
let (_m_nom, q_nom, _, _) = solve_capacity(None);
|
|
assert!(q_nom > 0.0);
|
|
|
|
let (_m, q, u, x) = solve_capacity(Some(capacity_controller(q_nom, 0.5, 1.5)));
|
|
|
|
assert!(
|
|
(q - q_nom).abs() < 0.03 * q_nom,
|
|
"wide saturated loop should track nominal capacity: got {q:.1} W, target {q_nom:.1} W"
|
|
);
|
|
assert!(
|
|
(0.5..=1.5).contains(&u) && x.abs() < 0.25,
|
|
"controller should remain unsaturated: u={u:.4}, x={x:.4}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn saturated_lwt_control_pins_actuator_when_saturated() {
|
|
let (_m_nom, q_nom, _, _) = solve_capacity(None);
|
|
let target = 1.30 * q_nom;
|
|
|
|
let (_m, q, u, x) = solve_capacity(Some(capacity_controller(target, 0.75, 1.0)));
|
|
|
|
assert!(
|
|
(u - 1.0).abs() < 2.0e-3,
|
|
"tight loop should pin compressor f_m at upper bound: u={u:.6}"
|
|
);
|
|
assert!(
|
|
x > 1.0,
|
|
"anti-windup state should move beyond the saturation band: x={x:.4}"
|
|
);
|
|
assert!(
|
|
(q - target).abs() > 0.10 * q_nom,
|
|
"tracking error should be released at saturation: q={q:.1} W, target={target:.1} W"
|
|
);
|
|
}
|