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:
716
crates/components/src/pump_controller.rs
Normal file
716
crates/components/src/pump_controller.rs
Normal file
@@ -0,0 +1,716 @@
|
||||
//! PumpController component for intelligent pump sequencing and VFD optimization
|
||||
//!
|
||||
//! This component manages multiple pumps with optimal sequencing, runtime-based rotation,
|
||||
//! and energy-efficient VFD control.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::OperationalState;
|
||||
|
||||
/// Sequencing strategy for pump selection
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SequencingStrategy {
|
||||
/// Fixed rotation (pump 1, 2, 3, 1, 2, 3...)
|
||||
FixedRotation,
|
||||
/// Based on operating hours
|
||||
RuntimeBased,
|
||||
/// Based on efficiency (energy optimization)
|
||||
EfficiencyBased,
|
||||
/// Alternation based on start count
|
||||
StartCountBased,
|
||||
}
|
||||
|
||||
/// Configuration for an individual pump
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PumpConfig {
|
||||
/// Pump identifier
|
||||
pub id: String,
|
||||
/// Nominal power (W)
|
||||
pub nominal_power_w: f64,
|
||||
/// Nominal flow rate (m³/s)
|
||||
pub nominal_flow_m3s: f64,
|
||||
/// Nominal head (m)
|
||||
pub nominal_head_m: f64,
|
||||
/// Nominal speed (RPM)
|
||||
pub nominal_rpm: f64,
|
||||
/// Supports VFD
|
||||
pub supports_vfd: bool,
|
||||
/// VFD speed range (min, max) as fraction of nominal
|
||||
pub vfd_range: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
/// State of an individual pump
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PumpState {
|
||||
/// Identifier
|
||||
pub id: String,
|
||||
/// Current operational state
|
||||
pub operational_state: OperationalState,
|
||||
/// Cumulative operating hours
|
||||
pub runtime_hours: f64,
|
||||
/// Cumulative start count
|
||||
pub start_count: u64,
|
||||
/// Current speed (fraction of nominal, 0.0-1.0)
|
||||
pub speed_fraction: f64,
|
||||
/// Current power consumption (W)
|
||||
pub current_power_w: f64,
|
||||
/// Current flow rate (m³/s)
|
||||
pub current_flow_m3s: f64,
|
||||
/// Last start time
|
||||
pub last_start: Option<Instant>,
|
||||
/// Last stop time
|
||||
pub last_stop: Option<Instant>,
|
||||
/// Is in fault state
|
||||
pub is_faulted: bool,
|
||||
}
|
||||
|
||||
/// Configuration for the PumpController
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PumpControllerConfig {
|
||||
/// Configured pumps
|
||||
pub pumps: Vec<PumpConfig>,
|
||||
/// Minimum number of active pumps
|
||||
pub min_active_pumps: usize,
|
||||
/// Maximum number of active pumps
|
||||
pub max_active_pumps: usize,
|
||||
/// Sequencing strategy
|
||||
pub sequencing_strategy: SequencingStrategy,
|
||||
/// Rotation interval (hours)
|
||||
pub rotation_interval_hours: f64,
|
||||
/// Energy optimization enabled
|
||||
pub energy_optimization: bool,
|
||||
/// Minimum time between changes (seconds)
|
||||
pub min_switch_interval_secs: u64,
|
||||
/// Anti-short-cycle time (seconds)
|
||||
pub anti_short_cycle_time_secs: u64,
|
||||
}
|
||||
|
||||
/// Pump controller for intelligent pump management
|
||||
#[derive(Debug)]
|
||||
pub struct PumpController {
|
||||
/// Configuration
|
||||
config: PumpControllerConfig,
|
||||
/// Pump states
|
||||
pump_states: Vec<PumpState>,
|
||||
/// Rotation queue
|
||||
rotation_queue: VecDeque<String>,
|
||||
/// Last rotation time
|
||||
last_rotation: Option<Instant>,
|
||||
/// Last pump count change
|
||||
last_pump_count_change: Option<Instant>,
|
||||
/// Flow setpoint (m³/s)
|
||||
flow_setpoint_m3s: f64,
|
||||
/// Current total flow
|
||||
current_total_flow_m3s: f64,
|
||||
/// Load demand (0.0-1.0)
|
||||
load_demand: f64,
|
||||
}
|
||||
|
||||
impl PumpController {
|
||||
/// Creates a new pump controller
|
||||
pub fn new(config: PumpControllerConfig) -> Result<Self, PumpControllerError> {
|
||||
// Validation
|
||||
if config.min_active_pumps > config.max_active_pumps {
|
||||
return Err(PumpControllerError::InvalidConfiguration(
|
||||
"min_active_pumps cannot be greater than max_active_pumps".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if config.pumps.len() < config.max_active_pumps {
|
||||
return Err(PumpControllerError::InvalidConfiguration(
|
||||
"Number of configured pumps must be >= max_active_pumps".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Initialize pump states
|
||||
let mut pump_states = Vec::new();
|
||||
let mut rotation_queue = VecDeque::new();
|
||||
|
||||
for pump_config in &config.pumps {
|
||||
pump_states.push(PumpState {
|
||||
id: pump_config.id.clone(),
|
||||
operational_state: OperationalState::Off,
|
||||
runtime_hours: 0.0,
|
||||
start_count: 0,
|
||||
speed_fraction: 0.0,
|
||||
current_power_w: 0.0,
|
||||
current_flow_m3s: 0.0,
|
||||
last_start: None,
|
||||
last_stop: None,
|
||||
is_faulted: false,
|
||||
});
|
||||
rotation_queue.push_back(pump_config.id.clone());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
pump_states,
|
||||
rotation_queue,
|
||||
last_rotation: None,
|
||||
last_pump_count_change: None,
|
||||
flow_setpoint_m3s: 0.0,
|
||||
current_total_flow_m3s: 0.0,
|
||||
load_demand: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates load demand and calculates required pumps
|
||||
pub fn update_demand(
|
||||
&mut self,
|
||||
load_demand: f64,
|
||||
flow_setpoint_m3s: f64,
|
||||
) -> Result<(), PumpControllerError> {
|
||||
self.load_demand = load_demand.clamp(0.0, 1.0);
|
||||
self.flow_setpoint_m3s = flow_setpoint_m3s.max(0.0);
|
||||
|
||||
// Calculate required number of pumps
|
||||
let required_pumps = self.calculate_required_pumps()?;
|
||||
|
||||
// Check if change is needed
|
||||
let current_active = self.count_active_pumps();
|
||||
|
||||
if required_pumps != current_active {
|
||||
// Check minimum interval
|
||||
if let Some(last_change) = self.last_pump_count_change {
|
||||
let elapsed = last_change.elapsed().as_secs();
|
||||
if elapsed < self.config.min_switch_interval_secs {
|
||||
return Ok(()); // Wait more
|
||||
}
|
||||
}
|
||||
|
||||
// Apply change
|
||||
if required_pumps > current_active {
|
||||
self.start_pumps(required_pumps - current_active)?;
|
||||
} else {
|
||||
self.stop_pumps(current_active - required_pumps)?;
|
||||
}
|
||||
|
||||
self.last_pump_count_change = Some(Instant::now());
|
||||
}
|
||||
|
||||
// Optimize VFD speeds if enabled
|
||||
if self.config.energy_optimization {
|
||||
self.optimize_vfd_speeds()?;
|
||||
}
|
||||
|
||||
// Update flows
|
||||
self.update_flows()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculates required pumps based on demand
|
||||
fn calculate_required_pumps(&self) -> Result<usize, PumpControllerError> {
|
||||
// Simple calculation based on demand
|
||||
let flow_per_pump = self.calculate_nominal_flow_per_pump();
|
||||
let required = (self.flow_setpoint_m3s / flow_per_pump).ceil() as usize;
|
||||
|
||||
// Apply limits
|
||||
Ok(required.clamp(self.config.min_active_pumps, self.config.max_active_pumps))
|
||||
}
|
||||
|
||||
/// Starts N pumps
|
||||
fn start_pumps(&mut self, count: usize) -> Result<(), PumpControllerError> {
|
||||
let mut started = 0;
|
||||
|
||||
for _ in 0..count {
|
||||
if let Some(pump_id) = self.get_next_pump_to_start()? {
|
||||
self.start_pump(&pump_id)?;
|
||||
started += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if started < count {
|
||||
return Err(PumpControllerError::InsufficientPumps(format!(
|
||||
"Could only start {} of {} requested pumps",
|
||||
started, count
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops N pumps
|
||||
fn stop_pumps(&mut self, count: usize) -> Result<(), PumpControllerError> {
|
||||
let mut _stopped = 0;
|
||||
|
||||
for _ in 0..count {
|
||||
if let Some(pump_id) = self.get_next_pump_to_stop()? {
|
||||
self.stop_pump(&pump_id)?;
|
||||
_stopped += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finds the next pump to start (based on strategy)
|
||||
fn get_next_pump_to_start(&mut self) -> Result<Option<String>, PumpControllerError> {
|
||||
match self.config.sequencing_strategy {
|
||||
SequencingStrategy::FixedRotation => {
|
||||
// Take next in queue
|
||||
Ok(self.rotation_queue.pop_front())
|
||||
}
|
||||
SequencingStrategy::RuntimeBased => {
|
||||
// Find pump with least operating hours
|
||||
let mut candidates: Vec<_> = self
|
||||
.pump_states
|
||||
.iter()
|
||||
.filter(|p| p.operational_state == OperationalState::Off && !p.is_faulted)
|
||||
.collect();
|
||||
|
||||
candidates.sort_by(|a, b| a.runtime_hours.partial_cmp(&b.runtime_hours).unwrap());
|
||||
|
||||
Ok(candidates.first().map(|p| p.id.clone()))
|
||||
}
|
||||
SequencingStrategy::StartCountBased => {
|
||||
// Find pump with least starts
|
||||
let mut candidates: Vec<_> = self
|
||||
.pump_states
|
||||
.iter()
|
||||
.filter(|p| p.operational_state == OperationalState::Off && !p.is_faulted)
|
||||
.collect();
|
||||
|
||||
candidates.sort_by(|a, b| a.start_count.cmp(&b.start_count));
|
||||
|
||||
Ok(candidates.first().map(|p| p.id.clone()))
|
||||
}
|
||||
SequencingStrategy::EfficiencyBased => {
|
||||
// TODO: Implement based on performance curves
|
||||
Ok(self.rotation_queue.pop_front())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the next pump to stop
|
||||
fn get_next_pump_to_stop(&self) -> Result<Option<String>, PumpControllerError> {
|
||||
// Simple logic: stop the most recently started
|
||||
let active_pumps: Vec<_> = self
|
||||
.pump_states
|
||||
.iter()
|
||||
.filter(|p| p.operational_state == OperationalState::On)
|
||||
.collect();
|
||||
|
||||
if active_pumps.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Return the most recent (based on last_start)
|
||||
let mut sorted = active_pumps;
|
||||
sorted.sort_by(|a, b| {
|
||||
let a_time = a.last_start.unwrap_or(Instant::now());
|
||||
let b_time = b.last_start.unwrap_or(Instant::now());
|
||||
b_time.cmp(&a_time) // Most recent first
|
||||
});
|
||||
|
||||
Ok(sorted.first().map(|p| p.id.clone()))
|
||||
}
|
||||
|
||||
/// Starts a specific pump
|
||||
fn start_pump(&mut self, pump_id: &str) -> Result<(), PumpControllerError> {
|
||||
let pump = self
|
||||
.pump_states
|
||||
.iter_mut()
|
||||
.find(|p| p.id == pump_id)
|
||||
.ok_or_else(|| PumpControllerError::PumpNotFound(pump_id.to_string()))?;
|
||||
|
||||
if pump.is_faulted {
|
||||
return Err(PumpControllerError::PumpFaulted(pump_id.to_string()));
|
||||
}
|
||||
|
||||
pump.operational_state = OperationalState::On;
|
||||
pump.speed_fraction = 1.0; // Full speed by default
|
||||
pump.last_start = Some(Instant::now());
|
||||
pump.start_count += 1;
|
||||
|
||||
// Update rotation queue
|
||||
self.rotation_queue.push_back(pump_id.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops a specific pump
|
||||
fn stop_pump(&mut self, pump_id: &str) -> Result<(), PumpControllerError> {
|
||||
let pump = self
|
||||
.pump_states
|
||||
.iter_mut()
|
||||
.find(|p| p.id == pump_id)
|
||||
.ok_or_else(|| PumpControllerError::PumpNotFound(pump_id.to_string()))?;
|
||||
|
||||
pump.operational_state = OperationalState::Off;
|
||||
pump.speed_fraction = 0.0;
|
||||
pump.current_power_w = 0.0;
|
||||
pump.current_flow_m3s = 0.0;
|
||||
pump.last_stop = Some(Instant::now());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Optimizes VFD speeds to minimize power consumption
|
||||
fn optimize_vfd_speeds(&mut self) -> Result<(), PumpControllerError> {
|
||||
let active_pumps = self.count_active_pumps();
|
||||
|
||||
if active_pumps == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Calculate optimal speed for each pump
|
||||
let optimal_speed = self.calculate_optimal_speed()?;
|
||||
|
||||
for pump in &mut self.pump_states {
|
||||
if pump.operational_state == OperationalState::On {
|
||||
let pump_config = self.config.pumps.iter().find(|c| c.id == pump.id).unwrap();
|
||||
|
||||
if pump_config.supports_vfd {
|
||||
// Apply optimal speed with VFD limits
|
||||
if let Some((min_speed, max_speed)) = pump_config.vfd_range {
|
||||
pump.speed_fraction = optimal_speed.clamp(min_speed, max_speed);
|
||||
} else {
|
||||
pump.speed_fraction = optimal_speed;
|
||||
}
|
||||
|
||||
// Calculate new power (affinity laws)
|
||||
pump.current_power_w =
|
||||
pump_config.nominal_power_w * pump.speed_fraction.powi(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculates optimal VFD speed
|
||||
fn calculate_optimal_speed(&self) -> Result<f64, PumpControllerError> {
|
||||
// Affinity laws: Q ∝ N, P ∝ N³
|
||||
// To minimize energy, we want the lowest speed that satisfies flow requirement
|
||||
|
||||
let active_pumps = self.count_active_pumps() as f64;
|
||||
let flow_per_pump = self.flow_setpoint_m3s / active_pumps;
|
||||
|
||||
// Required speed (as fraction of nominal)
|
||||
let required_speed = flow_per_pump / self.calculate_nominal_flow_per_pump();
|
||||
|
||||
// Apply safety margin (5%)
|
||||
let safety_margin = 1.05;
|
||||
|
||||
Ok((required_speed * safety_margin).clamp(0.3, 1.0)) // Min 30%, max 100%
|
||||
}
|
||||
|
||||
/// Updates current flow rates
|
||||
fn update_flows(&mut self) -> Result<(), PumpControllerError> {
|
||||
let mut total_flow = 0.0;
|
||||
|
||||
for pump in &mut self.pump_states {
|
||||
if pump.operational_state == OperationalState::On {
|
||||
let pump_config = self.config.pumps.iter().find(|c| c.id == pump.id).unwrap();
|
||||
|
||||
// Affinity laws: Q ∝ N
|
||||
pump.current_flow_m3s = pump_config.nominal_flow_m3s * pump.speed_fraction;
|
||||
total_flow += pump.current_flow_m3s;
|
||||
} else {
|
||||
pump.current_flow_m3s = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_total_flow_m3s = total_flow;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculates nominal flow per pump (average)
|
||||
fn calculate_nominal_flow_per_pump(&self) -> f64 {
|
||||
let total_nominal_flow: f64 = self.config.pumps.iter().map(|p| p.nominal_flow_m3s).sum();
|
||||
|
||||
total_nominal_flow / self.config.pumps.len() as f64
|
||||
}
|
||||
|
||||
/// Counts active pumps
|
||||
pub fn count_active_pumps(&self) -> usize {
|
||||
self.pump_states
|
||||
.iter()
|
||||
.filter(|p| p.operational_state == OperationalState::On)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Returns pump states
|
||||
pub fn pump_states(&self) -> &[PumpState] {
|
||||
&self.pump_states
|
||||
}
|
||||
|
||||
/// Returns total current power consumption
|
||||
pub fn total_power_consumption(&self) -> f64 {
|
||||
self.pump_states.iter().map(|p| p.current_power_w).sum()
|
||||
}
|
||||
|
||||
/// Returns total current flow
|
||||
pub fn total_flow(&self) -> f64 {
|
||||
self.current_total_flow_m3s
|
||||
}
|
||||
|
||||
/// Checks if a pump is faulted
|
||||
pub fn is_pump_faulted(&self, pump_id: &str) -> bool {
|
||||
self.pump_states
|
||||
.iter()
|
||||
.find(|p| p.id == pump_id)
|
||||
.map(|p| p.is_faulted)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Sets a pump fault state
|
||||
pub fn set_pump_fault(
|
||||
&mut self,
|
||||
pump_id: &str,
|
||||
faulted: bool,
|
||||
) -> Result<(), PumpControllerError> {
|
||||
let pump = self
|
||||
.pump_states
|
||||
.iter_mut()
|
||||
.find(|p| p.id == pump_id)
|
||||
.ok_or_else(|| PumpControllerError::PumpNotFound(pump_id.to_string()))?;
|
||||
|
||||
pump.is_faulted = faulted;
|
||||
|
||||
if faulted && pump.operational_state == OperationalState::On {
|
||||
// Stop pump if running
|
||||
self.stop_pump(pump_id)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs scheduled pump rotation
|
||||
pub fn rotate_pumps(&mut self) -> Result<(), PumpControllerError> {
|
||||
// Check rotation interval
|
||||
if let Some(last_rotation) = self.last_rotation {
|
||||
let elapsed_hours = last_rotation.elapsed().as_secs() as f64 / 3600.0;
|
||||
if elapsed_hours < self.config.rotation_interval_hours {
|
||||
return Ok(()); // Not time to rotate yet
|
||||
}
|
||||
}
|
||||
|
||||
// Rotation: take first active pump and put it at end of queue
|
||||
if let Some(first_active) = self
|
||||
.pump_states
|
||||
.iter()
|
||||
.find(|p| p.operational_state == OperationalState::On)
|
||||
.map(|p| p.id.clone())
|
||||
{
|
||||
// Remove from queue and add to end
|
||||
if let Some(pos) = self
|
||||
.rotation_queue
|
||||
.iter()
|
||||
.position(|id| *id == first_active)
|
||||
{
|
||||
self.rotation_queue.remove(pos);
|
||||
self.rotation_queue.push_back(first_active);
|
||||
}
|
||||
}
|
||||
|
||||
self.last_rotation = Some(Instant::now());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// PumpController errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PumpControllerError {
|
||||
#[error("Invalid configuration: {0}")]
|
||||
InvalidConfiguration(String),
|
||||
|
||||
#[error("Pump not found: {0}")]
|
||||
PumpNotFound(String),
|
||||
|
||||
#[error("Pump faulted: {0}")]
|
||||
PumpFaulted(String),
|
||||
|
||||
#[error("Insufficient pumps: {0}")]
|
||||
InsufficientPumps(String),
|
||||
|
||||
#[error("Calculation error")]
|
||||
CalculationError,
|
||||
}
|
||||
|
||||
impl Default for PumpControllerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pumps: Vec::new(),
|
||||
min_active_pumps: 1,
|
||||
max_active_pumps: 3,
|
||||
sequencing_strategy: SequencingStrategy::RuntimeBased,
|
||||
rotation_interval_hours: 168.0, // 1 week
|
||||
energy_optimization: true,
|
||||
min_switch_interval_secs: 300, // 5 minutes
|
||||
anti_short_cycle_time_secs: 300,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pump_controller_creation() {
|
||||
let config = PumpControllerConfig {
|
||||
pumps: vec![
|
||||
PumpConfig {
|
||||
id: "pump1".to_string(),
|
||||
nominal_power_w: 1000.0,
|
||||
nominal_flow_m3s: 0.01,
|
||||
nominal_head_m: 20.0,
|
||||
nominal_rpm: 2900.0,
|
||||
supports_vfd: true,
|
||||
vfd_range: Some((0.3, 1.0)),
|
||||
},
|
||||
PumpConfig {
|
||||
id: "pump2".to_string(),
|
||||
nominal_power_w: 1000.0,
|
||||
nominal_flow_m3s: 0.01,
|
||||
nominal_head_m: 20.0,
|
||||
nominal_rpm: 2900.0,
|
||||
supports_vfd: true,
|
||||
vfd_range: Some((0.3, 1.0)),
|
||||
},
|
||||
],
|
||||
min_active_pumps: 1,
|
||||
max_active_pumps: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let controller = PumpController::new(config);
|
||||
assert!(controller.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_configuration() {
|
||||
let config = PumpControllerConfig {
|
||||
pumps: vec![],
|
||||
min_active_pumps: 2,
|
||||
max_active_pumps: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let controller = PumpController::new(config);
|
||||
assert!(controller.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pump_sequencing() {
|
||||
let config = PumpControllerConfig {
|
||||
pumps: vec![
|
||||
PumpConfig {
|
||||
id: "pump1".to_string(),
|
||||
nominal_power_w: 1000.0,
|
||||
nominal_flow_m3s: 0.01,
|
||||
nominal_head_m: 20.0,
|
||||
nominal_rpm: 2900.0,
|
||||
supports_vfd: false,
|
||||
vfd_range: None,
|
||||
},
|
||||
PumpConfig {
|
||||
id: "pump2".to_string(),
|
||||
nominal_power_w: 1000.0,
|
||||
nominal_flow_m3s: 0.01,
|
||||
nominal_head_m: 20.0,
|
||||
nominal_rpm: 2900.0,
|
||||
supports_vfd: false,
|
||||
vfd_range: None,
|
||||
},
|
||||
PumpConfig {
|
||||
id: "pump3".to_string(),
|
||||
nominal_power_w: 1000.0,
|
||||
nominal_flow_m3s: 0.01,
|
||||
nominal_head_m: 20.0,
|
||||
nominal_rpm: 2900.0,
|
||||
supports_vfd: false,
|
||||
vfd_range: None,
|
||||
},
|
||||
],
|
||||
min_active_pumps: 1,
|
||||
max_active_pumps: 3,
|
||||
sequencing_strategy: SequencingStrategy::FixedRotation,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut controller = PumpController::new(config).unwrap();
|
||||
|
||||
// Low demand: 1 pump
|
||||
controller.update_demand(0.3, 0.005).unwrap();
|
||||
assert_eq!(controller.count_active_pumps(), 1);
|
||||
assert_eq!(controller.pump_states()[0].id, "pump1");
|
||||
|
||||
// Medium demand: 2 pumps
|
||||
controller.update_demand(0.6, 0.015).unwrap();
|
||||
assert_eq!(controller.count_active_pumps(), 2);
|
||||
|
||||
// High demand: 3 pumps
|
||||
controller.update_demand(0.9, 0.025).unwrap();
|
||||
assert_eq!(controller.count_active_pumps(), 3);
|
||||
|
||||
// Back to low demand
|
||||
controller.update_demand(0.2, 0.005).unwrap();
|
||||
assert_eq!(controller.count_active_pumps(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pump_fault_handling() {
|
||||
let config = PumpControllerConfig {
|
||||
pumps: vec![PumpConfig {
|
||||
id: "pump1".to_string(),
|
||||
nominal_power_w: 1000.0,
|
||||
nominal_flow_m3s: 0.01,
|
||||
nominal_head_m: 20.0,
|
||||
nominal_rpm: 2900.0,
|
||||
supports_vfd: false,
|
||||
vfd_range: None,
|
||||
}],
|
||||
min_active_pumps: 1,
|
||||
max_active_pumps: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut controller = PumpController::new(config).unwrap();
|
||||
|
||||
// Start pump
|
||||
controller.update_demand(1.0, 0.01).unwrap();
|
||||
assert_eq!(controller.count_active_pumps(), 1);
|
||||
|
||||
// Set fault
|
||||
controller.set_pump_fault("pump1", true).unwrap();
|
||||
assert!(controller.is_pump_faulted("pump1"));
|
||||
assert_eq!(controller.count_active_pumps(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vfd_optimization() {
|
||||
let config = PumpControllerConfig {
|
||||
pumps: vec![PumpConfig {
|
||||
id: "pump1".to_string(),
|
||||
nominal_power_w: 1000.0,
|
||||
nominal_flow_m3s: 0.01,
|
||||
nominal_head_m: 20.0,
|
||||
nominal_rpm: 2900.0,
|
||||
supports_vfd: true,
|
||||
vfd_range: Some((0.3, 1.0)),
|
||||
}],
|
||||
min_active_pumps: 1,
|
||||
max_active_pumps: 1,
|
||||
energy_optimization: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut controller = PumpController::new(config).unwrap();
|
||||
|
||||
// 50% demand
|
||||
controller.update_demand(0.5, 0.005).unwrap();
|
||||
|
||||
let pump_state = &controller.pump_states()[0];
|
||||
assert!(pump_state.speed_fraction < 1.0);
|
||||
assert!(pump_state.current_power_w < 1000.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user