# 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`) - **Edges** are flow connections carrying state (P, h) - Each edge has two state vector indices: `state_index_p` and `state_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) ```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) ```rust 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: ```rust 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): ```rust let edge = system.add_edge(source, target)?; ``` ### Connection with Port Validation For real components with defined ports: ```rust // 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: ```rust // 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 ```rust 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:** ```rust match system.finalize() { Ok(()) => println!("System ready. State vector: {} elements", system.state_vector_len()), Err(e) => eprintln!("Topology error: {:?}", e), } ``` `finalize()` performs: 1. Assigns state indices to all edges 2. Validates topology (no disconnected components, proper port connections) 3. Sets up internal data structures for solving ## Complete Example: Simple Cycle ```rust 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 ```python 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](./05-solver-configuration.md) - Configure Newton, Picard, and Fallback solvers - [Components Reference](./03-components.md) - Detailed component documentation