mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-03-18 18:44:04 +00:00
Cleanup: parameters: extra=forbid, optimize: battery, inverter optional (#361)
* Cleanup: parameters: extra=forbid, optimize: battery, inverter optional * Don't allow extra fields for parameters/REST-API (at least for now while changing API). * Allow both battery and inverter to be set optionally (atm optional battery not implemented, no API constraints). * inverter: Remove default max_power_wh * single_test_optimization: Add more cli-parameters * Workflow docker-build: Don't try to authenticate for PRs * Secrets are not available anyway for forks.
This commit is contained in:
parent
745086c2eb
commit
9ad61f66b2
13
.github/workflows/docker-build.yml
vendored
13
.github/workflows/docker-build.yml
vendored
@ -89,12 +89,16 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
# skip for pull requests
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
# skip for pull requests
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@ -102,6 +106,8 @@ jobs:
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
# skip for pull requests
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@ -114,21 +120,22 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
outputs: type=image,"name=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,"push=${{ github.event_name != 'pull_request' }}","annotation-index.org.opencontainers.image.description=${{ env.EOS_REPO_DESCRIPTION }}"
|
||||
#push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Generate artifact attestation DockerHub
|
||||
uses: actions/attest-build-provenance@v2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: ${{ github.event_name != 'pull_request' }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Generate artifact attestation GitHub
|
||||
uses: actions/attest-build-provenance@v2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: ${{ github.event_name != 'pull_request' }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
|
36
openapi.json
36
openapi.json
@ -6108,6 +6108,7 @@
|
||||
"default": 100
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"capacity_wh"
|
||||
@ -6231,6 +6232,7 @@
|
||||
"description": "An array of floats representing the total load (consumption) in watts for different time intervals."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pv_prognose_wh",
|
||||
@ -6326,6 +6328,7 @@
|
||||
"description": "An integer representing the usage duration of a household device in hours."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"consumption_wh",
|
||||
@ -6338,11 +6341,14 @@
|
||||
"max_power_wh": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"title": "Max Power Wh",
|
||||
"default": 10000
|
||||
"title": "Max Power Wh"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"max_power_wh"
|
||||
],
|
||||
"title": "InverterParameters"
|
||||
},
|
||||
"OptimizationParameters": {
|
||||
@ -6351,13 +6357,24 @@
|
||||
"$ref": "#/components/schemas/EnergieManagementSystemParameters"
|
||||
},
|
||||
"pv_akku": {
|
||||
"$ref": "#/components/schemas/SolarPanelBatteryParameters"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SolarPanelBatteryParameters"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inverter": {
|
||||
"$ref": "#/components/schemas/InverterParameters",
|
||||
"default": {
|
||||
"max_power_wh": 10000.0
|
||||
}
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/InverterParameters"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"eauto": {
|
||||
"anyOf": [
|
||||
@ -6417,10 +6434,12 @@
|
||||
"description": "Can be `null` or contain a previous solution (if available)."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ems",
|
||||
"pv_akku",
|
||||
"inverter",
|
||||
"eauto"
|
||||
],
|
||||
"title": "OptimizationParameters"
|
||||
@ -6507,6 +6526,7 @@
|
||||
"description": "Can be `null` or contain an object representing the start of washing (if applicable)."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ac_charge",
|
||||
@ -8834,6 +8854,7 @@
|
||||
"description": "Used Electricity Price, including predictions"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Last_Wh_pro_Stunde",
|
||||
@ -8917,6 +8938,7 @@
|
||||
"default": 100
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"capacity_wh"
|
||||
|
48
single_test_optimization.py
Normal file → Executable file
48
single_test_optimization.py
Normal file → Executable file
@ -2,9 +2,11 @@
|
||||
|
||||
import argparse
|
||||
import cProfile
|
||||
import json
|
||||
import pstats
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@ -295,7 +297,9 @@ def prepare_optimization_parameters() -> OptimizationParameters:
|
||||
)
|
||||
|
||||
|
||||
def run_optimization(real_world: bool = False, start_hour: int = 0, verbose: bool = False) -> dict:
|
||||
def run_optimization(
|
||||
real_world: bool, start_hour: int, verbose: bool, seed: int, parameters_file: str, ngen: int
|
||||
) -> Any:
|
||||
"""Run the optimization problem.
|
||||
|
||||
Args:
|
||||
@ -306,7 +310,10 @@ def run_optimization(real_world: bool = False, start_hour: int = 0, verbose: boo
|
||||
dict: Optimization result as a dictionary
|
||||
"""
|
||||
# Prepare parameters
|
||||
if real_world:
|
||||
if parameters_file:
|
||||
with open(parameters_file, "r") as f:
|
||||
parameters = OptimizationParameters(**json.load(f))
|
||||
elif real_world:
|
||||
parameters = prepare_optimization_real_parameters()
|
||||
else:
|
||||
parameters = prepare_optimization_parameters()
|
||||
@ -318,12 +325,12 @@ def run_optimization(real_world: bool = False, start_hour: int = 0, verbose: boo
|
||||
# Initialize the optimization problem using the default configuration
|
||||
config_eos = get_config()
|
||||
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 48})
|
||||
opt_class = optimization_problem(verbose=verbose, fixed_seed=42)
|
||||
opt_class = optimization_problem(verbose=verbose, fixed_seed=seed)
|
||||
|
||||
# Perform the optimisation based on the provided parameters and start hour
|
||||
result = opt_class.optimierung_ems(parameters=parameters, start_hour=start_hour)
|
||||
result = opt_class.optimierung_ems(parameters=parameters, start_hour=start_hour, ngen=ngen)
|
||||
|
||||
return result.model_dump()
|
||||
return result.model_dump_json()
|
||||
|
||||
|
||||
def main():
|
||||
@ -339,6 +346,19 @@ def main():
|
||||
parser.add_argument(
|
||||
"--start-hour", type=int, default=0, help="Starting hour for optimization (default: 0)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--parameters-file",
|
||||
type=str,
|
||||
default="",
|
||||
help="Load optimization parameters from json file (default: unset)",
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=42, help="Use fixed random seed (default: 42)")
|
||||
parser.add_argument(
|
||||
"--ngen",
|
||||
type=int,
|
||||
default=400,
|
||||
help="Number of generations during optimization process (default: 400)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -351,12 +371,16 @@ def main():
|
||||
real_world=args.real_world,
|
||||
start_hour=args.start_hour,
|
||||
verbose=args.verbose,
|
||||
seed=args.seed,
|
||||
parameters_file=args.parameters_file,
|
||||
ngen=args.ngen,
|
||||
)
|
||||
# Print profiling statistics
|
||||
stats = pstats.Stats(profiler)
|
||||
stats.strip_dirs().sort_stats("cumulative").print_stats(200)
|
||||
# Print result
|
||||
print("\nOptimization Result:")
|
||||
if args.verbose:
|
||||
print("\nOptimization Result:")
|
||||
print(result)
|
||||
|
||||
except Exception as e:
|
||||
@ -367,12 +391,18 @@ def main():
|
||||
try:
|
||||
start_time = time.time()
|
||||
result = run_optimization(
|
||||
real_world=args.real_world, start_hour=args.start_hour, verbose=args.verbose
|
||||
real_world=args.real_world,
|
||||
start_hour=args.start_hour,
|
||||
verbose=args.verbose,
|
||||
seed=args.seed,
|
||||
parameters_file=args.parameters_file,
|
||||
ngen=args.ngen,
|
||||
)
|
||||
end_time = time.time()
|
||||
elapsed_time = end_time - start_time
|
||||
print(f"\nElapsed time: {elapsed_time:.4f} seconds.")
|
||||
print("\nOptimization Result:")
|
||||
if args.verbose:
|
||||
print(f"\nElapsed time: {elapsed_time:.4f} seconds.")
|
||||
print("\nOptimization Result:")
|
||||
print(result)
|
||||
|
||||
except Exception as e:
|
||||
|
@ -1,64 +0,0 @@
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class HomeApplianceParameters(BaseModel):
|
||||
consumption_wh: int = Field(
|
||||
gt=0,
|
||||
description="An integer representing the energy consumption of a household device in watt-hours.",
|
||||
)
|
||||
duration_h: int = Field(
|
||||
gt=0,
|
||||
description="An integer representing the usage duration of a household device in hours.",
|
||||
)
|
||||
|
||||
|
||||
class HomeAppliance:
|
||||
def __init__(self, parameters: HomeApplianceParameters, hours: int):
|
||||
self.hours = hours # Total duration for which the planning is done
|
||||
self.consumption_wh = (
|
||||
parameters.consumption_wh
|
||||
) # Total energy consumption of the device in kWh
|
||||
self.duration_h = parameters.duration_h # Duration of use in hours
|
||||
self.load_curve = np.zeros(self.hours) # Initialize the load curve with zeros
|
||||
|
||||
def set_starting_time(self, start_hour: int, global_start_hour: int = 0) -> None:
|
||||
"""Sets the start time of the device and generates the corresponding load curve.
|
||||
|
||||
:param start_hour: The hour at which the device should start.
|
||||
"""
|
||||
self.reset()
|
||||
# Check if the duration of use is within the available time frame
|
||||
if start_hour + self.duration_h > self.hours:
|
||||
raise ValueError("The duration of use exceeds the available time frame.")
|
||||
if start_hour < global_start_hour:
|
||||
raise ValueError("The start time is earlier than the available time frame.")
|
||||
|
||||
# Calculate power per hour based on total consumption and duration
|
||||
power_per_hour = self.consumption_wh / self.duration_h # Convert to watt-hours
|
||||
|
||||
# Set the power for the duration of use in the load curve array
|
||||
self.load_curve[start_hour : start_hour + self.duration_h] = power_per_hour
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Resets the load curve."""
|
||||
self.load_curve = np.zeros(self.hours)
|
||||
|
||||
def get_load_curve(self) -> np.ndarray:
|
||||
"""Returns the current load curve."""
|
||||
return self.load_curve
|
||||
|
||||
def get_load_for_hour(self, hour: int) -> float:
|
||||
"""Returns the load for a specific hour.
|
||||
|
||||
:param hour: The hour for which the load is queried.
|
||||
:return: The load in watts for the specified hour.
|
||||
"""
|
||||
if hour < 0 or hour >= self.hours:
|
||||
raise ValueError("The specified hour is outside the available time frame.")
|
||||
|
||||
return self.load_curve[hour]
|
||||
|
||||
def get_latest_starting_point(self) -> int:
|
||||
"""Returns the latest possible start time at which the device can still run completely."""
|
||||
return self.hours - self.duration_h
|
@ -8,7 +8,7 @@ from typing_extensions import Self
|
||||
|
||||
from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMixin
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.core.pydantic import PydanticBaseModel
|
||||
from akkudoktoreos.core.pydantic import ParametersBaseModel, PydanticBaseModel
|
||||
from akkudoktoreos.devices.battery import Battery
|
||||
from akkudoktoreos.devices.generic import HomeAppliance
|
||||
from akkudoktoreos.devices.inverter import Inverter
|
||||
@ -18,7 +18,7 @@ from akkudoktoreos.utils.utils import NumpyEncoder
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class EnergieManagementSystemParameters(PydanticBaseModel):
|
||||
class EnergieManagementSystemParameters(ParametersBaseModel):
|
||||
pv_prognose_wh: list[float] = Field(
|
||||
description="An array of floats representing the forecasted photovoltaic output in watts for different time intervals."
|
||||
)
|
||||
@ -50,7 +50,7 @@ class EnergieManagementSystemParameters(PydanticBaseModel):
|
||||
return self
|
||||
|
||||
|
||||
class SimulationResult(PydanticBaseModel):
|
||||
class SimulationResult(ParametersBaseModel):
|
||||
"""This object contains the results of the simulation and provides insights into various parameters over the entire forecast period."""
|
||||
|
||||
Last_Wh_pro_Stunde: list[Optional[float]] = Field(description="TBD")
|
||||
|
@ -478,3 +478,7 @@ class PydanticDateTimeSeries(PydanticBaseModel):
|
||||
dtype=str(series.dtype),
|
||||
tz=tz,
|
||||
)
|
||||
|
||||
|
||||
class ParametersBaseModel(PydanticBaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
@ -4,6 +4,7 @@ import numpy as np
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.core.pydantic import ParametersBaseModel
|
||||
from akkudoktoreos.devices.devicesabc import DeviceBase
|
||||
from akkudoktoreos.utils.utils import NumpyEncoder
|
||||
|
||||
@ -24,7 +25,7 @@ def initial_soc_percentage_field(description: str) -> int:
|
||||
return Field(default=0, ge=0, le=100, description=description)
|
||||
|
||||
|
||||
class BaseBatteryParameters(BaseModel):
|
||||
class BaseBatteryParameters(ParametersBaseModel):
|
||||
"""Base class for battery parameters with fields for capacity, efficiency, and state of charge."""
|
||||
|
||||
capacity_wh: int = Field(
|
||||
|
@ -1,15 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import Field
|
||||
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.core.pydantic import ParametersBaseModel
|
||||
from akkudoktoreos.devices.devicesabc import DeviceBase
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HomeApplianceParameters(BaseModel):
|
||||
class HomeApplianceParameters(ParametersBaseModel):
|
||||
consumption_wh: int = Field(
|
||||
gt=0,
|
||||
description="An integer representing the energy consumption of a household device in watt-hours.",
|
||||
|
@ -1,17 +1,18 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import Field
|
||||
from scipy.interpolate import RegularGridInterpolator
|
||||
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.core.pydantic import ParametersBaseModel
|
||||
from akkudoktoreos.devices.battery import Battery
|
||||
from akkudoktoreos.devices.devicesabc import DeviceBase
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class InverterParameters(BaseModel):
|
||||
max_power_wh: float = Field(default=10000, gt=0)
|
||||
class InverterParameters(ParametersBaseModel):
|
||||
max_power_wh: float = Field(gt=0)
|
||||
|
||||
|
||||
class Inverter(DeviceBase):
|
||||
|
@ -6,7 +6,7 @@ from typing import Any, Optional
|
||||
|
||||
import numpy as np
|
||||
from deap import algorithms, base, creator, tools
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from akkudoktoreos.core.coreabc import (
|
||||
@ -16,6 +16,7 @@ from akkudoktoreos.core.coreabc import (
|
||||
)
|
||||
from akkudoktoreos.core.ems import EnergieManagementSystemParameters, SimulationResult
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.core.pydantic import ParametersBaseModel
|
||||
from akkudoktoreos.devices.battery import (
|
||||
Battery,
|
||||
ElectricVehicleParameters,
|
||||
@ -30,10 +31,10 @@ from akkudoktoreos.utils.utils import NumpyEncoder
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class OptimizationParameters(BaseModel):
|
||||
class OptimizationParameters(ParametersBaseModel):
|
||||
ems: EnergieManagementSystemParameters
|
||||
pv_akku: SolarPanelBatteryParameters
|
||||
inverter: InverterParameters = InverterParameters()
|
||||
pv_akku: Optional[SolarPanelBatteryParameters]
|
||||
inverter: Optional[InverterParameters]
|
||||
eauto: Optional[ElectricVehicleParameters]
|
||||
dishwasher: Optional[HomeApplianceParameters] = None
|
||||
temperature_forecast: Optional[list[Optional[float]]] = Field(
|
||||
@ -60,7 +61,7 @@ class OptimizationParameters(BaseModel):
|
||||
return start_solution
|
||||
|
||||
|
||||
class OptimizeResponse(BaseModel):
|
||||
class OptimizeResponse(ParametersBaseModel):
|
||||
"""**Note**: The first value of "Last_Wh_per_hour", "Netzeinspeisung_Wh_per_hour", and "Netzbezug_Wh_per_hour", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged."""
|
||||
|
||||
ac_charge: list[float] = Field(
|
||||
@ -565,11 +566,13 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
||||
)
|
||||
|
||||
# Initialize PV and EV batteries
|
||||
akku = Battery(
|
||||
parameters.pv_akku,
|
||||
hours=self.config.prediction_hours,
|
||||
)
|
||||
akku.set_charge_per_hour(np.full(self.config.prediction_hours, 1))
|
||||
akku: Optional[Battery] = None
|
||||
if parameters.pv_akku:
|
||||
akku = Battery(
|
||||
parameters.pv_akku,
|
||||
hours=self.config.prediction_hours,
|
||||
)
|
||||
akku.set_charge_per_hour(np.full(self.config.prediction_hours, 1))
|
||||
|
||||
eauto: Optional[Battery] = None
|
||||
if parameters.eauto:
|
||||
@ -595,11 +598,13 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
||||
)
|
||||
|
||||
# Initialize the inverter and energy management system
|
||||
inverter = Inverter(
|
||||
sc,
|
||||
parameters.inverter,
|
||||
akku,
|
||||
)
|
||||
inverter: Optional[Inverter] = None
|
||||
if parameters.inverter:
|
||||
inverter = Inverter(
|
||||
sc,
|
||||
parameters.inverter,
|
||||
akku,
|
||||
)
|
||||
self.ems.set_parameters(
|
||||
parameters.ems,
|
||||
inverter=inverter,
|
||||
|
4
tests/testdata/optimize_input_1.json
vendored
4
tests/testdata/optimize_input_1.json
vendored
@ -31,8 +31,8 @@
|
||||
"initial_soc_percentage": 80,
|
||||
"min_soc_percentage": 15
|
||||
},
|
||||
"wechselrichter": {
|
||||
"max_leistung_wh": 10000
|
||||
"inverter": {
|
||||
"max_power_wh": 10000
|
||||
},
|
||||
"eauto": {
|
||||
"capacity_wh": 60000,
|
||||
|
3
tests/testdata/optimize_input_2.json
vendored
3
tests/testdata/optimize_input_2.json
vendored
@ -158,6 +158,9 @@
|
||||
"initial_soc_percentage": 80,
|
||||
"min_soc_percentage": 0
|
||||
},
|
||||
"inverter": {
|
||||
"max_power_wh": 10000
|
||||
},
|
||||
"eauto": {
|
||||
"capacity_wh": 60000,
|
||||
"charging_efficiency": 0.95,
|
||||
|
Loading…
Reference in New Issue
Block a user