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>
717 lines
22 KiB
Rust
717 lines
22 KiB
Rust
//! 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);
|
|
}
|
|
}
|