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:
@@ -390,6 +390,19 @@ pub struct Compressor<State> {
|
||||
circuit_id: CircuitId,
|
||||
/// Operational state: On, Off, or Bypass (FR6-FR8)
|
||||
operational_state: OperationalState,
|
||||
/// State-vector index: suction mass flow (incoming edge, CM1.3)
|
||||
suction_m_idx: Option<usize>,
|
||||
/// State-vector index: suction enthalpy (incoming edge, CM1.3)
|
||||
suction_h_idx: Option<usize>,
|
||||
/// State-vector index: discharge mass flow (outgoing edge, CM1.3)
|
||||
discharge_m_idx: Option<usize>,
|
||||
/// State-vector index: discharge enthalpy (outgoing edge, CM1.3)
|
||||
discharge_h_idx: Option<usize>,
|
||||
/// True when suction and discharge share the same ṁ state index (same
|
||||
/// series branch). In this case the mass-conservation residual
|
||||
/// `ṁ_dis − ṁ_suc = 0` is trivially satisfied and must be dropped to
|
||||
/// keep the system square (CM1.4).
|
||||
same_branch_m: bool,
|
||||
/// Phantom data for type state
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
@@ -560,6 +573,11 @@ impl Compressor<Disconnected> {
|
||||
|
||||
circuit_id: CircuitId::default(), // Default circuit
|
||||
operational_state: OperationalState::default(), // Default to On
|
||||
suction_m_idx: None,
|
||||
suction_h_idx: None,
|
||||
discharge_m_idx: None,
|
||||
discharge_h_idx: None,
|
||||
same_branch_m: false,
|
||||
_state: PhantomData,
|
||||
})
|
||||
}
|
||||
@@ -682,6 +700,11 @@ impl Compressor<Disconnected> {
|
||||
fluid_id: self.fluid_id,
|
||||
circuit_id: self.circuit_id,
|
||||
operational_state: self.operational_state,
|
||||
suction_m_idx: None,
|
||||
suction_h_idx: None,
|
||||
discharge_m_idx: None,
|
||||
discharge_h_idx: None,
|
||||
same_branch_m: false,
|
||||
_state: PhantomData,
|
||||
})
|
||||
}
|
||||
@@ -785,10 +808,15 @@ impl Compressor<Connected> {
|
||||
let mass_flow_kg_per_s = match &self.model {
|
||||
CompressorModel::Ahri540(coeffs) => {
|
||||
// Calculate volumetric efficiency using inverse pressure ratio
|
||||
// η_vol = 1 - (P_suction/P_discharge)^(1/M2)
|
||||
// η_vol = f_etav × (1 - (P_suction/P_discharge)^(1/M2))
|
||||
// f_etav is read dynamically from the state vector when wired
|
||||
// as a calibration unknown (Story 5.5), like f_m / f_power.
|
||||
let f_etav = state
|
||||
.and_then(|st| self.calib_indices.z_etav.map(|idx| st[idx]))
|
||||
.unwrap_or(self.calib.z_etav);
|
||||
let inverse_pressure_ratio = p_suction / p_discharge;
|
||||
let volumetric_efficiency = (1.0 - inverse_pressure_ratio.powf(1.0 / coeffs.m2))
|
||||
* self.calib.f_etav;
|
||||
let volumetric_efficiency =
|
||||
(1.0 - inverse_pressure_ratio.powf(1.0 / coeffs.m2)) * f_etav;
|
||||
|
||||
if volumetric_efficiency < 0.0 {
|
||||
return Err(ComponentError::NumericalError(
|
||||
@@ -816,11 +844,11 @@ impl Compressor<Connected> {
|
||||
// Apply calibration: ṁ_eff = f_m × ṁ_nominal
|
||||
let f_m = if let Some(st) = state {
|
||||
self.calib_indices
|
||||
.f_m
|
||||
.z_flow
|
||||
.map(|idx| st[idx])
|
||||
.unwrap_or(self.calib.f_m)
|
||||
.unwrap_or(self.calib.z_flow)
|
||||
} else {
|
||||
self.calib.f_m
|
||||
self.calib.z_flow
|
||||
};
|
||||
Ok(MassFlow::from_kg_per_s(mass_flow_kg_per_s * f_m))
|
||||
}
|
||||
@@ -861,11 +889,11 @@ impl Compressor<Connected> {
|
||||
// Ẇ_eff = f_power × Ẇ_nominal
|
||||
let f_power = if let Some(st) = state {
|
||||
self.calib_indices
|
||||
.f_power
|
||||
.z_power
|
||||
.map(|idx| st[idx])
|
||||
.unwrap_or(self.calib.f_power)
|
||||
.unwrap_or(self.calib.z_power)
|
||||
} else {
|
||||
self.calib.f_power
|
||||
self.calib.z_power
|
||||
};
|
||||
power_nominal * f_power
|
||||
}
|
||||
@@ -907,11 +935,11 @@ impl Compressor<Connected> {
|
||||
// Ẇ_eff = f_power × Ẇ_nominal
|
||||
let f_power = if let Some(st) = state {
|
||||
self.calib_indices
|
||||
.f_power
|
||||
.z_power
|
||||
.map(|idx| st[idx])
|
||||
.unwrap_or(self.calib.f_power)
|
||||
.unwrap_or(self.calib.z_power)
|
||||
} else {
|
||||
self.calib.f_power
|
||||
self.calib.z_power
|
||||
};
|
||||
power_nominal * f_power
|
||||
}
|
||||
@@ -1061,6 +1089,28 @@ impl Compressor<Connected> {
|
||||
}
|
||||
|
||||
impl Component for Compressor<Connected> {
|
||||
fn set_system_context(
|
||||
&mut self,
|
||||
_state_offset: usize,
|
||||
external_edge_state_indices: &[(usize, usize, usize)],
|
||||
) {
|
||||
// Layout: [0] = incoming suction edge, [1] = outgoing discharge edge.
|
||||
// Triple: (m_idx, p_idx, h_idx)
|
||||
if !external_edge_state_indices.is_empty() {
|
||||
self.suction_m_idx = Some(external_edge_state_indices[0].0);
|
||||
self.suction_h_idx = Some(external_edge_state_indices[0].2);
|
||||
}
|
||||
if external_edge_state_indices.len() >= 2 {
|
||||
self.discharge_m_idx = Some(external_edge_state_indices[1].0);
|
||||
self.discharge_h_idx = Some(external_edge_state_indices[1].2);
|
||||
}
|
||||
// CM1.4: detect same-branch topology → conservation residual becomes trivial.
|
||||
self.same_branch_m = matches!(
|
||||
(self.suction_m_idx, self.discharge_m_idx),
|
||||
(Some(suc), Some(dis)) if suc == dis
|
||||
);
|
||||
}
|
||||
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
@@ -1074,17 +1124,32 @@ impl Component for Compressor<Connected> {
|
||||
});
|
||||
}
|
||||
|
||||
let (suc_m_idx, suc_h_idx, dis_m_idx, dis_h_idx) = match (
|
||||
self.suction_m_idx,
|
||||
self.suction_h_idx,
|
||||
self.discharge_m_idx,
|
||||
self.discharge_h_idx,
|
||||
) {
|
||||
(Some(sm), Some(sh), Some(dm), Some(dh)) => (sm, sh, dm, dh),
|
||||
_ => {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Compressor requires live suction/discharge mass-flow and enthalpy indices"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle operational states (FR6-FR8)
|
||||
match self.operational_state {
|
||||
OperationalState::Off => {
|
||||
// In Off state, mass flow is zero (FR7)
|
||||
residuals[0] = state[0]; // ṁ = 0
|
||||
residuals[0] = state[suc_m_idx]; // ṁ_suction = 0
|
||||
residuals[1] = 0.0; // No energy transfer
|
||||
residuals[2] = state[dis_m_idx] - state[suc_m_idx]; // mass conservation
|
||||
return Ok(());
|
||||
}
|
||||
OperationalState::Bypass => {
|
||||
// In Bypass state, behaves as adiabatic pipe (FR8)
|
||||
// P_in = P_out and h_in = h_out
|
||||
let p_suction = self.port_suction.pressure().to_pascals();
|
||||
let p_discharge = self.port_discharge.pressure().to_pascals();
|
||||
let h_suction = self.port_suction.enthalpy().to_joules_per_kg();
|
||||
@@ -1092,6 +1157,7 @@ impl Component for Compressor<Connected> {
|
||||
|
||||
residuals[0] = p_suction - p_discharge; // Pressure continuity
|
||||
residuals[1] = h_suction - h_discharge; // Enthalpy continuity (adiabatic)
|
||||
residuals[2] = state[dis_m_idx] - state[suc_m_idx]; // mass conservation
|
||||
return Ok(());
|
||||
}
|
||||
OperationalState::On => {
|
||||
@@ -1099,20 +1165,10 @@ impl Component for Compressor<Connected> {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate state vector has minimum required dimensions
|
||||
// We need at least 4 values: mass_flow, h_suction, h_discharge, power
|
||||
if state.len() < 4 {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 4,
|
||||
actual: state.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// Extract state variables
|
||||
let mass_flow_state = state[0]; // kg/s
|
||||
let h_suction = state[1]; // J/kg
|
||||
let h_discharge = state[2]; // J/kg
|
||||
let _power_state = state[3]; // W
|
||||
// Extract state variables using stored edge indices (CM1.3)
|
||||
let mass_flow_state = state[suc_m_idx]; // kg/s — ṁ at suction edge
|
||||
let h_suction = state[suc_h_idx]; // J/kg
|
||||
let h_discharge = state[dis_h_idx]; // J/kg
|
||||
|
||||
// Get port values
|
||||
let p_suction = self.port_suction.pressure().to_pascals();
|
||||
@@ -1154,6 +1210,13 @@ impl Component for Compressor<Connected> {
|
||||
|
||||
residuals[1] = power_calc - mass_flow_state * enthalpy_change / self.mechanical_efficiency;
|
||||
|
||||
// r2: ṁ_discharge − ṁ_suction = 0 (mass conservation, CM1.3)
|
||||
// CM1.4: skip when same_branch_m — ṁ_dis == ṁ_suc (same state index),
|
||||
// so the residual would be trivially 0 and is excluded from n_equations().
|
||||
if !self.same_branch_m {
|
||||
residuals[2] = state[dis_m_idx] - state[suc_m_idx];
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1162,139 +1225,160 @@ impl Component for Compressor<Connected> {
|
||||
state: &StateSlice,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// Validate state vector
|
||||
if state.len() < 4 {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 4,
|
||||
actual: state.len(),
|
||||
});
|
||||
}
|
||||
let (suc_m_idx, suc_h_idx, dis_m_idx, dis_h_idx) = match (
|
||||
self.suction_m_idx,
|
||||
self.suction_h_idx,
|
||||
self.discharge_m_idx,
|
||||
self.discharge_h_idx,
|
||||
) {
|
||||
(Some(sm), Some(sh), Some(dm), Some(dh)) => (sm, sh, dm, dh),
|
||||
_ => {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Compressor Jacobian requires live suction/discharge mass-flow and enthalpy indices".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract state variables
|
||||
let mass_flow_state = state[0];
|
||||
let h_suction = state[1];
|
||||
let h_discharge = state[2];
|
||||
let mass_flow_state = state[suc_m_idx];
|
||||
let h_suction = state[suc_h_idx];
|
||||
let h_discharge = state[dis_h_idx];
|
||||
|
||||
// Get port values
|
||||
let p_suction = self.port_suction.pressure().to_pascals();
|
||||
let p_discharge = self.port_discharge.pressure().to_pascals();
|
||||
|
||||
// Calculate temperatures for SST/SDT model
|
||||
let _t_suction_k =
|
||||
estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction).unwrap_or(273.15);
|
||||
let t_discharge_k = estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)
|
||||
.unwrap_or(273.15);
|
||||
let t_suction_k = estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)?;
|
||||
let t_discharge_k = estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)?;
|
||||
|
||||
// Row 0: Mass flow residual
|
||||
// ∂r₀/∂ṁ = -1
|
||||
jacobian.add_entry(0, 0, -1.0);
|
||||
// Row 0: Mass flow residual r0 = ṁ_calc − ṁ_suction
|
||||
// ∂r0/∂ṁ_suction = -1 (exact, CM1.3)
|
||||
jacobian.add_entry(0, suc_m_idx, -1.0);
|
||||
|
||||
// ∂r₀/∂h_suction - requires density derivative
|
||||
// For now, use finite difference approximation
|
||||
let dr0_dh_suction = approximate_derivative(
|
||||
// ∂r0/∂h_suction — numerical (density depends on h)
|
||||
let dr0_dh_suction = approximate_derivative_result(
|
||||
|h| {
|
||||
let density = estimate_density(self.fluid_id.as_str(), p_suction, h).unwrap_or(1.0);
|
||||
let t_k =
|
||||
estimate_temperature(self.fluid_id.as_str(), p_suction, h).unwrap_or(273.15);
|
||||
let density = estimate_density(self.fluid_id.as_str(), p_suction, h)?;
|
||||
let t_k = estimate_temperature(self.fluid_id.as_str(), p_suction, h)?;
|
||||
self.mass_flow_rate(density, t_k, t_discharge_k, Some(state))
|
||||
.map(|m| m.to_kg_per_s())
|
||||
.unwrap_or(0.0)
|
||||
.map_err(|e| ComponentError::CalculationFailed(e.to_string()))
|
||||
},
|
||||
h_suction,
|
||||
1.0,
|
||||
);
|
||||
jacobian.add_entry(0, 1, dr0_dh_suction);
|
||||
)?;
|
||||
jacobian.add_entry(0, suc_h_idx, dr0_dh_suction);
|
||||
|
||||
// ∂r₀/∂h_discharge = 0 (mass flow doesn't depend on discharge enthalpy)
|
||||
jacobian.add_entry(0, 2, 0.0);
|
||||
|
||||
// ∂r₀/∂Power = 0
|
||||
jacobian.add_entry(0, 3, 0.0);
|
||||
|
||||
// Row 1: Energy residual
|
||||
// ∂r₁/∂ṁ = -(h_discharge - h_suction) / η_mech
|
||||
// Row 1: Energy residual r1 = power_calc − ṁ × Δh / η_mech
|
||||
// ∂r1/∂ṁ_suction = −(h_discharge − h_suction) / η_mech
|
||||
let dr1_dm = -(h_discharge - h_suction) / self.mechanical_efficiency;
|
||||
jacobian.add_entry(1, 0, dr1_dm);
|
||||
jacobian.add_entry(1, suc_m_idx, dr1_dm);
|
||||
|
||||
// ∂r₁/∂h_suction - includes power derivative and mass flow term
|
||||
let dr1_dh_suction = approximate_derivative(
|
||||
// ∂r1/∂h_suction — numerical
|
||||
let dr1_dh_suction = approximate_derivative_result(
|
||||
|h| {
|
||||
let t = estimate_temperature(self.fluid_id.as_str(), p_suction, h).unwrap_or(300.0);
|
||||
let t = estimate_temperature(self.fluid_id.as_str(), p_suction, h)?;
|
||||
let t_discharge =
|
||||
estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)
|
||||
.unwrap_or(350.0);
|
||||
self.power_consumption_cooling(
|
||||
estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)?;
|
||||
Ok(self.power_consumption_cooling(
|
||||
Temperature::from_kelvin(t),
|
||||
Temperature::from_kelvin(t_discharge),
|
||||
None,
|
||||
)
|
||||
))
|
||||
},
|
||||
h_suction,
|
||||
1.0,
|
||||
) + mass_flow_state / self.mechanical_efficiency;
|
||||
jacobian.add_entry(1, 1, dr1_dh_suction);
|
||||
)? + mass_flow_state / self.mechanical_efficiency;
|
||||
jacobian.add_entry(1, suc_h_idx, dr1_dh_suction);
|
||||
|
||||
// ∂r₁/∂h_discharge - includes power derivative and mass flow term
|
||||
let dr1_dh_discharge = approximate_derivative(
|
||||
// ∂r1/∂h_discharge — numerical
|
||||
let dr1_dh_discharge = approximate_derivative_result(
|
||||
|h| {
|
||||
let t_suction = estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)
|
||||
.unwrap_or(300.0);
|
||||
let t =
|
||||
estimate_temperature(self.fluid_id.as_str(), p_discharge, h).unwrap_or(350.0);
|
||||
self.power_consumption_cooling(
|
||||
let t_suction = estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)?;
|
||||
let t = estimate_temperature(self.fluid_id.as_str(), p_discharge, h)?;
|
||||
Ok(self.power_consumption_cooling(
|
||||
Temperature::from_kelvin(t_suction),
|
||||
Temperature::from_kelvin(t),
|
||||
None,
|
||||
)
|
||||
))
|
||||
},
|
||||
h_discharge,
|
||||
1.0,
|
||||
) - mass_flow_state / self.mechanical_efficiency;
|
||||
jacobian.add_entry(1, 2, dr1_dh_discharge);
|
||||
)? - mass_flow_state / self.mechanical_efficiency;
|
||||
jacobian.add_entry(1, dis_h_idx, dr1_dh_discharge);
|
||||
|
||||
// ∂r₁/∂Power = -1
|
||||
jacobian.add_entry(1, 3, -1.0);
|
||||
|
||||
// Calibration derivatives (Story 5.5)
|
||||
if let Some(f_m_idx) = self.calib_indices.f_m {
|
||||
// ∂r₀/∂f_m = ṁ_nominal
|
||||
let density_suction =
|
||||
estimate_density(self.fluid_id.as_str(), p_suction, h_suction).unwrap_or(1.0);
|
||||
let m_nominal = self
|
||||
.mass_flow_rate(density_suction, _t_suction_k, t_discharge_k, None)
|
||||
.map(|m| m.to_kg_per_s())
|
||||
.unwrap_or(0.0);
|
||||
jacobian.add_entry(0, f_m_idx, m_nominal);
|
||||
// Row 2: Mass conservation r2 = ṁ_discharge − ṁ_suction (exact, CM1.3)
|
||||
// CM1.4: omit when same_branch_m (trivial equation removed from n_equations).
|
||||
if !self.same_branch_m {
|
||||
jacobian.add_entry(2, dis_m_idx, 1.0);
|
||||
jacobian.add_entry(2, suc_m_idx, -1.0);
|
||||
}
|
||||
|
||||
if let Some(f_power_idx) = self.calib_indices.f_power {
|
||||
// ∂r₁/∂f_power = Power_nominal
|
||||
// Calibration derivatives (Story 5.5)
|
||||
if let Some(z_flow_idx) = self.calib_indices.z_flow {
|
||||
let density_suction = estimate_density(self.fluid_id.as_str(), p_suction, h_suction)?;
|
||||
let m_nominal = self
|
||||
.mass_flow_rate(density_suction, t_suction_k, t_discharge_k, None)
|
||||
.map(|m| m.to_kg_per_s())
|
||||
.map_err(|e| ComponentError::CalculationFailed(e.to_string()))?;
|
||||
jacobian.add_entry(0, z_flow_idx, m_nominal);
|
||||
}
|
||||
|
||||
if let Some(z_power_idx) = self.calib_indices.z_power {
|
||||
let p_nominal = self.power_consumption_cooling(
|
||||
Temperature::from_kelvin(_t_suction_k),
|
||||
Temperature::from_kelvin(t_suction_k),
|
||||
Temperature::from_kelvin(t_discharge_k),
|
||||
None,
|
||||
);
|
||||
jacobian.add_entry(1, f_power_idx, p_nominal);
|
||||
jacobian.add_entry(1, z_power_idx, p_nominal);
|
||||
}
|
||||
|
||||
// ∂r0/∂f_etav (AHRI 540 only): ṁ_calc = f_m · f_etav · base with
|
||||
// base = M1 · (1 − (P_suc/P_dis)^(1/M2)) · ρ_suc · V_disp · N/60,
|
||||
// so the derivative is exactly f_m · base.
|
||||
if let Some(z_etav_idx) = self.calib_indices.z_etav {
|
||||
if let CompressorModel::Ahri540(coeffs) = &self.model {
|
||||
let density_suction =
|
||||
estimate_density(self.fluid_id.as_str(), p_suction, h_suction)?;
|
||||
let inverse_pressure_ratio = p_suction / p_discharge;
|
||||
let base = coeffs.m1
|
||||
* (1.0 - inverse_pressure_ratio.powf(1.0 / coeffs.m2))
|
||||
* density_suction
|
||||
* self.displacement_m3_per_rev
|
||||
* (self.speed_rpm / 60.0);
|
||||
let f_m = self
|
||||
.calib_indices
|
||||
.z_flow
|
||||
.map(|idx| state[idx])
|
||||
.unwrap_or(self.calib.z_flow);
|
||||
jacobian.add_entry(0, z_etav_idx, f_m * base);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
2 // Mass flow residual and energy residual
|
||||
// CM1.4: when suction and discharge share the same ṁ state index (same
|
||||
// series branch), the conservation residual r[2] = ṁ_dis − ṁ_suc is
|
||||
// trivially 0 and must be dropped to keep the system square.
|
||||
if self.same_branch_m {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
|
||||
fn port_mass_flows(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
if state.len() < 4 {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 4,
|
||||
actual: state.len(),
|
||||
});
|
||||
}
|
||||
let m = entropyk_core::MassFlow::from_kg_per_s(state[0]);
|
||||
let suc_m_idx = self.suction_m_idx.ok_or_else(|| {
|
||||
ComponentError::InvalidState(
|
||||
"Compressor mass-flow reporting requires a live suction mass-flow index"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let m = entropyk_core::MassFlow::from_kg_per_s(state[suc_m_idx]);
|
||||
// Suction (inlet), Discharge (outlet), Oil (no flow modeled yet)
|
||||
Ok(vec![
|
||||
m,
|
||||
@@ -1391,7 +1475,10 @@ impl Component for Compressor<Connected> {
|
||||
.with_param("speedRpm", self.speed_rpm)
|
||||
.with_param("displacementM3PerRev", self.displacement_m3_per_rev)
|
||||
.with_param("mechanicalEfficiency", self.mechanical_efficiency)
|
||||
.with_param("calib", serde_json::to_value(&self.calib).unwrap_or(serde_json::Value::Null));
|
||||
.with_param(
|
||||
"calib",
|
||||
serde_json::to_value(&self.calib).unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
match &self.model {
|
||||
CompressorModel::Ahri540(c) => {
|
||||
params = params
|
||||
@@ -1414,7 +1501,10 @@ impl Component for Compressor<Connected> {
|
||||
"massFlowCurve",
|
||||
serde_json::to_value(&c.mass_flow_curve).unwrap_or(serde_json::Value::Null),
|
||||
)
|
||||
.with_param("powerCurve", serde_json::to_value(&c.power_curve).unwrap_or(serde_json::Value::Null));
|
||||
.with_param(
|
||||
"powerCurve",
|
||||
serde_json::to_value(&c.power_curve).unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
}
|
||||
}
|
||||
params
|
||||
@@ -1429,6 +1519,48 @@ impl Component for Compressor<Connected> {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
|
||||
// Without this override the solver's finalize() wiring was silently
|
||||
// dropped (trait default is a no-op): dynamic calibration factors
|
||||
// (f_m, f_power, f_etav) registered as bounded variables never reached
|
||||
// compute_residuals/jacobian_entries, which already read
|
||||
// `self.calib_indices` (mass_flow_rate / power_consumption_cooling).
|
||||
self.calib_indices = indices;
|
||||
}
|
||||
|
||||
fn measure_output(&self, kind: crate::MeasuredOutput, state: &StateSlice) -> Option<f64> {
|
||||
use crate::MeasuredOutput;
|
||||
match kind {
|
||||
// Discharge gas temperature (DGT) so a controls[] loop can hold a
|
||||
// maximum-DGT limit on the AHRI-540 compressor (like the
|
||||
// IsentropicCompressor's liquid-injection loop).
|
||||
MeasuredOutput::Temperature => {
|
||||
let dis_h_idx = self.discharge_h_idx?;
|
||||
if dis_h_idx >= state.len() {
|
||||
return None;
|
||||
}
|
||||
let h_discharge = state[dis_h_idx];
|
||||
let p_discharge = self.port_discharge.pressure().to_pascals();
|
||||
if !h_discharge.is_finite() || p_discharge <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)
|
||||
.ok()
|
||||
.filter(|t| t.is_finite())
|
||||
}
|
||||
// Refrigerant mass flow rate at the suction edge.
|
||||
MeasuredOutput::MassFlowRate => {
|
||||
let m_idx = self.suction_m_idx.or(self.discharge_m_idx)?;
|
||||
if m_idx >= state.len() {
|
||||
return None;
|
||||
}
|
||||
let m = state[m_idx];
|
||||
m.is_finite().then_some(m)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::state_machine::StateManageable;
|
||||
@@ -1603,6 +1735,7 @@ fn estimate_temperature(
|
||||
///
|
||||
/// Uses a central difference approximation:
|
||||
/// f'(x) ≈ (f(x + h) - f(x - h)) / (2h)
|
||||
#[cfg(test)]
|
||||
fn approximate_derivative<F>(f: F, x: f64, h: f64) -> f64
|
||||
where
|
||||
F: Fn(f64) -> f64,
|
||||
@@ -1610,6 +1743,13 @@ where
|
||||
(f(x + h) - f(x - h)) / (2.0 * h)
|
||||
}
|
||||
|
||||
fn approximate_derivative_result<F>(f: F, x: f64, h: f64) -> Result<f64, ComponentError>
|
||||
where
|
||||
F: Fn(f64) -> Result<f64, ComponentError>,
|
||||
{
|
||||
Ok((f(x + h)? - f(x - h)?) / (2.0 * h))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1664,6 +1804,11 @@ mod tests {
|
||||
fluid_id: FluidId::new("R134a"),
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
suction_m_idx: None,
|
||||
suction_h_idx: None,
|
||||
discharge_m_idx: None,
|
||||
discharge_h_idx: None,
|
||||
same_branch_m: false,
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -1847,7 +1992,7 @@ mod tests {
|
||||
.to_kg_per_s();
|
||||
|
||||
compressor.set_calib(Calib {
|
||||
f_m: 1.1,
|
||||
z_flow: 1.1,
|
||||
..Calib::default()
|
||||
});
|
||||
let m_calib = compressor
|
||||
@@ -1865,7 +2010,7 @@ mod tests {
|
||||
let p_default = compressor.power_consumption_cooling(t_suction, t_discharge, None);
|
||||
|
||||
compressor.set_calib(Calib {
|
||||
f_power: 1.1,
|
||||
z_power: 1.1,
|
||||
..Calib::default()
|
||||
});
|
||||
let p_calib = compressor.power_consumption_cooling(t_suction, t_discharge, None);
|
||||
@@ -1884,7 +2029,7 @@ mod tests {
|
||||
.to_kg_per_s();
|
||||
|
||||
compressor.set_calib(Calib {
|
||||
f_etav: 0.9,
|
||||
z_etav: 0.9,
|
||||
..Calib::default()
|
||||
});
|
||||
let m_calib = compressor
|
||||
@@ -1935,6 +2080,11 @@ mod tests {
|
||||
fluid_id: FluidId::new("R134a"),
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
suction_m_idx: None,
|
||||
suction_h_idx: None,
|
||||
discharge_m_idx: None,
|
||||
discharge_h_idx: None,
|
||||
same_branch_m: false,
|
||||
_state: PhantomData,
|
||||
};
|
||||
|
||||
@@ -2035,28 +2185,32 @@ mod tests {
|
||||
#[test]
|
||||
fn test_component_n_equations() {
|
||||
let compressor = create_test_compressor();
|
||||
assert_eq!(compressor.n_equations(), 2);
|
||||
// CM1.3: 2 thermo + 1 mass-flow conservation = 3 equations
|
||||
assert_eq!(compressor.n_equations(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_compute_residuals() {
|
||||
let compressor = create_test_compressor();
|
||||
let mut compressor = create_test_compressor();
|
||||
compressor.set_system_context(0, &[(0, 0, 1), (3, 0, 2)]);
|
||||
let state = vec![0.05, 400000.0, 450000.0, 3500.0];
|
||||
let mut residuals = vec![0.0; 2];
|
||||
let mut residuals = vec![0.0; 3];
|
||||
|
||||
let result = compressor.compute_residuals(&state, &mut residuals);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.is_ok(), "jacobian error: {:?}", result);
|
||||
|
||||
// Verify residuals are calculated (actual values depend on fluid properties)
|
||||
assert!(!residuals[0].is_nan());
|
||||
assert!(!residuals[1].is_nan());
|
||||
assert!(!residuals[2].is_nan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_compute_residuals_wrong_size() {
|
||||
let compressor = create_test_compressor();
|
||||
let mut compressor = create_test_compressor();
|
||||
compressor.set_system_context(0, &[(0, 0, 1), (3, 0, 2)]);
|
||||
let state = vec![0.05, 400000.0, 450000.0, 3500.0];
|
||||
let mut residuals = vec![0.0; 3]; // Wrong size
|
||||
let mut residuals = vec![0.0; 2]; // Wrong size (n_equations is now 3)
|
||||
|
||||
let result = compressor.compute_residuals(&state, &mut residuals);
|
||||
assert!(result.is_err());
|
||||
@@ -2064,12 +2218,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_component_jacobian_entries() {
|
||||
let compressor = create_test_compressor();
|
||||
let mut compressor = create_test_compressor();
|
||||
compressor.set_system_context(0, &[(0, 0, 1), (3, 0, 2)]);
|
||||
let state = vec![0.05, 400000.0, 450000.0, 3500.0];
|
||||
let mut jacobian = JacobianBuilder::new();
|
||||
|
||||
let result = compressor.jacobian_entries(&state, &mut jacobian);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.is_ok(), "jacobian error: {:?}", result);
|
||||
|
||||
// Should have at least some entries
|
||||
assert!(jacobian.len() > 0);
|
||||
@@ -2136,6 +2291,11 @@ mod tests {
|
||||
fluid_id: FluidId::new("R410A"),
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
suction_m_idx: None,
|
||||
suction_h_idx: None,
|
||||
discharge_m_idx: None,
|
||||
discharge_h_idx: None,
|
||||
same_branch_m: false,
|
||||
_state: PhantomData,
|
||||
};
|
||||
|
||||
@@ -2185,6 +2345,11 @@ mod tests {
|
||||
fluid_id: FluidId::new("R454B"),
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
suction_m_idx: None,
|
||||
suction_h_idx: None,
|
||||
discharge_m_idx: None,
|
||||
discharge_h_idx: None,
|
||||
same_branch_m: false,
|
||||
_state: PhantomData,
|
||||
};
|
||||
|
||||
@@ -2238,6 +2403,11 @@ mod tests {
|
||||
fluid_id: FluidId::new("R134a"),
|
||||
circuit_id: CircuitId::default(),
|
||||
operational_state: OperationalState::default(),
|
||||
suction_m_idx: None,
|
||||
suction_h_idx: None,
|
||||
discharge_m_idx: None,
|
||||
discharge_h_idx: None,
|
||||
same_branch_m: false,
|
||||
_state: PhantomData,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user