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:
@@ -163,10 +163,29 @@ pub struct Fan<State> {
|
||||
air_density_kg_per_m3: f64,
|
||||
/// Speed ratio (0.0 to 1.0)
|
||||
speed_ratio: f64,
|
||||
/// VFD part-load efficiency curve (quadratic in speed ratio), default ≈0.97.
|
||||
vfd_eff_coeffs: [f64; 3],
|
||||
/// Motor part-load efficiency curve (quadratic in speed ratio), default ≈0.92.
|
||||
motor_eff_coeffs: [f64; 3],
|
||||
/// When true, `fan_power` returns wire-to-air electrical power.
|
||||
use_drive_chain: bool,
|
||||
/// Circuit identifier
|
||||
circuit_id: CircuitId,
|
||||
/// Operational state
|
||||
operational_state: OperationalState,
|
||||
/// When true, the fan participates in the (P,h) graph solver as a 2-port
|
||||
/// element imposing a design-point static pressure rise.
|
||||
edge_coupled: bool,
|
||||
/// Design volumetric flow (m³/s) at which the curve pressure rise is read.
|
||||
design_flow_m3_s: f64,
|
||||
/// Captured (m,P,h) global state indices of the inlet edge (incoming).
|
||||
inlet_m_idx: Option<usize>,
|
||||
inlet_p_idx: Option<usize>,
|
||||
inlet_h_idx: Option<usize>,
|
||||
/// Captured (m,P,h) global state indices of the outlet edge (outgoing).
|
||||
outlet_m_idx: Option<usize>,
|
||||
outlet_p_idx: Option<usize>,
|
||||
outlet_h_idx: Option<usize>,
|
||||
/// Phantom data for type state
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
@@ -204,12 +223,41 @@ impl Fan<Disconnected> {
|
||||
port_outlet,
|
||||
air_density_kg_per_m3: air_density,
|
||||
speed_ratio: 1.0,
|
||||
vfd_eff_coeffs: [0.97, 0.0, 0.0],
|
||||
motor_eff_coeffs: [0.92, 0.0, 0.0],
|
||||
use_drive_chain: false,
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
edge_coupled: false,
|
||||
design_flow_m3_s: 0.0,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Enables Bernier–Bourret wire-to-air drive chain (η_VFD × η_motor × η_fan).
|
||||
pub fn with_drive_chain(mut self, enabled: bool) -> Self {
|
||||
self.use_drive_chain = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets quadratic VFD efficiency coefficients η = a0 + a1·N* + a2·N*².
|
||||
pub fn with_vfd_efficiency(mut self, a0: f64, a1: f64, a2: f64) -> Self {
|
||||
self.vfd_eff_coeffs = [a0, a1, a2];
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets quadratic motor efficiency coefficients η = a0 + a1·N* + a2·N*².
|
||||
pub fn with_motor_efficiency(mut self, a0: f64, a1: f64, a2: f64) -> Self {
|
||||
self.motor_eff_coeffs = [a0, a1, a2];
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the fluid identifier.
|
||||
pub fn fluid_id(&self) -> &FluidId {
|
||||
self.port_inlet.fluid_id()
|
||||
@@ -235,6 +283,35 @@ impl Fan<Disconnected> {
|
||||
self.speed_ratio = ratio;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Connects the fan to inlet and outlet ports, transitioning the type-state
|
||||
/// from `Disconnected` to `Connected` at compile time.
|
||||
pub fn connect(
|
||||
self,
|
||||
inlet: Port<Disconnected>,
|
||||
outlet: Port<Disconnected>,
|
||||
) -> Result<Fan<Connected>, ComponentError> {
|
||||
let (p_in, _) = self
|
||||
.port_inlet
|
||||
.connect(inlet)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
let (p_out, _) = self
|
||||
.port_outlet
|
||||
.connect(outlet)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
|
||||
let mut fan = Fan::<Connected>::from_connected_parts(
|
||||
self.curves,
|
||||
p_in,
|
||||
p_out,
|
||||
self.air_density_kg_per_m3,
|
||||
)?;
|
||||
fan.set_speed_ratio(self.speed_ratio)?;
|
||||
fan.vfd_eff_coeffs = self.vfd_eff_coeffs;
|
||||
fan.motor_eff_coeffs = self.motor_eff_coeffs;
|
||||
fan.use_drive_chain = self.use_drive_chain;
|
||||
Ok(fan)
|
||||
}
|
||||
}
|
||||
|
||||
impl Fan<Connected> {
|
||||
@@ -256,12 +333,36 @@ impl Fan<Connected> {
|
||||
port_outlet,
|
||||
air_density_kg_per_m3: air_density,
|
||||
speed_ratio: 1.0,
|
||||
vfd_eff_coeffs: [0.97, 0.0, 0.0],
|
||||
motor_eff_coeffs: [0.92, 0.0, 0.0],
|
||||
use_drive_chain: false,
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
edge_coupled: false,
|
||||
design_flow_m3_s: 0.0,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Enables the edge-coupled (P,h) solver model, imposing a design-point
|
||||
/// static pressure rise read from the fan curve at `design_flow_m3_s`
|
||||
/// (and the current speed ratio). Shaft power is added to the air stream as
|
||||
/// an enthalpy rise so the coupled model satisfies the First Law.
|
||||
pub fn with_edge_coupling(mut self, design_flow_m3_s: f64) -> Self {
|
||||
self.design_flow_m3_s = design_flow_m3_s.max(0.0);
|
||||
self.edge_coupled = true;
|
||||
if self.operational_state == OperationalState::Off {
|
||||
self.operational_state = OperationalState::On;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the inlet port.
|
||||
pub fn port_inlet(&self) -> &Port<Connected> {
|
||||
&self.port_inlet
|
||||
@@ -341,10 +442,8 @@ impl Fan<Connected> {
|
||||
self.curves.efficiency_at_flow(equivalent_flow)
|
||||
}
|
||||
|
||||
/// Calculates the fan power consumption.
|
||||
///
|
||||
/// P_fan = Q × P_s / η
|
||||
pub fn fan_power(&self, flow_m3_per_s: f64) -> Power {
|
||||
/// Shaft aerodynamic power `Q × ΔP / η_fan` [W].
|
||||
pub fn shaft_power(&self, flow_m3_per_s: f64) -> Power {
|
||||
if flow_m3_per_s <= 0.0 || self.speed_ratio <= 0.0 {
|
||||
return Power::from_watts(0.0);
|
||||
}
|
||||
@@ -360,6 +459,64 @@ impl Fan<Connected> {
|
||||
Power::from_watts(power_w)
|
||||
}
|
||||
|
||||
/// Drive-chain efficiency η_VFD(N*) × η_motor(N*) at the current speed ratio.
|
||||
pub fn drive_chain_efficiency(&self) -> f64 {
|
||||
let n = self.speed_ratio.clamp(0.0, 1.0);
|
||||
let eta_vfd = (self.vfd_eff_coeffs[0]
|
||||
+ self.vfd_eff_coeffs[1] * n
|
||||
+ self.vfd_eff_coeffs[2] * n * n)
|
||||
.clamp(0.05, 1.0);
|
||||
let eta_motor = (self.motor_eff_coeffs[0]
|
||||
+ self.motor_eff_coeffs[1] * n
|
||||
+ self.motor_eff_coeffs[2] * n * n)
|
||||
.clamp(0.05, 1.0);
|
||||
eta_vfd * eta_motor
|
||||
}
|
||||
|
||||
/// Fan power consumption.
|
||||
///
|
||||
/// Shaft power by default; electrical wire-to-air power when drive chain is enabled:
|
||||
/// `P_elec = P_shaft / (η_VFD · η_motor)`.
|
||||
pub fn fan_power(&self, flow_m3_per_s: f64) -> Power {
|
||||
let shaft = self.shaft_power(flow_m3_per_s);
|
||||
if !self.use_drive_chain {
|
||||
return shaft;
|
||||
}
|
||||
let eta_chain = self.drive_chain_efficiency();
|
||||
if eta_chain <= 0.0 {
|
||||
return Power::from_watts(0.0);
|
||||
}
|
||||
Power::from_watts(shaft.to_watts() / eta_chain)
|
||||
}
|
||||
|
||||
/// Enables or disables the wire-to-air drive chain.
|
||||
pub fn set_drive_chain(&mut self, enabled: bool) {
|
||||
self.use_drive_chain = enabled;
|
||||
}
|
||||
|
||||
fn edge_coupled_flow_and_power(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
inlet_m_idx: usize,
|
||||
) -> Result<(f64, f64, f64), ComponentError> {
|
||||
let mass_flow_kg_s = state.get(inlet_m_idx).copied().ok_or_else(|| {
|
||||
ComponentError::InvalidState(format!(
|
||||
"Fan edge-coupled inlet mass-flow index {inlet_m_idx} is outside the state vector"
|
||||
))
|
||||
})?;
|
||||
let flow_m3_s = mass_flow_kg_s / self.air_density_kg_per_m3;
|
||||
let power_w = match self.operational_state {
|
||||
OperationalState::Off | OperationalState::Bypass => 0.0,
|
||||
OperationalState::On => self.fan_power(flow_m3_s).to_watts(),
|
||||
};
|
||||
let enthalpy_rise_j_kg = if mass_flow_kg_s.abs() > 1e-12 {
|
||||
power_w / mass_flow_kg_s
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
Ok((flow_m3_s, power_w, enthalpy_rise_j_kg))
|
||||
}
|
||||
|
||||
/// Calculates mass flow from volumetric flow.
|
||||
pub fn mass_flow_from_volumetric(&self, flow_m3_per_s: f64) -> MassFlow {
|
||||
MassFlow::from_kg_per_s(flow_m3_per_s * self.air_density_kg_per_m3)
|
||||
@@ -398,97 +555,99 @@ impl Fan<Connected> {
|
||||
}
|
||||
|
||||
impl Component for Fan<Connected> {
|
||||
fn set_system_context(
|
||||
&mut self,
|
||||
_state_offset: usize,
|
||||
external_edge_state_indices: &[(usize, usize, usize)],
|
||||
) {
|
||||
// Layout: [0] = incoming edge, [1] = outgoing edge.
|
||||
// Triple: (m_idx, p_idx, h_idx)
|
||||
if !external_edge_state_indices.is_empty() {
|
||||
self.inlet_m_idx = Some(external_edge_state_indices[0].0);
|
||||
self.inlet_p_idx = Some(external_edge_state_indices[0].1);
|
||||
self.inlet_h_idx = Some(external_edge_state_indices[0].2);
|
||||
}
|
||||
if external_edge_state_indices.len() >= 2 {
|
||||
self.outlet_m_idx = Some(external_edge_state_indices[1].0);
|
||||
self.outlet_p_idx = Some(external_edge_state_indices[1].1);
|
||||
self.outlet_h_idx = Some(external_edge_state_indices[1].2);
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
if residuals.len() != self.n_equations() {
|
||||
return Err(ComponentError::InvalidResidualDimensions {
|
||||
expected: self.n_equations(),
|
||||
actual: residuals.len(),
|
||||
});
|
||||
}
|
||||
|
||||
match self.operational_state {
|
||||
OperationalState::Off => {
|
||||
residuals[0] = state[0];
|
||||
residuals[1] = 0.0;
|
||||
// Edge-coupled (P,h) model: impose a design-point static pressure rise.
|
||||
if self.edge_coupled {
|
||||
if let (Some(in_m), Some(in_p), Some(in_h), Some(out_p), Some(out_h)) = (
|
||||
self.inlet_m_idx,
|
||||
self.inlet_p_idx,
|
||||
self.inlet_h_idx,
|
||||
self.outlet_p_idx,
|
||||
self.outlet_h_idx,
|
||||
) {
|
||||
if residuals.len() < 2 {
|
||||
return Err(ComponentError::InvalidResidualDimensions {
|
||||
expected: 2,
|
||||
actual: residuals.len(),
|
||||
});
|
||||
}
|
||||
let dp = match self.operational_state {
|
||||
OperationalState::Off => 0.0,
|
||||
_ => {
|
||||
let (flow_m3_s, _, _) = self.edge_coupled_flow_and_power(state, in_m)?;
|
||||
self.static_pressure_rise(flow_m3_s)
|
||||
}
|
||||
};
|
||||
let (_, _, enthalpy_rise_j_kg) = self.edge_coupled_flow_and_power(state, in_m)?;
|
||||
// r0: imposed static pressure rise (fan adds pressure)
|
||||
residuals[0] = state[out_p] - (state[in_p] + dp);
|
||||
// r1: adiabatic fan casing, shaft power heats the air stream
|
||||
residuals[1] = state[out_h] - (state[in_h] + enthalpy_rise_j_kg);
|
||||
return Ok(());
|
||||
}
|
||||
OperationalState::Bypass => {
|
||||
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;
|
||||
return Ok(());
|
||||
}
|
||||
OperationalState::On => {}
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Fan edge-coupled model requires inlet and outlet edge state indices".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if state.len() < 2 {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 2,
|
||||
actual: state.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mass_flow_kg_s = state[0];
|
||||
let _power_w = state[1];
|
||||
|
||||
let flow_m3_s = mass_flow_kg_s / self.air_density_kg_per_m3;
|
||||
let delta_p_calc = self.static_pressure_rise(flow_m3_s);
|
||||
|
||||
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;
|
||||
|
||||
residuals[0] = delta_p_calc - delta_p_actual;
|
||||
|
||||
let power_calc = self.fan_power(flow_m3_s).to_watts();
|
||||
residuals[1] = power_calc - _power_w;
|
||||
|
||||
Ok(())
|
||||
Err(ComponentError::InvalidState(
|
||||
"Fan physical simulation requires edge-coupled inlet/outlet state indices".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
_state: &StateSlice,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
if state.len() < 2 {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 2,
|
||||
actual: state.len(),
|
||||
});
|
||||
// Edge-coupled (P,h) model.
|
||||
if self.edge_coupled {
|
||||
if let (Some(_in_m), Some(in_p), Some(in_h), Some(out_p), Some(out_h)) = (
|
||||
self.inlet_m_idx,
|
||||
self.inlet_p_idx,
|
||||
self.inlet_h_idx,
|
||||
self.outlet_p_idx,
|
||||
self.outlet_h_idx,
|
||||
) {
|
||||
// r0 = P_out - (P_in + dp)
|
||||
jacobian.add_entry(0, out_p, 1.0);
|
||||
jacobian.add_entry(0, in_p, -1.0);
|
||||
// r1 = h_out - h_in
|
||||
jacobian.add_entry(1, out_h, 1.0);
|
||||
jacobian.add_entry(1, in_h, -1.0);
|
||||
return Ok(());
|
||||
}
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Fan edge-coupled model requires inlet and outlet edge state indices".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mass_flow_kg_s = state[0];
|
||||
let flow_m3_s = mass_flow_kg_s / self.air_density_kg_per_m3;
|
||||
|
||||
let h = 0.001;
|
||||
let p_plus = self.static_pressure_rise(flow_m3_s + h / self.air_density_kg_per_m3);
|
||||
let p_minus = self.static_pressure_rise(flow_m3_s - h / self.air_density_kg_per_m3);
|
||||
let dp_dm = (p_plus - p_minus) / (2.0 * h);
|
||||
|
||||
jacobian.add_entry(0, 0, dp_dm);
|
||||
jacobian.add_entry(0, 1, 0.0);
|
||||
|
||||
let pow_plus = self
|
||||
.fan_power(flow_m3_s + h / self.air_density_kg_per_m3)
|
||||
.to_watts();
|
||||
let pow_minus = self
|
||||
.fan_power(flow_m3_s - h / self.air_density_kg_per_m3)
|
||||
.to_watts();
|
||||
let dpow_dm = (pow_plus - pow_minus) / (2.0 * h);
|
||||
|
||||
jacobian.add_entry(1, 0, dpow_dm);
|
||||
jacobian.add_entry(1, 1, -1.0);
|
||||
|
||||
Ok(())
|
||||
Err(ComponentError::InvalidState(
|
||||
"Fan physical simulation requires edge-coupled inlet/outlet state indices".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
@@ -503,30 +662,57 @@ impl Component for Fan<Connected> {
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
if state.len() < 1 {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 1,
|
||||
actual: state.len(),
|
||||
});
|
||||
if self.edge_coupled {
|
||||
let (Some(in_m), Some(out_m)) = (self.inlet_m_idx, self.outlet_m_idx) else {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Fan edge-coupled model requires 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: max_idx + 1,
|
||||
actual: state.len(),
|
||||
});
|
||||
}
|
||||
return Ok(vec![
|
||||
entropyk_core::MassFlow::from_kg_per_s(state[in_m]),
|
||||
entropyk_core::MassFlow::from_kg_per_s(-state[out_m]),
|
||||
]);
|
||||
}
|
||||
// Fan has inlet and outlet with same mass flow (air is incompressible for HVAC applications)
|
||||
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()),
|
||||
])
|
||||
|
||||
Err(ComponentError::InvalidState(
|
||||
"Fan mass-flow reporting requires edge-coupled inlet/outlet indices".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn port_enthalpies(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
|
||||
// Fan uses internally simulated enthalpies
|
||||
Ok(vec![
|
||||
self.port_inlet.enthalpy(),
|
||||
self.port_outlet.enthalpy(),
|
||||
])
|
||||
if self.edge_coupled {
|
||||
let (Some(in_h), Some(out_h)) = (self.inlet_h_idx, self.outlet_h_idx) else {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Fan edge-coupled model requires 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(),
|
||||
});
|
||||
}
|
||||
return Ok(vec![
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(state[in_h]),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(state[out_h]),
|
||||
]);
|
||||
}
|
||||
|
||||
Err(ComponentError::InvalidState(
|
||||
"Fan enthalpy reporting requires edge-coupled inlet/outlet indices".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn energy_transfers(
|
||||
@@ -539,10 +725,21 @@ impl Component for Fan<Connected> {
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
)),
|
||||
OperationalState::On => {
|
||||
if state.is_empty() {
|
||||
return None;
|
||||
if self.edge_coupled {
|
||||
let Some(in_m) = self.inlet_m_idx else {
|
||||
return None;
|
||||
};
|
||||
let Ok((_, power_w, _)) = self.edge_coupled_flow_and_power(state, in_m) else {
|
||||
return None;
|
||||
};
|
||||
return Some((
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
entropyk_core::Power::from_watts(-power_w),
|
||||
));
|
||||
}
|
||||
let mass_flow_kg_s = state[0];
|
||||
|
||||
let in_m = self.inlet_m_idx?;
|
||||
let mass_flow_kg_s = *state.get(in_m)?;
|
||||
let flow_m3_s = mass_flow_kg_s / self.air_density_kg_per_m3;
|
||||
let power_calc = self.fan_power(flow_m3_s).to_watts();
|
||||
Some((
|
||||
@@ -632,12 +829,33 @@ mod tests {
|
||||
port_outlet: outlet_conn,
|
||||
air_density_kg_per_m3: 1.2,
|
||||
speed_ratio: 1.0,
|
||||
vfd_eff_coeffs: [0.97, 0.0, 0.0],
|
||||
motor_eff_coeffs: [0.92, 0.0, 0.0],
|
||||
use_drive_chain: false,
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
edge_coupled: false,
|
||||
design_flow_m3_s: 0.0,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wire_to_air_drive_chain_increases_power() {
|
||||
let mut fan = create_test_fan_connected();
|
||||
let shaft = fan.shaft_power(0.5).to_watts();
|
||||
fan.set_drive_chain(true);
|
||||
let elec = fan.fan_power(0.5).to_watts();
|
||||
assert!(elec > shaft);
|
||||
assert!((elec / shaft - 1.0 / (0.97 * 0.92)).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fan_curves_creation() {
|
||||
let curves = create_test_curves();
|
||||
|
||||
Reference in New Issue
Block a user