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>
245 lines
9.3 KiB
Rust
245 lines
9.3 KiB
Rust
//! End-to-end integration test for the **emergent-pressure** refrigeration cycle.
|
||
//!
|
||
//! This test assembles the REAL thermodynamic components
|
||
//! (`IsentropicCompressor`, `Condenser`, `IsenthalpicExpansionValve`,
|
||
//! `Evaporator`) — not mocks — with a real CoolProp fluid backend and solves the
|
||
//! canonical 4-component loop with the Newton solver.
|
||
//!
|
||
//! Unlike the fixed-design-point path (where the compressor pins
|
||
//! `P_cond = P_sat(t_cond_k)` and the EXV pins `P_evap = P_sat(t_evap_k)`), every
|
||
//! component here runs in **emergent-pressure mode**:
|
||
//!
|
||
//! | Component | emergent equations | pins |
|
||
//! |-----------|--------------------|------|
|
||
//! | Compressor | ṁ = ρ_suc·V_s·N·η_vol ; h_dis(P_suc,h_suc,P_dis) | ṁ, h_dis |
|
||
//! | Condenser | P2=P1 ; ṁ(h1−h2)=ε·C·(T_cond(P1)−T_sec,in) ; h2=h_satliq(P1)−cp·ΔT_sc | **P_cond** |
|
||
//! | EXV | h3=h2 (isenthalpic only) | h3 |
|
||
//! | Evaporator | P4=P3 ; ṁ(h4−h3)=ε·C·(T_sec,in−T_evap(P3)) ; h4=h(P3,T_evap+SH) | **P_evap** |
|
||
//!
|
||
//! DoF (same-branch series loop): 2 + 3 + 1 + 3 = **9 equations / 9 unknowns**.
|
||
//! The condensing/evaporating pressures are therefore EMERGENT: they are
|
||
//! determined by the heat-exchanger ↔ secondary balance, not imposed by the
|
||
//! compressor/EXV design points. The test verifies that varying the secondary
|
||
//! (water) inlet temperature genuinely moves the emergent pressures and COP.
|
||
//!
|
||
//! Requires the `coolprop` feature (entropy + saturation properties), which the
|
||
//! mock `TestBackend` does not provide:
|
||
//! cargo test -p entropyk-solver --features coolprop --test emergent_pressure_cycle
|
||
#![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::solver::{NewtonConfig, Solver};
|
||
use entropyk_solver::system::System;
|
||
|
||
/// State-vector layout (CM1.4 same-branch series loop, 9 unknowns):
|
||
/// `[ṁ, P0,h0, P1,h1, P2,h2, P3,h3]` where
|
||
/// E0 comp→cond, E1 cond→exv, E2 exv→evap, E3 evap→comp.
|
||
const N_STATE: usize = 9;
|
||
|
||
/// Result of a converged emergent-pressure solve, in engineering units.
|
||
struct CycleResult {
|
||
m_dot: f64, // kg/s
|
||
p_cond: f64, // Pa (emergent condensing pressure, edge E0)
|
||
p_evap: f64, // Pa (emergent evaporating pressure, edge E3)
|
||
w_comp: f64, // W (compression power)
|
||
q_evap: f64, // W (cooling capacity)
|
||
cop: f64, // - (Q_evap / W_comp)
|
||
}
|
||
|
||
/// Assembles and solves the emergent-pressure cycle for the given secondary
|
||
/// (water) inlet temperatures and returns the converged operating point.
|
||
fn solve_emergent_cycle(cond_sec_temp_k: f64, evap_sec_temp_k: f64) -> CycleResult {
|
||
let backend: Arc<dyn FluidBackend> = Arc::new(CoolPropBackend::new());
|
||
let fluid = "R134a";
|
||
|
||
// ── Compressor: emergent ṁ via volumetric displacement ────────────────────
|
||
// ṁ = ρ_suc · V_s · N · η_vol. V_s·N ≈ 3.25e-3 m³/s ⇒ ṁ ≈ 0.05 kg/s.
|
||
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)),
|
||
);
|
||
|
||
// ── Condenser: emergent P_cond via subcooling outlet closure ──────────────
|
||
let cond = Box::new(
|
||
Condenser::new(766.0)
|
||
.with_refrigerant(fluid)
|
||
.with_fluid_backend(backend.clone())
|
||
.with_secondary_stream(cond_sec_temp_k, 1500.0)
|
||
.with_emergent_pressure(5.0),
|
||
);
|
||
|
||
// ── EXV: emergent (isenthalpic only, drops the P_evap fix) ────────────────
|
||
let exv = Box::new(
|
||
IsenthalpicExpansionValve::new(278.15)
|
||
.with_refrigerant(fluid)
|
||
.with_fluid_backend(backend.clone())
|
||
.with_emergent_pressure(),
|
||
);
|
||
|
||
// ── Evaporator: emergent P_evap via superheat outlet closure ──────────────
|
||
let evap = Box::new(
|
||
Evaporator::new(1468.0)
|
||
.with_refrigerant(fluid)
|
||
.with_fluid_backend(backend.clone())
|
||
.with_secondary_stream(evap_sec_temp_k, 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.add_edge(n_comp, n_cond).unwrap(); // E0 comp→cond
|
||
system.add_edge(n_cond, n_exv).unwrap(); // E1 cond→exv
|
||
system.add_edge(n_exv, n_evap).unwrap(); // E2 exv→evap
|
||
system.add_edge(n_evap, n_comp).unwrap(); // E3 evap→comp
|
||
|
||
system.finalize().unwrap();
|
||
|
||
// DoF must be exactly balanced (2+3+1+3 = 9 == 9 unknowns).
|
||
assert_eq!(
|
||
system.full_state_vector_len(),
|
||
N_STATE,
|
||
"emergent same-branch loop must be 1 ṁ + 4×2(P,h) = 9 unknowns"
|
||
);
|
||
|
||
// Physically-consistent seed near the expected operating point.
|
||
// P_sat(R134a): 5 °C ≈ 3.50 bar, 45 °C ≈ 11.6 bar.
|
||
let initial_state = vec![
|
||
0.05, // ṁ [kg/s]
|
||
11.6e5, 445e3, // E0 comp→cond : P_cond, h_dis
|
||
11.6e5, 262e3, // E1 cond→exv : P_cond, h_liq
|
||
3.50e5, 262e3, // E2 exv→evap : P_evap, h (isenthalpic)
|
||
3.50e5, 405e3, // E3 evap→comp : P_evap, h_suction (superheated)
|
||
];
|
||
|
||
let mut config = NewtonConfig {
|
||
max_iterations: 200,
|
||
tolerance: 1e-6,
|
||
line_search: true,
|
||
use_numerical_jacobian: false,
|
||
initial_state: Some(initial_state),
|
||
..NewtonConfig::default()
|
||
};
|
||
|
||
let converged = config
|
||
.solve(&mut system)
|
||
.expect("emergent-pressure cycle must converge");
|
||
|
||
let sv = &converged.state;
|
||
let m_dot = sv[0];
|
||
let (p_cond, h_dis) = (sv[1], sv[2]);
|
||
let h_cond_out = sv[4];
|
||
let (p_evap, h_suc) = (sv[7], sv[8]);
|
||
let h_evap_in = sv[6];
|
||
let h_evap_out = sv[8];
|
||
|
||
let w_comp = m_dot * (h_dis - h_suc);
|
||
let q_evap = m_dot * (h_evap_out - h_evap_in);
|
||
|
||
// Sanity: subcooled liquid at condenser outlet, superheated vapour at suction.
|
||
assert!(h_dis > h_suc, "discharge enthalpy must exceed suction");
|
||
assert!(
|
||
h_cond_out < h_suc,
|
||
"condenser outlet must be subcooled liquid"
|
||
);
|
||
assert!(w_comp > 0.0, "compression power must be positive");
|
||
assert!(q_evap > 0.0, "cooling capacity must be positive");
|
||
|
||
CycleResult {
|
||
m_dot,
|
||
p_cond,
|
||
p_evap,
|
||
w_comp,
|
||
q_evap,
|
||
cop: q_evap / w_comp,
|
||
}
|
||
}
|
||
|
||
/// The emergent-pressure loop must converge and produce a physical operating
|
||
/// point (positive capacity, positive power, plausible pressures/COP).
|
||
#[test]
|
||
fn test_emergent_cycle_converges_to_physical_point() {
|
||
let r = solve_emergent_cycle(303.15, 285.15); // cond water 30 °C, evap water 12 °C
|
||
|
||
// Emergent pressures land in a physically reasonable R134a window.
|
||
assert!(
|
||
(5.0e5..20.0e5).contains(&r.p_cond),
|
||
"emergent P_cond out of range: {:.0} Pa",
|
||
r.p_cond
|
||
);
|
||
assert!(
|
||
(1.5e5..6.0e5).contains(&r.p_evap),
|
||
"emergent P_evap out of range: {:.0} Pa",
|
||
r.p_evap
|
||
);
|
||
assert!(
|
||
r.p_cond > r.p_evap,
|
||
"condensing must exceed evaporating pressure"
|
||
);
|
||
assert!(r.m_dot > 0.0, "mass flow must be positive: {}", r.m_dot);
|
||
assert!(
|
||
(1.5..12.0).contains(&r.cop),
|
||
"COP out of physical range: {:.2}",
|
||
r.cop
|
||
);
|
||
}
|
||
|
||
/// **Core emergence claim**: warming the condenser secondary (water) inlet must
|
||
/// raise the emergent condensing pressure and reduce COP — the machine
|
||
/// performance is genuinely qualified by the secondary conditions, not fixed by
|
||
/// compressor design points.
|
||
#[test]
|
||
fn test_warmer_condenser_water_raises_pcond_and_lowers_cop() {
|
||
let cool = solve_emergent_cycle(303.15, 285.15); // 30 °C condenser water
|
||
let warm = solve_emergent_cycle(313.15, 285.15); // 40 °C condenser water
|
||
|
||
assert!(
|
||
warm.p_cond > cool.p_cond + 1.0e4,
|
||
"warmer condenser water must raise emergent P_cond: {:.0} → {:.0} Pa",
|
||
cool.p_cond,
|
||
warm.p_cond
|
||
);
|
||
assert!(
|
||
warm.w_comp > cool.w_comp,
|
||
"higher lift must increase compression power: {:.0} → {:.0} W",
|
||
cool.w_comp,
|
||
warm.w_comp
|
||
);
|
||
assert!(
|
||
warm.cop < cool.cop,
|
||
"warmer condenser water must lower COP: {:.2} → {:.2}",
|
||
cool.cop,
|
||
warm.cop
|
||
);
|
||
}
|
||
|
||
/// Warming the evaporator secondary (water/brine) inlet must raise the emergent
|
||
/// evaporating pressure and increase cooling capacity.
|
||
#[test]
|
||
fn test_warmer_evaporator_water_raises_pevap_and_capacity() {
|
||
let cold = solve_emergent_cycle(303.15, 283.15); // 10 °C evaporator water
|
||
let warm = solve_emergent_cycle(303.15, 291.15); // 18 °C evaporator water
|
||
|
||
assert!(
|
||
warm.p_evap > cold.p_evap + 1.0e4,
|
||
"warmer evaporator water must raise emergent P_evap: {:.0} → {:.0} Pa",
|
||
cold.p_evap,
|
||
warm.p_evap
|
||
);
|
||
assert!(
|
||
warm.q_evap > cold.q_evap,
|
||
"warmer evaporator water must increase capacity: {:.0} → {:.0} W",
|
||
cold.q_evap,
|
||
warm.q_evap
|
||
);
|
||
}
|