chore: sync project state and current artifacts

This commit is contained in:
Sepehr
2026-02-22 23:27:31 +01:00
parent 1b6415776e
commit dd77089b22
232 changed files with 37056 additions and 4296 deletions

View File

@@ -0,0 +1,225 @@
# Getting Started with Entropyk
This guide will help you set up your environment and understand the core concepts of thermodynamic simulation with Entropyk.
## Prerequisites
- **Rust**: Install the latest stable version via [rustup](https://rustup.rs/)
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update stable
```
- **Python** (optional): For Python bindings, Python 3.8+ is required
```bash
pip install maturin
```
## Installation
### Clone and Build
```bash
git clone https://github.com/your-username/Entropyk.git
cd Entropyk
cargo build --workspace
```
### Run Tests
```bash
cargo test --workspace
```
### Build Documentation
```bash
cargo doc --workspace --open
```
## Project Structure
```
Entropyk/
├── crates/
│ ├── core/ # Core types (Pressure, Temperature, Enthalpy, MassFlow, Calib)
│ ├── components/ # Thermodynamic components (Compressor, HeatExchanger, etc.)
│ ├── solver/ # Solver strategies (Newton, Picard, Fallback)
│ └── entropyk/ # Unified API crate
├── bindings/
│ ├── python/ # Python bindings
│ └── wasm/ # WebAssembly bindings
├── demo/ # Example applications
└── docs/ # Documentation
```
## Current Status
**Important**: The Python bindings currently use placeholder adapters (`SimpleAdapter`) that:
- Accept component parameters
- Build system topology correctly
- Return zero residuals (no actual physics equations)
This means the solver will not converge because there are no real thermodynamic equations to solve. The Python bindings are useful for learning the API structure and testing topology, but not for actual simulations.
## Core Concepts
### 1. System Topology
Entropyk models thermodynamic systems as a **directed graph**:
- **Nodes**: Components (Compressor, Heat Exchanger, Valve, etc.)
- **Edges**: Fluid connections between components
Each edge carries state variables: **Pressure (P)** and **Enthalpy (h)**.
### 2. Component Categories
| Category | Purpose | Components |
|----------|---------|------------|
| **Active** | Add work to fluid | Compressor, Pump, Fan |
| **Passive** | Pressure drop, no work | Pipe, ExpansionValve |
| **Heat Transfer** | Exchange heat between fluids | Evaporator, Condenser, Economizer |
| **Boundary** | Fixed conditions | FlowSource, FlowSink |
| **Junction** | Flow splitting/merging | FlowSplitter, FlowMerger |
### 3. Heat Exchanger Architecture
**Important**: Heat exchangers in Entropyk have **4 ports** (not 2):
```
┌─────────────────────┐
Hot Inlet ──────►│ │──────► Hot Outlet
│ Heat Exchanger │
Cold Inlet ──────►│ │──────► Cold Outlet
└─────────────────────┘
```
- **Evaporator**: Refrigerant (cold side) evaporates, absorbing heat from hot side (water/air)
- **Condenser**: Refrigerant (hot side) condenses, releasing heat to cold side (water/air)
### 4. Boundary Conditions
Every fluid circuit needs boundary conditions:
- **FlowSource**: Imposes fixed P and h at circuit inlet
- **FlowSink**: Imposes back-pressure at circuit outlet
## Running the Demo
The `macro-chiller` demo shows the system architecture:
```bash
cargo run --bin macro-chiller
```
This demonstrates:
- Two chillers in parallel (MacroComponent pattern)
- Hierarchical system composition
- JSON snapshot serialization
**Note**: The demo uses `LinearComponent` placeholders, so the solver shows "NonConvergence" - this is expected because placeholders don't implement real thermodynamic equations.
## Working Rust Example
Here's a working example using the test infrastructure:
```rust
use entropyk_solver::System;
use entropyk_components::{Component, ComponentError, ConnectedPort,
JacobianBuilder, ResidualVector, SystemState};
/// A simple component that returns zero residuals (for testing topology)
struct TestComponent {
n_eq: usize,
}
impl Component for TestComponent {
fn compute_residuals(
&self,
_state: &SystemState,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut().take(self.n_eq) {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &SystemState,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
for i in 0..self.n_eq {
jacobian.add_entry(i, i, 1.0);
}
Ok(())
}
fn n_equations(&self) -> usize {
self.n_eq
}
fn get_ports(&self) -> &[ConnectedPort] {
&[]
}
}
fn main() {
let mut system = System::new();
// Add components
let comp = system.add_component(Box::new(TestComponent { n_eq: 2 }));
let cond = system.add_component(Box::new(TestComponent { n_eq: 3 }));
let valve = system.add_component(Box::new(TestComponent { n_eq: 1 }));
let evap = system.add_component(Box::new(TestComponent { n_eq: 3 }));
// Connect in cycle
system.add_edge(comp, cond).unwrap();
system.add_edge(cond, valve).unwrap();
system.add_edge(valve, evap).unwrap();
system.add_edge(evap, comp).unwrap();
// Finalize
system.finalize().unwrap();
println!("System created with {} state variables", system.state_vector_len());
}
```
## Python Bindings Status
The Python bindings are useful for:
- Learning the API structure
- Building system topology
- Testing component creation
```python
import entropyk
# Create components (placeholders)
compressor = entropyk.Compressor(
speed_rpm=2900.0,
displacement=0.0001,
efficiency=0.85,
fluid="R134a",
)
# Build system topology
system = entropyk.System()
comp_idx = system.add_component(compressor)
# ... add more components
# Finalize
system.finalize()
print(f"State vector length: {system.state_vector_len}")
```
**Note**: The Python bindings use `SimpleAdapter` placeholders. They accept parameters but don't implement real thermodynamic equations. The solver will not converge because there are no actual physics equations to solve.
## Next Steps
- Learn about [Physical Types](./02-physical-types.md) for type-safe unit handling
- Explore [Components](./03-components.md) for all available thermodynamic components
- See [Solver Configuration](./05-solver-configuration.md) for convergence tuning
- Check the `demo/` directory for working Rust examples
- Read the `crates/solver/tests/` directory for integration test examples

View File

@@ -0,0 +1,203 @@
# Physical Types
Entropyk uses the **NewType pattern** for compile-time unit safety. This prevents accidental mixing of units (e.g., adding Pascals to bar) at compile time.
## Overview
All physical types store values in **SI base units** internally:
| Type | Internal Unit | Display Unit |
|------|---------------|--------------|
| `Pressure` | Pascals (Pa) | Pa, bar, PSI |
| `Temperature` | Kelvin (K) | K, °C, °F |
| `Enthalpy` | J/kg | J/kg, kJ/kg |
| `MassFlow` | kg/s | kg/s, g/s |
| `Power` | Watts (W) | W, kW, MW |
## Pressure
```rust
use entropyk_core::Pressure;
// Create from different units
let p1 = Pressure::from_pascals(101_325.0); // 1 atm
let p2 = Pressure::from_bar(1.0); // 1 bar = 100,000 Pa
let p3 = Pressure::from_psi(14.7); // ~1 atm
// Convert to different units
println!("{} Pa", p1.to_pascals()); // 101325.0
println!("{} bar", p1.to_bar()); // 1.01325
println!("{} PSI", p1.to_psi()); // ~14.7
// Arithmetic (same type)
let p_sum = p1 + p2;
let p_diff = p1 - p2;
let p_double = p1 * 2.0;
let p_half = p1 / 2.0;
// Comparison
assert!(p1 > p2); // PartialOrd implemented
```
### Common Pressure Values
| Description | Value |
|-------------|-------|
| Atmospheric pressure | 101,325 Pa (1.01325 bar) |
| Low-pressure refrigerant (R134a evaporating at -10°C) | ~200 kPa (2 bar) |
| High-pressure refrigerant (R134a condensing at 40°C) | ~1,000 kPa (10 bar) |
## Temperature
```rust
use entropyk_core::Temperature;
// Create from different units
let t1 = Temperature::from_kelvin(273.15); // 0°C
let t2 = Temperature::from_celsius(25.0); // 25°C = 298.15 K
let t3 = Temperature::from_fahrenheit(32.0); // 0°C = 273.15 K
// Convert to different units
println!("{} K", t2.to_kelvin()); // 298.15
println!("{} °C", t2.to_celsius()); // 25.0
println!("{} °F", t2.to_fahrenheit()); // 77.0
// Arithmetic
let t_sum = t1 + Temperature::from_kelvin(10.0);
let t_diff = t2 - t1; // 25 K difference
```
### Thermodynamic Reference Points
| Description | Kelvin | Celsius |
|-------------|--------|---------|
| Absolute zero | 0 K | -273.15°C |
| Water freezing point | 273.15 K | 0°C |
| Room temperature | 293.15 K | 20°C |
| Water boiling point (1 atm) | 373.15 K | 100°C |
## Enthalpy
Specific enthalpy (energy per unit mass) is the primary state variable in thermodynamic cycles.
```rust
use entropyk_core::Enthalpy;
// Create from different units
let h1 = Enthalpy::from_joules_per_kg(400_000.0); // 400 kJ/kg
let h2 = Enthalpy::from_kilojoules_per_kg(400.0); // Same value
// Convert
println!("{} J/kg", h1.to_joules_per_kg()); // 400000.0
println!("{} kJ/kg", h1.to_kilojoules_per_kg()); // 400.0
// Arithmetic
let h_diff = h2 - h1; // Enthalpy difference (e.g., for work/heat)
```
### Typical Enthalpy Values for R134a
| State | Enthalpy (kJ/kg) |
|-------|------------------|
| Saturated liquid at 0°C | ~200 |
| Saturated vapor at 0°C | ~400 |
| Superheated vapor at 40°C, 1.2 MPa | ~420 |
| Subcooled liquid at 40°C, 1.0 MPa | ~256 |
## MassFlow
Mass flow rate is critical for energy balance calculations.
```rust
use entropyk_core::MassFlow;
// Create from different units
let m1 = MassFlow::from_kg_per_s(0.1); // 100 g/s
let m2 = MassFlow::from_grams_per_s(100.0); // Same value
// Convert
println!("{} kg/s", m1.to_kg_per_s()); // 0.1
println!("{} g/s", m1.to_grams_per_s()); // 100.0
// Zero-flow regularization (IMPORTANT for solver stability)
let zero_flow = MassFlow::from_kg_per_s(0.0);
let safe_flow = zero_flow.regularized(); // Returns at least 1e-12 kg/s
// Use regularized flow in denominators
let enthalpy_per_mass = Enthalpy::from_joules_per_kg(1000.0);
let power = enthalpy_per_mass * safe_flow; // Won't cause NaN
```
### Zero-Flow Regularization
When a component is off (zero mass flow), dividing by mass flow would cause NaN or Inf. The `regularized()` method clamps the flow to at least `1e-12 kg/s`:
```rust
// BAD: Can cause NaN when flow is zero
let specific_power = power / flow.to_kg_per_s();
// GOOD: Safe for zero-flow conditions
let specific_power = power / flow.regularized().to_kg_per_s();
```
## Power
```rust
use entropyk_core::Power;
// Create from different units
let p1 = Power::from_watts(1000.0); // 1 kW
let p2 = Power::from_kilowatts(1.0); // Same value
let p3 = Power::from_megawatts(0.001); // Same value
// Convert
println!("{} W", p1.to_watts()); // 1000.0
println!("{} kW", p1.to_kilowatts()); // 1.0
println!("{} MW", p1.to_megawatts()); // 0.001
```
## Type Safety Benefits
The NewType pattern catches unit errors at compile time:
```rust
// This WON'T COMPILE - cannot add Pressure to Temperature
let p = Pressure::from_bar(1.0);
let t = Temperature::from_celsius(25.0);
// let bad = p + t; // Error: mismatched types
// This WORKS - same types
let p1 = Pressure::from_bar(1.0);
let p2 = Pressure::from_bar(2.0);
let p_sum = p1 + p2; // OK: Pressure + Pressure = Pressure
```
## Python Bindings
In Python, the types are exposed as simple classes:
```python
import entropyk
# Create types
p = entropyk.Pressure.from_bar(1.0)
t = entropyk.Temperature.from_celsius(25.0)
h = entropyk.Enthalpy.from_kilojoules_per_kg(400.0)
m = entropyk.MassFlow.from_kg_per_s(0.1)
# Convert
print(p.to_pascals()) # 100000.0
print(t.to_kelvin()) # 298.15
```
## Best Practices
1. **Always use the type constructors** - Never pass raw `f64` values for physical quantities
2. **Use regularized mass flow** - When dividing by mass flow, always use `.regularized()`
3. **Document units in comments** - When interfacing with external APIs, note the expected units
4. **Convert at boundaries** - Convert to/from SI units at system boundaries (user input, file I/O)
## Next Steps
- [Components Reference](./03-components.md) - See how types are used in component parameters
- [Building Systems](./04-building-systems.md) - Connect components with type-safe ports

View File

@@ -0,0 +1,412 @@
# Components Reference
This document describes all available thermodynamic components in Entropyk, their equations, and usage examples.
## Component Categories
| Category | Purpose | Components |
|----------|---------|------------|
| **Active** | Add work to fluid | Compressor, Pump, Fan |
| **Passive** | Pressure drop, no work | Pipe, ExpansionValve |
| **Heat Transfer** | Exchange heat between fluids | Evaporator, Condenser, Economizer, HeatExchanger |
| **Boundary** | Fixed conditions | FlowSource, FlowSink |
| **Junction** | Flow splitting/merging | FlowSplitter, FlowMerger |
---
## Active Components
### Compressor
A positive displacement compressor using the AHRI 540 performance model.
**Equations (2)**:
- Mass flow: `ṁ = M1 × (1 - (P_suc/P_dis)^(1/M2)) × ρ_suc × V_disp × N/60`
- Power: `Ẇ = M3 + M4 × (P_dis/P_suc) + M5 × T_suc + M6 × T_dis`
**Ports**: 2 (inlet, outlet)
```rust
use entropyk_components::compressor::{Compressor, Ahri540Coefficients};
use entropyk_components::port::{FluidId, Port};
use entropyk_core::{Pressure, Enthalpy};
// Create with AHRI 540 coefficients
let coeffs = Ahri540Coefficients::new(
0.85, 2.5, // M1, M2 (flow)
500.0, 1500.0, -2.5, 1.8, // M3-M6 (cooling power)
600.0, 1600.0, -3.0, 2.0 // M7-M10 (heating power)
);
// Create disconnected ports
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(3.5),
Enthalpy::from_kj_per_kg(405.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(12.0),
Enthalpy::from_kj_per_kg(440.0),
);
// Build compressor
let compressor = Compressor::new(
coeffs,
2900.0, // speed_rpm
0.0001, // displacement m³/rev
inlet,
outlet,
)?;
```
**Calibration**: `f_m` (mass flow), `f_power` (power), `f_etav` (volumetric efficiency)
---
### Pump
A centrifugal pump for incompressible fluids (water, brine, glycol).
**Equations (2)**:
- Pressure rise: `ΔP = f_head × ρ × g × H_curve(Q)`
- Power: `Ẇ = ρ × g × Q × H / η`
**Ports**: 2 (inlet, outlet)
```rust
use entropyk_components::pump::Pump;
use entropyk_components::port::{FluidId, Port};
use entropyk_core::{Pressure, Enthalpy};
let inlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(1.0),
Enthalpy::from_kj_per_kg(63.0), // ~15°C
);
let outlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(3.0),
Enthalpy::from_kj_per_kg(63.0),
);
let pump = Pump::new(
"Chilled Water Pump",
inlet,
outlet,
0.5, // design flow rate kg/s
200.0, // design head kPa
)?;
```
---
### Fan
An air-moving device for condenser cooling or ventilation.
**Equations (2)**:
- Pressure rise: `ΔP = fan_curve(Q, N)`
- Power: `Ẇ = Q × ΔP / η`
**Ports**: 2 (inlet, outlet)
```rust
use entropyk_components::fan::Fan;
let fan = Fan::new(
"Condenser Fan",
inlet_port,
outlet_port,
2.0, // design flow m³/s
150.0, // design static pressure Pa
)?;
```
---
## Passive Components
### Pipe
A fluid transport pipe with Darcy-Weisbach pressure drop.
**Equations (1)**:
- Pressure drop: `ΔP = f × (L/D) × (ρ × v² / 2)`
**Ports**: 2 (inlet, outlet)
```rust
use entropyk_components::pipe::{Pipe, PipeGeometry, roughness};
let geometry = PipeGeometry::new(
10.0, // length m
0.025, // inner diameter m
roughness::SMOOTH, // roughness m
)?;
let pipe = Pipe::for_incompressible(
geometry,
inlet_port,
outlet_port,
998.0, // density kg/m³
0.001, // viscosity Pa·s
)?;
```
---
### Expansion Valve
A thermostatic or electronic expansion valve for refrigerant systems.
**Equations (1)**:
- Mass flow: `ṁ = C_v × opening × √(ρ × ΔP)`
**Ports**: 2 (inlet, outlet)
```rust
use entropyk_components::expansion_valve::ExpansionValve;
let valve = ExpansionValve::new(
FluidId::new("R134a"),
0.8, // opening (0-1)
inlet_port,
outlet_port,
)?;
```
---
## Heat Transfer Components
### Heat Exchanger Architecture
All heat exchangers have **4 ports**:
```
Hot Side:
Inlet ─────►┌──────────┐──────► Outlet
│ HX │
Cold Side: │ │
Inlet ─────►└──────────┘──────► Outlet
```
| Component | Hot Side | Cold Side | Model |
|-----------|----------|-----------|-------|
| Condenser | Refrigerant (condensing) | Water/Air | LMTD |
| Evaporator | Water/Air | Refrigerant (evaporating) | ε-NTU |
| Economizer | Hot refrigerant | Cold refrigerant | ε-NTU |
---
### Condenser
A heat exchanger where refrigerant condenses from vapor to liquid.
**Equations (3)**:
- Hot side energy: `Q_hot = ṁ_hot × (h_in - h_out)`
- Cold side energy: `Q_cold = ṁ_cold × Cp × (T_out - T_in)`
- Heat transfer: `Q = UA × LMTD`
**Ports**: 4 (hot_in, hot_out, cold_in, cold_out)
```rust
use entropyk_components::heat_exchanger::Condenser;
let condenser = Condenser::new(10_000.0); // UA = 10 kW/K
// Set saturation temperature for condensation
condenser.set_saturation_temp(323.15); // 50°C
// Calibration for matching real data
condenser.set_calib(Calib { f_ua: 1.1, ..Default::default() });
```
**Thermodynamic Notes**:
- Refrigerant enters as superheated vapor
- Refrigerant exits as subcooled liquid
- Typical subcooling: 3-10 K
---
### Evaporator
A heat exchanger where refrigerant evaporates from liquid to vapor.
**Equations (3)**:
- Hot side energy: `Q_hot = ṁ_hot × Cp × (T_in - T_out)`
- Cold side energy: `Q_cold = ṁ_cold × (h_out - h_in)`
- Heat transfer: `Q = ε × ṁ_cold × Cp_cold × (T_hot_in - T_cold_in)`
**Ports**: 4 (hot_in, hot_out, cold_in, cold_out)
```rust
use entropyk_components::heat_exchanger::Evaporator;
let evaporator = Evaporator::new(8_000.0); // UA = 8 kW/K
// Set saturation temperature and superheat target
evaporator.set_saturation_temp(278.15); // 5°C
evaporator.set_superheat_target(5.0); // 5 K superheat
```
**Thermodynamic Notes**:
- Refrigerant enters as two-phase mixture (after expansion valve)
- Refrigerant exits as superheated vapor
- Typical superheat: 5-10 K
---
### Economizer
A heat exchanger for subcooling liquid refrigerant using expanded vapor.
**Equations (3)**: Same as generic heat exchanger
**Ports**: 4 (hot_in, hot_out, cold_in, cold_out)
```rust
use entropyk_components::heat_exchanger::Economizer;
let economizer = Economizer::new(2_000.0); // UA = 2 kW/K
```
---
## Boundary Components
### FlowSource
Imposes fixed pressure and enthalpy at circuit inlet.
**Equations (2)**:
- `r_P = P_edge - P_set = 0`
- `r_h = h_edge - h_set = 0`
**Ports**: 1 (outlet only)
```rust
use entropyk_components::flow_boundary::FlowSource;
// City water supply: 3 bar, 15°C
let source = FlowSource::incompressible(
"Water",
3.0e5, // pressure Pa
63_000.0, // enthalpy J/kg (~15°C)
connected_port,
)?;
// Refrigerant reservoir: 12 bar, 40°C subcooled
let source = FlowSource::compressible(
"R134a",
12.0e5, // pressure Pa
250_000.0, // enthalpy J/kg (subcooled liquid)
connected_port,
)?;
```
---
### FlowSink
Imposes back-pressure at circuit outlet.
**Equations (1-2)**:
- `r_P = P_edge - P_back = 0`
- Optional: `r_h = h_edge - h_back = 0`
**Ports**: 1 (inlet only)
```rust
use entropyk_components::flow_boundary::FlowSink;
// Return header with back-pressure
let sink = FlowSink::incompressible(
"Water",
1.5e5, // back-pressure Pa
None, // no fixed enthalpy
connected_port,
)?;
// With fixed return temperature
let sink = FlowSink::incompressible(
"Water",
1.5e5,
Some(84_000.0), // fixed enthalpy (~20°C)
connected_port,
)?;
```
---
## Junction Components
### FlowSplitter
Divides one inlet flow into multiple outlets.
**Equations (N_outlets)**:
- Mass conservation: `ṁ_in = Σ ṁ_out`
- Pressure equality at all outlets
**Ports**: 1 inlet + N outlets
```rust
use entropyk_components::flow_junction::FlowSplitter;
let splitter = FlowSplitter::new(
"Chilled Water Splitter",
inlet_port,
vec![outlet1, outlet2, outlet3],
)?;
```
---
### FlowMerger
Combines multiple inlet flows into one outlet.
**Equations (N_inlets)**:
- Mass conservation: `Σ ṁ_in = ṁ_out`
- Enthalpy mixing: `h_out = Σ(ṁ_in × h_in) / ṁ_out`
**Ports**: N inlets + 1 outlet
```rust
use entropyk_components::flow_junction::FlowMerger;
let merger = FlowMerger::new(
"Return Water Merger",
vec![inlet1, inlet2, inlet3],
outlet_port,
)?;
```
---
## Component Summary Table
| Component | Equations | Ports | Calibration |
|-----------|-----------|-------|-------------|
| Compressor | 2 | 2 | f_m, f_power, f_etav |
| Pump | 2 | 2 | f_m, f_power |
| Fan | 2 | 2 | f_m, f_power |
| Pipe | 1 | 2 | f_dp |
| ExpansionValve | 1 | 2 | f_m |
| Condenser | 3 | 4 | f_ua, f_dp |
| Evaporator | 3 | 4 | f_ua, f_dp |
| Economizer | 3 | 4 | f_ua, f_dp |
| FlowSource | 2 | 1 | - |
| FlowSink | 1-2 | 1 | - |
| FlowSplitter | N | 1+N | - |
| FlowMerger | N | N+1 | - |
---
## Next Steps
- See [Building Systems](./04-building-systems.md) for connecting components
- Learn about [Refrigeration Cycles](./06-refrigeration-cycles.md) for complete examples
- Explore [Calibration](./10-calibration.md) for matching real data

View File

@@ -0,0 +1,309 @@
# 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

View File

@@ -0,0 +1,306 @@
# Solver Configuration
Entropyk provides multiple solver strategies for thermodynamic system simulation.
## Overview
| Solver | Convergence | Use Case |
|--------|-------------|----------|
| **Newton-Raphson** | Quadratic | Well-conditioned systems with good initial guess |
| **Picard** | Linear | Strongly nonlinear systems, more robust |
| **Fallback** | Adaptive | Automatic Newton→Picard fallback on divergence |
## Newton-Raphson Solver
The Newton-Raphson method solves F(x) = 0 by iteratively computing:
```
x_{n+1} = x_n - J^{-1} * F(x_n)
```
Where J is the Jacobian matrix of partial derivatives.
### Configuration
```rust
use entropyk_solver::{NewtonConfig, NewtonSolver, ConvergenceCriteria};
let config = NewtonConfig {
max_iterations: 100,
tolerance: 1e-6,
relaxation: 1.0, // Damping factor (0 < α ≤ 1)
..Default::default()
};
let solver = NewtonSolver::new(config);
```
### With Convergence Criteria
```rust
let criteria = ConvergenceCriteria {
pressure_tolerance: 100.0, // Pa
enthalpy_tolerance: 1000.0, // J/kg
residual_tolerance: 1e-6,
..Default::default()
};
let solver = NewtonSolver::new(NewtonConfig::default())
.with_convergence_criteria(criteria);
```
### With Initial State
```rust
// Provide initial guess for state vector
let initial_state = vec![
1e6, // P_edge0 (Pa)
400e3, // h_edge0 (J/kg)
0.9e6, // P_edge1 (Pa)
250e3, // h_edge1 (J/kg)
];
let solver = NewtonSolver::new(NewtonConfig::default())
.with_initial_state(initial_state);
```
### Jacobian Freezing
For performance, the Jacobian can be reused across iterations:
```rust
use entropyk_solver::JacobianFreezingConfig;
let freeze_config = JacobianFreezingConfig {
max_frozen_iterations: 5, // Reuse J for up to 5 iterations
recompute_on_divergence: true,
};
let solver = NewtonSolver::new(NewtonConfig {
jacobian_freezing: Some(freeze_config),
..Default::default()
});
```
## Picard Solver
The Picard (fixed-point) iteration is more robust for strongly nonlinear systems:
```
x_{n+1} = G(x_n)
```
### Configuration
```rust
use entropyk_solver::{PicardConfig, PicardSolver};
let config = PicardConfig {
max_iterations: 200,
tolerance: 1e-5,
relaxation: 0.8, // Under-relaxation for stability
..Default::default()
};
let solver = PicardSolver::new(config);
```
### When to Use Picard
- **Phase change problems**: Evaporation/condensation with sharp property changes
- **Poor initial guess**: When Newton diverges
- **Near singularities**: Close to critical points
## Fallback Solver
The Fallback solver automatically switches from Newton to Picard on divergence:
```rust
use entropyk_solver::{FallbackConfig, FallbackSolver};
let config = FallbackConfig {
newton_max_iterations: 50,
picard_max_iterations: 100,
switch_on_newton_divergence: true,
..Default::default()
};
let solver = FallbackSolver::new(config);
```
### Fallback Behavior
1. Start with Newton-Raphson
2. If Newton diverges (residual increases), switch to Picard
3. If Picard converges, optionally try Newton again for faster convergence
## Convergence Criteria
### Basic Tolerance
```rust
let criteria = ConvergenceCriteria {
pressure_tolerance: 100.0, // |ΔP| < 100 Pa
enthalpy_tolerance: 1000.0, // |Δh| < 1000 J/kg
residual_tolerance: 1e-6, // ||F(x)|| < 1e-6
max_iterations: 100,
};
```
### Per-Circuit Convergence
For multi-circuit systems, convergence is tracked per circuit:
```rust
// After solving, check the convergence report
if let Some(report) = converged_state.convergence_report() {
for circuit in &report.circuits {
println!(
"Circuit {}: converged={}, max_ΔP={:.1} Pa, max_Δh={:.1} J/kg",
circuit.circuit_id,
circuit.converged,
circuit.max_pressure_change,
circuit.max_enthalpy_change
);
}
}
```
## Smart Initialization
Good initial guess is critical for convergence. Use `SmartInitializer`:
```rust
use entropyk_solver::{SmartInitializer, InitializerConfig};
let config = InitializerConfig {
fluid: "R134a".to_string(),
source_temperature_celsius: 40.0, // Condensing temperature
sink_temperature_celsius: 5.0, // Evaporating temperature
superheat_kelvin: 5.0,
subcool_kelvin: 3.0,
};
let initializer = SmartInitializer::new(config);
// Estimate saturation pressures
let (p_high, p_low) = initializer.estimate_pressures(
40.0, // Source temperature (°C)
5.0, // Sink temperature (°C)
);
// Populate state vector
let state = initializer.populate_state(&system, 40.0, 5.0)?;
```
## Running the Solver
### Basic Solve
```rust
use entropyk_solver::{System, NewtonSolver, NewtonConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut system = build_system();
system.finalize()?;
let solver = NewtonSolver::new(NewtonConfig::default());
let result = solver.solve(&system)?;
println!("Converged in {} iterations", result.iterations);
println!("Final state: {:?}", result.state);
Ok(())
}
```
### With Timeout
```rust
use entropyk_solver::TimeoutConfig;
let config = NewtonConfig {
timeout: Some(TimeoutConfig {
max_time_seconds: 10.0,
return_best_on_timeout: true,
}),
..Default::default()
};
```
## Error Handling
```rust
use entropyk_solver::SolverError;
match solver.solve(&system) {
Ok(result) => {
println!("Converged: {:?}", result.status);
}
Err(SolverError::MaxIterationsExceeded { iterations, residual }) => {
eprintln!("Failed to converge after {} iterations", iterations);
eprintln!("Final residual: {}", residual);
}
Err(SolverError::JacobianSingular { condition_number }) => {
eprintln!("Jacobian is singular (condition number: {})", condition_number);
eprintln!("Check system topology and initial guess");
}
Err(e) => {
eprintln!("Solver error: {:?}", e);
}
}
```
## Solver Strategy Selection
```rust
use entropyk_solver::SolverStrategy;
let strategy = SolverStrategy::Fallback; // Recommended for production
match strategy {
SolverStrategy::Newton => {
let solver = NewtonSolver::new(NewtonConfig::default());
}
SolverStrategy::Picard => {
let solver = PicardSolver::new(PicardConfig::default());
}
SolverStrategy::Fallback => {
let solver = FallbackSolver::new(FallbackConfig::default());
}
}
```
## Best Practices
1. **Always use SmartInitializer** - Good initial guess is critical
2. **Use Fallback solver** - More robust for production use
3. **Set appropriate tolerances** - Balance accuracy vs. convergence time
4. **Monitor convergence** - Log iteration progress for debugging
5. **Handle errors gracefully** - Provide meaningful feedback on failure
## Python Bindings
```python
import entropyk
# Note: Python bindings use placeholder adapters
# The solver won't converge because there are no real physics equations
system = entropyk.System()
# ... build system ...
solver = entropyk.NewtonSolver(
max_iterations=100,
tolerance=1e-6
)
result = solver.solve(system)
print(f"Status: {result.status}")
print(f"Iterations: {result.iterations}")
```
## Next Steps
- [Components Reference](./03-components.md) - Detailed component documentation
- [Building Systems](./04-building-systems.md) - Create system topology

137
docs/tutorial/README.md Normal file
View File

@@ -0,0 +1,137 @@
# Entropyk Tutorial
Welcome to the Entropyk tutorial! This guide covers the thermodynamic simulation library.
## Current Status
**Important**: The library is under active development. Key points:
-**Rust API**: Full component library with real thermodynamic equations
-**System topology**: Graph-based component connection works
-**Solver infrastructure**: Newton, Picard, and Fallback solvers implemented
- ⚠️ **Python bindings**: Use placeholder adapters (no real physics equations)
- ⚠️ **Convergence**: Requires proper initialization and boundary conditions
## Table of Contents
1. **[Getting Started](./01-getting-started.md)** - Installation, first simulation, basic concepts
2. **[Physical Types](./02-physical-types.md)** - Type-safe units (Pressure, Temperature, Enthalpy, MassFlow)
3. **[Components Reference](./03-components.md)** - All available components with examples
4. **[Building Systems](./04-building-systems.md)** - Creating system topology, connecting components
5. **[Solver Configuration](./05-solver-configuration.md)** - Newton, Picard, Fallback strategies
## Additional Resources
- **[Main Documentation](../README.md)** - Project overview and API reference
- **[Python Bindings](../../bindings/python/README.md)** - Python API documentation
- **[Examples](../../bindings/python/examples/)** - Example scripts
## Quick Start
### Rust
```rust
use entropyk_solver::System;
use entropyk_components::{Component, ComponentError, ConnectedPort,
JacobianBuilder, ResidualVector, SystemState};
// Create a simple component
struct MyComponent { n_eq: usize }
impl Component for MyComponent {
fn compute_residuals(&self, _state: &SystemState, residuals: &mut ResidualVector)
-> Result<(), ComponentError> {
for r in residuals.iter_mut().take(self.n_eq) { *r = 0.0; }
Ok(())
}
fn jacobian_entries(&self, _state: &SystemState, jacobian: &mut JacobianBuilder)
-> Result<(), ComponentError> {
for i in 0..self.n_eq { jacobian.add_entry(i, i, 1.0); }
Ok(())
}
fn n_equations(&self) -> usize { self.n_eq }
fn get_ports(&self) -> &[ConnectedPort] { &[] }
}
fn main() {
let mut system = System::new();
let n1 = system.add_component(Box::new(MyComponent { n_eq: 2 }));
let n2 = system.add_component(Box::new(MyComponent { n_eq: 2 }));
system.add_edge(n1, n2).unwrap();
system.finalize().unwrap();
println!("State vector length: {}", system.state_vector_len());
}
```
### Python
```python
import entropyk
# Note: Python bindings use placeholder adapters
# The solver won't converge because there are no real physics equations
system = entropyk.System()
comp = entropyk.Compressor(speed_rpm=2900.0, displacement=0.0001,
efficiency=0.85, fluid="R134a")
cond = entropyk.Condenser(ua=5000.0)
valve = entropyk.ExpansionValve(fluid="R134a", opening=0.8)
evap = entropyk.Evaporator(ua=3000.0)
c = system.add_component(comp)
d = system.add_component(cond)
v = system.add_component(valve)
e = system.add_component(evap)
system.add_edge(c, d)
system.add_edge(d, v)
system.add_edge(v, e)
system.add_edge(e, c)
system.finalize()
print(f"State vector length: {system.state_vector_len}")
```
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ System (Graph) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Compress.├────►│Condenser├────►│ Valve ├────►│Evaporat.│ │
│ └────▲────┘ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ Each edge carries: P (pressure), h (enthalpy) │
│ Each node has: n_equations(), compute_residuals() │
└─────────────────────────────────────────────────────────────────┘
```
## Thermodynamic Conventions
### Refrigerant Circuits
- **Flow direction**: Compressor → Condenser → Expansion Valve → Evaporator → Compressor
- **Pressure levels**: High pressure (condensing) → Low pressure (evaporating)
- **State points**: Superheated vapor at compressor inlet, subcooled liquid at expansion valve inlet
### Heat Exchangers (4 Ports)
```
Hot Side: Inlet ──►│ HX │──► Outlet
Cold Side: Inlet ──►│ │──► Outlet
```
- **Evaporator**: Refrigerant (cold) evaporates, water/air (hot) provides heat
- **Condenser**: Refrigerant (hot) condenses, water/air (cold) absorbs heat
## Running the Demo
```bash
cargo run --bin macro-chiller
```
Shows hierarchical system composition with MacroComponent pattern.
## Next Steps
Start with [Getting Started](./01-getting-started.md) to set up your environment.