Entropyk/docs/tutorial/04-building-systems.md

309 lines
8.2 KiB
Markdown

# 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_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