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

@@ -150,8 +150,9 @@ fn execute_simulation(
let mut system = System::new();
// Track component name -> node index mapping per circuit
let mut component_indices: HashMap<String, petgraph::graph::NodeIndex> = HashMap::new();
// Track component name -> (node index, component type) mapping per circuit
// The component type is needed for port-name-to-index resolution (Task 3.3)
let mut component_indices: HashMap<String, (petgraph::graph::NodeIndex, String)> = HashMap::new();
// Collect variables and constraints to add *after* components are added
struct PendingControl {
@@ -200,7 +201,10 @@ fn execute_simulation(
match create_component(&component_config, &fluid_id, Arc::clone(&backend)) {
Ok(component) => match system.add_component_to_circuit(component, circuit_id) {
Ok(node_id) => {
component_indices.insert(component_config.name.clone(), node_id);
component_indices.insert(
component_config.name.clone(),
(node_id, component_config.component_type.clone()),
);
// Check if this component needs explicit fan control
if let Some(fan_control) = component_config
@@ -238,6 +242,9 @@ fn execute_simulation(
});
}
}
// Register component name for constraint validation
system.register_component_name(&component_config.name, node_id);
}
Err(e) => {
return SimulationResult {
@@ -274,39 +281,61 @@ fn execute_simulation(
}
}
// Add edges between components
// NOTE: Port specifications (e.g., "component:port_name") are parsed but currently ignored.
// Components are treated as simple nodes without port-level routing.
// Multi-port components like ScrewEconomizerCompressor have all ports created,
// but the topology system doesn't yet support port-specific edge connections.
// See Story 12-3 Task 3.3 for port-aware edge implementation.
// Add edges between components (Task 3.3: port-aware edge routing)
// Port specifications (e.g., "screw_0:economizer") are resolved to port indices.
// For components with ports, add_edge_with_ports() is used to allow multi-port routing.
// Unknown port names default to index 0 (inlet) or 1 (outlet) with a warning.
for circuit_config in &config.circuits {
for edge in &circuit_config.edges {
let from_parts: Vec<&str> = edge.from.split(':').collect();
let to_parts: Vec<&str> = edge.to.split(':').collect();
let from_name = from_parts.get(0).unwrap_or(&"");
let to_name = to_parts.get(0).unwrap_or(&"");
let from_name = from_parts.get(0).copied().unwrap_or("");
let to_name = to_parts.get(0).copied().unwrap_or("");
let from_port_name = from_parts.get(1).copied();
let to_port_name = to_parts.get(1).copied();
let from_node = component_indices.get(*from_name);
let to_node = component_indices.get(*to_name);
let from_entry = component_indices.get(from_name);
let to_entry = component_indices.get(to_name);
match (from_node, to_node) {
(Some(from), Some(to)) => {
if let Err(e) = system.add_edge(*from, *to) {
return SimulationResult {
input: input_name.to_string(),
status: SimulationStatus::Error,
convergence: None,
iterations: None,
state: None,
performance: None,
error: Some(format!(
"Failed to add edge '{} -> {}': {:?}",
edge.from, edge.to, e
)),
elapsed_ms,
};
match (from_entry, to_entry) {
(Some((from_node, from_type)), Some((to_node, to_type))) => {
// Resolve port names to indices for port-aware routing
let from_port_idx = from_port_name
.map(|p| resolve_port_index(from_type, p, true))
.unwrap_or(1); // default: outlet = port 1
let to_port_idx = to_port_name
.map(|p| resolve_port_index(to_type, p, false))
.unwrap_or(0); // default: inlet = port 0
let add_result = system
.add_edge_with_ports(*from_node, from_port_idx, *to_node, to_port_idx)
.map_err(|e| format!("{:?}", e));
if let Err(e) = add_result {
// Fallback: try without port validation if port counts don't match
// (allows portless components like Placeholder to connect freely)
tracing::warn!(
from = %edge.from,
to = %edge.to,
error = %e,
"add_edge_with_ports failed — falling back to portless add_edge"
);
if let Err(fallback_err) = system.add_edge(*from_node, *to_node) {
return SimulationResult {
input: input_name.to_string(),
status: SimulationStatus::Error,
convergence: None,
iterations: None,
state: None,
performance: None,
error: Some(format!(
"Failed to add edge '{} -> {}': {} (fallback: {:?})",
edge.from, edge.to, e, fallback_err
)),
elapsed_ms,
};
}
}
}
_ => {
@@ -367,38 +396,24 @@ fn execute_simulation(
for control in pending_controls {
if control.control_type == "fan_speed" {
use entropyk_solver::inverse::{
BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintId,
BoundedVariable, BoundedVariableId,
};
// Generate unique IDs
let var_id =
BoundedVariableId::new(format!("fan_speed_var_{}", control.component_node.index()));
let cons_id =
ConstraintId::new(format!("fan_speed_cons_{}", control.component_node.index()));
// Find the component's generated name to use in ComponentOutput
// Find the component's generated name to use in BoundedVariable
let mut comp_name = String::new();
for (name, node) in &component_indices {
for (name, (node, _)) in &component_indices {
if *node == control.component_node {
comp_name = name.clone();
break;
}
}
// In the MCHX MVP, we want the fan speed itself to be a DOFs.
// Wait, bounded variable links to a constraint. A constraint targets an output.
// If the user wants to control CAPACITY by varying FAN SPEED...
// Let's check config to see what output they want to control.
// Actually, AC says: "Paramètre fan_control: "bounded" (crée une BoundedVariable avec Constraint)"
// Let's implement this generically if they provided target parameters.
let target = 0.0; // Needs to come from config, but config parsing doesn't provide constraint target yet.
// Story says: "Si oui, on crée une BoundedVariable..." but then "Constraint".
// If we don't have the constraint target in ComponentConfig, we can't fully wire it up just for fan speed without knowing what it controls (e.g. pressure or capacity).
// Let's log a warning for now and wait for full control loop config in a future story, or just add the variable.
let var = BoundedVariable::with_component(
var_id.clone(),
var_id,
&comp_name,
control.initial,
control.min,
@@ -477,6 +492,58 @@ fn execute_simulation(
}
}
/// Resolves a port name string to a port index for the given component type.
///
/// This enables named port connections in the edge JSON config, e.g.:
/// ```json
/// { "from": "screw_0:discharge", "to": "mchx_0:inlet" }
/// ```
///
/// Port index conventions:
/// - `ScrewEconomizerCompressor`: suction=0, discharge=1, economizer=2
/// - All other components: inlet=0, outlet=1
///
/// `is_source` is true when resolving the "from" side of an edge (outlet type),
/// false when resolving the "to" side (inlet type). This affects the default fallback.
fn resolve_port_index(component_type: &str, port_name: &str, is_source: bool) -> usize {
let port_lower = port_name.to_lowercase();
match component_type {
"ScrewEconomizerCompressor" | "ScrewCompressor" => match port_lower.as_str() {
"suction" | "inlet" | "in" => 0,
"discharge" | "outlet" | "out" => 1,
"economizer" | "eco" | "economiser" | "flash_in" => 2,
_ => {
tracing::warn!(
port_name,
component_type,
"Unknown port name for ScrewEconomizerCompressor, defaulting to {}",
if is_source { 1 } else { 0 }
);
if is_source { 1 } else { 0 }
}
},
_ => {
// Default: inlet=0, outlet=1 for all 2-port components
match port_lower.as_str() {
"inlet" | "in" | "suction" | "cold_in" | "hot_in" | "refrigerant_in"
| "flash_in" => 0,
"outlet" | "out" | "discharge" | "cold_out" | "hot_out" | "refrigerant_out"
| "flash_out" => 1,
_ => {
tracing::warn!(
port_name,
component_type,
"Unknown port name, defaulting to {}",
if is_source { 1 } else { 0 }
);
if is_source { 1 } else { 0 }
}
}
}
}
}
fn get_param_f64(
params: &std::collections::HashMap<String, serde_json::Value>,
key: &str,
@@ -557,7 +624,7 @@ fn create_component(
match component_type {
// ── NEW: ScrewEconomizerCompressor ─────────────────────────────────────
"ScrewEconomizerCompressor" | "ScrewCompressor" => {
use entropyk::{MchxCondenserCoil, Polynomial2D, ScrewEconomizerCompressor, ScrewPerformanceCurves};
use entropyk::{Polynomial2D, ScrewEconomizerCompressor, ScrewPerformanceCurves};
let fluid = params
.get("fluid")
@@ -575,22 +642,57 @@ fn create_component(
.unwrap_or(0.92);
// Economizer fraction (default 12%)
let eco_frac = params
let eco_frac_param = params
.get("economizer_fraction")
.and_then(|v| v.as_f64())
.unwrap_or(0.12);
.and_then(|v| v.as_f64());
// Task 3.4: Built-in manufacturer curve presets.
// Presets set default polynomial coefficients; explicit params override them.
// Available: "bitzer_generic_200kw", "grasso_generic_200kw"
let preset = params.get("preset").and_then(|v| v.as_str()).unwrap_or("");
let (
preset_mf_a00,
preset_mf_a10,
preset_mf_a01,
preset_mf_a11,
preset_pw_b00,
preset_pw_b10,
preset_pw_b01,
preset_pw_b11,
preset_eco_frac,
) = match preset {
"bitzer_generic_200kw" => {
// Bitzer screw ~200 kW R134a, SST=-5..+10°C, SDT=+35..+55°C
// ṁ_suc [kg/s] ≈ 1.35 + 0.004·SST - 0.0025·SDT + 0.000012·SST·SDT
// W_shaft [W] ≈ 58000 + 180·SST - 280·SDT + 0.4·SST·SDT
(1.35_f64, 0.004, -0.0025, 0.000_012, 58_000.0, 180.0, -280.0, 0.4, 0.13)
}
"grasso_generic_200kw" => {
// Grasso screw ~200 kW R134a, SST=-5..+10°C, SDT=+35..+55°C
// Similar range, slightly different power curve
(1.30_f64, 0.0035, -0.0022, 0.000_010, 60_000.0, 190.0, -310.0, 0.45, 0.11)
}
_ => {
// Default values (no preset)
(1.2_f64, 0.003, -0.002, 1e-5, 55_000.0, 200.0, -300.0, 0.5, 0.12)
}
};
// Mass-flow polynomial coefficients (bilinear SST/SDT)
let mf_a00 = params.get("mf_a00").and_then(|v| v.as_f64()).unwrap_or(1.2);
let mf_a10 = params.get("mf_a10").and_then(|v| v.as_f64()).unwrap_or(0.003);
let mf_a01 = params.get("mf_a01").and_then(|v| v.as_f64()).unwrap_or(-0.002);
let mf_a11 = params.get("mf_a11").and_then(|v| v.as_f64()).unwrap_or(1e-5);
// Explicit params override preset defaults
let mf_a00 = params.get("mf_a00").and_then(|v| v.as_f64()).unwrap_or(preset_mf_a00);
let mf_a10 = params.get("mf_a10").and_then(|v| v.as_f64()).unwrap_or(preset_mf_a10);
let mf_a01 = params.get("mf_a01").and_then(|v| v.as_f64()).unwrap_or(preset_mf_a01);
let mf_a11 = params.get("mf_a11").and_then(|v| v.as_f64()).unwrap_or(preset_mf_a11);
// Power polynomial coefficients (bilinear)
let pw_b00 = params.get("pw_b00").and_then(|v| v.as_f64()).unwrap_or(55_000.0);
let pw_b10 = params.get("pw_b10").and_then(|v| v.as_f64()).unwrap_or(200.0);
let pw_b01 = params.get("pw_b01").and_then(|v| v.as_f64()).unwrap_or(-300.0);
let pw_b11 = params.get("pw_b11").and_then(|v| v.as_f64()).unwrap_or(0.5);
let pw_b00 = params.get("pw_b00").and_then(|v| v.as_f64()).unwrap_or(preset_pw_b00);
let pw_b10 = params.get("pw_b10").and_then(|v| v.as_f64()).unwrap_or(preset_pw_b10);
let pw_b01 = params.get("pw_b01").and_then(|v| v.as_f64()).unwrap_or(preset_pw_b01);
let pw_b11 = params.get("pw_b11").and_then(|v| v.as_f64()).unwrap_or(preset_pw_b11);
// Use preset eco fraction if not specified explicitly
let eco_frac = eco_frac_param.unwrap_or(preset_eco_frac);
let curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
Polynomial2D::bilinear(mf_a00, mf_a10, mf_a01, mf_a11),
@@ -873,6 +975,7 @@ impl fmt::Debug for SimpleComponent {
}
#[derive(Debug, Clone)]
#[allow(dead_code)] // fields retained for documentation & future physical residuals
struct PyCompressor {
fluid: FluidsFluidId,
speed_rpm: f64,
@@ -972,6 +1075,7 @@ impl entropyk::Component for PyCompressor {
}
#[derive(Debug, Clone)]
#[allow(dead_code)] // fields retained for documentation & future physical residuals
struct PyExpansionValve {
fluid: FluidsFluidId,
opening: f64,