8.2 KiB
Building Systems
This guide explains how to create thermodynamic systems by connecting components into a directed graph.
System Architecture
A System is a directed graph where:
- Nodes are components (
Box<dyn Component>) - Edges are flow connections carrying state (P, h)
- Each edge has two state vector indices:
state_index_pandstate_index_h
┌─────────────────────────────────────────────────────────────────┐
│ System (Graph) │
│ │
│ NodeIndex(0) ──EdgeIndex(0)──► NodeIndex(1) ──EdgeIndex(1)──► │
│ [Compressor] [Condenser] │
│ │
│ State Vector: [P_edge0, h_edge0, P_edge1, h_edge1, ...] │
└─────────────────────────────────────────────────────────────────┘
Creating a System
Basic Example (Rust)
use entropyk_solver::System;
use entropyk_components::{Component, ComponentError, ConnectedPort,
JacobianBuilder, ResidualVector, SystemState};
// Create custom component (or use library components)
struct MySource {
outlet_pressure: f64,
outlet_enthalpy: f64,
}
impl Component for MySource {
fn compute_residuals(&self, state: &SystemState, residuals: &mut ResidualVector)
-> Result<(), ComponentError> {
// Set outlet pressure and enthalpy
residuals[0] = state.edge_pressure(0) - self.outlet_pressure;
residuals[1] = state.edge_enthalpy(0) - self.outlet_enthalpy;
Ok(())
}
fn jacobian_entries(&self, state: &SystemState, jacobian: &mut JacobianBuilder)
-> Result<(), ComponentError> {
jacobian.add_entry(0, state.pressure_col(0), 1.0);
jacobian.add_entry(1, state.enthalpy_col(0), 1.0);
Ok(())
}
fn n_equations(&self) -> usize { 2 }
fn get_ports(&self) -> &[ConnectedPort] { &[] }
}
fn main() {
let mut system = System::new();
// Add components
let source = system.add_component(Box::new(MySource {
outlet_pressure: 1e6, // 10 bar
outlet_enthalpy: 400e3, // 400 kJ/kg
}));
// Add more components...
// let sink = system.add_component(Box::new(MySink { ... }));
// Connect components
// system.add_edge(source, sink).unwrap();
// Finalize topology
system.finalize().unwrap();
println!("State vector length: {}", system.state_vector_len());
}
Adding Components
Single Circuit (Default)
let mut system = System::new();
// Add to default circuit (circuit 0)
let n1 = system.add_component(Box::new(compressor));
let n2 = system.add_component(Box::new(condenser));
Multi-Circuit Systems
For machines with multiple independent refrigerant circuits:
use entropyk_solver::CircuitId;
let mut system = System::new();
// Circuit 0 (primary)
let c1 = system.add_component_to_circuit(
Box::new(compressor1),
CircuitId::ZERO
).unwrap();
// Circuit 1 (secondary)
let c2 = system.add_component_to_circuit(
Box::new(compressor2),
CircuitId::new(1).unwrap()
).unwrap();
Limits: Maximum 5 circuits per machine (CircuitId 0-4).
Connecting Components
Simple Connection (No Port Validation)
For components without ports (e.g., test mocks):
let edge = system.add_edge(source, target)?;
Connection with Port Validation
For real components with defined ports:
// Connect source outlet (port 1) to target inlet (port 0)
let edge = system.add_edge_with_ports(
source, 1, // source node, outlet port
target, 0, // target node, inlet port
)?;
Port validation checks:
- Circuit compatibility: Both nodes must be in the same circuit
- Fluid compatibility: Same refrigerant on both sides
- Pressure continuity: Pressure tolerance check
- Enthalpy continuity: Enthalpy tolerance check
Cross-Circuit Connections
Not allowed for flow edges. Use thermal coupling instead:
// This will FAIL:
system.add_edge(node_in_circuit_0, node_in_circuit_1)?;
// Error: TopologyError::CrossCircuitConnection
State Vector Layout
After finalize(), the state vector is organized as:
Index: 0 1 2 3 4 5 ...
Value: [P_e0, h_e0, P_e1, h_e1, P_e2, h_e2, ...]
Each edge contributes 2 state variables:
state_index_p: Pressure (Pa)state_index_h: Specific enthalpy (J/kg)
Accessing State in Components
impl Component for MyComponent {
fn compute_residuals(&self, state: &SystemState, residuals: &mut ResidualVector)
-> Result<(), ComponentError> {
// Get inlet state (edge 0)
let p_in = state.edge_pressure(0);
let h_in = state.edge_enthalpy(0);
// Get outlet state (edge 1)
let p_out = state.edge_pressure(1);
let h_out = state.edge_enthalpy(1);
// Compute residuals...
residuals[0] = p_out - p_in + pressure_drop;
Ok(())
}
}
Finalizing the System
Always call finalize() before solving:
match system.finalize() {
Ok(()) => println!("System ready. State vector: {} elements", system.state_vector_len()),
Err(e) => eprintln!("Topology error: {:?}", e),
}
finalize() performs:
- Assigns state indices to all edges
- Validates topology (no disconnected components, proper port connections)
- Sets up internal data structures for solving
Complete Example: Simple Cycle
use entropyk_solver::System;
fn build_simple_cycle() -> System {
let mut system = System::new();
// Add components
let compressor = system.add_component(Box::new(Compressor::new(
2900.0, // RPM
0.0001, // displacement m³
0.85, // efficiency
"R134a".to_string(),
)));
let condenser = system.add_component(Box::new(Condenser::new(
5000.0, // UA W/K
)));
let valve = system.add_component(Box::new(ExpansionValve::new(
"R134a".to_string(),
0.8, // opening
)));
let evaporator = system.add_component(Box::new(Evaporator::new(
3000.0, // UA W/K
)));
// Connect in cycle order
system.add_edge(compressor, condenser).unwrap();
system.add_edge(condenser, valve).unwrap();
system.add_edge(valve, evaporator).unwrap();
system.add_edge(evaporator, compressor).unwrap();
// Finalize
system.finalize().unwrap();
system
}
Python Bindings
import entropyk
system = entropyk.System()
# Add components
c = system.add_component(entropyk.Compressor(
speed_rpm=2900.0,
displacement=0.0001,
efficiency=0.85,
fluid="R134a"
))
d = system.add_component(entropyk.Condenser(ua=5000.0))
v = system.add_component(entropyk.ExpansionValve(fluid="R134a", opening=0.8))
e = system.add_component(entropyk.Evaporator(ua=3000.0))
# Connect
system.add_edge(c, d)
system.add_edge(d, v)
system.add_edge(v, e)
system.add_edge(e, c)
# Finalize
system.finalize()
print(f"State vector: {system.state_vector_len} elements")
Note: Python bindings use placeholder adapters. The topology works, but the solver won't converge because there are no real physics equations.
Common Errors
Cross-Circuit Connection
TopologyError::CrossCircuitConnection { source_circuit: 0, target_circuit: 1 }
Fix: Use thermal coupling for cross-circuit heat transfer, or move components to the same circuit.
Disconnected Components
TopologyError::DisconnectedComponent { node: NodeIndex(2) }
Fix: Ensure all components are connected to the cycle.
Port Mismatch
ConnectionError::FluidMismatch { source: "R134a", target: "R717" }
Fix: Use the same refrigerant throughout a circuit.
Next Steps
- Solver Configuration - Configure Newton, Picard, and Fallback solvers
- Components Reference - Detailed component documentation