Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
33e5296
introduce self.time as casadi symbolic
sarahleidolf Feb 5, 2025
cfa6269
add at Example OneRoom_SimpleMPC
sarahleidolf Feb 13, 2025
c511e39
adjust parsing and data handling for ml mpc
sarahleidolf Feb 17, 2025
5294934
add example
sarahleidolf Feb 17, 2025
468ff65
Merge branch 'main' into 42-add-datadriven-mpc
sarahleidolf Apr 14, 2025
8240253
change requirements
sarahleidolf May 22, 2025
43a6538
change self.time
sarahleidolf May 22, 2025
b07d8a3
first approach for new objective handling
sarahleidolf May 23, 2025
be5fcf2
Merge branch
sarahleidolf May 27, 2025
1097e96
Revert "first approach for new objective handling"
sarahleidolf Jul 2, 2025
7fd7811
Revert "change self.time"
sarahleidolf Jul 2, 2025
cc3004e
Revert "Revert "change self.time""
sarahleidolf Jul 2, 2025
ca461e1
add set_actuation
sarahleidolf Jul 14, 2025
1766125
update requirements
sarahleidolf Jan 21, 2026
87f0d25
main into #42
sarahleidolf Jan 21, 2026
69b9be8
update globals
sarahleidolf Jan 21, 2026
e9c4d97
minor updates after merge from main branch
sarahleidolf Jan 21, 2026
da6e785
allow multiple shooting
sarahleidolf Feb 10, 2026
6d4a1b4
minor import changes
sarahleidolf Feb 24, 2026
39b7fb5
Merge branch 'main' into 42-add-datadriven-mpc
sarahleidolf Feb 24, 2026
16220f6
new numpy version
sarahleidolf Feb 24, 2026
3f8c377
move example
sarahleidolf Feb 24, 2026
099df9f
move example
sarahleidolf Feb 24, 2026
6ea896c
adjust snapshots of ci tests for new time grid formulation
sarahleidolf Feb 25, 2026
6f8f0dc
adjust ci workflow
sarahleidolf Feb 25, 2026
9d92f57
change r_del_u handling
sarahleidolf Feb 25, 2026
758861b
include review
sarahleidolf Mar 26, 2026
c259f92
Merge branch 'main' into 42-add-datadriven-mpc_
sarahleidolf Mar 27, 2026
647f780
update examples and tests
sarahleidolf Mar 27, 2026
733c708
Update snapshots after assumption "Set the first value of power_flex…
sarahleidolf Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
CREATE_PAGES_ON_FAILURE: true
EXECUTE_TESTS: true
EXECUTE_COVERAGE_TEST: true

