chore: remove BMAD framework files and IDE configuration artifacts
Clean up unused BMAD workflow, agent, and command files across all IDE configurations (.agent, .clinerules, .cursor, .gemini, .github, .kilocode, .opencode) and internal module files (_bmad/bmb, _bmad/bmm). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
475
crates/components/src/free_cooling_exchanger.rs
Normal file
475
crates/components/src/free_cooling_exchanger.rs
Normal file
@@ -0,0 +1,475 @@
|
||||
//! FreeCoolingExchanger component for water-side economizer simulation
|
||||
//!
|
||||
//! This component models a water-to-water heat exchanger used for free cooling,
|
||||
//! allowing the use of outdoor air as a cooling source without operating the compressor.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use entropyk_core::{Power, Temperature};
|
||||
use entropyk_fluids::FluidBackend;
|
||||
|
||||
use crate::{
|
||||
CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
|
||||
ResidualVector, SystemState,
|
||||
};
|
||||
|
||||
/// Operating mode of the FreeCoolingExchanger
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum FreeCoolingMode {
|
||||
/// Free cooling active (direct heat exchange)
|
||||
Active,
|
||||
/// Full bypass (no heat exchange)
|
||||
Bypass,
|
||||
/// Mixed mode (partial bypass)
|
||||
Mixed { bypass_fraction: f64 },
|
||||
}
|
||||
|
||||
/// Configuration for the free cooling heat exchanger
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FreeCoolingConfig {
|
||||
/// Effectiveness of the heat exchanger (0.0 - 1.0)
|
||||
pub effectiveness: f64,
|
||||
/// Bypass fraction (0.0 - 1.0)
|
||||
pub bypass_fraction: f64,
|
||||
/// Minimum outdoor temperature for free cooling (K)
|
||||
pub min_outdoor_temp: f64,
|
||||
/// Hysteresis to prevent rapid cycling (K)
|
||||
pub hysteresis: f64,
|
||||
/// Control mode
|
||||
pub control_mode: FreeCoolingControlMode,
|
||||
}
|
||||
|
||||
/// Control mode for free cooling
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum FreeCoolingControlMode {
|
||||
/// Manual control (fixed mode)
|
||||
Manual,
|
||||
/// Automatic control based on outdoor temperature
|
||||
AutoTemperature,
|
||||
/// Optimized control (minimizes energy consumption)
|
||||
Optimized,
|
||||
}
|
||||
|
||||
/// FreeCoolingExchanger component
|
||||
pub struct FreeCoolingExchanger {
|
||||
/// Unique identifier
|
||||
id: String,
|
||||
/// Circuit ID
|
||||
circuit_id: CircuitId,
|
||||
/// Configuration
|
||||
config: FreeCoolingConfig,
|
||||
/// Current mode
|
||||
mode: FreeCoolingMode,
|
||||
/// Ports (4 ports: cold water in/out, hot water in/out)
|
||||
port_cold_inlet: ConnectedPort,
|
||||
port_cold_outlet: ConnectedPort,
|
||||
port_hot_inlet: ConnectedPort,
|
||||
port_hot_outlet: ConnectedPort,
|
||||
/// Outdoor temperature (for auto mode)
|
||||
outdoor_temp: Option<Temperature>,
|
||||
/// Calculated after convergence
|
||||
heat_transfer_rate: Option<Power>,
|
||||
/// Current effectiveness (can vary with flow rates)
|
||||
current_effectiveness: f64,
|
||||
/// Fluid backend for property calculations
|
||||
fluid_backend: Option<Arc<dyn FluidBackend>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FreeCoolingExchanger {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FreeCoolingExchanger")
|
||||
.field("id", &self.id)
|
||||
.field("circuit_id", &self.circuit_id)
|
||||
.field("config", &self.config)
|
||||
.field("mode", &self.mode)
|
||||
.field("outdoor_temp", &self.outdoor_temp)
|
||||
.field("heat_transfer_rate", &self.heat_transfer_rate)
|
||||
.field("current_effectiveness", &self.current_effectiveness)
|
||||
.field("fluid_backend", &"<FluidBackend>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FreeCoolingExchanger {
|
||||
/// Creates a new free cooling heat exchanger
|
||||
pub fn new(
|
||||
id: &str,
|
||||
circuit_id: CircuitId,
|
||||
config: FreeCoolingConfig,
|
||||
port_cold_inlet: ConnectedPort,
|
||||
port_cold_outlet: ConnectedPort,
|
||||
port_hot_inlet: ConnectedPort,
|
||||
port_hot_outlet: ConnectedPort,
|
||||
) -> Result<Self, ComponentError> {
|
||||
// Validate parameters
|
||||
if config.effectiveness < 0.0 || config.effectiveness > 1.0 {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Effectiveness must be between 0.0 and 1.0".to_string(),
|
||||
));
|
||||
}
|
||||
if config.bypass_fraction < 0.0 || config.bypass_fraction > 1.0 {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Bypass fraction must be between 0.0 and 1.0".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let current_effectiveness = config.effectiveness;
|
||||
Ok(Self {
|
||||
id: id.to_string(),
|
||||
circuit_id,
|
||||
config,
|
||||
mode: FreeCoolingMode::Bypass, // Starts in bypass
|
||||
port_cold_inlet,
|
||||
port_cold_outlet,
|
||||
port_hot_inlet,
|
||||
port_hot_outlet,
|
||||
outdoor_temp: None,
|
||||
heat_transfer_rate: None,
|
||||
current_effectiveness,
|
||||
fluid_backend: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the fluid backend for property calculations
|
||||
pub fn set_fluid_backend(&mut self, backend: Arc<dyn FluidBackend>) {
|
||||
self.fluid_backend = Some(backend);
|
||||
}
|
||||
|
||||
/// Calculates maximum possible heat transfer
|
||||
fn calculate_max_heat_transfer(&self, state: &SystemState) -> Result<Power, ComponentError> {
|
||||
// Get inlet temperatures
|
||||
let t_cold_in = self.get_cold_inlet_temp(state)?;
|
||||
let t_hot_in = self.get_hot_inlet_temp(state)?;
|
||||
|
||||
// Heat capacity rates
|
||||
let c_cold = self.get_cold_capacity_rate(state)?;
|
||||
let c_hot = self.get_hot_capacity_rate(state)?;
|
||||
let c_min = c_cold.min(c_hot);
|
||||
|
||||
// Maximum heat transfer
|
||||
let q_max = c_min * (t_hot_in - t_cold_in);
|
||||
|
||||
Ok(Power::from_watts(q_max.max(0.0)))
|
||||
}
|
||||
|
||||
/// Updates the mode based on conditions
|
||||
pub fn update_mode(&mut self, outdoor_temp: Option<Temperature>) -> Result<(), ComponentError> {
|
||||
if let Some(t_outdoor) = outdoor_temp {
|
||||
self.outdoor_temp = Some(t_outdoor);
|
||||
|
||||
match self.config.control_mode {
|
||||
FreeCoolingControlMode::AutoTemperature => {
|
||||
let t_cold_in = self.get_current_cold_inlet_temp()?;
|
||||
|
||||
// Switching logic with hysteresis
|
||||
if self.mode == FreeCoolingMode::Bypass {
|
||||
// Check if we can switch to free cooling
|
||||
if t_outdoor.0 < (t_cold_in - self.config.min_outdoor_temp) {
|
||||
self.mode = FreeCoolingMode::Active;
|
||||
}
|
||||
} else {
|
||||
// Check if we should go back to bypass
|
||||
if t_outdoor.0
|
||||
> (t_cold_in - self.config.min_outdoor_temp + self.config.hysteresis)
|
||||
{
|
||||
self.mode = FreeCoolingMode::Bypass;
|
||||
}
|
||||
}
|
||||
}
|
||||
FreeCoolingControlMode::Optimized => {
|
||||
// TODO: Implement energy optimization
|
||||
self.mode = FreeCoolingMode::Active;
|
||||
}
|
||||
FreeCoolingControlMode::Manual => {
|
||||
// Do nothing, fixed mode
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper methods for temperature and flow calculations
|
||||
fn get_cold_inlet_temp(&self, _state: &SystemState) -> Result<f64, ComponentError> {
|
||||
// Placeholder - would extract from state vector
|
||||
Ok(285.15) // 12°C
|
||||
}
|
||||
|
||||
fn get_hot_inlet_temp(&self, _state: &SystemState) -> Result<f64, ComponentError> {
|
||||
// Placeholder - would extract from state vector
|
||||
Ok(298.15) // 25°C
|
||||
}
|
||||
|
||||
fn get_cold_capacity_rate(&self, _state: &SystemState) -> Result<f64, ComponentError> {
|
||||
// Placeholder - would calculate from mass flow and specific heat
|
||||
Ok(4186.0 * 0.1) // Water at 0.1 kg/s
|
||||
}
|
||||
|
||||
fn get_hot_capacity_rate(&self, _state: &SystemState) -> Result<f64, ComponentError> {
|
||||
// Placeholder - would calculate from mass flow and specific heat
|
||||
Ok(4186.0 * 0.1) // Water at 0.1 kg/s
|
||||
}
|
||||
|
||||
fn get_current_cold_inlet_temp(&self) -> Result<f64, ComponentError> {
|
||||
Ok(285.15) // Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FreeCoolingExchanger {
|
||||
fn n_equations(&self) -> usize {
|
||||
// 4 equations for energy balances at each port
|
||||
// + 1 equation for heat transfer
|
||||
// + 1 equation for flow continuity
|
||||
6
|
||||
}
|
||||
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &[f64],
|
||||
_residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
// TODO: Implement actual residual calculations
|
||||
// For now, return zero residuals
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &[f64],
|
||||
_jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// TODO: Implement partial derivatives
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
// Return the 4 ports
|
||||
&[] // Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
/// Specific methods for FreeCoolingExchanger
|
||||
impl FreeCoolingExchanger {
|
||||
/// Returns the current operational state
|
||||
pub fn operational_state(&self) -> OperationalState {
|
||||
match self.mode {
|
||||
FreeCoolingMode::Bypass => OperationalState::Bypass,
|
||||
_ => OperationalState::On,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the operational state
|
||||
pub fn set_operational_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
|
||||
match state {
|
||||
OperationalState::On => self.mode = FreeCoolingMode::Active,
|
||||
OperationalState::Off | OperationalState::Bypass => {
|
||||
self.mode = FreeCoolingMode::Bypass;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the current heat transfer rate
|
||||
pub fn heat_transfer_rate(&self) -> Option<Power> {
|
||||
self.heat_transfer_rate
|
||||
}
|
||||
|
||||
/// Returns the current mode
|
||||
pub fn current_mode(&self) -> FreeCoolingMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
/// Returns estimated energy savings (in %)
|
||||
pub fn energy_savings_percent(&self) -> f64 {
|
||||
match self.mode {
|
||||
FreeCoolingMode::Active => {
|
||||
// Estimation based on effectiveness
|
||||
self.current_effectiveness * 100.0
|
||||
}
|
||||
FreeCoolingMode::Bypass => 0.0,
|
||||
FreeCoolingMode::Mixed { bypass_fraction } => {
|
||||
self.current_effectiveness * bypass_fraction * 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns outdoor temperature
|
||||
pub fn outdoor_temperature(&self) -> Option<Temperature> {
|
||||
self.outdoor_temp
|
||||
}
|
||||
|
||||
/// Updates configuration
|
||||
pub fn update_config(&mut self, config: FreeCoolingConfig) -> Result<(), ComponentError> {
|
||||
// Validation
|
||||
if config.effectiveness < 0.0 || config.effectiveness > 1.0 {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Effectiveness must be between 0.0 and 1.0".to_string(),
|
||||
));
|
||||
}
|
||||
self.config = config;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if free cooling is active
|
||||
pub fn is_free_cooling_active(&self) -> bool {
|
||||
self.mode != FreeCoolingMode::Bypass
|
||||
}
|
||||
|
||||
/// Calculates effective COP (very high in free cooling)
|
||||
pub fn effective_cop(&self) -> f64 {
|
||||
match self.mode {
|
||||
FreeCoolingMode::Active => {
|
||||
// Typical COP > 20 for free cooling (only pumps)
|
||||
20.0 + self.current_effectiveness * 10.0
|
||||
}
|
||||
FreeCoolingMode::Bypass => 1.0, // No gain
|
||||
FreeCoolingMode::Mixed { bypass_fraction } => {
|
||||
// Weighted COP
|
||||
let cop_fc = 20.0 + self.current_effectiveness * 10.0;
|
||||
bypass_fraction * cop_fc + (1.0 - bypass_fraction) * 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unique identifier
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Returns the circuit ID
|
||||
pub fn circuit_id(&self) -> CircuitId {
|
||||
self.circuit_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FreeCoolingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
effectiveness: 0.85,
|
||||
bypass_fraction: 0.2,
|
||||
min_outdoor_temp: 285.15, // 12°C
|
||||
hysteresis: 2.0,
|
||||
control_mode: FreeCoolingControlMode::AutoTemperature,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::port::{FluidId, Port};
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
|
||||
/// Creates a pair of connected ports for tests (same fluid, P, h).
|
||||
fn make_connected_ports() -> (ConnectedPort, ConnectedPort) {
|
||||
let fluid = FluidId::new("Water");
|
||||
let p = Pressure::from_pascals(3e5);
|
||||
let h = Enthalpy::from_joules_per_kg(63_000.0);
|
||||
let a = Port::new(fluid, p, h);
|
||||
let b = Port::new(FluidId::new("Water"), p, h);
|
||||
a.connect(b).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_free_cooling_exchanger_creation() {
|
||||
let config = FreeCoolingConfig::default();
|
||||
let (cold_in, cold_out) = make_connected_ports();
|
||||
let (hot_in, hot_out) = make_connected_ports();
|
||||
|
||||
let exchanger = FreeCoolingExchanger::new(
|
||||
"fc_1",
|
||||
CircuitId(0),
|
||||
config,
|
||||
cold_in,
|
||||
cold_out,
|
||||
hot_in,
|
||||
hot_out,
|
||||
);
|
||||
|
||||
assert!(exchanger.is_ok());
|
||||
let exchanger = exchanger.unwrap();
|
||||
assert_eq!(exchanger.current_mode(), FreeCoolingMode::Bypass);
|
||||
assert!(!exchanger.is_free_cooling_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_effectiveness() {
|
||||
let config = FreeCoolingConfig {
|
||||
effectiveness: 1.5,
|
||||
..Default::default()
|
||||
};
|
||||
let (cold_in, cold_out) = make_connected_ports();
|
||||
let (hot_in, hot_out) = make_connected_ports();
|
||||
|
||||
let exchanger = FreeCoolingExchanger::new(
|
||||
"fc_1",
|
||||
CircuitId(0),
|
||||
config,
|
||||
cold_in,
|
||||
cold_out,
|
||||
hot_in,
|
||||
hot_out,
|
||||
);
|
||||
|
||||
assert!(exchanger.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_energy_savings_calculation() {
|
||||
let config = FreeCoolingConfig {
|
||||
effectiveness: 0.85,
|
||||
..Default::default()
|
||||
};
|
||||
let (cold_in, cold_out) = make_connected_ports();
|
||||
let (hot_in, hot_out) = make_connected_ports();
|
||||
|
||||
let mut exchanger = FreeCoolingExchanger::new(
|
||||
"fc_1",
|
||||
CircuitId(0),
|
||||
config,
|
||||
cold_in,
|
||||
cold_out,
|
||||
hot_in,
|
||||
hot_out,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Bypass mode -> 0% savings
|
||||
assert_eq!(exchanger.energy_savings_percent(), 0.0);
|
||||
|
||||
// Active mode -> effectiveness * 100%
|
||||
exchanger.mode = FreeCoolingMode::Active;
|
||||
assert_eq!(exchanger.energy_savings_percent(), 85.0);
|
||||
|
||||
// Mixed mode
|
||||
exchanger.mode = FreeCoolingMode::Mixed {
|
||||
bypass_fraction: 0.3,
|
||||
};
|
||||
assert_eq!(exchanger.energy_savings_percent(), 85.0 * 0.3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_effective_cop() {
|
||||
let (cold_in, cold_out) = make_connected_ports();
|
||||
let (hot_in, hot_out) = make_connected_ports();
|
||||
|
||||
let mut exchanger = FreeCoolingExchanger::new(
|
||||
"fc_1",
|
||||
CircuitId(0),
|
||||
FreeCoolingConfig::default(),
|
||||
cold_in,
|
||||
cold_out,
|
||||
hot_in,
|
||||
hot_out,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// COP in free cooling
|
||||
exchanger.mode = FreeCoolingMode::Active;
|
||||
assert!(exchanger.effective_cop() > 20.0);
|
||||
|
||||
// COP in bypass
|
||||
exchanger.mode = FreeCoolingMode::Bypass;
|
||||
assert_eq!(exchanger.effective_cop(), 1.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user