class_ems: AC / DC Charging

class_optimize: Timing Bugs fixed
class_numpy_encoder: JSON Encoder with Numpy support
visualize: AC / DC / Discharge
test_class_ems_2: New Test for AC / DC charging decision
This commit is contained in:
Andreas 2024-10-16 15:40:04 +02:00 committed by Andreas
parent cafed7eaca
commit 87ec02a90e
8 changed files with 585 additions and 240 deletions

View File

@ -5,60 +5,63 @@ import time
# Import necessary modules from the project
from akkudoktoreos.class_optimize import optimization_problem
start_hour = 10
from akkudoktoreos.visualize import *
from akkudoktoreos.class_numpy_encoder import *
start_hour = 0
# PV Forecast (in W)
pv_forecast = [
0,
0,
0,
0,
0,
0,
0,
8.05,
352.91,
728.51,
930.28,
1043.25,
1106.74,
1161.69,
6018.82,
5519.07,
3969.88,
3017.96,
1943.07,
1007.17,
319.67,
7.88,
0,
0,
0,
0,
0,
0,
0,
0,
0,
5.04,
335.59,
705.32,
1121.12,
1604.79,
2157.38,
1433.25,
5718.49,
4553.96,
3027.55,
2574.46,
1720.4,
963.4,
383.3,
0,
0,
0,
]
pv_forecast = np.zeros(48)
pv_forecast[12] = 5000
# [
# 0,
# 0,
# 0,
# 0,
# 0,
# 0,
# 0,
# 8.05,
# 352.91,
# 728.51,
# 930.28,
# 1043.25,
# 1106.74,
# 1161.69,
# 1018.82,
# 1519.07,
# 1969.88,
# 1017.96,
# 1043.07,
# 1007.17,
# 319.67,
# 7.88,
# 0,
# 0,
# 0,
# 0,
# 0,
# 0,
# 0,
# 0,
# 0,
# 5.04,
# 335.59,
# 705.32,
# 1121.12,
# 1604.79,
# 2157.38,
# 1433.25,
# 5718.49,
# 4553.96,
# 3027.55,
# 2574.46,
# 1720.4,
# 963.4,
# 383.3,
# 0,
# 0,
# 0,
# ]
# Temperature Forecast (in degree C)
temperature_forecast = [
@ -113,56 +116,60 @@ temperature_forecast = [
]
# Electricity Price (in Euro per Wh)
strompreis_euro_pro_wh = [
0.0003384,
0.0003318,
0.0003284,
0.0003283,
0.0003289,
0.0003334,
0.0003290,
0.0003302,
0.0003042,
0.0002430,
0.0002280,
0.0002212,
0.0002093,
0.0001879,
0.0001838,
0.0002004,
0.0002198,
0.0002270,
0.0002997,
0.0003195,
0.0003081,
0.0002969,
0.0002921,
0.0002780,
0.0003384,
0.0003318,
0.0003284,
0.0003283,
0.0003289,
0.0003334,
0.0003290,
0.0003302,
0.0003042,
0.0002430,
0.0002280,
0.0002212,
0.0002093,
0.0001879,
0.0001838,
0.0002004,
0.0002198,
0.0002270,
0.0002997,
0.0003195,
0.0003081,
0.0002969,
0.0002921,
0.0002780,
]
strompreis_euro_pro_wh = np.full(48, 0.001)
strompreis_euro_pro_wh [0:10] = 0.00001
strompreis_euro_pro_wh [11:15] = 0.00005
strompreis_euro_pro_wh [20] = 0.00001
# [
# 0.0000384,
# 0.0000318,
# 0.0000284,
# 0.0008283,
# 0.0008289,
# 0.0008334,
# 0.0008290,
# 0.0003302,
# 0.0003042,
# 0.0002430,
# 0.0002280,
# 0.0002212,
# 0.0002093,
# 0.0001879,
# 0.0001838,
# 0.0002004,
# 0.0002198,
# 0.0002270,
# 0.0002997,
# 0.0003195,
# 0.0003081,
# 0.0002969,
# 0.0002921,
# 0.0002780,
# 0.0003384,
# 0.0003318,
# 0.0003284,
# 0.0003283,
# 0.0003289,
# 0.0003334,
# 0.0003290,
# 0.0003302,
# 0.0003042,
# 0.0002430,
# 0.0002280,
# 0.0002212,
# 0.0002093,
# 0.0001879,
# 0.0001838,
# 0.0002004,
# 0.0002198,
# 0.0002270,
# 0.0002997,
# 0.0003195,
# 0.0003081,
# 0.0002969,
# 0.0002921,
# 0.0002780,
# ]
# Overall System Load (in W)
gesamtlast = [
@ -221,10 +228,10 @@ start_solution = None
# Define parameters for the optimization problem
parameter = {
# Cost of storing energy in battery (per Wh)
"preis_euro_pro_wh_akku": 10e-05,
# Value of energy in battery (per Wh)
"preis_euro_pro_wh_akku": 0e-05,
# Initial state of charge (SOC) of PV battery (%)
"pv_soc": 80,
"pv_soc": 15,
# Battery capacity (in Wh)
"pv_akku_cap": 26400,
# Yearly energy consumption (in Wh)
@ -242,7 +249,7 @@ parameter = {
# Electricity price forecast (48 hours)
"strompreis_euro_pro_wh": strompreis_euro_pro_wh,
# Minimum SOC for electric car
"eauto_min_soc": 20,
"eauto_min_soc": 50,
# Electric car battery capacity (Wh)
"eauto_cap": 60000,
# Charging efficiency of the electric car
@ -250,7 +257,7 @@ parameter = {
# Charging power of the electric car (W)
"eauto_charge_power": 11040,
# Current SOC of the electric car (%)
"eauto_soc": 5,
"eauto_soc": 15,
# Current PV power generation (W)
"pvpowernow": 211.137503624,
# Initial solution for the optimization
@ -258,7 +265,7 @@ parameter = {
# Household appliance consumption (Wh)
"haushaltsgeraet_wh": 5000,
# Duration of appliance usage (hours)
"haushaltsgeraet_dauer": 2,
"haushaltsgeraet_dauer": 0,
# Minimum Soc PV Battery
"min_soc_prozent": 15,
}
@ -282,8 +289,25 @@ elapsed_time = end_time - start_time
print(f"Elapsed time: {elapsed_time:.4f} seconds")
# Print or visualize the result
# pprint(ergebnis)
#json_data = json.dumps(ergebnis)
#print(json_data)
ac_charge, dc_charge, discharge = (ergebnis["ac_charge"],ergebnis["dc_charge"],ergebnis["discharge_allowed"])
visualisiere_ergebnisse(
gesamtlast,
pv_forecast,
strompreis_euro_pro_wh,
ergebnis["result"],
ac_charge,
dc_charge,
discharge,
temperature_forecast,
start_hour,
48,
np.full(48, parameter["einspeiseverguetung_euro_pro_wh"]),
filename="visualization_results.pdf",
extra_data=None,
)
json_data = NumpyEncoder.dumps(ergebnis)
print(json_data)

View File

@ -20,7 +20,7 @@ class PVAkku:
self.soc_wh = (start_soc_prozent / 100) * kapazitaet_wh
self.hours = hours if hours is not None else 24 # Default to 24 hours if not specified
self.discharge_array = np.full(self.hours, 1)
self.charge_array = np.full(self.hours, 0)
self.charge_array = np.full(self.hours, 1)
# Charge and discharge efficiency
self.lade_effizienz = lade_effizienz
self.entlade_effizienz = entlade_effizienz
@ -70,26 +70,19 @@ class PVAkku:
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, 0)
self.charge_array = np.full(self.hours, 1)
def set_discharge_per_hour(self, discharge_array):
assert len(discharge_array) == self.hours
self.discharge_array = np.array(discharge_array)
# Ensure no simultaneous charging and discharging in the same hour using NumPy mask
conflict_mask = (self.charge_array > 0) & (self.discharge_array > 0)
# Prioritize discharge by setting charge to 0 where both are > 0
self.charge_array[conflict_mask] = 0
def set_charge_per_hour(self, charge_array):
assert len(charge_array) == self.hours
self.charge_array = np.array(charge_array)
# Ensure no simultaneous charging and discharging in the same hour using NumPy mask
conflict_mask = (self.charge_array > 0) & (self.discharge_array > 0)
# Prioritize discharge by setting charge to 0 where both are > 0
self.discharge_array[conflict_mask] = 0
def set_charge_allowed_for_hour(self, charge, hour):
assert hour < self.hours
self.charge_array[hour] = charge
def ladezustand_in_prozent(self):
return (self.soc_wh / self.kapazitaet_wh) * 100
@ -125,17 +118,14 @@ class PVAkku:
# Return the actually discharged energy and the losses
return tatsaechlich_abgegeben_wh, verluste_wh
def energie_laden(self, wh, hour):
def energie_laden(self, wh, hour, relative_power=0.0):
if hour is not None and self.charge_array[hour] == 0:
return 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
# Relative to the maximum charging power (between 0 and 1)
relative_ladeleistung = self.charge_array[hour]
effektive_ladeleistung = relative_ladeleistung * self.max_ladeleistung_w
# 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
@ -144,8 +134,8 @@ class PVAkku:
max_possible_charge_wh = max(max_possible_charge_wh, 0.0) # Ensure non-negative
# The actually charged energy cannot exceed requested energy, charging power, or maximum possible charge
effektive_lademenge = min(wh, effektive_ladeleistung, max_possible_charge_wh)
effektive_lademenge = min(wh, max_possible_charge_wh)
# Energy actually stored in the battery
geladene_menge = effektive_lademenge * self.lade_effizienz
@ -153,10 +143,9 @@ class PVAkku:
self.soc_wh += geladene_menge
# Ensure soc_wh does not exceed max_soc_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
def aktueller_energieinhalt(self):

View File

@ -1,6 +1,6 @@
from datetime import datetime
from typing import Dict, List, Optional, Union
from akkudoktoreos.config import *
import numpy as np
@ -23,12 +23,17 @@ class EnergieManagementSystem:
self.eauto = eauto
self.haushaltsgeraet = haushaltsgeraet
self.wechselrichter = wechselrichter
self.ac_charge_hours = np.full(prediction_hours,0)
self.dc_charge_hours = np.full(prediction_hours,1)
def set_akku_discharge_hours(self, ds: List[int]) -> None:
self.akku.set_discharge_per_hour(ds)
def set_akku_charge_hours(self, ds: List[int]) -> None:
self.akku.set_charge_per_hour(ds)
def set_akku_ac_charge_hours(self, ds: np.ndarray) -> None:
self.ac_charge_hours = ds
def set_akku_dc_charge_hours(self, ds: np.ndarray) -> None:
self.dc_charge_hours = ds
def set_eauto_charge_hours(self, ds: List[int]) -> None:
self.eauto.set_charge_per_hour(ds)
@ -46,7 +51,12 @@ class EnergieManagementSystem:
return self.simuliere(start_stunde)
def simuliere(self, start_stunde: int) -> dict:
# Ensure arrays have the same length
'''
hour:
akku_soc_pro_stunde begin of the hour, initial hour state!
last_wh_pro_stunde integral of last hour (end state)
'''
lastkurve_wh = self.gesamtlast
assert (
len(lastkurve_wh) == len(self.pv_prognose_wh) == len(self.strompreis_euro_pro_wh)
@ -91,16 +101,21 @@ class EnergieManagementSystem:
eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent()
# AC PV Battery Charge
if self.akku.charge_array[stunde] > 0.0:
geladene_menge, verluste_wh = self.akku.energie_laden(None,stunde)
if self.ac_charge_hours[stunde] > 0.0:
self.akku.set_charge_allowed_for_hour(self.ac_charge_hours[stunde],stunde)
geladene_menge, verluste_wh = self.akku.energie_laden(None,stunde,relative_power=self.ac_charge_hours[stunde])
verbrauch += geladene_menge
verluste_wh_pro_stunde[stunde_since_now] += verluste_wh
verluste_wh_pro_stunde[stunde_since_now] += verluste_wh
# Process inverter logic
erzeugung = self.pv_prognose_wh[stunde]
self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde],stunde)
netzeinspeisung, netzbezug, verluste, eigenverbrauch = (
self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde)
)
netzeinspeisung_wh_pro_stunde[stunde_since_now] = netzeinspeisung
netzbezug_wh_pro_stunde[stunde_since_now] = netzbezug
verluste_wh_pro_stunde[stunde_since_now] += verluste
@ -136,4 +151,5 @@ class EnergieManagementSystem:
"Gesamt_Verluste": np.nansum(verluste_wh_pro_stunde),
"Haushaltsgeraet_wh_pro_stunde": haushaltsgeraet_wh_pro_stunde,
}
return out

View File

@ -0,0 +1,24 @@
import json
import numpy as np
class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.ndarray):
return obj.tolist() # Convert NumPy arrays to lists
if isinstance(obj, np.generic):
return obj.item() # Convert NumPy scalars to native Python types
return super(NumpyEncoder, self).default(obj)
@staticmethod
def dumps(data):
"""
Static method to serialize a Python object into a JSON string using NumpyEncoder.
Args:
data: The Python object to serialize.
Returns:
str: A JSON string representation of the object.
"""
return json.dumps(data, cls=NumpyEncoder)

