mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-07-12 21:08:13 +00:00
fix(genetic): limit EV charging to unmet min SoC
This commit is contained in:
@@ -983,10 +983,7 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
"Penalty function parameter `ev_soc_miss` not configured, using {}.", penalty
|
"Penalty function parameter `ev_soc_miss` not configured, using {}.", penalty
|
||||||
)
|
)
|
||||||
ev_soc_percentage = self.simulation.ev.current_soc_percentage()
|
ev_soc_percentage = self.simulation.ev.current_soc_percentage()
|
||||||
if (
|
if ev_soc_percentage < parameters.eauto.min_soc_percentage:
|
||||||
ev_soc_percentage < parameters.eauto.min_soc_percentage
|
|
||||||
or ev_soc_percentage > parameters.eauto.max_soc_percentage
|
|
||||||
):
|
|
||||||
gesamtbilanz += (
|
gesamtbilanz += (
|
||||||
abs(parameters.eauto.min_soc_percentage - ev_soc_percentage) * penalty
|
abs(parameters.eauto.min_soc_percentage - ev_soc_percentage) * penalty
|
||||||
)
|
)
|
||||||
@@ -1020,10 +1017,25 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
|
|
||||||
logger.debug("Start optimize: {}", start_solution)
|
logger.debug("Start optimize: {}", start_solution)
|
||||||
|
|
||||||
# Insert the start solution into the population if provided
|
# Insert the start solution into the population if provided and compatible with the
|
||||||
|
# currently active genome layout. EV optimization adds one gene per prediction hour,
|
||||||
|
# so a cached solution from a previous run without EV optimization must not be reused.
|
||||||
if start_solution is not None:
|
if start_solution is not None:
|
||||||
for _ in range(10):
|
expected_length = self.config.prediction.hours
|
||||||
population.insert(0, creator.Individual(start_solution))
|
if self.optimize_ev:
|
||||||
|
expected_length += self.config.prediction.hours
|
||||||
|
if self.opti_param.get("home_appliance", 0) > 0:
|
||||||
|
expected_length += 1
|
||||||
|
|
||||||
|
if len(start_solution) == expected_length:
|
||||||
|
for _ in range(10):
|
||||||
|
population.insert(0, creator.Individual(start_solution))
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Ignoring start_solution with incompatible length {} (expected {}).",
|
||||||
|
len(start_solution),
|
||||||
|
expected_length,
|
||||||
|
)
|
||||||
|
|
||||||
# Run the evolutionary algorithm
|
# Run the evolutionary algorithm
|
||||||
pop, log = algorithms.eaMuPlusLambda(
|
pop, log = algorithms.eaMuPlusLambda(
|
||||||
@@ -1105,7 +1117,7 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
)
|
)
|
||||||
eauto.set_charge_per_hour(np.full(self.config.prediction.hours, 1))
|
eauto.set_charge_per_hour(np.full(self.config.prediction.hours, 1))
|
||||||
self.optimize_ev = (
|
self.optimize_ev = (
|
||||||
parameters.eauto.min_soc_percentage - parameters.eauto.initial_soc_percentage >= 0
|
parameters.eauto.min_soc_percentage > parameters.eauto.initial_soc_percentage
|
||||||
)
|
)
|
||||||
# electrical vehicle charge rates
|
# electrical vehicle charge rates
|
||||||
if parameters.eauto.charge_rates is not None:
|
if parameters.eauto.charge_rates is not None:
|
||||||
@@ -1208,11 +1220,9 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
if self.simulation.home_appliance:
|
if self.simulation.home_appliance:
|
||||||
washingstart_int = self.simulation.home_appliance_start_hour
|
washingstart_int = self.simulation.home_appliance_start_hour
|
||||||
|
|
||||||
eautocharge_hours_float = (
|
eautocharge_hours_float = None
|
||||||
[self.ev_possible_charge_values[i] for i in eautocharge_hours_index]
|
if eautocharge_hours_index is not None and self.simulation.ev is not None:
|
||||||
if eautocharge_hours_index is not None
|
eautocharge_hours_float = self.simulation.ev.charge_array.tolist()
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Simulation may have changed something, use simulation values
|
# Simulation may have changed something, use simulation values
|
||||||
ac_charge_hours = self.simulation.ac_charge_hours
|
ac_charge_hours = self.simulation.ac_charge_hours
|
||||||
|
|||||||
@@ -591,6 +591,73 @@ def _run_evaluate_with_mocked_sim(
|
|||||||
return fitness[0]
|
return fitness[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_evaluate_with_mocked_ev_soc(config_eos, ev_soc_percentage: float) -> float:
|
||||||
|
"""Return fitness for a mocked EV SoC while EV optimization is active."""
|
||||||
|
from akkudoktoreos.optimization.genetic.genetic import GeneticOptimization
|
||||||
|
|
||||||
|
config_eos.merge_settings_from_dict(
|
||||||
|
{
|
||||||
|
"prediction": {"hours": 48},
|
||||||
|
"optimization": {"hours": 48},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
config_eos.optimization.genetic.penalties = {
|
||||||
|
"ev_soc_miss": 10,
|
||||||
|
"ac_charge_break_even": 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
optim = GeneticOptimization.__new__(GeneticOptimization)
|
||||||
|
optim.config = config_eos
|
||||||
|
optim.optimize_ev = True
|
||||||
|
optim.verbose = False
|
||||||
|
optim.opti_param = {"home_appliance": 0}
|
||||||
|
|
||||||
|
mock_ev = Mock()
|
||||||
|
mock_ev.current_soc_percentage.return_value = ev_soc_percentage
|
||||||
|
|
||||||
|
sim = Mock()
|
||||||
|
sim.battery = None
|
||||||
|
sim.inverter = None
|
||||||
|
sim.ev = mock_ev
|
||||||
|
sim.ac_charge_hours = None
|
||||||
|
sim.elect_price_hourly = None
|
||||||
|
sim.load_energy_array = None
|
||||||
|
optim.simulation = sim
|
||||||
|
|
||||||
|
dummy_result = {
|
||||||
|
"Gesamtbilanz_Euro": 1.0,
|
||||||
|
"Gesamt_Verluste": 0.0,
|
||||||
|
"EAuto_SoC_pro_Stunde": np.zeros(48),
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Ind(list): # noqa: N801
|
||||||
|
pass
|
||||||
|
|
||||||
|
fake_individual = _Ind([0] * 96)
|
||||||
|
|
||||||
|
with patch.object(optim, "evaluate_inner", return_value=dummy_result):
|
||||||
|
fitness = optim.evaluate(
|
||||||
|
fake_individual,
|
||||||
|
parameters=Mock(
|
||||||
|
ems=Mock(preis_euro_pro_wh_akku=0.0),
|
||||||
|
eauto=Mock(min_soc_percentage=70, max_soc_percentage=100),
|
||||||
|
),
|
||||||
|
start_hour=0,
|
||||||
|
worst_case=False,
|
||||||
|
)
|
||||||
|
return fitness[0]
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvSocPenalty:
|
||||||
|
"""EV SoC target is a minimum-only penalty."""
|
||||||
|
|
||||||
|
def test_ev_soc_below_min_adds_penalty(self, config_eos):
|
||||||
|
assert _run_evaluate_with_mocked_ev_soc(config_eos, 65) == pytest.approx(51.0)
|
||||||
|
|
||||||
|
def test_ev_soc_above_max_does_not_add_min_soc_penalty(self, config_eos):
|
||||||
|
assert _run_evaluate_with_mocked_ev_soc(config_eos, 105) == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
class TestAcChargeBreakEvenPenalty:
|
class TestAcChargeBreakEvenPenalty:
|
||||||
"""Break-even penalty in GeneticOptimization.evaluate().
|
"""Break-even penalty in GeneticOptimization.evaluate().
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user