mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-08-31 12:46:38 +00:00
perf: cache AC charge break-even prices
This commit is contained in:
@@ -208,21 +208,59 @@ data and rely on file/JSON imports only for initial setup.
|
|||||||
|
|
||||||
Prediction keys:
|
Prediction keys:
|
||||||
|
|
||||||
- `feed_in_tarif_wh`: Feed in tarif per Wh (€/Wh).
|
- `feed_in_tariff_wh`: Feed-in tariff per Wh (€/Wh).
|
||||||
- `feed_in_tarif_kwh`: Feed in tarif per kWh (€/kWh)
|
- `feed_in_tariff_kwh`: Feed-in tariff per kWh (€/kWh).
|
||||||
|
|
||||||
Configuration options:
|
Configuration options:
|
||||||
|
|
||||||
- `feedintarif`: Feed in tariff configuration.
|
- `feedintariff`: Feed-in tariff configuration.
|
||||||
|
|
||||||
- `provider`: Feed in tariff provider id of provider to be used.
|
- `provider`: Feed in tariff provider id of provider to be used.
|
||||||
|
|
||||||
- `FeedInTariffFixed`: Provides fixed feed in tariff values.
|
- `FeedInTariffFixed`: Provides fixed feed in tariff values.
|
||||||
|
- `FeedInTariffEnergyCharts`: Retrieves Energy-Charts day-ahead market prices and extends
|
||||||
|
them to the configured prediction horizon when necessary.
|
||||||
- `FeedInTariffImport`: Imports from a file or JSON string or by endpoint data provision.
|
- `FeedInTariffImport`: Imports from a file or JSON string or by endpoint data provision.
|
||||||
|
|
||||||
- `provider_settings.feed_in_tariff_kwh`: Fixed feed in tariff (€/kWh).
|
- `provider_settings.FeedInTariffFixed.feed_in_tariff_kwh`: Fixed feed-in tariff (€/kWh).
|
||||||
- `provider_settings.import_file_path`: Path to the file to import feed in tariff forecast data from.
|
- `provider_settings.FeedInTariffEnergyCharts.bidding_zone`: Energy-Charts bidding zone.
|
||||||
- `provider_settings.import_json`: JSON string, dictionary of feed in tariff value lists.
|
- `provider_settings.FeedInTariffImport.import_file_path`: Path to the file containing imported
|
||||||
|
feed-in tariff prediction data.
|
||||||
|
- `provider_settings.FeedInTariffImport.import_json`: JSON string containing feed-in tariff
|
||||||
|
prediction data.
|
||||||
|
|
||||||
|
### FeedInTariffEnergyCharts Provider
|
||||||
|
|
||||||
|
The `FeedInTariffEnergyCharts` provider uses the raw Energy-Charts day-ahead market price as the
|
||||||
|
feed-in tariff. It stores prices in `feed_in_tariff_wh` without adding electricity import charges
|
||||||
|
or VAT. The native Energy-Charts resolution, including quarter-hour data, is retained.
|
||||||
|
|
||||||
|
Energy-Charts usually supplies prices only for the published day-ahead period. If that data does
|
||||||
|
not cover the complete configured prediction horizon, the provider extends it as follows:
|
||||||
|
|
||||||
|
- With more than 800 hours of history, an ETS (Holt-Winters exponential smoothing) forecast with
|
||||||
|
weekly seasonality is used.
|
||||||
|
- With more than 168 hours of history, an ETS forecast with daily seasonality is used.
|
||||||
|
- With less history, the median of the available values is used as a constant fallback.
|
||||||
|
|
||||||
|
The seasonal periods are adjusted to the source resolution. For example, quarter-hour data uses
|
||||||
|
four values per hour. Values already supplied by Energy-Charts are kept unchanged; only missing
|
||||||
|
future slots after the last published price are forecast.
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"feedintariff": {
|
||||||
|
"provider": "FeedInTariffEnergyCharts",
|
||||||
|
"provider_settings": {
|
||||||
|
"FeedInTariffEnergyCharts": {
|
||||||
|
"bidding_zone": "DE-LU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### FeedInTariffImport Provider
|
### FeedInTariffImport Provider
|
||||||
|
|
||||||
|
|||||||
@@ -561,6 +561,9 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
self.fix_seed = random.randint(1, 100000000000) # noqa: S311
|
self.fix_seed = random.randint(1, 100000000000) # noqa: S311
|
||||||
random.seed(self.fix_seed)
|
random.seed(self.fix_seed)
|
||||||
|
|
||||||
|
# Per-run cache for the AC-charge break-even penalty (see evaluate()).
|
||||||
|
self._ac_break_even_best_prices: Optional[list[float]] = None
|
||||||
|
|
||||||
# Create Simulation
|
# Create Simulation
|
||||||
self.simulation = GeneticSimulation()
|
self.simulation = GeneticSimulation()
|
||||||
|
|
||||||
@@ -571,6 +574,47 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _ac_break_even_prices(
|
||||||
|
self,
|
||||||
|
prices_arr: Any,
|
||||||
|
load_arr: Any,
|
||||||
|
free_ac_wh: float,
|
||||||
|
) -> list[float]:
|
||||||
|
"""Best still-uncovered future price per potential AC-charge slot.
|
||||||
|
|
||||||
|
The AC-charge break-even penalty needs, for every potential charge slot,
|
||||||
|
the highest future price whose load is not already covered by the energy
|
||||||
|
that is in the battery at simulation start. Prices, loads and the free
|
||||||
|
battery energy are constant within one optimization run, so this table
|
||||||
|
is computed once per run and looked up in every fitness evaluation.
|
||||||
|
(Previously the future list was rebuilt and sorted per slot per
|
||||||
|
individual, which dominated the fitness runtime.) The loops replicate
|
||||||
|
the former inline computation exactly, keeping results bit-identical.
|
||||||
|
"""
|
||||||
|
n = len(prices_arr)
|
||||||
|
best_prices = [0.0] * n
|
||||||
|
for hour in range(n):
|
||||||
|
# Build list of (price, load_wh) for all future hours in the horizon
|
||||||
|
future = [(float(prices_arr[h]), float(load_arr[h])) for h in range(hour + 1, n)]
|
||||||
|
# Sort descending by price so we "use" the most expensive hours first
|
||||||
|
future.sort(key=lambda x: -x[0])
|
||||||
|
|
||||||
|
# Consume free PV energy against the highest-price future hours.
|
||||||
|
# The first uncovered (partially or fully) hour defines the best
|
||||||
|
# price still available for the new AC charge.
|
||||||
|
remaining_free = free_ac_wh
|
||||||
|
best_uncovered_price = 0.0
|
||||||
|
for fp, fl in future:
|
||||||
|
if remaining_free >= fl:
|
||||||
|
# Entire expensive hour is already covered by free PV energy
|
||||||
|
remaining_free -= fl
|
||||||
|
else:
|
||||||
|
# First hour not (fully) covered: this is where new charge goes
|
||||||
|
best_uncovered_price = fp
|
||||||
|
break
|
||||||
|
best_prices[hour] = best_uncovered_price
|
||||||
|
return best_prices
|
||||||
|
|
||||||
def _parameters_for_config(
|
def _parameters_for_config(
|
||||||
self, parameters: GeneticOptimizationParameters
|
self, parameters: GeneticOptimizationParameters
|
||||||
) -> GeneticOptimizationParameters:
|
) -> GeneticOptimizationParameters:
|
||||||
@@ -1157,7 +1201,17 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
* inv.dc_to_ac_efficiency
|
* inv.dc_to_ac_efficiency
|
||||||
)
|
)
|
||||||
|
|
||||||
if round_trip_eff > 0:
|
# Configurable penalty multiplier (default 1 = economic loss in €)
|
||||||
|
try:
|
||||||
|
ac_penalty_factor = float(
|
||||||
|
self.config.optimization.genetic.penalties["ac_charge_break_even"]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
ac_penalty_factor = 1.0
|
||||||
|
|
||||||
|
# A factor of 0 multiplies every penalty term to zero - skip the
|
||||||
|
# whole computation in that case.
|
||||||
|
if round_trip_eff > 0 and ac_penalty_factor != 0.0:
|
||||||
ac_charge_arr = self.simulation.ac_charge_hours
|
ac_charge_arr = self.simulation.ac_charge_hours
|
||||||
prices_arr = self.simulation.elect_price_hourly
|
prices_arr = self.simulation.elect_price_hourly
|
||||||
load_arr = self.simulation.load_energy_array
|
load_arr = self.simulation.load_energy_array
|
||||||
@@ -1173,13 +1227,13 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
* inv.dc_to_ac_efficiency
|
* inv.dc_to_ac_efficiency
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configurable penalty multiplier (default 1 = economic loss in €)
|
# Prices/loads/free energy are constant within one optimization
|
||||||
try:
|
# run - compute the break-even lookup once, reuse it for every
|
||||||
ac_penalty_factor = float(
|
# individual (cache is reset per run in optimierung_ems()).
|
||||||
self.config.optimization.genetic.penalties["ac_charge_break_even"]
|
best_prices = getattr(self, "_ac_break_even_best_prices", None)
|
||||||
)
|
if best_prices is None:
|
||||||
except Exception:
|
best_prices = self._ac_break_even_prices(prices_arr, load_arr, free_ac_wh)
|
||||||
ac_penalty_factor = 1.0
|
self._ac_break_even_best_prices = best_prices
|
||||||
|
|
||||||
for hour in range(start_hour, min(len(ac_charge_arr), n)):
|
for hour in range(start_hour, min(len(ac_charge_arr), n)):
|
||||||
ac_factor = ac_charge_arr[hour]
|
ac_factor = ac_charge_arr[hour]
|
||||||
@@ -1193,26 +1247,7 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
# Price that a future discharge hour must reach to break even
|
# Price that a future discharge hour must reach to break even
|
||||||
break_even_price = charge_price / round_trip_eff
|
break_even_price = charge_price / round_trip_eff
|
||||||
|
|
||||||
# Build list of (price, load_wh) for all future hours in the horizon
|
best_uncovered_price = best_prices[hour]
|
||||||
future = [
|
|
||||||
(float(prices_arr[h]), float(load_arr[h])) for h in range(hour + 1, n)
|
|
||||||
]
|
|
||||||
# Sort descending by price so we "use" the most expensive hours first
|
|
||||||
future.sort(key=lambda x: -x[0])
|
|
||||||
|
|
||||||
# Consume free PV energy against the highest-price future hours.
|
|
||||||
# The first uncovered (partially or fully) hour defines the best
|
|
||||||
# price still available for the new AC charge.
|
|
||||||
remaining_free = free_ac_wh
|
|
||||||
best_uncovered_price = 0.0
|
|
||||||
for fp, fl in future:
|
|
||||||
if remaining_free >= fl:
|
|
||||||
# Entire expensive hour is already covered by free PV energy
|
|
||||||
remaining_free -= fl
|
|
||||||
else:
|
|
||||||
# First hour not (fully) covered: this is where new charge goes
|
|
||||||
best_uncovered_price = fp
|
|
||||||
break
|
|
||||||
|
|
||||||
if best_uncovered_price < break_even_price:
|
if best_uncovered_price < break_even_price:
|
||||||
# AC charging at this hour is economically unjustified.
|
# AC charging at this hour is economically unjustified.
|
||||||
@@ -1369,6 +1404,9 @@ class GeneticOptimization(OptimizationBase):
|
|||||||
logger.error("Generations not configured. Using {}.", generations)
|
logger.error("Generations not configured. Using {}.", generations)
|
||||||
|
|
||||||
self.simulation.reset()
|
self.simulation.reset()
|
||||||
|
# Prices/loads/initial SoC may differ from the previous run - the
|
||||||
|
# break-even lookup must be rebuilt lazily on first evaluation.
|
||||||
|
self._ac_break_even_best_prices = None
|
||||||
|
|
||||||
# Initialize PV and EV batteries. slot_duration_h lets the Battery scale
|
# Initialize PV and EV batteries. slot_duration_h lets the Battery scale
|
||||||
# its power caps (max_charge_power_w) to a per-slot energy cap.
|
# its power caps (max_charge_power_w) to a per-slot energy cap.
|
||||||
|
|||||||
Reference in New Issue
Block a user