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:
@@ -1,3 +1,4 @@
|
||||
use entropyk_components::port::{Connected, FluidId, Port};
|
||||
/// Integration test: calibrated refrigeration cycle vs synthetic test data.
|
||||
///
|
||||
/// Validates that Calib factors correctly scale component outputs and that
|
||||
@@ -12,85 +13,176 @@
|
||||
///
|
||||
/// Energy balance: compressor_work + evaporator_absorption = condenser_rejection ✓
|
||||
/// Pressure balance: closes for any f_dp ✓
|
||||
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
|
||||
};
|
||||
use entropyk_core::{Calib, MassFlow};
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
use entropyk_solver::{
|
||||
solver::{NewtonConfig, Solver},
|
||||
system::System,
|
||||
system::DEFAULT_MASS_FLOW_SEED_KG_S,
|
||||
};
|
||||
use entropyk_components::port::{Connected, FluidId, Port};
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
|
||||
type CP = Port<Connected>;
|
||||
|
||||
// ─── Calibrated mock components ────────────────────────────────────────────────
|
||||
|
||||
struct CalibCompressor { port_suc: CP, port_disc: CP, calib: Calib }
|
||||
struct CalibCompressor {
|
||||
port_suc: CP,
|
||||
port_disc: CP,
|
||||
calib: Calib,
|
||||
}
|
||||
impl Component for CalibCompressor {
|
||||
fn compute_residuals(&self, _s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let dh_eff = 75_000.0 * self.calib.f_m * self.calib.f_power;
|
||||
r[0] = self.port_disc.pressure().to_pascals() - (self.port_suc.pressure().to_pascals() + 1_000_000.0);
|
||||
r[1] = self.port_disc.enthalpy().to_joules_per_kg() - (self.port_suc.enthalpy().to_joules_per_kg() + dh_eff);
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
r: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
let dh_eff = 75_000.0 * self.calib.z_flow * self.calib.z_power;
|
||||
r[0] = self.port_disc.pressure().to_pascals()
|
||||
- (self.port_suc.pressure().to_pascals() + 1_000_000.0);
|
||||
r[1] = self.port_disc.enthalpy().to_joules_per_kg()
|
||||
- (self.port_suc.enthalpy().to_joules_per_kg() + dh_eff);
|
||||
Ok(())
|
||||
}
|
||||
fn jacobian_entries(&self, _s: &StateSlice, _j: &mut JacobianBuilder) -> Result<(), ComponentError> { Ok(()) }
|
||||
fn n_equations(&self) -> usize { 2 }
|
||||
fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
_j: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
Ok(())
|
||||
}
|
||||
fn n_equations(&self) -> usize {
|
||||
2
|
||||
}
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
||||
Ok(vec![MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05)])
|
||||
Ok(vec![
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
MassFlow::from_kg_per_s(-0.05),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
struct CalibCondenser { port_in: CP, port_out: CP, calib: Calib }
|
||||
struct CalibCondenser {
|
||||
port_in: CP,
|
||||
port_out: CP,
|
||||
calib: Calib,
|
||||
}
|
||||
impl Component for CalibCondenser {
|
||||
fn compute_residuals(&self, _s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let dp_eff = 20_000.0 * self.calib.f_dp;
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
r: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
let dp_eff = 20_000.0 * self.calib.z_dp;
|
||||
// Condenser rejects compressor work + evaporator load (energy balance)
|
||||
let dh_reject = 75_000.0 * self.calib.f_m * self.calib.f_power + 150_000.0 * self.calib.f_ua;
|
||||
r[0] = self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - dp_eff);
|
||||
r[1] = self.port_out.enthalpy().to_joules_per_kg() - (self.port_in.enthalpy().to_joules_per_kg() - dh_reject);
|
||||
let dh_reject =
|
||||
75_000.0 * self.calib.z_flow * self.calib.z_power + 150_000.0 * self.calib.z_ua;
|
||||
r[0] =
|
||||
self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - dp_eff);
|
||||
r[1] = self.port_out.enthalpy().to_joules_per_kg()
|
||||
- (self.port_in.enthalpy().to_joules_per_kg() - dh_reject);
|
||||
Ok(())
|
||||
}
|
||||
fn jacobian_entries(&self, _s: &StateSlice, _j: &mut JacobianBuilder) -> Result<(), ComponentError> { Ok(()) }
|
||||
fn n_equations(&self) -> usize { 2 }
|
||||
fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
_j: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
Ok(())
|
||||
}
|
||||
fn n_equations(&self) -> usize {
|
||||
2
|
||||
}
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
||||
Ok(vec![MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05)])
|
||||
Ok(vec![
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
MassFlow::from_kg_per_s(-0.05),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
struct CalibValve { port_in: CP, port_out: CP, calib: Calib }
|
||||
struct CalibValve {
|
||||
port_in: CP,
|
||||
port_out: CP,
|
||||
calib: Calib,
|
||||
}
|
||||
impl Component for CalibValve {
|
||||
fn compute_residuals(&self, _s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let dp_eff = 1_000_000.0 - 20_000.0 * self.calib.f_dp;
|
||||
r[0] = self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - dp_eff);
|
||||
r[1] = self.port_out.enthalpy().to_joules_per_kg() - self.port_in.enthalpy().to_joules_per_kg();
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
r: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
let dp_eff = 1_000_000.0 - 20_000.0 * self.calib.z_dp;
|
||||
r[0] =
|
||||
self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - dp_eff);
|
||||
r[1] = self.port_out.enthalpy().to_joules_per_kg()
|
||||
- self.port_in.enthalpy().to_joules_per_kg();
|
||||
Ok(())
|
||||
}
|
||||
fn jacobian_entries(&self, _s: &StateSlice, _j: &mut JacobianBuilder) -> Result<(), ComponentError> { Ok(()) }
|
||||
fn n_equations(&self) -> usize { 2 }
|
||||
fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
_j: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
Ok(())
|
||||
}
|
||||
fn n_equations(&self) -> usize {
|
||||
2
|
||||
}
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
||||
Ok(vec![MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05)])
|
||||
Ok(vec![
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
MassFlow::from_kg_per_s(-0.05),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
struct CalibEvaporator { port_in: CP, port_out: CP, calib: Calib }
|
||||
struct CalibEvaporator {
|
||||
port_in: CP,
|
||||
port_out: CP,
|
||||
calib: Calib,
|
||||
}
|
||||
impl Component for CalibEvaporator {
|
||||
fn compute_residuals(&self, _s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let dh_eff = 150_000.0 * self.calib.f_ua;
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
r: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
let dh_eff = 150_000.0 * self.calib.z_ua;
|
||||
r[0] = self.port_out.pressure().to_pascals() - self.port_in.pressure().to_pascals();
|
||||
r[1] = self.port_out.enthalpy().to_joules_per_kg() - (self.port_in.enthalpy().to_joules_per_kg() + dh_eff);
|
||||
r[1] = self.port_out.enthalpy().to_joules_per_kg()
|
||||
- (self.port_in.enthalpy().to_joules_per_kg() + dh_eff);
|
||||
Ok(())
|
||||
}
|
||||
fn jacobian_entries(&self, _s: &StateSlice, _j: &mut JacobianBuilder) -> Result<(), ComponentError> { Ok(()) }
|
||||
fn n_equations(&self) -> usize { 2 }
|
||||
fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_s: &StateSlice,
|
||||
_j: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
Ok(())
|
||||
}
|
||||
fn n_equations(&self) -> usize {
|
||||
2
|
||||
}
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
||||
Ok(vec![MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05)])
|
||||
Ok(vec![
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
MassFlow::from_kg_per_s(-0.05),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,21 +191,24 @@ fn port(p_pa: f64, h_j_kg: f64) -> CP {
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(p_pa),
|
||||
Enthalpy::from_joules_per_kg(h_j_kg),
|
||||
).connect(Port::new(
|
||||
)
|
||||
.connect(Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(p_pa),
|
||||
Enthalpy::from_joules_per_kg(h_j_kg),
|
||||
)).unwrap();
|
||||
))
|
||||
.unwrap();
|
||||
connected
|
||||
}
|
||||
|
||||
fn make_calib() -> Calib {
|
||||
Calib {
|
||||
f_m: 1.0,
|
||||
f_dp: 1.0,
|
||||
f_ua: 1.0,
|
||||
f_power: 1.0,
|
||||
f_etav: 1.0,
|
||||
z_flow: 1.0,
|
||||
z_flow_eco: 1.0,
|
||||
z_dp: 1.0,
|
||||
z_ua: 1.0,
|
||||
z_power: 1.0,
|
||||
z_etav: 1.0,
|
||||
calibration_source: None,
|
||||
}
|
||||
}
|
||||
@@ -123,9 +218,9 @@ fn analytical_solution(calib: &Calib) -> [f64; 8] {
|
||||
let p3 = 350_000.0;
|
||||
let h3 = 410_000.0;
|
||||
let p0 = p3 + 1_000_000.0;
|
||||
let h0 = h3 + 75_000.0 * calib.f_m * calib.f_power;
|
||||
let p1 = p0 - 20_000.0 * calib.f_dp;
|
||||
let h1 = h0 - 75_000.0 * calib.f_m * calib.f_power - 150_000.0 * calib.f_ua;
|
||||
let h0 = h3 + 75_000.0 * calib.z_flow * calib.z_power;
|
||||
let p1 = p0 - 20_000.0 * calib.z_dp;
|
||||
let h1 = h0 - 75_000.0 * calib.z_flow * calib.z_power - 150_000.0 * calib.z_ua;
|
||||
let p2 = p3;
|
||||
let h2 = h1;
|
||||
[p0, h0, p1, h1, p2, h2, p3, h3]
|
||||
@@ -166,16 +261,36 @@ fn solve_calibrated_cycle(calib: &Calib) -> Vec<f64> {
|
||||
system.add_edge(n_evap, n_comp).unwrap();
|
||||
system.finalize().unwrap();
|
||||
|
||||
// CM1.2: state layout is now (ṁ, P, h) per edge (stride 3). Map the analytical
|
||||
// (P, h) pairs onto the correct slots and seed each edge's mass flow.
|
||||
let mut initial_state = vec![0.0; system.full_state_vector_len()];
|
||||
for (i, edge_idx) in system.edge_indices().enumerate() {
|
||||
let (m_idx, p_idx, h_idx) = system.edge_state_indices_full(edge_idx);
|
||||
initial_state[m_idx] = DEFAULT_MASS_FLOW_SEED_KG_S;
|
||||
initial_state[p_idx] = sol[2 * i];
|
||||
initial_state[h_idx] = sol[2 * i + 1];
|
||||
}
|
||||
|
||||
let mut config = NewtonConfig {
|
||||
max_iterations: 100,
|
||||
tolerance: 1e-8,
|
||||
line_search: false,
|
||||
use_numerical_jacobian: true,
|
||||
initial_state: Some(sol.to_vec()),
|
||||
initial_state: Some(initial_state),
|
||||
..NewtonConfig::default()
|
||||
};
|
||||
|
||||
config.solve(&mut system).unwrap().state
|
||||
let result = config.solve(&mut system).unwrap().state;
|
||||
|
||||
// CM1.2: re-extract the (P, h) pairs per edge so downstream assertions keep
|
||||
// the historical [p0, h0, p1, h1, ...] layout independent of the ṁ slots.
|
||||
let mut ph = vec![0.0; 2 * system.edge_count()];
|
||||
for (i, edge_idx) in system.edge_indices().enumerate() {
|
||||
let (p_idx, h_idx) = system.edge_state_indices(edge_idx);
|
||||
ph[2 * i] = result[p_idx];
|
||||
ph[2 * i + 1] = result[h_idx];
|
||||
}
|
||||
ph
|
||||
}
|
||||
|
||||
/// Baseline: all Calib = 1.0 → results match nominal analytical solution.
|
||||
@@ -187,7 +302,14 @@ fn test_calibrated_cycle_nominal_baseline() {
|
||||
|
||||
for i in 0..8 {
|
||||
let diff = (sv[i] - expected[i]).abs();
|
||||
assert!(diff < 10.0, "sv[{}]: got {}, expected {}, diff {}", i, sv[i], expected[i], diff);
|
||||
assert!(
|
||||
diff < 10.0,
|
||||
"sv[{}]: got {}, expected {}, diff {}",
|
||||
i,
|
||||
sv[i],
|
||||
expected[i],
|
||||
diff
|
||||
);
|
||||
}
|
||||
|
||||
// Energy balance check
|
||||
@@ -203,7 +325,11 @@ fn test_calibrated_cycle_nominal_baseline() {
|
||||
#[test]
|
||||
fn test_calibrated_cycle_fua_increases_capacity() {
|
||||
let nom = make_calib();
|
||||
let cal = Calib { f_ua: 1.1, calibration_source: Some("synthetic-fua".into()), ..make_calib() };
|
||||
let cal = Calib {
|
||||
z_ua: 1.1,
|
||||
calibration_source: Some("synthetic-fua".into()),
|
||||
..make_calib()
|
||||
};
|
||||
|
||||
let sv_nom = solve_calibrated_cycle(&nom);
|
||||
let sv_cal = solve_calibrated_cycle(&cal);
|
||||
@@ -223,8 +349,8 @@ fn test_calibrated_cycle_fua_increases_capacity() {
|
||||
fn test_calibrated_cycle_fm_fpower_scales_compressor_work() {
|
||||
let nom = make_calib();
|
||||
let cal = Calib {
|
||||
f_m: 1.05,
|
||||
f_power: 1.03,
|
||||
z_flow: 1.05,
|
||||
z_power: 1.03,
|
||||
calibration_source: Some("test-bench-2024-A".into()),
|
||||
..make_calib()
|
||||
};
|
||||
@@ -248,7 +374,7 @@ fn test_calibrated_cycle_fm_fpower_scales_compressor_work() {
|
||||
fn test_calibrated_cycle_fdp_scales_pressure_drop() {
|
||||
let nom = make_calib();
|
||||
let cal = Calib {
|
||||
f_dp: 1.5,
|
||||
z_dp: 1.5,
|
||||
calibration_source: Some("dp-test-synthetic".into()),
|
||||
..make_calib()
|
||||
};
|
||||
@@ -283,7 +409,7 @@ fn test_calibrated_cycle_with_calibration_source_metadata() {
|
||||
calib.calibration_source.as_deref(),
|
||||
Some("manufacturer-test-report-2024-TR-001")
|
||||
);
|
||||
assert_eq!(calib.f_ua, 1.1);
|
||||
assert_eq!(calib.z_ua, 1.1);
|
||||
|
||||
let sv = solve_calibrated_cycle(&calib);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user