mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-08 23:46:38 +00:00
fix(optimization): clear disabled AC charge in the reported plan
With grid charging switched off (inverter.max_ac_charge_power_w = 0) the simulation zeroes the AC charge array before using it, but it did so by rebinding a local name. The solution is read back from the original array, so it still carried the optimizer's AC charge genes - genes the fitness never evaluated, because the simulation ignored them, and which are therefore arbitrary. The effect was visible as a slot with ac_charge = 0.8 where the battery SoC does not move. Harmless inside EOS, but a controller that follows the plan would grid-charge the battery at a time nobody planned or paid for. Zero the array in place so the reported plan matches what was simulated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
two applies. The `ev_soc_miss` penalty is then evaluated at that slot instead of at the end
|
||||
of the horizon, and the seeding heuristics only propose charge slots before it. Without a
|
||||
deadline the behaviour is unchanged.
|
||||
- Fix: with grid charging disabled (`inverter.max_ac_charge_power_w = 0`) the returned
|
||||
`ac_charge` array kept the optimizer's unused gene values. The simulation ignored them, so
|
||||
they were never costed - but a controller acting on the plan would grid-charge the battery
|
||||
anyway. The disabled AC charge is now cleared in the reported plan as well.
|
||||
- EV Bug (wrong output in genetic.py / no senseful results)
|
||||
- Direktvermarktung active / Battery discharge into grid (new state / action battery_grid_export_allowed) + (new simulation output Feed_in_tariff)
|
||||
- New PV forecast providers giving operators more cloud forecast sources to choose from in
|
||||
|
||||
@@ -350,9 +350,13 @@ class GeneticSimulation(PydanticBaseModel):
|
||||
max_ac_charge_w_fast is None or max_ac_charge_w_fast > 0
|
||||
)
|
||||
|
||||
# If AC charging is disabled via inverter, zero out AC charge hours
|
||||
# If AC charging is disabled via inverter, zero out AC charge hours.
|
||||
# In place, not by rebinding: the reported plan is read back from
|
||||
# this very array, so a rebind would leave AC charge values in the
|
||||
# solution that the simulation never executed - and a controller
|
||||
# acting on them would grid-charge the battery unplanned.
|
||||
if not ac_charging_possible:
|
||||
ac_charge_hours_fast = np.zeros_like(ac_charge_hours_fast)
|
||||
ac_charge_hours_fast[:] = 0.0
|
||||
|
||||
# Fill the charge array of the battery
|
||||
dc_charge_hours_fast[0:start_hour] = 0
|
||||
|
||||
@@ -560,3 +560,62 @@ def test_battery_lcos_is_charged_once_on_delivered_energy(config_eos):
|
||||
assert result["Gesamtkosten_Euro"] == pytest.approx(0.06)
|
||||
assert result["Einnahmen_Euro_pro_Stunde"][0] == pytest.approx(0.08)
|
||||
assert result["Gesamtbilanz_Euro"] == pytest.approx(-0.02)
|
||||
|
||||
|
||||
def test_disabled_ac_charging_clears_the_reported_plan(config_eos):
|
||||
"""With AC charging off the reported plan must not keep charge commands.
|
||||
|
||||
The simulation ignores the AC charge genes when the inverter forbids grid
|
||||
charging. The solution is read back from the same array, so a controller
|
||||
acting on it would grid-charge the battery although no such charge was ever
|
||||
simulated or paid for.
|
||||
"""
|
||||
config_eos.merge_settings_from_dict(
|
||||
{"prediction": {"hours": 2}, "optimization": {"horizon_hours": 2}}
|
||||
)
|
||||
|
||||
battery = Battery(
|
||||
SolarPanelBatteryParameters(
|
||||
device_id="battery1",
|
||||
capacity_wh=10000,
|
||||
initial_soc_percentage=50,
|
||||
min_soc_percentage=0,
|
||||
charging_efficiency=1.0,
|
||||
discharging_efficiency=1.0,
|
||||
max_charge_power_w=5000,
|
||||
),
|
||||
prediction_hours=config_eos.prediction.hours,
|
||||
)
|
||||
inverter = Inverter(
|
||||
InverterParameters(
|
||||
device_id="inverter1",
|
||||
max_power_wh=5000.0,
|
||||
battery_id=battery.parameters.device_id,
|
||||
max_ac_charge_power_w=0, # Netzladen deaktiviert
|
||||
),
|
||||
battery=battery,
|
||||
)
|
||||
|
||||
simulation = GeneticSimulation()
|
||||
simulation.prepare(
|
||||
GeneticEnergyManagementParameters(
|
||||
pv_prognose_wh=[0.0, 0.0],
|
||||
strompreis_euro_pro_wh=[0.0003, 0.0003],
|
||||
einspeiseverguetung_euro_pro_wh=[0.0001, 0.0001],
|
||||
preis_euro_pro_wh_akku=0.0,
|
||||
gesamtlast=[0.0, 0.0],
|
||||
),
|
||||
optimization_hours=config_eos.optimization.horizon_hours,
|
||||
prediction_hours=config_eos.prediction.hours,
|
||||
inverter=inverter,
|
||||
)
|
||||
simulation.ac_charge_hours = np.array([0.8, 0.0])
|
||||
|
||||
soc_before = battery.current_soc_percentage()
|
||||
simulation.simulate(start_hour=0)
|
||||
|
||||
# Nothing was charged ...
|
||||
assert battery.current_soc_percentage() == pytest.approx(soc_before)
|
||||
# ... and the plan says so.
|
||||
assert simulation.ac_charge_hours is not None
|
||||
assert list(simulation.ac_charge_hours) == [0.0, 0.0]
|
||||
|
||||
Reference in New Issue
Block a user