Source code for models.bess
"""BESS (Battery Energy Storage System) model."""
from dataclasses import dataclass
from enum import Enum
[docs]
class BESSControlStrategy(Enum):
"""BESS control strategy; DEFAULT = charge from PV surplus, discharge when needed."""
DEFAULT = "default"
[docs]
@dataclass
class BESS:
"""
Battery energy storage system with SOC limits and round-trip efficiency.
Attributes:
name: BESS identifier.
capacity: Total energy capacity (kWh).
max_charge_power: Maximum charge power (kW).
max_discharge_power: Maximum discharge power (kW).
efficiency: Round-trip efficiency in (0, 1]; default 0.90.
soc_min: Minimum SOC in [0, 1]; default 0.10.
soc_max: Maximum SOC in [0, 1]; default 0.90.
initial_soc: Initial SOC; default 0.50.
control_strategy: Control strategy enum.
current_soc: Current SOC (updated by charge/discharge).
current_power: Current power (kW); positive = charging, negative = discharging.
"""
name: str
capacity: float
max_charge_power: float
max_discharge_power: float
efficiency: float = 0.90
soc_min: float = 0.10
soc_max: float = 0.90
initial_soc: float = 0.50
control_strategy: BESSControlStrategy = BESSControlStrategy.DEFAULT
current_soc: float = 0.50
current_power: float = 0.0
def __post_init__(self):
"""Validate attributes and set current_soc from initial_soc."""
if self.capacity <= 0:
raise ValueError("BESS capacity must be positive")
if self.max_charge_power <= 0:
raise ValueError("Max charge power must be positive")
if self.max_discharge_power <= 0:
raise ValueError("Max discharge power must be positive")
if not 0 < self.efficiency <= 1:
raise ValueError("Efficiency must be between 0 and 1")
if not 0 <= self.soc_min < self.soc_max <= 1:
raise ValueError("SOC limits must satisfy: 0 ≤ soc_min < soc_max ≤ 1")
if not self.soc_min <= self.initial_soc <= self.soc_max:
raise ValueError("Initial SOC must be between soc_min and soc_max")
self.current_soc = self.initial_soc
[docs]
def charge(self, power: float, timestep_seconds: float) -> float:
"""
Charge for the given power and timestep; SOC is capped at soc_max.
Args:
power: Requested charging power (kW), must be positive.
timestep_seconds: Timestep duration (s).
Returns:
Actual power charged (kW); may be less than requested if SOC limit reached.
"""
if power < 0:
raise ValueError("Charging power must be positive")
actual_power = min(power, self.max_charge_power)
energy_added = (actual_power * timestep_seconds / 3600.0) * self.efficiency
new_soc = self.current_soc + (energy_added / self.capacity)
if new_soc > self.soc_max:
allowed_energy = (self.soc_max - self.current_soc) * self.capacity
actual_power = (allowed_energy / self.efficiency) / (
timestep_seconds / 3600.0
)
self.current_soc = self.soc_max
else:
self.current_soc = new_soc
self.current_power = actual_power
return actual_power
[docs]
def discharge(self, power: float, timestep_seconds: float) -> float:
"""
Discharge for the given power and timestep; SOC is floored at soc_min.
Args:
power: Requested discharge power (kW), must be positive.
timestep_seconds: Timestep duration (s).
Returns:
Actual power discharged (kW); may be less than requested if SOC limit reached.
"""
if power < 0:
raise ValueError("Discharging power must be positive")
actual_power = min(power, self.max_discharge_power)
energy_removed = actual_power * timestep_seconds / 3600.0
energy_from_battery = energy_removed / self.efficiency
new_soc = self.current_soc - (energy_from_battery / self.capacity)
if new_soc < self.soc_min:
allowed_energy = (self.current_soc - self.soc_min) * self.capacity
actual_power = (allowed_energy * self.efficiency) / (
timestep_seconds / 3600.0
)
self.current_soc = self.soc_min
else:
self.current_soc = new_soc
self.current_power = -actual_power
return actual_power
[docs]
def get_available_energy(self) -> float:
"""Energy (kWh) that can still be discharged (from current_soc down to soc_min, after efficiency)."""
return (self.current_soc - self.soc_min) * self.capacity * self.efficiency
[docs]
def get_available_charge_capacity(self) -> float:
"""Capacity (kWh) available for charging (from current_soc to soc_max, accounting for efficiency)."""
return (self.soc_max - self.current_soc) * self.capacity / self.efficiency
[docs]
def get_max_discharge_power_available(self, timestep_seconds: float) -> float:
"""Maximum discharge power (kW) in this timestep (min of max_discharge_power and energy-limited power)."""
available_energy = self.get_available_energy()
energy_limited_power = available_energy / (timestep_seconds / 3600.0)
return min(self.max_discharge_power, energy_limited_power)
[docs]
def get_max_charge_power_available(self, timestep_seconds: float) -> float:
"""Maximum charge power (kW) in this timestep (min of max_charge_power and capacity-limited power)."""
available_capacity = self.get_available_charge_capacity()
capacity_limited_power = available_capacity / (timestep_seconds / 3600.0)
return min(self.max_charge_power, capacity_limited_power)
[docs]
def idle(self) -> None:
"""Set current_power to 0 (no charge or discharge)."""
self.current_power = 0.0
[docs]
def get_energy_stored(self) -> float:
"""Current energy stored in the battery (kWh)."""
return self.current_soc * self.capacity
def __repr__(self) -> str:
return (
f"BESS(name='{self.name}', capacity={self.capacity}kWh, "
f"SOC={self.current_soc*100:.1f}%, "
f"power={self.current_power:+.2f}kW, "
f"strategy={self.control_strategy.value})"
)