EXTRA_REQUIREMENTS: '["ml"]'
89 changes: 51 additions & 38 deletions agentlib_flexquant/data_structures/flex_kpis.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ class FlexibilityKPIs(pydantic.BaseModel):
default=KPISeries(name="power_flex_offer", unit="kW", integration_method=LINEAR),
description="Power flexibility",
)
power_flex_offer_prepared: KPISeries = pydantic.Field(
default=KPISeries(name="power_flex_offer_prepared", unit="kW", integration_method=LINEAR),
description="Power flexibility Series prepared for integration",
)
power_flex_offer_max: KPI = pydantic.Field(
default=KPI(name="power_flex_offer_max", unit="kW"),
description="Maximum power flexibility",
Expand Down Expand Up @@ -201,7 +205,7 @@ def calculate(
enable_energy_costs_correction: bool,
calculate_flex_cost: bool,
integration_method: INTEGRATION_METHOD,
collocation_time_grid: list = None,
time_grid_info: dict = None,
):
"""Calculate the KPIs based on the power and electricity price input profiles.

Expand All @@ -217,7 +221,8 @@ def calculate(
enable_energy_costs_correction: whether the energy costs should be corrected
calculate_flex_cost: whether the cost of the flexibility should be calculated
integration_method: method used for integration of KPISeries e.g. linear, constant
collocation_time_grid: Time grid of the mpc output with collocation discretization
time_grid_info: Dictionary with 'type' ('collocation', 'multiple_shooting', 'none')
and 'grid' (list of time points) keys


"""
Expand All @@ -229,10 +234,10 @@ def calculate(
integration_method=integration_method,
)
self._calculate_power_flex_stats(
mpc_time_grid=mpc_time_grid, collocation_time_grid=collocation_time_grid
mpc_time_grid=mpc_time_grid, time_grid_info=time_grid_info
)
self._calculate_energy_flex(
mpc_time_grid=mpc_time_grid, collocation_time_grid=collocation_time_grid
mpc_time_grid=mpc_time_grid, time_grid_info=time_grid_info
)

# Costs KPIs
Expand All @@ -250,7 +255,7 @@ def calculate(
stored_energy_diff=stored_energy_diff,
integration_method=integration_method,
mpc_time_grid=mpc_time_grid,
collocation_time_grid=collocation_time_grid,
time_grid_info=time_grid_info,
)
self._calculate_costs_rel()

Expand Down Expand Up @@ -292,10 +297,6 @@ def _calculate_power_flex(
# Set values to zero if the difference is small
relative_difference = (power_flex / power_profile_base).abs()
power_flex.loc[relative_difference < relative_error_acceptance] = 0
# Set the first value of power_flex to zero, since it comes from the measurement/simulator
# and is the same for baseline and shadow mpcs.
# For quantification of flexibility, only power difference is of interest.
power_flex.iloc[0] = 0

# Set values
self.power_flex_full.value = power_flex
Expand All @@ -308,28 +309,38 @@ def _calculate_power_flex(
self.power_flex_offer.integration_method = integration_method

def _calculate_power_flex_stats(
self, mpc_time_grid: np.array, collocation_time_grid: list = None
self, mpc_time_grid: np.array, time_grid_info: dict = None
):
"""Calculate the characteristic values of the power flexibility for the offer."""
"""Calculate the characteristic values of the power flexibility for the offer.

Args:
mpc_time_grid: the MPC time grid over the horizon
time_grid_info: Dictionary with 'type' and 'grid' keys for discretization info
"""
if self.power_flex_offer.value is None:
raise ValueError("Power flexibility value is empty.")

# Calculate characteristic values
# max and min of power flex offer
power_flex_offer = self.power_flex_offer.value.iloc[:-1].drop(
collocation_time_grid, errors="ignore"
)

self.power_flex_offer_prepared = self.power_flex_offer.__deepcopy__()

# Only drop collocation points if using collocation method
if time_grid_info and time_grid_info.get("type") == "collocation":
self.power_flex_offer_prepared.value = self.power_flex_offer_prepared.value.drop(
time_grid_info["grid"], errors="ignore"
)
power_flex_offer = self.power_flex_offer_prepared.value.iloc[:-1]

power_flex_offer_max = power_flex_offer.max()
power_flex_offer_min = power_flex_offer.min()

# Average of the power flex offer
# Get the series for integration before calculating average
power_flex_offer_integration = self._get_series_for_integration(
series=self.power_flex_offer, mpc_time_grid=mpc_time_grid
)
power_flex_offer_integration.value = power_flex_offer_integration.value.drop(
collocation_time_grid, errors="ignore"
series=self.power_flex_offer_prepared, mpc_time_grid=mpc_time_grid
)
# Calculate the average and stores the original value

power_flex_offer_avg = power_flex_offer_integration.avg()

# Set values
Expand All @@ -338,7 +349,7 @@ def _calculate_power_flex_stats(
self.power_flex_offer_avg.value = power_flex_offer_avg

def _get_series_for_integration(
self, series: KPISeries, mpc_time_grid: np.ndarray
self, series: Union[pd.Series, KPISeries], mpc_time_grid: np.ndarray
) -> KPISeries:
"""Return the KPISeries value sampled on the MPC time grid when the integration
method is constant.
Expand All @@ -357,20 +368,23 @@ def _get_series_for_integration(
else:
return series.__deepcopy__()

def _calculate_energy_flex(self, mpc_time_grid, collocation_time_grid: list = None):
def _calculate_energy_flex(self, mpc_time_grid, time_grid_info: dict = None):
"""Calculate the energy flexibility by integrating the power flexibility
of the offer window."""
of the offer window.

Args:
mpc_time_grid: the MPC time grid over the horizon
time_grid_info: Dictionary with 'type' and 'grid' keys for discretization info
"""
if self.power_flex_offer.value is None:
raise ValueError("Power flexibility value of the offer is empty.")

# Calculate flexibility
# Get the series for integration before calculating average
power_flex_offer_integration = self._get_series_for_integration(
series=self.power_flex_offer, mpc_time_grid=mpc_time_grid
)
power_flex_offer_integration.value = power_flex_offer_integration.value.drop(
collocation_time_grid, errors="ignore"
series=self.power_flex_offer_prepared, mpc_time_grid=mpc_time_grid
)

# Calculate the energy flex and stores the original value
energy_flex = power_flex_offer_integration.integrate(time_unit="hours")

Expand All @@ -386,7 +400,7 @@ def _calculate_costs(
stored_energy_diff: float,
integration_method: INTEGRATION_METHOD,
mpc_time_grid: np.ndarray,
collocation_time_grid: list = None,
time_grid_info: dict = None,
):
"""Calculate the costs of the flexibility event based on the electricity costs profile,
the power flexibility profile and difference of stored energy.
Expand All @@ -399,7 +413,7 @@ def _calculate_costs(
stored_energy_diff: the difference of the stored energy between baseline and shadow mpc
integration_method: the integration method used to integrate KPISeries
mpc_time_grid: the MPC time grid over the horizon
collocation_time_grid: Time grid of the mpc output with collocation discretization
time_grid_info: Dictionary with 'type' and 'grid' keys for discretization info


"""
Expand Down Expand Up @@ -435,9 +449,10 @@ def _calculate_costs(
power_flex_full_integration = self._get_series_for_integration(
series=self.power_flex_full, mpc_time_grid=mpc_time_grid
)
power_flex_full_integration.value = power_flex_full_integration.value.drop(
collocation_time_grid, errors="ignore"
)
if time_grid_info and time_grid_info.get("type") == "collocation":
power_flex_full_integration.value = power_flex_full_integration.value.drop(
time_grid_info["grid"], errors="ignore"
)

# Difference in costs between shadow and baseline mpc
delta_cost = cost_profile_shadow - cost_profile_base
Expand All @@ -447,7 +462,6 @@ def _calculate_costs(

# Calculate the costs and stores the original value
costs = self.electricity_costs_series.integrate(time_unit="hours")

# correct the costs
corrected_costs = costs - stored_energy_diff * np.mean(electricity_price_signal)

Expand Down Expand Up @@ -640,15 +654,16 @@ def calculate(
enable_energy_costs_correction: bool,
calculate_flex_cost: bool,
integration_method: INTEGRATION_METHOD,
collocation_time_grid: list = None,
time_grid_info: dict = None,
):
"""Calculate the KPIs for the positive and negative flexibility.

Args:
enable_energy_costs_correction: whether the energy costs should be corrected
calculate_flex_cost: whether the cost of the flexibility should be calculated
integration_method: method used for integration of KPISeries e.g. linear, constant
collocation_time_grid: Time grid of the mpc output with collocation discretization
time_grid_info: Dictionary with 'type' ('collocation', 'multiple_shooting', 'none')
and 'grid' (list of time points) keys

"""
self.kpis_pos.calculate(
Expand All @@ -663,7 +678,7 @@ def calculate(
enable_energy_costs_correction=enable_energy_costs_correction,
calculate_flex_cost=calculate_flex_cost,
integration_method=integration_method,
collocation_time_grid=collocation_time_grid,
time_grid_info=time_grid_info,
)
self.kpis_neg.calculate(
power_profile_base=self.power_profile_base,
Expand All @@ -677,7 +692,7 @@ def calculate(
enable_energy_costs_correction=enable_energy_costs_correction,
calculate_flex_cost=calculate_flex_cost,
integration_method=integration_method,
collocation_time_grid=collocation_time_grid,
time_grid_info=time_grid_info,
)
self.reset_time_grid()
return self.kpis_pos, self.kpis_neg
Expand All @@ -701,5 +716,3 @@ def update_profile(self, name: str, value: pd.Series, mpc:bool) -> None:
if value is not None:
value = self.unify_inputs(series=value, mpc= mpc)
setattr(self, name, value)


6 changes: 3 additions & 3 deletions agentlib_flexquant/data_structures/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
CONSTANT = 'constant'
COLLOCATION = 'collocation'
INTEGRATION_METHOD = Literal[LINEAR, CONSTANT]

FlexibilityDirections = Literal["positive", "negative"]

POWER_ALIAS_BASE = "_P_el_base"
POWER_ALIAS_NEG = "_P_el_neg"
POWER_ALIAS_POS = "_P_el_pos"
Expand All @@ -25,7 +27,7 @@
full_trajectory_suffix: str = "_full"
base_vars_to_communicate_suffix: str = "_base"
shadow_suffix: str = "_shadow"
COLLOCATION_TIME_GRID = 'collocation_time_grid'
TIME_GRID_INFO = 'time_grid_info'
PROVISION_VAR_NAME = "in_provision"
ACCEPTED_POWER_VAR_NAME = "_P_external"
RELATIVE_EVENT_START_TIME_VAR_NAME = "rel_start"
Expand All @@ -43,11 +45,9 @@

def return_baseline_cost_function(power_variable: str, comfort_variable: str) -> str:
"""Return baseline cost function

Args:
power_variable: name of the power variable
comfort_variable: name of the comfort variable

Returns:
Cost function in the baseline mpc, obj_std is to be evaluated according to
user definition
Expand Down
Loading
Loading