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:
@@ -92,9 +92,9 @@ pub struct FreeCoolingExchanger {
|
||||
/// Fluid backend for property calculations
|
||||
fluid_backend: Option<Arc<dyn FluidBackend>>,
|
||||
/// Calibration factor for UA scaling (default 1.0)
|
||||
f_ua: f64,
|
||||
z_ua: f64,
|
||||
/// Calibration factor for pressure drop scaling (default 1.0)
|
||||
f_dp: f64,
|
||||
z_dp: f64,
|
||||
/// Calibration indices for inverse calibration
|
||||
calib_indices: CalibIndices,
|
||||
}
|
||||
@@ -109,8 +109,8 @@ impl std::fmt::Debug for FreeCoolingExchanger {
|
||||
.field("outdoor_temp", &self.outdoor_temp)
|
||||
.field("heat_transfer_rate", &self.heat_transfer_rate)
|
||||
.field("current_effectiveness", &self.current_effectiveness)
|
||||
.field("f_ua", &self.f_ua)
|
||||
.field("f_dp", &self.f_dp)
|
||||
.field("f_ua", &self.z_ua)
|
||||
.field("f_dp", &self.z_dp)
|
||||
.field("fluid_backend", &"<FluidBackend>")
|
||||
.finish()
|
||||
}
|
||||
@@ -150,13 +150,18 @@ impl FreeCoolingExchanger {
|
||||
circuit_id,
|
||||
config,
|
||||
mode: FreeCoolingMode::Bypass,
|
||||
ports: [port_cold_inlet, port_cold_outlet, port_hot_inlet, port_hot_outlet],
|
||||
ports: [
|
||||
port_cold_inlet,
|
||||
port_cold_outlet,
|
||||
port_hot_inlet,
|
||||
port_hot_outlet,
|
||||
],
|
||||
outdoor_temp: None,
|
||||
heat_transfer_rate: None,
|
||||
current_effectiveness,
|
||||
fluid_backend: None,
|
||||
f_ua: 1.0,
|
||||
f_dp: 1.0,
|
||||
z_ua: 1.0,
|
||||
z_dp: 1.0,
|
||||
calib_indices: CalibIndices::default(),
|
||||
})
|
||||
}
|
||||
@@ -229,7 +234,8 @@ impl FreeCoolingExchanger {
|
||||
}
|
||||
_ => {
|
||||
if t_outdoor.0
|
||||
> (t_cold_in - self.config.min_outdoor_temp + self.config.hysteresis)
|
||||
> (t_cold_in - self.config.min_outdoor_temp
|
||||
+ self.config.hysteresis)
|
||||
{
|
||||
self.mode = FreeCoolingMode::Bypass;
|
||||
}
|
||||
@@ -245,24 +251,24 @@ impl FreeCoolingExchanger {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the f_ua calibration factor
|
||||
pub fn set_f_ua(&mut self, f_ua: f64) {
|
||||
self.f_ua = f_ua;
|
||||
/// Sets the z_ua calibration factor (BOLT `Z_UA`).
|
||||
pub fn set_z_ua(&mut self, z_ua: f64) {
|
||||
self.z_ua = z_ua;
|
||||
}
|
||||
|
||||
/// Returns the f_ua calibration factor
|
||||
pub fn f_ua(&self) -> f64 {
|
||||
self.f_ua
|
||||
/// Returns the z_ua calibration factor.
|
||||
pub fn z_ua(&self) -> f64 {
|
||||
self.z_ua
|
||||
}
|
||||
|
||||
/// Sets the f_dp calibration factor
|
||||
pub fn set_f_dp(&mut self, f_dp: f64) {
|
||||
self.f_dp = f_dp;
|
||||
/// Sets the z_dp calibration factor (BOLT `Z_dpc`).
|
||||
pub fn set_z_dp(&mut self, z_dp: f64) {
|
||||
self.z_dp = z_dp;
|
||||
}
|
||||
|
||||
/// Returns the f_dp calibration factor
|
||||
pub fn f_dp(&self) -> f64 {
|
||||
self.f_dp
|
||||
/// Returns the z_dp calibration factor.
|
||||
pub fn z_dp(&self) -> f64 {
|
||||
self.z_dp
|
||||
}
|
||||
|
||||
/// Returns the current operational state
|
||||
@@ -414,7 +420,7 @@ impl Component for FreeCoolingExchanger {
|
||||
let c_min = c_cold.min(c_hot);
|
||||
|
||||
// UA with calibration scaling
|
||||
let ua_eff = self.f_ua * self.config.ua;
|
||||
let ua_eff = self.z_ua * self.config.ua;
|
||||
|
||||
// ε-NTU effectiveness
|
||||
let eps = self.compute_effectiveness(ua_eff, c_cold, c_hot);
|
||||
@@ -442,8 +448,7 @@ impl Component for FreeCoolingExchanger {
|
||||
residuals[0] = m_cold * dh_cold - q;
|
||||
residuals[1] = m_hot * dh_hot + q;
|
||||
residuals[2] = m_cold * dh_cold + m_hot * dh_hot;
|
||||
residuals[3] =
|
||||
(p_cold_in - p_cold_out) - self.f_dp * self.config.cold_dp_nominal;
|
||||
residuals[3] = (p_cold_in - p_cold_out) - self.z_dp * self.config.cold_dp_nominal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,8 +461,8 @@ impl Component for FreeCoolingExchanger {
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// Jacobian entries for calibration variable sensitivities
|
||||
if let Some(f_ua_idx) = self.calib_indices.f_ua {
|
||||
// ∂r[0]/∂f_ua: cold-side energy balance sensitivity
|
||||
if let Some(z_ua_idx) = self.calib_indices.z_ua {
|
||||
// ∂r[0]/∂z_ua: cold-side energy balance sensitivity
|
||||
// r[0] = m_cold * (h_cold_out - h_cold_in) - Q(f_ua)
|
||||
// ∂r[0]/∂f_ua = -∂Q/∂f_ua = -ε × C_min × (T_hot_in - T_cold_in) × UA_nominal
|
||||
let m_cold = self.config.cold_mass_flow;
|
||||
@@ -468,28 +473,26 @@ impl Component for FreeCoolingExchanger {
|
||||
|
||||
let h_cold_in = self.port_enthalpy_raw(COLD_INLET);
|
||||
let h_hot_in = self.port_enthalpy_raw(HOT_INLET);
|
||||
let t_cold_in =
|
||||
self.temperature_from_enthalpy(h_cold_in, self.config.cold_cp);
|
||||
let t_hot_in =
|
||||
self.temperature_from_enthalpy(h_hot_in, self.config.hot_cp);
|
||||
let t_cold_in = self.temperature_from_enthalpy(h_cold_in, self.config.cold_cp);
|
||||
let t_hot_in = self.temperature_from_enthalpy(h_hot_in, self.config.hot_cp);
|
||||
|
||||
let dt = t_hot_in - t_cold_in;
|
||||
// Approximate ∂Q/∂f_ua ≈ C_min × dt × (∂ε/∂f_ua) × UA_nominal
|
||||
// For small variations: ∂Q/∂f_ua ≈ Q / f_ua when linearized
|
||||
let ua_eff = self.f_ua * self.config.ua;
|
||||
let ua_eff = self.z_ua * self.config.ua;
|
||||
let eps = self.compute_effectiveness(ua_eff, c_cold, c_hot);
|
||||
let q_per_f_ua = eps * c_min * dt; // Q / f_ua at current operating point
|
||||
|
||||
jacobian.add_entry(0, f_ua_idx, -q_per_f_ua);
|
||||
jacobian.add_entry(1, f_ua_idx, q_per_f_ua);
|
||||
jacobian.add_entry(0, z_ua_idx, -q_per_f_ua);
|
||||
jacobian.add_entry(1, z_ua_idx, q_per_f_ua);
|
||||
// r[2] = r[0] + r[1], so ∂r[2]/∂f_ua = ∂r[0]/∂f_ua + ∂r[1]/∂f_ua = 0
|
||||
jacobian.add_entry(2, f_ua_idx, 0.0);
|
||||
jacobian.add_entry(2, z_ua_idx, 0.0);
|
||||
}
|
||||
|
||||
if let Some(f_dp_idx) = self.calib_indices.f_dp {
|
||||
if let Some(z_dp_idx) = self.calib_indices.z_dp {
|
||||
// r[3] = (P_cold_in - P_cold_out) - f_dp × ΔP_nominal
|
||||
// ∂r[3]/∂f_dp = -ΔP_nominal
|
||||
jacobian.add_entry(3, f_dp_idx, -self.config.cold_dp_nominal);
|
||||
jacobian.add_entry(3, z_dp_idx, -self.config.cold_dp_nominal);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -499,10 +502,7 @@ impl Component for FreeCoolingExchanger {
|
||||
&self.ports
|
||||
}
|
||||
|
||||
fn set_fluid_backend_from_builder(
|
||||
&mut self,
|
||||
backend: Arc<dyn FluidBackend>,
|
||||
) {
|
||||
fn set_fluid_backend_from_builder(&mut self, backend: Arc<dyn FluidBackend>) {
|
||||
if self.fluid_backend.is_none() {
|
||||
self.fluid_backend = Some(backend);
|
||||
}
|
||||
@@ -517,10 +517,7 @@ impl Component for FreeCoolingExchanger {
|
||||
Some((Power::from_watts(0.0), Power::from_watts(0.0)))
|
||||
}
|
||||
|
||||
fn port_enthalpies(
|
||||
&self,
|
||||
_state: &[f64],
|
||||
) -> Result<Vec<Enthalpy>, ComponentError> {
|
||||
fn port_enthalpies(&self, _state: &[f64]) -> Result<Vec<Enthalpy>, ComponentError> {
|
||||
Ok(vec![
|
||||
Enthalpy::from_joules_per_kg(self.port_enthalpy_raw(COLD_INLET)),
|
||||
Enthalpy::from_joules_per_kg(self.port_enthalpy_raw(COLD_OUTLET)),
|
||||
@@ -532,7 +529,7 @@ impl Component for FreeCoolingExchanger {
|
||||
fn signature(&self) -> String {
|
||||
format!(
|
||||
"FreeCoolingExchanger(id={},eff={},ua={},mode={:?},f_ua={},f_dp={})",
|
||||
self.id, self.config.effectiveness, self.config.ua, self.mode, self.f_ua, self.f_dp
|
||||
self.id, self.config.effectiveness, self.config.ua, self.mode, self.z_ua, self.z_dp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -547,19 +544,19 @@ impl Component for FreeCoolingExchanger {
|
||||
.with_param("coldCp", self.config.cold_cp)
|
||||
.with_param("hotCp", self.config.hot_cp)
|
||||
.with_param("bypassFraction", self.config.bypass_fraction)
|
||||
.with_param("f_ua", self.f_ua)
|
||||
.with_param("f_dp", self.f_dp)
|
||||
.with_param("f_ua", self.z_ua)
|
||||
.with_param("f_dp", self.z_dp)
|
||||
.with_param("mode", format!("{:?}", self.mode))
|
||||
}
|
||||
|
||||
fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
|
||||
match factor {
|
||||
"f_ua" => {
|
||||
self.f_ua = value;
|
||||
self.z_ua = value;
|
||||
true
|
||||
}
|
||||
"f_dp" => {
|
||||
self.f_dp = value;
|
||||
self.z_dp = value;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -575,7 +572,7 @@ impl Default for FreeCoolingConfig {
|
||||
min_outdoor_temp: 285.15,
|
||||
hysteresis: 2.0,
|
||||
control_mode: FreeCoolingControlMode::AutoTemperature,
|
||||
ua: 10_000.0, // 10 kW/K typical for plate HX
|
||||
ua: 10_000.0, // 10 kW/K typical for plate HX
|
||||
cold_mass_flow: 0.5,
|
||||
hot_mass_flow: 0.5,
|
||||
cold_cp: CP_WATER,
|
||||
@@ -712,9 +709,7 @@ mod tests {
|
||||
bypass_fraction: 0.3,
|
||||
};
|
||||
let expected = 85.0 * 0.3;
|
||||
assert!(
|
||||
(exchanger.energy_savings_percent() - expected).abs() < 1e-10
|
||||
);
|
||||
assert!((exchanger.energy_savings_percent() - expected).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -754,11 +749,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_residuals_bypass_mode() {
|
||||
let (ci, co, hi, ho) = make_connected_ports_with(
|
||||
Pressure::from_pascals(3e5),
|
||||
50_000.0,
|
||||
105_000.0,
|
||||
);
|
||||
let (ci, co, hi, ho) =
|
||||
make_connected_ports_with(Pressure::from_pascals(3e5), 50_000.0, 105_000.0);
|
||||
let fc = FreeCoolingExchanger::new(
|
||||
"fc_test",
|
||||
CircuitId(0),
|
||||
@@ -794,7 +786,7 @@ mod tests {
|
||||
|
||||
// With f_ua calib index
|
||||
let mut fc = fc;
|
||||
fc.calib_indices.f_ua = Some(100);
|
||||
fc.calib_indices.z_ua = Some(100);
|
||||
let mut jb = JacobianBuilder::new();
|
||||
fc.jacobian_entries(&[], &mut jb).unwrap();
|
||||
assert!(!jb.entries().is_empty(), "Should have f_ua entries");
|
||||
@@ -811,7 +803,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_jacobian_with_f_dp() {
|
||||
let mut fc = make_exchanger_active();
|
||||
fc.calib_indices.f_dp = Some(200);
|
||||
fc.calib_indices.z_dp = Some(200);
|
||||
fc.config.cold_dp_nominal = 5000.0;
|
||||
|
||||
let mut jb = JacobianBuilder::new();
|
||||
@@ -843,7 +835,7 @@ mod tests {
|
||||
fn test_calibration_scaling() {
|
||||
let fc1 = make_exchanger_active();
|
||||
let mut fc2 = make_exchanger_active();
|
||||
fc2.f_ua = 1.5; // 50% higher UA
|
||||
fc2.z_ua = 1.5; // 50% higher UA
|
||||
|
||||
let mut r1 = vec![0.0; N_EQUATIONS];
|
||||
let mut r2 = vec![0.0; N_EQUATIONS];
|
||||
@@ -875,13 +867,13 @@ mod tests {
|
||||
fn test_set_calib_indices() {
|
||||
let mut fc = make_exchanger_active();
|
||||
let indices = CalibIndices {
|
||||
f_ua: Some(10),
|
||||
f_dp: Some(20),
|
||||
z_ua: Some(10),
|
||||
z_dp: Some(20),
|
||||
..Default::default()
|
||||
};
|
||||
fc.set_calib_indices(indices);
|
||||
assert_eq!(fc.calib_indices.f_ua, Some(10));
|
||||
assert_eq!(fc.calib_indices.f_dp, Some(20));
|
||||
assert_eq!(fc.calib_indices.z_ua, Some(10));
|
||||
assert_eq!(fc.calib_indices.z_dp, Some(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user