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:
@@ -144,6 +144,10 @@ pub struct FlowSplitter {
|
||||
inlet: ConnectedPort,
|
||||
/// Outlet ports (N branches).
|
||||
outlets: Vec<ConnectedPort>,
|
||||
/// Captured (P,h) global state indices of the inlet edge (incoming).
|
||||
inlet_idx: Option<(usize, usize)>,
|
||||
/// Captured (P,h) global state indices of the outlet edges (outgoing).
|
||||
outlet_idx: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl FlowSplitter {
|
||||
@@ -208,6 +212,8 @@ impl FlowSplitter {
|
||||
fluid_id: fluid,
|
||||
inlet,
|
||||
outlets,
|
||||
inlet_idx: None,
|
||||
outlet_idx: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -240,18 +246,37 @@ impl FlowSplitter {
|
||||
}
|
||||
|
||||
impl Component for FlowSplitter {
|
||||
/// `2N − 1` equations:
|
||||
/// - `N−1` pressure equalities
|
||||
/// - `N−1` enthalpy equalities
|
||||
/// - `1` mass balance (represented as N-th outlet consistency)
|
||||
/// `2N` equations — the splitter owns its `N` outgoing edges, contributing
|
||||
/// 2 equations (pressure + enthalpy) per branch:
|
||||
/// - `P_out_k − P_in = 0` for k = 0..N
|
||||
/// - `h_out_k − h_in = 0` for k = 0..N
|
||||
fn n_equations(&self) -> usize {
|
||||
let n = self.outlets.len();
|
||||
2 * n - 1
|
||||
2 * self.outlets.len()
|
||||
}
|
||||
|
||||
fn set_system_context(
|
||||
&mut self,
|
||||
_state_offset: usize,
|
||||
external_edge_state_indices: &[(usize, usize, usize)],
|
||||
) {
|
||||
// Layout: [0] = incoming inlet edge, [1..] = outgoing outlet edges.
|
||||
// Triple: (m_idx, p_idx, h_idx) — extract (p, h) pairs for residuals.
|
||||
if external_edge_state_indices.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.inlet_idx = Some((
|
||||
external_edge_state_indices[0].1,
|
||||
external_edge_state_indices[0].2,
|
||||
));
|
||||
self.outlet_idx = external_edge_state_indices[1..]
|
||||
.iter()
|
||||
.map(|&(_, p, h)| (p, h))
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
let n_eqs = self.n_equations();
|
||||
@@ -262,38 +287,21 @@ impl Component for FlowSplitter {
|
||||
});
|
||||
}
|
||||
|
||||
// Inlet state indices come from the ConnectedPort.
|
||||
// In the Entropyk solver, the state vector is indexed by edge:
|
||||
// each edge contributes (P_idx, h_idx) set during System::finalize().
|
||||
let p_in = self.inlet.pressure().to_pascals();
|
||||
let h_in = self.inlet.enthalpy().to_joules_per_kg();
|
||||
let (in_p, in_h) = match self.inlet_idx {
|
||||
Some(idx) if self.outlet_idx.len() == self.outlets.len() => idx,
|
||||
_ => {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"FlowSplitter requires one live inlet edge and all live outlet edges"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let n = self.outlets.len();
|
||||
let mut r_idx = 0;
|
||||
|
||||
// --- Isobaric constraints: outlets 0..N-1 ---
|
||||
for k in 0..(n - 1) {
|
||||
let p_out_k = self.outlets[k].pressure().to_pascals();
|
||||
residuals[r_idx] = p_out_k - p_in;
|
||||
r_idx += 1;
|
||||
for (k, &(out_p, out_h)) in self.outlet_idx.iter().enumerate() {
|
||||
residuals[2 * k] = state[out_p] - state[in_p];
|
||||
residuals[2 * k + 1] = state[out_h] - state[in_h];
|
||||
}
|
||||
|
||||
// --- Isenthalpic constraints: outlets 0..N-1 ---
|
||||
for k in 0..(n - 1) {
|
||||
let h_out_k = self.outlets[k].enthalpy().to_joules_per_kg();
|
||||
residuals[r_idx] = h_out_k - h_in;
|
||||
r_idx += 1;
|
||||
}
|
||||
|
||||
// --- Mass balance (1 equation) ---
|
||||
// Express as: P_out_N = P_in AND h_out_N = h_in (implicitly guaranteed
|
||||
// by the solver topology, but we add one algebraic check on the last branch).
|
||||
// We use the last outlet as the "free" degree of freedom:
|
||||
let p_out_last = self.outlets[n - 1].pressure().to_pascals();
|
||||
residuals[r_idx] = p_out_last - p_in;
|
||||
// Note: h_out_last = h_in is implied once all other constraints are met
|
||||
// (conservation is guaranteed by the graph topology).
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -302,17 +310,23 @@ impl Component for FlowSplitter {
|
||||
_state: &StateSlice,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// All residuals are linear differences → constant Jacobian.
|
||||
// Each residual r_i depends on exactly two state variables with
|
||||
// coefficients +1 and -1. Since the state indices are stored in the
|
||||
// ConnectedPort pressure/enthalpy values (not raw state indices here),
|
||||
// we emit a diagonal approximation: ∂r_i/∂x_i = 1.
|
||||
//
|
||||
// The full off-diagonal coupling is handled by the System assembler
|
||||
// which maps port values to state vector positions.
|
||||
let n_eqs = self.n_equations();
|
||||
for i in 0..n_eqs {
|
||||
jacobian.add_entry(i, i, 1.0);
|
||||
let (in_p, in_h) = match self.inlet_idx {
|
||||
Some(idx) if self.outlet_idx.len() == self.outlets.len() => idx,
|
||||
_ => {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"FlowSplitter Jacobian requires one live inlet edge and all live outlet edges"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
for (k, &(out_p, out_h)) in self.outlet_idx.iter().enumerate() {
|
||||
// r[2k] = P_out_k - P_in
|
||||
jacobian.add_entry(2 * k, out_p, 1.0);
|
||||
jacobian.add_entry(2 * k, in_p, -1.0);
|
||||
// r[2k+1] = h_out_k - h_in
|
||||
jacobian.add_entry(2 * k + 1, out_h, 1.0);
|
||||
jacobian.add_entry(2 * k + 1, in_h, -1.0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -386,7 +400,11 @@ impl Component for FlowSplitter {
|
||||
}
|
||||
|
||||
fn signature(&self) -> String {
|
||||
format!("FlowSplitter(fluid={}, outlets={})", self.fluid_id, self.outlets.len())
|
||||
format!(
|
||||
"FlowSplitter(fluid={}, outlets={})",
|
||||
self.fluid_id,
|
||||
self.outlets.len()
|
||||
)
|
||||
}
|
||||
|
||||
fn to_params(&self) -> crate::ComponentParams {
|
||||
@@ -428,6 +446,10 @@ pub struct FlowMerger {
|
||||
outlet: ConnectedPort,
|
||||
/// Optional mass flow weights per inlet (kg/s). If None, equal weighting.
|
||||
mass_flow_weights: Option<Vec<f64>>,
|
||||
/// Captured (P,h) global state indices of the inlet edges (incoming).
|
||||
inlet_idx: Vec<(usize, usize)>,
|
||||
/// Captured (P,h) global state indices of the outlet edge (outgoing).
|
||||
outlet_idx: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl FlowMerger {
|
||||
@@ -482,6 +504,8 @@ impl FlowMerger {
|
||||
inlets,
|
||||
outlet,
|
||||
mass_flow_weights: None,
|
||||
inlet_idx: Vec::new(),
|
||||
outlet_idx: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -536,81 +560,90 @@ impl FlowMerger {
|
||||
|
||||
// ── Mixing helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Computes the mixed outlet enthalpy (weighted or equal).
|
||||
fn mixed_enthalpy(&self) -> f64 {
|
||||
/// Returns the mass-flow weights normalised so they sum to 1.0.
|
||||
///
|
||||
/// Falls back to equal weighting when no weights are set or the total is
|
||||
/// non-positive.
|
||||
fn normalized_weights(&self) -> Vec<f64> {
|
||||
let n = self.inlets.len();
|
||||
match &self.mass_flow_weights {
|
||||
Some(weights) => {
|
||||
let total_flow: f64 = weights.iter().sum();
|
||||
if total_flow <= 0.0 {
|
||||
// Fall back to equal weighting
|
||||
self.inlets
|
||||
.iter()
|
||||
.map(|p| p.enthalpy().to_joules_per_kg())
|
||||
.sum::<f64>()
|
||||
/ n as f64
|
||||
let total: f64 = weights.iter().sum();
|
||||
if total <= 0.0 {
|
||||
vec![1.0 / n as f64; n]
|
||||
} else {
|
||||
self.inlets
|
||||
.iter()
|
||||
.zip(weights.iter())
|
||||
.map(|(p, &w)| p.enthalpy().to_joules_per_kg() * w)
|
||||
.sum::<f64>()
|
||||
/ total_flow
|
||||
weights.iter().map(|&w| w / total).collect()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Equal weighting
|
||||
self.inlets
|
||||
.iter()
|
||||
.map(|p| p.enthalpy().to_joules_per_kg())
|
||||
.sum::<f64>()
|
||||
/ n as f64
|
||||
}
|
||||
None => vec![1.0 / n as f64; n],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FlowMerger {
|
||||
/// `N + 1` equations:
|
||||
/// - `N−1` pressure equalisations across inlets
|
||||
/// - `1` mixing enthalpy for the outlet
|
||||
/// - `1` outlet pressure consistency
|
||||
/// `2` equations — the merger owns its single outgoing edge:
|
||||
/// - `P_out − P_in_0 = 0` (outlet pressure = reference inlet pressure)
|
||||
/// - `h_out − Σ wₖ·h_in_k = 0` (mass-weighted enthalpy mixing)
|
||||
fn n_equations(&self) -> usize {
|
||||
self.inlets.len() + 1
|
||||
2
|
||||
}
|
||||
|
||||
fn set_system_context(
|
||||
&mut self,
|
||||
_state_offset: usize,
|
||||
external_edge_state_indices: &[(usize, usize, usize)],
|
||||
) {
|
||||
// Layout: [0..N] = incoming inlet edges, [N] = outgoing outlet edge.
|
||||
// Triple: (m_idx, p_idx, h_idx) — extract (p, h) pairs for residuals.
|
||||
let n = self.inlets.len();
|
||||
if external_edge_state_indices.len() >= n {
|
||||
self.inlet_idx = external_edge_state_indices[0..n]
|
||||
.iter()
|
||||
.map(|&(_, p, h)| (p, h))
|
||||
.collect();
|
||||
}
|
||||
if external_edge_state_indices.len() >= n + 1 {
|
||||
self.outlet_idx = Some((
|
||||
external_edge_state_indices[n].1,
|
||||
external_edge_state_indices[n].2,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
let n_eqs = self.n_equations();
|
||||
if residuals.len() < n_eqs {
|
||||
if residuals.len() < 2 {
|
||||
return Err(ComponentError::InvalidResidualDimensions {
|
||||
expected: n_eqs,
|
||||
expected: 2,
|
||||
actual: residuals.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let p_ref = self.inlets[0].pressure().to_pascals();
|
||||
let mut r_idx = 0;
|
||||
let (out_p, out_h) = match self.outlet_idx {
|
||||
Some(idx) if self.inlet_idx.len() == self.inlets.len() => idx,
|
||||
_ => {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"FlowMerger requires all live inlet edges and one live outlet edge".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// --- Pressure equalisation: inlets 1..N must match inlet 0 ---
|
||||
for k in 1..self.inlets.len() {
|
||||
let p_k = self.inlets[k].pressure().to_pascals();
|
||||
residuals[r_idx] = p_k - p_ref;
|
||||
r_idx += 1;
|
||||
}
|
||||
// r0: outlet pressure tracks the (reference) first inlet pressure.
|
||||
let p_ref = state[self.inlet_idx[0].0];
|
||||
residuals[0] = state[out_p] - p_ref;
|
||||
|
||||
// --- Outlet pressure = reference inlet pressure ---
|
||||
let p_out = self.outlet.pressure().to_pascals();
|
||||
residuals[r_idx] = p_out - p_ref;
|
||||
r_idx += 1;
|
||||
|
||||
// --- Mixing enthalpy ---
|
||||
let h_mixed = self.mixed_enthalpy();
|
||||
let h_out = self.outlet.enthalpy().to_joules_per_kg();
|
||||
residuals[r_idx] = h_out - h_mixed;
|
||||
// r1: mass-weighted enthalpy mixing.
|
||||
let weights = self.normalized_weights();
|
||||
let h_mix: f64 = self
|
||||
.inlet_idx
|
||||
.iter()
|
||||
.zip(weights.iter())
|
||||
.map(|(&(_, h_idx), &w)| w * state[h_idx])
|
||||
.sum();
|
||||
residuals[1] = state[out_h] - h_mix;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -620,11 +653,25 @@ impl Component for FlowMerger {
|
||||
_state: &StateSlice,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// Diagonal approximation — the full coupling is resolved by the System
|
||||
// assembler through the edge state indices.
|
||||
let n_eqs = self.n_equations();
|
||||
for i in 0..n_eqs {
|
||||
jacobian.add_entry(i, i, 1.0);
|
||||
let (out_p, out_h) = match self.outlet_idx {
|
||||
Some(idx) if self.inlet_idx.len() == self.inlets.len() => idx,
|
||||
_ => {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"FlowMerger Jacobian requires all live inlet edges and one live outlet edge"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// r0 = P_out - P_in_0
|
||||
jacobian.add_entry(0, out_p, 1.0);
|
||||
jacobian.add_entry(0, self.inlet_idx[0].0, -1.0);
|
||||
|
||||
// r1 = h_out - Σ wₖ·h_in_k
|
||||
jacobian.add_entry(1, out_h, 1.0);
|
||||
let weights = self.normalized_weights();
|
||||
for (&(_, h_idx), &w) in self.inlet_idx.iter().zip(weights.iter()) {
|
||||
jacobian.add_entry(1, h_idx, -w);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -693,7 +740,11 @@ impl Component for FlowMerger {
|
||||
}
|
||||
|
||||
fn signature(&self) -> String {
|
||||
format!("FlowMerger(fluid={}, inlets={})", self.fluid_id, self.inlets.len())
|
||||
format!(
|
||||
"FlowMerger(fluid={}, inlets={})",
|
||||
self.fluid_id,
|
||||
self.inlets.len()
|
||||
)
|
||||
}
|
||||
|
||||
fn to_params(&self) -> crate::ComponentParams {
|
||||
@@ -762,8 +813,8 @@ mod tests {
|
||||
let s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
|
||||
assert_eq!(s.n_outlets(), 2);
|
||||
assert_eq!(s.fluid_kind(), FluidKind::Incompressible);
|
||||
// n_equations = 2*2 - 1 = 3
|
||||
assert_eq!(s.n_equations(), 3);
|
||||
// n_equations = 2*N = 4 (owns 2 outgoing edges, P+h each)
|
||||
assert_eq!(s.n_equations(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -776,8 +827,8 @@ mod tests {
|
||||
let s = FlowSplitter::compressible("R410A", inlet, vec![out_a, out_b, out_c]).unwrap();
|
||||
assert_eq!(s.n_outlets(), 3);
|
||||
assert_eq!(s.fluid_kind(), FluidKind::Compressible);
|
||||
// n_equations = 2*3 - 1 = 5
|
||||
assert_eq!(s.n_equations(), 5);
|
||||
// n_equations = 2*N = 6
|
||||
assert_eq!(s.n_equations(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -802,13 +853,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_splitter_residuals_zero_at_consistent_state() {
|
||||
// Consistent state: all pressures and enthalpies equal
|
||||
// Consistent state: all branch pressures/enthalpies equal the inlet.
|
||||
let inlet = make_port("Water", 3.0e5, 2.0e5);
|
||||
let out_a = make_port("Water", 3.0e5, 2.0e5);
|
||||
let out_b = make_port("Water", 3.0e5, 2.0e5);
|
||||
|
||||
let s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
|
||||
let state = vec![0.0; 6]; // dummy, not used by current impl
|
||||
let mut s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
|
||||
// Edges (CM1.3 stride-3): inlet=(ṁ@0,P@1,h@2), out_a=(ṁ@3,P@4,h@5), out_b=(ṁ@6,P@7,h@8)
|
||||
s.set_system_context(0, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);
|
||||
|
||||
// State: ṁ slots are don't-care (splitter ignores them); P and h are consistent.
|
||||
let state = vec![0.0, 3.0e5, 2.0e5, 0.0, 3.0e5, 2.0e5, 0.0, 3.0e5, 2.0e5];
|
||||
let mut res = vec![0.0; s.n_equations()];
|
||||
s.compute_residuals(&state, &mut res).unwrap();
|
||||
|
||||
@@ -825,11 +880,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_splitter_residuals_nonzero_on_pressure_mismatch() {
|
||||
let inlet = make_port("Water", 3.0e5, 2.0e5);
|
||||
let out_a = make_port("Water", 2.5e5, 2.0e5); // lower pressure!
|
||||
let out_a = make_port("Water", 3.0e5, 2.0e5);
|
||||
let out_b = make_port("Water", 3.0e5, 2.0e5);
|
||||
|
||||
let s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
|
||||
let state = vec![0.0; 6];
|
||||
let mut s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
|
||||
s.set_system_context(0, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);
|
||||
|
||||
// out_a pressure lower than inlet by 0.5e5; ṁ slots are don't-care.
|
||||
let state = vec![0.0, 3.0e5, 2.0e5, 0.0, 2.5e5, 2.0e5, 0.0, 3.0e5, 2.0e5];
|
||||
let mut res = vec![0.0; s.n_equations()];
|
||||
s.compute_residuals(&state, &mut res).unwrap();
|
||||
|
||||
@@ -846,8 +904,8 @@ mod tests {
|
||||
let inlet = make_port("Water", 3.0e5, 2.0e5);
|
||||
let outlets: Vec<_> = (0..3).map(|_| make_port("Water", 3.0e5, 2.0e5)).collect();
|
||||
let s = FlowSplitter::incompressible("Water", inlet, outlets).unwrap();
|
||||
// N=3 → 2*3-1 = 5
|
||||
assert_eq!(s.n_equations(), 5);
|
||||
// N=3 → 2*N = 6
|
||||
assert_eq!(s.n_equations(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -872,8 +930,8 @@ mod tests {
|
||||
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
|
||||
assert_eq!(m.n_inlets(), 2);
|
||||
assert_eq!(m.fluid_kind(), FluidKind::Incompressible);
|
||||
// N=2 → N+1 = 3
|
||||
assert_eq!(m.n_equations(), 3);
|
||||
// Merger owns its single outgoing edge → 2 equations (P + h)
|
||||
assert_eq!(m.n_equations(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -886,8 +944,8 @@ mod tests {
|
||||
let m = FlowMerger::compressible("R134a", vec![in_a, in_b, in_c], outlet).unwrap();
|
||||
assert_eq!(m.n_inlets(), 3);
|
||||
assert_eq!(m.fluid_kind(), FluidKind::Compressible);
|
||||
// N=3 → N+1 = 4
|
||||
assert_eq!(m.n_equations(), 4);
|
||||
// Merger owns its single outgoing edge → 2 equations (P + h)
|
||||
assert_eq!(m.n_equations(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -907,8 +965,11 @@ mod tests {
|
||||
let in_b = make_port("Water", p, h);
|
||||
let outlet = make_port("Water", p, h); // h_mixed = (h+h)/2 = h
|
||||
|
||||
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
|
||||
let state = vec![0.0; 6];
|
||||
let mut m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
|
||||
// Edges (CM1.3 stride-3): in_a=(ṁ@0,P@1,h@2), in_b=(ṁ@3,P@4,h@5), outlet=(ṁ@6,P@7,h@8)
|
||||
m.set_system_context(0, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);
|
||||
|
||||
let state = vec![0.0, p, h, 0.0, p, h, 0.0, p, h];
|
||||
let mut res = vec![0.0; m.n_equations()];
|
||||
m.compute_residuals(&state, &mut res).unwrap();
|
||||
|
||||
@@ -928,8 +989,10 @@ mod tests {
|
||||
let in_b = make_port("Water", p, h_b);
|
||||
let outlet = make_port("Water", p, h_expected);
|
||||
|
||||
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
|
||||
let state = vec![0.0; 6];
|
||||
let mut m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
|
||||
m.set_system_context(0, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);
|
||||
|
||||
let state = vec![0.0, p, h_a, 0.0, p, h_b, 0.0, p, h_expected];
|
||||
let mut res = vec![0.0; m.n_equations()];
|
||||
m.compute_residuals(&state, &mut res).unwrap();
|
||||
|
||||
@@ -948,12 +1011,13 @@ mod tests {
|
||||
let in_b = make_port("Water", p, 3.0e5);
|
||||
let outlet = make_port("Water", p, 2.7e5);
|
||||
|
||||
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet)
|
||||
let mut m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet)
|
||||
.unwrap()
|
||||
.with_mass_flows(vec![0.3, 0.7])
|
||||
.unwrap();
|
||||
m.set_system_context(0, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);
|
||||
|
||||
let state = vec![0.0; 6];
|
||||
let state = vec![0.0, p, 2.0e5, 0.0, p, 3.0e5, 0.0, p, 2.7e5];
|
||||
let mut res = vec![0.0; m.n_equations()];
|
||||
m.compute_residuals(&state, &mut res).unwrap();
|
||||
|
||||
@@ -973,7 +1037,7 @@ mod tests {
|
||||
|
||||
let merger: Box<dyn Component> =
|
||||
Box::new(FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap());
|
||||
assert_eq!(merger.n_equations(), 3);
|
||||
assert_eq!(merger.n_equations(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -984,7 +1048,7 @@ mod tests {
|
||||
|
||||
let splitter: Box<dyn Component> =
|
||||
Box::new(FlowSplitter::compressible("R410A", inlet, vec![out_a, out_b]).unwrap());
|
||||
assert_eq!(splitter.n_equations(), 3);
|
||||
assert_eq!(splitter.n_equations(), 4);
|
||||
}
|
||||
|
||||
// ── energy_transfers tests ─────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user