translation of battery.py v3 (#262)

This commit is contained in:
Normann
2024-12-19 14:50:19 +01:00
committed by GitHub
parent 0e122a9a49
commit 5f898e8aab
18 changed files with 684 additions and 648 deletions

View File

@@ -10,345 +10,279 @@ from akkudoktoreos.utils.utils import NumpyEncoder
logger = get_logger(__name__)
def max_ladeleistung_w_field(default: Optional[float] = None) -> Optional[float]:
def max_charging_power_field(description: Optional[str] = None) -> float:
if description is None:
description = "Maximum charging power in watts."
return Field(
default=default,
default=5000,
gt=0,
description="An integer representing the charging power of the battery in watts.",
description=description,
)
def start_soc_prozent_field(description: str) -> int:
def initial_soc_percentage_field(description: str) -> int:
return Field(default=0, ge=0, le=100, description=description)
class BaseAkkuParameters(BaseModel):
kapazitaet_wh: int = Field(
class BaseBatteryParameters(BaseModel):
"""Base class for battery parameters with fields for capacity, efficiency, and state of charge."""
capacity_wh: int = Field(
gt=0, description="An integer representing the capacity of the battery in watt-hours."
)
lade_effizienz: float = Field(
charging_efficiency: float = Field(
default=0.88,
gt=0,
le=1,
description="A float representing the charging efficiency of the battery.",
)
entlade_effizienz: float = Field(default=0.88, gt=0, le=1)
max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field()
start_soc_prozent: int = start_soc_prozent_field(
discharging_efficiency: float = Field(
default=0.88,
gt=0,
le=1,
description="A float representing the discharge efficiency of the battery.",
)
max_charge_power_w: Optional[float] = max_charging_power_field()
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)."
)
min_soc_prozent: int = Field(
min_soc_percentage: int = Field(
default=0,
ge=0,
le=100,
description="An integer representing the minimum state of charge (SOC) of the battery in percentage.",
)
max_soc_prozent: int = Field(default=100, ge=0, le=100)
max_soc_percentage: int = Field(
default=100,
ge=0,
le=100,
description="An integer representing the maximum state of charge (SOC) of the battery in percentage.",
)
class PVAkkuParameters(BaseAkkuParameters):
max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field(5000)
class SolarPanelBatteryParameters(BaseBatteryParameters):
max_charge_power_w: Optional[float] = max_charging_power_field()
class EAutoParameters(BaseAkkuParameters):
entlade_effizienz: float = 1.0
start_soc_prozent: int = start_soc_prozent_field(
class ElectricVehicleParameters(BaseBatteryParameters):
"""Parameters specific to an electric vehicle (EV)."""
discharging_efficiency: float = 1.0
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the current state of charge (SOC) of the battery in percentage."
)
class EAutoResult(BaseModel):
"""This object contains information related to the electric vehicle and its charging and discharging behavior."""
class ElectricVehicleResult(BaseModel):
"""Result class containing information related to the electric vehicle's charging and discharging behavior."""
charge_array: list[float] = Field(
description="Indicates for each hour whether the EV is charging (`0` for no charging, `1` for charging)."
description="Hourly charging status (0 for no charging, 1 for charging)."
)
discharge_array: list[int] = Field(
description="Indicates for each hour whether the EV is discharging (`0` for no discharging, `1` for discharging)."
description="Hourly discharging status (0 for no discharging, 1 for discharging)."
)
entlade_effizienz: float = Field(description="The discharge efficiency as a float.")
hours: int = Field(description="Amount of hours the simulation is done for.")
kapazitaet_wh: int = Field(description="The capacity of the EVs battery in watt-hours.")
lade_effizienz: float = Field(description="The charging efficiency as a float.")
max_ladeleistung_w: int = Field(description="The maximum charging power of the EV in watts.")
discharging_efficiency: float = Field(description="The discharge efficiency as a float..")
hours: int = Field(description="Number of hours in the simulation.")
capacity_wh: int = Field(description="Capacity of the EVs battery in watt-hours.")
charging_efficiency: float = Field(description="Charging efficiency as a float..")
max_charge_power_w: int = Field(description="Maximum charging power in watts.")
soc_wh: float = Field(
description="The state of charge of the battery in watt-hours at the start of the simulation."
description="State of charge of the battery in watt-hours at the start of the simulation."
)
start_soc_prozent: int = Field(
description="The state of charge of the battery in percentage at the start of the simulation."
initial_soc_percentage: int = Field(
description="State of charge at the start of the simulation in percentage."
)
@field_validator(
"discharge_array",
"charge_array",
mode="before",
)
@field_validator("discharge_array", "charge_array", mode="before")
def convert_numpy(cls, field: Any) -> Any:
return NumpyEncoder.convert_numpy(field)[0]
class PVAkku(DeviceBase):
class Battery(DeviceBase):
"""Represents a battery device with methods to simulate energy charging and discharging."""
def __init__(
self,
parameters: Optional[BaseAkkuParameters] = None,
parameters: Optional[BaseBatteryParameters] = None,
hours: Optional[int] = 24,
provider_id: Optional[str] = None,
):
# Configuration initialisation
# Initialize configuration and parameters
self.provider_id = provider_id
self.prefix = "<invalid>"
if self.provider_id == "GenericBattery":
self.prefix = "battery"
elif self.provider_id == "GenericBEV":
self.prefix = "bev"
# Parameter initialisiation
self.parameters = parameters
if hours is None:
self.hours = self.total_hours
self.hours = self.total_hours # TODO where does that come from?
else:
self.hours = hours
self.initialised = False
# Run setup if parameters are given, otherwise setup() has to be called later when the config is initialised.
if self.parameters is not None:
self.setup()
def setup(self) -> None:
"""Sets up the battery parameters based on configuration or provided parameters."""
if self.initialised:
return
if self.provider_id is not None:
# Setup by configuration
# Battery capacity in Wh
self.kapazitaet_wh = getattr(self.config, f"{self.prefix}_capacity")
# Initial state of charge in Wh
self.start_soc_prozent = getattr(self.config, f"{self.prefix}_soc_start")
self.hours = self.total_hours
# Charge and discharge efficiency
self.lade_effizienz = getattr(self.config, f"{self.prefix}_charge_efficiency")
self.entlade_effizienz = getattr(self.config, f"{self.prefix}_discharge_efficiency")
self.max_ladeleistung_w = getattr(self.config, f"{self.prefix}_charge_power_max")
# Only assign for storage battery
if self.provider_id:
# Setup from configuration
self.capacity_wh = getattr(self.config, f"{self.prefix}_capacity")
self.initial_soc_percentage = getattr(self.config, f"{self.prefix}_initial_soc")
self.hours = self.total_hours # TODO where does that come from?
self.charging_efficiency = getattr(self.config, f"{self.prefix}_charging_efficiency")
self.discharging_efficiency = getattr(
self.config, f"{self.prefix}_discharging_efficiency"
)
self.max_charge_power_w = getattr(self.config, f"{self.prefix}_max_charging_power")
if self.provider_id == "GenericBattery":
self.min_soc_prozent = getattr(self.config, f"{self.prefix}_soc_mint")
self.min_soc_percentage = getattr(
self.config,
f"{self.prefix}_soc_min",
)
else:
self.min_soc_prozent = 0
self.max_soc_prozent = getattr(self.config, f"{self.prefix}_soc_mint")
elif self.parameters is not None:
# Setup by parameters
# Battery capacity in Wh
self.kapazitaet_wh = self.parameters.kapazitaet_wh
# Initial state of charge in Wh
self.start_soc_prozent = self.parameters.start_soc_prozent
# Charge and discharge efficiency
self.lade_effizienz = self.parameters.lade_effizienz
self.entlade_effizienz = self.parameters.entlade_effizienz
self.max_ladeleistung_w = self.parameters.max_ladeleistung_w
self.min_soc_percentage = 0
self.max_soc_percentage = getattr(
self.config,
f"{self.prefix}_soc_max",
)
elif self.parameters:
# Setup from parameters
self.capacity_wh = self.parameters.capacity_wh
self.initial_soc_percentage = self.parameters.initial_soc_percentage
self.charging_efficiency = self.parameters.charging_efficiency
self.discharging_efficiency = self.parameters.discharging_efficiency
self.max_charge_power_w = self.parameters.max_charge_power_w
# Only assign for storage battery
self.min_soc_prozent = (
self.parameters.min_soc_prozent
if isinstance(self.parameters, PVAkkuParameters)
self.min_soc_percentage = (
self.parameters.min_soc_percentage
if isinstance(self.parameters, SolarPanelBatteryParameters)
else 0
)
self.max_soc_prozent = self.parameters.max_soc_prozent
self.max_soc_percentage = self.parameters.max_soc_percentage
else:
error_msg = "Parameters and provider ID missing. Can't instantiate."
error_msg = "Parameters and provider ID are missing. Cannot instantiate."
logger.error(error_msg)
raise ValueError(error_msg)
# init
if self.max_ladeleistung_w is None:
self.max_ladeleistung_w = self.kapazitaet_wh
# Initialize state of charge
if self.max_charge_power_w is None:
self.max_charge_power_w = self.capacity_wh # TODO this should not be equal capacity_wh
self.discharge_array = np.full(self.hours, 1)
self.charge_array = np.full(self.hours, 1)
# Calculate start, min and max SoC in Wh
self.soc_wh = (self.start_soc_prozent / 100) * self.kapazitaet_wh
self.min_soc_wh = (self.min_soc_prozent / 100) * self.kapazitaet_wh
self.max_soc_wh = (self.max_soc_prozent / 100) * self.kapazitaet_wh
self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh
self.min_soc_wh = (self.min_soc_percentage / 100) * self.capacity_wh
self.max_soc_wh = (self.max_soc_percentage / 100) * self.capacity_wh
self.initialised = True
def to_dict(self) -> dict[str, Any]:
"""Converts the object to a dictionary representation."""
return {
"kapazitaet_wh": self.kapazitaet_wh,
"start_soc_prozent": self.start_soc_prozent,
"capacity_wh": self.capacity_wh,
"initial_soc_percentage": self.initial_soc_percentage,
"soc_wh": self.soc_wh,
"hours": self.hours,
"discharge_array": self.discharge_array,
"charge_array": self.charge_array,
"lade_effizienz": self.lade_effizienz,
"entlade_effizienz": self.entlade_effizienz,
"max_ladeleistung_w": self.max_ladeleistung_w,
"charging_efficiency": self.charging_efficiency,
"discharging_efficiency": self.discharging_efficiency,
"max_charge_power_w": self.max_charge_power_w,
}
def reset(self) -> None:
self.soc_wh = (self.start_soc_prozent / 100) * self.kapazitaet_wh
# Ensure soc_wh is within min and max limits
"""Resets the battery state to its initial values."""
self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh
self.soc_wh = min(max(self.soc_wh, self.min_soc_wh), self.max_soc_wh)
self.discharge_array = np.full(self.hours, 1)
self.charge_array = np.full(self.hours, 1)
def set_discharge_per_hour(self, discharge_array: np.ndarray) -> None:
assert len(discharge_array) == self.hours
"""Sets the discharge values for each hour."""
if len(discharge_array) != self.hours:
raise ValueError(f"Discharge array must have exactly {self.hours} elements.")
self.discharge_array = np.array(discharge_array)
def set_charge_per_hour(self, charge_array: np.ndarray) -> None:
assert len(charge_array) == self.hours
"""Sets the charge values for each hour."""
if len(charge_array) != self.hours:
raise ValueError(f"Charge array must have exactly {self.hours} elements.")
self.charge_array = np.array(charge_array)
def set_charge_allowed_for_hour(self, charge: float, hour: int) -> None:
assert hour < self.hours
"""Sets the charge for a specific hour."""
if hour >= self.hours:
raise ValueError(f"Hour {hour} is out of range. Must be less than {self.hours}.")
self.charge_array[hour] = charge
def ladezustand_in_prozent(self) -> float:
return (self.soc_wh / self.kapazitaet_wh) * 100
def current_soc_percentage(self) -> float:
"""Calculates the current state of charge in percentage."""
return (self.soc_wh / self.capacity_wh) * 100
def energie_abgeben(self, wh: float, hour: int) -> tuple[float, float]:
def discharge_energy(self, wh: float, hour: int) -> tuple[float, float]:
"""Discharges energy from the battery."""
if self.discharge_array[hour] == 0:
return 0.0, 0.0 # No energy discharge and no losses
return 0.0, 0.0
# Calculate the maximum energy that can be discharged considering min_soc and efficiency
max_possible_discharge_wh = (self.soc_wh - self.min_soc_wh) * self.entlade_effizienz
max_possible_discharge_wh = max(max_possible_discharge_wh, 0.0) # Ensure non-negative
max_possible_discharge_wh = (self.soc_wh - self.min_soc_wh) * self.discharging_efficiency
max_possible_discharge_wh = max(max_possible_discharge_wh, 0.0)
# Consider the maximum discharge power of the battery
max_abgebbar_wh = min(max_possible_discharge_wh, self.max_ladeleistung_w)
max_possible_discharge_wh = min(
max_possible_discharge_wh, self.max_charge_power_w
) # TODO make a new cfg variable max_discharge_power_w
# The actually discharged energy cannot exceed requested energy or maximum discharge
tatsaechlich_abgegeben_wh = min(wh, max_abgebbar_wh)
actual_discharge_wh = min(wh, max_possible_discharge_wh)
actual_withdrawal_wh = (
actual_discharge_wh / self.discharging_efficiency
if self.discharging_efficiency > 0
else 0.0
)
# Calculate the actual amount withdrawn from the battery (before efficiency loss)
if self.entlade_effizienz > 0:
tatsaechliche_entnahme_wh = tatsaechlich_abgegeben_wh / self.entlade_effizienz
else:
tatsaechliche_entnahme_wh = 0.0
# Update the state of charge considering the actual withdrawal
self.soc_wh -= tatsaechliche_entnahme_wh
# Ensure soc_wh does not go below min_soc_wh
self.soc_wh -= actual_withdrawal_wh
self.soc_wh = max(self.soc_wh, self.min_soc_wh)
# Calculate losses due to efficiency
verluste_wh = tatsaechliche_entnahme_wh - tatsaechlich_abgegeben_wh
losses_wh = actual_withdrawal_wh - actual_discharge_wh
return actual_discharge_wh, losses_wh
# Return the actually discharged energy and the losses
return tatsaechlich_abgegeben_wh, verluste_wh
def energie_laden(
def charge_energy(
self, wh: Optional[float], hour: int, relative_power: float = 0.0
) -> tuple[float, float]:
"""Charges energy into the battery."""
if hour is not None and self.charge_array[hour] == 0:
return 0.0, 0.0 # Charging not allowed in this hour
if relative_power > 0.0:
wh = self.max_ladeleistung_w * relative_power
# If no value for wh is given, use the maximum charging power
wh = wh if wh is not None else self.max_ladeleistung_w
wh = self.max_charge_power_w * relative_power
# Calculate the maximum energy that can be charged considering max_soc and efficiency
if self.lade_effizienz > 0:
max_possible_charge_wh = (self.max_soc_wh - self.soc_wh) / self.lade_effizienz
else:
max_possible_charge_wh = 0.0
max_possible_charge_wh = max(max_possible_charge_wh, 0.0) # Ensure non-negative
wh = wh if wh is not None else self.max_charge_power_w
# The actually charged energy cannot exceed requested energy, charging power, or maximum possible charge
effektive_lademenge = min(wh, max_possible_charge_wh)
max_possible_charge_wh = (
(self.max_soc_wh - self.soc_wh) / self.charging_efficiency
if self.charging_efficiency > 0
else 0.0
)
max_possible_charge_wh = max(max_possible_charge_wh, 0.0)
# Energy actually stored in the battery
geladene_menge = effektive_lademenge * self.lade_effizienz
effective_charge_wh = min(wh, max_possible_charge_wh)
charged_wh = effective_charge_wh * self.charging_efficiency
# Update soc_wh
self.soc_wh += geladene_menge
# Ensure soc_wh does not exceed max_soc_wh
self.soc_wh += charged_wh
self.soc_wh = min(self.soc_wh, self.max_soc_wh)
# Calculate losses
verluste_wh = effektive_lademenge - geladene_menge
return geladene_menge, verluste_wh
losses_wh = effective_charge_wh - charged_wh
return charged_wh, losses_wh
def aktueller_energieinhalt(self) -> float:
"""This method returns the current remaining energy considering efficiency.
It accounts for both charging and discharging efficiency.
"""
# Calculate remaining energy considering discharge efficiency
nutzbare_energie = (self.soc_wh - self.min_soc_wh) * self.entlade_effizienz
return max(nutzbare_energie, 0.0)
if __name__ == "__main__":
# Test battery discharge below min_soc
print("Test: Discharge below min_soc")
akku = PVAkku(
PVAkkuParameters(
kapazitaet_wh=10000,
start_soc_prozent=50,
min_soc_prozent=20,
max_soc_prozent=80,
),
hours=1,
)
akku.reset()
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
# Try to discharge 5000 Wh
abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0)
print(f"Energy discharged: {abgegeben_wh} Wh, Losses: {verlust_wh} Wh")
print(f"SoC after discharge: {akku.ladezustand_in_prozent()}%")
print(f"Expected min SoC: {akku.min_soc_prozent}%")
# Test battery charge above max_soc
print("\nTest: Charge above max_soc")
akku = PVAkku(
PVAkkuParameters(
kapazitaet_wh=10000,
start_soc_prozent=50,
min_soc_prozent=20,
max_soc_prozent=80,
),
hours=1,
)
akku.reset()
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
# Try to charge 5000 Wh
geladen_wh, verlust_wh = akku.energie_laden(5000, 0)
print(f"Energy charged: {geladen_wh} Wh, Losses: {verlust_wh} Wh")
print(f"SoC after charge: {akku.ladezustand_in_prozent()}%")
print(f"Expected max SoC: {akku.max_soc_prozent}%")
# Test charging when battery is at max_soc
print("\nTest: Charging when at max_soc")
akku = PVAkku(
PVAkkuParameters(
kapazitaet_wh=10000,
start_soc_prozent=80,
min_soc_prozent=20,
max_soc_prozent=80,
),
hours=1,
)
akku.reset()
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
geladen_wh, verlust_wh = akku.energie_laden(5000, 0)
print(f"Energy charged: {geladen_wh} Wh, Losses: {verlust_wh} Wh")
print(f"SoC after charge: {akku.ladezustand_in_prozent()}%")
# Test discharging when battery is at min_soc
print("\nTest: Discharging when at min_soc")
akku = PVAkku(
PVAkkuParameters(
kapazitaet_wh=10000,
start_soc_prozent=20,
min_soc_prozent=20,
max_soc_prozent=80,
),
hours=1,
)
akku.reset()
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0)
print(f"Energy discharged: {abgegeben_wh} Wh, Losses: {verlust_wh} Wh")
print(f"SoC after discharge: {akku.ladezustand_in_prozent()}%")
def current_energy_content(self) -> float:
"""Returns the current usable energy in the battery."""
usable_energy = (self.soc_wh - self.min_soc_wh) * self.discharging_efficiency
return max(usable_energy, 0.0)