chore: remove deprecated flow_boundary and update docs to match new architecture

This commit is contained in:
Sepehr
2026-03-01 20:00:09 +01:00
parent 20700afce8
commit d88914a44f
105 changed files with 11222 additions and 2994 deletions

View File

@@ -127,25 +127,117 @@ fn execute_simulation(
use std::collections::HashMap;
let fluid_id = FluidId::new(&config.fluid);
let backend: Arc<dyn entropyk_fluids::FluidBackend> = Arc::new(TestBackend::new());
let backend: Arc<dyn entropyk_fluids::FluidBackend> = match config.fluid_backend.as_deref() {
Some("CoolProp") => Arc::new(entropyk_fluids::CoolPropBackend::new()),
Some("Test") | None => Arc::new(TestBackend::new()),
Some(other) => {
return SimulationResult {
input: input_name.to_string(),
status: SimulationStatus::Error,
convergence: None,
iterations: None,
state: None,
performance: None,
error: Some(format!(
"Unknown fluid backend: '{}'. Supported: 'CoolProp', 'Test'",
other
)),
elapsed_ms,
};
}
};
let mut system = System::new();
// Track component name -> node index mapping per circuit
let mut component_indices: HashMap<String, petgraph::graph::NodeIndex> = HashMap::new();
for circuit_config in &config.circuits {
let circuit_id = CircuitId(circuit_config.id as u8);
// Collect variables and constraints to add *after* components are added
struct PendingControl {
component_node: petgraph::graph::NodeIndex,
control_type: String,
min: f64,
max: f64,
initial: f64,
}
let mut pending_controls = Vec::new();
for circuit_config in &config.circuits {
let circuit_id = CircuitId(circuit_config.id as u16);
// Pre-process components to expand banks
let mut expanded_components = Vec::new();
for component_config in &circuit_config.components {
match create_component(
&component_config.component_type,
&component_config.params,
&fluid_id,
Arc::clone(&backend),
) {
if let Some(bank_config) = &component_config.condenser_bank {
// Expand MCHX condenser bank into multiple coils
for c in 0..bank_config.circuits {
for i in 0..bank_config.coils_per_circuit {
let mut expanded = component_config.clone();
// Clear the bank config to avoid infinite recursion logically
expanded.condenser_bank = None;
// Set the specific coil index
let coil_index = c * bank_config.coils_per_circuit + i;
expanded.params.insert(
"coil_index".to_string(),
serde_json::Value::Number(coil_index.into()),
);
// Modify the name (e.g., mchx_0a, mchx_0b for circuit 0, coils a, b)
let letter = (b'a' + (i as u8)) as char;
expanded.name = format!("{}_{}{}", component_config.name, c, letter);
expanded_components.push(expanded);
}
}
} else {
expanded_components.push(component_config.clone());
}
}
for component_config in &expanded_components {
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);
// Check if this component needs explicit fan control
if let Some(fan_control) = component_config
.params
.get("fan_control")
.and_then(|v| v.as_str())
{
if fan_control == "bounded" {
let min = component_config
.params
.get("fan_speed_min")
.and_then(|v| v.as_f64())
.unwrap_or(0.1);
let max = component_config
.params
.get("fan_speed_max")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
let initial = component_config
.fan_speed
.or_else(|| {
component_config
.params
.get("fan_speed")
.and_then(|v| v.as_f64())
})
.unwrap_or(1.0);
pending_controls.push(PendingControl {
component_node: node_id,
control_type: "fan_speed".to_string(),
min,
max,
initial,
});
}
}
}
Err(e) => {
return SimulationResult {
@@ -183,6 +275,11 @@ 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.
for circuit_config in &config.circuits {
for edge in &circuit_config.edges {
let from_parts: Vec<&str> = edge.from.split(':').collect();
@@ -233,8 +330,8 @@ fn execute_simulation(
for coupling_config in &config.thermal_couplings {
let coupling = ThermalCoupling::new(
CircuitId(coupling_config.hot_circuit as u8),
CircuitId(coupling_config.cold_circuit as u8),
CircuitId(coupling_config.hot_circuit as u16),
CircuitId(coupling_config.cold_circuit as u16),
ThermalConductance::from_watts_per_kelvin(coupling_config.ua),
)
.with_efficiency(coupling_config.efficiency);
@@ -266,6 +363,56 @@ fn execute_simulation(
};
}
// Add variables and constraints
for control in pending_controls {
if control.control_type == "fan_speed" {
use entropyk_solver::inverse::{
BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintId,
};
// 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
let mut comp_name = String::new();
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(),
&comp_name,
control.initial,
control.min,
control.max,
);
if let Ok(var) = var {
if let Err(e) = system.add_bounded_variable(var) {
tracing::warn!("Failed to add fan_speed variable: {:?}", e);
}
}
}
}
let result = match config.solver.strategy.as_str() {
"newton" => {
let mut strategy = SolverStrategy::NewtonRaphson(NewtonConfig::default());
@@ -305,16 +452,28 @@ fn execute_simulation(
elapsed_ms,
}
}
Err(e) => SimulationResult {
input: input_name.to_string(),
status: SimulationStatus::Error,
convergence: None,
iterations: None,
state: None,
performance: None,
error: Some(format!("Solver error: {:?}", e)),
elapsed_ms,
},
Err(e) => {
let e_str = format!("{:?}", e);
let error_msg = if e_str.contains("FluidError")
|| e_str.contains("backend")
|| e_str.contains("CoolProp")
{
format!("Thermodynamic/Fluid error: {}", e_str)
} else {
format!("Solver error: {}", e_str)
};
SimulationResult {
input: input_name.to_string(),
status: SimulationStatus::Error,
convergence: None,
iterations: None,
state: None,
performance: None,
error: Some(error_msg),
elapsed_ms,
}
}
}
}
@@ -364,17 +523,174 @@ fn parse_side_conditions(
)?)
}
/// Creates a pair of connected ports for components that need them (screw, MCHX, fan...).
///
/// Ports are initialised at the given pressure and enthalpy. Both ports are connected
/// to each other — the first port is returned as the `ConnectedPort`.
fn make_connected_port(fluid: &str, p_bar: f64, h_kj_kg: f64) -> entropyk::ConnectedPort {
use entropyk::{ComponentFluidId, Enthalpy, Port, Pressure};
let a = Port::new(
ComponentFluidId::new(fluid),
Pressure::from_bar(p_bar),
Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
);
let b = Port::new(
ComponentFluidId::new(fluid),
Pressure::from_bar(p_bar),
Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
);
a.connect(b).expect("port connection ok").0
}
/// Create a component from configuration.
fn create_component(
component_type: &str,
params: &std::collections::HashMap<String, serde_json::Value>,
component_config: &crate::config::ComponentConfig,
_primary_fluid: &entropyk::FluidId,
backend: Arc<dyn entropyk_fluids::FluidBackend>,
) -> CliResult<Box<dyn entropyk::Component>> {
use entropyk::{Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger};
use entropyk_components::heat_exchanger::{FlowConfiguration, LmtdModel};
let params = &component_config.params;
let component_type = component_config.component_type.as_str();
match component_type {
// ── NEW: ScrewEconomizerCompressor ─────────────────────────────────────
"ScrewEconomizerCompressor" | "ScrewCompressor" => {
use entropyk::{MchxCondenserCoil, Polynomial2D, ScrewEconomizerCompressor, ScrewPerformanceCurves};
let fluid = params
.get("fluid")
.and_then(|v| v.as_str())
.unwrap_or_else(|| _primary_fluid.as_str());
let nominal_freq = params
.get("nominal_frequency_hz")
.and_then(|v| v.as_f64())
.unwrap_or(50.0);
let eta_mech = params
.get("mechanical_efficiency")
.and_then(|v| v.as_f64())
.unwrap_or(0.92);
// Economizer fraction (default 12%)
let eco_frac = params
.get("economizer_fraction")
.and_then(|v| v.as_f64())
.unwrap_or(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);
// 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 curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
Polynomial2D::bilinear(mf_a00, mf_a10, mf_a01, mf_a11),
Polynomial2D::bilinear(pw_b00, pw_b10, pw_b01, pw_b11),
eco_frac,
);
// Initial port conditions — use typical chiller values as defaults
let p_suc = params.get("p_suction_bar").and_then(|v| v.as_f64()).unwrap_or(3.2);
let h_suc = params.get("h_suction_kj_kg").and_then(|v| v.as_f64()).unwrap_or(400.0);
let p_dis = params.get("p_discharge_bar").and_then(|v| v.as_f64()).unwrap_or(12.8);
let h_dis = params.get("h_discharge_kj_kg").and_then(|v| v.as_f64()).unwrap_or(440.0);
let p_eco = params.get("p_eco_bar").and_then(|v| v.as_f64()).unwrap_or(6.4);
let h_eco = params.get("h_eco_kj_kg").and_then(|v| v.as_f64()).unwrap_or(260.0);
let port_suc = make_connected_port(fluid, p_suc, h_suc);
let port_dis = make_connected_port(fluid, p_dis, h_dis);
let port_eco = make_connected_port(fluid, p_eco, h_eco);
let mut comp = ScrewEconomizerCompressor::new(
curves,
fluid,
nominal_freq,
eta_mech,
port_suc,
port_dis,
port_eco,
)
.map_err(|e| CliError::Component(e))?;
if let Some(freq_hz) = params.get("frequency_hz").and_then(|v| v.as_f64()) {
comp.set_frequency_hz(freq_hz)
.map_err(|e| CliError::Component(e))?;
}
Ok(Box::new(comp))
}
// ── NEW: MchxCondenserCoil ─────────────────────────────────────────────
"MchxCondenserCoil" | "MchxCoil" => {
use entropyk::MchxCondenserCoil;
// Optional explicit field vs fallback to params for backward compatibility
let ua_kw_k = component_config.ua_nominal_kw_k.or_else(|| {
params.get("ua_nominal_kw_k").and_then(|v| v.as_f64())
}).unwrap_or(15.0); // Safe fallback 15 kW/K
let ua_w_k = ua_kw_k * 1000.0;
let n_air = component_config.n_air_exponent.or_else(|| {
params.get("n_air_exponent").and_then(|v| v.as_f64())
}).unwrap_or(0.5); // ASHRAE louvered-fin default
let coil_index = params
.get("coil_index")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let t_air_c = component_config.air_inlet_temp_c.or_else(|| {
params.get("air_inlet_temp_c").and_then(|v| v.as_f64())
}).unwrap_or(35.0);
let fan_speed = component_config.fan_speed.or_else(|| {
params.get("fan_speed").and_then(|v| v.as_f64())
}).unwrap_or(1.0);
let mut coil = MchxCondenserCoil::new(ua_w_k, n_air, coil_index);
coil.set_air_temperature_celsius(t_air_c);
coil.set_fan_speed_ratio(fan_speed);
Ok(Box::new(coil))
}
// ── NEW: FloodedEvaporator ─────────────────────────────────────────────
"FloodedEvaporator" => {
use entropyk::FloodedEvaporator;
let ua = get_param_f64(params, "ua")?;
let target_quality = params
.get("target_quality")
.and_then(|v| v.as_f64())
.unwrap_or(0.7);
let refrigerant = params
.get("refrigerant")
.and_then(|v| v.as_str())
.unwrap_or_else(|| _primary_fluid.as_str());
let secondary_fluid = params
.get("secondary_fluid")
.and_then(|v| v.as_str())
.unwrap_or("MEG");
let evap = FloodedEvaporator::new(ua)
.with_target_quality(target_quality)
.with_refrigerant(refrigerant)
.with_secondary_fluid(secondary_fluid)
.with_fluid_backend(Arc::clone(&backend));
Ok(Box::new(evap))
}
"Condenser" | "CondenserCoil" => {
let ua = get_param_f64(params, "ua")?;
let t_sat_k = params.get("t_sat_k").and_then(|v| v.as_f64());
@@ -468,7 +784,7 @@ fn create_component(
}
_ => Err(CliError::Config(format!(
"Unknown component type: '{}'. Supported: Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, Pump, Placeholder",
"Unknown component type: '{}'. Supported: ScrewEconomizerCompressor, MchxCondenserCoil, FloodedEvaporator, Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, Pump, Placeholder",
component_type
))),
}
@@ -516,7 +832,7 @@ impl SimpleComponent {
impl entropyk::Component for SimpleComponent {
fn compute_residuals(
&self,
state: &entropyk::SystemState,
state: &[f64],
residuals: &mut entropyk::ResidualVector,
) -> Result<(), entropyk::ComponentError> {
for i in 0..self.n_eqs.min(residuals.len()) {
@@ -531,7 +847,7 @@ impl entropyk::Component for SimpleComponent {
fn jacobian_entries(
&self,
_state: &entropyk::SystemState,
_state: &[f64],
jacobian: &mut entropyk::JacobianBuilder,
) -> Result<(), entropyk::ComponentError> {
for i in 0..self.n_eqs {
@@ -624,7 +940,7 @@ impl PyCompressor {
impl entropyk::Component for PyCompressor {
fn compute_residuals(
&self,
state: &entropyk::SystemState,
state: &[f64],
residuals: &mut entropyk::ResidualVector,
) -> Result<(), entropyk::ComponentError> {
for r in residuals.iter_mut() {
@@ -639,7 +955,7 @@ impl entropyk::Component for PyCompressor {
fn jacobian_entries(
&self,
_state: &entropyk::SystemState,
_state: &[f64],
jacobian: &mut entropyk::JacobianBuilder,
) -> Result<(), entropyk::ComponentError> {
jacobian.add_entry(0, 0, 1.0);
@@ -673,7 +989,7 @@ impl PyExpansionValve {
impl entropyk::Component for PyExpansionValve {
fn compute_residuals(
&self,
state: &entropyk::SystemState,
state: &[f64],
residuals: &mut entropyk::ResidualVector,
) -> Result<(), entropyk::ComponentError> {
for r in residuals.iter_mut() {
@@ -687,7 +1003,7 @@ impl entropyk::Component for PyExpansionValve {
fn jacobian_entries(
&self,
_state: &entropyk::SystemState,
_state: &[f64],
jacobian: &mut entropyk::JacobianBuilder,
) -> Result<(), entropyk::ComponentError> {
jacobian.add_entry(0, 0, 1.0);