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

@@ -56,7 +56,11 @@
#![warn(rust_2018_idioms)]
pub mod air_boundary;
pub mod anchor;
pub mod brine_boundary;
pub mod capillary_tube;
pub mod centrifugal_compressor;
pub mod dof;
pub mod bypass_valve;
pub mod compressor;
pub mod curves;
@@ -67,67 +71,83 @@ pub mod fan;
pub mod flow_junction;
pub mod free_cooling_exchanger;
pub mod heat_exchanger;
pub mod heat_source;
pub mod isenthalpic_expansion_valve;
pub mod isentropic_compressor;
pub mod node;
pub mod params;
pub mod pipe;
pub mod polynomials;
pub mod port;
pub mod pump;
pub mod registry;
pub mod python_components;
pub mod refrigerant_boundary;
pub mod registry;
pub mod reversing_valve;
pub mod screw_economizer_compressor;
pub mod state_machine;
pub mod thermal_load;
pub mod valve_flow;
pub use air_boundary::{AirSink, AirSource};
pub use anchor::{Anchor, AnchorConstraint};
pub use brine_boundary::{BrineSink, BrineSource};
pub use capillary_tube::{CapillaryGeometry, CapillaryTube};
pub use centrifugal_compressor::{CentrifugalCompressor, CentrifugalMap, CentrifugalMapPoint};
pub use dof::{unspecified_roles, EquationRole};
pub use bypass_valve::{BypassValve, BypassValveConfig, ValveCharacteristics};
pub use compressor::{Ahri540Coefficients, Compressor, CompressorModel, SstSdtCoefficients};
pub use curves::{
BoundedCurve, CurveEngine, CurveEval, CurveResult, CurveSet, CurveWarning,
};
pub use curves::{BoundedCurve, CurveEngine, CurveEval, CurveResult, CurveSet, CurveWarning};
pub use drum::Drum;
pub use expansion_valve::{ExpansionValve, PhaseRegion};
pub use valve_flow::{valve_mass_flow, valve_mass_flow_dp_up, ValveFlowInput, ValveFlowModel};
pub use external_model::{
ExternalModel, ExternalModelConfig, ExternalModelError, ExternalModelMetadata,
ExternalModelType, MockExternalModel, ThreadSafeExternalModel,
};
pub use fan::{Fan, FanCurves};
pub use free_cooling_exchanger::{
FreeCoolingConfig, FreeCoolingControlMode, FreeCoolingExchanger, FreeCoolingMode,
};
pub use flow_junction::{
CompressibleMerger, CompressibleSplitter, FlowMerger, FlowSplitter, FluidKind,
IncompressibleMerger, IncompressibleSplitter,
};
pub use free_cooling_exchanger::{
FreeCoolingConfig, FreeCoolingControlMode, FreeCoolingExchanger, FreeCoolingMode,
};
pub use heat_exchanger::model::FluidState;
pub use heat_exchanger::{
Condenser, CondenserCoil, Economizer, EpsNtuModel, Evaporator, EvaporatorCoil, ExchangerType,
FloodedCondenser, FloodedEvaporator, FlowConfiguration, HeatExchanger, HeatExchangerBuilder,
HeatTransferModel, HxSideConditions, LmtdModel, MchxCondenserCoil,
AirCooledCondenser, CoilGeometry, Condenser, CondenserCoil, CondenserRating, Economizer,
EpsNtuModel, Evaporator, EvaporatorCoil, EvaporatorRating, ExchangerType, FanCoilUnit,
FinCoilCondenser, FinType, FloodedCondenser, FloodedEvaporator, FloodedPoolBoilingConfig,
FlowConfiguration, GasCooler, HeatExchanger, HeatExchangerBuilder, HeatTransferModel,
HxSideConditions, LmtdModel, MchxCondenserCoil, ShellAndTubeHx, UaMode,
};
pub use heat_source::HeatSource;
pub use isenthalpic_expansion_valve::IsenthalpicExpansionValve;
pub use isentropic_compressor::IsentropicCompressor;
pub use isentropic_compressor::{VolumetricEfficiency, VsdSpeedMap};
pub use node::{Node, NodeMeasurements, NodePhase};
pub use params::ComponentParams;
pub use registry::{RegistryError, create_component};
pub use pipe::{friction_factor, roughness, Pipe, PipeGeometry};
pub use polynomials::{AffinityLaws, PerformanceCurves, Polynomial1D, Polynomial2D};
pub use port::{
validate_port_continuity, Connected, ConnectedPort, ConnectionError, Disconnected, FluidId,
Port,
Port, PortKind,
};
pub use pump::{Pump, PumpCurves};
pub use python_components::{
PyCompressorReal, PyExpansionValveReal, PyFlowMergerReal, PyFlowSinkReal, PyFlowSourceReal,
PyFlowSplitterReal, PyHeatExchangerReal, PyPipeReal,
PyRefrigerantSourceReal, PyRefrigerantSinkReal, PyBrineSourceReal, PyBrineSinkReal,
PyAirSourceReal, PyAirSinkReal,
PyAirSinkReal, PyAirSourceReal, PyBrineSinkReal, PyBrineSourceReal, PyCompressorReal,
PyExpansionValveReal, PyFlowMergerReal, PyFlowSinkReal, PyFlowSourceReal, PyFlowSplitterReal,
PyHeatExchangerReal, PyPipeReal, PyRefrigerantSinkReal, PyRefrigerantSourceReal,
};
pub use refrigerant_boundary::{RefrigerantSink, RefrigerantSource};
pub use registry::{create_component, RegistryError};
pub use reversing_valve::{ReversingMode, ReversingValve};
pub use screw_economizer_compressor::{ScrewEconomizerCompressor, ScrewPerformanceCurves};
pub use state_machine::{
CircuitId, OperationalState, StateHistory, StateManageable, StateTransitionError,
StateTransitionRecord,
};
pub use thermal_load::ThermalLoad;
use entropyk_core::{MassFlow, Power};
use thiserror::Error;
@@ -386,6 +406,32 @@ impl JacobianBuilder {
///
/// Both computation methods return [`Result`] to allow components to report
/// errors such as invalid state dimensions, numerical issues, or invalid
/// Physical output that can be measured from a component's converged state.
///
/// Used by the inverse-control layer to evaluate constraint residuals from
/// *real* thermodynamics (via [`Component::measure_output`]) instead of
/// placeholder formulas. Each variant maps to a solver-side `ComponentOutput`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MeasuredOutput {
/// Refrigerant-side heat/duty [W] (evaporator/condenser capacity).
Capacity,
/// Heat-transfer rate [W] (alias of capacity for HX duty).
HeatTransferRate,
/// Suction/outlet superheat above saturation [K].
Superheat,
/// Liquid-line subcooling below saturation [K].
Subcooling,
/// Refrigerant mass flow rate [kg/s].
MassFlowRate,
/// Absolute pressure at the component [Pa].
Pressure,
/// Refrigerant temperature at the component [K].
Temperature,
/// Saturation temperature at the component's reference pressure [K]
/// (SST for an evaporator, SDT for a condenser).
SaturationTemperature,
}
/// component configuration.
///
/// # Type Parameters
@@ -519,6 +565,32 @@ pub trait Component {
/// ```
fn n_equations(&self) -> usize;
/// Semantic roles for each residual equation contributed by this component.
///
/// Used by the system-wide DoF ledger (`entropyk_solver::dof`) to audit that
/// the algebraic system is square and that every fix has a corresponding free
/// unknown (or an intentional residual drop).
///
/// # Contract
///
/// - Prefer returning exactly [`n_equations`](Self::n_equations) roles.
/// - An empty default is allowed for legacy components; the solver then
/// labels rows as `Unspecified`.
/// - **Never** add a residual "to make it converge" without declaring its
/// role and the free unknown it closes.
///
/// # Fix / Free discipline
///
/// | Role | DoF effect |
/// |------|------------|
/// | `BoundaryDirichlet` | Fixes a boundary state (machine input) |
/// | `OutletClosure` | Consumes one DoF — pair with a free actuator or drop another residual |
/// | `EnergyBalance` / `Momentum…` | Closes a physics residual on live ports |
/// | `ActuatorClosure` | Closes a free actuator unknown |
fn equation_roles(&self) -> Vec<EquationRole> {
Vec::new()
}
/// Returns the connected ports of this component.
///
/// This method provides access to the component's ports for topology
@@ -625,18 +697,49 @@ pub trait Component {
/// *internal* state block begins. For ordinary leaf components this is never
/// needed; for `MacroComponent` it replaces the manual `set_global_state_offset`
/// call.
/// * `external_edge_state_indices` — A slice of `(p_idx, h_idx)` pairs for every
/// edge incident to this component's node in the parent graph (incoming and
/// outgoing), in traversal order. `MacroComponent` uses these to emit
/// port-coupling residuals.
/// * `external_edge_state_indices` — A slice of `(m_idx, p_idx, h_idx)` triples for
/// every edge incident to this component's node in the parent graph (incoming and
/// outgoing), in traversal order. The `m_idx` is the mass-flow state index added
/// by CM1.2; components use it to contribute mass-flow residuals (CM1.3).
/// `MacroComponent` uses all three indices to emit port-coupling residuals.
fn set_system_context(
&mut self,
_state_offset: usize,
_external_edge_state_indices: &[(usize, usize)],
_external_edge_state_indices: &[(usize, usize, usize)],
) {
// Default: no-op for all ordinary leaf components.
}
/// Injects the per-port edge state indices resolved by the solver.
///
/// `port_edges[i]` is `Some((m_idx, p_idx, h_idx))` when a flow edge is
/// connected at this component's local port index `i` (the same index used
/// by `System::add_edge_with_ports`), or `None` when the port is
/// unconnected. Unlike [`set_system_context`](Self::set_system_context),
/// whose incident-edge ordering depends on graph traversal, this mapping is
/// deterministic — multi-port components (Modelica-style 4-port heat
/// exchangers, economizer compressors, drums…) should prefer it.
///
/// Called by `System::finalize()` right after `set_system_context`.
fn set_port_context(&mut self, _port_edges: &[Option<(usize, usize, usize)>]) {
// Default: no-op — positional wiring via set_system_context stays valid.
}
/// Declares the internal series flow paths of a multi-port component as
/// `(inlet_port, outlet_port)` pairs.
///
/// The mass-flow topology presolve uses these to walk *through* the
/// component: an edge entering at `inlet_port` and the edge leaving at
/// `outlet_port` belong to the same series branch (they share one ṁ
/// unknown), exactly like Modelica's `port_a.m_flow + port_b.m_flow = 0`.
///
/// A Modelica-style 4-port heat exchanger returns `[(0, 1), (2, 3)]`
/// (refrigerant path + secondary path). Genuine junctions (splitters,
/// mergers, drums) keep the empty default so they stay branch boundaries.
fn flow_paths(&self) -> Vec<(usize, usize)> {
Vec::new()
}
/// Returns the number of internal state variables this component maintains.
///
/// The default implementation returns 0, which is correct for all ordinary
@@ -695,6 +798,28 @@ pub trait Component {
// Default: no-op for components that don't support inverse calibration
}
/// Injects the state index of an externally-determined heat rate Q [W]
/// (an inter-circuit thermal-coupling unknown) into this component.
///
/// Called by the solver's `System::finalize()` for the cold-side receiver
/// of a physical thermal coupling (e.g. [`ThermalLoad`]), which then reads
/// `Q = state[idx]` in its energy-balance residual.
fn set_external_heat_index(&mut self, _idx: usize) {
// Default: no-op for components that don't consume external heat
}
/// Whether this component's [`energy_transfers`](Self::energy_transfers)
/// contribute to the refrigerant-cycle performance aggregation
/// (cooling/heating capacity, COP).
///
/// Secondary-side receivers such as [`ThermalLoad`] return `false`: the
/// heat they absorb is the *rejected* duty of the primary cycle and must
/// not be double-counted as cooling capacity. They still participate in
/// per-component First Law validation.
fn counts_in_cycle_performance(&self) -> bool {
true
}
/// Updates a single calibration factor on this component.
///
/// Returns `true` if the factor was recognized and updated. The default
@@ -713,7 +838,10 @@ pub trait Component {
///
/// The default implementation is a no-op — components that don't use fluid backends
/// silently ignore this.
fn set_fluid_backend_from_builder(&mut self, _backend: std::sync::Arc<dyn entropyk_fluids::FluidBackend>) {
fn set_fluid_backend_from_builder(
&mut self,
_backend: std::sync::Arc<dyn entropyk_fluids::FluidBackend>,
) {
// Default: no-op for components that don't use fluid backends
}
@@ -730,6 +858,27 @@ pub trait Component {
None
}
/// Measures a physical output from the current state for inverse control.
///
/// Returns the *real* value of the requested [`MeasuredOutput`] using this
/// component's thermodynamics, or `None` when the component cannot provide
/// it. This replaces the placeholder constraint formulas: the inverse-control
/// solver calls this to form genuine constraint residuals `measure target`.
///
/// The default implementation derives `Capacity`/`HeatTransferRate` from
/// [`energy_transfers`](Self::energy_transfers) (the refrigerant-side heat,
/// as an absolute duty in Watts). All other outputs return `None`; heat
/// exchangers and the compressor override this to add superheat, subcooling,
/// mass flow, pressure and temperature.
fn measure_output(&self, kind: MeasuredOutput, state: &StateSlice) -> Option<f64> {
match kind {
MeasuredOutput::Capacity | MeasuredOutput::HeatTransferRate => self
.energy_transfers(state)
.map(|(heat, _work)| heat.to_watts().abs()),
_ => None,
}
}
/// Generates a string signature of the component's configuration (parameters, fluid, etc.).
/// Used for simulation traceability (input hashing).
/// Default implementation is provided, but components should override this to include