# 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