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:
2026-07-17 22:46:46 +02:00
parent 62efea0646
commit 3358b74342
275 changed files with 70187 additions and 5230 deletions

View File

@@ -0,0 +1,244 @@
//! 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 ; ṁ(h1h2)=ε·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 ; ṁ(h4h3)=ε·C·(T_sec,inT_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
);
}