chore: update documentation to reflect recent architectural changes and improve clarity

This commit is contained in:
Sepehr
2026-03-10 22:59:04 +01:00
parent d88914a44f
commit 891c4ba436
530 changed files with 2544 additions and 1513 deletions

View File

@@ -191,7 +191,9 @@ fn test_run_simulation_with_coolprop() {
assert!(
err_msg.contains("CoolProp")
|| err_msg.contains("Fluid")
|| err_msg.contains("Component"),
|| err_msg.contains("Component")
|| err_msg.contains("IsolatedNode")
|| err_msg.contains("finalization"),
"Unexpected error: {}",
err_msg
);
@@ -199,3 +201,454 @@ fn test_run_simulation_with_coolprop() {
_ => panic!("Unexpected status: {:?}", result.status),
}
}
/// Task 3.3: Verify that port-spec syntax in edges (e.g., "screw_0:discharge")
/// is correctly parsed - the config should parse and the component/type info should
/// be available with named port reference.
#[test]
fn test_edge_port_spec_syntax_parsed() {
use entropyk_cli::config::ScenarioConfig;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let config_path = dir.path().join("screw_port_spec.json");
// Config with correct port spec syntax: "component:port_name"
let json = r#"
{
"name": "Port Spec Test",
"fluid": "R134a",
"circuits": [
{
"id": 0,
"components": [
{
"type": "ScrewEconomizerCompressor",
"name": "screw_0",
"nominal_frequency_hz": 50.0,
"mechanical_efficiency": 0.92,
"economizer_fraction": 0.12,
"mf_a00": 1.2, "mf_a10": 0.003, "mf_a01": -0.002, "mf_a11": 0.00001,
"pw_b00": 55000.0, "pw_b10": 200.0, "pw_b01": -300.0, "pw_b11": 0.5,
"p_suction_bar": 3.2, "h_suction_kj_kg": 400.0,
"p_discharge_bar": 12.8, "h_discharge_kj_kg": 440.0,
"p_eco_bar": 6.4, "h_eco_kj_kg": 260.0
},
{
"type": "Placeholder",
"name": "condenser",
"n_equations": 2
},
{
"type": "Placeholder",
"name": "evaporator",
"n_equations": 2
}
],
"edges": [
{ "from": "screw_0:discharge", "to": "condenser:inlet" },
{ "from": "condenser:outlet", "to": "evaporator:inlet" },
{ "from": "evaporator:outlet", "to": "screw_0:suction" }
]
}
],
"solver": { "strategy": "fallback", "max_iterations": 5 }
}
"#;
std::fs::write(&config_path, json).unwrap();
let config = ScenarioConfig::from_file(&config_path);
assert!(config.is_ok(), "Config should parse successfully");
let config = config.unwrap();
// Verify the edge port specs are preserved in the raw config
let edges = &config.circuits[0].edges;
assert_eq!(edges.len(), 3);
assert_eq!(edges[0].from, "screw_0:discharge");
assert_eq!(edges[0].to, "condenser:inlet");
assert_eq!(edges[2].from, "evaporator:outlet");
assert_eq!(edges[2].to, "screw_0:suction");
}
/// Task 3.4: Verify preset configuration is correctly parsed and overridable.
#[test]
fn test_screw_compressor_preset_config() {
use entropyk_cli::config::ScenarioConfig;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let config_path = dir.path().join("screw_preset.json");
// Config using preset with explicit frequency override
let json = r#"
{
"name": "Preset Bitzer Test",
"fluid": "R134a",
"circuits": [
{
"id": 0,
"components": [
{
"type": "ScrewEconomizerCompressor",
"name": "screw_0",
"preset": "bitzer_generic_200kw",
"nominal_frequency_hz": 50.0,
"frequency_hz": 45.0,
"mechanical_efficiency": 0.92,
"p_suction_bar": 3.2, "h_suction_kj_kg": 400.0,
"p_discharge_bar": 12.8, "h_discharge_kj_kg": 440.0,
"p_eco_bar": 6.4, "h_eco_kj_kg": 260.0
}
],
"edges": []
}
],
"solver": { "strategy": "fallback", "max_iterations": 5 }
}
"#;
std::fs::write(&config_path, json).unwrap();
let config = ScenarioConfig::from_file(&config_path);
assert!(config.is_ok(), "Config with preset should parse successfully");
let config = config.unwrap();
let params = &config.circuits[0].components[0].params;
// Verify preset is stored as param
assert_eq!(
params.get("preset").and_then(|v| v.as_str()),
Some("bitzer_generic_200kw"),
"preset field should be in params"
);
// Verify frequency_hz override
assert_eq!(
params.get("frequency_hz").and_then(|v| v.as_f64()),
Some(45.0),
"frequency_hz should be overridden to 45.0"
);
// Verify that explicit mf coefficients can coexist with preset
// (no explicit mf_a00 means it will use the preset default 1.35)
assert!(
params.get("mf_a00").is_none(),
"Preset should not require explicit mf_a00"
);
}
/// Task 3.4: Verify grasso preset is also recognized.
#[test]
fn test_screw_compressor_grasso_preset_config() {
use entropyk_cli::config::ScenarioConfig;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let config_path = dir.path().join("screw_grasso.json");
let json = r#"
{
"fluid": "R134a",
"circuits": [
{
"id": 0,
"components": [
{
"type": "ScrewEconomizerCompressor",
"name": "screw_0",
"preset": "grasso_generic_200kw",
"nominal_frequency_hz": 50.0,
"mechanical_efficiency": 0.90,
"p_suction_bar": 3.2, "h_suction_kj_kg": 400.0,
"p_discharge_bar": 12.8, "h_discharge_kj_kg": 440.0,
"p_eco_bar": 6.4, "h_eco_kj_kg": 260.0
}
],
"edges": []
}
],
"solver": { "max_iterations": 1 }
}
"#;
std::fs::write(&config_path, json).unwrap();
let config = ScenarioConfig::from_file(&config_path).unwrap();
let params = &config.circuits[0].components[0].params;
assert_eq!(
params.get("preset").and_then(|v| v.as_str()),
Some("grasso_generic_200kw")
);
}
/// AC2 validation: Given frequency_hz: 40.0 in config, the CLI path correctly applies
/// set_frequency_hz(), yielding frequency_ratio() == 0.8.
///
/// Replicates the create_component() logic for ScrewEconomizerCompressor to validate AC2.
#[test]
fn test_ac2_frequency_ratio_set_correctly_by_cli() {
use entropyk_components::{
polynomials::Polynomial2D,
screw_economizer_compressor::{ScrewEconomizerCompressor, ScrewPerformanceCurves},
port::{FluidId, Port},
};
use entropyk_core::{Enthalpy, Pressure};
let make_port = |p_bar: f64, h_kj_kg: f64| {
let a = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(p_bar),
Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
);
let b = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(p_bar),
Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
);
a.connect(b).unwrap().0
};
let curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
Polynomial2D::bilinear(1.2, 0.003, -0.002, 1e-5),
Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5),
0.12,
);
let mut comp = ScrewEconomizerCompressor::new(
curves,
"R134a",
50.0, // nominal_frequency_hz: 50 Hz
0.92,
make_port(3.2, 400.0),
make_port(12.8, 440.0),
make_port(6.4, 260.0),
)
.expect("valid compressor");
// Mirrors what create_component() does when "frequency_hz" present in JSON params
comp.set_frequency_hz(40.0)
.expect("set_frequency_hz(40.0) should succeed");
// AC2 core assertion: 40 / 50 == 0.8
assert!(
(comp.frequency_ratio() - 0.8).abs() < 1e-10,
"AC2 FAILED: expected frequency_ratio 0.8 but got {:.6}",
comp.frequency_ratio()
);
}
/// AC1: Given ua_nominal_kw_k: 8.5, component's ua_nominal() == 8500.0 W/K.
#[test]
fn test_ac1_mchx_ua_nominal_parsed_from_config() {
use entropyk_cli::config::ScenarioConfig;
let json = r#"
{
"fluid": "R134a",
"circuits": [{
"id": 0,
"components": [{
"type": "MchxCondenserCoil",
"name": "mchx_coil",
"ua_nominal_kw_k": 8.5,
"fan_speed": 1.0,
"air_inlet_temp_c": 35.0
}],
"edges": []
}]
}"#;
let config = ScenarioConfig::from_json(json).unwrap();
let comp = &config.circuits[0].components[0];
// AC1: ua_nominal_kw_k field parsed correctly
assert_eq!(comp.ua_nominal_kw_k, Some(8.5), "ua_nominal_kw_k should be 8.5 kW/K");
assert_eq!(comp.fan_speed, Some(1.0));
assert_eq!(comp.air_inlet_temp_c, Some(35.0));
}
/// AC2: Given fan_speed=0.64, n_air_exponent=0.5, UA_eff ≈ UA_nom × √0.64 = UA_nom × 0.8.
#[test]
fn test_ac2_fan_speed_064_yields_ua_eff_08() {
use entropyk_components::heat_exchanger::MchxCondenserCoil;
use approx::assert_relative_eq;
let ua_nominal = 8_500.0; // W/K (8.5 kW/K)
let n_air = 0.5;
let mut coil = MchxCondenserCoil::new(ua_nominal, n_air, 0);
// Set design conditions: 35°C air, fan_speed=0.64
coil.set_air_temperature_celsius(35.0);
coil.set_fan_speed_ratio(0.64);
// AC2: UA_eff ≈ UA_nom × 0.64^0.5 = UA_nom × 0.8
let expected_ua = ua_nominal * 0.8; // 0.64^0.5 = 0.8
// Allow 5% tolerance for density correction at 35°C
let ua_eff = coil.ua_effective();
assert_relative_eq!(ua_eff, expected_ua, epsilon = expected_ua * 0.05);
}
/// AC3: condenser_bank with 2 circuits × 2 coils → 4 components with names mchx_0a..mchx_1b.
#[test]
fn test_ac3_condenser_bank_2x2_generates_4_components() {
use entropyk_cli::config::ScenarioConfig;
let json = r#"
{
"fluid": "R134a",
"circuits": [{
"id": 0,
"components": [{
"type": "MchxCondenserCoil",
"name": "mchx",
"ua_nominal_kw_k": 8.5,
"fan_speed": 1.0,
"air_inlet_temp_c": 35.0,
"condenser_bank": {
"circuits": 2,
"coils_per_circuit": 2
}
}],
"edges": []
}]
}"#;
let config = ScenarioConfig::from_json(json).unwrap();
let bank_comp = &config.circuits[0].components[0];
// Verify bank config parsed
let bank = bank_comp.condenser_bank.as_ref().expect("condenser_bank must be present");
assert_eq!(bank.circuits, 2);
assert_eq!(bank.coils_per_circuit, 2);
// Verify bank expansion logic: 2*2 = 4 coils with correct names
// This mirrors the bank expansion in execute_simulation()
let mut expanded_names = Vec::new();
for c in 0..bank.circuits {
for i in 0..bank.coils_per_circuit {
let letter = (b'a' + (i as u8)) as char;
expanded_names.push(format!("{}_{}{}", bank_comp.name, c, letter));
}
}
assert_eq!(expanded_names.len(), 4, "2×2 bank should expand to 4 coils");
assert_eq!(expanded_names[0], "mchx_0a");
assert_eq!(expanded_names[1], "mchx_0b");
assert_eq!(expanded_names[2], "mchx_1a");
assert_eq!(expanded_names[3], "mchx_1b");
}
/// Integration: run_simulation() with frequency_hz: 40.0 in a complete 3-port
/// screw topology does not produce a frequency-validation error.
#[test]
fn test_frequency_hz_40_passes_cli_simulation() {
use entropyk_cli::run::run_simulation;
let dir = tempdir().unwrap();
let config_path = dir.path().join("screw_freq_integration.json");
let json = r#"
{
"name": "AC2 Integration",
"fluid": "R134a",
"circuits": [
{
"id": 0,
"components": [
{
"type": "ScrewEconomizerCompressor",
"name": "screw_0",
"nominal_frequency_hz": 50.0,
"frequency_hz": 40.0,
"mechanical_efficiency": 0.92,
"economizer_fraction": 0.12,
"mf_a00": 1.2, "mf_a10": 0.003, "mf_a01": -0.002, "mf_a11": 0.00001,
"pw_b00": 55000.0, "pw_b10": 200.0, "pw_b01": -300.0, "pw_b11": 0.5,
"p_suction_bar": 3.2, "h_suction_kj_kg": 400.0,
"p_discharge_bar": 12.8, "h_discharge_kj_kg": 440.0,
"p_eco_bar": 6.4, "h_eco_kj_kg": 260.0
},
{ "type": "Placeholder", "name": "cond", "n_equations": 2 },
{ "type": "Placeholder", "name": "evap", "n_equations": 2 },
{ "type": "Placeholder", "name": "eco_hx", "n_equations": 2 }
],
"edges": [
{ "from": "screw_0:discharge", "to": "cond:inlet" },
{ "from": "cond:outlet", "to": "evap:inlet" },
{ "from": "evap:outlet", "to": "screw_0:suction" },
{ "from": "eco_hx:outlet", "to": "screw_0:economizer" }
]
}
],
"solver": { "strategy": "fallback", "max_iterations": 5 }
}
"#;
std::fs::write(&config_path, json).unwrap();
let result = run_simulation(&config_path, None, false).unwrap();
// The simulation may fail due to topology/solver mismatches with placeholder components.
// Critical assertion: it must NOT error because of frequency validation (= AC2 would fail).
if let Some(err) = &result.error {
assert!(
!err.to_lowercase().contains("frequency"),
"CLI must not error on frequency validation (AC2): {}",
err
);
}
}
/// Task 4.3: Verify that fan_control: "bounded" config goes through the full CLI pipeline
/// without panicking or erroring at the BoundedVariable insertion step.
///
/// This exercises the post-finalize() control path in execute_simulation().
#[test]
fn test_fan_control_bounded_does_not_error() {
use entropyk_cli::run::run_simulation;
let dir = tempdir().unwrap();
let config_path = dir.path().join("mchx_fan_bounded.json");
let json = r#"
{
"fluid": "R134a",
"circuits": [{
"id": 0,
"components": [{
"type": "MchxCondenserCoil",
"name": "mchx_coil",
"ua_nominal_kw_k": 8.5,
"fan_speed": 0.8,
"air_inlet_temp_c": 35.0,
"fan_control": "bounded",
"fan_speed_min": 0.1,
"fan_speed_max": 1.0
}],
"edges": []
}],
"solver": { "strategy": "fallback", "max_iterations": 3 }
}
"#;
std::fs::write(&config_path, json).unwrap();
let result = run_simulation(&config_path, None, false).unwrap();
// The simulation should proceed without erroring at config/finalize/variable-insertion stage.
// It may not converge (isolated single-port component) but must not produce a
// fan_speed-related or bounded-variable insertion error.
if let Some(ref err) = result.error {
assert!(
!err.to_lowercase().contains("bounded"),
"CLI must not error on bounded-variable insertion (Task 4.3): {}",
err
);
assert!(
!err.to_lowercase().contains("fan_speed"),
"CLI must not error on fan_speed variable creation (Task 4.3): {}",
err
);
}
}