mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-08-31 12:46:38 +00:00
fix(optimization): add battery self-consumption state
This commit is contained in:
@@ -80,6 +80,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
are deprecated in favour of `appliance_starts` and `result.home_appliance_energy_wh`.
|
are deprecated in favour of `appliance_starts` and `result.home_appliance_energy_wh`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Allow the direct-marketing optimizer to select a true battery self-consumption state with DC
|
||||||
|
charging and local-load discharge enabled in the same slot. Existing warm-start state numbers
|
||||||
|
remain compatible, and educated guesses now use the combined state for PV/load overlap instead
|
||||||
|
of unnecessarily bypassing PV while serving loads such as EV charging.
|
||||||
|
- Account for EV charging losses in the AC load seen by the inverter and grid, so fitness and
|
||||||
|
energy costs use the charger's raw input rather than only the energy stored in the EV battery.
|
||||||
|
- Treat fitness memoization as disabled for lightweight optimizer instances constructed without
|
||||||
|
the normal initializer, preserving isolated penalty evaluation and test callers.
|
||||||
- Re-simulate genetic candidates after removing EV charging genes from slots that begin at full
|
- Re-simulate genetic candidates after removing EV charging genes from slots that begin at full
|
||||||
SoC, keeping the repaired genome and its assigned fitness consistent.
|
SoC, keeping the repaired genome and its assigned fitness consistent.
|
||||||
- FeedInTariffEnergyCharts no longer aborts the whole prediction/optimization when the
|
- FeedInTariffEnergyCharts no longer aborts the whole prediction/optimization when the
|
||||||
|
|||||||
@@ -357,6 +357,11 @@ smaller values (e.g. `0.0`) disable the penalty entirely.
|
|||||||
- `discharge_allowed`: Battery discharge permission for local self-consumption/load coverage (0 or 1)
|
- `discharge_allowed`: Battery discharge permission for local self-consumption/load coverage (0 or 1)
|
||||||
- `battery_grid_export_allowed`: Battery discharge permission for grid export/direct marketing (0 or 1)
|
- `battery_grid_export_allowed`: Battery discharge permission for grid export/direct marketing (0 or 1)
|
||||||
|
|
||||||
|
With direct marketing enabled, `dc_charge = 1` and `discharge_allowed = 1` may occur together. This
|
||||||
|
is the normal self-consumption mode: within a coarse optimization slot, the battery may cover
|
||||||
|
probabilistic load gaps and store PV surplus from different sub-intervals. A discharge-only state
|
||||||
|
remains available when deliberately bypassing PV charging is economically preferable.
|
||||||
|
|
||||||
0 (no charge)
|
0 (no charge)
|
||||||
1 (charge with full load)
|
1 (charge with full load)
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ class FitnessCacheEntry:
|
|||||||
extra_data: tuple[float, float, float]
|
extra_data: tuple[float, float, float]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BatteryStateLayout:
|
||||||
|
"""Indices of optional battery states appended to the legacy state ranges."""
|
||||||
|
|
||||||
|
total_states: int
|
||||||
|
dc_not_allowed_state: Optional[int] = None
|
||||||
|
dc_allowed_state: Optional[int] = None
|
||||||
|
grid_export_state: Optional[int] = None
|
||||||
|
self_consumption_state: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class GeneticSimulation(PydanticBaseModel):
|
class GeneticSimulation(PydanticBaseModel):
|
||||||
"""Device simulation for GENETIC optimization algorithm."""
|
"""Device simulation for GENETIC optimization algorithm."""
|
||||||
|
|
||||||
@@ -411,10 +422,12 @@ class GeneticSimulation(PydanticBaseModel):
|
|||||||
if ev_fast:
|
if ev_fast:
|
||||||
soc_ev_per_hour[hour_idx] = ev_fast.current_soc_percentage() # save begin state
|
soc_ev_per_hour[hour_idx] = ev_fast.current_soc_percentage() # save begin state
|
||||||
if ev_charge_hours_fast[hour] > 0:
|
if ev_charge_hours_fast[hour] > 0:
|
||||||
loaded_energy_ev, verluste_eauto = ev_fast.charge_energy(
|
stored_energy_ev, verluste_eauto = ev_fast.charge_energy(
|
||||||
wh=None, hour=hour, charge_factor=ev_charge_hours_fast[hour]
|
wh=None, hour=hour, charge_factor=ev_charge_hours_fast[hour]
|
||||||
)
|
)
|
||||||
consumption += loaded_energy_ev
|
# The inverter/grid must supply the EV charger's raw input,
|
||||||
|
# not only the energy stored after charging losses.
|
||||||
|
consumption += stored_energy_ev + verluste_eauto
|
||||||
losses_wh_per_hour[hour_idx] += verluste_eauto
|
losses_wh_per_hour[hour_idx] += verluste_eauto
|
||||||
|
|
||||||
# Save battery SOC before inverter processing = true begin-of-interval state.
|
# Save battery SOC before inverter processing = true begin-of-interval state.
|
||||||
@@ -663,6 +676,40 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _battery_state_layout(self) -> BatteryStateLayout:
|
||||||
|
"""Build optional state indices without renumbering legacy warm starts.
|
||||||
|
|
||||||
|
The pre-existing order is retained exactly: base charge/discharge ranges,
|
||||||
|
two optional DC states, then optional grid export. SELF_CONSUMPTION is
|
||||||
|
appended last so an old export gene never changes its meaning.
|
||||||
|
"""
|
||||||
|
next_state = 3 * len(self.bat_possible_charge_values)
|
||||||
|
dc_not_allowed_state: Optional[int] = None
|
||||||
|
dc_allowed_state: Optional[int] = None
|
||||||
|
grid_export_state: Optional[int] = None
|
||||||
|
self_consumption_state: Optional[int] = None
|
||||||
|
|
||||||
|
if self.optimize_dc_charge:
|
||||||
|
dc_not_allowed_state = next_state
|
||||||
|
dc_allowed_state = next_state + 1
|
||||||
|
next_state += 2
|
||||||
|
|
||||||
|
if self.optimize_battery_grid_export:
|
||||||
|
grid_export_state = next_state
|
||||||
|
next_state += 1
|
||||||
|
|
||||||
|
if self.optimize_dc_charge:
|
||||||
|
self_consumption_state = next_state
|
||||||
|
next_state += 1
|
||||||
|
|
||||||
|
return BatteryStateLayout(
|
||||||
|
total_states=next_state,
|
||||||
|
dc_not_allowed_state=dc_not_allowed_state,
|
||||||
|
dc_allowed_state=dc_allowed_state,
|
||||||
|
grid_export_state=grid_export_state,
|
||||||
|
self_consumption_state=self_consumption_state,
|
||||||
|
)
|
||||||
|
|
||||||
def _appliance_horizon_end_slot(self) -> int:
|
def _appliance_horizon_end_slot(self) -> int:
|
||||||
"""Exclusive upper slot bound for appliance runs (end of horizon).
|
"""Exclusive upper slot bound for appliance runs (end of horizon).
|
||||||
|
|
||||||
@@ -966,9 +1013,8 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
# AC Charge: 2*len_bat .. 3*len_bat - 1 (maps to bat_possible_charge_values)
|
# AC Charge: 2*len_bat .. 3*len_bat - 1 (maps to bat_possible_charge_values)
|
||||||
# DC optional: 3*len_bat (not allowed), 3*len_bat + 1 (allowed)
|
# DC optional: 3*len_bat (not allowed), 3*len_bat + 1 (allowed)
|
||||||
# Grid export: next state, if direct marketing/export optimization is enabled
|
# Grid export: next state, if direct marketing/export optimization is enabled
|
||||||
|
# Self-consumption: final state, with DC charging and local discharge enabled
|
||||||
# Idle states
|
state_layout = self._battery_state_layout()
|
||||||
idle_mask = (discharge_hours_bin_np >= 0) & (discharge_hours_bin_np < len_bat)
|
|
||||||
|
|
||||||
# Discharge states
|
# Discharge states
|
||||||
discharge_mask = (discharge_hours_bin_np >= len_bat) & (
|
discharge_mask = (discharge_hours_bin_np >= len_bat) & (
|
||||||
@@ -980,24 +1026,28 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
ac_indices = (discharge_hours_bin_np[ac_mask] - 2 * len_bat).astype(int)
|
ac_indices = (discharge_hours_bin_np[ac_mask] - 2 * len_bat).astype(int)
|
||||||
|
|
||||||
# DC states (if enabled)
|
# DC states (if enabled)
|
||||||
if self.optimize_dc_charge:
|
if state_layout.dc_allowed_state is not None:
|
||||||
dc_not_allowed_state = 3 * len_bat
|
dc_mask = discharge_hours_bin_np == state_layout.dc_allowed_state
|
||||||
dc_allowed_state = 3 * len_bat + 1
|
if state_layout.self_consumption_state is not None:
|
||||||
dc_charge = np.where(discharge_hours_bin_np == dc_allowed_state, 1, 0)
|
dc_mask |= discharge_hours_bin_np == state_layout.self_consumption_state
|
||||||
|
dc_charge = np.where(dc_mask, 1, 0)
|
||||||
else:
|
else:
|
||||||
dc_charge = np.ones_like(discharge_hours_bin_np, dtype=float)
|
dc_charge = np.ones_like(discharge_hours_bin_np, dtype=float)
|
||||||
|
|
||||||
# Generate the result arrays
|
# Generate the result arrays
|
||||||
discharge = np.zeros_like(discharge_hours_bin_np, dtype=int)
|
discharge = np.zeros_like(discharge_hours_bin_np, dtype=int)
|
||||||
discharge[discharge_mask] = 1 # Set Discharge states to 1
|
discharge[discharge_mask] = 1 # Set Discharge states to 1
|
||||||
|
if state_layout.self_consumption_state is not None:
|
||||||
|
discharge[discharge_hours_bin_np == state_layout.self_consumption_state] = 1
|
||||||
|
|
||||||
ac_charge = np.zeros_like(discharge_hours_bin_np, dtype=float)
|
ac_charge = np.zeros_like(discharge_hours_bin_np, dtype=float)
|
||||||
ac_charge[ac_mask] = [self.bat_possible_charge_values[i] for i in ac_indices]
|
ac_charge[ac_mask] = [self.bat_possible_charge_values[i] for i in ac_indices]
|
||||||
|
|
||||||
battery_grid_export = np.zeros_like(discharge_hours_bin_np, dtype=int)
|
battery_grid_export = np.zeros_like(discharge_hours_bin_np, dtype=int)
|
||||||
if self.optimize_battery_grid_export:
|
if state_layout.grid_export_state is not None:
|
||||||
grid_export_state = 3 * len_bat + (2 if self.optimize_dc_charge else 0)
|
battery_grid_export = np.where(
|
||||||
battery_grid_export = np.where(discharge_hours_bin_np == grid_export_state, 1, 0)
|
discharge_hours_bin_np == state_layout.grid_export_state, 1, 0
|
||||||
|
)
|
||||||
|
|
||||||
# Idle is just 0, already default.
|
# Idle is just 0, already default.
|
||||||
|
|
||||||
@@ -1005,14 +1055,7 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
|
|
||||||
def mutate(self, individual: list[int]) -> tuple[list[int]]:
|
def mutate(self, individual: list[int]) -> tuple[list[int]]:
|
||||||
"""Custom mutation function for the individual."""
|
"""Custom mutation function for the individual."""
|
||||||
# Calculate the number of states using battery charge levels
|
total_states = self._battery_state_layout().total_states
|
||||||
len_bat = len(self.bat_possible_charge_values)
|
|
||||||
if self.optimize_dc_charge:
|
|
||||||
total_states = 3 * len_bat + 2
|
|
||||||
else:
|
|
||||||
total_states = 3 * len_bat
|
|
||||||
if self.optimize_battery_grid_export:
|
|
||||||
total_states += 1
|
|
||||||
|
|
||||||
# 1. Mutating the charge_discharge part
|
# 1. Mutating the charge_discharge part
|
||||||
charge_discharge_part = individual[: self.total_slots]
|
charge_discharge_part = individual[: self.total_slots]
|
||||||
@@ -1146,14 +1189,15 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
must re-simulate after a change so the individual's genome, simulation
|
must re-simulate after a change so the individual's genome, simulation
|
||||||
state and assigned fitness always describe the same schedule.
|
state and assigned fitness always describe the same schedule.
|
||||||
"""
|
"""
|
||||||
if not self.optimize_ev or not self.ev_possible_charge_values:
|
ev_possible_charge_values = getattr(self, "ev_possible_charge_values", None)
|
||||||
|
if not self.optimize_ev or not ev_possible_charge_values:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
zero_charge_index = min(
|
zero_charge_index = min(
|
||||||
range(len(self.ev_possible_charge_values)),
|
range(len(ev_possible_charge_values)),
|
||||||
key=lambda index: abs(self.ev_possible_charge_values[index]),
|
key=lambda index: abs(ev_possible_charge_values[index]),
|
||||||
)
|
)
|
||||||
if abs(self.ev_possible_charge_values[zero_charge_index]) > 1e-12:
|
if abs(ev_possible_charge_values[zero_charge_index]) > 1e-12:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_, ev_charge_indices, _ = self.split_individual(individual)
|
_, ev_charge_indices, _ = self.split_individual(individual)
|
||||||
@@ -1172,7 +1216,7 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
charge_index = int(ev_charge_indices[slot])
|
charge_index = int(ev_charge_indices[slot])
|
||||||
if (
|
if (
|
||||||
ev_soc[offset] >= 100.0 - 1e-9
|
ev_soc[offset] >= 100.0 - 1e-9
|
||||||
and self.ev_possible_charge_values[charge_index] > 0.0
|
and ev_possible_charge_values[charge_index] > 0.0
|
||||||
):
|
):
|
||||||
ev_charge_indices[slot] = zero_charge_index
|
ev_charge_indices[slot] = zero_charge_index
|
||||||
changed = True
|
changed = True
|
||||||
@@ -1279,11 +1323,13 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
slots = self.total_slots
|
slots = self.total_slots
|
||||||
start_slot = self._start_day_slot()
|
start_slot = self._start_day_slot()
|
||||||
len_bat = len(self.bat_possible_charge_values)
|
len_bat = len(self.bat_possible_charge_values)
|
||||||
|
state_layout = self._battery_state_layout()
|
||||||
idle_state = 0
|
idle_state = 0
|
||||||
discharge_state = len_bat
|
discharge_state = len_bat
|
||||||
ac_charge_state = 3 * len_bat - 1
|
ac_charge_state = 3 * len_bat - 1
|
||||||
dc_allowed_state = 3 * len_bat + 1
|
dc_allowed_state = state_layout.dc_allowed_state
|
||||||
export_state = 3 * len_bat + (2 if self.optimize_dc_charge else 0)
|
export_state = state_layout.grid_export_state
|
||||||
|
self_consumption_state = state_layout.self_consumption_state
|
||||||
|
|
||||||
prices = np.asarray(self.simulation.elect_price_hourly, dtype=float)
|
prices = np.asarray(self.simulation.elect_price_hourly, dtype=float)
|
||||||
feed_in = np.asarray(self.simulation.elect_revenue_per_hour_arr, dtype=float)
|
feed_in = np.asarray(self.simulation.elect_revenue_per_hour_arr, dtype=float)
|
||||||
@@ -1333,15 +1379,20 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
for slot in range(start_slot, slots):
|
for slot in range(start_slot, slots):
|
||||||
high_feed_in = (
|
high_feed_in = (
|
||||||
export_quantile is not None
|
export_quantile is not None
|
||||||
and self.optimize_battery_grid_export
|
and export_state is not None
|
||||||
and feed_spread > 1e-12
|
and feed_spread > 1e-12
|
||||||
and feed_in[slot] > 0.0
|
and feed_in[slot] > 0.0
|
||||||
and feed_in[slot] >= export_threshold
|
and feed_in[slot] >= export_threshold
|
||||||
)
|
)
|
||||||
pv_surplus = pv[slot] > load[slot] * pv_surplus_ratio
|
pv_surplus = pv[slot] > load[slot] * pv_surplus_ratio
|
||||||
if high_feed_in:
|
if high_feed_in and export_state is not None:
|
||||||
battery_genes[slot] = export_state
|
battery_genes[slot] = export_state
|
||||||
elif self.optimize_dc_charge and pv_surplus:
|
elif self_consumption_state is not None and pv[slot] > 0.0 and load[slot] > 0.0:
|
||||||
|
# The probabilistic inverter model can see a residual load
|
||||||
|
# and a PV surplus within the same coarse slot. Normal
|
||||||
|
# self-consumption must therefore allow both directions.
|
||||||
|
battery_genes[slot] = self_consumption_state
|
||||||
|
elif dc_allowed_state is not None and pv_surplus:
|
||||||
battery_genes[slot] = dc_allowed_state
|
battery_genes[slot] = dc_allowed_state
|
||||||
elif allow_ac_arbitrage and prices[slot] <= low_price_threshold:
|
elif allow_ac_arbitrage and prices[slot] <= low_price_threshold:
|
||||||
battery_genes[slot] = ac_charge_state
|
battery_genes[slot] = ac_charge_state
|
||||||
@@ -1415,7 +1466,9 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
for slot in random.sample(future_slots, min(perturbations, len(future_slots))): # noqa: S311
|
for slot in random.sample(future_slots, min(perturbations, len(future_slots))): # noqa: S311
|
||||||
if randomized[slot] != idle_state:
|
if randomized[slot] != idle_state:
|
||||||
randomized[slot] = idle_state
|
randomized[slot] = idle_state
|
||||||
elif self.optimize_dc_charge and pv[slot] > load[slot]:
|
elif self_consumption_state is not None and pv[slot] > 0.0 and load[slot] > 0.0:
|
||||||
|
randomized[slot] = self_consumption_state
|
||||||
|
elif dc_allowed_state is not None and pv[slot] > load[slot]:
|
||||||
randomized[slot] = dc_allowed_state
|
randomized[slot] = dc_allowed_state
|
||||||
elif load[slot] > pv[slot] and prices[slot] >= high_import_price:
|
elif load[slot] > pv[slot] and prices[slot] >= high_import_price:
|
||||||
randomized[slot] = discharge_state
|
randomized[slot] = discharge_state
|
||||||
@@ -1480,12 +1533,8 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
# AC-Charge: len_bat states (maps to bat_possible_charge_values)
|
# AC-Charge: len_bat states (maps to bat_possible_charge_values)
|
||||||
# With DC: + 2 additional states
|
# With DC: + 2 additional states
|
||||||
# With battery grid export: + 1 additional state
|
# With battery grid export: + 1 additional state
|
||||||
if self.optimize_dc_charge:
|
# With DC: + 1 final SELF_CONSUMPTION state
|
||||||
total_states = 3 * len_bat + 2
|
total_states = self._battery_state_layout().total_states
|
||||||
else:
|
|
||||||
total_states = 3 * len_bat
|
|
||||||
if self.optimize_battery_grid_export:
|
|
||||||
total_states += 1
|
|
||||||
|
|
||||||
# State space: 0 .. (total_states - 1)
|
# State space: 0 .. (total_states - 1)
|
||||||
self.toolbox.register("attr_discharge_state", random.randint, 0, total_states - 1)
|
self.toolbox.register("attr_discharge_state", random.randint, 0, total_states - 1)
|
||||||
@@ -1579,7 +1628,10 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
worst_case: bool,
|
worst_case: bool,
|
||||||
) -> tuple[float]:
|
) -> tuple[float]:
|
||||||
"""Evaluate an individual, using run-local canonical memoization when active."""
|
"""Evaluate an individual, using run-local canonical memoization when active."""
|
||||||
if not self._fitness_cache_enabled:
|
# Some lightweight callers construct the optimizer without __init__
|
||||||
|
# (for example isolated penalty evaluations). Memoization is opt-in, so
|
||||||
|
# a missing flag must behave exactly like a disabled cache.
|
||||||
|
if not getattr(self, "_fitness_cache_enabled", False):
|
||||||
return self._evaluate_uncached(individual, parameters, start_hour, worst_case)
|
return self._evaluate_uncached(individual, parameters, start_hour, worst_case)
|
||||||
|
|
||||||
original_key = tuple(int(value) for value in individual)
|
original_key = tuple(int(value) for value in individual)
|
||||||
|
|||||||
@@ -187,9 +187,11 @@ def test_educated_guesses_encode_high_price_direct_marketing(config_eos: ConfigE
|
|||||||
|
|
||||||
dc_allowed_state = 4
|
dc_allowed_state = 4
|
||||||
export_state = 5
|
export_state = 5
|
||||||
|
self_consumption_state = 6
|
||||||
assert len(guesses) == opt.EDUCATED_GUESS_TARGET
|
assert len(guesses) == opt.EDUCATED_GUESS_TARGET
|
||||||
assert all(len(guess) == slots for guess in guesses)
|
assert all(len(guess) == slots for guess in guesses)
|
||||||
assert any(guess[0] == dc_allowed_state for guess in guesses)
|
assert any(dc_allowed_state in guess or self_consumption_state in guess for guess in guesses)
|
||||||
|
assert any(self_consumption_state in guess for guess in guesses)
|
||||||
assert any(guess[-1] == export_state for guess in guesses)
|
assert any(guess[-1] == export_state for guess in guesses)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ def test_simulation(genetic_simulation):
|
|||||||
|
|
||||||
# Verify the total balance
|
# Verify the total balance
|
||||||
assert (
|
assert (
|
||||||
abs(result["Gesamtbilanz_Euro"] - 7.025236588371921) < 1e-5
|
abs(result["Gesamtbilanz_Euro"] - 7.224316588371922) < 1e-5
|
||||||
), "Total balance should reflect the shared per-slot battery power limit."
|
), "Total balance should reflect the shared per-slot battery power limit."
|
||||||
|
|
||||||
# Check total revenue and total costs
|
# Check total revenue and total costs
|
||||||
@@ -346,7 +346,7 @@ def test_simulation(genetic_simulation):
|
|||||||
abs(result["Gesamteinnahmen_Euro"] - 2.3247787887715) < 1e-5
|
abs(result["Gesamteinnahmen_Euro"] - 2.3247787887715) < 1e-5
|
||||||
), "Total revenue should respect the shared per-slot battery power limit."
|
), "Total revenue should respect the shared per-slot battery power limit."
|
||||||
assert (
|
assert (
|
||||||
abs(result["Gesamtkosten_Euro"] - 9.350015377143421) < 1e-5
|
abs(result["Gesamtkosten_Euro"] - 9.549095377143422) < 1e-5
|
||||||
), "Total costs should respect the shared per-slot battery power limit."
|
), "Total costs should respect the shared per-slot battery power limit."
|
||||||
|
|
||||||
# Check the losses
|
# Check the losses
|
||||||
@@ -379,6 +379,47 @@ def test_simulation(genetic_simulation):
|
|||||||
print("All tests passed successfully.")
|
print("All tests passed successfully.")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ev_charging_uses_raw_input_energy_for_load_and_grid(config_eos):
|
||||||
|
config_eos.merge_settings_from_dict(
|
||||||
|
{"prediction": {"hours": 1}, "optimization": {"horizon_hours": 1}}
|
||||||
|
)
|
||||||
|
ev = Battery(
|
||||||
|
ElectricVehicleParameters(
|
||||||
|
device_id="ev1",
|
||||||
|
capacity_wh=1000,
|
||||||
|
charging_efficiency=0.8,
|
||||||
|
max_charge_power_w=100,
|
||||||
|
initial_soc_percentage=0,
|
||||||
|
min_soc_percentage=0,
|
||||||
|
),
|
||||||
|
prediction_hours=1,
|
||||||
|
)
|
||||||
|
inverter = Inverter(InverterParameters(device_id="inverter1", max_power_wh=1000.0))
|
||||||
|
simulation = GeneticSimulation()
|
||||||
|
simulation.prepare(
|
||||||
|
GeneticEnergyManagementParameters(
|
||||||
|
pv_prognose_wh=[0.0],
|
||||||
|
strompreis_euro_pro_wh=[0.001],
|
||||||
|
einspeiseverguetung_euro_pro_wh=[0.0],
|
||||||
|
preis_euro_pro_wh_akku=0.0,
|
||||||
|
gesamtlast=[0.0],
|
||||||
|
),
|
||||||
|
optimization_hours=1,
|
||||||
|
prediction_hours=1,
|
||||||
|
inverter=inverter,
|
||||||
|
ev=ev,
|
||||||
|
)
|
||||||
|
simulation.ev_charge_hours = np.array([1.0])
|
||||||
|
|
||||||
|
result = simulation.simulate(start_hour=0)
|
||||||
|
|
||||||
|
assert result["Last_Wh_pro_Stunde"][0] == pytest.approx(100.0)
|
||||||
|
assert result["Netzbezug_Wh_pro_Stunde"][0] == pytest.approx(100.0)
|
||||||
|
assert result["Kosten_Euro_pro_Stunde"][0] == pytest.approx(0.1)
|
||||||
|
assert result["Verluste_Pro_Stunde"][0] == pytest.approx(20.0)
|
||||||
|
assert ev.current_soc_percentage() == pytest.approx(8.0)
|
||||||
|
|
||||||
|
|
||||||
def test_direct_marketing_curtails_negative_feed_in(config_eos, monkeypatch):
|
def test_direct_marketing_curtails_negative_feed_in(config_eos, monkeypatch):
|
||||||
config_eos.merge_settings_from_dict(
|
config_eos.merge_settings_from_dict(
|
||||||
{"prediction": {"hours": 2}, "optimization": {"horizon_hours": 2}}
|
{"prediction": {"hours": 2}, "optimization": {"horizon_hours": 2}}
|
||||||
|
|||||||
@@ -54,3 +54,23 @@ def test_decode_charge_discharge_has_separate_battery_grid_export_state():
|
|||||||
assert dc_charge.tolist() == [0]
|
assert dc_charge.tolist() == [0]
|
||||||
assert discharge.tolist() == [0]
|
assert discharge.tolist() == [0]
|
||||||
assert battery_grid_export.tolist() == [1]
|
assert battery_grid_export.tolist() == [1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_charge_discharge_has_self_consumption_state_after_legacy_export():
|
||||||
|
optimization = GeneticOptimization()
|
||||||
|
optimization.bat_possible_charge_values = [1.0]
|
||||||
|
optimization.optimize_dc_charge = True
|
||||||
|
optimization.optimize_battery_grid_export = True
|
||||||
|
|
||||||
|
layout = optimization._battery_state_layout()
|
||||||
|
ac_charge, dc_charge, discharge, battery_grid_export = (
|
||||||
|
optimization.decode_charge_discharge(np.array([6]))
|
||||||
|
)
|
||||||
|
|
||||||
|
assert layout.total_states == 7
|
||||||
|
assert layout.grid_export_state == 5
|
||||||
|
assert layout.self_consumption_state == 6
|
||||||
|
assert ac_charge.tolist() == [0.0]
|
||||||
|
assert dc_charge.tolist() == [1]
|
||||||
|
assert discharge.tolist() == [1]
|
||||||
|
assert battery_grid_export.tolist() == [0]
|
||||||
|
|||||||
BIN
Binary file not shown.
+71
-71
@@ -202,15 +202,15 @@
|
|||||||
],
|
],
|
||||||
"result": {
|
"result": {
|
||||||
"Last_Wh_pro_Stunde": [
|
"Last_Wh_pro_Stunde": [
|
||||||
10230.07,
|
10713.07,
|
||||||
7618.91,
|
7963.91,
|
||||||
7875.5599999999995,
|
8220.56,
|
||||||
7565.03,
|
7772.03,
|
||||||
12840.67,
|
13323.67,
|
||||||
9042.82,
|
9456.82,
|
||||||
9082.22,
|
9496.22,
|
||||||
5036.78,
|
5243.78,
|
||||||
2177.92,
|
2233.12,
|
||||||
1178.71,
|
1178.71,
|
||||||
1050.98,
|
1050.98,
|
||||||
988.56,
|
988.56,
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
0.05936927575872234,
|
0.053661230306193436,
|
||||||
0.05435777788576338,
|
0.05435777788576338,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -322,9 +322,9 @@
|
|||||||
0.0
|
0.0
|
||||||
],
|
],
|
||||||
"Gesamt_Verluste": 6227.580163897914,
|
"Gesamt_Verluste": 6227.580163897914,
|
||||||
"Gesamtbilanz_Euro": 11.420868526053463,
|
"Gesamtbilanz_Euro": 12.033027623527284,
|
||||||
"Gesamteinnahmen_Euro": 0.11372705364448572,
|
"Gesamteinnahmen_Euro": 0.10801900819195681,
|
||||||
"Gesamtkosten_Euro": 11.534595579697948,
|
"Gesamtkosten_Euro": 12.14104663171924,
|
||||||
"Home_appliance_wh_per_hour": [
|
"Home_appliance_wh_per_hour": [
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -408,15 +408,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Kosten_Euro_pro_Stunde": [
|
"Kosten_Euro_pro_Stunde": [
|
||||||
2.1203521199999997,
|
2.23047612,
|
||||||
1.4545721927072854,
|
1.5308798733801507,
|
||||||
1.4167558060312964,
|
1.4889564723943407,
|
||||||
1.2032659414129376,
|
1.242149738556099,
|
||||||
1.2892826874611185,
|
1.3742255912165289,
|
||||||
0.770122611209644,
|
0.8479283355019763,
|
||||||
1.1459236583154115,
|
1.2336553365360492,
|
||||||
0.4965507655670841,
|
0.5407163922683618,
|
||||||
0.1912938683161112,
|
0.20558284318867343,
|
||||||
0.1614775630079859,
|
0.1614775630079859,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -448,15 +448,15 @@
|
|||||||
0.16484566
|
0.16484566
|
||||||
],
|
],
|
||||||
"Netzbezug_Wh_pro_Stunde": [
|
"Netzbezug_Wh_pro_Stunde": [
|
||||||
9299.789999999999,
|
9782.789999999999,
|
||||||
6575.823656000386,
|
6920.795087613701,
|
||||||
6769.01961792306,
|
7113.9821901306295,
|
||||||
6403.757005923032,
|
6610.695787951565,
|
||||||
7014.595688036554,
|
7476.74423948057,
|
||||||
3842.9272016449304,
|
4231.179318872138,
|
||||||
5213.483431826258,
|
5612.626644840988,
|
||||||
2187.4483064629258,
|
2382.0105386271443,
|
||||||
638.2845122326032,
|
685.962106068313,
|
||||||
505.40708296709204,
|
505.40708296709204,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -519,7 +519,7 @@
|
|||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
848.1325108388907,
|
766.589004374192,
|
||||||
776.5396840823341,
|
776.5396840823341,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -529,14 +529,14 @@
|
|||||||
],
|
],
|
||||||
"Verluste_Pro_Stunde": [
|
"Verluste_Pro_Stunde": [
|
||||||
483.0,
|
483.0,
|
||||||
345.0196387200463,
|
345.0162105136441,
|
||||||
345.0239541507672,
|
345.0194628156755,
|
||||||
207.05004071076388,
|
207.04269455418785,
|
||||||
506.1294825643864,
|
503.6273087376684,
|
||||||
452.3012641973916,
|
449.2115182646565,
|
||||||
426.13721181915116,
|
424.3543973809186,
|
||||||
227.23539677555112,
|
225.74286463525735,
|
||||||
103.61214146791241,
|
102.70945272819762,
|
||||||
40.06404995605101,
|
40.06404995605101,
|
||||||
106.80230977350088,
|
106.80230977350088,
|
||||||
133.7321802766326,
|
133.7321802766326,
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
538.2984000000001,
|
538.2984000000001,
|
||||||
441.9379674303641,
|
441.9379674303641,
|
||||||
261.1952696860876,
|
261.1952696860876,
|
||||||
75.0748095419566,
|
84.86003031772043,
|
||||||
0.0,
|
0.0,
|
||||||
111.78035844493081,
|
111.78035844493081,
|
||||||
90.18403329253945,
|
90.18403329253945,
|
||||||
@@ -570,36 +570,36 @@
|
|||||||
"akku_soc_pro_stunde": [
|
"akku_soc_pro_stunde": [
|
||||||
80.0,
|
80.0,
|
||||||
80.0,
|
80.0,
|
||||||
80.00054552000128,
|
80.00045029204567,
|
||||||
80.00121091307815,
|
80.00099092581443,
|
||||||
80.0026009328216,
|
80.00217688565297,
|
||||||
80.6450865596101,
|
80.57515768392155,
|
||||||
81.70901056509321,
|
81.55325541349534,
|
||||||
82.04615533784741,
|
81.8408775629653,
|
||||||
82.60824969272383,
|
82.36151269172245,
|
||||||
83.95303140016584,
|
83.68121971195016,
|
||||||
85.06592167672281,
|
84.79410998850713,
|
||||||
81.89125383667168,
|
81.619442148456,
|
||||||
77.66999557804807,
|
77.39818388983241,
|
||||||
77.66999557804807,
|
77.39818388983241,
|
||||||
77.66999557804807,
|
77.39818388983241,
|
||||||
75.44732856702878,
|
75.17551687881311,
|
||||||
71.71088745132629,
|
71.43907576311062,
|
||||||
68.72216499953565,
|
68.45035331131999,
|
||||||
66.10168462763482,
|
65.82987293941916,
|
||||||
63.7070995725384,
|
63.43528788432273,
|
||||||
61.60271768548606,
|
61.33090599727039,
|
||||||
59.42077037143647,
|
59.14895868322081,
|
||||||
55.97788013048819,
|
55.706068442272525,
|
||||||
52.48021001194556,
|
52.208398323729895,
|
||||||
53.09999027665313,
|
52.82817858843747,
|
||||||
54.60520137546928,
|
54.33338968725362,
|
||||||
57.01140766586834,
|
56.73959597765268,
|
||||||
61.59918181733974,
|
61.327370129124084,
|
||||||
63.43037648171088,
|
63.158564793495216,
|
||||||
78.38310981504422,
|
78.11129812682856,
|
||||||
90.65916446588767,
|
90.387352777672,
|
||||||
97.91458862383455,
|
97.64277693561888,
|
||||||
100.0,
|
100.0,
|
||||||
100.0,
|
100.0,
|
||||||
98.60068046043587,
|
98.60068046043587,
|
||||||
|
|||||||
+70
-70
@@ -202,14 +202,14 @@
|
|||||||
],
|
],
|
||||||
"result": {
|
"result": {
|
||||||
"Last_Wh_pro_Stunde": [
|
"Last_Wh_pro_Stunde": [
|
||||||
11541.07,
|
12093.07,
|
||||||
6307.91,
|
6583.91,
|
||||||
9186.56,
|
9600.56,
|
||||||
10309.03,
|
10792.03,
|
||||||
6407.67,
|
6683.67,
|
||||||
5109.82,
|
5316.82,
|
||||||
11704.22,
|
12256.22,
|
||||||
5036.78,
|
5243.78,
|
||||||
1129.12,
|
1129.12,
|
||||||
1178.71,
|
1178.71,
|
||||||
1050.98,
|
1050.98,
|
||||||
@@ -321,10 +321,10 @@
|
|||||||
0.0,
|
0.0,
|
||||||
0.0
|
0.0
|
||||||
],
|
],
|
||||||
"Gesamt_Verluste": 7430.87215820259,
|
"Gesamt_Verluste": 7415.050669156861,
|
||||||
"Gesamtbilanz_Euro": 9.4881560601163,
|
"Gesamtbilanz_Euro": 10.086958235191952,
|
||||||
"Gesamteinnahmen_Euro": 0.0,
|
"Gesamteinnahmen_Euro": 0.0,
|
||||||
"Gesamtkosten_Euro": 9.4881560601163,
|
"Gesamtkosten_Euro": 10.086958235191952,
|
||||||
"Home_appliance_wh_per_hour": [
|
"Home_appliance_wh_per_hour": [
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -408,14 +408,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Kosten_Euro_pro_Stunde": [
|
"Kosten_Euro_pro_Stunde": [
|
||||||
1.4160601199999998,
|
1.5419161199999998,
|
||||||
0.19137741085396395,
|
0.2524105556335792,
|
||||||
1.691123530177008,
|
1.7777665549410084,
|
||||||
1.7187910298948639,
|
1.809540886,
|
||||||
0.20791005863613377,
|
0.24971825220475874,
|
||||||
0.09234842570092357,
|
0.12061259291950001,
|
||||||
1.709869129414328,
|
1.83015129135275,
|
||||||
0.4965507655670841,
|
0.5407163922683618,
|
||||||
0.05258762370598476,
|
0.05258762370598476,
|
||||||
0.1614775630079859,
|
0.1614775630079859,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -448,14 +448,14 @@
|
|||||||
0.16484566
|
0.16484566
|
||||||
],
|
],
|
||||||
"Netzbezug_Wh_pro_Stunde": [
|
"Netzbezug_Wh_pro_Stunde": [
|
||||||
6210.789999999999,
|
6762.789999999999,
|
||||||
865.1781684175585,
|
1141.0965444556023,
|
||||||
8079.902198647912,
|
8493.867916583891,
|
||||||
9147.371101090283,
|
9630.34,
|
||||||
1131.1755094457767,
|
1358.64119806724,
|
||||||
460.8204875295587,
|
601.8592461052895,
|
||||||
7779.204410438253,
|
8326.43899614536,
|
||||||
2187.4483064629258,
|
2382.0105386271443,
|
||||||
175.4675465665157,
|
175.4675465665157,
|
||||||
505.40708296709204,
|
505.40708296709204,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -529,13 +529,13 @@
|
|||||||
],
|
],
|
||||||
"Verluste_Pro_Stunde": [
|
"Verluste_Pro_Stunde": [
|
||||||
1152.0,
|
1152.0,
|
||||||
876.0621802101069,
|
876.0523853346722,
|
||||||
414.00986383774955,
|
414.00574999006693,
|
||||||
483.00373213083384,
|
483.0,
|
||||||
365.07906113349316,
|
359.25494376806876,
|
||||||
311.4084585035471,
|
303.4931095326348,
|
||||||
557.3837292525905,
|
556.8118795374434,
|
||||||
227.23539677555112,
|
225.74286463525735,
|
||||||
118.73010558798194,
|
118.73010558798194,
|
||||||
40.06404995605101,
|
40.06404995605101,
|
||||||
106.80230977350088,
|
106.80230977350088,
|
||||||
@@ -570,42 +570,42 @@
|
|||||||
"akku_soc_pro_stunde": [
|
"akku_soc_pro_stunde": [
|
||||||
80.0,
|
80.0,
|
||||||
61.06060606060606,
|
61.06060606060606,
|
||||||
42.12293934927065,
|
42.12266726939745,
|
||||||
42.12321334476369,
|
42.122826991343764,
|
||||||
42.123317015064636,
|
42.122826991343764,
|
||||||
44.59773537988389,
|
44.435464318234565,
|
||||||
47.49797033831576,
|
47.11582847191886,
|
||||||
47.64751837310994,
|
47.249491792403404,
|
||||||
48.209612727986354,
|
47.770126921160546,
|
||||||
51.507671216541404,
|
51.0681854097156,
|
||||||
52.620561493098386,
|
52.181075686272585,
|
||||||
49.44589365304724,
|
49.006407846221435,
|
||||||
45.224635394423636,
|
44.78514958759783,
|
||||||
45.224635394423636,
|
44.78514958759783,
|
||||||
45.224635394423636,
|
44.78514958759783,
|
||||||
43.00196838340435,
|
42.562482576578546,
|
||||||
39.26552726770188,
|
38.82604146087607,
|
||||||
36.27680481591124,
|
35.837319009085434,
|
||||||
33.656324444010416,
|
33.2168386371846,
|
||||||
31.261739388913995,
|
30.822253582088187,
|
||||||
29.157357501861657,
|
28.717871695035846,
|
||||||
26.97541018781207,
|
26.53592438098626,
|
||||||
23.53251994686378,
|
23.09303414003797,
|
||||||
20.034849828321157,
|
19.595364021495346,
|
||||||
20.654630093028718,
|
20.215144286202914,
|
||||||
22.159841191844876,
|
21.72035538501907,
|
||||||
24.566047482243945,
|
24.126561675418138,
|
||||||
29.15382163371534,
|
28.71433582688953,
|
||||||
29.229581775454538,
|
28.79009596862873,
|
||||||
35.84898177545453,
|
35.40949596862873,
|
||||||
48.12503642629798,
|
47.68555061947217,
|
||||||
55.38046058424485,
|
54.94097477741905,
|
||||||
60.29298032987328,
|
59.85349452304748,
|
||||||
61.68112016833327,
|
61.241634361507465,
|
||||||
62.6777205736456,
|
62.2382347668198,
|
||||||
60.18956135662841,
|
59.7500755498026,
|
||||||
55.94106789932813,
|
55.50158209250233,
|
||||||
55.94106789932813
|
55.50158209250233
|
||||||
],
|
],
|
||||||
"Electricity_price": [
|
"Electricity_price": [
|
||||||
0.000228,
|
0.000228,
|
||||||
|
|||||||
Reference in New Issue
Block a user