Entropyk/EXAMPLES_FULL.md

5.8 KiB

Entropyk: Comprehensive Examples Suite

This document provides advanced modeling scenarios for Entropyk across its multi-platform ecosystem.


1. Multi-Circuit Industrial Chiller (Rust)

Modeling a water-cooled chiller where a refrigerant loop (R134a) and a water loop are coupled via an evaporator (bridge).

1.1 System Architecture

graph LR
    subgraph Circuit_0 [Circuit 0: Refrigerant]
        comp[Compressor] --> cond[Condenser]
        cond --> valve[Expansion Valve]
        valve --> evap_a[Evaporator Side A]
        evap_a --> comp
    end

    subgraph Circuit_1 [Circuit 1: Water Loop]
        pump[Pump] --> evap_b[Evaporator Side B]
        evap_b --> building[Building Load]
        building --> pump
    end

    evap_a <-.->|Thermal Coupling| evap_b

1.2 Implementation Detail

use entropyk_components::{Compressor, HeatExchanger, Pump};
use entropyk_solver::{System, NewtonConfig, ThermalCoupling};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut system = System::new();

    // Circuit 0: Refrigerant Loop
    let comp = system.add_component(Compressor::new(coeffs, ...));
    let cond = system.add_component(HeatExchanger::new_condenser(ua_air));
    let valve = system.add_component(ExpansionValve::new(cv));
    let evap = system.add_component(HeatExchanger::new_bridge(ua_water)); // COUPLING POINT

    system.add_edge_in_circuit(comp, cond, 0)?;
    system.add_edge_in_circuit(cond, valve, 0)?;
    system.add_edge_in_circuit(valve, evap.side_a, 0)?;
    system.add_edge_in_circuit(evap.side_a, comp, 0)?;

    // Circuit 1: Water loop
    let pump = system.add_component(Pump::new(curve));
    let building = system.add_component(HeatExchanger::new_load(50_000.0)); // 50kW Load

    system.add_edge_in_circuit(pump, evap.side_b, 1)?;
    system.add_edge_in_circuit(evap.side_b, building, 1)?;
    system.add_edge_in_circuit(building, pump, 1)?;

    system.finalize()?;

    // Simultaneous Multi-Circuit Solve
    let solver = NewtonConfig::default().with_line_search(true);
    let state = solver.solve(&mut system)?;

    println!("Chiller System COP: {}", state.cop());
    Ok(())
}

1.3 Control & Coupling Logic

The solver treats both circuits as a unified graph. The HeatExchanger bridge enforces the following boundary conditions:

  • Energy Balance: \dot{Q}_{refrig} = \dot{Q}_{water} (assuming no ambient loss).
  • Temperature Coupling: The effectiveness-NTU model internally calculates the heat transfer based on the inlet temperatures of both circuits.
  • Unified Jacobian: The solver constructs a single Jacobian matrix where off-diagonal blocks represent the thermal coupling, allowing for simultaneous convergence of both loops.

2. Inverse Control & Parameter Estimation (Python)

Finding the Heat Exchanger Fouling (UA) by matching simulation to sensor data.

2.1 Control Flow Diagram

sequenceDiagram
    participant U as User (Script)
    participant S as System Solver
    participant P as Physical Model
    participant C as Constraint Engine

    U->>S: Define Architecture & Constraints
    Note over S,C: Link Constraint (Temp) to Control (UA)
    loop Iterations (Newton-Raphson)
        S->>P: Compute Residuals F(x)
        P->>S: Physical Violations
        S->>C: Compute Constraint Gradients (dC/dua)
        C->>S: Jacobian Block
        S->>S: Solve Augmented System [J | G]
        S->>S: Update State (x) & Control (ua)
    end
    S->>U: Converged Parameters (UA)

2.2 Implementation Breakdown

import entropyk as ek

# 1. Define physical system
sys = ek.System()
hx = sys.add_component(ek.HeatExchanger(ua=5000.0)) # Initial guess

# 2. Add Constraint: We KNOW the exit temperature from a sensor
# Target: Exit port of HX must be 280.15 K
sys.add_constraint(
    node_id=hx, 
    variable="exit_temp", 
    target=280.15, 
    tolerance=0.01
)

# 3. Designate UA as a "Calibration Variable" (Solver will tune this)
sys.link_constraint_to_control(hx, "ua", bounds=(1000.0, 10000.0))

# 4. Solve Sparse Inverse Problem
solver = ek.NewtonConfig(inverse_mode=True)
result = solver.solve(sys)

print(f"Estimated UA based on sensor: {hx.ua:.2f} W/K")

2.3 Logic Breakdown: The Augmented Matrix

In standard "Forward" mode, the solver solves F(x) = 0. In "Inverse" mode, we add a constraint C(x, u) = 0 (where u is our control, e.g., UA). The solver internally solves:


\begin{bmatrix} 
\mathcal{J}_x & \mathcal{G}_u \\
\mathcal{C}_x & 0 
\end{bmatrix} 
\begin{bmatrix} \Delta x \\ \Delta u \end{bmatrix} = 
-\begin{bmatrix} F(x) \\ C(x, u) \end{bmatrix}
  • \mathcal{J}_x: Standard physical Jacobian.
  • \mathcal{G}_u: Sensitivity of physics to the control variable (how a change in UA affects mass/energy residuals).
  • \mathcal{C}_x: Sensitivity of the constraint to state variables.
  • \Delta u: The correction to our estimated parameter (UA) to satisfy the sensor target.

3. Real-Time HIL Integration (C FFI)

Zero-allocation solving for embedded controllers at 100Hz.

#include "entropyk.h"

int main() {
    // 1. Initialize system once (pre-allocate hooks)
    ek_system_t* sys = ek_system_create();
    ek_compressor_t* comp = ek_compressor_create(coeffs);
    ek_system_add_component(sys, comp);
    // ... connections ...
    ek_system_finalize(sys);

    // 2. Control Loop (10ms steps)
    while (running) {
        // Update boundary conditions (e.g. ambient T)
        ek_system_set_source_temp(sys, source_node, get_sensor_t());

        // Solve using previous state as hot-start
        ek_converged_state_t* res = ek_solve(sys, PICARD_STRATEGY);

        if (ek_converged_state_is_ok(res)) {
            float p_disch = ek_converged_state_get_p(res, discharge_port);
            apply_to_plc(p_disch);
        }

        ek_converged_state_free(res);
    }

    ek_system_free(sys);
}