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
|
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
|
of the horizon, and the seeding heuristics only propose charge slots before it. Without a
|
||||||
deadline the behaviour is unchanged.
|
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)
|
- 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)
|
- 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
|
- 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
|
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:
|
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
|
# Fill the charge array of the battery
|
||||||
dc_charge_hours_fast[0:start_hour] = 0
|
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["Gesamtkosten_Euro"] == pytest.approx(0.06)
|
||||||
assert result["Einnahmen_Euro_pro_Stunde"][0] == pytest.approx(0.08)
|
assert result["Einnahmen_Euro_pro_Stunde"][0] == pytest.approx(0.08)
|
||||||
assert result["Gesamtbilanz_Euro"] == pytest.approx(-0.02)
|
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