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:
Dominique Lasserre 2025-01-13 21:44:17 +01:00 committed by GitHub
parent 745086c2eb
commit 9ad61f66b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 119 additions and 109 deletions

View File

@ -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: |

View File

@ -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
View 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:

View File

@ -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

View File

@ -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")

View File

@ -478,3 +478,7 @@ class PydanticDateTimeSeries(PydanticBaseModel):
dtype=str(series.dtype),
tz=tz,
)
class ParametersBaseModel(PydanticBaseModel):
model_config = ConfigDict(extra="forbid")

View File

@ -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(

View File

@ -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.",

View File

@ -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):

View File

@ -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,

View File

@ -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,

View File

@ -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,