From bed1f0f2751d038c719a6d3ad50f624ecdd2f127 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 15 Jul 2026 08:52:16 +0200 Subject: [PATCH] feat(optimization): model battery LCOS and probabilistic bypass --- CHANGELOG.md | 8 + docs/_generated/configdevices.md | 2 +- docs/_generated/configelecprice.md | 2 +- docs/_generated/configexample.md | 2 + docs/_generated/configoptimization.md | 6 + docs/akkudoktoreos/optimauto.md | 21 +- docs/akkudoktoreos/optimpost.md | 74 ++- openapi.json | 72 ++- src/akkudoktoreos/devices/devices.py | 6 +- src/akkudoktoreos/devices/genetic/battery.py | 9 + src/akkudoktoreos/devices/genetic/inverter.py | 204 +++--- .../optimization/genetic/genetic.py | 23 +- .../optimization/genetic/geneticdevices.py | 11 + .../optimization/genetic/geneticparams.py | 21 +- .../optimization/optimization.py | 12 + src/akkudoktoreos/prediction/interpolator.py | 71 ++- tests/test_battery.py | 5 + tests/test_geneticsimulation.py | 38 +- tests/test_interpolator.py | 44 +- tests/test_inverter.py | 119 +++- tests/test_inverter_efficiency.py | 51 +- tests/test_optimization_settings.py | 30 + tests/testdata/optimize_result_1.json | 196 +++--- tests/testdata/optimize_result_1_be.json | 208 +++---- tests/testdata/optimize_result_2.json | 206 +++--- tests/testdata/optimize_result_2_be.json | 226 +++---- tests/testdata/optimize_result_2_full.json | 585 ++++++++++-------- 27 files changed, 1362 insertions(+), 890 deletions(-) create mode 100644 tests/test_optimization_settings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9205114f..cb54cd50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). resolution, so both the hourly and the 15-minute optimizer are fed the correct grid. The seasonal price extrapolation is resolution-agnostic and stays identical at the default hourly resolution. +- Separate battery economics into two independent settings: battery + `levelized_cost_of_storage_kwh` is now charged once on delivered DC energy, while + `optimization.terminal_value_euro_per_kwh` values usable battery energy left at the end of the + optimization horizon. LCOS applies to both local battery supply and battery-to-grid export and is + included in hourly and total costs. +- Model direct PV-to-load consumption probabilistically from the bundled conditional minute-load + table. The expected direct flow is used consistently for PV bypass, residual load, battery + charging, and grid export on hourly and 15-minute optimization grids. ## 0.3.0 (2026-03-17) diff --git a/docs/_generated/configdevices.md b/docs/_generated/configdevices.md index 38e3d691..f63f6652 100644 --- a/docs/_generated/configdevices.md +++ b/docs/_generated/configdevices.md @@ -456,7 +456,7 @@ as a cohesive unit for scheduling and availability checking. | charging_efficiency | `float` | `rw` | `0.88` | Charging efficiency [0.01 ... 1.00]. | | device_id | `str` | `rw` | `` | ID of device | | discharging_efficiency | `float` | `rw` | `0.88` | Discharge efficiency [0.01 ... 1.00]. | -| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh]. | +| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | Levelized cost of storage (LCOS), applied once to each kWh delivered by the battery [€/kWh]. | | max_charge_power_w | `Optional[float]` | `rw` | `5000` | Maximum charging power [W]. | | max_soc_percentage | `int` | `rw` | `100` | Maximum state of charge (SOC) as percentage of capacity [%]. | | measurement_key_power_3_phase_sym_w | `str` | `ro` | `N/A` | Measurement key for the symmetric 3 phase power the battery is charged or discharged with [W]. | diff --git a/docs/_generated/configelecprice.md b/docs/_generated/configelecprice.md index f5916324..9df69d24 100644 --- a/docs/_generated/configelecprice.md +++ b/docs/_generated/configelecprice.md @@ -99,7 +99,7 @@ | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | | access_token | `Optional[str]` | `rw` | `None` | Tibber API access token. | -| home_id | `Optional[str]` | `rw` | `None` | Tibber home id to read prices from. | +| home_id | `Optional[str]` | `rw` | `None` | Optional Tibber home id. If omitted, the first home with a subscription is used. | ::: diff --git a/docs/_generated/configexample.md b/docs/_generated/configexample.md index c94a1e30..a3431c5d 100644 --- a/docs/_generated/configexample.md +++ b/docs/_generated/configexample.md @@ -176,6 +176,8 @@ "horizon_hours": 24, "interval": 3600, "algorithm": "GENETIC", + "visualize_pdf": true, + "terminal_value_euro_per_kwh": 0.0, "genetic": { "individuals": 400, "generations": 400, diff --git a/docs/_generated/configoptimization.md b/docs/_generated/configoptimization.md index 6a002a4f..973cd3e3 100644 --- a/docs/_generated/configoptimization.md +++ b/docs/_generated/configoptimization.md @@ -13,6 +13,8 @@ | horizon_hours | `EOS_OPTIMIZATION__HORIZON_HOURS` | `int` | `rw` | `24` | The general time window within which the energy optimization goal shall be achieved [h]. Defaults to 24 hours. | | interval | `EOS_OPTIMIZATION__INTERVAL` | `int` | `rw` | `3600` | The optimization interval (slot length) [sec]. The genetic optimizer supports 3600 (1 hour) and 900 (15 min); other values fall back to 3600. Defaults to 3600 seconds (1 hour). | | keys | | `list[str]` | `ro` | `N/A` | The keys of the solution. | +| terminal_value_euro_per_kwh | `EOS_OPTIMIZATION__TERMINAL_VALUE_EURO_PER_KWH` | `float` | `rw` | `0.0` | Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Defaults to 0 EUR/kWh. | +| visualize_pdf | `EOS_OPTIMIZATION__VISUALIZE_PDF` | `bool` | `rw` | `True` | Generate the PDF visualization after each optimization run. Disable for headless setups (e.g. Node-RED integration) to save several seconds per run. Defaults to True. | ::: @@ -27,6 +29,8 @@ "horizon_hours": 24, "interval": 3600, "algorithm": "GENETIC", + "visualize_pdf": true, + "terminal_value_euro_per_kwh": 0.0, "genetic": { "individuals": 400, "generations": 400, @@ -51,6 +55,8 @@ "horizon_hours": 24, "interval": 3600, "algorithm": "GENETIC", + "visualize_pdf": true, + "terminal_value_euro_per_kwh": 0.0, "genetic": { "individuals": 400, "generations": 400, diff --git a/docs/akkudoktoreos/optimauto.md b/docs/akkudoktoreos/optimauto.md index a96fa664..e15c5d6f 100644 --- a/docs/akkudoktoreos/optimauto.md +++ b/docs/akkudoktoreos/optimauto.md @@ -145,6 +145,11 @@ The energy management can be run in three modes: `prediction.hours * (3600 / interval)`, and device power caps as well as the solution and energy-management-plan serializers are slot-aware. +- **terminal_value_euro_per_kwh** (`float`, default: `0.0`): Monetary value assigned to usable + battery energy remaining at the end of the optimization horizon. This terminal value influences + whether the optimizer preserves or depletes the battery near the horizon. It is independent of + the battery's `levelized_cost_of_storage_kwh`, which prices actual discharge throughput. + :::{note} Use `900` together with a 15-minute electricity price source (for example a dynamic or exchange-priced tariff) to let the optimizer schedule on a quarter-hour grid. Keeping the @@ -224,8 +229,9 @@ The behavior of the genetic algorithm can be customized using the following conf ```json { "optimization": { - "hours": 24, + "horizon_hours": 24, "interval": 3600, + "terminal_value_euro_per_kwh": 0.20, "genetic" : { "individuals": 300, "generations": 400, @@ -342,6 +348,19 @@ The inverter supports separate AC↔DC conversion efficiencies: } ``` +`levelized_cost_of_storage_kwh` is an optional variable cost in EUR/kWh (default `0.0`). EOS applies +it exactly once to the DC energy actually delivered by the battery, whether that energy supplies +the local load or is exported to the grid: + +```{math} +C_{LCOS} = \frac{E_{bat,out,Wh}}{1000}\,c_{LCOS,EUR/kWh} +``` + +Charging energy, battery-internal discharge losses, and the subsequent DC-to-AC inverter loss do +not receive another LCOS charge. LCOS is included in the hourly and total simulation costs and is +separate from `optimization.terminal_value_euro_per_kwh`, which values only the usable energy left +at the end of the optimization horizon. + #### Home appliance simulation configuration **Example:** diff --git a/docs/akkudoktoreos/optimpost.md b/docs/akkudoktoreos/optimpost.md index 73e884e6..1b523564 100644 --- a/docs/akkudoktoreos/optimpost.md +++ b/docs/akkudoktoreos/optimpost.md @@ -70,6 +70,7 @@ to `DISABLED` in the configuration. "pv_akku": { "device_id": "battery1", "capacity_wh": 26400, + "levelized_cost_of_storage_kwh": 0.12, "max_charge_power_w": 5000, "initial_soc_percentage": 80, "min_soc_percentage": 15 @@ -112,12 +113,15 @@ to `DISABLED` in the configuration. ### Energy Management System (EMS) -#### Battery Cost (`preis_euro_pro_wh_akku`) +#### Battery Terminal Value (`preis_euro_pro_wh_akku`) - Unit: €/Wh - Purpose: Represents the residual value of energy stored in the battery - Impact: Lower values encourage battery depletion, higher values preserve charge at the end of the simulation. +- Separation from LCOS: This value is only applied to usable battery energy remaining at the end of + the optimization horizon. Battery discharge throughput is priced separately with + `pv_akku.levelized_cost_of_storage_kwh`. #### Feed-in Tariff (`einspeiseverguetung_euro_pro_wh`) @@ -145,6 +149,51 @@ to `DISABLED` in the configuration. - Format: Array of hourly values - Data Source: `GET /v1/prediction/series?key=pvforecast_ac_power` +#### Probabilistic Direct PV Consumption and Bypass + +Hourly or 15-minute mean values alone would optimistically assume that the smaller of mean PV +generation and mean load is consumed directly. Real household load varies within the interval. EOS +therefore uses a conditional probability table derived from one-minute load samples. For a forecast +mean load \(\mu_L\), the table contains load-bin powers \(L_i\) and their conditional probabilities +\(p_i = P(L=L_i\mid\mu_L)\), with \(\sum_i p_i=1\). + +Because the finite 50 W table grid can deviate slightly from the requested forecast mean, the load +bins are first normalized without changing the shape of the distribution: + +```{math} +\widetilde{L}_i = L_i \frac{\mu_L}{\sum_j p_j L_j} +``` + +For mean PV power \(P_{PV}\), the expected power flowing directly from PV to the load is: + +```{math} +P_{direct} = \sum_i p_i \min\left(\widetilde{L}_i, P_{PV}\right) +``` + +For a slot of duration \(\Delta t\), EOS converts this power into energy and derives both residual +flows from the same direct-consumption value: + +```{math} +\begin{aligned} +E_{direct} &= \Delta t\,P_{direct} \\ +E_{load,residual} &= E_{load}-E_{direct} \\ +E_{PV,surplus} &= E_{PV}-E_{direct} +\end{aligned} +``` + +The residual load is supplied by the battery and then the grid. The PV surplus charges the battery; +any remainder bypasses the battery and is exported. Both residual load and PV surplus may be +positive in the same coarse slot because they occur during different sub-intervals. This is expected +and preserves the energy balances +\(E_{direct}+E_{load,residual}=E_{load}\) and +\(E_{direct}+E_{PV,surplus}=E_{PV}\). + +The bundled table is conditioned on a one-hour mean load and models load variation only; mean PV is +treated as constant inside the slot. For a 15-minute grid produced by splitting hourly energy, the +power lookup retains the original hourly mean. A native 15-minute load forecast uses the same table +as an approximation until a separately calibrated 15-minute distribution is available. Fast PV +variability, for example from clouds, is not represented by this table. + #### Electricity Price Forecast (`strompreis_euro_pro_wh`) - Unit: €/Wh @@ -162,8 +211,27 @@ Verify prices against your local tariffs. - `capacity_wh`: Total battery capacity in Wh - `charging_efficiency`: Charging efficiency (0-1) - `discharging_efficiency`: Discharging efficiency (0-1) +- `levelized_cost_of_storage_kwh`: LCOS in EUR/kWh, charged once for every kWh of DC energy + delivered by the battery. Default: `0.0`. - `max_charge_power_w`: Maximum charging power in W +#### Battery LCOS (`levelized_cost_of_storage_kwh`) + +LCOS and terminal value have different purposes. LCOS is a variable battery-use cost and is added +once when the battery delivers energy, both for local load coverage and battery-to-grid export. It +is not charged when the battery is charged and is not charged again on battery-internal or +DC-to-AC inverter losses. + +For battery-delivered DC energy `E_bat,out` in one slot: + +```{math} +C_{LCOS} = \frac{E_{bat,out}}{1000}\,c_{LCOS} +``` + +where `E_bat,out` is in Wh and `c_LCOS` is in EUR/kWh. This cost is included in +`Kosten_Euro_pro_Stunde`, `Gesamtkosten_Euro`, and therefore `Gesamtbilanz_Euro`. The terminal value +`preis_euro_pro_wh_akku`, by contrast, applies only to usable energy remaining after the last slot. + #### State of Charge (SoC) - `initial_soc_percentage`: Current battery level (%) @@ -198,7 +266,7 @@ Round-trip efficiency for AC charging and discharging: `η_round_trip = ac_to_dc_efficiency × charging_efficiency × discharging_efficiency × dc_to_ac_efficiency` For profitability, the discharge electricity price must exceed: -`buy_price / η_round_trip` +`buy_price / η_round_trip + LCOS / dc_to_ac_efficiency` **Backward compatibility**: With default values (`ac_to_dc_efficiency=1.0`, `dc_to_ac_efficiency=1.0`, `max_ac_charge_power_w=null`), existing configurations work identically. @@ -223,7 +291,7 @@ penalty = ac_wh_charged × (break_even_price − best_uncovered_price) × factor ``` where: -- `break_even_price = charge_price / η_round_trip` +- `break_even_price = charge_price / η_round_trip + LCOS / dc_to_ac_efficiency` - `best_uncovered_price` = highest future price not already covered by free PV battery energy - `factor` = `optimization.genetic.penalties.ac_charge_break_even` (default `1.0`) diff --git a/openapi.json b/openapi.json index c06f2226..c00679ee 100644 --- a/openapi.json +++ b/openapi.json @@ -2213,8 +2213,9 @@ }, "levelized_cost_of_storage_kwh": { "type": "number", + "minimum": 0.0, "title": "Levelized Cost Of Storage Kwh", - "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [\u20ac/kWh].", + "description": "Levelized cost of storage (LCOS), applied once to each kWh delivered by the battery [\u20ac/kWh].", "default": 0.0, "examples": [ 0.12 @@ -2367,8 +2368,9 @@ }, "levelized_cost_of_storage_kwh": { "type": "number", + "minimum": 0.0, "title": "Levelized Cost Of Storage Kwh", - "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [\u20ac/kWh].", + "description": "Levelized cost of storage (LCOS), applied once to each kWh delivered by the battery [\u20ac/kWh].", "default": 0.0, "examples": [ 0.12 @@ -4886,7 +4888,7 @@ "preis_euro_pro_wh_akku": { "type": "number", "title": "Preis Euro Pro Wh Akku", - "description": "A float representing the cost of battery energy per watt-hour." + "description": "Terminal value of usable battery energy remaining at the end of the optimization horizon [EUR/Wh]. This is not the battery LCOS." }, "gesamtlast": { "items": { @@ -5111,6 +5113,14 @@ "type": "array", "title": "Electricity Price", "description": "Used Electricity Price, including predictions" + }, + "Feed_in_tariff": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Feed In Tariff", + "description": "Used feed-in tariff in \u20ac/Wh per hour, including predictions" } }, "additionalProperties": false, @@ -6860,7 +6870,7 @@ "maximum": 3600.0, "minimum": 900.0, "title": "Interval", - "description": "The optimization interval [sec]. Defaults to 3600 seconds (1 hour)", + "description": "The optimization interval (slot length) [sec]. The genetic optimizer supports 3600 (1 hour) and 900 (15 min); other values fall back to 3600. Defaults to 3600 seconds (1 hour).", "default": 3600, "examples": [ 3600, @@ -6876,6 +6886,26 @@ "GENETIC" ] }, + "visualize_pdf": { + "type": "boolean", + "title": "Visualize Pdf", + "description": "Generate the PDF visualization after each optimization run. Disable for headless setups (e.g. Node-RED integration) to save several seconds per run. Defaults to True.", + "default": true, + "examples": [ + true, + false + ] + }, + "terminal_value_euro_per_kwh": { + "type": "number", + "title": "Terminal Value Euro Per Kwh", + "description": "Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Defaults to 0 EUR/kWh.", + "default": 0.0, + "examples": [ + 0.0, + 0.2 + ] + }, "genetic": { "$ref": "#/components/schemas/GeneticCommonSettings", "description": "Genetic optimization algorithm configuration.", @@ -6910,7 +6940,7 @@ "maximum": 3600.0, "minimum": 900.0, "title": "Interval", - "description": "The optimization interval [sec]. Defaults to 3600 seconds (1 hour)", + "description": "The optimization interval (slot length) [sec]. The genetic optimizer supports 3600 (1 hour) and 900 (15 min); other values fall back to 3600. Defaults to 3600 seconds (1 hour).", "default": 3600, "examples": [ 3600, @@ -6926,6 +6956,26 @@ "GENETIC" ] }, + "visualize_pdf": { + "type": "boolean", + "title": "Visualize Pdf", + "description": "Generate the PDF visualization after each optimization run. Disable for headless setups (e.g. Node-RED integration) to save several seconds per run. Defaults to True.", + "default": true, + "examples": [ + true, + false + ] + }, + "terminal_value_euro_per_kwh": { + "type": "number", + "title": "Terminal Value Euro Per Kwh", + "description": "Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Defaults to 0 EUR/kWh.", + "default": 0.0, + "examples": [ + 0.0, + 0.2 + ] + }, "genetic": { "$ref": "#/components/schemas/GeneticCommonSettings", "description": "Genetic optimization algorithm configuration.", @@ -8877,6 +8927,16 @@ ], null ] + }, + "levelized_cost_of_storage_kwh": { + "type": "number", + "minimum": 0.0, + "title": "Levelized Cost Of Storage Kwh", + "description": "Levelized cost of storage applied once to each kWh delivered by the battery [EUR/kWh].", + "default": 0.0, + "examples": [ + 0.12 + ] } }, "additionalProperties": false, @@ -9465,4 +9525,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/akkudoktoreos/devices/devices.py b/src/akkudoktoreos/devices/devices.py index 98b155a7..9171fb22 100644 --- a/src/akkudoktoreos/devices/devices.py +++ b/src/akkudoktoreos/devices/devices.py @@ -50,8 +50,12 @@ class BatteriesCommonSettings(DevicesBaseSettings): levelized_cost_of_storage_kwh: float = Field( default=0.0, + ge=0.0, json_schema_extra={ - "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh].", + "description": ( + "Levelized cost of storage (LCOS), applied once to each kWh delivered " + "by the battery [€/kWh]." + ), "examples": [0.12], }, ) diff --git a/src/akkudoktoreos/devices/genetic/battery.py b/src/akkudoktoreos/devices/genetic/battery.py index e6aff926..29cd174c 100644 --- a/src/akkudoktoreos/devices/genetic/battery.py +++ b/src/akkudoktoreos/devices/genetic/battery.py @@ -34,6 +34,11 @@ class Battery: self.initial_soc_percentage = self.parameters.initial_soc_percentage self.charging_efficiency = self.parameters.charging_efficiency self.discharging_efficiency = self.parameters.discharging_efficiency + self.levelized_cost_of_storage_kwh = ( + self.parameters.levelized_cost_of_storage_kwh + if isinstance(self.parameters, SolarPanelBatteryParameters) + else 0.0 + ) # Charge rates, in case of None use default self.charge_rates = np.array(BATTERY_DEFAULT_CHARGE_RATES, dtype=float) @@ -115,6 +120,10 @@ class Battery: raw_soc_available_wh = max(self.soc_wh - self.min_soc_wh, 0.0) return min(raw_power_remaining_wh, raw_soc_available_wh) * self.discharging_efficiency + def discharged_energy_wh(self, hour: int) -> float: + """Return DC energy delivered by the battery in one optimization slot.""" + return self._discharged_raw_wh_per_slot[hour] * self.discharging_efficiency + def set_discharge_per_hour(self, discharge_array: np.ndarray) -> None: """Sets the discharge values for each hour.""" if len(discharge_array) != self.prediction_hours: diff --git a/src/akkudoktoreos/devices/genetic/inverter.py b/src/akkudoktoreos/devices/genetic/inverter.py index da098583..7d28078c 100644 --- a/src/akkudoktoreos/devices/genetic/inverter.py +++ b/src/akkudoktoreos/devices/genetic/inverter.py @@ -14,9 +14,6 @@ class Inverter: battery: Optional[Battery] = None, slot_duration_h: float = 1.0, ): - # slot_duration_h scales the per-slot energy cap (max_power_wh). It - # defaults to 1.0, which keeps the hourly behaviour for the default - # optimization interval of 3600 s. self.parameters: InverterParameters = parameters self.battery: Optional[Battery] = battery self.slot_duration_h: float = slot_duration_h @@ -28,16 +25,13 @@ class Inverter: logger.error(error_msg) raise ValueError(error_msg) self.self_consumption_predictor = get_eos_load_interpolator() - # max_power_wh is supplied as a power [W] that the legacy hourly code - # treats as Wh-per-hour. Scale it to the actual slot length so a 15-min - # slot can move at most a quarter of that energy. - self.max_power_wh = ( - self.parameters.max_power_wh * self.slot_duration_h - ) # Maximum energy the inverter can move in one optimization slot + # max_power_wh is supplied as power [W] but used as the maximum energy + # the inverter can move during one optimization slot. + self.max_power_wh = self.parameters.max_power_wh * self.slot_duration_h self.dc_to_ac_efficiency = self.parameters.dc_to_ac_efficiency self.ac_to_dc_efficiency = self.parameters.ac_to_dc_efficiency - # max_ac_charge_power_w stays in Watts. It feeds a dimensionless, - # slot-agnostic power-ratio cap in genetic.py simulate(). + # This value remains a power [W]. GeneticSimulation converts it into a + # slot-independent charge-factor limit. self.max_ac_charge_power_w = self.parameters.max_ac_charge_power_w def _discharge_battery_to_ac(self, requested_ac_wh: float, hour: int) -> tuple[float, float]: @@ -58,128 +52,86 @@ class Inverter: hour: int, allow_battery_grid_export: bool = False, ) -> tuple[float, float, float, float]: + """Process one slot using probabilistic direct PV-to-load overlap. + + ``generation`` and ``consumption`` are interval energies. The load + probability table is evaluated in watts and yields the expected direct + PV-to-load power. The remaining load and PV surplus are then handled + independently, because both can occur during different sub-intervals of + the same hourly or 15-minute slot. + """ losses = 0.0 grid_export = 0.0 - grid_import = 0.0 - self_consumption = 0.0 + generation = max(float(generation), 0.0) + consumption = max(float(consumption), 0.0) - # Cache inverter DC→AC efficiency for discharge path - dc_to_ac_eff = self.dc_to_ac_efficiency - - if generation >= consumption: - if consumption > self.max_power_wh: - # If consumption exceeds maximum inverter power - losses += generation - self.max_power_wh - remaining_power = self.max_power_wh - consumption - grid_import = -remaining_power # Negative indicates feeding into the grid - self_consumption = self.max_power_wh - else: - # Calculate scr using cached results per energy management/optimization run. - # The interpolator expects power levels [W]; consumption/generation are - # energy per slot [Wh], so convert via the slot duration (identical at - # the hourly default, ×4 on the 15-minute grid). - scr = self.self_consumption_predictor.calculate_self_consumption( - consumption / self.slot_duration_h, generation / self.slot_duration_h + # Convert interval energy [Wh] to mean power [W] for the probability + # lookup, then convert its expected direct power back to slot energy. + if generation > 0.0 and consumption > 0.0: + expected_direct_power_w = ( + self.self_consumption_predictor.calculate_expected_direct_consumption( + consumption / self.slot_duration_h, + generation / self.slot_duration_h, ) - - # Remaining power after consumption - remaining_power = (generation - consumption) * scr # EVQ - # Remaining load Self Consumption not perfect - remaining_load_evq = (generation - consumption) * (1.0 - scr) - - from_battery_dc = 0.0 - if remaining_load_evq > 0: - # Akku muss den Restverbrauch decken - if self.battery: - # Request more DC from battery to account for DC→AC conversion loss - dc_request = remaining_load_evq / dc_to_ac_eff - from_battery_dc, discharge_losses = self.battery.discharge_energy( - dc_request, hour - ) - # Convert DC output to AC - from_battery_ac = from_battery_dc * dc_to_ac_eff - inverter_discharge_losses = from_battery_dc - from_battery_ac - remaining_load_evq -= from_battery_ac - losses += discharge_losses + inverter_discharge_losses - else: - from_battery_ac = 0.0 - - # Wenn der Akku den Restverbrauch nicht vollständig decken kann, wird der Rest ins Netz gezogen - if remaining_load_evq > 0: - grid_import += remaining_load_evq - remaining_load_evq = 0 - else: - from_battery_ac = 0.0 - - if remaining_power > 0: - # Load battery with excess energy (DC path, no inverter conversion needed) - charge_losses = 0.0 - if self.battery: - charged_energie, charge_losses = self.battery.charge_energy( - remaining_power, hour - ) - remaining_surplus = remaining_power - (charged_energie + charge_losses) - else: - remaining_surplus = remaining_power - - # Feed-in to the grid based on remaining capacity - if remaining_surplus > self.max_power_wh - consumption: - grid_export = self.max_power_wh - consumption - losses += remaining_surplus - grid_export - else: - grid_export = remaining_surplus - - losses += charge_losses - self_consumption = ( - consumption + from_battery_ac - ) # Self-consumption is equal to the load - - if allow_battery_grid_export and self.battery: - export_capacity = max(self.max_power_wh - consumption - grid_export, 0.0) - remaining_battery_ac = ( - self.battery.remaining_discharge_energy_wh(hour) * dc_to_ac_eff - ) - export_capacity = min(export_capacity, remaining_battery_ac) - battery_export_ac, battery_export_losses = self._discharge_battery_to_ac( - export_capacity, hour - ) - grid_export += battery_export_ac - losses += battery_export_losses - + ) + direct_pv_energy = expected_direct_power_w * self.slot_duration_h else: - # Case 2: Insufficient generation, cover shortfall - shortfall = consumption - generation - available_ac_power = max(self.max_power_wh - generation, 0) + direct_pv_energy = 0.0 - # Discharge battery to cover shortfall, if possible - if self.battery: - # Need shortfall in AC, request more DC from battery for DC→AC conversion - ac_needed = min(shortfall, available_ac_power) - dc_request = ac_needed / dc_to_ac_eff - battery_discharge_dc, discharge_losses = self.battery.discharge_energy( - dc_request, hour - ) - # Convert DC output to AC - battery_discharge_ac = battery_discharge_dc * dc_to_ac_eff - inverter_discharge_losses = battery_discharge_dc - battery_discharge_ac - losses += discharge_losses + inverter_discharge_losses - else: - battery_discharge_ac = 0 + # Direct PV is bounded by both input energies and by the AC energy the + # inverter can move during this slot. + direct_pv_energy = min( + max(direct_pv_energy, 0.0), + generation, + consumption, + self.max_power_wh, + ) + remaining_load = max(consumption - direct_pv_energy, 0.0) + pv_surplus = max(generation - direct_pv_energy, 0.0) + remaining_inverter_ac_capacity = max(self.max_power_wh - direct_pv_energy, 0.0) - # Draw remaining required power from the grid (discharge_losses are already subtracted in the battery) - grid_import = shortfall - battery_discharge_ac - self_consumption = generation + battery_discharge_ac + # Load gaps and PV surplus may both occur within the same coarse slot. + # Cover the load gap first; this preserves the existing chronological + # approximation and can create headroom for later PV charging. + battery_discharge_ac = 0.0 + if remaining_load > 0.0 and self.battery and remaining_inverter_ac_capacity > 0.0: + requested_ac_wh = min(remaining_load, remaining_inverter_ac_capacity) + battery_discharge_ac, battery_discharge_losses = self._discharge_battery_to_ac( + requested_ac_wh, hour + ) + remaining_load = max(remaining_load - battery_discharge_ac, 0.0) + remaining_inverter_ac_capacity = max( + remaining_inverter_ac_capacity - battery_discharge_ac, 0.0 + ) + losses += battery_discharge_losses - if allow_battery_grid_export and self.battery and grid_import <= 0.0: - export_capacity = max(self.max_power_wh - consumption, 0.0) - remaining_battery_ac = ( - self.battery.remaining_discharge_energy_wh(hour) * dc_to_ac_eff - ) - export_capacity = min(export_capacity, remaining_battery_ac) - battery_export_ac, battery_export_losses = self._discharge_battery_to_ac( - export_capacity, hour - ) - grid_export += battery_export_ac - losses += battery_export_losses + grid_import = remaining_load + # Charge from the probabilistic PV surplus on the DC path. Stored energy + # plus charge losses equals the PV energy accepted by the battery. + remaining_surplus = pv_surplus + if remaining_surplus > 0.0 and self.battery: + charged_energy, charge_losses = self.battery.charge_energy(remaining_surplus, hour) + remaining_surplus = max(remaining_surplus - charged_energy - charge_losses, 0.0) + losses += charge_losses + + pv_grid_export = min(remaining_surplus, remaining_inverter_ac_capacity) + grid_export += pv_grid_export + remaining_inverter_ac_capacity = max(remaining_inverter_ac_capacity - pv_grid_export, 0.0) + # PV which can neither charge the battery nor pass through the inverter + # is curtailed and reported as a loss. + losses += max(remaining_surplus - pv_grid_export, 0.0) + + if allow_battery_grid_export and self.battery and remaining_inverter_ac_capacity > 0.0: + remaining_battery_ac = ( + self.battery.remaining_discharge_energy_wh(hour) * self.dc_to_ac_efficiency + ) + export_capacity = min(remaining_inverter_ac_capacity, remaining_battery_ac) + battery_export_ac, battery_export_losses = self._discharge_battery_to_ac( + export_capacity, hour + ) + grid_export += battery_export_ac + losses += battery_export_losses + + self_consumption = direct_pv_energy + battery_discharge_ac return grid_export, grid_import, losses, self_consumption diff --git a/src/akkudoktoreos/optimization/genetic/genetic.py b/src/akkudoktoreos/optimization/genetic/genetic.py index 2bbe2479..d654e1d8 100644 --- a/src/akkudoktoreos/optimization/genetic/genetic.py +++ b/src/akkudoktoreos/optimization/genetic/genetic.py @@ -450,7 +450,18 @@ class GeneticSimulation(PydanticBaseModel): feed_in_tariff_per_hour[hour_idx] = hourly_feed_in_tariff # Financial calculations - costs_per_hour[hour_idx] = energy_consumption_grid_actual * hourly_electricity_price + grid_cost = energy_consumption_grid_actual * hourly_electricity_price + # LCOS is charged exactly once on battery-delivered DC energy. It is + # not charged on input energy, internal discharge losses, or the + # downstream DC-to-AC inverter loss. + battery_lcos_cost = 0.0 + if battery_fast: + battery_lcos_cost = ( + battery_fast.discharged_energy_wh(hour) + * battery_fast.levelized_cost_of_storage_kwh + / 1000.0 + ) + costs_per_hour[hour_idx] = grid_cost + battery_lcos_cost revenue_per_hour[hour_idx] = energy_feedin_grid_actual * hourly_feed_in_tariff total_cost = np.nansum(costs_per_hour) @@ -1244,8 +1255,14 @@ class GeneticOptimization(OptimizationBase): if charge_price <= 0: continue - # Price that a future discharge hour must reach to break even - break_even_price = charge_price / round_trip_eff + # Price that a future AC discharge hour must reach to break + # even. LCOS is defined per DC Wh delivered by the battery; + # dividing it by DC-to-AC efficiency converts it to the + # corresponding cost per useful/exported AC Wh. + lcos_per_wh_dc = getattr(bat, "levelized_cost_of_storage_kwh", 0.0) / 1000.0 + break_even_price = ( + charge_price / round_trip_eff + lcos_per_wh_dc / inv.dc_to_ac_efficiency + ) best_uncovered_price = best_prices[hour] diff --git a/src/akkudoktoreos/optimization/genetic/geneticdevices.py b/src/akkudoktoreos/optimization/genetic/geneticdevices.py index d998c9a0..7baa4745 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticdevices.py +++ b/src/akkudoktoreos/optimization/genetic/geneticdevices.py @@ -98,6 +98,17 @@ class BaseBatteryParameters(DeviceParameters): class SolarPanelBatteryParameters(BaseBatteryParameters): """PV battery device simulation configuration.""" + levelized_cost_of_storage_kwh: float = Field( + default=0.0, + ge=0.0, + json_schema_extra={ + "description": ( + "Levelized cost of storage applied once to each kWh delivered " + "by the battery [EUR/kWh]." + ), + "examples": [0.12], + }, + ) max_charge_power_w: Optional[float] = max_charging_power_field() diff --git a/src/akkudoktoreos/optimization/genetic/geneticparams.py b/src/akkudoktoreos/optimization/genetic/geneticparams.py index edc6d2d6..e01e0758 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticparams.py +++ b/src/akkudoktoreos/optimization/genetic/geneticparams.py @@ -54,7 +54,10 @@ class GeneticEnergyManagementParameters(GeneticParametersBaseModel): ) preis_euro_pro_wh_akku: float = Field( json_schema_extra={ - "description": "A float representing the cost of battery energy per watt-hour." + "description": ( + "Terminal value of usable battery energy remaining at the end of the " + "optimization horizon [EUR/Wh]. This is not the battery LCOS." + ) } ) gesamtlast: list[float] = Field( @@ -416,7 +419,6 @@ class GeneticOptimizationParameters( cls.config.devices.max_batteries = 1 if cls.config.devices.max_batteries == 0: battery_params = None - battery_lcos_kwh = 0 else: if cls.config.devices.batteries is None: logger.info("No battery device data available - defaulting to demo data.") @@ -428,6 +430,9 @@ class GeneticOptimizationParameters( capacity_wh=battery_config.capacity_wh, charging_efficiency=battery_config.charging_efficiency, discharging_efficiency=battery_config.discharging_efficiency, + levelized_cost_of_storage_kwh=( + battery_config.levelized_cost_of_storage_kwh + ), max_charge_power_w=battery_config.max_charge_power_w, min_soc_percentage=battery_config.min_soc_percentage, max_soc_percentage=battery_config.max_soc_percentage, @@ -441,14 +446,6 @@ class GeneticOptimizationParameters( cls.config.devices.batteries = [{"device_id": "battery1", "capacity_wh": 8000}] # Retry continue - # Levelized cost of ownership - if battery_config.levelized_cost_of_storage_kwh is None: - logger.info( - "No battery device LCOS data available - defaulting to 0 €/kWh. Parameter preparation attempt {}.", - attempt, - ) - battery_config.levelized_cost_of_storage_kwh = 0 - battery_lcos_kwh = battery_config.levelized_cost_of_storage_kwh # Initial SOC try: initial_soc_factor = cls.measurement.key_to_value( @@ -654,7 +651,9 @@ class GeneticOptimizationParameters( strompreis_euro_pro_wh=elecprice_marketprice_wh, einspeiseverguetung_euro_pro_wh=feed_in_tariff_wh, gesamtlast=loadforecast_power_w, - preis_euro_pro_wh_akku=battery_lcos_kwh / 1000, + preis_euro_pro_wh_akku=( + cls.config.optimization.terminal_value_euro_per_kwh / 1000 + ), ), temperature_forecast=weather_temp_air, pv_akku=battery_params, diff --git a/src/akkudoktoreos/optimization/optimization.py b/src/akkudoktoreos/optimization/optimization.py index 3b0a5a98..f1d0c0c6 100644 --- a/src/akkudoktoreos/optimization/optimization.py +++ b/src/akkudoktoreos/optimization/optimization.py @@ -101,6 +101,18 @@ class OptimizationCommonSettings(SettingsBaseModel): }, ) + terminal_value_euro_per_kwh: float = Field( + default=0.0, + json_schema_extra={ + "description": ( + "Value assigned to usable battery energy remaining at the end of the " + "optimization horizon [EUR/kWh]. This terminal value is independent " + "of the battery LCOS. Defaults to 0 EUR/kWh." + ), + "examples": [0.0, 0.20], + }, + ) + genetic: GeneticCommonSettings = Field( default_factory=GeneticCommonSettings, json_schema_extra={ diff --git a/src/akkudoktoreos/prediction/interpolator.py b/src/akkudoktoreos/prediction/interpolator.py index ff7b256a..5e70485c 100644 --- a/src/akkudoktoreos/prediction/interpolator.py +++ b/src/akkudoktoreos/prediction/interpolator.py @@ -17,8 +17,32 @@ class SelfConsumptionProbabilityInterpolator: self.interpolator: RegularGridInterpolator = pickle.load(file) # noqa: S301 self.load_power_min_w = float(self.interpolator.grid[0][0]) self.load_power_max_w = float(self.interpolator.grid[0][-1]) + self.minute_load_levels_w = np.asarray(self.interpolator.grid[1], dtype=float) self.minute_load_max_w = float(self.interpolator.grid[1][-1]) + def _load_distribution(self, mean_load_power_w: float) -> tuple[np.ndarray, np.ndarray]: + """Return the conditional minute-load distribution for a mean load. + + The table stores one probability mass for each 50 W minute-load bin. + Linear interpolation between its mean-load rows can introduce very small + numerical deviations, so negative masses are removed and the result is + normalized explicitly. + """ + bounded_mean_load_w = float( + np.clip(mean_load_power_w, self.load_power_min_w, self.load_power_max_w) + ) + points = np.column_stack( + ( + np.full(self.minute_load_levels_w.shape, bounded_mean_load_w), + self.minute_load_levels_w, + ) + ) + probabilities = np.maximum(np.asarray(self.interpolator(points), dtype=float), 0.0) + probability_sum = float(probabilities.sum()) + if probability_sum <= 0.0: + return self.minute_load_levels_w, probabilities + return self.minute_load_levels_w, probabilities / probability_sum + def _generate_points( self, mean_load_power_w: float, pv_power_w: float ) -> tuple[np.ndarray, np.ndarray]: @@ -39,7 +63,12 @@ class SelfConsumptionProbabilityInterpolator: @cache_energy_management def calculate_self_consumption(self, mean_load_power_w: float, pv_power_w: float) -> float: - """Calculate the PV self-consumption rate using RegularGridInterpolator. + """Return the legacy cumulative minute-load probability. + + This method is retained for API compatibility. Its result is the + probability that the minute load is no greater than ``pv_power_w``; + it is not an energy self-consumption ratio. New energy-flow code must + use :meth:`calculate_expected_direct_consumption`. The results are cached until the start of the next energy management run/ optimization. @@ -54,6 +83,46 @@ class SelfConsumptionProbabilityInterpolator: probabilities = self.interpolator(points) return float(np.clip(probabilities.sum(), 0.0, 1.0)) + @cache_energy_management + def calculate_expected_direct_consumption( + self, mean_load_power_w: float, pv_power_w: float + ) -> float: + """Calculate expected direct PV-to-load power in watts. + + For conditional minute-load probabilities ``p_i`` and load-bin powers + ``L_i``, the expected direct consumption is + + ``sum(p_i * min(L_i, pv_power_w))``. + + The tabulated load-bin powers are rescaled to preserve the supplied + forecast mean exactly. This compensates for discretization and the + finite upper table boundary while retaining the distribution shape. + + Args: + mean_load_power_w: Mean load power of the forecast interval [W]. + pv_power_w: Mean PV power of the forecast interval [W]. + + Returns: + Expected direct PV-to-load power [W]. + """ + mean_load_power_w = max(float(mean_load_power_w), 0.0) + pv_power_w = max(float(pv_power_w), 0.0) + if mean_load_power_w == 0.0 or pv_power_w == 0.0: + return 0.0 + + load_levels_w, probabilities = self._load_distribution(mean_load_power_w) + modeled_mean_load_w = float(np.dot(probabilities, load_levels_w)) + if modeled_mean_load_w <= 0.0: + return 0.0 + + # Preserve the requested mean load while keeping the conditional shape + # from the probability table. + normalized_load_levels_w = load_levels_w * (mean_load_power_w / modeled_mean_load_w) + expected_direct_power_w = float( + np.dot(probabilities, np.minimum(normalized_load_levels_w, pv_power_w)) + ) + return float(np.clip(expected_direct_power_w, 0.0, min(mean_load_power_w, pv_power_w))) + # def calculate_self_consumption(self, load_1h_power: float, pv_power: float) -> float: # """Calculate the PV self-consumption rate using RegularGridInterpolator. diff --git a/tests/test_battery.py b/tests/test_battery.py index 1d6864ac..53665fed 100644 --- a/tests/test_battery.py +++ b/tests/test_battery.py @@ -337,4 +337,9 @@ def test_quarter_hour_discharge_calls_share_one_power_budget(): assert first_delivered == pytest.approx(200.0) assert second_delivered == pytest.approx(50.0) + assert battery.discharged_energy_wh(0) == pytest.approx(250.0) assert battery.soc_wh == pytest.approx(9_750.0) + + battery.reset() + + assert battery.discharged_energy_wh(0) == 0.0 diff --git a/tests/test_geneticsimulation.py b/tests/test_geneticsimulation.py index 0c068ee0..d532f586 100644 --- a/tests/test_geneticsimulation.py +++ b/tests/test_geneticsimulation.py @@ -338,15 +338,15 @@ def test_simulation(genetic_simulation): # Verify the total balance assert ( - abs(result["Gesamtbilanz_Euro"] - 6.62818441758576) < 1e-5 + abs(result["Gesamtbilanz_Euro"] - 7.025236588371921) < 1e-5 ), "Total balance should reflect the shared per-slot battery power limit." # Check total revenue and total costs assert ( - abs(result["Gesamteinnahmen_Euro"] - 1.9606946615517515) < 1e-5 + abs(result["Gesamteinnahmen_Euro"] - 2.3247787887715) < 1e-5 ), "Total revenue should respect the shared per-slot battery power limit." assert ( - abs(result["Gesamtkosten_Euro"] - 8.588879079137512) < 1e-5 + abs(result["Gesamtkosten_Euro"] - 9.350015377143421) < 1e-5 ), "Total costs should respect the shared per-slot battery power limit." # Check the losses @@ -387,8 +387,8 @@ def test_direct_marketing_curtails_negative_feed_in(config_eos, monkeypatch): inverter = Inverter(InverterParameters(device_id="inverter1", max_power_wh=1000.0)) monkeypatch.setattr( inverter.self_consumption_predictor, - "calculate_self_consumption", - Mock(return_value=1.0), + "calculate_expected_direct_consumption", + Mock(side_effect=min), ) simulation = GeneticSimulation() @@ -413,7 +413,11 @@ def test_direct_marketing_curtails_negative_feed_in(config_eos, monkeypatch): assert result["Verluste_Pro_Stunde"][0] == pytest.approx(500.0) -def _direct_marketing_battery_export_simulation(config_eos) -> GeneticSimulation: +def _direct_marketing_battery_export_simulation( + config_eos, + levelized_cost_of_storage_kwh: float = 0.0, + dc_to_ac_efficiency: float = 1.0, +) -> GeneticSimulation: config_eos.merge_settings_from_dict( {"prediction": {"hours": 2}, "optimization": {"horizon_hours": 2}} ) @@ -426,6 +430,7 @@ def _direct_marketing_battery_export_simulation(config_eos) -> GeneticSimulation min_soc_percentage=0, charging_efficiency=1.0, discharging_efficiency=1.0, + levelized_cost_of_storage_kwh=levelized_cost_of_storage_kwh, max_charge_power_w=500, ), prediction_hours=config_eos.prediction.hours, @@ -435,6 +440,7 @@ def _direct_marketing_battery_export_simulation(config_eos) -> GeneticSimulation device_id="inverter1", max_power_wh=500.0, battery_id=battery.parameters.device_id, + dc_to_ac_efficiency=dc_to_ac_efficiency, ), battery=battery, ) @@ -479,3 +485,23 @@ def test_direct_marketing_battery_grid_export_uses_separate_signal(config_eos): assert result["Einnahmen_Euro_pro_Stunde"][0] == pytest.approx(0.1) assert simulation.battery is not None assert simulation.battery.current_soc_percentage() == 50.0 + + +def test_battery_lcos_is_charged_once_on_delivered_energy(config_eos): + simulation = _direct_marketing_battery_export_simulation( + config_eos, + levelized_cost_of_storage_kwh=0.12, + dc_to_ac_efficiency=0.8, + ) + assert simulation.bat_grid_export_hours is not None + simulation.bat_grid_export_hours[0] = 1 + + result = simulation.simulate(start_hour=0) + + # The battery delivers 500 Wh DC, so LCOS is 0.5 kWh * 0.12 EUR/kWh + # = 0.06 EUR exactly once. After the 80% inverter, 400 Wh AC reaches + # the grid and earns 400 Wh * 0.0002 EUR/Wh = 0.08 EUR. + assert result["Kosten_Euro_pro_Stunde"][0] == 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["Gesamtbilanz_Euro"] == pytest.approx(-0.02) diff --git a/tests/test_interpolator.py b/tests/test_interpolator.py index c39591bc..8e746153 100644 --- a/tests/test_interpolator.py +++ b/tests/test_interpolator.py @@ -10,8 +10,8 @@ def test_quarter_hour_energy_is_converted_back_to_same_mean_power(): hourly_pv_wh = 1200.0 slot_duration_h = 0.25 - hourly = interpolator.calculate_self_consumption(hourly_load_wh, hourly_pv_wh) - quarter_hour = interpolator.calculate_self_consumption( + hourly = interpolator.calculate_expected_direct_consumption(hourly_load_wh, hourly_pv_wh) + quarter_hour = interpolator.calculate_expected_direct_consumption( (hourly_load_wh / 4) / slot_duration_h, (hourly_pv_wh / 4) / slot_duration_h, ) @@ -28,3 +28,43 @@ def test_load_above_probability_grid_uses_highest_supported_distribution(): assert above_boundary == pytest.approx(at_boundary) assert above_boundary > 0.99 + + +def test_expected_direct_consumption_accounts_for_subhourly_load_variation(): + """Expected overlap must be below the optimistic overlap of interval means.""" + interpolator = get_eos_load_interpolator() + + direct_power_w = interpolator.calculate_expected_direct_consumption(800.0, 1200.0) + + assert direct_power_w == pytest.approx(621.0, abs=2.0) + assert 0.0 < direct_power_w < 800.0 + + +@pytest.mark.parametrize( + ("mean_load_power_w", "pv_power_w"), + [(800.0, 1200.0), (1000.0, 500.0), (1500.0, 1500.0)], +) +def test_expected_direct_consumption_produces_conservative_energy_balance( + mean_load_power_w, pv_power_w +): + """Direct use, residual load and surplus must conserve both mean powers.""" + interpolator = get_eos_load_interpolator() + + direct_power_w = interpolator.calculate_expected_direct_consumption( + mean_load_power_w, pv_power_w + ) + residual_load_w = mean_load_power_w - direct_power_w + pv_surplus_w = pv_power_w - direct_power_w + + assert 0.0 <= direct_power_w <= min(mean_load_power_w, pv_power_w) + assert direct_power_w + residual_load_w == pytest.approx(mean_load_power_w) + assert direct_power_w + pv_surplus_w == pytest.approx(pv_power_w) + + +def test_expected_direct_consumption_preserves_forecast_mean_at_high_pv(): + """A PV level above every normalized load bin covers the complete mean load.""" + interpolator = get_eos_load_interpolator() + + direct_power_w = interpolator.calculate_expected_direct_consumption(3000.0, 10000.0) + + assert direct_power_w == pytest.approx(3000.0) diff --git a/tests/test_inverter.py b/tests/test_inverter.py index 041b00c5..734b9bb1 100644 --- a/tests/test_inverter.py +++ b/tests/test_inverter.py @@ -5,7 +5,9 @@ import pytest from akkudoktoreos.devices.genetic.battery import Battery from akkudoktoreos.devices.genetic.inverter import Inverter, InverterParameters -from akkudoktoreos.optimization.genetic.geneticdevices import SolarPanelBatteryParameters +from akkudoktoreos.optimization.genetic.geneticdevices import ( + SolarPanelBatteryParameters, +) @pytest.fixture @@ -20,7 +22,7 @@ def mock_battery() -> Mock: @pytest.fixture def inverter(mock_battery) -> Inverter: mock_self_consumption_predictor = Mock() - mock_self_consumption_predictor.calculate_self_consumption.return_value = 1.0 + mock_self_consumption_predictor.calculate_expected_direct_consumption.side_effect = min with patch( "akkudoktoreos.devices.genetic.inverter.get_eos_load_interpolator", return_value=mock_self_consumption_predictor, @@ -91,7 +93,7 @@ def test_process_energy_excess_generation(inverter, mock_battery): assert self_consumption == 200.0 # All consumption is met mock_battery.charge_energy.assert_called_once_with(400.0, hour) mock_battery.discharge_energy.assert_not_called() - inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( consumption, generation ) @@ -100,7 +102,8 @@ def test_process_energy_excess_generation_interpolator(inverter, mock_battery): # Battery charges 100 Wh with 10 Wh loss mock_battery.charge_energy.return_value = (100.0, 10.0) mock_battery.discharge_energy.return_value = (20.0, 2.0) - inverter.self_consumption_predictor.calculate_self_consumption.return_value = 0.95 + inverter.self_consumption_predictor.calculate_expected_direct_consumption.side_effect = None + inverter.self_consumption_predictor.calculate_expected_direct_consumption.return_value = 180.0 generation = 600.0 consumption = 200.0 @@ -110,19 +113,71 @@ def test_process_energy_excess_generation_interpolator(inverter, mock_battery): generation, consumption, hour ) - assert grid_export == pytest.approx( - 270.0, rel=1e-2 - ) # 290 Wh feed-in - 5% of generation-consumption self consumption after battery charges + assert grid_export == pytest.approx(300.0, rel=1e-2) assert grid_import == pytest.approx(0.0, rel=1e-2) # No grid draw - assert losses == 12.0 # Battery charging losses - assert self_consumption == 220.0 # All consumption is met - mock_battery.charge_energy.assert_called_once_with(pytest.approx(380.0, rel=1e-2), hour) + assert losses == 22.0 # Battery/inverter losses plus curtailed PV + assert self_consumption == 200.0 # 180 Wh direct PV + 20 Wh battery + mock_battery.charge_energy.assert_called_once_with(pytest.approx(420.0, rel=1e-2), hour) mock_battery.discharge_energy.assert_called_once_with(pytest.approx(20.0, rel=1e-2), hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( consumption, generation ) +def test_probabilistic_bypass_conserves_energy_without_battery(): + predictor = Mock() + predictor.calculate_expected_direct_consumption.return_value = 150.0 + with patch( + "akkudoktoreos.devices.genetic.inverter.get_eos_load_interpolator", + return_value=predictor, + ): + inverter_without_battery = Inverter( + InverterParameters(device_id="inverter", max_power_wh=1000.0) + ) + + generation = 600.0 + consumption = 200.0 + grid_export, grid_import, losses, self_consumption = ( + inverter_without_battery.process_energy(generation, consumption, hour=0) + ) + + assert self_consumption == pytest.approx(150.0) + assert grid_import == pytest.approx(50.0) + assert grid_export == pytest.approx(450.0) + assert losses == 0.0 + assert generation + grid_import == pytest.approx( + consumption + grid_export + losses + ) + + +def test_probabilistic_bypass_conserves_energy_on_quarter_hour_grid(): + predictor = Mock() + predictor.calculate_expected_direct_consumption.return_value = 600.0 + with patch( + "akkudoktoreos.devices.genetic.inverter.get_eos_load_interpolator", + return_value=predictor, + ): + inverter_without_battery = Inverter( + InverterParameters(device_id="inverter", max_power_wh=2000.0), + slot_duration_h=0.25, + ) + + generation = 300.0 # 1200 W over 15 minutes + consumption = 200.0 # 800 W over 15 minutes + grid_export, grid_import, losses, self_consumption = ( + inverter_without_battery.process_energy(generation, consumption, hour=0) + ) + + predictor.calculate_expected_direct_consumption.assert_called_once_with(800.0, 1200.0) + assert self_consumption == pytest.approx(150.0) + assert grid_import == pytest.approx(50.0) + assert grid_export == pytest.approx(150.0) + assert losses == 0.0 + assert generation + grid_import == pytest.approx( + consumption + grid_export + losses + ) + + def test_process_energy_generation_equals_consumption(inverter, mock_battery): generation = 300.0 consumption = 300.0 @@ -139,7 +194,7 @@ def test_process_energy_generation_equals_consumption(inverter, mock_battery): mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_not_called() - inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( consumption, generation ) @@ -163,7 +218,9 @@ def test_process_energy_battery_discharges(inverter, mock_battery): assert self_consumption == 200.0 # Generation + battery discharge mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(150.0, hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( + consumption, generation + ) def test_process_energy_allows_battery_grid_export(inverter, mock_battery): @@ -183,7 +240,7 @@ def test_process_energy_allows_battery_grid_export(inverter, mock_battery): assert losses == 0.0 assert self_consumption == 100.0 mock_battery.discharge_energy.assert_has_calls([call(100.0, 12), call(200.0, 12)]) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_not_called() def test_process_energy_battery_empty(inverter, mock_battery): @@ -203,7 +260,9 @@ def test_process_energy_battery_empty(inverter, mock_battery): assert self_consumption == 100.0 # Only generation is consumed mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(200.0, hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( + consumption, generation + ) def test_process_energy_battery_full_at_start(inverter, mock_battery): @@ -225,7 +284,7 @@ def test_process_energy_battery_full_at_start(inverter, mock_battery): assert self_consumption == 200.0 # Only consumption is met mock_battery.charge_energy.assert_called_once_with(300.0, hour) mock_battery.discharge_energy.assert_not_called() - inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( consumption, generation ) @@ -247,7 +306,9 @@ def test_process_energy_insufficient_generation_no_battery(inverter, mock_batter assert self_consumption == 100.0 # Only generation is consumed mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(400.0, hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( + consumption, generation + ) def test_process_energy_insufficient_generation_battery_assists(inverter, mock_battery): @@ -272,7 +333,9 @@ def test_process_energy_insufficient_generation_battery_assists(inverter, mock_b assert self_consumption == 250.0 # Generation + battery discharge mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(200.0, hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( + consumption, generation + ) def test_process_energy_zero_generation(inverter, mock_battery): @@ -295,7 +358,7 @@ def test_process_energy_zero_generation(inverter, mock_battery): assert self_consumption == 100.0 # Only battery discharge is consumed mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(300.0, hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_not_called() def test_process_energy_zero_consumption(inverter, mock_battery): @@ -315,9 +378,7 @@ def test_process_energy_zero_consumption(inverter, mock_battery): assert self_consumption == 0.0 # Zero consumption mock_battery.charge_energy.assert_called_once_with(500.0, hour) mock_battery.discharge_energy.assert_not_called() - inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( - consumption, generation - ) + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_not_called() def test_process_energy_zero_generation_zero_consumption(inverter, mock_battery): @@ -335,9 +396,7 @@ def test_process_energy_zero_generation_zero_consumption(inverter, mock_battery) assert self_consumption == 0.0 # No consumption mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_not_called() - inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( - consumption, generation - ) + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_not_called() def test_process_energy_partial_battery_discharge(inverter, mock_battery): @@ -358,7 +417,9 @@ def test_process_energy_partial_battery_discharge(inverter, mock_battery): assert self_consumption == 250.0 # Generation + battery discharge mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(200.0, 12) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( + consumption, generation + ) def test_process_energy_consumption_exceeds_max_no_battery(inverter, mock_battery): @@ -378,7 +439,9 @@ def test_process_energy_consumption_exceeds_max_no_battery(inverter, mock_batter assert self_consumption == 100.0 # Only the generation is consumed, maxing out the inverter mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(400.0, hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_called_once_with( + consumption, generation + ) def test_process_energy_zero_generation_full_battery_high_consumption(inverter, mock_battery): @@ -400,4 +463,4 @@ def test_process_energy_zero_generation_full_battery_high_consumption(inverter, assert self_consumption == 500.0 # Battery fully discharges to meet consumption mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(500.0, hour) - inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() + inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_not_called() diff --git a/tests/test_inverter_efficiency.py b/tests/test_inverter_efficiency.py index 2cd7b4e2..073fc0ca 100644 --- a/tests/test_inverter_efficiency.py +++ b/tests/test_inverter_efficiency.py @@ -38,7 +38,7 @@ def _make_inverter( ) -> Inverter: """Create an Inverter with custom efficiency parameters and a mock battery.""" mock_self_consumption_predictor = Mock() - mock_self_consumption_predictor.calculate_self_consumption.return_value = 1.0 + mock_self_consumption_predictor.calculate_expected_direct_consumption.side_effect = min params = InverterParameters( device_id="inv1", @@ -165,12 +165,14 @@ class TestDcToAcEfficiency: assert losses == pytest.approx(10.0, rel=1e-5) # Only battery losses def test_discharge_surplus_path_with_efficiency(self, mock_battery): - """When generation > consumption but SCR < 1, discharge goes through inverter.""" - mock_battery.discharge_energy.return_value = (50.0, 5.0) + """A probabilistic load gap discharges through the inverter.""" + mock_battery.discharge_energy.return_value = (30.0 / 0.90, 5.0) mock_battery.charge_energy.return_value = (100.0, 10.0) inv = _make_inverter(dc_to_ac_efficiency=0.90, mock_battery=mock_battery) - cast(Mock, inv.self_consumption_predictor).calculate_self_consumption.return_value = 0.90 + predictor = cast(Mock, inv.self_consumption_predictor) + predictor.calculate_expected_direct_consumption.side_effect = None + predictor.calculate_expected_direct_consumption.return_value = 170.0 generation = 500.0 consumption = 200.0 @@ -180,18 +182,23 @@ class TestDcToAcEfficiency: generation, consumption, hour ) - # surplus = 300, remaining_power = 300*0.9 = 270, remaining_load_evq = 300*0.1 = 30 - # DC request for discharge = 30 / 0.90 = 33.333 + # Expected direct PV is 170 Wh, leaving 30 Wh of load gap and + # 330 Wh of PV surplus within different sub-periods of the slot. + # DC request for discharge = 30 / 0.90 = 33.333 Wh. expected_dc_request = 30.0 / 0.90 mock_battery.discharge_energy.assert_called_once_with( pytest.approx(expected_dc_request, rel=1e-3), hour ) - # Battery delivers 50 Wh DC → 45 Wh AC - from_battery_ac = 50.0 * 0.90 # 45 Wh - inverter_discharge_loss = 50.0 - from_battery_ac # 5 Wh + # Battery delivers 33.333 Wh DC -> 30 Wh AC. + from_battery_dc = 30.0 / 0.90 + from_battery_ac = from_battery_dc * 0.90 + inverter_discharge_loss = from_battery_dc - from_battery_ac - assert self_consumption == pytest.approx(consumption + from_battery_ac, rel=1e-5) + assert self_consumption == pytest.approx(170.0 + from_battery_ac, rel=1e-5) + assert grid_import == pytest.approx(0.0) + assert grid_export == pytest.approx(220.0) + assert losses == pytest.approx(5.0 + inverter_discharge_loss + 10.0) # =================================================================== @@ -492,6 +499,7 @@ def _make_mock_simulation( initial_soc_percentage: float = 0.0, # fraction of capacity already stored (0 = empty) min_soc_wh: float = 0.0, max_charge_power_w: float = 5_000.0, + levelized_cost_of_storage_kwh: float = 0.0, # Arrays (must be same length) ac_charge_hours: list | None = None, elect_price_hourly: list | None = None, @@ -517,6 +525,7 @@ def _make_mock_simulation( initial_soc_percentage=initial_soc_percentage, min_soc_wh=min_soc_wh, max_charge_power_w=max_charge_power_w, + levelized_cost_of_storage_kwh=levelized_cost_of_storage_kwh, current_energy_content=Mock(return_value=0.0), ) @@ -744,6 +753,28 @@ class TestAcChargeBreakEvenPenalty: # Fitness must be worse (higher) than base assert fitness > base + 1e-6 + def test_lcos_is_included_in_ac_charge_break_even_price(self, config_eos): + """LCOS can make an otherwise profitable price spread unprofitable.""" + n = 24 + prices = [0.0001] + [0.00015] * (n - 1) + sim = _make_mock_simulation( + ac_to_dc_efficiency=1.0, + dc_to_ac_efficiency=1.0, + charging_efficiency=1.0, + discharging_efficiency=1.0, + levelized_cost_of_storage_kwh=0.10, + ac_charge_hours=[1.0] + [0.0] * (n - 1), + elect_price_hourly=prices, + load_energy_array=[1000.0] * n, + initial_soc_percentage=0.0, + ) + + fitness = _run_evaluate_with_mocked_sim(config_eos, sim) + + # Break-even is 0.0001 + 0.0001 = 0.0002 EUR/Wh. + # The 0.00005 EUR/Wh gap on a 5000 Wh charge adds 0.25 EUR. + assert fitness == pytest.approx(0.25) + # ----------------------------------------------------------------- # 5d. Free PV energy covers expensive hours → penalty reduced/eliminated # ----------------------------------------------------------------- diff --git a/tests/test_optimization_settings.py b/tests/test_optimization_settings.py new file mode 100644 index 00000000..7cde0315 --- /dev/null +++ b/tests/test_optimization_settings.py @@ -0,0 +1,30 @@ +from akkudoktoreos.devices.devices import BatteriesCommonSettings +from akkudoktoreos.optimization.genetic.geneticdevices import ( + SolarPanelBatteryParameters, +) +from akkudoktoreos.optimization.optimization import OptimizationCommonSettings + + +def test_terminal_value_is_independent_from_battery_lcos(): + battery = BatteriesCommonSettings( + device_id="battery1", + levelized_cost_of_storage_kwh=0.12, + ) + optimization = OptimizationCommonSettings(terminal_value_euro_per_kwh=0.20) + + assert battery.levelized_cost_of_storage_kwh == 0.12 + assert optimization.terminal_value_euro_per_kwh == 0.20 + + +def test_terminal_value_defaults_to_zero(): + assert OptimizationCommonSettings().terminal_value_euro_per_kwh == 0.0 + + +def test_genetic_battery_lcos_is_independent_from_terminal_value(): + battery = SolarPanelBatteryParameters( + device_id="battery1", + capacity_wh=8000, + levelized_cost_of_storage_kwh=0.12, + ) + + assert battery.levelized_cost_of_storage_kwh == 0.12 diff --git a/tests/testdata/optimize_result_1.json b/tests/testdata/optimize_result_1.json index 12b01bf9..7110b93d 100644 --- a/tests/testdata/optimize_result_1.json +++ b/tests/testdata/optimize_result_1.json @@ -157,7 +157,7 @@ 1063.91, 1320.56, 1132.03, - 1308.5200000002487, + 1308.5200000000004, 1176.82, 1216.22, 1103.78, @@ -238,10 +238,12 @@ 0.0, 0.0, 0.0, - 0.20303854854278033, - 0.1899652543898917, - 0.12833851757957424, - 0.04233866391809883, + 0.21077535122459587, + 0.19320652266312205, + 0.1358062627100041, + 0.0692592282596561, + 0.023370695807696434, + 0.0019327051509204403, 0.0, 0.0, 0.0, @@ -260,22 +262,20 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.16357655037574115, - 0.14900060381217387, - 0.08949503544160814, - 0.010110585812541543, - 0.0, - 0.0, + 0.18814608875566813, + 0.15236390731688437, + 0.10316291465819699, + 0.029150936607659956, + 0.020928608511559126, + 0.003524558735906156, 0.0, 0.0, 0.0 ], - "Gesamt_Verluste": 2807.7292841655817, - "Gesamtbilanz_Euro": 0.8879905947253857, - "Gesamteinnahmen_Euro": 0.9758637598724098, - "Gesamtkosten_Euro": 1.8638543545977955, + "Gesamt_Verluste": 3425.4668727209255, + "Gesamtbilanz_Euro": 0.9585224311392879, + "Gesamteinnahmen_Euro": 1.1316277804018695, + "Gesamtkosten_Euro": 2.0901502115411574, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -320,14 +320,14 @@ 0.0, 0.0, 0.0, - 0.001880612819166666, - 0.026623430000091274, - 4.482711108977355e-14, - 0.008763569215739961, - 0.018335381563380656, - 0.06267084962493971, - 0.05480703000000003, - 0.225316611, + 0.07557452231671152, + 0.026623430000000066, + 4.55656845588237e-17, + 0.001414013162203277, + 0.005881449073870462, + 0.05258762370598476, + 0.1614775630079859, + 0.2338232746714084, 0.0, 0.26650619799999997, 0.19588158, @@ -343,15 +343,15 @@ 0.0, 0.0, 0.0, - 0.02844719513362201, + 0.009619897970888898, 0.0, 0.0, - 0.0004412281431084693, - 0.008372170029774037, - 0.03130999506792791, + 2.3325608707865465e-05, + 0.0021886029750169123, + 0.013012984677295973, 0.0, - 0.08231598, - 0.174597189, + 0.17784012884918773, + 0.19011028252189552, 0.0, 0.0, 0.16484566 @@ -360,14 +360,14 @@ 0.0, 0.0, 0.0, - 10.008583390988111, - 144.8500000004966, - 2.236881790906864e-10, - 39.870651572975255, - 80.77260600608219, - 209.11194402715952, - 171.54000000000008, - 731.31, + 402.20607938643707, + 144.85000000000036, + 2.2737367544323206e-13, + 6.433180901743754, + 25.909467285772962, + 175.4675465665157, + 505.40708296709204, + 758.9200735845777, 0.0, 912.38, 704.61, @@ -383,15 +383,15 @@ 0.0, 0.0, 0.0, - 135.91588692604878, + 45.96224544141853, 0.0, 0.0, - 2.201737241060226, - 38.089945540373236, - 137.92949369131236, + 0.11639525303326081, + 9.957247384062384, + 57.32592368852852, 0.0, - 257.64, - 566.69, + 556.6201215937018, + 617.0408390843736, 0.0, 0.0, 592.97 @@ -402,10 +402,12 @@ 0.0, 0.0, 0.0, - 2900.550693468291, - 2713.7893484270244, - 1833.4073939939178, - 604.8380559728405, + 3011.0764460656555, + 2760.093180901744, + 1940.089467285773, + 989.4175465665157, + 333.86708296709196, + 27.61007358457772, 0.0, 0.0, 0.0, @@ -424,31 +426,29 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 2336.807862510588, - 2128.580054459627, - 1278.5005063086878, - 144.4369401791649, - 0.0, - 0.0, + 2687.8012679381163, + 2176.6272473840627, + 1473.7559236885286, + 416.4419515379994, + 298.9801215937018, + 50.35083908437366, 0.0, 0.0, 0.0 ], "Verluste_Pro_Stunde": [ - 16.744090909090914, - 2.817272727272737, - 29.157272727272726, - 2.358169993081436, - 600.0000000000002, - 173.00391678377832, + 97.85621559422765, + 101.92059640365329, + 114.42806532440363, + 51.82392952637247, + 599.9999999999995, + 159.7408264721214, 0.0, 0.0, 0.0, 0.0, 0.0, - 133.72909090909081, + 133.7321802766326, 0.0, 0.0, 70.41409090909087, @@ -458,18 +458,18 @@ 0.0, 0.0, 0.0, - 109.0704545454546, - 109.96227272727276, - 47.952272727272714, - 16.031648664443644, - 55.9754990365584, - 143.33449356887422, - 21.973794868109557, - 538.2984000000038, - 161.2428480298022, + 109.07302361034766, + 116.99491129525349, + 95.61900944932736, + 98.21987178167876, + 123.8591383343284, + 165.15986945297027, + 116.9350623689079, + 538.2984000000001, + 119.40181527778998, 0.0, 0.0, - 44.91187685729284, + 81.2380484620021, 0.0, 0.0, 134.59227272727276, @@ -478,35 +478,35 @@ ], "akku_soc_pro_stunde": [ 80.0, - 79.4714617768595, - 79.38253271349862, - 78.46216425619835, - 78.52766897822838, - 95.19433564489505, + 79.16421888032488, + 78.69989843940196, + 77.45653455559739, + 78.89608815355218, + 95.56275482021886, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, - 95.77875344352617, - 95.77875344352617, - 95.77875344352617, - 93.55608643250687, - 89.8196453168044, - 86.83092286501376, - 84.21044249311292, - 84.21044249311292, - 84.21044249311292, - 84.21044249311292, - 80.76756198347105, - 77.2965306473829, - 75.78288567493111, - 75.93522642877453, - 76.44194846937079, - 80.42346217961729, - 80.56829866584056, - 95.52103199917215, + 95.77874174137638, + 95.77874174137638, + 95.77874174137638, + 93.5560747303571, + 89.81963361465462, + 86.83091116286398, + 84.21043079096314, + 84.21043079096314, + 84.21043079096314, + 84.21043079096314, + 80.76754055001486, + 77.26987043147221, + 75.57566963810356, + 75.69097315408206, + 76.9218097513005, + 81.5095839027719, + 81.73054957561693, + 96.68328290895028, 100.0, 100.0, 100.0, @@ -758,4 +758,4 @@ 0.0 ], "washingstart": null -} +} \ No newline at end of file diff --git a/tests/testdata/optimize_result_1_be.json b/tests/testdata/optimize_result_1_be.json index c2772055..bdead719 100644 --- a/tests/testdata/optimize_result_1_be.json +++ b/tests/testdata/optimize_result_1_be.json @@ -157,7 +157,7 @@ 1063.91, 1320.56, 1132.03, - 1308.5200000002487, + 1308.5200000000004, 1176.82, 1216.22, 1103.78, @@ -238,10 +238,12 @@ 0.0, 0.0, 0.0, - 0.20303854854278033, - 0.1899652543898917, - 0.12833851757957424, - 0.04233866391809883, + 0.21077535122459587, + 0.19320652266312205, + 0.1358062627100041, + 0.0692592282596561, + 0.023370695807696434, + 0.0019327051509204403, 0.0, 0.0, 0.0, @@ -260,22 +262,20 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.16452297290911175, - 0.1455575560489687, - 0.08949503544160814, - 0.024046008596276005, - 0.0, - 0.0, + 0.24478962017661132, + 0.15146384621553569, + 0.10316291465819699, + 0.05435777788576338, + 0.020928608511559126, + 0.003524558735906156, 0.0, 0.0, 0.0 ], - "Gesamt_Verluste": 2717.1309464905708, - "Gesamtbilanz_Euro": 0.998163925609791, - "Gesamteinnahmen_Euro": 0.9873025574263097, - "Gesamtkosten_Euro": 1.9854664830361006, + "Gesamt_Verluste": 3110.842855499046, + "Gesamtbilanz_Euro": 1.1850381627731374, + "Gesamteinnahmen_Euro": 1.2125780919995675, + "Gesamtkosten_Euro": 2.397616254772705, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -320,14 +320,14 @@ 0.0, 0.0, 0.0, - 0.001880612819166666, - 0.026623430000091274, - 4.482711108977355e-14, - 0.008763569215739961, - 0.018335381563380656, - 0.06267084962493971, - 0.05480703000000003, - 0.225316611, + 0.07557452231671152, + 0.026623430000000066, + 4.55656845588237e-17, + 0.001414013162203277, + 0.005881449073870462, + 0.05258762370598476, + 0.1614775630079859, + 0.2338232746714084, 0.0, 0.26650619799999997, 0.19588158, @@ -340,18 +340,18 @@ 0.0, 0.0, 0.0, - 0.08545095, - 0.008254784724581705, - 0.028650916976410694, - 0.02844719513362201, + 0.1306329312971816, + 0.07362195915902499, + 0.060401289430882174, + 0.009619897970888898, 0.0, 0.0, - 0.0004412281431084693, + 2.3325608707865465e-05, 0.0, - 0.03130999506792791, - 0.04620342776708689, - 0.08231598, - 0.174597189, + 0.013012984677295973, + 0.08357424731947552, + 0.17784012884918773, + 0.19011028252189552, 0.293043269, 0.0, 0.0 @@ -360,14 +360,14 @@ 0.0, 0.0, 0.0, - 10.008583390988111, - 144.8500000004966, - 2.236881790906864e-10, - 39.870651572975255, - 80.77260600608219, - 209.11194402715952, - 171.54000000000008, - 731.31, + 402.20607938643707, + 144.85000000000036, + 2.2737367544323206e-13, + 6.433180901743754, + 25.909467285772962, + 175.4675465665157, + 505.40708296709204, + 758.9200735845777, 0.0, 912.38, 704.61, @@ -380,18 +380,18 @@ 0.0, 0.0, 0.0, - 351.65, - 36.20519616044607, - 129.52494112301397, - 135.91588692604878, + 537.58407941227, + 322.9033296448464, + 273.0618871197205, + 45.96224544141853, 0.0, 0.0, - 2.201737241060226, + 0.11639525303326081, 0.0, - 137.92949369131236, - 154.1655914817714, - 257.64, - 566.69, + 57.32592368852852, + 278.85968408233407, + 556.6201215937018, + 617.0408390843736, 987.01, 0.0, 0.0 @@ -402,10 +402,12 @@ 0.0, 0.0, 0.0, - 2900.550693468291, - 2713.7893484270244, - 1833.4073939939178, - 604.8380559728405, + 3011.0764460656555, + 2760.093180901744, + 1940.089467285773, + 989.4175465665157, + 333.86708296709196, + 27.61007358457772, 0.0, 0.0, 0.0, @@ -424,31 +426,29 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 2350.3281844158823, - 2079.39365784241, - 1278.5005063086878, - 343.51440851822866, - 0.0, - 0.0, + 3496.9945739515906, + 2163.76923165051, + 1473.7559236885286, + 776.5396840823341, + 298.9801215937018, + 50.35083908437366, 0.0, 0.0, 0.0 ], "Verluste_Pro_Stunde": [ - 16.744090909090914, - 2.817272727272737, - 29.157272727272726, - 2.358169993081436, - 600.0000000000002, - 173.00391678377832, + 97.85621559422765, + 101.92059640365329, + 114.42806532440363, + 51.82392952637247, + 599.9999999999995, + 159.7408264721214, 0.0, 0.0, 0.0, 0.0, 0.0, - 133.72909090909081, + 133.7321802766326, 0.0, 0.0, 70.41409090909087, @@ -458,16 +458,16 @@ 0.0, 0.0, 69.12409090909085, - 109.0704545454546, - 109.96227272727276, - 0.0, - 11.094576460746453, - 38.31300706523831, - 143.33449356887422, - 21.973794868109557, - 538.2984000000038, - 159.62040940116685, - 11.096451076844168, + 109.07302361034766, + 116.99491129525349, + 22.312089529472388, + 54.18759955738153, + 86.6234264543665, + 165.15986945297027, + 116.9350623689079, + 538.2984000000001, + 22.298618556173096, + 2.900768349489402, 0.0, 0.0, 0.0, @@ -478,35 +478,35 @@ ], "akku_soc_pro_stunde": [ 80.0, - 79.4714617768595, - 79.38253271349862, - 78.46216425619835, - 78.52766897822838, - 95.19433564489505, + 79.16421888032488, + 78.69989843940196, + 77.45653455559739, + 78.89608815355218, + 95.56275482021886, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, - 95.77875344352617, - 95.77875344352617, - 95.77875344352617, - 93.55608643250687, - 89.8196453168044, - 86.83092286501376, - 84.21044249311292, - 84.21044249311292, - 84.21044249311292, - 82.02849517906333, - 78.58561466942146, - 75.1145833333333, - 75.1145833333333, - 75.42276601279848, - 76.4870162090551, - 80.4685299193016, - 80.61336640552487, - 95.56609973885648, + 95.77874174137638, + 95.77874174137638, + 95.77874174137638, + 93.5560747303571, + 89.81963361465462, + 86.83091116286398, + 84.21043079096314, + 84.21043079096314, + 84.21043079096314, + 82.02848347691355, + 78.58559323596526, + 75.08792311742263, + 75.7077033821302, + 77.21291448094635, + 79.61912077134542, + 84.20689492281682, + 84.42786059566187, + 99.38059392899518, 100.0, 100.0, 100.0, @@ -758,4 +758,4 @@ 1.0 ], "washingstart": null -} +} \ No newline at end of file diff --git a/tests/testdata/optimize_result_2.json b/tests/testdata/optimize_result_2.json index 618e5843..89d7236a 100644 --- a/tests/testdata/optimize_result_2.json +++ b/tests/testdata/optimize_result_2.json @@ -204,18 +204,18 @@ "Last_Wh_pro_Stunde": [ 1053.07, 10240.91, - 14186.56, + 14186.477801352086, 11620.03, - 11218.67, + 10686.11504084929, 7609.82, 9082.22, 10280.78, 2177.92, 1178.71, 1050.98, - 1988.56, + 1488.5587949275546, 912.38, - 1704.6100000000001, + 2204.61, 516.37, 868.05, 694.34, @@ -288,6 +288,10 @@ 0.0, 0.0, 0.0, + 0.005594705401588846, + 0.0016437871377312284, + 0.028240415856282213, + 0.023370695807696434, 0.0, 0.0, 0.0, @@ -306,25 +310,21 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.00826914812545985, - 0.25743585772309185, - 0.1455575560489687, - 0.07702723513376727, - 0.010110585812541543, - 0.0, - 0.0, + 0.09023487592359272, + 0.2577866264025879, + 0.15146384621553569, + 0.09798107754792196, + 0.029150936607659956, + 0.020928608511559126, + 0.003524558735906156, 0.0, 0.0, 0.0 ], - "Gesamt_Verluste": 5776.448801897527, - "Gesamtbilanz_Euro": 12.789452797857164, - "Gesamteinnahmen_Euro": 0.49840038284382926, - "Gesamtkosten_Euro": 13.287853180700994, + "Gesamt_Verluste": 6008.882879266785, + "Gesamtbilanz_Euro": 13.081213953988335, + "Gesamteinnahmen_Euro": 0.7099201341480623, + "Gesamtkosten_Euro": 13.791134088136397, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -370,16 +370,16 @@ 2.034522392, 2.737606326, 1.9651220859999998, - 0.9557324300000001, - 0.4189863, - 1.1236923319999998, - 1.64866014, - 0.07038454500000005, - 0.05480703000000003, + 0.9176848434271027, + 0.5064650351737862, + 1.1459236583154115, + 1.6539907068609285, + 0.1912938683161112, + 0.1614775630079859, 0.0, - 0.588063892, + 0.4396171120740812, 0.0, - 0.47388158, + 0.61288158, 0.174739608, 0.28801899, 0.0, @@ -390,8 +390,8 @@ 0.0, 0.0, 0.0, - 0.008254784724581705, - 0.028650916976410694, + 0.07362195915902499, + 0.060401289430882174, 0.0, 0.0, 0.0, @@ -399,8 +399,8 @@ 0.0, 0.0, 0.0, - 0.08231598, - 0.174597189, + 0.17784012884918773, + 0.19011028252189552, 0.293043269, 0.0, 0.0 @@ -410,16 +410,16 @@ 9197.66, 13079.82, 10458.34, - 5199.85, - 2090.75, - 5112.339999999999, - 7262.820000000001, - 234.85000000000014, - 171.54000000000008, + 4992.84463235638, + 2527.2706345997312, + 5213.483431826258, + 7286.3026733961615, + 638.2845122326032, + 505.40708296709204, 0.0, - 1980.6799999999998, + 1480.6908456520082, 0.0, - 1704.6100000000001, + 2204.61, 516.37, 868.05, 0.0, @@ -430,8 +430,8 @@ 0.0, 0.0, 0.0, - 36.20519616044607, - 129.52494112301397, + 322.9033296448464, + 273.0618871197205, 0.0, 0.0, 0.0, @@ -439,8 +439,8 @@ 0.0, 0.0, 0.0, - 257.64, - 566.69, + 556.6201215937018, + 617.0408390843736, 987.01, 0.0, 0.0 @@ -452,6 +452,10 @@ 0.0, 0.0, 0.0, + 79.92436287984066, + 23.482673396160408, + 403.4345122326031, + 333.86708296709196, 0.0, 0.0, 0.0, @@ -470,36 +474,32 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 118.1306875065693, - 3677.655110329884, - 2079.39365784241, - 1100.3890733395326, - 144.4369401791649, - 0.0, - 0.0, + 1289.0696560513247, + 3682.6660914655417, + 2163.76923165051, + 1399.729679256028, + 416.4419515379994, + 298.9801215937018, + 50.35083908437366, 0.0, 0.0, 0.0 ], "Verluste_Pro_Stunde": [ - 16.744090909090914, + 97.85621559422765, 483.0, - 1014.0, + 1014.0000000000001, 552.0, - 465.0, - 207.0, - 414.0, + 440.15935588276557, + 259.38247615196775, + 416.54628827357027, 483.0, 55.200000000000045, 0.0, - 99.72409090909093, - 120.0, + 106.80230977350088, + 60.001301478240954, 124.41545454545451, - 120.0, + 180.0, 0.0, 0.0, 94.68272727272722, @@ -507,18 +507,18 @@ 75.86045454545456, 66.66681818181814, 0.0, - 109.0704545454546, - 109.96227272727276, - 47.952272727272714, - 11.094576460746453, - 38.31300706523831, - 161.86847814969906, - 21.973794868109557, - 524.1227174992155, - 0.6414151879949013, - 11.096451076844168, - 40.181939277841224, - 44.91187685729284, + 109.07302361034766, + 116.99491129525349, + 95.61900944932736, + 54.18759955738153, + 86.6234264543665, + 171.42744837680007, + 116.9350623689079, + 383.6100412738411, + 0.03390853445803674, + 2.900768349489402, + 16.700320743972142, + 81.2380484620021, 0.0, 0.0, 0.0, @@ -527,34 +527,34 @@ ], "akku_soc_pro_stunde": [ 80.0, - 79.4714617768595, - 79.4714617768595, - 96.13812844352617, - 96.13812844352617, - 99.4714617768595, - 99.4714617768595, - 99.4714617768595, - 99.4714617768595, - 99.4714617768595, - 99.4714617768595, - 96.32360537190083, - 99.65693870523415, - 95.72968319559227, - 99.0630165289256, - 99.0630165289256, - 99.0630165289256, - 96.07429407713497, - 93.45381370523414, - 91.05922865013771, - 88.95484676308537, - 88.95484676308537, - 85.51196625344349, - 82.04093491735533, - 80.52728994490354, - 80.83547262436872, - 81.89972282062534, - 85.29619913880036, - 85.44103562502363, + 79.16421888032488, + 79.16421888032488, + 95.83088554699157, + 95.83088554699157, + 98.47420098817949, + 99.9292697701786, + 100.0, + 100.0, + 100.0, + 100.0, + 96.82533215994886, + 98.49203497878888, + 94.56477946914701, + 99.564779469147, + 99.564779469147, + 99.564779469147, + 96.57605701735636, + 93.95557664545554, + 91.56099159035911, + 89.45660970330677, + 89.45660970330677, + 86.01371946235848, + 82.51604934381585, + 80.82184855044719, + 82.32705964926333, + 84.7332659396624, + 89.12319984732603, + 89.34416552017109, 100.0, 100.0, 100.0, @@ -856,4 +856,4 @@ 14.0 ], "washingstart": 14 -} +} \ No newline at end of file diff --git a/tests/testdata/optimize_result_2_be.json b/tests/testdata/optimize_result_2_be.json index a7db2f39..ecacac1a 100644 --- a/tests/testdata/optimize_result_2_be.json +++ b/tests/testdata/optimize_result_2_be.json @@ -204,22 +204,22 @@ "Last_Wh_pro_Stunde": [ 16541.07, 8929.91, - 8875.56, + 8375.540038207693, 6376.03, 5096.67, 12853.82, 8960.220000000001, - 13969.78, + 13936.309375923789, 1129.12, 1178.71, 1050.98, 988.56, - 1912.38, + 1412.38, 704.61, 516.37, 868.05, 694.34, - 2108.79, + 2608.79, 556.31, 488.89, 506.91, @@ -286,7 +286,12 @@ 0.0, 0.0, 0.0, - 0.06455049999999336, + 0.024981227816847, + 0.0, + 0.0, + 0.0, + 0.038217249326498136, + 0.023370695807696434, 0.0, 0.0, 0.0, @@ -305,26 +310,21 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.12279079241545988, - 0.25743585772309185, - 0.1455575560489687, - 0.07702723513376727, - 0.024046008596276005, + 0.19904591069188757, + 0.2577866264025879, + 0.15146384621553569, + 0.09798107754792196, + 0.05435777788576338, 0.0, 0.0, 0.0, 0.0, 0.0 ], - "Gesamt_Verluste": 6842.366945701403, - "Gesamtbilanz_Euro": 14.10088133917546, - "Gesamteinnahmen_Euro": 0.6914079499175572, - "Gesamtkosten_Euro": 14.792289289093018, + "Gesamt_Verluste": 7095.270423117038, + "Gesamtbilanz_Euro": 14.155149367775268, + "Gesamteinnahmen_Euro": 0.847204411694738, + "Gesamtkosten_Euro": 15.002353779470006, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -367,80 +367,80 @@ ], "Kosten_Euro_pro_Stunde": [ 3.55926012, - 1.7445291920000001, - 1.6260140259999998, - 0.979774486, + 1.7445413792641737, + 1.5214016280281666, + 0.9799189133780641, 0.0, - 0.5881238999999999, - 1.0968767320000004, + 0.6146086578009714, + 1.120219902993293, 2.4860631399999997, - 0.06267084962493971, - 0.05480703000000003, + 0.05258762370598476, + 0.1614775630079859, 0.0, - 0.291163892, - 0.558606198, + 0.29116746986009023, + 0.41255619800000004, 0.0, 0.174739608, 0.28801899, 0.0, - 0.692315757, + 0.856465757, 0.0, 0.0, 0.16677339, 0.0, 0.0, 0.0, - 0.008254784724581705, - 0.028650916976410694, + 0.07362195915902499, + 0.060401289430882174, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.04620342776708689, + 0.08357424731947552, 0.0, - 0.174597189, + 0.19011028252189552, 0.0, 0.0, 0.16484566 ], "Netzbezug_Wh_pro_Stunde": [ 15610.789999999999, - 7886.66, - 7768.82, - 5214.34, + 7886.7150961309835, + 7268.9996561307535, + 5215.108639585227, 0.0, - 2934.75, - 4990.340000000001, + 3066.9094700647274, + 5096.541869851197, 10951.82, - 209.11194402715952, - 171.54000000000008, + 175.4675465665157, + 505.40708296709204, 0.0, - 980.68, - 1912.38, + 980.6920507244535, + 1412.38, 0.0, 516.37, 868.05, 0.0, - 2108.79, + 2608.79, 0.0, 0.0, 506.91, 0.0, 0.0, 0.0, - 36.20519616044607, - 129.52494112301397, + 322.9033296448464, + 273.0618871197205, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 154.1655914817714, + 278.85968408233407, 0.0, - 566.69, + 617.0408390843736, 0.0, 0.0, 592.97 @@ -450,7 +450,12 @@ 0.0, 0.0, 0.0, - 922.1499999999053, + 356.87468309781434, + 0.0, + 0.0, + 0.0, + 545.9607046642591, + 333.86708296709196, 0.0, 0.0, 0.0, @@ -469,16 +474,11 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1754.1541773637127, - 3677.655110329884, - 2079.39365784241, - 1100.3890733395326, - 343.51440851822866, + 2843.513009884108, + 3682.6660914655417, + 2163.76923165051, + 1399.729679256028, + 776.5396840823341, 0.0, 0.0, 0.0, @@ -487,40 +487,40 @@ ], "Verluste_Pro_Stunde": [ 1152.0, - 414.0, - 465.0, - 276.0, - 207.00000000001197, - 1083.0, - 276.0, - 1014.0, - 72.5805667167408, + 414.0066115357181, + 405.0215587356905, + 276.0922367502273, + 333.15830172751845, + 1098.8591364077674, + 288.74422438214356, + 1013.9999999999997, + 53.21482102827082, 0.0, - 99.72409090909093, - 0.0, - 120.0, + 106.80230977350088, + 0.0014460869344160802, + 60.0, 96.08318181818186, 0.0, 0.0, 94.68272727272722, - 180.0, + 240.0, 75.86045454545456, 66.66681818181814, 0.0, - 109.0704545454546, - 109.96227272727276, - 47.952272727272714, - 11.094576460746453, - 38.31300706523831, - 161.86847814969906, - 21.973794868109557, - 327.79989871635826, - 0.6414151879949013, - 11.096451076844168, - 40.181939277841224, - 0.0, - 35.132727272727266, + 109.07302361034766, + 116.99491129525349, + 95.61900944932736, + 54.18759955738153, + 86.6234264543665, + 171.42744837680007, + 116.9350623689079, + 197.07683881390702, + 0.03390853445803674, + 2.900768349489402, + 16.700320743972142, 0.0, + 111.78035844493081, + 6.04210069012484, 134.59227272727276, 100.08954545454549, 0.0 @@ -528,42 +528,42 @@ "akku_soc_pro_stunde": [ 80.0, 96.66666666666667, - 96.66666666666667, + 96.66685032043661, + 98.33411584087247, + 98.33667797282322, + 100.0, + 81.50113762748849, + 81.85514386032581, + 98.52181052699248, 100.0, 100.0, - 100.0, - 81.06060606060606, - 81.06060606060606, - 97.72727272727273, - 99.74339958051553, - 99.74339958051553, - 96.59554317555684, - 96.59554317555684, - 99.92887650889017, - 96.89594778988192, - 96.89594778988192, - 96.89594778988192, - 93.90722533809128, - 98.90722533809128, - 96.51264028299485, - 94.40825839594251, - 94.40825839594251, - 90.96537788630063, - 87.49434655021247, - 85.9807015777607, - 86.28888425722586, - 87.35313445348248, - 90.7496107716575, - 90.89444725788077, + 96.82533215994886, + 96.82537232903037, + 98.49203899569704, + 95.45911027668879, + 95.45911027668879, + 95.45911027668879, + 92.47038782489815, + 99.13705449156481, + 96.7424694364684, + 94.63808754941604, + 94.63808754941604, + 91.19519730846775, + 87.69752718992511, + 86.00332639655647, + 87.50853749537262, + 89.91474378577168, + 94.30467769343532, + 94.52564336628036, 100.0, 100.0, 100.0, 100.0, 100.0, - 98.89101239669421, - 98.89101239669421, - 94.64251893939394, - 91.48312672176309 + 98.60068046043587, + 98.76851659071711, + 94.52002313341684, + 91.360630915786 ], "Electricity_price": [ 0.000228, @@ -856,4 +856,4 @@ 15.0 ], "washingstart": 15 -} +} \ No newline at end of file diff --git a/tests/testdata/optimize_result_2_full.json b/tests/testdata/optimize_result_2_full.json index 357757c2..7bcc954c 100644 --- a/tests/testdata/optimize_result_2_full.json +++ b/tests/testdata/optimize_result_2_full.json @@ -111,8 +111,8 @@ 0, 0, 0, - 1, 0, + 1, 0, 0, 0, @@ -123,6 +123,23 @@ 1, 1, 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, 0, 1, 1, @@ -130,87 +147,71 @@ 1, 1, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1 ], + "battery_grid_export_allowed": [], "eautocharge_hours_float": [ - 0.875, - 0.75, - 0.5, - 1.0, - 0.625, - 0.625, - 0.875, - 0.625, - 0.875, - 0.875, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.375, + 0.375, + 0.875, 1.0, - 0.5, - 0.625, - 1.0, - 0.75, - 0.5, + 0.875, 0.625, 0.375, - 0.5, - 0.625, - 0.625, - 0.5, - 0.625, - 0.625, - 0.0, - 0.625, - 0.5, - 0.5, - 1.0, 0.375, - 0.625, - 0.875, - 0.5, - 0.5, - 0.75, - 0.75, - 0.75, + 0.375, + 0.1, 0.0, - 0.875, - 0.75, - 1.0, - 1.0, - 0.75, - 0.625, - 0.875, - 0.5, - 0.875 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], "result": { "Last_Wh_pro_Stunde": [ - 1053.07, - 11551.91, - 6564.5599999999995, - 7687.03, - 14151.67, - 11542.82, - 6460.22, - 7658.78, + 4986.07, + 4996.91, + 12997.56, + 14120.029999999999, + 10340.67, + 7731.82, + 5149.22, + 5036.78, 5062.12, - 1178.71, + 2227.51, 1050.98, 988.56, 912.38, @@ -242,43 +243,43 @@ ], "EAuto_SoC_pro_Stunde": [ 5.0, - 5.0, - 22.48, - 31.22, - 42.144999999999996, - 59.62499999999999, - 72.735, - 81.475, - 92.4, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955, - 98.955 + 11.555, + 18.11, + 33.405, + 50.885000000000005, + 66.18, + 77.105, + 83.66, + 90.215, + 96.77, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518 ], "Einnahmen_Euro_pro_Stunde": [ 0.0, @@ -320,13 +321,11 @@ 0.0, 0.0 ], - "Gesamt_Verluste": 7647.623819992857, - "Gesamtbilanz_Euro": 7.824605715847156, + "Gesamt_Verluste": 8404.44594732788, + "Gesamtbilanz_Euro": 7.836546975121494, "Gesamteinnahmen_Euro": 0.0, - "Gesamtkosten_Euro": 7.824605715847156, + "Gesamtkosten_Euro": 7.836546975121494, "Home_appliance_wh_per_hour": [ - 0.0, - 0.0, 0.0, 0.0, 2500.0, @@ -362,23 +361,19 @@ 0.0, 0.0, 0.0, + 0.0, + 0.0, 0.0 ], "Kosten_Euro_pro_Stunde": [ - 0.027996119999999992, - 1.351235592, - 1.1423217259999998, - 1.226111386, - 1.4948178300000001, - 1.2071595, + 0.9248695241652183, + 0.8749092776800845, + 1.5678286259999998, + 2.4348720859999995, + 0.8528744919675278, + 0.5282751491259897, 0.0, - 1.0534661399999998, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.19588158, + 0.4965507655670841, 0.0, 0.0, 0.0, @@ -387,10 +382,6 @@ 0.0, 0.0, 0.0, - 0.01995551999999987, - 0.08545095, - 0.007989913613567745, - 0.012219458233588571, 0.0, 0.0, 0.0, @@ -398,6 +389,16 @@ 0.0, 0.0, 0.0, + 0.11338560853163265, + 0.04079284310894048, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0021886029750169123, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -405,20 +406,14 @@ 0.0 ], "Netzbezug_Wh_pro_Stunde": [ - 122.78999999999996, - 6108.66, - 5457.82, - 6525.34, - 8132.85, - 6023.75, + 4056.445281426396, + 3955.2860654615033, + 7490.82, + 12958.339999999998, + 4640.231185895146, + 2636.103538552843, 0.0, - 4640.82, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 704.61, + 2187.4483064629258, 0.0, 0.0, 0.0, @@ -427,10 +422,6 @@ 0.0, 0.0, 0.0, - 65.59999999999957, - 351.65, - 35.04348076126204, - 55.24167375040041, 0.0, 0.0, 0.0, @@ -438,6 +429,16 @@ 0.0, 0.0, 0.0, + 466.6074425170068, + 178.91597854798454, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 9.957247384062384, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -485,20 +486,20 @@ 0.0 ], "Verluste_Pro_Stunde": [ - 0.0, - 1152.0, - 276.0, - 345.0, + 207.07863377116755, + 207.1951278553804, + 1083.0, 552.0, - 414.0, - 615.5918181818183, - 345.0, - 632.3249999999998, - 23.391818181818195, - 99.72409090909093, - 133.72909090909081, + 521.2057423074175, + 395.8024246263412, + 458.55183787620945, + 227.23539677555112, + 640.3849261442235, + 253.88850337853552, + 106.80230977350088, + 133.7321802766326, 124.41545454545451, - 0.0, + 96.08318181818186, 70.41409090909087, 118.37045454545455, 94.68272727272722, @@ -506,63 +507,63 @@ 75.86045454545456, 66.66681818181814, 69.12409090909085, - 109.0704545454546, - 101.01681818181828, - 0.0, - 11.233982308648535, - 48.41330768174522, - 161.62968357967037, - 21.962728535423857, - 538.2984000000038, - 441.95211196761403, - 260.56941082122324, - 171.99990368477063, - 62.214291413756285, - 35.132727272727266, - 77.27590909090907, + 109.07302361034766, + 116.99491129525349, + 31.990721833371907, + 73.82223834331725, + 123.8591383343284, + 171.42744837680007, + 116.9350623689079, + 538.2984000000001, + 441.9538395103232, + 261.1952696860876, + 184.66788225469543, + 131.21108264656203, + 111.78035844493081, + 90.18403329253945, 134.59227272727276, 100.08954545454549, 80.85954545454547 ], "akku_soc_pro_stunde": [ 80.0, - 80.0, - 61.06060606060606, - 61.06060606060606, - 61.06060606060606, - 61.06060606060606, - 61.06060606060606, - 50.341167355371894, - 50.341167355371894, - 36.91550447658402, - 36.17712637741047, - 33.0292699724518, - 28.808023415977967, - 24.88076790633609, - 24.88076790633609, - 22.658100895316807, - 18.921659779614323, - 15.932937327823693, - 13.312456955922865, - 10.917871900826448, - 8.813490013774107, - 6.63154269972452, - 3.1886621900826473, - 0.0, - 0.0, - 0.3120550641291261, - 1.07020564583707, - 4.578139530578446, - 4.728141236897871, - 19.68087457022947, - 31.94341995234899, - 38.90006700591099, - 42.674787887051245, - 43.17025540478875, - 42.06126780148296, - 39.622002994320425, - 35.373509537020155, - 32.214117319389295 + 80.00218427142131, + 80.00760448962633, + 61.06821055023239, + 61.06821055023239, + 62.12948116988288, + 63.5406596317257, + 58.120614791285504, + 58.682709146161926, + 45.22651624410048, + 39.85140827675753, + 36.67674043670639, + 32.45548217808278, + 28.528226668440908, + 25.495297949432643, + 23.27263093841336, + 19.53618982271088, + 16.54746737092025, + 13.926986999019423, + 11.532401943923004, + 9.428020056870663, + 7.2460727428210765, + 3.8031825018727887, + 0.30551238333016156, + 0.6197802647075666, + 1.5052110988161542, + 2.736047696034607, + 7.125981603698244, + 7.346947276543289, + 22.29968060987662, + 34.57523424809509, + 41.83065840604197, + 46.49642400356206, + 47.884563842022054, + 46.48524430245792, + 43.99708508544073, + 39.74859162814045, + 36.5891994105096 ], "Electricity_price": [ 0.000228, @@ -603,6 +604,46 @@ 0.0002969, 0.0002921, 0.000278 + ], + "Feed_in_tariff": [ + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05, + 7e-05 ] }, "eauto_obj": { @@ -619,16 +660,16 @@ 0.0, 0.0, 0.0, - 0.0, + 0.375, + 0.375, + 0.875, 1.0, - 0.5, - 0.625, - 1.0, - 0.75, - 0.5, + 0.875, 0.625, 0.375, - 0.0, + 0.375, + 0.375, + 0.1, 0.0, 0.0, 0.0, @@ -712,26 +753,26 @@ "capacity_wh": 60000, "charging_efficiency": 0.95, "max_charge_power_w": 11040, - "soc_wh": 59373.0, + "soc_wh": 59110.8, "initial_soc_percentage": 5 }, "start_solution": [ + 2.0, + 1.0, + 2.0, 0.0, + 2.0, 0.0, + 1.0, 2.0, 1.0, 0.0, - 1.0, - 2.0, - 2.0, - 1.0, - 1.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 1.0, @@ -739,6 +780,23 @@ 1.0, 1.0, 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, 0.0, 1.0, 1.0, @@ -748,71 +806,54 @@ 1.0, 1.0, 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, 5.0, - 4.0, - 2.0, 6.0, - 3.0, - 3.0, - 5.0, - 3.0, - 5.0, - 5.0, - 0.0, - 6.0, - 2.0, - 3.0, - 6.0, - 4.0, - 2.0, - 3.0, 1.0, - 2.0, - 3.0, - 3.0, - 2.0, - 3.0, - 3.0, 0.0, - 3.0, - 2.0, - 2.0, 6.0, 1.0, 3.0, 5.0, + 1.0, + 1.0, + 1.0, + 5.0, + 6.0, + 5.0, + 3.0, + 1.0, + 1.0, + 1.0, + 1.0, + 4.0, + 2.0, + 6.0, + 5.0, + 5.0, + 3.0, 2.0, 2.0, - 4.0, - 4.0, + 6.0, + 5.0, + 2.0, + 5.0, 4.0, 0.0, + 4.0, + 1.0, + 0.0, + 0.0, 5.0, - 4.0, - 6.0, - 6.0, - 4.0, + 0.0, + 0.0, + 2.0, 3.0, - 5.0, + 4.0, + 4.0, 2.0, 5.0, - 14.0 + 3.0, + 12.0 ], - "washingstart": 14 + "washingstart": 12 } \ No newline at end of file