View File

@ -12,6 +12,7 @@ from akkudoktoreos.config import possible_ev_charge_currents
from akkudoktoreos.visualize import visualisiere_ergebnisse
class optimization_problem:
def __init__(
self,
@ -35,53 +36,104 @@ class optimization_problem:
if fixed_seed is not None:
random.seed(fixed_seed)
def split_charge_discharge(self, discharge_hours_bin: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
def decode_charge_discharge(self, discharge_hours_bin: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Split the input array `discharge_hours_bin` into two separate arrays:
- `charge`: Contains only the negative values from `discharge_hours_bin` (charging values).
- `discharge`: Contains only the positive values from `discharge_hours_bin` (discharging values).
Decode the input array `discharge_hours_bin` into three separate arrays for AC charging, DC charging, and discharge.
The function maps AC and DC charging values to relative power levels (0 to 1), while the discharge remains binary (0 or 1).
Parameters:
- discharge_hours_bin (np.ndarray): Input array with both positive and negative values.
- discharge_hours_bin (np.ndarray): Input array with integer values representing the different states.
The states are:
0: No action ("idle")
1: Discharge ("discharge")
2-6: AC charging with different power levels ("ac_charge")
7-8: DC charging Dissallowed/allowed ("dc_charge")
Returns:
- charge (np.ndarray): Array with negative values from `discharge_hours_bin`, other values set to 0.
- discharge (np.ndarray): Array with positive values from `discharge_hours_bin`, other values set to 0.
- ac_charge (np.ndarray): Array with AC charging values as relative power (0-1), other values set to 0.
- dc_charge (np.ndarray): Array with DC charging values as relative power (0-1), other values set to 0.
- discharge (np.ndarray): Array with discharge values (1 for discharge, 0 otherwise).
"""
# Convert the input list to a NumPy array, if it's not already
discharge_hours_bin = np.array(discharge_hours_bin)
# Create charge array: Keep only negative values, set the rest to 0
charge = -np.where(discharge_hours_bin < 0, discharge_hours_bin, 0)
charge = charge / np.max(charge)
# Create discharge array: Keep only positive values, set the rest to 0
discharge = np.where(discharge_hours_bin > 0, discharge_hours_bin, 0)
# Create ac_charge array: Only consider values between 2 and 6 (AC charging power levels), set the rest to 0
ac_charge = np.where((discharge_hours_bin >= 2) & (discharge_hours_bin <= 6), discharge_hours_bin - 1, 0)
ac_charge = ac_charge / 5.0 # Normalize AC charge to range between 0 and 1
# Create dc_charge array: 7 = Not allowed (mapped to 0), 8 = Allowed (mapped to 1)
dc_charge = np.where(discharge_hours_bin == 8, 1, 0)
# Create discharge array: Only consider value 1 (Discharge), set the rest to 0 (binary output)
discharge = np.where(discharge_hours_bin == 1, 1, 0)
return ac_charge, dc_charge, discharge
return charge, discharge
# Custom mutation function that applies type-specific mutations
def mutate(self,individual):
# Mutate the discharge state genes (-1, 0, 1)
individual[:self.prediction_hours], = self.toolbox.mutate_discharge(
individual[:self.prediction_hours]
)
def mutate(self, individual):
"""
Custom mutation function for the individual. This function mutates different parts of the individual:
- Mutates the discharge and charge states (AC, DC, idle) using the split_charge_discharge method.
- Mutates the EV charging schedule if EV optimization is enabled.
- Mutates appliance start times if household appliances are part of the optimization.
Parameters:
- individual (list): The individual being mutated, which includes different optimization parameters.
Returns:
- (tuple): The mutated individual as a tuple (required by DEAP).
"""
# Step 1: Mutate the charge/discharge states (idle, discharge, AC charge, DC charge)
# Extract the relevant part of the individual for prediction hours, which represents the charge/discharge behavior.
charge_discharge_part = individual[:self.prediction_hours]
# Apply the mutation to the charge/discharge part
charge_discharge_mutated, = self.toolbox.mutate_charge_discharge(charge_discharge_part)
# Ensure that no invalid states are introduced during mutation (valid values: 0-8)
charge_discharge_mutated = np.clip(charge_discharge_mutated, 0, 8)
# Use split_charge_discharge to split the mutated array into AC charge, DC charge, and discharge components
#ac_charge, dc_charge, discharge = self.split_charge_discharge(charge_discharge_mutated)
# Optionally: You can process the split arrays further if needed, for example,
# applying additional constraints or penalties, or keeping track of charging limits.
# Reassign the mutated values back to the individual
individual[:self.prediction_hours] = charge_discharge_mutated
# Step 2: Mutate EV charging schedule if enabled
if self.optimize_ev:
# Mutate the EV charging indices
# Extract the relevant part for EV charging schedule
ev_charge_part = individual[self.prediction_hours : self.prediction_hours * 2]
# Apply mutation on the EV charging schedule
ev_charge_part_mutated, = self.toolbox.mutate_ev_charge_index(ev_charge_part)
# Ensure the EV does not charge during fixed hours (set those hours to 0)
ev_charge_part_mutated[self.prediction_hours - self.fixed_eauto_hours :] = [0] * self.fixed_eauto_hours
# Reassign the mutated EV charging part back to the individual
individual[self.prediction_hours : self.prediction_hours * 2] = ev_charge_part_mutated
# Mutate the appliance start hour if present
# Step 3: Mutate appliance start times if household appliances are part of the optimization
if self.opti_param["haushaltsgeraete"] > 0:
# Extract the appliance part (typically a single value for the start hour)
appliance_part = [individual[-1]]
# Apply mutation on the appliance start hour
appliance_part_mutated, = self.toolbox.mutate_hour(appliance_part)
# Reassign the mutated appliance part back to the individual
individual[-1] = appliance_part_mutated[0]
return (individual,)
# Method to create an individual based on the conditions
def create_individual(self):
# Start with discharge states for the individual
@ -106,8 +158,14 @@ class optimization_problem:
2. Electric vehicle charge hours (possible_charge_values),
3. Dishwasher start time (integer if applicable).
"""
discharge_hours_bin = individual[: self.prediction_hours]
eautocharge_hours_float = individual[self.prediction_hours : self.prediction_hours * 2]
eautocharge_hours_float = (
individual[self.prediction_hours : self.prediction_hours * 2]
if self.optimize_ev
else None
)
spuelstart_int = (
individual[-1]
if self.opti_param and self.opti_param.get("haushaltsgeraete", 0) > 0
@ -132,13 +190,12 @@ class optimization_problem:
# Initialize toolbox with attributes and operations
self.toolbox = base.Toolbox()
self.toolbox.register("attr_discharge_state", random.randint, -5, 1)
self.toolbox.register("attr_discharge_state", random.randint, 0,11)
if self.optimize_ev:
self.toolbox.register("attr_ev_charge_index", random.randint, 0, len(possible_ev_charge_currents) - 1)
self.toolbox.register("attr_int", random.randint, start_hour, 23)
# Register individual creation function
self.toolbox.register("individual", self.create_individual)
@ -148,7 +205,7 @@ class optimization_problem:
#self.toolbox.register("mutate", tools.mutFlipBit, indpb=0.1)
# Register separate mutation functions for each type of value:
# - Discharge state mutation (-5, 0, 1)
self.toolbox.register("mutate_discharge", tools.mutUniformInt, low=-5, up=1, indpb=0.1)
self.toolbox.register("mutate_charge_discharge", tools.mutUniformInt, low=0, up=8, indpb=0.1)
# - Float mutation for EV charging values
self.toolbox.register("mutate_ev_charge_index", tools.mutUniformInt, low=0, up=len(possible_ev_charge_currents) - 1, indpb=0.1)
# - Start hour mutation for household devices
@ -173,11 +230,12 @@ class optimization_problem:
if self.opti_param.get("haushaltsgeraete", 0) > 0:
ems.set_haushaltsgeraet_start(spuelstart_int, global_start_hour=start_hour)
charge, discharge = self.split_charge_discharge(discharge_hours_bin)
ac,dc,discharge = self.decode_charge_discharge(discharge_hours_bin)
ems.set_akku_discharge_hours(discharge)
ems.set_akku_charge_hours(charge)
ems.set_akku_dc_charge_hours(dc)
ems.set_akku_ac_charge_hours(ac)
if self.optimize_ev:
eautocharge_hours_float = [
@ -212,13 +270,6 @@ class optimization_problem:
0.01 for i in range(self.prediction_hours) if discharge_hours_bin[i] == 0.0
)
# Penalty for charging the electric vehicle during restricted hours
# gesamtbilanz += sum(
# self.strafe
# for i in range(self.prediction_hours - self.fixed_eauto_hours, self.prediction_hours)
# if eautocharge_hours_float[i] != 0.0
# )
# Penalty for not meeting the minimum SOC (State of Charge) requirement
if parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent() <= 0.0:
gesamtbilanz += sum(
@ -266,8 +317,8 @@ class optimization_problem:
self.toolbox,
mu=100,
lambda_=150,
cxpb=0.5,
mutpb=0.5,
cxpb=0.7,
mutpb=0.3,
ngen=ngen,
stats=stats,
halloffame=hof,
@ -361,14 +412,18 @@ class optimization_problem:
start_solution
)
ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin)
# Visualize the results
visualisiere_ergebnisse(
parameter["gesamtlast"],
parameter["pv_forecast"],
parameter["strompreis_euro_pro_wh"],
o,
discharge_hours_bin,
eautocharge_hours_float,
ac_charge,
dc_charge,
discharge,
parameter["temperature_forecast"],
start_hour,
self.prediction_hours,
@ -395,7 +450,7 @@ class optimization_problem:
element_list = o[key].tolist()
# Change the first value to None
element_list[0] = None
#element_list[0] = None
# Change the NaN to None (JSON)
element_list = [
None if isinstance(x, (int, float)) and np.isnan(x) else x for x in element_list
@ -406,7 +461,9 @@ class optimization_problem:
# Return final results as a dictionary
return {
"discharge_hours_bin": discharge_hours_bin,
"ac_charge": ac_charge.tolist(),
"dc_charge":dc_charge.tolist(),
"discharge_allowed":discharge.tolist(),
"eautocharge_hours_float": eautocharge_hours_float,
"result": o,
"eauto_obj": ems.eauto.to_dict(),

View File

@ -18,8 +18,9 @@ def visualisiere_ergebnisse(
pv_forecast,
strompreise,
ergebnisse,
discharge_hours,
laden_moeglich,
ac, # AC charging allowed
dc, # DC charging allowed
discharge, # Discharge allowed
temperature,
start_hour,
prediction_hours,
@ -58,21 +59,7 @@ def visualisiere_ergebnisse(
plt.grid(True)
plt.legend()
# Electricity prices
hours_p = np.arange(0, len(strompreise))
plt.subplot(3, 2, 2)
plt.plot(
hours_p,
strompreise,
label="Electricity Price (€/Wh)",
color="purple",
marker="s",
)
plt.title("Electricity Prices")
plt.xlabel("Hour of the Day")
plt.ylabel("Price (€/Wh)")
plt.legend()
plt.grid(True)
# PV forecast
plt.subplot(3, 2, 3)
@ -122,30 +109,48 @@ def visualisiere_ergebnisse(
# Energy flow, grid feed-in, and grid consumption
plt.subplot(3, 2, 1)
plt.plot(hours, ergebnisse["Last_Wh_pro_Stunde"], label="Load (Wh)", marker="o")
# Plot with transparency (alpha) and different linestyles
plt.plot(
hours,
ergebnisse["Haushaltsgeraet_wh_pro_stunde"],
label="Household Device (Wh)",
marker="o",
hours, ergebnisse["Last_Wh_pro_Stunde"], label="Load (Wh)", marker="o", linestyle="-", alpha=0.8
)
plt.plot(
hours,
ergebnisse["Netzeinspeisung_Wh_pro_Stunde"],
label="Grid Feed-in (Wh)",
marker="x",
hours, ergebnisse["Haushaltsgeraet_wh_pro_stunde"], label="Household Device (Wh)", marker="o", linestyle="--", alpha=0.8
)
plt.plot(
hours,
ergebnisse["Netzbezug_Wh_pro_Stunde"],
label="Grid Consumption (Wh)",
marker="^",
hours, ergebnisse["Netzeinspeisung_Wh_pro_Stunde"], label="Grid Feed-in (Wh)", marker="x", linestyle=":", alpha=0.8
)
plt.plot(hours, ergebnisse["Verluste_Pro_Stunde"], label="Losses (Wh)", marker="^")
plt.plot(
hours, ergebnisse["Netzbezug_Wh_pro_Stunde"], label="Grid Consumption (Wh)", marker="^", linestyle="-.", alpha=0.8
)
plt.plot(
hours, ergebnisse["Verluste_Pro_Stunde"], label="Losses (Wh)", marker="^", linestyle="-", alpha=0.8
)
# Title and labels
plt.title("Energy Flow per Hour")
plt.xlabel("Hour")
plt.ylabel("Energy (Wh)")
# Show legend with a higher number of columns to avoid overlap
plt.legend(ncol=2)
# Electricity prices
hours_p = np.arange(0, len(strompreise))
plt.subplot(3, 2, 3)
plt.plot(
hours_p,
strompreise,
label="Electricity Price (€/Wh)",
color="purple",
marker="s",
)
plt.title("Electricity Prices")
plt.xlabel("Hour of the Day")
plt.ylabel("Price (€/Wh)")
plt.legend()
plt.grid(True)
# State of charge for batteries
plt.subplot(3, 2, 2)
@ -159,42 +164,30 @@ def visualisiere_ergebnisse(
plt.legend(loc="upper left", bbox_to_anchor=(1, 1)) # Place legend outside the plot
plt.grid(True, which="both", axis="x") # Grid for every hour
ax1 = plt.subplot(3, 2, 3)
# Plot charge and discharge values
for hour, value in enumerate(discharge_hours):
# Determine color and label based on the value
if value > 0: # Positive values (discharge)
color = "red"
label = "Discharge" if hour == 0 else ""
elif value < 0: # Negative values (charge)
color = "blue"
label = "Charge" if hour == 0 else ""
# Plot for AC, DC charging, and Discharge status using bar charts
ax1 = plt.subplot(3, 2, 5)
else:
continue # Skip zero values
# Plot AC charging as bars (relative values between 0 and 1)
plt.bar(hours, ac, width=0.4, label="AC Charging (relative)", color="blue", alpha=0.6)
# Create colored areas with `axvspan`
ax1.axvspan(
hour, # Start of the hour
hour + 1, # End of the hour
ymin=0, # Lower bound
ymax=abs(value) / 5 if value < 0 else value, # Adjust height based on the value
color=color,
alpha=0.3,
label=label
)
# Plot DC charging as bars (relative values between 0 and 1)
plt.bar(hours + 0.4, dc, width=0.4, label="DC Charging (relative)", color="green", alpha=0.6)
# Plot Discharge as bars (0 or 1, binary values)
plt.bar(hours, discharge, width=0.4, label="Discharge Allowed", color="red", alpha=0.6, bottom=np.maximum(ac, dc))
# Configure the plot
ax1.legend(loc="upper left")
ax1.set_xlim(0, prediction_hours)
ax1.set_xlabel("Hour")
ax1.set_ylabel("Charge/Discharge Level")
ax1.set_title("Charge and Discharge Hours Overview")
ax1.set_ylabel("Relative Power (0-1) / Discharge (0 or 1)")
ax1.set_title("AC/DC Charging and Discharge Overview")
ax1.grid(True)
pdf.savefig() # Save the current figure state to the PDF
plt.close() # Close the current figure to free up memory
@ -219,7 +212,6 @@ def visualisiere_ergebnisse(
)
# Annotate costs
for hour, value in enumerate(costs):
print(hour, " ", value)
if value == None or np.isnan(value):
value=0
axs[0].annotate(f'{value:.2f}', (hour, value), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8, color='red')

View File

@ -5,6 +5,7 @@ from akkudoktoreos.class_akku import PVAkku
from akkudoktoreos.class_ems import EnergieManagementSystem
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
from akkudoktoreos.class_inverter import Wechselrichter # Example import
from akkudoktoreos.visualize import *
prediction_hours = 48
optimization_hours = 24
@ -32,7 +33,7 @@ def create_ems_instance():
# Example initialization of electric car battery
eauto = PVAkku(kapazitaet_wh=26400, start_soc_prozent=10, hours=48, min_soc_prozent=10)
eauto.set_charge_per_hour(np.full(48,1))
# Parameters based on previous example data
pv_prognose_wh = [
0,
@ -199,6 +200,9 @@ def create_ems_instance():
haushaltsgeraet=home_appliance,
wechselrichter=wechselrichter,
)
return ems
@ -212,6 +216,21 @@ def test_simulation(create_ems_instance):
result = ems.simuliere(start_stunde=start_hour)
visualisiere_ergebnisse(
ems.gesamtlast,
ems.pv_prognose_wh,
ems.strompreis_euro_pro_wh,
result,
ems.akku.discharge_array+ems.akku.charge_array,
None,
ems.pv_prognose_wh,
start_hour,
48,
np.full(48, 0.0),
filename="visualization_results.pdf",
extra_data=None,
)
# Assertions to validate results
assert result is not None, "Result should not be None"
assert isinstance(result, dict), "Result should be a dictionary"

224
tests/test_class_ems_2.py Normal file
View File

@ -0,0 +1,224 @@
import numpy as np
import pytest
from akkudoktoreos.visualize import *
from akkudoktoreos.class_akku import PVAkku
from akkudoktoreos.class_ems import EnergieManagementSystem
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
from akkudoktoreos.class_inverter import Wechselrichter # Example import
prediction_hours = 48
optimization_hours = 24
start_hour = 0
# Example initialization of necessary components
@pytest.fixture
def create_ems_instance():
"""
Fixture to create an EnergieManagementSystem instance with given test parameters.
"""
# Initialize the battery and the inverter
akku = PVAkku(kapazitaet_wh=5000, start_soc_prozent=80, hours=48, min_soc_prozent=10)
akku.reset()
wechselrichter = Wechselrichter(10000, akku)
# Household device (currently not used, set to None)
home_appliance = Haushaltsgeraet(
hours=prediction_hours,
verbrauch_wh=2000,
dauer_h=2,
)
home_appliance.set_startzeitpunkt(2)
# Example initialization of electric car battery
eauto = PVAkku(kapazitaet_wh=26400, start_soc_prozent=100, hours=48, min_soc_prozent=100)
# Parameters based on previous example data
pv_prognose_wh = np.full(prediction_hours, 0)
pv_prognose_wh[10] = 5000.0
pv_prognose_wh[11] = 5000.0
strompreis_euro_pro_wh = np.full(48, 0.001)
strompreis_euro_pro_wh [0:10] = 0.00001
strompreis_euro_pro_wh [11:15] = 0.00005
strompreis_euro_pro_wh [20] = 0.00001
einspeiseverguetung_euro_pro_wh = [0.00007] * len(strompreis_euro_pro_wh)
gesamtlast = [
676.71,
876.19,
527.13,
468.88,
531.38,
517.95,
483.15,
472.28,
1011.68,
995.00,
1053.07,
1063.91,
1320.56,
1132.03,
1163.67,
1176.82,
1216.22,
1103.78,
1129.12,
1178.71,
1050.98,
988.56,
912.38,
704.61,
516.37,
868.05,
694.34,
608.79,
556.31,
488.89,
506.91,
804.89,
1141.98,
1056.97,
992.46,
1155.99,
827.01,
1257.98,
1232.67,
871.26,
860.88,
1158.03,
1222.72,
1221.04,
949.99,
987.01,
733.99,
592.97,
]
# Initialize the energy management system with the respective parameters
ems = EnergieManagementSystem(
pv_prognose_wh=pv_prognose_wh,
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
eauto=eauto,
gesamtlast=gesamtlast,
haushaltsgeraet=home_appliance,
wechselrichter=wechselrichter,
)
ac= np.full(prediction_hours,0)
ac[20] = 1
ems.set_akku_ac_charge_hours(ac)
dc= np.full(prediction_hours,0)
dc[11] = 1
ems.set_akku_dc_charge_hours(dc)
return ems
def test_simulation(create_ems_instance):
"""
Test the EnergieManagementSystem simulation method.
"""
ems = create_ems_instance
# Simulate starting from hour 0 (this value can be adjusted)
result = ems.simuliere(start_stunde=start_hour)
# --- Pls do not remove! ---
# visualisiere_ergebnisse(
# ems.gesamtlast,
# ems.pv_prognose_wh,
# ems.strompreis_euro_pro_wh,
# result,
# ems.akku.discharge_array+ems.akku.charge_array,
# None,
# ems.pv_prognose_wh,
# start_hour,
# 48,
# np.full(48, 0.0),
# filename="visualization_results.pdf",
# extra_data=None,
# )
# Assertions to validate results
assert result is not None, "Result should not be None"
assert isinstance(result, dict), "Result should be a dictionary"
assert "Last_Wh_pro_Stunde" in result, "Result should contain 'Last_Wh_pro_Stunde'"
"""
Check the result of the simulation based on expected values.
"""
# Example result returned from the simulation (used for assertions)
assert result is not None, "Result should not be None."
# Check that the result is a dictionary
assert isinstance(result, dict), "Result should be a dictionary."
# Verify that the expected keys are present in the result
expected_keys = [
"Last_Wh_pro_Stunde",
"Netzeinspeisung_Wh_pro_Stunde",
"Netzbezug_Wh_pro_Stunde",
"Kosten_Euro_pro_Stunde",
"akku_soc_pro_stunde",
"Einnahmen_Euro_pro_Stunde",
"Gesamtbilanz_Euro",
"E-Auto_SoC_pro_Stunde",
"Gesamteinnahmen_Euro",
"Gesamtkosten_Euro",
"Verluste_Pro_Stunde",
"Gesamt_Verluste",
"Haushaltsgeraet_wh_pro_stunde",
]
for key in expected_keys:
assert key in result, f"The key '{key}' should be present in the result."
# Check the length of the main arrays
assert (
len(result["Last_Wh_pro_Stunde"]) == 48
), "The length of 'Last_Wh_pro_Stunde' should be 48."
assert (
len(result["Netzeinspeisung_Wh_pro_Stunde"]) == 48
), "The length of 'Netzeinspeisung_Wh_pro_Stunde' should be 48."
assert (
len(result["Netzbezug_Wh_pro_Stunde"]) == 48
), "The length of 'Netzbezug_Wh_pro_Stunde' should be 48."
assert (
len(result["Kosten_Euro_pro_Stunde"]) == 48
), "The length of 'Kosten_Euro_pro_Stunde' should be 48."
assert (
len(result["akku_soc_pro_stunde"]) == 48
), "The length of 'akku_soc_pro_stunde' should be 48."
# Verfify DC and AC Charge Bins
assert (
abs(result["akku_soc_pro_stunde"][10] - 10.0) < 1e-5
), "'akku_soc_pro_stunde[10]' should be 10."
assert (
abs(result["akku_soc_pro_stunde"][11] -79.275184) < 1e-5
), "'akku_soc_pro_stunde[11]' should be 79.275184."
assert (
abs(result["Netzeinspeisung_Wh_pro_Stunde"][10] - 3946.93) < 1e-3
), "'Netzeinspeisung_Wh_pro_Stunde[11]' should be 4000."
assert (
abs(result["Netzeinspeisung_Wh_pro_Stunde"][11] - 0.0) < 1e-3
), "'Netzeinspeisung_Wh_pro_Stunde[11]' should be 0.0."
assert (
abs(result["akku_soc_pro_stunde"][20] - 98
) < 1e-5
), "'akku_soc_pro_stunde[11]' should be 98."
assert (
abs(result["Last_Wh_pro_Stunde"][20] - 5450.98) < 1e-3
), "'Netzeinspeisung_Wh_pro_Stunde[11]' should be 0.0."
print("All tests passed successfully.")