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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user