Update project structure and configurations

This commit is contained in:
2026-05-23 10:19:55 +02:00
parent ab5dc7e568
commit 62efea0646
1832 changed files with 83568 additions and 51829 deletions

View File

@@ -0,0 +1,296 @@
/// Integration test: calibrated refrigeration cycle vs synthetic test data.
///
/// Validates that Calib factors correctly scale component outputs and that
/// the solver converges on a calibrated cycle matching expected targets
/// within configurable tolerances (capacity ±2%, power ±3%).
///
/// The mock components form a self-consistent cycle for any Calib values:
/// Compressor : dp = +1 MPa, dh = +75kJ × f_m × f_power
/// Condenser : dp = -20kPa×f_dp, dh = -(75kJ×f_m×f_power + 150kJ×f_ua)
/// Valve : dp = -(1MPa - 20kPa×f_dp), dh = 0 (isenthalpic)
/// Evaporator : dp = 0, dh = +150kJ × f_ua
///
/// 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_solver::{
solver::{NewtonConfig, Solver},
system::System,
};
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 }
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);
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 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)])
}
}
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;
// 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);
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 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)])
}
}
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();
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 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)])
}
}
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;
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);
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 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)])
}
}
fn port(p_pa: f64, h_j_kg: f64) -> CP {
let (connected, _) = Port::new(
FluidId::new("R134a"),
Pressure::from_pascals(p_pa),
Enthalpy::from_joules_per_kg(h_j_kg),
).connect(Port::new(
FluidId::new("R134a"),
Pressure::from_pascals(p_pa),
Enthalpy::from_joules_per_kg(h_j_kg),
)).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,
calibration_source: None,
}
}
/// Compute the analytical solution for the calibrated cycle.
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 p2 = p3;
let h2 = h1;
[p0, h0, p1, h1, p2, h2, p3, h3]
}
fn solve_calibrated_cycle(calib: &Calib) -> Vec<f64> {
let sol = analytical_solution(calib);
let comp = Box::new(CalibCompressor {
port_suc: port(sol[6], sol[7]),
port_disc: port(sol[0], sol[1]),
calib: calib.clone(),
});
let cond = Box::new(CalibCondenser {
port_in: port(sol[0], sol[1]),
port_out: port(sol[2], sol[3]),
calib: calib.clone(),
});
let valv = Box::new(CalibValve {
port_in: port(sol[2], sol[3]),
port_out: port(sol[4], sol[5]),
calib: calib.clone(),
});
let evap = Box::new(CalibEvaporator {
port_in: port(sol[4], sol[5]),
port_out: port(sol[6], sol[7]),
calib: calib.clone(),
});
let mut system = System::new();
let n_comp = system.add_component(comp);
let n_cond = system.add_component(cond);
let n_valv = system.add_component(valv);
let n_evap = system.add_component(evap);
system.add_edge(n_comp, n_cond).unwrap();
system.add_edge(n_cond, n_valv).unwrap();
system.add_edge(n_valv, n_evap).unwrap();
system.add_edge(n_evap, n_comp).unwrap();
system.finalize().unwrap();
let mut config = NewtonConfig {
max_iterations: 100,
tolerance: 1e-8,
line_search: false,
use_numerical_jacobian: true,
initial_state: Some(sol.to_vec()),
..NewtonConfig::default()
};
config.solve(&mut system).unwrap().state
}
/// Baseline: all Calib = 1.0 → results match nominal analytical solution.
#[test]
fn test_calibrated_cycle_nominal_baseline() {
let calib = make_calib();
let sv = solve_calibrated_cycle(&calib);
let expected = analytical_solution(&calib);
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);
}
// Energy balance check
let dh_comp = sv[1] - sv[7];
let dh_cond = sv[3] - sv[1];
let dh_valve = sv[5] - sv[3];
let dh_evap = sv[7] - sv[5];
let imbalance = dh_comp + dh_cond + dh_valve + dh_evap;
assert!(imbalance.abs() < 10.0, "Energy imbalance: {imbalance}");
}
/// f_ua = 1.1 on evaporator → capacity increases by 10% (±2% tolerance).
#[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 sv_nom = solve_calibrated_cycle(&nom);
let sv_cal = solve_calibrated_cycle(&cal);
let dh_evap_nom = sv_nom[7] - sv_nom[5];
let dh_evap_cal = sv_cal[7] - sv_cal[5];
let capacity_ratio = dh_evap_cal / dh_evap_nom;
assert!(
(capacity_ratio - 1.10).abs() < 0.02,
"Capacity ratio: {capacity_ratio:.4}, expected ~1.10 ±2%"
);
}
/// f_m * f_power on compressor → compressor work scales accordingly (±3% tolerance).
#[test]
fn test_calibrated_cycle_fm_fpower_scales_compressor_work() {
let nom = make_calib();
let cal = Calib {
f_m: 1.05,
f_power: 1.03,
calibration_source: Some("test-bench-2024-A".into()),
..make_calib()
};
let sv_nom = solve_calibrated_cycle(&nom);
let sv_cal = solve_calibrated_cycle(&cal);
let dh_comp_nom = sv_nom[1] - sv_nom[7];
let dh_comp_cal = sv_cal[1] - sv_cal[7];
let power_ratio = dh_comp_cal / dh_comp_nom;
let expected = 1.05 * 1.03;
assert!(
(power_ratio - expected).abs() < 0.03,
"Power ratio: {power_ratio:.4}, expected ~{expected:.4} ±3%"
);
}
/// f_dp on condenser → pressure drop scales by f_dp factor.
#[test]
fn test_calibrated_cycle_fdp_scales_pressure_drop() {
let nom = make_calib();
let cal = Calib {
f_dp: 1.5,
calibration_source: Some("dp-test-synthetic".into()),
..make_calib()
};
let sv_nom = solve_calibrated_cycle(&nom);
let sv_cal = solve_calibrated_cycle(&cal);
let dp_nom = sv_nom[2] - sv_nom[0]; // negative (pressure drop)
let dp_cal = sv_cal[2] - sv_cal[0];
let dp_ratio = dp_cal / dp_nom;
assert!(
(dp_ratio - 1.5).abs() < 0.05,
"Pressure drop ratio: {dp_ratio:.4}, expected ~1.50 ±5%"
);
}
/// Calib with calibration_source roundtrips through JSON and still produces correct results.
#[test]
fn test_calibrated_cycle_with_calibration_source_metadata() {
let calib_json = r#"{
"f_m": 1.0,
"f_dp": 1.0,
"f_ua": 1.1,
"f_power": 1.0,
"f_etav": 1.0,
"calibration_source": "manufacturer-test-report-2024-TR-001"
}"#;
let calib: Calib = serde_json::from_str(calib_json).unwrap();
assert_eq!(
calib.calibration_source.as_deref(),
Some("manufacturer-test-report-2024-TR-001")
);
assert_eq!(calib.f_ua, 1.1);
let sv = solve_calibrated_cycle(&calib);
// f_ua=1.1 → evaporator Δh = 150kJ × 1.1 = 165 kJ/kg
let dh_evap = sv[7] - sv[5];
assert!(
(dh_evap - 165_000.0).abs() < 1_000.0,
"Evaporator Δh with f_ua=1.1: {dh_evap:.0}, expected ~165000"
);
}