feat(optimization): model battery LCOS and probabilistic bypass

This commit is contained in:
Andreas
2026-07-15 09:23:29 +02:00
parent 8ddb7ce754
commit bed1f0f275
27 changed files with 1362 additions and 890 deletions
+8
View File
@@ -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)
+1 -1
View File
@@ -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` | `<unknown>` | 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]. |
+1 -1
View File
@@ -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. |
:::
<!-- pyml enable line-length -->
+2
View File
@@ -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,
+6
View File
@@ -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. |
:::
<!-- pyml enable line-length -->
@@ -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,
+20 -1
View File
@@ -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:**
+71 -3
View File
@@ -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`)
+65 -5
View File
@@ -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,
+5 -1
View File
@@ -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],
},
)
@@ -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:
+78 -126
View File
@@ -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
@@ -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]
@@ -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()
@@ -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,
@@ -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={
+70 -1
View File
@@ -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.
+5
View File
@@ -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
+32 -6
View File
@@ -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)
+42 -2
View File
@@ -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)
+91 -28
View File
@@ -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()
+41 -10
View File
@@ -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
# -----------------------------------------------------------------
+30
View File
@@ -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
+97 -97
View File
@@ -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,
+103 -103
View File
@@ -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,
+102 -102
View File
@@ -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,
+112 -112
View File
@@ -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,
+313 -272
View File
@@ -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
}