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

@@ -2,114 +2,372 @@
//!
//! Tests cover:
//! - Round-trip serialization (system → JSON → system)
//! - Topology preservation (nodes, edges, component types)
//! - Constraint and bounded variable preservation
//! - Thermal coupling preservation
//! - Version compatibility checks
//! - Backend validation
//! - File save/load round-trip
//! - Human-readable JSON format
use entropyk_components::{Compressor, FluidId, Port};
use entropyk_core::{Enthalpy, Pressure};
use entropyk_solver::System;
use entropyk_core::{CircuitId, Enthalpy, Pressure, ThermalConductance};
use entropyk_solver::{System, ThermalCoupling};
use serde_json::{json, Value};
#[test]
fn test_simple_system_round_trip() {
// Create a simple system with one component
/// Helper: create a minimal system with a single compressor component.
fn build_single_compressor_system() -> System {
let mut system = System::new();
// Create compressor with Ahri540 coefficients
let coefficients = entropyk_components::Ahri540Coefficients::new(
0.85, // m1
2.5, // m2
500.0, // m3
1500.0, // m4
-2.5, // m5
1.8, // m6
600.0, // m7
1600.0, // m8
-3.0, // m9
2.0, // m10
0.85, 2.5, 500.0, 1500.0, -2.5, 1.8, 600.0, 1600.0, -3.0, 2.0,
);
// Create disconnected ports
let port_suction = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(400000.0),
);
let port_discharge = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
);
// Create disconnected compressor
let disconnected_compressor = Compressor::new(
let disconnected = Compressor::new(
coefficients,
port_suction,
port_discharge,
2900.0, // speed_rpm
0.0001, // displacement_m3_per_rev
0.85, // mechanical_efficiency
).expect("Failed to create compressor");
2900.0,
0.0001,
0.85,
)
.expect("Failed to create compressor");
// Connect the ports (this converts to Compressor<Connected>)
let suction_port = Port::new(
let connected = disconnected
.connect(
Port::new(
FluidId::new("R134a"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(400000.0),
),
Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
),
)
.expect("Failed to connect compressor");
let node = system.add_component(Box::new(connected));
system.register_component_name("compressor", node);
system
}
/// Helper: create a system with two components and an edge between them,
/// plus a thermal coupling.
fn build_two_component_system() -> System {
let mut system = System::new();
let coefficients = entropyk_components::Ahri540Coefficients::new(
0.85, 2.5, 500.0, 1500.0, -2.5, 1.8, 600.0, 1600.0, -3.0, 2.0,
);
// Create compressor
let port_s = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(400000.0),
);
let discharge_port = Port::new(
let port_d = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
);
let comp = Compressor::new(coefficients, port_s, port_d, 2900.0, 0.0001, 0.85)
.expect("create compressor")
.connect(
Port::new(
FluidId::new("R134a"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(400000.0),
),
Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
),
)
.expect("connect compressor");
let connected_compressor = disconnected_compressor
.connect(suction_port, discharge_port)
.expect("Failed to connect compressor");
let node_comp = system.add_component(Box::new(comp));
system.register_component_name("compressor", node_comp);
// Add to system as Box<dyn Component>
system.add_component(Box::new(connected_compressor));
// Create a second compressor (acting as condenser proxy)
let coefficients2 = entropyk_components::Ahri540Coefficients::new(
0.9, 3.0, 600.0, 1400.0, -1.5, 2.0, 700.0, 1700.0, -2.0, 1.5,
);
let port_s2 = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
);
let port_d2 = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(8.0),
Enthalpy::from_joules_per_kg(420000.0),
);
let comp2 = Compressor::new(coefficients2, port_s2, port_d2, 2900.0, 0.00012, 0.88)
.expect("create comp2")
.connect(
Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
),
Port::new(
FluidId::new("R134a"),
Pressure::from_bar(8.0),
Enthalpy::from_joules_per_kg(420000.0),
),
)
.expect("connect comp2");
// Test to_json_string and from_json_string
let node_comp2 = system.add_component(Box::new(comp2));
system.register_component_name("condenser", node_comp2);
// Add edge between them
system.add_edge(node_comp, node_comp2).expect("add edge");
// Add thermal coupling
let coupling = ThermalCoupling::new(
CircuitId(0),
CircuitId(0),
ThermalConductance::from_watts_per_kelvin(500.0),
);
let _ = system.add_thermal_coupling(coupling);
system
}
// ────────────────────────────────────────────────────────────────────────
// Test 1: Topology round-trip
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_topology_round_trip() {
let original = build_two_component_system();
let json_str = original.to_json_string().expect("Serialization failed");
let restored = System::from_json_string(&json_str).expect("Deserialization failed");
// Verify topology is identical
assert_eq!(
original.node_count(),
restored.node_count(),
"Node count mismatch"
);
assert_eq!(
original.edge_count(),
restored.edge_count(),
"Edge count mismatch"
);
assert_eq!(
original.thermal_coupling_count(),
restored.thermal_coupling_count(),
"Thermal coupling count mismatch"
);
// Verify component names are preserved (order-independent since deserialization sorts keys)
let mut original_names: Vec<&str> = original.registered_component_names().collect();
let mut restored_names: Vec<&str> = restored.registered_component_names().collect();
original_names.sort();
restored_names.sort();
assert_eq!(original_names, restored_names, "Component names mismatch");
// Verify component types via the JSON snapshot
let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");
let params = parsed.get("parameters").expect("parameters field");
assert!(params.get("compressor").is_some(), "compressor in params");
assert!(params.get("condenser").is_some(), "condenser in params");
}
// ────────────────────────────────────────────────────────────────────────
// Test 3: Constraints preservation
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_constraints_preserved_in_round_trip() {
use entropyk_solver::inverse::{ComponentOutput, Constraint, ConstraintId};
let mut system = build_single_compressor_system();
// Add a constraint referencing the compressor
let constraint = Constraint::new(
ConstraintId::new("superheat_ctrl"),
ComponentOutput::Superheat {
component_id: "compressor".to_string(),
},
5.0,
);
system.add_constraint(constraint).expect("add constraint");
assert_eq!(system.constraint_count(), 1);
// Serialize
let json_str = system.to_json_string().expect("Serialization failed");
// Verify JSON is valid and human-readable
let parsed: Value = serde_json::from_str(&json_str).expect("JSON parsing failed");
assert!(parsed.is_object());
assert!(parsed.get("version").is_some());
assert_eq!(parsed["version"], "1.0");
// Verify constraints are in the JSON
let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");
let constraints = parsed.get("constraints").expect("constraints field");
assert!(constraints.is_array());
assert_eq!(constraints.as_array().unwrap().len(), 1);
// Deserialize
let restored_system = System::from_json_string(&json_str).expect("Deserialization failed");
let c = &constraints.as_array().unwrap()[0];
assert_eq!(c["id"], "superheat_ctrl");
assert_eq!(c["component"], "compressor");
assert_eq!(c["target"], 5.0);
// Verify the system is reconstructed
// (Full component reconstruction will be implemented in future tasks)
assert!(true);
// Verify the constraint snapshot round-trips through serde
let snapshot: entropyk_solver::SystemSnapshot =
serde_json::from_str(&json_str).expect("snapshot parse");
assert_eq!(snapshot.constraints.len(), 1);
assert_eq!(snapshot.constraints[0].id, "superheat_ctrl");
assert_eq!(snapshot.constraints[0].component, "compressor");
assert!((snapshot.constraints[0].target - 5.0).abs() < 1e-12);
}
// ────────────────────────────────────────────────────────────────────────
// Test 4: Thermal couplings preservation
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_thermal_couplings_preserved_in_round_trip() {
let original = build_two_component_system();
let json_str = original.to_json_string().expect("Serialization failed");
// Verify thermal couplings in JSON
let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");
let couplings = parsed
.get("topology")
.and_then(|t| t.get("thermalCouplings"))
.expect("thermal couplings in topology");
assert!(couplings.is_array());
assert_eq!(couplings.as_array().unwrap().len(), 1);
let c = &couplings.as_array().unwrap()[0];
assert_eq!(c["hotCircuit"], 0);
assert_eq!(c["coldCircuit"], 0);
// Verify the snapshot round-trips
let snapshot: entropyk_solver::SystemSnapshot =
serde_json::from_str(&json_str).expect("snapshot parse");
assert_eq!(snapshot.topology.thermal_couplings.len(), 1);
assert_eq!(snapshot.topology.thermal_couplings[0].hot_circuit, CircuitId(0));
assert_eq!(snapshot.topology.thermal_couplings[0].cold_circuit, CircuitId(0));
// Verify ua value round-trip
let ua_val = snapshot.topology.thermal_couplings[0].ua.to_watts_per_kelvin();
assert!((ua_val - 500.0).abs() < 1e-6, "UA value mismatch: {}", ua_val);
}
// ────────────────────────────────────────────────────────────────────────
// Test 5: File save/load round-trip
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_file_save_and_load() {
let system = build_two_component_system();
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("entropyk_test_round_trip.json");
// Save
system.save_json(&file_path).expect("Save failed");
assert!(file_path.exists());
// Load
let loaded = System::load_json(&file_path).expect("Load failed");
// Verify topology matches
assert_eq!(system.node_count(), loaded.node_count());
assert_eq!(system.edge_count(), loaded.edge_count());
assert_eq!(
system.thermal_coupling_count(),
loaded.thermal_coupling_count()
);
// Clean up
std::fs::remove_file(&file_path).ok();
}
// ────────────────────────────────────────────────────────────────────────
// Test 6: Missing backend error
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_missing_backend_returns_error() {
// AC5: missing backend must produce an explicit error
let json_with_unknown_backend = json!({
"version": "1.0",
"topology": {
"edges": [],
"thermalCouplings": []
},
"parameters": {},
"fluidBackend": {
"name": "NonExistentBackend",
"version": "99.0.0"
}
})
.to_string();
let result = System::from_json_string(&json_with_unknown_backend);
assert!(result.is_err(), "Should fail with BackendUnavailable for unknown backend");
}
// ────────────────────────────────────────────────────────────────────────
// Test 7: Version mismatch error
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_version_mismatch() {
let json_with_wrong_version = json!({
"version": "999.0", // Incompatible version
"version": "99.0",
"topology": {
"edges": [],
"thermal_couplings": []
"thermalCouplings": []
},
"parameters": {},
"fluid_backend": {
"fluidBackend": {
"name": "TestBackend",
"version": "1.0.0",
"hash": "abc123"
}
}).to_string();
})
.to_string();
let result = System::from_json_string(&json_with_wrong_version);
assert!(result.is_err());
// Just verify it's an error - don't try to unwrap
assert!(true);
assert!(result.is_err(), "Should fail with version mismatch");
}
// ────────────────────────────────────────────────────────────────────────
// Additional: JSON human-readable and deterministic
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_simple_system_round_trip() {
let system = build_single_compressor_system();
let json_str = system.to_json_string().expect("Serialization failed");
let parsed: Value = serde_json::from_str(&json_str).expect("JSON parsing failed");
assert!(parsed.is_object());
assert_eq!(parsed["version"], "1.0");
// Single isolated component should fail finalize during deserialization
let result = System::from_json_string(&json_str);
assert!(result.is_err(), "Isolated node should fail deserialization");
}
#[test]
@@ -117,43 +375,60 @@ fn test_json_is_human_readable() {
let system = System::new();
let json_str = system.to_json_string().expect("Serialization failed");
// Check that JSON is pretty-printed (contains newlines and indentation)
assert!(json_str.contains('\n'));
assert!(json_str.contains(" ")); // Indentation
assert!(json_str.contains(" "));
// Verify it's valid JSON
let _: Value = serde_json::from_str(&json_str).expect("Should be valid JSON");
}
#[test]
fn test_deterministic_serialization() {
let system = System::new();
// Note: HashMap-based fields (parameters, ComponentParams) may produce
// different key ordering across serializations, so we compare parsed
// JSON values rather than raw strings.
let system = build_single_compressor_system();
let json1 = system.to_json_string().expect("Serialization failed");
let json2 = system.to_json_string().expect("Serialization failed");
// Same system should produce same JSON
assert_eq!(json1, json2);
let val1: Value = serde_json::from_str(&json1).expect("parse json1");
let val2: Value = serde_json::from_str(&json2).expect("parse json2");
assert_eq!(val1, val2, "Same system should produce identical JSON (structurally)");
}
// ────────────────────────────────────────────────────────────────────────
// Test: Bounded variables in snapshot
// ────────────────────────────────────────────────────────────────────────
#[test]
fn test_file_save_and_load() {
let system = System::new();
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("test_system.json");
fn test_bounded_variables_in_snapshot() {
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId};
// Save to file
system.save_json(&file_path).expect("Save failed");
let mut system = build_single_compressor_system();
// Verify file exists
assert!(file_path.exists());
let valve =
BoundedVariable::with_component(BoundedVariableId::new("valve"), "compressor", 0.5, 0.0, 1.0)
.expect("create bounded var");
system.add_bounded_variable(valve).expect("add bounded var");
// Load from file
let _loaded_system = System::load_json(&file_path).expect("Load failed");
let json_str = system.to_json_string().expect("Serialization failed");
let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");
// Clean up
std::fs::remove_file(&file_path).ok();
let bounded = parsed.get("boundedVariables").expect("boundedVariables field");
assert!(bounded.is_array());
assert_eq!(bounded.as_array().unwrap().len(), 1);
// Verify system is reconstructed
assert!(true);
let bv = &bounded.as_array().unwrap()[0];
assert_eq!(bv["id"], "valve");
assert_eq!(bv["component"], "compressor");
assert!((bv["initialValue"].as_f64().unwrap() - 0.5).abs() < 1e-12);
// Verify snapshot round-trip
let snapshot: entropyk_solver::SystemSnapshot =
serde_json::from_str(&json_str).expect("snapshot parse");
assert_eq!(snapshot.bounded_variables.len(), 1);
assert_eq!(snapshot.bounded_variables[0].id, "valve");
assert_eq!(snapshot.bounded_variables[0].component, "compressor");
assert!((snapshot.bounded_variables[0].initial_value - 0.5).abs() < 1e-12);
assert!((snapshot.bounded_variables[0].lower_bound - 0.0).abs() < 1e-12);
assert!((snapshot.bounded_variables[0].upper_bound - 1.0).abs() < 1e-12);
}