Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.

Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 22:46:46 +02:00
parent 62efea0646
commit 3358b74342
275 changed files with 70187 additions and 5230 deletions

View File

@@ -25,10 +25,28 @@ use petgraph::graph::{DiGraph, NodeIndex};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn default_duty_scale() -> f64 {
1.0
}
/// Thermal coupling between two circuits via a heat exchanger.
///
/// Heat flows from `hot_circuit` to `cold_circuit` proportional to the
/// temperature difference and thermal conductance (UA value).
///
/// ## Physical (duty-transfer) mode
///
/// When [`hot_component`](Self::hot_component) and
/// [`cold_component`](Self::cold_component) are set, the coupling is **physical**:
/// the per-coupling state unknown is the transferred heat `Q` [W], closed by the
/// residual `r = Q η·duty(hot_component)` where the duty is *measured* from the
/// solved state via the hot component's `measure_output(Capacity)` (e.g. the real
/// ε-NTU condenser duty). The cold-side component (a
/// [`ThermalLoad`](entropyk_components::ThermalLoad)) consumes `Q` in its energy
/// balance `ṁ·(h_out h_in) = Q`, so the cold circuit genuinely warms up.
///
/// Without the component references the legacy MVP stub applies (the residual
/// simply pins `Q = 0`), preserved for backward compatibility.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ThermalCoupling {
@@ -42,6 +60,19 @@ pub struct ThermalCoupling {
pub ua: ThermalConductance,
/// Efficiency factor (0.0 to 1.0). Default is 1.0 (no losses).
pub efficiency: f64,
/// Multiplier applied to the measured duty before it is injected into the
/// receiver. Use `-1.0` when the receiver must be cooled by the source duty
/// (for example, chilled water across an evaporator).
#[serde(default = "default_duty_scale", alias = "duty_scale")]
pub duty_scale: f64,
/// Name of the registered component in the hot circuit whose *measured duty*
/// (`measure_output(Capacity)`) is transferred (e.g. an ε-NTU `Condenser`).
#[serde(default, alias = "hot_component")]
pub hot_component: Option<String>,
/// Name of the registered `ThermalLoad` component in the cold circuit that
/// receives `Q` in its energy balance.
#[serde(default, alias = "cold_component")]
pub cold_component: Option<String>,
}
impl ThermalCoupling {
@@ -71,6 +102,9 @@ impl ThermalCoupling {
cold_circuit,
ua,
efficiency: 1.0,
duty_scale: 1.0,
hot_component: None,
cold_component: None,
}
}
@@ -82,6 +116,35 @@ impl ThermalCoupling {
self.efficiency = efficiency.clamp(0.0, 1.0);
self
}
/// Sets the signed duty multiplier applied before injecting heat into the
/// receiver component.
pub fn with_duty_scale(mut self, duty_scale: f64) -> Self {
self.duty_scale = if duty_scale.is_finite() {
duty_scale
} else {
1.0
};
self
}
/// Enables the **physical duty-transfer mode**: the measured duty of
/// `hot_component` (via `measure_output(Capacity)`) is transferred, scaled
/// by `efficiency`, into `cold_component`'s energy balance (a `ThermalLoad`).
pub fn with_interface_components(
mut self,
hot_component: impl Into<String>,
cold_component: impl Into<String>,
) -> Self {
self.hot_component = Some(hot_component.into());
self.cold_component = Some(cold_component.into());
self
}
/// Returns `true` when the coupling is in physical duty-transfer mode.
pub fn is_physical(&self) -> bool {
self.hot_component.is_some() && self.cold_component.is_some()
}
}
/// Computes heat transfer for a thermal coupling.