309 lines
8.2 KiB
Markdown
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 |