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

@@ -183,6 +183,12 @@ pub struct Pump<State> {
circuit_id: CircuitId,
/// Operational state
operational_state: OperationalState,
inlet_m_idx: Option<usize>,
inlet_p_idx: Option<usize>,
inlet_h_idx: Option<usize>,
outlet_m_idx: Option<usize>,
outlet_p_idx: Option<usize>,
outlet_h_idx: Option<usize>,
/// Phantom data for type state
_state: PhantomData<State>,
}
@@ -228,6 +234,12 @@ impl Pump<Disconnected> {
speed_ratio: 1.0,
circuit_id: CircuitId::default(),
operational_state: OperationalState::default(),
inlet_m_idx: None,
inlet_p_idx: None,
inlet_h_idx: None,
outlet_m_idx: None,
outlet_p_idx: None,
outlet_h_idx: None,
_state: PhantomData,
})
}
@@ -299,6 +311,12 @@ impl Pump<Disconnected> {
speed_ratio: self.speed_ratio,
circuit_id: self.circuit_id,
operational_state: self.operational_state,
inlet_m_idx: None,
inlet_p_idx: None,
inlet_h_idx: None,
outlet_m_idx: None,
outlet_p_idx: None,
outlet_h_idx: None,
_state: PhantomData,
})
}
@@ -460,6 +478,23 @@ impl Pump<Connected> {
}
impl Component for Pump<Connected> {
fn set_system_context(
&mut self,
_state_offset: usize,
external_edge_state_indices: &[(usize, usize, usize)],
) {
if let Some(&(m, p, h)) = external_edge_state_indices.first() {
self.inlet_m_idx = Some(m);
self.inlet_p_idx = Some(p);
self.inlet_h_idx = Some(h);
}
if let Some(&(m, p, h)) = external_edge_state_indices.get(1) {
self.outlet_m_idx = Some(m);
self.outlet_p_idx = Some(p);
self.outlet_h_idx = Some(h);
}
}
fn compute_residuals(
&self,
state: &StateSlice,
@@ -472,55 +507,55 @@ impl Component for Pump<Connected> {
});
}
// Handle operational states
let (in_m, in_p, in_h, out_p, out_h) = match (
self.inlet_m_idx,
self.inlet_p_idx,
self.inlet_h_idx,
self.outlet_p_idx,
self.outlet_h_idx,
) {
(Some(in_m), Some(in_p), Some(in_h), Some(out_p), Some(out_h)) => {
(in_m, in_p, in_h, out_p, out_h)
}
_ => {
return Err(ComponentError::InvalidState(
"Pump requires live inlet and outlet edge state indices".to_string(),
));
}
};
let max_idx = in_m.max(in_p).max(in_h).max(out_p).max(out_h);
if max_idx >= state.len() {
return Err(ComponentError::InvalidStateDimensions {
expected: max_idx + 1,
actual: state.len(),
});
}
match self.operational_state {
OperationalState::Off => {
residuals[0] = state[0]; // Mass flow = 0
residuals[1] = 0.0; // No energy transfer
residuals[0] = state[out_p] - state[in_p];
residuals[1] = state[out_h] - state[in_h];
return Ok(());
}
OperationalState::Bypass => {
// Behaves as a pipe: no pressure rise, no energy change
let p_in = self.port_inlet.pressure().to_pascals();
let p_out = self.port_outlet.pressure().to_pascals();
let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
residuals[0] = p_in - p_out;
residuals[1] = h_in - h_out;
residuals[0] = state[out_p] - state[in_p];
residuals[1] = state[out_h] - state[in_h];
return Ok(());
}
OperationalState::On => {}
}
if state.len() < 2 {
return Err(ComponentError::InvalidStateDimensions {
expected: 2,
actual: state.len(),
});
}
// State: [mass_flow_kg_s, power_w]
let mass_flow_kg_s = state[0];
let _power_w = state[1];
// Convert to volumetric flow
let mass_flow_kg_s = state[in_m];
let flow_m3_s = mass_flow_kg_s / self.fluid_density_kg_per_m3;
// Calculate pressure rise from curves
let delta_p_calc = self.pressure_rise(flow_m3_s);
// Get port pressures
let p_in = self.port_inlet.pressure().to_pascals();
let p_out = self.port_outlet.pressure().to_pascals();
let delta_p_actual = p_out - p_in;
// Residual 0: Pressure balance
residuals[0] = delta_p_calc - delta_p_actual;
// Residual 1: Power balance
residuals[0] = state[out_p] - (state[in_p] + delta_p_calc);
let power_calc = self.hydraulic_power(flow_m3_s).to_watts();
residuals[1] = power_calc - _power_w;
let enthalpy_rise_j_kg = if mass_flow_kg_s.abs() > 1e-12 {
power_calc / mass_flow_kg_s
} else {
0.0
};
residuals[1] = state[out_h] - (state[in_h] + enthalpy_rise_j_kg);
Ok(())
}
@@ -530,14 +565,29 @@ impl Component for Pump<Connected> {
state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
if state.len() < 2 {
let (in_m, in_p, in_h, out_p, out_h) = match (
self.inlet_m_idx,
self.inlet_p_idx,
self.inlet_h_idx,
self.outlet_p_idx,
self.outlet_h_idx,
) {
(Some(in_m), Some(in_p), Some(in_h), Some(out_p), Some(out_h)) => {
(in_m, in_p, in_h, out_p, out_h)
}
_ => {
return Err(ComponentError::InvalidState(
"Pump Jacobian requires live inlet and outlet edge state indices".to_string(),
));
}
};
if in_m >= state.len() {
return Err(ComponentError::InvalidStateDimensions {
expected: 2,
expected: in_m + 1,
actual: state.len(),
});
}
let mass_flow_kg_s = state[0];
let mass_flow_kg_s = state[in_m];
let flow_m3_s = mass_flow_kg_s / self.fluid_density_kg_per_m3;
// Numerical derivative of pressure with respect to mass flow
@@ -547,10 +597,9 @@ impl Component for Pump<Connected> {
let dp_dm = (p_plus - p_minus) / (2.0 * h);
// ∂r₀/∂ṁ = dΔP/dṁ
jacobian.add_entry(0, 0, dp_dm);
// ∂r₀/∂P = -1 (constant)
jacobian.add_entry(0, 1, 0.0);
jacobian.add_entry(0, in_m, -dp_dm);
jacobian.add_entry(0, out_p, 1.0);
jacobian.add_entry(0, in_p, -1.0);
// Numerical derivative of power with respect to mass flow
let pow_plus = self
@@ -561,11 +610,15 @@ impl Component for Pump<Connected> {
.to_watts();
let dpow_dm = (pow_plus - pow_minus) / (2.0 * h);
// ∂r₁/∂ṁ
jacobian.add_entry(1, 0, dpow_dm);
// ∂r₁/∂P = -1
jacobian.add_entry(1, 1, -1.0);
let dh_dm = if mass_flow_kg_s.abs() > 1e-12 {
(dpow_dm * mass_flow_kg_s - self.hydraulic_power(flow_m3_s).to_watts())
/ (mass_flow_kg_s * mass_flow_kg_s)
} else {
0.0
};
jacobian.add_entry(1, in_m, -dh_dm);
jacobian.add_entry(1, out_h, 1.0);
jacobian.add_entry(1, in_h, -1.0);
Ok(())
}
@@ -582,18 +635,22 @@ impl Component for Pump<Connected> {
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
if state.len() < 1 {
let (Some(in_m), Some(out_m)) = (self.inlet_m_idx, self.outlet_m_idx) else {
return Err(ComponentError::InvalidState(
"Pump mass-flow reporting requires live inlet and outlet mass-flow indices"
.to_string(),
));
};
let max_idx = in_m.max(out_m);
if max_idx >= state.len() {
return Err(ComponentError::InvalidStateDimensions {
expected: 1,
expected: max_idx + 1,
actual: state.len(),
});
}
// Pump has inlet and outlet with same mass flow (incompressible)
let m = entropyk_core::MassFlow::from_kg_per_s(state[0]);
// Inlet (positive = entering), Outlet (negative = leaving)
Ok(vec![
m,
entropyk_core::MassFlow::from_kg_per_s(-m.to_kg_per_s()),
entropyk_core::MassFlow::from_kg_per_s(state[in_m]),
entropyk_core::MassFlow::from_kg_per_s(-state[out_m]),
])
}
@@ -601,10 +658,22 @@ impl Component for Pump<Connected> {
&self,
_state: &StateSlice,
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
// Pump uses internally simulated enthalpies
let (Some(in_h), Some(out_h)) = (self.inlet_h_idx, self.outlet_h_idx) else {
return Err(ComponentError::InvalidState(
"Pump enthalpy reporting requires live inlet and outlet enthalpy indices"
.to_string(),
));
};
let max_idx = in_h.max(out_h);
if max_idx >= _state.len() {
return Err(ComponentError::InvalidStateDimensions {
expected: max_idx + 1,
actual: _state.len(),
});
}
Ok(vec![
self.port_inlet.enthalpy(),
self.port_outlet.enthalpy(),
entropyk_core::Enthalpy::from_joules_per_kg(_state[in_h]),
entropyk_core::Enthalpy::from_joules_per_kg(_state[out_h]),
])
}
@@ -618,10 +687,10 @@ impl Component for Pump<Connected> {
entropyk_core::Power::from_watts(0.0),
)),
OperationalState::On => {
if state.is_empty() {
let Some(in_m) = self.inlet_m_idx else {
return None;
}
let mass_flow_kg_s = state[0];
};
let mass_flow_kg_s = *state.get(in_m)?;
let flow_m3_s = mass_flow_kg_s / self.fluid_density_kg_per_m3;
let power_calc = self.hydraulic_power(flow_m3_s).to_watts();
Some((
@@ -728,6 +797,12 @@ mod tests {
speed_ratio: 1.0,
circuit_id: CircuitId::default(),
operational_state: OperationalState::default(),
inlet_m_idx: None,
inlet_p_idx: None,
inlet_h_idx: None,
outlet_m_idx: None,
outlet_p_idx: None,
outlet_h_idx: None,
_state: PhantomData,
}
}
@@ -871,8 +946,9 @@ mod tests {
#[test]
fn test_pump_component_compute_residuals() {
let pump = create_test_pump_connected();
let state = vec![50.0, 2000.0]; // mass flow, power
let mut pump = create_test_pump_connected();
pump.set_system_context(0, &[(0, 1, 2), (3, 4, 5)]);
let state = vec![50.0, 1.0e5, 100_000.0, 50.0, 4.0e5, 105_000.0];
let mut residuals = vec![0.0; 2];
let result = pump.compute_residuals(&state, &mut residuals);