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>
This commit is contained in:
192
crates/solver/tests/saturated_lwt_control_integration.rs
Normal file
192
crates/solver/tests/saturated_lwt_control_integration.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user