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