chore: sync project state and current artifacts
This commit is contained in:
225
docs/tutorial/01-getting-started.md
Normal file
225
docs/tutorial/01-getting-started.md
Normal 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
|
||||
203
docs/tutorial/02-physical-types.md
Normal file
203
docs/tutorial/02-physical-types.md
Normal 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
|
||||
412
docs/tutorial/03-components.md
Normal file
412
docs/tutorial/03-components.md
Normal 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
|
||||
309
docs/tutorial/04-building-systems.md
Normal file
309
docs/tutorial/04-building-systems.md
Normal 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
|
||||
306
docs/tutorial/05-solver-configuration.md
Normal file
306
docs/tutorial/05-solver-configuration.md
Normal 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
137
docs/tutorial/README.md
Normal 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.
|
||||
Reference in New Issue
Block a user