From 33e529629e82064e8dc778b8c59d4f9eeda1149b Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Wed, 5 Feb 2025 11:36:18 +0100 Subject: [PATCH 01/25] introduce self.time as casadi symbolic --- .../data_structures/globals.py | 8 ++++---- .../generate_flex_agents.py | 11 ----------- flexibility_quantification/utils/parsing.py | 16 +++++----------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/flexibility_quantification/data_structures/globals.py b/flexibility_quantification/data_structures/globals.py index ebe884da..7c619a0f 100644 --- a/flexibility_quantification/data_structures/globals.py +++ b/flexibility_quantification/data_structures/globals.py @@ -16,8 +16,8 @@ POWER_ALIAS_NEG = "__P_el_neg" POWER_ALIAS_POS = "__P_el_pos" -SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.Time.sym < self.prep_time.sym + " - "self.market_time.sym, obj_std, ca.if_else(self.Time.sym < " +SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.time < self.prep_time.sym + " + "self.market_time.sym, obj_std, ca.if_else(self.time < " "(self.prep_time.sym + self.flex_event_duration.sym + " "self.market_time.sym), obj_flex, obj_std))") @@ -26,8 +26,8 @@ def return_baseline_cost_function(profile_deviation_weight, power_variable): cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.Time.sym < self.rel_start.sym, obj_std, " - "ca.if_else(self.Time.sym >= self.rel_end.sym, obj_std, " + "ca.if_else(self.time < self.rel_start.sym, obj_std, " + "ca.if_else(self.time >= self.rel_end.sym, obj_std, " f"sum([{profile_deviation_weight}*(self.{power_variable} - " "self._P_external)**2]))),obj_std)") return cost_func diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py index 431416ba..3c2c0237 100644 --- a/flexibility_quantification/generate_flex_agents.py +++ b/flexibility_quantification/generate_flex_agents.py @@ -280,7 +280,6 @@ def adapt_mpc_module_config( - names of the control variables for the shadow mpcs - reduce communicated variables of shadow mpcs to outputs - add the power variable to the outputs - - add the Time variable to the inputs - add parameters for the activation and quantification of flexibility """ @@ -390,16 +389,6 @@ def adapt_mpc_module_config( alias=mpc_dataclass.power_alias, ) ) - # add inputs for the Time variable as well as extra inputs needed for activation of flex - module_config.inputs.append( - MPCVariable( - name="Time", - value=[ - i * module_config.time_step - for i in range(module_config.prediction_horizon) - ], - ) - ) module_config.inputs.extend(mpc_dataclass.config_inputs_appendix) # CONFIG_PARAMETERS_APPENDIX only includes dummy values # overwrite dummy values with values from flex config and append it to module config diff --git a/flexibility_quantification/utils/parsing.py b/flexibility_quantification/utils/parsing.py index 2ce35440..fc25fc49 100644 --- a/flexibility_quantification/utils/parsing.py +++ b/flexibility_quantification/utils/parsing.py @@ -177,11 +177,8 @@ def modify_config_class_shadow(self, node): """ # loop over config object and modify fields for body in node.body: - # add the time and full control trajectory inputs + # add full control trajectory inputs if body.target.id == "inputs": - body.value.elts.append( - add_input("Time", 0, "s", "time trajectory", "list") - ) for control in self.controls: body.value.elts.append( add_input( @@ -274,9 +271,6 @@ def modify_config_class_baseline(self, node): elif isinstance(body.value, ast.BinOp): # List concatenation case (a + b) value_list = body.value.left - value_list.elts.append( - add_input("Time", 0, "s", "time trajectory", "list") - ) value_list.elts.append( add_input( "_P_external", @@ -337,7 +331,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -345,7 +339,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], @@ -366,7 +360,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -374,7 +368,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], From cfa626999c6bdf44aa81a7d066644eb4f130adce Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Thu, 13 Feb 2025 11:47:19 +0100 Subject: [PATCH 02/25] add at Example OneRoom_SimpleMPC --- Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json | 3 +++ Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json b/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json index 164de359..8abf0faf 100644 --- a/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json +++ b/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json @@ -72,6 +72,9 @@ { "name": "P_el", "alias": "P_el" + }, + { + "name": "Time" } ], "controls": [ diff --git a/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py b/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py index 938ed90c..4e8a5798 100644 --- a/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py +++ b/Examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py @@ -88,6 +88,7 @@ class BaselineMPCModelConfig(CasadiModelConfig): unit="W", description="The power input to the system", ), + CasadiOutput(name="Time", unit="s", description="Test casadi time") ] class BaselineMPCModel(CasadiModel): @@ -99,6 +100,7 @@ def setup_system(self): self.cp * self.mDot / self.C * (self.T_in - self.T) + self.load / self.C ) self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in)/1000 + self.Time.alg = self.time # Define ae self.T_out.alg = self.T # math operation to get the symbolic variable From c511e3985fb6480d663e79a318b089c2adfeff0b Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Mon, 17 Feb 2025 12:02:17 +0100 Subject: [PATCH 03/25] adjust parsing and data handling for ml mpc --- flexibility_quantification/generate_flex_agents.py | 10 ++++++++-- flexibility_quantification/utils/data_handling.py | 2 ++ flexibility_quantification/utils/parsing.py | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py index 3c2c0237..049c2b32 100644 --- a/flexibility_quantification/generate_flex_agents.py +++ b/flexibility_quantification/generate_flex_agents.py @@ -470,8 +470,14 @@ def _generate_flex_model_definition(self): opt_backend = self.orig_mpc_module_config.optimization_backend["model"]["type"] # Extract the config class of the casadi model to check cost functions - config_class = inspect.get_annotations(custom_injection(opt_backend))["config"] - config_instance = config_class() + if self.orig_mpc_module_config.optimization_backend["type"] == "casadi_ml": + config_class = inspect.get_annotations(custom_injection(opt_backend))["config"] + ml_model_sources = self.orig_mpc_module_config.optimization_backend["model"]["ml_model_sources"] + config_instance = config_class(ml_model_sources=ml_model_sources) + else: + config_class = inspect.get_annotations(custom_injection(opt_backend))["config"] + config_instance = config_class() + self.check_variables_in_casadi_config( config_instance, self.flex_config.shadow_mpc_config_generator_data.neg_flex.flex_cost_function, diff --git a/flexibility_quantification/utils/data_handling.py b/flexibility_quantification/utils/data_handling.py index 216f600b..f5d76693 100644 --- a/flexibility_quantification/utils/data_handling.py +++ b/flexibility_quantification/utils/data_handling.py @@ -16,6 +16,8 @@ def fill_nans(series: pd.Series, method: FillNansMethods) -> pd.Series: - mean: fill NaN values with the mean of the following values. - interpolate: interpolate missing values. """ + #ignore lags from casadi_ml models + series = series[series.index >= 0] if method == MEAN: series = _set_mean_values(series=series) elif method == INTERPOLATE: diff --git a/flexibility_quantification/utils/parsing.py b/flexibility_quantification/utils/parsing.py index fc25fc49..3e4c7013 100644 --- a/flexibility_quantification/utils/parsing.py +++ b/flexibility_quantification/utils/parsing.py @@ -142,13 +142,13 @@ def visit_ClassDef(self, node): """ for base in node.bases: - if isinstance(base, ast.Name) and base.id == "CasadiModelConfig": + if isinstance(base, ast.Name) and (base.id == "CasadiModelConfig" or base.id == "CasadiMLModelConfig"): # get ast object and trigger modification self.config_obj = node self.modify_config_class(node) # change class name node.name = self.mpc_data.class_name + "Config" - if isinstance(base, ast.Name) and base.id == "CasadiModel": + if isinstance(base, ast.Name) and (base.id == "CasadiModel" or base.id == "CasadiMLModel"): # get ast object and trigger modification self.model_obj = node for item in node.body: From 52949341e7818a9a3427a214efcf99b6b89e43db Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Mon, 17 Feb 2025 12:14:56 +0100 Subject: [PATCH 04/25] add example --- .../flexibility_agent_config.json | 68 +++++++ .../flex_configs/flexibility_market.json | 27 +++ .../main_one_room_flex.py | 50 +++++ .../mpc_and_sim/Trainer/evaluation_T.png | Bin 0 -> 65260 bytes .../mpc_and_sim/Trainer/ml_model.json | 1 + .../mpc_and_sim/simple_model.json | 98 +++++++++ .../mpc_and_sim/simple_model.py | 118 +++++++++++ .../mpc_and_sim/simple_model_sim.py | 125 ++++++++++++ .../mpc_and_sim/simple_sim.json | 30 +++ .../OneRoom_SimpleLinRegMPC/plot_results.py | 186 ++++++++++++++++++ .../predictor/predictor_config.json | 26 +++ .../predictor/simple_predictor.py | 53 +++++ 12 files changed, 782 insertions(+) create mode 100644 Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json create mode 100644 Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json create mode 100644 Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py create mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/evaluation_T.png create mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json create mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json create mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py create mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py create mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json create mode 100644 Examples/OneRoom_SimpleLinRegMPC/plot_results.py create mode 100644 Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json create mode 100644 Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py diff --git a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json new file mode 100644 index 00000000..bcddabe1 --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json @@ -0,0 +1,68 @@ +{ + "prep_time": 900, + "flex_event_duration": 7200, + "market_time": 900, + "indicator_config": { + "agent_config": { + "id": "FlexibilityIndicator", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "FlexibilityIndicator", + "type": "flexibility_quantification.flexibility_indicator", + "price_variable": "r_pel", + "parameters": [ + { + "name": "prep_time", + "value": 900 + }, + { + "name": "market_time", + "value": 900 + }, + { + "name": "flex_event_duration", + "value": 7200 + }, + { + "name": "time_step", + "value": 900 + }, + { + "name": "prediction_horizon", + "value": 48 + } + ], + "inputs": [ + { + "name": "r_pel", + "alias": "r_pel" + } + ] + } + ] + }, + "name_of_created_file": "indicator.json" + }, + "market_config": "flex_configs/flexibility_market.json", + "baseline_config_generator_data": { + "power_variable": "P_el", + "power_unit": "kW", + "profile_deviation_weight": 100 + }, + "shadow_mpc_config_generator_data": { + "weights": [{"name": "s_P", "value": 10}], + "pos_flex": { + "flex_cost_function": "sum([self.s_T * self.T_slack ** 2, self.s_P * self.P_el])" + }, + "neg_flex": { + "flex_cost_function": "sum([self.s_T * self.T_slack ** 2, -self.s_P * self.P_el])" + } + }, + "path_to_flex_files": "created_flex_files", + "delete_files": false, + "overwrite_files": true +} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json new file mode 100644 index 00000000..b7b68870 --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json @@ -0,0 +1,27 @@ +{ + "agent_config": { + "id": "FlexibilityMarket", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "FlexibilityMarket", + "type": "flexibility_quantification.flexibility_market", + "time_step": 900, + "market_specs": { + "type": "single", + "cooldown": 10, + "minimum_average_flex": 0, + "options": { + "start_time": 9000, + "direction": "positive" + } + } + } + ] + + }, + "name_of_created_file": "market.json" + } \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py b/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py new file mode 100644 index 00000000..f3673d2e --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py @@ -0,0 +1,50 @@ +import logging +from flexibility_quantification.generate_flex_agents import FlexAgentGenerator +from agentlib.utils.multi_agent_system import LocalMASAgency +from flexibility_quantification.utils.interactive import Dashboard, CustomBound +from plot_results import plot_results + +# Set the log-level +logging.basicConfig(level=logging.WARN) +until = 21600 + +ENV_CONFIG = {"rt": False, "factor": 0.01, "t_sample": 60} + + +def run_example(until=until): + results = [] + mpc_config = "mpc_and_sim/simple_model.json" + sim_config = "mpc_and_sim/simple_sim.json" + predictor_config = "predictor/predictor_config.json" + flex_config = "flex_configs/flexibility_agent_config.json" + agent_configs = [sim_config, predictor_config] + + config_list = FlexAgentGenerator( + flex_config=flex_config, mpc_agent_config=mpc_config + ).generate_flex_agents() + agent_configs.extend(config_list) + + mas = LocalMASAgency( + agent_configs=agent_configs, env=ENV_CONFIG, variable_logging=False + ) + + mas.run(until=until) + results = mas.get_results(cleanup=False) + + plot_results(results_data=results) # Alternative plotscript using matplotlib, + Dashboard( + flex_config="flex_configs/flexibility_agent_config.json", + simulator_agent_config="mpc_and_sim/simple_sim.json", + results=results + ).show( + custom_bounds=CustomBound( + for_variable="T", + lb_name="T_lower", + ub_name="T_upper" + ) + ) + return results + + +if __name__ == "__main__": + run_example(until) diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/evaluation_T.png b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/evaluation_T.png new file mode 100644 index 0000000000000000000000000000000000000000..97f99021826f68ef53399ba5f6622d048ce11809 GIT binary patch literal 65260 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

bR)kM0R!If9MTs8Go8f<#tU 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 literal 0 HcmV?d00001 diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json new file mode 100644 index 00000000..976e0381 --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json @@ -0,0 +1 @@ +{"dt":900.0,"input":{"mDot":{"name":"mDot","lag":3},"load":{"name":"load","lag":2},"T_in":{"name":"T_in","lag":1}},"output":{"T":{"name":"T","lag":2,"output_type":"difference","recursive":true}},"agentlib_mpc_hash":"5302c96","training_info":null,"model_type":"LinReg","parameters":{"coef":[[-98.39137379499294,-39.21682530199981,-0.5848461265173618,2.59314258954646e-8,6.637570493239764e-9,-0.503047937143595,-0.492892772393688,0.3401290832302948]],"intercept":[187.30625067405674],"n_features_in":8,"rank":6,"singular":[35.99361869005735,18.9771165622424,0.2417850576555883,0.21421375073807045,0.023474240917668355,1.309867755231031e-12,4.219908371740855e-19,2.1045347182571248e-19]}} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json new file mode 100644 index 00000000..f86feb28 --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json @@ -0,0 +1,98 @@ +{ + "id": "FlexModel", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "BaselineMPC", + "type": "agentlib_mpc.mpc", + "optimization_backend": { + "type": "casadi_ml", + "model": { + "type": { + "file": "mpc_and_sim/simple_model.py", + "class_name": "BaselineMPCModel" + }, + "ml_model_sources": ["mpc_and_sim/Trainer/ml_model.json"] + }, + "discretization_options": { + "method": "multiple_shooting" + }, + "solver": { + "name": "ipopt", + "options": { + "ipopt": { + "max_iter": 100, + "tol": 1e-4 + } + } + }, + "results_file": "results/mpc.csv", + "save_results": true, + "overwrite_result_file": true + }, + "time_step": 900, + "prediction_horizon": 48, + "set_outputs": true, + "parameters": [ + { + "name": "s_T", + "value": 250 + }, + { + "name": "r_mDot", + "value": 1 + } + ], + "inputs": [ + { + "name": "load", + "value": 150 + }, + { + "name": "T_upper", + "value": 294.15 + }, + { + "name": "T_lower", + "value": 292.15 + }, + { + "name": "T_in", + "value": 280.15 + } + ], + "outputs": [ + { + "name": "T_out", + "alias": "T_out" + }, + { + "name": "P_el", + "alias": "P_el" + }, + { + "name": "Time" + } + ], + "controls": [ + { + "name": "mDot", + "value": 0.02, + "ub": 0.05, + "lb": 0 + } + ], + "states": [ + { + "name": "T", + "value": 298.16, + "ub": 303.15, + "lb": 288.15 + } + ] + } + ] +} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py new file mode 100644 index 00000000..21574e3e --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py @@ -0,0 +1,118 @@ +from agentlib_mpc.models.casadi_model import ( + CasadiModel, + CasadiInput, + CasadiState, + CasadiParameter, + CasadiOutput, + CasadiModelConfig, +) +from typing import List +from math import inf +from agentlib_mpc.models.casadi_ml_model import CasadiMLModel, CasadiMLModelConfig + + +class BaselineMPCModelConfig(CasadiMLModelConfig): + inputs: List[CasadiInput] = [ + # controls + CasadiInput( + name="mDot", value=0.0225, unit="kg/s", description="Air mass flow into zone" + ), + # disturbances + CasadiInput( + name="load", value=150, unit="W", description="Heat " "load into zone" + ), + CasadiInput( + name="T_in", value=280.15, unit="K", description="Inflow air temperature" + ), + # settings + CasadiInput( + name="T_upper", + value=294.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + CasadiInput( + name="T_lower", + value=292.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + ] + + states: List[CasadiState] = [ + # differential + CasadiState( + name="T", value=293.15, unit="K", description="Temperature of zone" + ), + # algebraic + # slack variables + CasadiState( + name="T_slack", + value=0, + unit="K", + description="Slack variable of temperature of zone", + ), + + ] + parameters: List[CasadiParameter] = [ + CasadiParameter( + name="cp", + value=1000, + unit="J/kg*K", + description="thermal capacity of the air", + ), + CasadiParameter( + name="C", value=100000, unit="J/K", description="thermal capacity of zone" + ), + CasadiParameter( + name="s_T", + value=1, + unit="-", + description="Weight for T in constraint function", + ), + CasadiParameter( + name="r_mDot", + value=1, + unit="-", + description="Weight for mDot in objective function", + ), + + ] + outputs: List[CasadiOutput] = [ + CasadiOutput(name="T_out", unit="K", description="Temperature of zone"), + CasadiOutput( + name="P_el", + unit="W", + description="The power input to the system", + ), + CasadiOutput(name="Time", unit="s", description="Test casadi time") + ] + +class BaselineMPCModel(CasadiMLModel): + config: BaselineMPCModelConfig + + def setup_system(self): + # Define ode + self.T_out.alg = self.T + self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in) / 1000 + self.Time.alg = self.time + + # Constraints: List[(lower bound, function, upper bound)] + self.constraints = [ + # soft constraints + (self.T_lower, self.T + self.T_slack, inf), + (-inf, self.T - self.T_slack, self.T_upper), + (0, self.T_slack, inf) + ] + # Objective function + objective = sum( + [ + self.r_mDot * self.mDot, + self.s_T * self.T_slack ** 2, + ] + ) + return objective + + + + diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py new file mode 100644 index 00000000..aa5af3c2 --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py @@ -0,0 +1,125 @@ +from agentlib_mpc.models.casadi_model import ( + CasadiModel, + CasadiInput, + CasadiState, + CasadiParameter, + CasadiOutput, + CasadiModelConfig, +) +from typing import List +from math import inf +from agentlib_mpc.models.casadi_ml_model import CasadiMLModel, CasadiMLModelConfig + + +class BaselineMPCModelConfig(CasadiModelConfig): + inputs: List[CasadiInput] = [ + # controls + CasadiInput( + name="mDot", value=0.0225, unit="kg/s", description="Air mass flow into zone" + ), + # disturbances + CasadiInput( + name="load", value=150, unit="W", description="Heat " "load into zone" + ), + CasadiInput( + name="T_in", value=280.15, unit="K", description="Inflow air temperature" + ), + # settings + CasadiInput( + name="T_upper", + value=294.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + CasadiInput( + name="T_lower", + value=292.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + + ] + + states: List[CasadiState] = [ + CasadiState(name="t_sim", value=0, unit="sec", description="simulation time"), + + # differential + CasadiState( + name="T", value=293.15, unit="K", description="Temperature of zone" + ), + # algebraic + # slack variables + CasadiState( + name="T_slack", + value=0, + unit="K", + description="Slack variable of temperature of zone", + ), + + ] + + parameters: List[CasadiParameter] = [ + CasadiParameter( + name="cp", + value=1000, + unit="J/kg*K", + description="thermal capacity of the air", + ), + CasadiParameter( + name="C", value=100000, unit="J/K", description="thermal capacity of zone" + ), + CasadiParameter( + name="s_T", + value=1, + unit="-", + description="Weight for T in constraint function", + ), + CasadiParameter( + name="r_mDot", + value=1, + unit="-", + description="Weight for mDot in objective function", + ), + + ] + outputs: List[CasadiOutput] = [ + CasadiOutput(name="T_out", unit="K", description="Temperature of zone"), + CasadiOutput( + name="P_el", + unit="W", + description="The power input to the system", + ), + CasadiOutput(name="Time", unit="s", description="Test casadi time") + ] + +class BaselineMPCModel(CasadiModel): + config: BaselineMPCModelConfig + + def setup_system(self): + # Define ode + self.T.ode = ( + self.cp * self.mDot / self.C * (self.T_in - self.T) + self.load / self.C + ) + self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in)/1000 + self.Time.alg = self.time + + # Define ae + self.T_out.alg = self.T # math operation to get the symbolic variable + # Constraints: List[(lower bound, function, upper bound)] + self.constraints = [ + # soft constraints + (self.T_lower, self.T + self.T_slack, inf), + (-inf, self.T - self.T_slack, self.T_upper), + (0, self.T_slack, inf) + ] + # Objective function + objective = sum( + [ + self.r_mDot * self.mDot, + self.s_T * self.T_slack**2, + ] + ) + return objective + + + diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json new file mode 100644 index 00000000..cb1fada9 --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json @@ -0,0 +1,30 @@ +{ + "id": "SimAgent", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "room", + "type": "simulator", + "model": { + "type": {"file": "mpc_and_sim/simple_model_sim.py", "class_name": "BaselineMPCModel"}, + "states": [{"name": "T", "value": 298}] + + }, + "t_sample": 10, + "update_inputs_on_callback": false, + "save_results": true, + "result_filename": "results/sim_room.csv", + "overwrite_result_file": true, + "outputs": [ + {"name": "T_out", "alias": "T"}, + {"name": "P_el","alias": "P_el_sim"} + ], + "inputs": [ + {"name": "mDot", "value": 0.02, "alias": "mDot"} + ] + } + ] +} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/plot_results.py b/Examples/OneRoom_SimpleLinRegMPC/plot_results.py new file mode 100644 index 00000000..f02d89f1 --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/plot_results.py @@ -0,0 +1,186 @@ +import numpy as np +import matplotlib.pyplot as plt +import agentlib_mpc.utils.plotting.basic as mpcplot +from agentlib_mpc.utils.analysis import mpc_at_time_step +from flexibility_quantification.data_structures.flex_results import Results + + +def plot_results(results_data: dict = None): + """ + Example how plotting with matplotlib and mpcplot from agentlib_mpc works + """ + if results_data is None: + res = Results( + flex_config="flex_configs/flexibility_agent_config.json", + simulator_agent_config="mpc_and_sim/simple_sim.json", + results="results" + ) + else: + res = Results( + flex_config="flex_configs/flexibility_agent_config.json", + simulator_agent_config="mpc_and_sim/simple_sim.json", + results=results_data + ) + + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=2) + (ax1, ax2) = axs + # load + ax1.set_ylabel(r"$\dot{Q}_{Room}$ in W") + res.df_simulation["load"].plot(ax=ax1) + # T_in + ax2.set_ylabel("$T_{in}$ in K") + res.df_simulation["T_in"].plot(ax=ax2) + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax2.set_xticks(x_ticks) + ax2.set_xticklabels(x_tick_labels) + ax2.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + + # room temp + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=1) + ax1 = axs[0] + # T out + ax1.set_ylabel("$T_{room}$ in K") + res.df_simulation["T_upper"].plot(ax=ax1, color="0.5") + res.df_simulation["T_lower"].plot(ax=ax1, color="0.5") + res.df_simulation["T_out"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) + mpc_at_time_step( + data=res.df_neg_flex, time_step=9000, variable="T" + ).plot(ax=ax1, label="neg", linestyle="--", color=mpcplot.EBCColors.red) + mpc_at_time_step( + data=res.df_pos_flex, time_step=9000, variable="T" + ).plot(ax=ax1, label="pos", linestyle="--", color=mpcplot.EBCColors.blue) + mpc_at_time_step( + data=res.df_baseline, time_step=9900, variable="T" + ).plot(ax=ax1, label="base", linestyle="--", color=mpcplot.EBCColors.dark_grey) + + ax1.legend() + ax1.vlines(9000, ymin=0, ymax=500, colors="black") + ax1.vlines(9900, ymin=0, ymax=500, colors="black") + ax1.vlines(10800, ymin=0, ymax=500, colors="black") + ax1.vlines(18000, ymin=0, ymax=500, colors="black") + + ax1.set_ylim(289, 299) + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax1.set_xticks(x_ticks) + ax1.set_xticklabels(x_tick_labels) + ax1.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + + # predictions + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=2) + (ax1, ax2) = axs + # P_el + ax1.set_ylabel("$P_{el}$ in kW") + res.df_simulation["P_el"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) + mpc_at_time_step( + data=res.df_neg_flex, time_step=9000, variable="P_el" + ).ffill().plot( + ax=ax1, + drawstyle="steps-post", + label="neg", + linestyle="--", + color=mpcplot.EBCColors.red, + ) + mpc_at_time_step( + data=res.df_pos_flex, time_step=9000, variable="P_el" + ).ffill().plot( + ax=ax1, + drawstyle="steps-post", + label="pos", + linestyle="--", + color=mpcplot.EBCColors.blue, + ) + mpc_at_time_step( + data=res.df_baseline, time_step=9000, variable="P_el" + ).ffill().plot( + ax=ax1, + drawstyle="steps-post", + label="base", + linestyle="--", + color=mpcplot.EBCColors.dark_grey, + ) + ax1.legend() + ax1.vlines(9000, ymin=-1000, ymax=5000, colors="black") + ax1.vlines(9900, ymin=-1000, ymax=5000, colors="black") + ax1.vlines(10800, ymin=-1000, ymax=5000, colors="black") + ax1.vlines(18000, ymin=-1000, ymax=5000, colors="black") + ax1.set_ylim(-0.1, 1) + + # mdot + ax2.set_ylabel(r"$\dot{m}$ in kg/s") + res.df_simulation["mDot"].plot(ax=ax2, color=mpcplot.EBCColors.dark_grey) + mpc_at_time_step( + data=res.df_neg_flex, time_step=9000, variable="mDot" + ).ffill().plot( + ax=ax2, + drawstyle="steps-post", + label="neg", + linestyle="--", + color=mpcplot.EBCColors.red, + ) + mpc_at_time_step( + data=res.df_pos_flex, time_step=9000, variable="mDot" + ).ffill().plot( + ax=ax2, + drawstyle="steps-post", + label="pos", + linestyle="--", + color=mpcplot.EBCColors.blue, + ) + mpc_at_time_step( + data=res.df_baseline, time_step=9900, variable="mDot" + ).ffill().plot( + ax=ax2, + drawstyle="steps-post", + label="base", + linestyle="--", + color=mpcplot.EBCColors.dark_grey, + ) + ax2.legend() + ax2.vlines(9000, ymin=0, ymax=500, colors="black") + ax2.vlines(9900, ymin=0, ymax=500, colors="black") + ax2.vlines(10800, ymin=0, ymax=500, colors="black") + ax2.vlines(18000, ymin=0, ymax=500, colors="black") + + ax2.set_ylim(0, 0.06) + + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax2.set_xticks(x_ticks) + ax2.set_xticklabels(x_tick_labels) + ax2.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + + # flexibility + # get only the first prediction time of each time step + energy_flex_neg = res.df_indicator.xs("energyflex_neg", axis=1).droplevel(1).dropna() + energy_flex_pos = res.df_indicator.xs("energyflex_pos", axis=1).droplevel(1).dropna() + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=1) + ax1 = axs[0] + ax1.set_ylabel(r"$\epsilon$ in kWh") + energy_flex_neg.plot(ax=ax1, label="neg") + energy_flex_pos.plot(ax=ax1, label="pos") + energy_flex_neg.plot(ax=ax1, label="neg", color=mpcplot.EBCColors.red) + energy_flex_pos.plot(ax=ax1, label="pos", color=mpcplot.EBCColors.blue) + + ax1.legend() + + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax1.set_xticks(x_ticks) + ax1.set_xticklabels(x_tick_labels) + ax1.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + # + plt.show() diff --git a/Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json b/Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json new file mode 100644 index 00000000..baa0c6ad --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json @@ -0,0 +1,26 @@ +{ + "id": "myPredictorAgent", + "modules": [ + { + "module_id": "Ag4Com", + "type": "local_broadcast" + }, + { + "module_id": "MyPredictor", + "type": { + "file": "predictor/simple_predictor.py", + "class_name": "PredictorModule" + }, + "parameters": [ + { + "name": "time_step", + "value": 900 + }, + { + "name": "prediction_horizon", + "value": 49 + } + ] + } + ] +} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py b/Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py new file mode 100644 index 00000000..9bb3a2ea --- /dev/null +++ b/Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py @@ -0,0 +1,53 @@ +import agentlib as al +import numpy as np +import pandas as pd +from agentlib.core import Agent +from typing import List +import json +import csv +from datetime import datetime + +class PredictorModuleConfig(al.BaseModuleConfig): + """Module that outputs a prediction of the heat load at a specified + interval.""" + outputs: al.AgentVariables = [ + al.AgentVariable( + name="r_pel", unit="ct/kWh", type="pd.Series", description="Weight for P_el in objective function" + ), + ] + parameters: al.AgentVariables = [ + al.AgentVariable( + name="time_step", value=900, description="Sampling time for prediction." + ), + al.AgentVariable( + name="prediction_horizon", + value=8, + description="Number of sampling points for prediction.", + ) + ] + + + shared_variable_fields:List[str] = ["outputs"] + + +class PredictorModule(al.BaseModule): + """Module that outputs a prediction of the heat load at a specified + interval.""" + + config: PredictorModuleConfig + + def register_callbacks(self): + pass + + def process(self): + while True: + sample_time = self.env.config.t_sample + ts = self.get("time_step").value + k = self.get("prediction_horizon").value + now = self.env.now + + grid = np.arange(now, now + k * ts + 1, sample_time) + p_traj = pd.Series([1 for i in grid], index=list(grid)) + self.set("r_pel", p_traj) + + yield self.env.timeout(sample_time) From 82402538bf90395d817457548e916da5ae14ecbc Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Thu, 22 May 2025 11:48:55 +0200 Subject: [PATCH 05/25] change requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4887de6b..ccc0b96f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ setuptools agentlib[full]==0.8.2 -agentlib_mpc[full] @ git+https://github.com/RWTH-EBC/AgentLib-MPC.git@d1a9c206d3f83581fd7dd12a8ed583388ac15b3e +agentlib_mpc[full] @ git+https://github.com/RWTH-EBC/AgentLib-MPC.git@43-combine_newObj_with_OL pathlib astor==0.8.1 black From 43a653889c83f8ad2ef3f4bb6087bd8621bee81c Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Thu, 22 May 2025 17:11:46 +0200 Subject: [PATCH 06/25] change self.time --- .../data_structures/globals.py | 12 ++++++------ .../generate_flex_agents.py | 12 +----------- flexibility_quantification/utils/parsing.py | 16 +++++----------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/flexibility_quantification/data_structures/globals.py b/flexibility_quantification/data_structures/globals.py index 5cbb991c..e347d4f7 100644 --- a/flexibility_quantification/data_structures/globals.py +++ b/flexibility_quantification/data_structures/globals.py @@ -21,8 +21,8 @@ STORED_ENERGY_ALIAS_NEG = "_E_stored_neg" STORED_ENERGY_ALIAS_POS = "_E_stored_pos" -SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.Time.sym < self.prep_time.sym + " - "self.market_time.sym, obj_std, ca.if_else(self.Time.sym < " +SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.time < self.prep_time.sym + " + "self.market_time.sym, obj_std, ca.if_else(self.time < " "(self.prep_time.sym + self.flex_event_duration.sym + " "self.market_time.sym), obj_flex, obj_std))") @@ -33,15 +33,15 @@ def return_baseline_cost_function(power_variable, comfort_variable): if comfort_variable: cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.Time.sym < self.rel_start.sym, obj_std, " - "ca.if_else(self.Time.sym >= self.rel_end.sym, obj_std, " + "ca.if_else(self.time < self.rel_start.sym, obj_std, " + "ca.if_else(self.time >= self.rel_end.sym, obj_std, " f"sum([self.profile_deviation_weight*(self.{power_variable} - " f"self._P_external)**2, " f"self.{comfort_variable} * self.profile_comfort_weight]))),obj_std)") else: cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.Time.sym < self.rel_start.sym, obj_std, " - "ca.if_else(self.Time.sym >= self.rel_end.sym, obj_std, " + "ca.if_else(self.time < self.rel_start.sym, obj_std, " + "ca.if_else(self.time >= self.rel_end.sym, obj_std, " f"sum([self.profile_deviation_weight*(self.{power_variable} - " f"self._P_external)**2]))),obj_std)") return cost_func diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py index 9cf76fe2..bcadeec3 100644 --- a/flexibility_quantification/generate_flex_agents.py +++ b/flexibility_quantification/generate_flex_agents.py @@ -316,7 +316,6 @@ def adapt_mpc_module_config( - names of the control variables for the shadow mpcs - reduce communicated variables of shadow mpcs to outputs - add the power variable to the outputs - - add the Time variable to the inputs - add parameters for the activation and quantification of flexibility """ @@ -432,16 +431,7 @@ def adapt_mpc_module_config( self.indicator_module_config.correct_costs.stored_energy_variable ].alias = mpc_dataclass.stored_energy_alias - # add inputs for the Time variable as well as extra inputs needed for activation of flex - module_config.inputs.append( - MPCVariable( - name="Time", - value=[ - i * module_config.time_step - for i in range(module_config.prediction_horizon) - ], - ) - ) + module_config.inputs.extend(mpc_dataclass.config_inputs_appendix) # CONFIG_PARAMETERS_APPENDIX only includes dummy values # overwrite dummy values with values from flex config and append it to module config diff --git a/flexibility_quantification/utils/parsing.py b/flexibility_quantification/utils/parsing.py index 49edde1f..dcdc4178 100644 --- a/flexibility_quantification/utils/parsing.py +++ b/flexibility_quantification/utils/parsing.py @@ -203,11 +203,8 @@ def modify_config_class_shadow(self, node): """ # loop over config object and modify fields for body in node.body: - # add the time and full control trajectory inputs + # add control trajectory inputs if body.target.id == "inputs": - body.value.elts.append( - add_input("Time", 0, "s", "time trajectory", "list") - ) for control in self.controls: body.value.elts.append( add_input( @@ -300,9 +297,6 @@ def modify_config_class_baseline(self, node): elif isinstance(body.value, ast.BinOp) or isinstance(body.value, ast.Tuple): # Complex case with concatenated lists or tuple value_list = self.get_leftmost_list(body.value) - value_list.elts.append( - add_input("Time", 0, "s", "time trajectory", "list") - ) value_list.elts.append( add_input( "_P_external", @@ -370,7 +364,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -378,7 +372,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], @@ -399,7 +393,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -407,7 +401,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], From b07d8a3ad2bf34d9986914c753287615e52cf0b5 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Fri, 23 May 2025 14:16:44 +0200 Subject: [PATCH 07/25] first approach for new objective handling --- .../data_structures/mpcs.py | 6 +- .../generate_flex_agents.py | 45 ++++- flexibility_quantification/utils/parsing.py | 181 +++++++++++++----- 3 files changed, 166 insertions(+), 66 deletions(-) diff --git a/flexibility_quantification/data_structures/mpcs.py b/flexibility_quantification/data_structures/mpcs.py index 9cf5587a..2eb47b69 100644 --- a/flexibility_quantification/data_structures/mpcs.py +++ b/flexibility_quantification/data_structures/mpcs.py @@ -1,6 +1,6 @@ import pydantic from pydantic import ConfigDict, model_validator -from typing import List, Optional +from typing import List, Optional, Union from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable import flexibility_quantification.data_structures.globals as glbs import flexibility_quantification.utils.config_management as cmng @@ -107,7 +107,7 @@ class PFMPCData(BaseMPCData): # variables power_alias: str = glbs.POWER_ALIAS_POS stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_POS - flex_cost_function: str = pydantic.Field( + flex_cost_function: Optional[Union[list[str], str]] = pydantic.Field( default=None, description="Cost function of the PF-MPC", ) @@ -140,7 +140,7 @@ class NFMPCData(BaseMPCData): # variables power_alias: str = glbs.POWER_ALIAS_NEG stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_NEG - flex_cost_function: str = pydantic.Field( + flex_cost_function: Optional[Union[list[str], str]] = pydantic.Field( default=None, description="Cost function of the NF-MPC", ) diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py index bcadeec3..842417ba 100644 --- a/flexibility_quantification/generate_flex_agents.py +++ b/flexibility_quantification/generate_flex_agents.py @@ -570,30 +570,55 @@ def _generate_flex_model_definition(self): with open(output_file, "w") as f: f.write(formatted_code) - def check_variables_in_casadi_config(self, config: CasadiModelConfig, expr: str): + def check_variables_in_casadi_config(self, config: CasadiModelConfig, expr): """Check if all variables in the expression are defined in the config. Args: config (CasadiModelConfig): casadi model config. - expr (str): The expression to check. + expr (str or list[str]): The expression to check, can be a string or list of strings. Raises: ValueError: If any variable in the expression is not defined in the config. - """ variables_in_config = set(config.get_variable_names()) - variables_in_cost_function = set(ast.walk(ast.parse(expr))) - variables_in_cost_function = { - node.attr - for node in variables_in_cost_function - if isinstance(node, ast.Attribute) - } + variables_in_cost_function = set() + + # Handle both string and list of strings + if isinstance(expr, list): + # Join all lines into a single string for parsing + combined_expr = "\n".join(expr) + else: + combined_expr = expr + + try: + # Parse the code and extract all self.attribute references + parsed_tree = ast.parse(combined_expr) + for node in ast.walk(parsed_tree): + if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == 'self': + variables_in_cost_function.add(node.attr) + except SyntaxError as e: + # If we can't parse it as a whole, try line by line + logging.warning(f"Could not parse full cost function, trying line by line: {e}") + for line in combined_expr.splitlines(): + try: + parsed = ast.parse(line) + for node in ast.walk(parsed): + if isinstance(node, ast.Attribute) and isinstance(node.value, + ast.Name) and node.value.id == 'self': + variables_in_cost_function.add(node.attr) + except SyntaxError: + logging.warning(f"Could not parse line: {line}") + variables_newly_created = set( weight.name for weight in self.flex_config.shadow_mpc_config_generator_data.weights ) + + # Add common parameter names that are known to be defined + known_vars = {"time", "market_time", "prep_time", "flex_event_duration"} + unknown_vars = ( - variables_in_cost_function - variables_in_config - variables_newly_created + variables_in_cost_function - variables_in_config - variables_newly_created - known_vars ) if unknown_vars: raise ValueError(f"Unknown variables in new cost function: {unknown_vars}") diff --git a/flexibility_quantification/utils/parsing.py b/flexibility_quantification/utils/parsing.py index dcdc4178..01242ca6 100644 --- a/flexibility_quantification/utils/parsing.py +++ b/flexibility_quantification/utils/parsing.py @@ -125,10 +125,14 @@ def visit_Module(self, module): if isinstance(self.mpc_data, BaselineMPCData): module = add_import_to_tree(name="pandas", alias="pd", tree=module) module = add_import_to_tree(name="casadi", alias="ca", tree=module) - # delete imports for shadow MPCs - if isinstance(self.mpc_data, (NFMPCData, PFMPCData)): + module = add_objective_imports_to_tree(tree=module) # Add objective imports + elif isinstance(self.mpc_data, (NFMPCData, PFMPCData)): module = remove_all_imports_from_tree(module) - # trigger the next visit method (ClassDef) + # Still need these imports for shadow MPCs + module = add_import_to_tree(name="pandas", alias="pd", tree=module) + module = add_import_to_tree(name="casadi", alias="ca", tree=module) + module = add_objective_imports_to_tree(tree=module) # Add objective imports + self.generic_visit(module) return module @@ -416,41 +420,54 @@ def modify_setup_system_shadow(self, node): ) item.value.elts.append(new_element) break - # loop through setup_system function to find return statement for i, stmt in enumerate(node.body): if isinstance(stmt, ast.Return): # store current return statement original_return = stmt.value - new_body = [ - # create new standard objective variable - ast.Assign( - targets=[ast.Name(id="obj_std", ctx=ast.Store())], - value=original_return, - ), - # create flex objective variable - ast.Assign( - targets=[ast.Name(id="obj_flex", ctx=ast.Store())], - value=ast.parse( - self.mpc_data.flex_cost_function, mode="eval" - ).body, - ), - # overwrite return statement with custom function - ast.Return(value=ast.parse(SHADOW_MPC_COST_FUNCTION).body[0].value), - ] - # append new variables to end of function - node.body[i:] = new_body - break - - def modify_setup_system_baseline(self, node): - """Modify the setup_system method of the baseline mpc model class. - This method changes the return statement of the setup_system method and adds - all necessary new lines of code. + # Create standard objective statement + std_obj_assign = ast.Assign( + targets=[ast.Name(id="obj_std", ctx=ast.Store())], + value=original_return + ) - Args: - node (ast.FunctionDef): The function definition node of setup_system. + # Create market time condition + market_time_condition = ast.parse( + "market_time_condition = ca.logic_and(" + "self.time >= (self.prep_time.sym + self.market_time.sym), " + "self.time < (self.prep_time.sym + self.flex_event_duration.sym + self.market_time.sym))" + ).body[0] + + # Parse and execute flex objective code lines + flex_obj_statements = [] + if isinstance(self.mpc_data.flex_cost_function, list): + for line in self.mpc_data.flex_cost_function: + try: + parsed_stmt = ast.parse(line).body + flex_obj_statements.extend(parsed_stmt) + except SyntaxError as e: + print(f"Could not parse flex cost function line: {line}, error: {e}") + else: + try: + flex_obj_statements = ast.parse(self.mpc_data.flex_cost_function).body + except SyntaxError as e: + print(f"Could not parse flex cost function: {self.mpc_data.flex_cost_function}, error: {e}") + + # Create conditional objective + cond_obj_stmt = ast.parse( + "cond_obj = ConditionalObjective((market_time_condition, obj_flex), default_objective=obj_std)" + ).body[0] + + # Return conditional objective + return_stmt = ast.Return(value=ast.Name(id="cond_obj", ctx=ast.Load())) + + # Replace the return statement with our new statements + new_body = [std_obj_assign, market_time_condition] + flex_obj_statements + [cond_obj_stmt, return_stmt] + node.body[i:i + 1] = new_body + break - """ + def modify_setup_system_baseline(self, node): + """Modify the setup_system method of the baseline mpc model class.""" # set the control trajectories with the respective variables if self.binary_controls: controls_list = self.controls + self.binary_controls @@ -462,7 +479,7 @@ def modify_setup_system_baseline(self, node): ast.Attribute( value=ast.Name(id="self", ctx=ast.Load()), attr=f"{full_trajectory_prefix}{control.name}" - f"{full_trajectory_suffix}.alg", + f"{full_trajectory_suffix}.alg", ctx=ast.Store(), ) ], @@ -477,28 +494,56 @@ def modify_setup_system_baseline(self, node): # loop through setup_system function to find return statement for i, stmt in enumerate(node.body): if isinstance(stmt, ast.Return): - # store current return statement + # Store current return statement original_return = stmt.value - new_body = [ - # create new standard objective variable - ast.Assign( - targets=[ast.Name(id="obj_std", ctx=ast.Store())], - value=original_return, - ), - # overwrite return statement with custom function - ast.Return( - value=ast.parse( - return_baseline_cost_function( - power_variable=self.mpc_data.power_variable, - comfort_variable=self.mpc_data.comfort_variable - ) - ) - .body[0] - .value - ), - ] - # append new variables to end of function - node.body[i:] = full_traj_list + new_body + + # Create standard objective + std_obj_assign = ast.Assign( + targets=[ast.Name(id="obj_std", ctx=ast.Store())], + value=original_return + ) + + # Create provision objective + if self.mpc_data.comfort_variable: + prov_obj_code = ast.parse( + f"provision_obj = FullObjective(" + f" SqObjective(expressions=self.{self.mpc_data.power_variable} - self._P_external, " + f" weight=self.profile_deviation_weight, " + f" name='profile_deviation')," + f" SqObjective(expressions=self.{self.mpc_data.comfort_variable}, " + f" weight=self.profile_comfort_weight, " + f" name='comfort')," + f" normalization=43200" + f")" + ).body[0] + else: + prov_obj_code = ast.parse( + f"provision_obj = FullObjective(" + f" SqObjective(expressions=self.{self.mpc_data.power_variable} - self._P_external, " + f" weight=self.profile_deviation_weight, " + f" name='profile_deviation')," + f" normalization=43200" + f")" + ).body[0] + + # Create provision time condition + provision_cond_stmt = ast.parse( + "provision_condition = ca.logic_and(self.in_provision.sym, " + "ca.logic_and(self.time >= self.rel_start.sym, " + " self.time < self.rel_end.sym))" + ).body[0] + + # Create conditional objective + cond_obj_code = ast.parse( + "cond_obj = ConditionalObjective((provision_condition, provision_obj), default_objective=obj_std)" + ).body[0] + + # Return conditional objective + return_stmt = ast.Return(value=ast.Name(id="cond_obj", ctx=ast.Load())) + + # Replace the return statement with our new statements + new_body = [std_obj_assign, prov_obj_code, provision_cond_stmt, cond_obj_code, return_stmt] + node.body[i:i + 1] = full_traj_list + new_body break @@ -519,6 +564,36 @@ def add_import_to_tree(name: str, alias: str, tree: ast.Module): return tree +def add_objective_imports_to_tree(tree: ast.Module): + """Add imports for objective classes to the AST""" + objective_imports = ast.ImportFrom( + module="agentlib_mpc.data_structures.objective", + names=[ + ast.alias(name="FullObjective", asname=None), + ast.alias(name="EqObjective", asname=None), + ast.alias(name="DeltaUObjective", asname=None), + ast.alias(name="SqObjective", asname=None), + ast.alias(name="ConditionalObjective", asname=None), + ], + level=0 + ) + + # Check if the import already exists + for node in tree.body: + if (isinstance(node, ast.ImportFrom) and + node.module == "agentlib_mpc.data_structures.objective"): + # Add any missing names to the existing import + existing_names = {alias.name for alias in node.names} + for name in ["FullObjective", "EqObjective", "DeltaUObjective", "SqObjective", "ConditionalObjective"]: + if name not in existing_names: + node.names.append(ast.alias(name=name, asname=None)) + return tree + + # Import doesn't exist, add it + tree.body.insert(0, objective_imports) + return tree + + def remove_all_imports_from_tree(tree: ast.Module): # Create a new list to hold nodes that are not imports new_body = [ From 1097e96d5d484646e72804e2fbc1a58dd1020793 Mon Sep 17 00:00:00 2001 From: Sarah Leidolf Date: Wed, 2 Jul 2025 11:56:45 +0200 Subject: [PATCH 08/25] Revert "first approach for new objective handling" This reverts commit b07d8a3ad2bf34d9986914c753287615e52cf0b5 --- .../data_structures/mpcs.py | 6 +- .../generate_flex_agents.py | 45 +---- flexibility_quantification/utils/parsing.py | 181 +++++------------- 3 files changed, 66 insertions(+), 166 deletions(-) diff --git a/flexibility_quantification/data_structures/mpcs.py b/flexibility_quantification/data_structures/mpcs.py index 2eb47b69..9cf5587a 100644 --- a/flexibility_quantification/data_structures/mpcs.py +++ b/flexibility_quantification/data_structures/mpcs.py @@ -1,6 +1,6 @@ import pydantic from pydantic import ConfigDict, model_validator -from typing import List, Optional, Union +from typing import List, Optional from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable import flexibility_quantification.data_structures.globals as glbs import flexibility_quantification.utils.config_management as cmng @@ -107,7 +107,7 @@ class PFMPCData(BaseMPCData): # variables power_alias: str = glbs.POWER_ALIAS_POS stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_POS - flex_cost_function: Optional[Union[list[str], str]] = pydantic.Field( + flex_cost_function: str = pydantic.Field( default=None, description="Cost function of the PF-MPC", ) @@ -140,7 +140,7 @@ class NFMPCData(BaseMPCData): # variables power_alias: str = glbs.POWER_ALIAS_NEG stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_NEG - flex_cost_function: Optional[Union[list[str], str]] = pydantic.Field( + flex_cost_function: str = pydantic.Field( default=None, description="Cost function of the NF-MPC", ) diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py index c5189b9f..43f5f010 100644 --- a/flexibility_quantification/generate_flex_agents.py +++ b/flexibility_quantification/generate_flex_agents.py @@ -576,55 +576,30 @@ def _generate_flex_model_definition(self): with open(output_file, "w") as f: f.write(formatted_code) - def check_variables_in_casadi_config(self, config: CasadiModelConfig, expr): + def check_variables_in_casadi_config(self, config: CasadiModelConfig, expr: str): """Check if all variables in the expression are defined in the config. Args: config (CasadiModelConfig): casadi model config. - expr (str or list[str]): The expression to check, can be a string or list of strings. + expr (str): The expression to check. Raises: ValueError: If any variable in the expression is not defined in the config. + """ variables_in_config = set(config.get_variable_names()) - variables_in_cost_function = set() - - # Handle both string and list of strings - if isinstance(expr, list): - # Join all lines into a single string for parsing - combined_expr = "\n".join(expr) - else: - combined_expr = expr - - try: - # Parse the code and extract all self.attribute references - parsed_tree = ast.parse(combined_expr) - for node in ast.walk(parsed_tree): - if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == 'self': - variables_in_cost_function.add(node.attr) - except SyntaxError as e: - # If we can't parse it as a whole, try line by line - logging.warning(f"Could not parse full cost function, trying line by line: {e}") - for line in combined_expr.splitlines(): - try: - parsed = ast.parse(line) - for node in ast.walk(parsed): - if isinstance(node, ast.Attribute) and isinstance(node.value, - ast.Name) and node.value.id == 'self': - variables_in_cost_function.add(node.attr) - except SyntaxError: - logging.warning(f"Could not parse line: {line}") - + variables_in_cost_function = set(ast.walk(ast.parse(expr))) + variables_in_cost_function = { + node.attr + for node in variables_in_cost_function + if isinstance(node, ast.Attribute) + } variables_newly_created = set( weight.name for weight in self.flex_config.shadow_mpc_config_generator_data.weights ) - - # Add common parameter names that are known to be defined - known_vars = {"time", "market_time", "prep_time", "flex_event_duration"} - unknown_vars = ( - variables_in_cost_function - variables_in_config - variables_newly_created - known_vars + variables_in_cost_function - variables_in_config - variables_newly_created ) if unknown_vars: raise ValueError(f"Unknown variables in new cost function: {unknown_vars}") diff --git a/flexibility_quantification/utils/parsing.py b/flexibility_quantification/utils/parsing.py index 576bdc8c..4492ab85 100644 --- a/flexibility_quantification/utils/parsing.py +++ b/flexibility_quantification/utils/parsing.py @@ -125,14 +125,10 @@ def visit_Module(self, module): if isinstance(self.mpc_data, BaselineMPCData): module = add_import_to_tree(name="pandas", alias="pd", tree=module) module = add_import_to_tree(name="casadi", alias="ca", tree=module) - module = add_objective_imports_to_tree(tree=module) # Add objective imports - elif isinstance(self.mpc_data, (NFMPCData, PFMPCData)): + # delete imports for shadow MPCs + if isinstance(self.mpc_data, (NFMPCData, PFMPCData)): module = remove_all_imports_from_tree(module) - # Still need these imports for shadow MPCs - module = add_import_to_tree(name="pandas", alias="pd", tree=module) - module = add_import_to_tree(name="casadi", alias="ca", tree=module) - module = add_objective_imports_to_tree(tree=module) # Add objective imports - + # trigger the next visit method (ClassDef) self.generic_visit(module) return module @@ -420,54 +416,41 @@ def modify_setup_system_shadow(self, node): ) item.value.elts.append(new_element) break + # loop through setup_system function to find return statement for i, stmt in enumerate(node.body): if isinstance(stmt, ast.Return): # store current return statement original_return = stmt.value - - # Create standard objective statement - std_obj_assign = ast.Assign( - targets=[ast.Name(id="obj_std", ctx=ast.Store())], - value=original_return - ) - - # Create market time condition - market_time_condition = ast.parse( - "market_time_condition = ca.logic_and(" - "self.time >= (self.prep_time.sym + self.market_time.sym), " - "self.time < (self.prep_time.sym + self.flex_event_duration.sym + self.market_time.sym))" - ).body[0] - - # Parse and execute flex objective code lines - flex_obj_statements = [] - if isinstance(self.mpc_data.flex_cost_function, list): - for line in self.mpc_data.flex_cost_function: - try: - parsed_stmt = ast.parse(line).body - flex_obj_statements.extend(parsed_stmt) - except SyntaxError as e: - print(f"Could not parse flex cost function line: {line}, error: {e}") - else: - try: - flex_obj_statements = ast.parse(self.mpc_data.flex_cost_function).body - except SyntaxError as e: - print(f"Could not parse flex cost function: {self.mpc_data.flex_cost_function}, error: {e}") - - # Create conditional objective - cond_obj_stmt = ast.parse( - "cond_obj = ConditionalObjective((market_time_condition, obj_flex), default_objective=obj_std)" - ).body[0] - - # Return conditional objective - return_stmt = ast.Return(value=ast.Name(id="cond_obj", ctx=ast.Load())) - - # Replace the return statement with our new statements - new_body = [std_obj_assign, market_time_condition] + flex_obj_statements + [cond_obj_stmt, return_stmt] - node.body[i:i + 1] = new_body + new_body = [ + # create new standard objective variable + ast.Assign( + targets=[ast.Name(id="obj_std", ctx=ast.Store())], + value=original_return, + ), + # create flex objective variable + ast.Assign( + targets=[ast.Name(id="obj_flex", ctx=ast.Store())], + value=ast.parse( + self.mpc_data.flex_cost_function, mode="eval" + ).body, + ), + # overwrite return statement with custom function + ast.Return(value=ast.parse(SHADOW_MPC_COST_FUNCTION).body[0].value), + ] + # append new variables to end of function + node.body[i:] = new_body break def modify_setup_system_baseline(self, node): - """Modify the setup_system method of the baseline mpc model class.""" + """Modify the setup_system method of the baseline mpc model class. + + This method changes the return statement of the setup_system method and adds + all necessary new lines of code. + + Args: + node (ast.FunctionDef): The function definition node of setup_system. + + """ # set the control trajectories with the respective variables if self.binary_controls: controls_list = self.controls + self.binary_controls @@ -479,7 +462,7 @@ def modify_setup_system_baseline(self, node): ast.Attribute( value=ast.Name(id="self", ctx=ast.Load()), attr=f"{full_trajectory_prefix}{control.name}" - f"{full_trajectory_suffix}.alg", + f"{full_trajectory_suffix}.alg", ctx=ast.Store(), ) ], @@ -494,56 +477,28 @@ def modify_setup_system_baseline(self, node): # loop through setup_system function to find return statement for i, stmt in enumerate(node.body): if isinstance(stmt, ast.Return): - # Store current return statement + # store current return statement original_return = stmt.value - - # Create standard objective - std_obj_assign = ast.Assign( - targets=[ast.Name(id="obj_std", ctx=ast.Store())], - value=original_return - ) - - # Create provision objective - if self.mpc_data.comfort_variable: - prov_obj_code = ast.parse( - f"provision_obj = FullObjective(" - f" SqObjective(expressions=self.{self.mpc_data.power_variable} - self._P_external, " - f" weight=self.profile_deviation_weight, " - f" name='profile_deviation')," - f" SqObjective(expressions=self.{self.mpc_data.comfort_variable}, " - f" weight=self.profile_comfort_weight, " - f" name='comfort')," - f" normalization=43200" - f")" - ).body[0] - else: - prov_obj_code = ast.parse( - f"provision_obj = FullObjective(" - f" SqObjective(expressions=self.{self.mpc_data.power_variable} - self._P_external, " - f" weight=self.profile_deviation_weight, " - f" name='profile_deviation')," - f" normalization=43200" - f")" - ).body[0] - - # Create provision time condition - provision_cond_stmt = ast.parse( - "provision_condition = ca.logic_and(self.in_provision.sym, " - "ca.logic_and(self.time >= self.rel_start.sym, " - " self.time < self.rel_end.sym))" - ).body[0] - - # Create conditional objective - cond_obj_code = ast.parse( - "cond_obj = ConditionalObjective((provision_condition, provision_obj), default_objective=obj_std)" - ).body[0] - - # Return conditional objective - return_stmt = ast.Return(value=ast.Name(id="cond_obj", ctx=ast.Load())) - - # Replace the return statement with our new statements - new_body = [std_obj_assign, prov_obj_code, provision_cond_stmt, cond_obj_code, return_stmt] - node.body[i:i + 1] = full_traj_list + new_body + new_body = [ + # create new standard objective variable + ast.Assign( + targets=[ast.Name(id="obj_std", ctx=ast.Store())], + value=original_return, + ), + # overwrite return statement with custom function + ast.Return( + value=ast.parse( + return_baseline_cost_function( + power_variable=self.mpc_data.power_variable, + comfort_variable=self.mpc_data.comfort_variable + ) + ) + .body[0] + .value + ), + ] + # append new variables to end of function + node.body[i:] = full_traj_list + new_body break @@ -564,36 +519,6 @@ def add_import_to_tree(name: str, alias: str, tree: ast.Module): return tree -def add_objective_imports_to_tree(tree: ast.Module): - """Add imports for objective classes to the AST""" - objective_imports = ast.ImportFrom( - module="agentlib_mpc.data_structures.objective", - names=[ - ast.alias(name="FullObjective", asname=None), - ast.alias(name="EqObjective", asname=None), - ast.alias(name="DeltaUObjective", asname=None), - ast.alias(name="SqObjective", asname=None), - ast.alias(name="ConditionalObjective", asname=None), - ], - level=0 - ) - - # Check if the import already exists - for node in tree.body: - if (isinstance(node, ast.ImportFrom) and - node.module == "agentlib_mpc.data_structures.objective"): - # Add any missing names to the existing import - existing_names = {alias.name for alias in node.names} - for name in ["FullObjective", "EqObjective", "DeltaUObjective", "SqObjective", "ConditionalObjective"]: - if name not in existing_names: - node.names.append(ast.alias(name=name, asname=None)) - return tree - - # Import doesn't exist, add it - tree.body.insert(0, objective_imports) - return tree - - def remove_all_imports_from_tree(tree: ast.Module): # Create a new list to hold nodes that are not imports new_body = [ From 7fd7811471d40bb0c588d2cdaff344b2e3750a27 Mon Sep 17 00:00:00 2001 From: Sarah Leidolf Date: Wed, 2 Jul 2025 14:57:42 +0200 Subject: [PATCH 09/25] Revert "change self.time" This reverts commit 43a653889c83f8ad2ef3f4bb6087bd8621bee81c --- .../data_structures/globals.py | 12 ++++++------ .../generate_flex_agents.py | 12 +++++++++++- flexibility_quantification/utils/parsing.py | 16 +++++++++++----- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/flexibility_quantification/data_structures/globals.py b/flexibility_quantification/data_structures/globals.py index e347d4f7..5cbb991c 100644 --- a/flexibility_quantification/data_structures/globals.py +++ b/flexibility_quantification/data_structures/globals.py @@ -21,8 +21,8 @@ STORED_ENERGY_ALIAS_NEG = "_E_stored_neg" STORED_ENERGY_ALIAS_POS = "_E_stored_pos" -SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.time < self.prep_time.sym + " - "self.market_time.sym, obj_std, ca.if_else(self.time < " +SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.Time.sym < self.prep_time.sym + " + "self.market_time.sym, obj_std, ca.if_else(self.Time.sym < " "(self.prep_time.sym + self.flex_event_duration.sym + " "self.market_time.sym), obj_flex, obj_std))") @@ -33,15 +33,15 @@ def return_baseline_cost_function(power_variable, comfort_variable): if comfort_variable: cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.time < self.rel_start.sym, obj_std, " - "ca.if_else(self.time >= self.rel_end.sym, obj_std, " + "ca.if_else(self.Time.sym < self.rel_start.sym, obj_std, " + "ca.if_else(self.Time.sym >= self.rel_end.sym, obj_std, " f"sum([self.profile_deviation_weight*(self.{power_variable} - " f"self._P_external)**2, " f"self.{comfort_variable} * self.profile_comfort_weight]))),obj_std)") else: cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.time < self.rel_start.sym, obj_std, " - "ca.if_else(self.time >= self.rel_end.sym, obj_std, " + "ca.if_else(self.Time.sym < self.rel_start.sym, obj_std, " + "ca.if_else(self.Time.sym >= self.rel_end.sym, obj_std, " f"sum([self.profile_deviation_weight*(self.{power_variable} - " f"self._P_external)**2]))),obj_std)") return cost_func diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py index 43f5f010..6917575e 100644 --- a/flexibility_quantification/generate_flex_agents.py +++ b/flexibility_quantification/generate_flex_agents.py @@ -316,6 +316,7 @@ def adapt_mpc_module_config( - names of the control variables for the shadow mpcs - reduce communicated variables of shadow mpcs to outputs - add the power variable to the outputs + - add the Time variable to the inputs - add parameters for the activation and quantification of flexibility """ @@ -431,7 +432,16 @@ def adapt_mpc_module_config( self.indicator_module_config.correct_costs.stored_energy_variable ].alias = mpc_dataclass.stored_energy_alias - + # add inputs for the Time variable as well as extra inputs needed for activation of flex + module_config.inputs.append( + MPCVariable( + name="Time", + value=[ + i * module_config.time_step + for i in range(module_config.prediction_horizon) + ], + ) + ) module_config.inputs.extend(mpc_dataclass.config_inputs_appendix) # CONFIG_PARAMETERS_APPENDIX only includes dummy values # overwrite dummy values with values from flex config and append it to module config diff --git a/flexibility_quantification/utils/parsing.py b/flexibility_quantification/utils/parsing.py index 4492ab85..fc937cb1 100644 --- a/flexibility_quantification/utils/parsing.py +++ b/flexibility_quantification/utils/parsing.py @@ -203,8 +203,11 @@ def modify_config_class_shadow(self, node): """ # loop over config object and modify fields for body in node.body: - # add control trajectory inputs + # add the time and full control trajectory inputs if body.target.id == "inputs": + body.value.elts.append( + add_input("Time", 0, "s", "time trajectory", "list") + ) for control in self.controls: body.value.elts.append( add_input( @@ -297,6 +300,9 @@ def modify_config_class_baseline(self, node): elif isinstance(body.value, ast.BinOp) or isinstance(body.value, ast.Tuple): # Complex case with concatenated lists or tuple value_list = self.get_leftmost_list(body.value) + value_list.elts.append( + add_input("Time", 0, "s", "time trajectory", "list") + ) value_list.elts.append( add_input( "_P_external", @@ -364,7 +370,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -372,7 +378,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], @@ -393,7 +399,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -401,7 +407,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], From cc3004e3e33650d77a0c8e0d328b8fa36e66bd6d Mon Sep 17 00:00:00 2001 From: Sarah Leidolf Date: Wed, 2 Jul 2025 16:13:11 +0200 Subject: [PATCH 10/25] Revert "Revert "change self.time"" This reverts commit 7fd7811471d40bb0c588d2cdaff344b2e3750a27 --- .../data_structures/globals.py | 12 ++++++------ .../generate_flex_agents.py | 12 +----------- flexibility_quantification/utils/parsing.py | 16 +++++----------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/flexibility_quantification/data_structures/globals.py b/flexibility_quantification/data_structures/globals.py index 5cbb991c..e347d4f7 100644 --- a/flexibility_quantification/data_structures/globals.py +++ b/flexibility_quantification/data_structures/globals.py @@ -21,8 +21,8 @@ STORED_ENERGY_ALIAS_NEG = "_E_stored_neg" STORED_ENERGY_ALIAS_POS = "_E_stored_pos" -SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.Time.sym < self.prep_time.sym + " - "self.market_time.sym, obj_std, ca.if_else(self.Time.sym < " +SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.time < self.prep_time.sym + " + "self.market_time.sym, obj_std, ca.if_else(self.time < " "(self.prep_time.sym + self.flex_event_duration.sym + " "self.market_time.sym), obj_flex, obj_std))") @@ -33,15 +33,15 @@ def return_baseline_cost_function(power_variable, comfort_variable): if comfort_variable: cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.Time.sym < self.rel_start.sym, obj_std, " - "ca.if_else(self.Time.sym >= self.rel_end.sym, obj_std, " + "ca.if_else(self.time < self.rel_start.sym, obj_std, " + "ca.if_else(self.time >= self.rel_end.sym, obj_std, " f"sum([self.profile_deviation_weight*(self.{power_variable} - " f"self._P_external)**2, " f"self.{comfort_variable} * self.profile_comfort_weight]))),obj_std)") else: cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.Time.sym < self.rel_start.sym, obj_std, " - "ca.if_else(self.Time.sym >= self.rel_end.sym, obj_std, " + "ca.if_else(self.time < self.rel_start.sym, obj_std, " + "ca.if_else(self.time >= self.rel_end.sym, obj_std, " f"sum([self.profile_deviation_weight*(self.{power_variable} - " f"self._P_external)**2]))),obj_std)") return cost_func diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py index 6917575e..43f5f010 100644 --- a/flexibility_quantification/generate_flex_agents.py +++ b/flexibility_quantification/generate_flex_agents.py @@ -316,7 +316,6 @@ def adapt_mpc_module_config( - names of the control variables for the shadow mpcs - reduce communicated variables of shadow mpcs to outputs - add the power variable to the outputs - - add the Time variable to the inputs - add parameters for the activation and quantification of flexibility """ @@ -432,16 +431,7 @@ def adapt_mpc_module_config( self.indicator_module_config.correct_costs.stored_energy_variable ].alias = mpc_dataclass.stored_energy_alias - # add inputs for the Time variable as well as extra inputs needed for activation of flex - module_config.inputs.append( - MPCVariable( - name="Time", - value=[ - i * module_config.time_step - for i in range(module_config.prediction_horizon) - ], - ) - ) + module_config.inputs.extend(mpc_dataclass.config_inputs_appendix) # CONFIG_PARAMETERS_APPENDIX only includes dummy values # overwrite dummy values with values from flex config and append it to module config diff --git a/flexibility_quantification/utils/parsing.py b/flexibility_quantification/utils/parsing.py index fc937cb1..4492ab85 100644 --- a/flexibility_quantification/utils/parsing.py +++ b/flexibility_quantification/utils/parsing.py @@ -203,11 +203,8 @@ def modify_config_class_shadow(self, node): """ # loop over config object and modify fields for body in node.body: - # add the time and full control trajectory inputs + # add control trajectory inputs if body.target.id == "inputs": - body.value.elts.append( - add_input("Time", 0, "s", "time trajectory", "list") - ) for control in self.controls: body.value.elts.append( add_input( @@ -300,9 +297,6 @@ def modify_config_class_baseline(self, node): elif isinstance(body.value, ast.BinOp) or isinstance(body.value, ast.Tuple): # Complex case with concatenated lists or tuple value_list = self.get_leftmost_list(body.value) - value_list.elts.append( - add_input("Time", 0, "s", "time trajectory", "list") - ) value_list.elts.append( add_input( "_P_external", @@ -370,7 +364,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -378,7 +372,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], @@ -399,7 +393,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_upper = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_upper = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.ub)" ).body[0], @@ -407,7 +401,7 @@ def modify_setup_system_shadow(self, node): node.body.insert( 0, ast.parse( - f"{control.name}_lower = ca.if_else(self.Time.sym < self.market_time.sym, " + f"{control.name}_lower = ca.if_else(self.time < self.market_time.sym, " f"self.{full_trajectory_prefix}{control.name}{full_trajectory_suffix}.sym, " f"self.{control.name}.lb)" ).body[0], From ca461e10587e7f850eabe6ce8fb6ad7bff8df47c Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Mon, 14 Jul 2025 11:14:51 +0200 Subject: [PATCH 11/25] add set_actuation --- flexibility_quantification/modules/shadow_mpc.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/flexibility_quantification/modules/shadow_mpc.py b/flexibility_quantification/modules/shadow_mpc.py index 778045a5..a79a87a5 100644 --- a/flexibility_quantification/modules/shadow_mpc.py +++ b/flexibility_quantification/modules/shadow_mpc.py @@ -6,6 +6,8 @@ ) from typing import Dict, Union from agentlib.core.datamodels import AgentVariable +from agentlib_mpc.data_structures.mpc_datamodels import Results + class FlexibilityShadowMPC(mpc_full.MPC): @@ -63,6 +65,17 @@ def process(self): # the shadow mpc should only be run after the results of the baseline are sent yield self.env.event() + def set_actuation(self, solution: Results): + """Takes the solution from optimization backend and sends the first + step to AgentVariables.""" + self.logger.info("Sending optimal control values to data_broker.") + tolerance = 1e-5 + for control in self.var_ref.controls: + ub = self.get(control).ub + lb = self.get(control).lb + # take the first entry of the control trajectory + actuation = solution.df.variable[control].dropna() + self.set(control, actuation) class FlexibilityShadowMINLPMPC(minlp_mpc.MINLPMPC): From 176612500d61c3ff0183aec1581da9147d7daf53 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Wed, 21 Jan 2026 14:33:38 +0100 Subject: [PATCH 12/25] update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ccc0b96f..2a9761c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ setuptools agentlib[full]==0.8.2 -agentlib_mpc[full] @ git+https://github.com/RWTH-EBC/AgentLib-MPC.git@43-combine_newObj_with_OL +agentlib_mpc[full] ==1.0.1 pathlib astor==0.8.1 black From 69b9be860ecb34d942bd64e37ef23baa4c208747 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Wed, 21 Jan 2026 15:12:04 +0100 Subject: [PATCH 13/25] update globals --- agentlib_flexquant/data_structures/globals.py | 64 ++++++++++++++----- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/agentlib_flexquant/data_structures/globals.py b/agentlib_flexquant/data_structures/globals.py index e347d4f7..36823d33 100644 --- a/agentlib_flexquant/data_structures/globals.py +++ b/agentlib_flexquant/data_structures/globals.py @@ -2,7 +2,7 @@ from typing import Literal - +# fixed string definitions PREP_TIME = "prep_time" MARKET_TIME = "market_time" FLEX_EVENT_DURATION = "flex_event_duration" @@ -10,7 +10,11 @@ PROFILE_COMFORT_WEIGHT = "profile_comfort_weight" TIME_STEP = "time_step" PREDICTION_HORIZON = "prediction_horizon" -FlexibilityOffer = "FlexibilityOffer" +FLEXIBILITY_OFFER = "FlexibilityOffer" +LINEAR = 'linear' +CONSTANT = 'constant' +COLLOCATION = 'collocation' +INTEGRATION_METHOD = Literal[LINEAR, CONSTANT] FlexibilityDirections = Literal["positive", "negative"] @@ -20,28 +24,56 @@ STORED_ENERGY_ALIAS_BASE = "_E_stored_base" STORED_ENERGY_ALIAS_NEG = "_E_stored_neg" STORED_ENERGY_ALIAS_POS = "_E_stored_pos" +full_trajectory_suffix: str = "_full" +base_vars_to_communicate_suffix: str = "_base" +shadow_suffix: str = "_shadow" +COLLOCATION_TIME_GRID = 'collocation_time_grid' +PROVISION_VAR_NAME = "in_provision" +ACCEPTED_POWER_VAR_NAME = "_P_external" +RELATIVE_EVENT_START_TIME_VAR_NAME = "rel_start" +RELATIVE_EVENT_END_TIME_VAR_NAME = "rel_end" + +# cost function in the shadow mpc. obj_std and obj_flex are to be evaluated according +# to user definition +SHADOW_MPC_COST_FUNCTION = ( + "return ca.if_else(self.time < self.prep_time.sym + " + "self.market_time.sym, obj_std, ca.if_else(self.time < " + "(self.prep_time.sym + self.flex_event_duration.sym + " + "self.market_time.sym), obj_flex, obj_std))" +) SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.time < self.prep_time.sym + " "self.market_time.sym, obj_std, ca.if_else(self.time < " "(self.prep_time.sym + self.flex_event_duration.sym + " "self.market_time.sym), obj_flex, obj_std))") -full_trajectory_suffix: str = "_full" -full_trajectory_prefix: str = "_" -def return_baseline_cost_function(power_variable, comfort_variable): +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 + + """ if comfort_variable: - cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.time < self.rel_start.sym, obj_std, " - "ca.if_else(self.time >= self.rel_end.sym, obj_std, " - f"sum([self.profile_deviation_weight*(self.{power_variable} - " - f"self._P_external)**2, " - f"self.{comfort_variable} * self.profile_comfort_weight]))),obj_std)") + cost_func = ( + f"return ca.if_else(self.{PROVISION_VAR_NAME}.sym, " + f"ca.if_else(self.time < self.{RELATIVE_EVENT_START_TIME_VAR_NAME}.sym, obj_std, " + f"ca.if_else(self.time >= self.{RELATIVE_EVENT_END_TIME_VAR_NAME}.sym, obj_std, " + f"sum([self.profile_deviation_weight*(self.{power_variable} - " + f"self.{ACCEPTED_POWER_VAR_NAME})**2, " + f"self.{comfort_variable}**2 * self.profile_comfort_weight]))),obj_std)" + ) else: - cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.time < self.rel_start.sym, obj_std, " - "ca.if_else(self.time >= self.rel_end.sym, obj_std, " - f"sum([self.profile_deviation_weight*(self.{power_variable} - " - f"self._P_external)**2]))),obj_std)") + cost_func = ( + f"return ca.if_else(self.{PROVISION_VAR_NAME}.sym, " + f"ca.if_else(self.time < self.{RELATIVE_EVENT_START_TIME_VAR_NAME}.sym, obj_std, " + f"ca.if_else(self.time >= self.{RELATIVE_EVENT_END_TIME_VAR_NAME}.sym, obj_std, " + f"sum([self.profile_deviation_weight*(self.{power_variable} - " + f"self.{ACCEPTED_POWER_VAR_NAME})**2]))),obj_std)" + ) return cost_func From e9c4d976428897103cae9626c0f7e4e6558a4490 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Wed, 21 Jan 2026 15:31:38 +0100 Subject: [PATCH 14/25] minor updates after merge from main branch --- agentlib_flexquant/generate_flex_agents.py | 852 +++++++++++++----- agentlib_flexquant/modules/shadow_mpc.py | 660 ++++++++++++-- agentlib_flexquant/utils/data_handling.py | 69 +- .../data_structures/globals.py | 47 - .../generate_flex_agents.py | 605 ------------- .../modules/shadow_mpc.py | 133 --- .../utils/data_handling.py | 82 -- 7 files changed, 1245 insertions(+), 1203 deletions(-) delete mode 100644 flexibility_quantification/data_structures/globals.py delete mode 100644 flexibility_quantification/generate_flex_agents.py delete mode 100644 flexibility_quantification/modules/shadow_mpc.py delete mode 100644 flexibility_quantification/utils/data_handling.py diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index 43f5f010..12429d9c 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -1,41 +1,74 @@ +"""Generate agents for flexibility quantification. + +This module provides the FlexAgentGenerator class that creates and configures flexibility agents. +The agents created include the baseline, positive and negative flexibility agents, +the flexibility indicator and market agents. The agents are created based on the flex config and +the MPC config. +""" +import ast +import atexit import inspect +import json import logging +import os from copy import deepcopy -from agentlib.utils import custom_injection, load_config +from pathlib import Path +from typing import Union + +import astor +import black +import json +import numpy as np +from copy import deepcopy +from pathlib import Path +from typing import List, Union +from pydantic import FilePath +from agentlib.core.agent import AgentConfig +from agentlib.core.datamodels import AgentVariable from agentlib.core.errors import ConfigurationError -from flexibility_quantification.data_structures.flexquant import ( - FlexQuantConfig, +from agentlib.core.module import BaseModuleConfig +from agentlib.utils import custom_injection, load_config +from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable +from agentlib_mpc.models.casadi_model import CasadiModelConfig +from agentlib_mpc.modules.mpc_full import MPCConfig + +from agentlib_mpc.optimization_backends.casadi_.basic import DirectCollocation +from agentlib_mpc.data_structures.casadi_utils import CasadiDiscretizationOptions +import agentlib_flexquant.data_structures.globals as glbs +import agentlib_flexquant.utils.config_management as cmng +from agentlib_flexquant.utils.parsing import SetupSystemModifier +from agentlib_flexquant.data_structures.flexquant import ( FlexibilityIndicatorConfig, FlexibilityMarketConfig, + FlexQuantConfig, ) -import flexibility_quantification.data_structures.globals as glbs -from flexibility_quantification.data_structures.mpcs import BaseMPCData, BaselineMPCData -import flexibility_quantification.utils.config_management as cmng -from flexibility_quantification.modules.flexibility_indicator import ( +from agentlib_flexquant.data_structures.mpcs import BaselineMPCData, BaseMPCData +from agentlib_flexquant.modules.flexibility_indicator import ( FlexibilityIndicatorModuleConfig, ) -from flexibility_quantification.modules.flexibility_market import ( - FlexibilityMarketModuleConfig, -) -from agentlib_mpc.modules.mpc_full import BaseMPCConfig -from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable -from agentlib_mpc.models.casadi_model import CasadiModelConfig -from agentlib.core.agent import AgentConfig -from agentlib.core.module import BaseModuleConfig -import ast -import atexit -import os -from typing import Union, List -from pydantic import FilePath -from pathlib import Path -import black +from agentlib_flexquant.modules.flexibility_market import FlexibilityMarketModuleConfig class FlexAgentGenerator: - orig_mpc_module_config: BaseMPCConfig - baseline_mpc_module_config: BaseMPCConfig - pos_flex_mpc_module_config: BaseMPCConfig - neg_flex_mpc_module_config: BaseMPCConfig + """Class for generating the flex agents + + orig_mpc_module_config: the config for the original mpc, + which has nothing to do with the flexibility quantification + baseline_mpc_module_config: the config for the baseline mpc + for flexibility quantification + pos_flex_mpc_module_config: the config for the positive flexibility mpc + for flexibility quantification + neg_flex_mpc_module_config: the config for the negative flexibility mpc + for flexibility quantification + indicator_module_config: the config for the indicator for flexibility quantification + market_module_config: the config for the market for flexibility quantification + + """ + + orig_mpc_module_config: MPCConfig + baseline_mpc_module_config: MPCConfig + pos_flex_mpc_module_config: MPCConfig + neg_flex_mpc_module_config: MPCConfig indicator_module_config: FlexibilityIndicatorModuleConfig market_module_config: FlexibilityMarketModuleConfig @@ -44,15 +77,15 @@ def __init__( flex_config: Union[str, FilePath, FlexQuantConfig], mpc_agent_config: Union[str, FilePath, AgentConfig], ): + self.logger = logging.getLogger(__name__) + if isinstance(flex_config, str or FilePath): self.flex_config_file_name = os.path.basename(flex_config) else: # provide default name for json self.flex_config_file_name = "flex_config.json" # load configs - self.flex_config = load_config.load_config( - flex_config, config_type=FlexQuantConfig - ) + self.flex_config = load_config.load_config(flex_config, config_type=FlexQuantConfig) # original mpc agent self.orig_mpc_agent_config = load_config.load_config( @@ -60,10 +93,18 @@ def __init__( ) # baseline agent self.baseline_mpc_agent_config = self.orig_mpc_agent_config.__deepcopy__() + self.baseline_mpc_agent_config.id = (self.flex_config. + baseline_config_generator_data.agent_id) # pos agent self.pos_flex_mpc_agent_config = self.orig_mpc_agent_config.__deepcopy__() + self.pos_flex_mpc_agent_config.id = (self.flex_config. + shadow_mpc_config_generator_data. + pos_flex.agent_id) # neg agent self.neg_flex_mpc_agent_config = self.orig_mpc_agent_config.__deepcopy__() + self.neg_flex_mpc_agent_config.id = (self.flex_config. + shadow_mpc_config_generator_data. + neg_flex.agent_id) # original mpc module self.orig_mpc_module_config = cmng.get_module( @@ -75,6 +116,15 @@ def __init__( config=self.baseline_mpc_agent_config, module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), ) + # convert agentlib_mpc’s ModuleConfig to flexquant’s ModuleConfig to include additional + # fields not present in the original + self.baseline_mpc_module_config = cmng.get_flex_mpc_module_config( + agent_config=self.baseline_mpc_agent_config, + module_config=self.baseline_mpc_module_config, + module_type=self.flex_config.baseline_config_generator_data.module_types[ + self.baseline_mpc_module_config.type + ], + ) # pos module self.pos_flex_mpc_module_config = cmng.get_module( config=self.pos_flex_mpc_agent_config, @@ -91,7 +141,7 @@ def __init__( ) # load indicator module config self.indicator_agent_config = load_config.load_config( - self.flex_config.indicator_config.agent_config, config_type=AgentConfig + self.indicator_config.agent_config, config_type=AgentConfig ) self.indicator_module_config = cmng.get_module( config=self.indicator_agent_config, module_type=cmng.INDICATOR_CONFIG_TYPE @@ -111,115 +161,85 @@ def __init__( else: self.flex_config.market_time = 0 - def generate_flex_agents( - self, - ) -> [ - BaseMPCConfig, - BaseMPCConfig, - BaseMPCConfig, - FlexibilityIndicatorModuleConfig, - FlexibilityMarketModuleConfig, - ]: - """Generates the configs and the python module for the flexibility agents. - Power variable must be defined in the mpc config. + self.run_config_validations() - """ + def generate_flex_agents(self) -> list[str]: + """Generate the configs and the python module for the flexibility agents. - # adapt modules to include necessary communication variables and dump jsons of the agents including the adapted module configs - indicator_module_config = self.adapt_indicator_config( - module_config=self.indicator_module_config - ) - self.append_module_and_dump_agent( - module=indicator_module_config, - agent=self.indicator_agent_config, - module_type=cmng.INDICATOR_CONFIG_TYPE, - config_name=self.flex_config.indicator_config.name_of_created_file, - ) - if self.flex_config.market_config: - market_module_config = self.adapt_market_config( - module_config=self.market_module_config - ) - self.append_module_and_dump_agent( - module=market_module_config, - agent=self.market_agent_config, - module_type=cmng.MARKET_CONFIG_TYPE, - config_name=self.market_config.name_of_created_file, - ) - - # check if the power variable exists in the mpc config - if self.flex_config.baseline_config_generator_data.power_variable not in [ - output.name for output in self.baseline_mpc_module_config.outputs - ]: - raise ConfigurationError( - f"Given power variable {self.flex_config.baseline_config_generator_data.power_variable} is not defined as output in baseline mpc config." - ) - # check if the comfort variable exists in the mpc slack variables - if self.flex_config.baseline_config_generator_data.comfort_variable: - file_path = self.baseline_mpc_module_config.optimization_backend["model"]["type"]["file"] - class_name = self.baseline_mpc_module_config.optimization_backend["model"]["type"]["class_name"] - # Get the class - dynamic_class = cmng.get_class_from_file(file_path, class_name) - if self.flex_config.baseline_config_generator_data.comfort_variable not in [ - state.name for state in dynamic_class().states - ]: - raise ConfigurationError( - f"Given comfort variable {self.flex_config.baseline_config_generator_data.comfort_variable} is not defined as state in baseline mpc config." - ) - # check if the energy storage variable exists in the mpc config - if indicator_module_config.correct_costs.enable_energy_costs_correction: - if indicator_module_config.correct_costs.stored_energy_variable not in [ - output.name for output in self.baseline_mpc_module_config.outputs - ]: - raise ConfigurationError( - f"The stored energy variable {indicator_module_config.correct_costs.stored_energy_variable} is not defined in baseline mpc config." - f"It must be defined in the base MPC model and config as output if the correction of costs is enabled" - ) + Returns: + list of the full path for baseline mpc, pos_flex mpc, neg_flex mpc, indicator + and market config + """ # adapt modules to include necessary communication variables baseline_mpc_config = self.adapt_mpc_module_config( module_config=self.baseline_mpc_module_config, mpc_dataclass=self.flex_config.baseline_config_generator_data, + agent_id=self.flex_config.baseline_config_generator_data.agent_id, ) pf_mpc_config = self.adapt_mpc_module_config( module_config=self.pos_flex_mpc_module_config, mpc_dataclass=self.flex_config.shadow_mpc_config_generator_data.pos_flex, + agent_id=self.flex_config.shadow_mpc_config_generator_data.pos_flex.agent_id, ) nf_mpc_config = self.adapt_mpc_module_config( module_config=self.neg_flex_mpc_module_config, mpc_dataclass=self.flex_config.shadow_mpc_config_generator_data.neg_flex, + agent_id=self.flex_config.shadow_mpc_config_generator_data.neg_flex.agent_id, + ) + indicator_module_config = self.adapt_indicator_module_config( + module_config=self.indicator_module_config ) + if self.flex_config.market_config: + market_module_config = self.adapt_market_module_config( + module_config=self.market_module_config + ) # dump jsons of the agents including the adapted module configs self.append_module_and_dump_agent( module=baseline_mpc_config, agent=self.baseline_mpc_agent_config, module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - config_name=self.flex_config.baseline_config_generator_data.name_of_created_file, + config_name=self.flex_config.baseline_config_generator_data. + name_of_created_file, ) self.append_module_and_dump_agent( module=pf_mpc_config, agent=self.pos_flex_mpc_agent_config, module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - config_name=self.flex_config.shadow_mpc_config_generator_data.pos_flex.name_of_created_file, + config_name=self.flex_config.shadow_mpc_config_generator_data. + pos_flex.name_of_created_file, ) self.append_module_and_dump_agent( module=nf_mpc_config, agent=self.neg_flex_mpc_agent_config, module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - config_name=self.flex_config.shadow_mpc_config_generator_data.neg_flex.name_of_created_file, + config_name=self.flex_config.shadow_mpc_config_generator_data. + neg_flex.name_of_created_file, ) - + self.append_module_and_dump_agent( + module=indicator_module_config, + agent=self.indicator_agent_config, + module_type=cmng.INDICATOR_CONFIG_TYPE, + config_name=self.indicator_config.name_of_created_file, + ) + if self.flex_config.market_config: + self.append_module_and_dump_agent( + module=market_module_config, + agent=self.market_agent_config, + module_type=cmng.MARKET_CONFIG_TYPE, + config_name=self.market_config.name_of_created_file, + ) # generate python files for the shadow mpcs self._generate_flex_model_definition() - # save flex config to created flex files - with open(os.path.join(self.flex_config.path_to_flex_files, self.flex_config_file_name), "w") as f: - config_json = self.flex_config.model_dump_json(exclude_defaults=True) - f.write(config_json) + # add new paths to flex config and dump it + self.adapt_and_dump_flex_config() # register the exit function if the corresponding flag is set if self.flex_config.delete_files: atexit.register(lambda: self._delete_created_files()) + return self.get_config_file_paths() def append_module_and_dump_agent( @@ -229,88 +249,95 @@ def append_module_and_dump_agent( module_type: str, config_name: str, ): - """Appends the given module config to the given agent config and dumps the agent config to a - json file. The json file is named based on the config_name.""" + """Append the given module config to the given agent config and + dumps the agent config to a json file. + + The json file is named based on the config_name. + + Args: + module: The module config to be appended. + agent: The agent config to be updated. + module_type: The type of the module + config_name: The name of the json file for module config (e.g. baseline.json) - # if module is not from the baseline, set a new agent id, based on module id - if module.type is not self.baseline_mpc_module_config.type: - agent.id = module.module_id + """ # get the module as a dict without default values module_dict = cmng.to_dict_and_remove_unnecessary_fields(module=module) # write given module to agent config for i, agent_module in enumerate(agent.modules): - if ( - cmng.MODULE_TYPE_DICT[module_type] - is cmng.MODULE_TYPE_DICT[agent_module["type"]] - ): + if cmng.MODULE_TYPE_DICT[module_type] is cmng.MODULE_TYPE_DICT[agent_module["type"]]: agent.modules[i] = module_dict - # create folder - Path(self.flex_config.path_to_flex_files).mkdir(parents=True, exist_ok=True) # dump agent config if agent.modules: if self.flex_config.overwrite_files: try: - Path( - os.path.join(self.flex_config.path_to_flex_files, config_name) - ).unlink() + Path(os.path.join(self.flex_config.flex_files_directory, + config_name)).unlink() except OSError: pass with open( - os.path.join(self.flex_config.path_to_flex_files, config_name), "w+" + os.path.join(self.flex_config.flex_files_directory, config_name), + "w+", + encoding="utf-8", ) as f: module_json = agent.model_dump_json(exclude_defaults=True) f.write(module_json) else: logging.error("Provided agent config does not contain any modules.") - def get_config_file_paths(self) -> List[str]: - """Returns a list of paths with the created config files""" + def get_config_file_paths(self) -> list[str]: + """Return a list of paths with the created config files.""" paths = [ os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.baseline_config_generator_data.name_of_created_file, + self.flex_config.flex_files_directory, + self.flex_config.baseline_config_generator_data. + name_of_created_file, ), os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.shadow_mpc_config_generator_data.pos_flex.name_of_created_file, + self.flex_config.flex_files_directory, + self.flex_config.shadow_mpc_config_generator_data.pos_flex. + name_of_created_file, ), os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.shadow_mpc_config_generator_data.neg_flex.name_of_created_file, + self.flex_config.flex_files_directory, + self.flex_config.shadow_mpc_config_generator_data.neg_flex. + name_of_created_file, ), os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.indicator_config.name_of_created_file, + self.flex_config.flex_files_directory, + self.indicator_config.name_of_created_file, ), ] if self.flex_config.market_config: paths.append( os.path.join( - self.flex_config.path_to_flex_files, + self.flex_config.flex_files_directory, self.market_config.name_of_created_file, ) ) return paths def _delete_created_files(self): - """Function to run at exit if the files are to be deleted""" + """Function to run at exit if the files are to be deleted.""" to_be_deleted = self.get_config_file_paths() to_be_deleted.append( os.path.join( - self.flex_config.path_to_flex_files, + self.flex_config.flex_files_directory, self.flex_config_file_name, - )) + ) + ) # delete files for file in to_be_deleted: Path(file).unlink() # also delete folder - Path(self.flex_config.path_to_flex_files).rmdir() + Path(self.flex_config.flex_files_directory).rmdir() def adapt_mpc_module_config( - self, module_config: BaseMPCConfig, mpc_dataclass: BaseMPCData - ) -> BaseMPCConfig: - """Adapts the mpc module config for automated flexibility quantification. + self, module_config: MPCConfig, mpc_dataclass: BaseMPCData, agent_id: str + ) -> MPCConfig: + """Adapt the mpc module config for automated flexibility quantification. + Things adapted among others are: - the file name/path of the mpc config file - names of the control variables for the shadow mpcs @@ -318,139 +345,230 @@ def adapt_mpc_module_config( - add the power variable to the outputs - add parameters for the activation and quantification of flexibility + Args: + module_config: The module config to be adapted + mpc_dataclass: The dataclass corresponding to the type of the MPC module. + It contains all the extra data necessary for flexibility + quantification, which will be used to update the + module_config. + agent_id: agent_id for creating the FlexQuant mpc module config + + Returns: + The adapted module config + """ # allow the module config to be changed module_config.model_config["frozen"] = False - module_config.module_id = mpc_dataclass.module_id + # set new MPC type + module_config.type = mpc_dataclass.module_types[ + cmng.get_orig_module_type(self.orig_mpc_agent_config) + ] + + # set the MPC config type from the MPCConfig in agentlib_mpc to the + # corresponding one in flexquant and add additional fields + module_config_flex_dict = module_config.model_dump() + module_config_flex_dict["casadi_sim_time_step"] = ( + self.flex_config.casadi_sim_time_step) + module_config_flex_dict["power_variable_name"] = ( + self.flex_config.baseline_config_generator_data.power_variable) + module_config_flex_dict["storage_variable_name"] = ( + self.indicator_module_config.correct_costs.stored_energy_variable) + module_config_flex = cmng.MODULE_TYPE_DICT[module_config.type]( + **module_config_flex_dict, _agent_id=agent_id + ) + + # allow the module config to be changed + module_config_flex.model_config["frozen"] = False + + module_config_flex.module_id = mpc_dataclass.module_id # append the new weights as parameter to the MPC or update its value - parameter_dict = { - parameter.name: parameter for parameter in module_config.parameters - } + parameter_dict = {parameter.name: parameter for parameter in + module_config_flex.parameters} for weight in mpc_dataclass.weights: if weight.name in parameter_dict: parameter_dict[weight.name].value = weight.value else: - module_config.parameters.append(weight) + module_config_flex.parameters.append(weight) - # set new MPC type - module_config.type = mpc_dataclass.module_types[ - cmng.get_orig_module_type(self.orig_mpc_agent_config) - ] # set new id (needed for plotting) - module_config.module_id = mpc_dataclass.module_id + module_config_flex.module_id = mpc_dataclass.module_id # update optimization backend to use the created mpc files and classes - module_config.optimization_backend["model"]["type"] = { + module_config_flex.optimization_backend["model"]["type"] = { "file": os.path.join( - self.flex_config.path_to_flex_files, + self.flex_config.flex_files_directory, mpc_dataclass.created_flex_mpcs_file, ), "class_name": mpc_dataclass.class_name, } - # update results file with suffix - module_config.optimization_backend["results_file"] = ( - module_config.optimization_backend["results_file"].replace( - ".csv", mpc_dataclass.results_suffix - ) - ) + # extract filename from results file and update it with + # suffix and parent directory + result_filename = Path( + module_config_flex.optimization_backend["results_file"] + ).name.replace(".csv", mpc_dataclass.results_suffix) + full_path = self.flex_config.results_directory / result_filename + module_config_flex.optimization_backend["results_file"] = str(full_path) # change cia backend to custom backend of flexquant - if module_config.optimization_backend["type"] == "casadi_cia": - module_config.optimization_backend["type"] = "casadi_cia_cons" - module_config.optimization_backend["market_time"] = ( - self.flex_config.market_time - ) - - # add the control signal of the baseline to outputs (used during market time) - # and as inputs for the shadow mpcs - if type(mpc_dataclass) is not BaselineMPCData: - for control in module_config.controls: - module_config.inputs.append( + if module_config_flex.optimization_backend["type"] == "casadi_cia": + module_config_flex.optimization_backend["type"] = "casadi_cia_cons" + module_config_flex.optimization_backend["market_time"] = ( + self.flex_config.market_time) + + # add the full control trajectory output from the baseline as input for the + # shadow mpcs, they are directly included in the optimization problem + if not isinstance(mpc_dataclass, BaselineMPCData): + for control in module_config_flex.controls: + module_config_flex.inputs.append( MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, + name=control.name + glbs.full_trajectory_suffix, + value=None, + type="pd.Series", ) ) + # add full control names to shadow MPC config for inputs tracking + module_config_flex.full_control_names.append( + control.name + glbs.full_trajectory_suffix) + # change the alias of control variable in shadow mpc to + # prevent it from triggering the wrong callback + control.alias = control.name + glbs.shadow_suffix # also include binary controls - if hasattr(module_config, "binary_controls"): - for control in module_config.binary_controls: - module_config.inputs.append( + if hasattr(module_config_flex, "binary_controls"): + for control in module_config_flex.binary_controls: + module_config_flex.inputs.append( MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, + name=control.name + glbs.full_trajectory_suffix, + value=None, + type="pd.Series", ) ) - + # add full control names to shadow MPC config for inputs tracking + module_config_flex.full_control_names.append( + control.name + glbs.full_trajectory_suffix) + # change the alias of control variable in shadow mpc to + # prevent it from triggering the wrong callback + control.alias = control.name + glbs.shadow_suffix # only communicate outputs for the shadow mpcs - module_config.shared_variable_fields = ["outputs"] + module_config_flex.shared_variable_fields = ["outputs"] + + # In addition to creating the full control variables, the inputs + # and states of the Baseline are communicated to the Shadow MPC + # to ensure synchronisation. Therefore, all inputs and states of + # the Baseline are added to the Shadow MPCs with an alias + for i, input in enumerate(module_config_flex.inputs): + if input in self.baseline_mpc_module_config.inputs: + module_config_flex.inputs[i].alias = ( + input.alias + glbs.base_vars_to_communicate_suffix) + # add Baseline input names to shadow MPC config for inputs tracking + module_config_flex.baseline_input_names = [ + input.alias + glbs.base_vars_to_communicate_suffix for input in + self.baseline_mpc_module_config.inputs] + + for i, state in enumerate(module_config_flex.states): + if state in self.baseline_mpc_module_config.states: + module_config_flex.states[i].alias = ( + state.alias + glbs.base_vars_to_communicate_suffix) + # add Baseline state names to shadow MPC config for inputs tracking + module_config_flex.baseline_state_names = [ + state.alias + glbs.base_vars_to_communicate_suffix for state in + self.baseline_mpc_module_config.states] + module_config_flex.baseline_agent_id = ( + self.flex_config.baseline_config_generator_data.agent_id) + else: - for control in module_config.controls: - module_config.outputs.append( - MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, + # all the variables here are added to the custom MPCConfig of + # FlexQuant to avoid them being added to the optimization problem + # add full_controls trajectory as AgentVariable to the config of + # Baseline mpc + for control in module_config_flex.controls: + module_config_flex.full_controls.append( + AgentVariable( + name=control.name + glbs.full_trajectory_suffix, + alias=control.name + glbs.full_trajectory_suffix, + shared=True, ) ) - # also include binary controls - if hasattr(module_config, "binary_controls"): - for control in module_config.binary_controls: - module_config.outputs.append( - MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, + if hasattr(module_config_flex, "binary_controls"): + for binary_controls in module_config_flex.binary_controls: + module_config_flex.full_controls.append( + AgentVariable( + name=binary_controls.name + glbs.full_trajectory_suffix, + alias=binary_controls.name + glbs.full_trajectory_suffix, + shared=True, ) ) - module_config.set_outputs = True + # add input and states copy variables which send the Baseline inputs + # to the shadow MPC + for input in module_config_flex.inputs: + module_config_flex.vars_to_communicate.append( + AgentVariable( + name=input.name + glbs.base_vars_to_communicate_suffix, + alias=input.name + glbs.base_vars_to_communicate_suffix, + shared=True, + ) + ) + for state in module_config_flex.states: + module_config_flex.vars_to_communicate.append( + AgentVariable( + name=state.name + glbs.base_vars_to_communicate_suffix, + alias=state.name + glbs.base_vars_to_communicate_suffix, + shared=True, + ) + ) + + module_config_flex.set_outputs = True # add outputs for the power variables, for easier handling create a lookup dict - output_dict = {output.name: output for output in module_config.outputs} - if ( - self.flex_config.baseline_config_generator_data.power_variable - in output_dict - ): + output_dict = {output.name: output for output in module_config_flex.outputs} + if self.flex_config.baseline_config_generator_data.power_variable in output_dict: output_dict[ self.flex_config.baseline_config_generator_data.power_variable ].alias = mpc_dataclass.power_alias else: - module_config.outputs.append( + module_config_flex.outputs.append( MPCVariable( name=self.flex_config.baseline_config_generator_data.power_variable, alias=mpc_dataclass.power_alias, ) ) - # add or change alias for stored energy variable + # add or change alias for stored energy variable if self.indicator_module_config.correct_costs.enable_energy_costs_correction: output_dict[ self.indicator_module_config.correct_costs.stored_energy_variable ].alias = mpc_dataclass.stored_energy_alias - - module_config.inputs.extend(mpc_dataclass.config_inputs_appendix) + # add extra inputs needed for activation of flex + module_config_flex.inputs.extend(mpc_dataclass.config_inputs_appendix) # CONFIG_PARAMETERS_APPENDIX only includes dummy values - # overwrite dummy values with values from flex config and append it to module config + # overwrite dummy values with values from flex config and + # append it to module config for var in mpc_dataclass.config_parameters_appendix: if var.name in self.flex_config.model_fields: var.value = getattr(self.flex_config, var.name) if var.name in self.flex_config.baseline_config_generator_data.model_fields: var.value = getattr(self.flex_config.baseline_config_generator_data, var.name) - module_config.parameters.extend(mpc_dataclass.config_parameters_appendix) + module_config_flex.parameters.extend(mpc_dataclass.config_parameters_appendix) # freeze the config again - module_config.model_config["frozen"] = True + module_config_flex.model_config["frozen"] = True - return module_config + return module_config_flex - def adapt_indicator_config( + def adapt_indicator_module_config( self, module_config: FlexibilityIndicatorModuleConfig ) -> FlexibilityIndicatorModuleConfig: - """Adapts the indicator module config for automated flexibility quantification.""" + """Adapt the indicator module config for automated flexibility + quantification. + + """ + # append user-defined price var to indicator module config + module_config.inputs.append( + AgentVariable( + name=module_config.price_variable, + unit="ct/kWh", + type="pd.Series", + description="electricity price", + ) + ) # allow the module config to be changed module_config.model_config["frozen"] = False for parameter in module_config.parameters: @@ -460,52 +578,104 @@ def adapt_indicator_config( parameter.value = self.flex_config.market_time if parameter.name == glbs.FLEX_EVENT_DURATION: parameter.value = self.flex_config.flex_event_duration - if parameter.name == "time_step": + if parameter.name == glbs.TIME_STEP: parameter.value = self.baseline_mpc_module_config.time_step - if parameter.name == "prediction_horizon": + if parameter.name == glbs.PREDICTION_HORIZON: parameter.value = self.baseline_mpc_module_config.prediction_horizon + if parameter.name == glbs.COLLOCATION_TIME_GRID: + dis_op = self.baseline_mpc_module_config.optimization_backend[ + "discretization_options" + ] + parameter.value = self.get_collocation_time_grid( + discretization_options=dis_op + ) # set power unit module_config.power_unit = ( - self.flex_config.baseline_config_generator_data.power_unit - ) - module_config.results_file = Path( - Path(self.orig_mpc_module_config.optimization_backend["results_file"]).parent, - self.indicator_config.name_of_created_file.replace(".json", ".csv"), + self.flex_config.baseline_config_generator_data.power_unit) + module_config.results_file = ( + self.flex_config.results_directory / module_config.results_file.name ) module_config.model_config["frozen"] = True return module_config - def adapt_market_config( + def adapt_market_module_config( self, module_config: FlexibilityMarketModuleConfig ) -> FlexibilityMarketModuleConfig: - """Adapts the market module config for automated flexibility quantification.""" + """Adapt the market module config for automated flexibility quantification.""" # allow the module config to be changed module_config.model_config["frozen"] = False for field in module_config.__fields__: if field in self.market_module_config.__fields__.keys(): - module_config.__setattr__( - field, getattr(self.market_module_config, field) - ) - module_config.results_file = Path( - Path(self.orig_mpc_module_config.optimization_backend["results_file"]).parent, - self.market_config.name_of_created_file.replace(".json", ".csv"), + module_config.__setattr__(field, getattr(self.market_module_config, + field)) + module_config.results_file = ( + self.flex_config.results_directory / module_config.results_file.name ) + for parameter in module_config.parameters: + if parameter.name == glbs.COLLOCATION_TIME_GRID: + dis_op = self.baseline_mpc_module_config.optimization_backend[ + "discretization_options" + ] + parameter.value = self.get_collocation_time_grid( + discretization_options=dis_op + ) + if parameter.name == glbs.TIME_STEP: + parameter.value = self.baseline_mpc_module_config.time_step module_config.model_config["frozen"] = True return module_config - def _generate_flex_model_definition(self): - """Generates a python module for negative and positive flexibility agents from - the Baseline MPC model - + def adapt_and_dump_flex_config(self): + """Updates the flex_config with the new paths of the market or indicator config, + if these were given as paths to the FlexAgentGenerator. + Dumps the flex config to the new path """ - from flexibility_quantification.utils.parsing import ( - SetupSystemModifier, - add_import_to_tree, + # store market and indicator with file path of created agent config + if self.flex_config.market_config: + self.flex_config.market_config = self.market_config + self.flex_config.market_config.agent_config = os.path.join( + self.flex_config.flex_files_directory, + self.market_config.name_of_created_file) + self.flex_config.indicator_config = self.indicator_config + self.flex_config.indicator_config.agent_config = os.path.join( + self.flex_config.flex_files_directory, + self.indicator_config.name_of_created_file) + # save flex config to created flex files + with open(os.path.join(self.flex_config.flex_files_directory, + self.flex_config_file_name), + "w", encoding="utf-8", ) as f: + config_json = self.flex_config.model_dump_json(exclude_defaults=True) + f.write(config_json) + + def get_collocation_time_grid(self, discretization_options: dict): + """Get the mpc output collocation grid over the horizon""" + # get the mpc time grid configuration + time_step = self.baseline_mpc_module_config.time_step + prediction_horizon = self.baseline_mpc_module_config.prediction_horizon + # get the collocation configuration + collocation_method = discretization_options["collocation_method"] + collocation_order = discretization_options["collocation_order"] + # get the collocation points + options = CasadiDiscretizationOptions( + collocation_order=collocation_order, collocation_method=collocation_method ) - import astor + collocation_points = DirectCollocation(options= + options)._collocation_polynomial().root + # compute the mpc output collocation grid + discretization_points = np.arange(0, time_step * prediction_horizon, time_step) + collocation_time_grid = ( + discretization_points[:, None] + collocation_points * time_step + ).ravel() + collocation_time_grid = collocation_time_grid[ + ~np.isin(collocation_time_grid, discretization_points) + ] + collocation_time_grid = collocation_time_grid.tolist() + return collocation_time_grid + def _generate_flex_model_definition(self): + """Generate a python module for negative and positive flexibility agents + from the Baseline MPC model.""" output_file = os.path.join( - self.flex_config.path_to_flex_files, + self.flex_config.flex_files_directory, self.flex_config.baseline_config_generator_data.created_flex_mpcs_file, ) opt_backend = self.orig_mpc_module_config.optimization_backend["model"]["type"] @@ -529,7 +699,7 @@ def _generate_flex_model_definition(self): ) # parse mpc python file - with open(opt_backend["file"], "r") as f: + with open(opt_backend["file"], "r", encoding="utf-8") as f: source = f.read() tree = ast.parse(source) @@ -537,17 +707,23 @@ def _generate_flex_model_definition(self): modifier_base = SetupSystemModifier( mpc_data=self.flex_config.baseline_config_generator_data, controls=self.baseline_mpc_module_config.controls, - binary_controls=self.baseline_mpc_module_config.binary_controls if hasattr(self.baseline_mpc_module_config, "binary_controls") else None, + binary_controls=self.baseline_mpc_module_config.binary_controls + if hasattr(self.baseline_mpc_module_config, "binary_controls") + else None, ) modifier_pos = SetupSystemModifier( mpc_data=self.flex_config.shadow_mpc_config_generator_data.pos_flex, controls=self.pos_flex_mpc_module_config.controls, - binary_controls=self.pos_flex_mpc_module_config.binary_controls if hasattr(self.pos_flex_mpc_module_config, "binary_controls") else None, + binary_controls=self.pos_flex_mpc_module_config.binary_controls + if hasattr(self.pos_flex_mpc_module_config, "binary_controls") + else None, ) modifier_neg = SetupSystemModifier( mpc_data=self.flex_config.shadow_mpc_config_generator_data.neg_flex, controls=self.neg_flex_mpc_module_config.controls, - binary_controls=self.neg_flex_mpc_module_config.binary_controls if hasattr(self.neg_flex_mpc_module_config, "binary_controls") else None, + binary_controls=self.neg_flex_mpc_module_config.binary_controls + if hasattr(self.neg_flex_mpc_module_config, "binary_controls") + else None, ) # run the modification modified_tree_base = modifier_base.visit(deepcopy(tree)) @@ -566,22 +742,22 @@ def _generate_flex_model_definition(self): try: Path( os.path.join( - self.flex_config.path_to_flex_files, + self.flex_config.flex_files_directory, self.flex_config.baseline_config_generator_data.created_flex_mpcs_file, ) ).unlink() except OSError: pass - with open(output_file, "w") as f: + with open(output_file, "w", encoding="utf-8") as f: f.write(formatted_code) def check_variables_in_casadi_config(self, config: CasadiModelConfig, expr: str): """Check if all variables in the expression are defined in the config. Args: - config (CasadiModelConfig): casadi model config. - expr (str): The expression to check. + config: casadi model config. + expr: The expression to check. Raises: ValueError: If any variable in the expression is not defined in the config. @@ -590,16 +766,200 @@ def check_variables_in_casadi_config(self, config: CasadiModelConfig, expr: str) variables_in_config = set(config.get_variable_names()) variables_in_cost_function = set(ast.walk(ast.parse(expr))) variables_in_cost_function = { - node.attr - for node in variables_in_cost_function - if isinstance(node, ast.Attribute) + node.attr for node in variables_in_cost_function if isinstance(node, + ast.Attribute) } variables_newly_created = set( - weight.name - for weight in self.flex_config.shadow_mpc_config_generator_data.weights - ) - unknown_vars = ( - variables_in_cost_function - variables_in_config - variables_newly_created + weight.name for weight in + self.flex_config.shadow_mpc_config_generator_data.weights ) + unknown_vars = (variables_in_cost_function - variables_in_config - + variables_newly_created) if unknown_vars: raise ValueError(f"Unknown variables in new cost function: {unknown_vars}") + + def run_config_validations(self): + """Function to validate integrity of user-supplied flex config. + + Since the validation depends on interactions between multiple configurations, + it is performed within this function rather than using Pydantic’s built-in + validators for individual configurations. + + The following checks are performed: + 1. Ensures the specified power variable exists in the MPC model outputs. + 2. Ensures the specified comfort variable exists in the MPC model states. + 3. Validates that the stored energy variable exists in MPC outputs if + energy cost correction is enabled. + 4. Verifies the supported collocation method is used; otherwise, + switches to 'legendre' and raises a warning. + 5. Ensures that the sum of prep time, market time, and flex event duration + does not exceed the prediction horizon. + 6. Ensures market time equals the MPC model time step if market config is + present. + 7. Ensures that all flex time values are multiples of the MPC model time step. + 8. Checks for mismatches between time-related parameters in the flex/MPC and + indicator configs and issues warnings when discrepancies exist, using the + flex/MPC config values as the source of truth. + + """ + # check if the power variable exists in the mpc config + power_var = self.flex_config.baseline_config_generator_data.power_variable + if power_var not in [output.name for output in + self.baseline_mpc_module_config.outputs]: + raise ConfigurationError( + f"Given power variable {power_var} is not defined " + f"as output in baseline mpc config." + ) + + # check if the comfort variable exists in the mpc slack variables + mod_type = self.baseline_mpc_module_config.optimization_backend["model"]["type"] + if self.flex_config.baseline_config_generator_data.comfort_variable: + file_path = mod_type["file"] + class_name = mod_type["class_name"] + # Get the class + dynamic_class = cmng.get_class_from_file(file_path, class_name) + if self.flex_config.baseline_config_generator_data.comfort_variable not in [ + state.name for state in dynamic_class().states + ]: + raise ConfigurationError( + f"Given comfort variable " + f"{self.flex_config.baseline_config_generator_data.comfort_variable} " + f"is not defined as state in baseline mpc config." + ) + + # check if the energy storage variable exists in the mpc config + if self.indicator_module_config.correct_costs.enable_energy_costs_correction: + if self.indicator_module_config.correct_costs.stored_energy_variable not in [ + output.name for output in self.baseline_mpc_module_config.outputs + ]: + raise ConfigurationError( + f"The stored energy variable " + f"{self.indicator_module_config.correct_costs.stored_energy_variable} " + f"is not defined in baseline mpc config. " + f"It must be defined in the base MPC model and config as output " + f"if the correction of costs is enabled." + ) + + # raise warning if unsupported collocation method is used and change + # to supported method + if ( + "collocation_method" + not in self.baseline_mpc_module_config.optimization_backend["discretization_options"] + ): + raise ConfigurationError( + "Please use collocation as discretization method and define the " + "collocation_method in the mpc config" + ) + else: + collocation_method = self.baseline_mpc_module_config.optimization_backend[ + "discretization_options" + ]["collocation_method"] + if collocation_method != "legendre": + self.logger.warning( + "Collocation method %s is not supported. Switching to " + "method legendre.", + collocation_method, + ) + self.baseline_mpc_module_config.optimization_backend["discretization_options"][ + "collocation_method" + ] = "legendre" + self.pos_flex_mpc_module_config.optimization_backend["discretization_options"][ + "collocation_method" + ] = "legendre" + self.neg_flex_mpc_module_config.optimization_backend["discretization_options"][ + "collocation_method" + ] = "legendre" + + # time data validations + flex_times = { + glbs.PREP_TIME: self.flex_config.prep_time, + glbs.MARKET_TIME: self.flex_config.market_time, + glbs.FLEX_EVENT_DURATION: self.flex_config.flex_event_duration, + } + mpc_times = { + glbs.TIME_STEP: self.baseline_mpc_module_config.time_step, + glbs.PREDICTION_HORIZON: self.baseline_mpc_module_config.prediction_horizon, + } + # total time length check (prep+market+flex_event) + if (sum(flex_times.values()) > mpc_times["time_step"] * + mpc_times["prediction_horizon"]): + raise ConfigurationError( + "Market time + prep time + flex event duration " + "can not exceed the prediction horizon." + ) + # market time val check + if self.flex_config.market_config: + if flex_times["market_time"] % mpc_times["time_step"] != 0: + raise ConfigurationError( + "Market time must be an integer multiple of the time step." + ) + # check for divisibility of flex_times by time_step + for name, value in flex_times.items(): + if value % mpc_times["time_step"] != 0: + raise ConfigurationError( + f"{name} is not a multiple of the time step. Please redefine." + ) + # raise warning if parameter value in flex indicator module config differs from + # value in flex config/ baseline mpc module config + for parameter in self.indicator_module_config.parameters: + if parameter.value is not None: + if parameter.name in flex_times: + flex_value = flex_times[parameter.name] + if parameter.value != flex_value: + self.logger.warning( + "Value mismatch for %s in flex config (field) " + "and indicator module config (parameter). " + "Flex config value will be used.", + parameter.name, + ) + elif parameter.name in mpc_times: + mpc_value = mpc_times[parameter.name] + if parameter.value != mpc_value: + self.logger.warning( + "Value mismatch for %s in baseline MPC module " + "config (field) and indicator module config (parameter). " + "Baseline MPC module config value will be used.", + parameter.name, + ) + + def adapt_sim_results_path(self, simulator_agent_config: Union[str, Path], + save_name_suffix: str = "") -> Union[str, Path]: + """ + Optional helper function to adapt file path for simulator results in sim config, + so that sim results land in the same results directory as flex results. + + Args: + simulator_agent_config: Path to the simulator agent config JSON file. + save_name_suffix: Suffix added to the newly created sim_config file. + + Returns: + The updated simulator config dictionary with the modified result file path. + + Raises: + FileNotFoundError: If the specified config file does not exist. + + """ + simulator_agent_config = Path(simulator_agent_config) + # open config and extract sim module + with open(simulator_agent_config, "r", encoding="utf-8") as f: + sim_config = json.load(f) + sim_module_config = next( + (module for module in sim_config["modules"] if + module["type"] == "simulator"), None) + # convert filename string to path and extract the name + sim_file_name = Path(sim_module_config["result_filename"]).name + # set results path so that sim results lands in same directory + # as flex result CSVs + sim_module_config["result_filename"] = str( + self.flex_config.results_directory / sim_file_name + ) + try: + with open(Path(str(simulator_agent_config) + save_name_suffix), + "w", encoding="utf-8") as f: + json.dump(sim_config, f, indent=4) + return simulator_agent_config + except Exception as e: + raise Exception(f"Could not adapt and create a new simulation config " + f"due to: {e}. " + f"Please check {simulator_agent_config} and " + f"'{save_name_suffix}'") \ No newline at end of file diff --git a/agentlib_flexquant/modules/shadow_mpc.py b/agentlib_flexquant/modules/shadow_mpc.py index a79a87a5..f3b8ea9c 100644 --- a/agentlib_flexquant/modules/shadow_mpc.py +++ b/agentlib_flexquant/modules/shadow_mpc.py @@ -1,133 +1,655 @@ -from agentlib_mpc.modules import mpc_full, minlp_mpc -from flexibility_quantification.utils.data_handling import strip_multi_index, fill_nans, MEAN, INTERPOLATE -from flexibility_quantification.data_structures.globals import ( - full_trajectory_prefix, - full_trajectory_suffix, -) +""" +Defines shadow MPC and MINLP-MPC for positive/negative flexibility quantification. +""" from typing import Dict, Union -from agentlib.core.datamodels import AgentVariable + +import os +import math +import numpy as np +import pandas as pd +from pydantic import Field +from typing import Dict, Union, Optional +from collections.abc import Iterable +from agentlib.core.datamodels import AgentVariable, Source +from agentlib_mpc.modules.mpc import mpc_full, minlp_mpc from agentlib_mpc.data_structures.mpc_datamodels import Results +from agentlib_flexquant.utils.data_handling import fill_nans, MEAN +from agentlib_flexquant.data_structures.globals import (full_trajectory_suffix, + base_vars_to_communicate_suffix) +import agentlib_flexquant.data_structures.globals as glbs + + +class FlexibilityShadowMPCConfig(mpc_full.MPCConfig): + + baseline_input_names: list[str] = Field(default=[]) + baseline_state_names: list[str] = Field(default=[]) + full_control_names: list[str] = Field(default=[]) + + baseline_agent_id: str = "" + casadi_sim_time_step: int = Field( + default=0, + description="Time step for simulation with Casadi simulator. " + "Value is read from FlexQuantConfig", + ) + power_variable_name: str = Field( + default=None, description="Name of the power variable in the " + "shadow mpc model." + ) + storage_variable_name: Optional[str] = Field( + default=None, description="Name of the storage variable in the " + "shadow mpc model." + ) class FlexibilityShadowMPC(mpc_full.MPC): + """Shadow MPC for calculating positive/negative flexibility offers.""" - config: mpc_full.MPCConfig + config: FlexibilityShadowMPCConfig def __init__(self, *args, **kwargs): - # create instance variable - self._full_controls: Dict[str, Union[AgentVariable, None]] = {} + # initialize flex_results with None + self.flex_results = None + super().__init__(*args, **kwargs) + # setup look up dict to track incoming inputs and states + # (maps name as str to actual AgentVariable) + self._track_base_comm_vars_dict: Dict[str, Union[AgentVariable, None]] = {} + for comm_var in self.config.inputs + self.config.states: + if (comm_var.name in self.config.full_control_names or + comm_var.name + base_vars_to_communicate_suffix in + self.config.baseline_input_names or + comm_var.name + base_vars_to_communicate_suffix in + self.config.baseline_state_names): + comm_var.value = None + self._track_base_comm_vars_dict[comm_var.name] = comm_var.copy(deep=True) + # set up necessary components if simulation is enabled + if self.config.casadi_sim_time_step > 0: + # generate a separate simulation model for integration to ensure + # the model used in MPC optimization remains unaffected + self.flex_model = type(self.model)(dt=self.config.casadi_sim_time_step) + # generate the filename for the simulation results + self.res_file_flex = self.config.optimization_backend["results_file"].replace( + "_flex", "_sim_flex" + ) + # clear the casadi simulator result at the first time step if already exists + try: + os.remove(self.res_file_flex) + except FileNotFoundError: + pass + + def set_output(self, solution): + """Takes the solution from optimization backend and sends it to AgentVariables.""" + # Output must be defined in the config as "type"="pd.Series" + if not self.config.set_outputs: + return + self.logger.info("Sending optimal output values to data_broker.") + df = solution.df + self.sim_flex_model(solution) + if self.flex_results is not None: + for output in self.var_ref.outputs: + if output not in [ + self.config.power_variable_name, + self.config.storage_variable_name, + ]: + series = df.variable[output] + self.set(output, series) + # send the power and storage variable value from simulation results + upsampled_output_power = self.flex_results[self.config.power_variable_name] + self.set(self.config.power_variable_name, upsampled_output_power) + if self.config.storage_variable_name is not None: + upsampled_output_storage = self.flex_results[self.config.storage_variable_name] + self.set(self.config.storage_variable_name, upsampled_output_storage.dropna()) + else: + for output in self.var_ref.outputs: + series = df.variable[output] + self.set(output, series) + + def set_actuation(self, solution: Results): + """Takes the solution from optimization backend and sends the first + step to AgentVariables.""" + self.logger.info("Sending optimal control values to data_broker.") + tolerance = 1e-5 + for control in self.var_ref.controls: + ub = self.get(control).ub + lb = self.get(control).lb + # take the first entry of the control trajectory + actuation = solution.df.variable[control].dropna() + self.set(control, actuation) + + def sim_flex_model(self, solution): + """simulate the flex model over the preditcion horizon and save results""" + + # return if sim_time_step is not a positive integer and system is in provision + if not (self.config.casadi_sim_time_step > 0 and not self.get(glbs.PROVISION_VAR_NAME).value): + return + + # read the defined simulation time step + sim_time_step = self.config.casadi_sim_time_step + mpc_time_step = self.config.time_step + + # set the horizon length and the number of simulation steps + total_horizon_time = int(self.config.prediction_horizon * self.config.time_step) + n_simulation_steps = math.ceil(total_horizon_time / sim_time_step) + + # read the current optimization result + result_df = solution.df + + # initialize the flex sim results Dataframe + self._initialize_flex_results( + n_simulation_steps, total_horizon_time, sim_time_step, result_df + ) + + # Update model parameters and initial states + self._update_model_parameters() + self._update_initial_states(result_df) + + # Run simulation + self._run_simulation( + n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + ) + + # set index of flex results to the same as mpc result + store_results_df = self.flex_results.copy(deep=True) + store_results_df.index = self.flex_results.index.tolist() + + # save results + if not os.path.exists(self.res_file_flex): + store_results_df.to_csv(self.res_file_flex) + else: + store_results_df.to_csv(self.res_file_flex, mode="a", header=False) + + # set the flex results format same as mpc result while updating Agentvariable + self.flex_results.index = self.flex_results.index.get_level_values(1) + def register_callbacks(self): for control_var in self.config.controls: self.agent.data_broker.register_callback( - name=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", - alias=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", + name=control_var.name + full_trajectory_suffix, + alias=control_var.name + full_trajectory_suffix, callback=self.calc_flex_callback, + source=Source(agent_id=self.config.baseline_agent_id, module_id=None) + ) + for base_inputs in self.config.baseline_input_names: + self.agent.data_broker.register_callback( + name=base_inputs.removesuffix(base_vars_to_communicate_suffix), # update MPC variable + alias=base_inputs, + callback=self.calc_flex_callback, + source=Source(agent_id=self.config.baseline_agent_id, module_id=None) + ) + for base_states in self.config.baseline_state_names: + self.agent.data_broker.register_callback( + name=base_states.removesuffix(base_vars_to_communicate_suffix), # update MPC variable + alias=base_states, + callback=self.calc_flex_callback, + source=Source(agent_id=self.config.baseline_agent_id, module_id=None) ) - for input_var in self.config.inputs: - if input_var.name.replace(full_trajectory_prefix, "", 1).replace( - full_trajectory_suffix, "" - ) in [control_var.name for control_var in self.config.controls]: - self._full_controls[input_var.name] = input_var - super().register_callbacks() - def calc_flex_callback(self, inp, name): - """set the control trajectories before calculating the flexibility offer. - self.model should account for flexibility in its cost function + def calc_flex_callback(self, inp: AgentVariable, name: str): + """Ensure that all control trajectories and Baseline inputs/states + have been set before starting the calculation. """ - # during provision dont calculate flex - if self.get("in_provision").value: + # during provision do not calculate flex + if self.get(glbs.PROVISION_VAR_NAME).value: return # do not trigger callback on self set variables if self.agent.config.id == inp.source.agent_id: return - vals = strip_multi_index(inp.value) - if vals.isna().any(): - vals = fill_nans(series=vals, method=MEAN) + # get the value of the input + vals = inp.value - # the MPC Predictions starts at t=env.now not t=0 - vals.index += self.env.time - self._full_controls[name].value = vals + if inp.name in self.config.full_control_names: + if vals.isna().any(): + vals = fill_nans(series=vals, method=MEAN) + # add time shift env.now to the mpc prediction index if it starts at t=0 + if vals.index[0] == 0: + vals.index += self.env.time + + # update value in the tracking dictionary + self._track_base_comm_vars_dict[name].value = vals + # set value self.set(name, vals) - # make sure all controls are set - if all(x.value is not None for x in self._full_controls.values()): + + # make sure all necessary inputs are set + if all(x.value is not None for x in self._track_base_comm_vars_dict.values()): self.do_step() - for name in self._full_controls.keys(): - self._full_controls[name].value = None + for _, comm_var in self._track_base_comm_vars_dict.items(): + comm_var.value = None def process(self): # the shadow mpc should only be run after the results of the baseline are sent yield self.env.event() - def set_actuation(self, solution: Results): - """Takes the solution from optimization backend and sends the first - step to AgentVariables.""" - self.logger.info("Sending optimal control values to data_broker.") - tolerance = 1e-5 - for control in self.var_ref.controls: - ub = self.get(control).ub - lb = self.get(control).lb - # take the first entry of the control trajectory - actuation = solution.df.variable[control].dropna() - self.set(control, actuation) + def _initialize_flex_results( + self, n_simulation_steps, horizon_length, sim_time_step, result_df + ): + """Initialize the flex results dataframe with the correct dimension + and index and fill with existing results from optimization + + """ + + # create MultiIndex for collocation points + index_coll = pd.MultiIndex.from_arrays( + [[self.env.now] * len(result_df.index), result_df.index], + names=["time_step", "time"] + # Match the names with multi_index but note they're reversed + ) + # create Multiindex for full simulation sample times + index_full_sample = pd.MultiIndex.from_tuples( + zip( + [self.env.now] * (n_simulation_steps + 1), + range(0, horizon_length + sim_time_step, sim_time_step), + ), + names=["time_step", "time"], + ) + # merge indexes + new_index = index_coll.union(index_full_sample).sort_values() + # initialize the flex results with correct dimension + self.flex_results = pd.DataFrame(np.nan, + index=new_index, + columns=self.var_ref.outputs) + + # Get the optimization outputs and create a series for fixed + # optimization outputs with the correct MultiIndex format + opti_outputs = result_df.variable[self.config.power_variable_name] + fixed_opti_output = pd.Series( + opti_outputs.values, + index=index_coll, + ) + # fill the output value at the time step where it already exists + # in optimization output + for idx in fixed_opti_output.index: + if idx in self.flex_results.index: + self.flex_results.loc[idx, self.config.power_variable_name] = ( + fixed_opti_output)[idx] + + def _update_model_parameters(self): + """update the value of module parameters with value from config, + since creating a model just reads the value in the model class + but not the config. + + """ + + for par in self.config.parameters: + self.flex_model.set(par.name, par.value) + + def _update_initial_states(self, result_df): + """set the initial value of states""" + + # get state values from the mpc optimization result + state_values = result_df.variable[self.var_ref.states] + # update state values with last measurement + for state, value in zip(self.var_ref.states, state_values.iloc[0]): + self.flex_model.set(state, value) + + def _run_simulation( + self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + ): + """simulate with flex model over the prediction horizon + + """ + + # get control and input values from the mpc optimization result + control_values = result_df.variable[self.var_ref.controls].dropna() + input_values = result_df.parameter[self.var_ref.inputs].dropna() + + # Get the simulation time step index + sim_time_index = np.arange(0, (n_simulation_steps + 1) * sim_time_step, sim_time_step) + + # Reindex the controls and inputs to sim_time_index + control_values_full = control_values.copy().reindex(sim_time_index, method="ffill") + input_values_full = input_values.copy().reindex(sim_time_index, method="nearest") + + for i in range(0, n_simulation_steps): + current_sim_time = i * sim_time_step + + # Apply control and input values from the appropriate MPC step + for control, value in zip( + self.var_ref.controls, control_values_full.loc[current_sim_time] + ): + self.flex_model.set(control, value) + + for input_var, value in zip( + self.var_ref.inputs, input_values_full.loc[current_sim_time] + ): + # change the type of iterable input, since casadi model can't deal with iterable + if issubclass(eval(self.flex_model.get(input_var).type), Iterable): + self.flex_model.get(input_var).type = type(value).__name__ + self.flex_model.set(input_var, value) + + # do integration + # reduce the simulation time step so that the total horizon time will not be exceeded + if current_sim_time + sim_time_step <= total_horizon_time: + t_sample = sim_time_step + else: + t_sample = total_horizon_time - current_sim_time + self.flex_model.do_step(t_start=0, t_sample=t_sample) + + # save output + for output in self.var_ref.outputs: + self.flex_results.loc[ + (self.env.now, current_sim_time + t_sample), output + ] = self.flex_model.get_output(output).value + + +class FlexibilityShadowMINLPMPCConfig(minlp_mpc.MINLPMPCConfig): + + baseline_input_names: list[str] = Field(default=[]) + baseline_state_names: list[str] = Field(default=[]) + full_control_names: list[str] = Field(default=[]) + + baseline_agent_id: str = "" + + casadi_sim_time_step: int = Field( + default=0, + description="Time step for simulation with Casadi simulator. " + "Value is read from FlexQuantConfig", + ) + power_variable_name: str = Field( + default=None, description="Name of the power variable in the " + "shadow mpc model." + ) + storage_variable_name: Optional[str] = Field( + default=None, description="Name of the storage variable in the " + "shadow mpc model." + ) + class FlexibilityShadowMINLPMPC(minlp_mpc.MINLPMPC): + """Shadow MINLP-MPC for calculating positive/negatives flexibility offers. + + """ - config: minlp_mpc.MINLPMPCConfig + config: FlexibilityShadowMINLPMPCConfig def __init__(self, *args, **kwargs): - # create instance variable - self._full_controls: Dict[str, Union[AgentVariable, None]] = {} + # initialize flex_results with None + self.flex_results = None + super().__init__(*args, **kwargs) + # setup look up dict to track incoming inputs and states + # (maps name as str to actual AgentVariable) + self._track_base_comm_vars_dict: Dict[str, Union[AgentVariable, None]] = {} + for comm_var in self.config.inputs + self.config.states: + if (comm_var.name in self.config.full_control_names or + comm_var.name + base_vars_to_communicate_suffix in + self.config.baseline_input_names or + comm_var.name + base_vars_to_communicate_suffix in + self.config.baseline_state_names): + comm_var.value = None + self._track_base_comm_vars_dict[comm_var.name] = comm_var.copy(deep=True) + # set up necessary components if simulation is enabled + if self.config.casadi_sim_time_step > 0: + # generate a separate simulation model for integration to ensure + # the model used in MPC optimization remains unaffected + self.flex_model = type(self.model)(dt=self.config.casadi_sim_time_step) + # generate the filename for the simulation results + self.res_file_flex = self.config.optimization_backend["results_file"].replace( + "_flex", "_sim_flex" + ) + # clear the casadi simulator result at the first time step if already exists + try: + os.remove(self.res_file_flex) + except FileNotFoundError: + pass + def register_callbacks(self): for control_var in self.config.controls + self.config.binary_controls: self.agent.data_broker.register_callback( - name=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", - alias=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", + name=control_var.name + full_trajectory_suffix, + alias=control_var.name + full_trajectory_suffix, + callback=self.calc_flex_callback, + source=Source(agent_id=self.config.baseline_agent_id, module_id=None) + ) + for base_inputs in self.config.baseline_input_names: + self.agent.data_broker.register_callback( + name=base_inputs.removesuffix(base_vars_to_communicate_suffix), # update MPC variable + alias=base_inputs, callback=self.calc_flex_callback, + source=Source(agent_id=self.config.baseline_agent_id, module_id=None) + ) + for base_states in self.config.baseline_state_names: + self.agent.data_broker.register_callback( + name=base_states.removesuffix(base_vars_to_communicate_suffix), # update MPC variable + alias=base_states, + callback=self.calc_flex_callback, + source=Source(agent_id=self.config.baseline_agent_id, module_id=None) ) - for input_var in self.config.inputs: - if input_var.name.replace(full_trajectory_prefix, "", 1).replace( - full_trajectory_suffix, "" - ) in [control_var.name for control_var in self.config.controls + self.config.binary_controls]: - self._full_controls[input_var.name] = input_var super().register_callbacks() - def calc_flex_callback(self, inp, name): - """set the control trajectories before calculating the flexibility offer. - self.model should account for flexibility in its cost function + def calc_flex_callback(self, inp: AgentVariable, name: str): + """Ensure that all control trajectories and Baseline inputs/states + have been set before starting the calculation. """ - # during provision dont calculate flex - if self.get("in_provision").value: + # during provision do not calculate flex + if self.get(glbs.PROVISION_VAR_NAME).value: return # do not trigger callback on self set variables if self.agent.config.id == inp.source.agent_id: return - vals = strip_multi_index(inp.value) - if vals.isna().any(): - vals = fill_nans(vals, method=MEAN) + # get the value of the input + vals = inp.value + + if inp.name in self.config.full_control_names: + if vals.isna().any(): + vals = fill_nans(series=vals, method=MEAN) + # add time shift env.now to the mpc prediction index if it starts at t=0 + if vals.index[0] == 0: + vals.index += self.env.time - # the MPC Predictions starts at t=env.now not t=0 - vals.index += self.env.time - self._full_controls[name].value = vals + # update value in the tracking dictionary + self._track_base_comm_vars_dict[name].value = vals + # set value self.set(name, vals) - # make sure all controls are set - if all(x.value is not None for x in self._full_controls.values()): + + # make sure all necessary inputs are set + if all(x.value is not None for x in self._track_base_comm_vars_dict.values()): self.do_step() - for name in self._full_controls.keys(): - self._full_controls[name].value = None + for _, comm_var in self._track_base_comm_vars_dict.items(): + comm_var.value = None def process(self): # the shadow mpc should only be run after the results of the baseline are sent yield self.env.event() + + def set_output(self, solution): + """Takes the solution from optimization backend and sends + it to AgentVariables. + + """ + # Output must be defined in the config as "type"="pd.Series" + if not self.config.set_outputs: + return + self.logger.info("Sending optimal output values to data_broker.") + + # simulate with the casadi simulator + self.sim_flex_model(solution) + + df = solution.df + if self.flex_results is not None: + for output in self.var_ref.outputs: + if output not in [ + self.config.power_variable_name, + self.config.storage_variable_name, + ]: + series = df.variable[output] + self.set(output, series) + # send the power and storage variable value from simulation results + upsampled_output_power = self.flex_results[self.config.power_variable_name] + self.set(self.config.power_variable_name, upsampled_output_power) + if self.config.storage_variable_name is not None: + upsampled_output_storage = self.flex_results[self.config.storage_variable_name] + self.set(self.config.storage_variable_name, upsampled_output_storage.dropna()) + else: + for output in self.var_ref.outputs: + series = df.variable[output] + self.set(output, series) + + def sim_flex_model(self, solution): + """simulate the flex model over the preditcion horizon and save results + + """ + + # return if sim_time_step is not a positive integer and system is in provision + if not (self.config.casadi_sim_time_step > 0 and not self.get(glbs.PROVISION_VAR_NAME).value): + return + + # read the defined simulation time step + sim_time_step = self.config.casadi_sim_time_step + mpc_time_step = self.config.time_step + + # set the horizon length and the number of simulation steps + total_horizon_time = int(self.config.prediction_horizon * self.config.time_step) + n_simulation_steps = math.ceil(total_horizon_time / sim_time_step) + + # read the current optimization result + result_df = solution.df + + # initialize the flex sim results Dataframe + self._initialize_flex_results( + n_simulation_steps, total_horizon_time, sim_time_step, result_df + ) + + # Update model parameters and initial states + self._update_model_parameters() + self._update_initial_states(result_df) + + # Run simulation + self._run_simulation( + n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + ) + + # set index of flex results to the same as mpc result + store_results_df = self.flex_results.copy(deep=True) + store_results_df.index = self.flex_results.index.tolist() + + # save results + if not os.path.exists(self.res_file_flex): + store_results_df.to_csv(self.res_file_flex) + else: + store_results_df.to_csv(self.res_file_flex, mode="a", header=False) + + # set the flex results format same as mpc result while updating Agentvariable + self.flex_results.index = self.flex_results.index.get_level_values(1) + + def _initialize_flex_results( + self, n_simulation_steps, horizon_length, sim_time_step, result_df + ): + """Initialize the flex results dataframe with the correct dimension + and index and fill with existing results from optimization + + """ + + # create MultiIndex for collocation points + index_coll = pd.MultiIndex.from_arrays( + [[self.env.now] * len(result_df.index), result_df.index], + names=["time_step", "time"] + # Match the names with multi_index but note they're reversed + ) + # create Multiindex for full simulation sample times + index_full_sample = pd.MultiIndex.from_tuples( + zip( + [self.env.now] * (n_simulation_steps + 1), + range(0, horizon_length + sim_time_step, sim_time_step), + ), + names=["time_step", "time"], + ) + # merge indexes + new_index = index_coll.union(index_full_sample).sort_values() + # initialize the flex results with correct dimension + self.flex_results = pd.DataFrame(np.nan, index=new_index, columns=self.var_ref.outputs) + + # Get the optimization outputs and create a series for fixed optimization outputs with the + # correct MultiIndex format + opti_outputs = result_df.variable[self.config.power_variable_name] + fixed_opti_output = pd.Series( + opti_outputs.values, + index=index_coll, + ) + # fill the output value at the time step where it already exists in optimization output + for idx in fixed_opti_output.index: + if idx in self.flex_results.index: + self.flex_results.loc[idx, self.config.power_variable_name] = fixed_opti_output[idx] + + def _update_model_parameters(self): + """update the value of module parameters with value from config, + since creating a model just reads the value in the model class but not the config + """ + + for par in self.config.parameters: + self.flex_model.set(par.name, par.value) + + def _update_initial_states(self, result_df): + """set the initial value of states""" + + # get state values from the mpc optimization result + state_values = result_df.variable[self.var_ref.states] + # update state values with last measurement + for state, value in zip(self.var_ref.states, state_values.iloc[0]): + self.flex_model.set(state, value) + + def _run_simulation( + self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + ): + """simulate with flex model over the prediction horizon""" + + # get control and input values from the mpc optimization result + control_values = result_df.variable[ + [*self.var_ref.controls, *self.var_ref.binary_controls] + ].dropna() + input_values = result_df.parameter[self.var_ref.inputs].dropna() + + # Get the simulation time step index + sim_time_index = np.arange(0, (n_simulation_steps + 1) * sim_time_step, sim_time_step) + + # Reindex the controls and inputs to sim_time_index + control_values_full = control_values.copy().reindex(sim_time_index, method="ffill") + input_values_full = input_values.copy().reindex(sim_time_index, method="nearest") + + for i in range(0, n_simulation_steps): + current_sim_time = i * sim_time_step + + # Apply control and input values from the appropriate MPC step + for control, value in zip( + self.var_ref.controls, + control_values_full.loc[current_sim_time, self.var_ref.controls], + ): + self.flex_model.set(control, value) + + for binary_control, value in zip( + self.var_ref.binary_controls, + control_values_full.loc[current_sim_time, self.var_ref.binary_controls], + ): + self.flex_model.set(binary_control, value) + + for input_var, value in zip( + self.var_ref.inputs, input_values_full.loc[current_sim_time] + ): + # change the type of iterable input, since casadi model can't deal with iterable + if issubclass(eval(self.flex_model.get(input_var).type), Iterable): + self.flex_model.get(input_var).type = type(value).__name__ + self.flex_model.set(input_var, value) + + # do integration + # reduce the simulation time step so that the total horizon time will not be exceeded + if current_sim_time + sim_time_step <= total_horizon_time: + t_sample = sim_time_step + else: + t_sample = total_horizon_time - current_sim_time + self.flex_model.do_step(t_start=0, t_sample=t_sample) + + # save output + for output in self.var_ref.outputs: + self.flex_results.loc[ + (self.env.now, current_sim_time + t_sample), output + ] = self.flex_model.get_output(output).value \ No newline at end of file diff --git a/agentlib_flexquant/utils/data_handling.py b/agentlib_flexquant/utils/data_handling.py index 0aa55392..1f19cf1b 100644 --- a/agentlib_flexquant/utils/data_handling.py +++ b/agentlib_flexquant/utils/data_handling.py @@ -1,7 +1,7 @@ from typing import Literal -import pandas as pd -from agentlib_mpc.utils import TimeConversionTypes, TIME_CONVERSION +import pandas as pd +from agentlib_mpc.utils import TIME_CONVERSION, TimeConversionTypes MEAN: str = "mean" INTERPOLATE: str = "interpolate" @@ -9,14 +9,19 @@ def fill_nans(series: pd.Series, method: FillNansMethods) -> pd.Series: - """ - Fill NaN values in the series with the given method. + """Fill NaN values in the series with the given method. + + Args: + series: the series to be filled + method: the method to be applied, there are two predefined + - mean: fill NaN values with the mean of the following values. + - interpolate: interpolate missing values. + + Returns: + A pd.Series with nan filled. - Implemented methods: - - mean: fill NaN values with the mean of the following values. - - interpolate: interpolate missing values. """ - #ignore lags from casadi_ml models + # ignore lags from casadi_ml models series = series[series.index >= 0] if method == MEAN: series = _set_mean_values(series=series) @@ -25,12 +30,17 @@ def fill_nans(series: pd.Series, method: FillNansMethods) -> pd.Series: series = series.interpolate(method="index", limit_direction="both") if series.isna().any(): - raise ValueError(f"NaN values are still present in the series after filling them with the method {method}\n{series}") + raise ValueError( + f"NaN values are still present in the series after filling them " + f"with the method {method}\n{series}" + ) return series def _set_mean_values(series: pd.Series) -> pd.Series: - """ Fills intervals including the nan with the mean of the following values. """ + """Fill intervals including the nan with the mean of the following values + before the next nan.""" + def _get_intervals_for_mean(s: pd.Series) -> list[pd.Interval]: intervals = [] start = None @@ -40,13 +50,21 @@ def _get_intervals_for_mean(s: pd.Series) -> list[pd.Interval]: start = index else: end = index - intervals.append(pd.Interval(left=start, right=end, closed="left")) + intervals.append(pd.Interval(left=start, right=end, closed="both")) start = end + elif index == s.index[-1]: + end = index + intervals.append(pd.Interval(left=start, right=end, closed="both")) return intervals for interval in _get_intervals_for_mean(series): - interval_index = (interval.left <= series.index) & (series.index < interval.right) + interval_index = (interval.left <= series.index) & ( + series.index <= interval.right + ) series[interval.left] = series[interval_index].mean(skipna=True) + # fill the last entry of series with mean value of previous entries + if interval.right == series.index[-1]: + series[interval.right] = series[interval.left] # remove last entry if nan, e.g. with collocation if pd.isna(series.iloc[-1]): @@ -60,23 +78,32 @@ def strip_multi_index(series: pd.Series) -> pd.Series: if isinstance(series.index[0], str): series.index = series.index.map(lambda x: eval(x)) series.index = pd.MultiIndex.from_tuples(series.index) - # vals is multicolumn so get rid of first value (start time of predictions) - series.index = series.index.get_level_values(1).astype(float) + # vals is multicolumn so get rid of first value (start time of predictions) + series.index = series.index.get_level_values(1).astype(float) return series -def convert_timescale_of_index(df: pd.DataFrame, from_unit: TimeConversionTypes, to_unit: TIME_CONVERSION) -> pd.DataFrame: - """ Convert the timescale of a dataframe index (from seconds) to the given time unit +def convert_timescale_of_index( + df: pd.DataFrame, from_unit: TimeConversionTypes, to_unit: TIME_CONVERSION +) -> pd.DataFrame: + """Convert the timescale of a dataframe index (from seconds) to the given time unit. + + Args: + from_unit: the time unit of the original index + to_unit: the time unit to convert the index to + + Returns: + A DataFrame with the converted index - Keyword arguments: - results -- The dictionary of the results with the dataframes - time_unit -- The time unit to convert the index to """ time_conversion_factor = TIME_CONVERSION[from_unit] / TIME_CONVERSION[to_unit] if isinstance(df.index, pd.MultiIndex): df.index = pd.MultiIndex.from_arrays( - [df.index.get_level_values(level) * time_conversion_factor for level in range(df.index.nlevels)] + [ + df.index.get_level_values(level) * time_conversion_factor + for level in range(df.index.nlevels) + ] ) else: df.index = df.index * time_conversion_factor - return df + return df \ No newline at end of file diff --git a/flexibility_quantification/data_structures/globals.py b/flexibility_quantification/data_structures/globals.py deleted file mode 100644 index e347d4f7..00000000 --- a/flexibility_quantification/data_structures/globals.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Script containing global variables""" - -from typing import Literal - - -PREP_TIME = "prep_time" -MARKET_TIME = "market_time" -FLEX_EVENT_DURATION = "flex_event_duration" -PROFILE_DEVIATION_WEIGHT = "profile_deviation_weight" -PROFILE_COMFORT_WEIGHT = "profile_comfort_weight" -TIME_STEP = "time_step" -PREDICTION_HORIZON = "prediction_horizon" -FlexibilityOffer = "FlexibilityOffer" - -FlexibilityDirections = Literal["positive", "negative"] - -POWER_ALIAS_BASE = "_P_el_base" -POWER_ALIAS_NEG = "_P_el_neg" -POWER_ALIAS_POS = "_P_el_pos" -STORED_ENERGY_ALIAS_BASE = "_E_stored_base" -STORED_ENERGY_ALIAS_NEG = "_E_stored_neg" -STORED_ENERGY_ALIAS_POS = "_E_stored_pos" - -SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.time < self.prep_time.sym + " - "self.market_time.sym, obj_std, ca.if_else(self.time < " - "(self.prep_time.sym + self.flex_event_duration.sym + " - "self.market_time.sym), obj_flex, obj_std))") - -full_trajectory_suffix: str = "_full" -full_trajectory_prefix: str = "_" - - -def return_baseline_cost_function(power_variable, comfort_variable): - if comfort_variable: - cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.time < self.rel_start.sym, obj_std, " - "ca.if_else(self.time >= self.rel_end.sym, obj_std, " - f"sum([self.profile_deviation_weight*(self.{power_variable} - " - f"self._P_external)**2, " - f"self.{comfort_variable} * self.profile_comfort_weight]))),obj_std)") - else: - cost_func = ("return ca.if_else(self.in_provision.sym, " - "ca.if_else(self.time < self.rel_start.sym, obj_std, " - "ca.if_else(self.time >= self.rel_end.sym, obj_std, " - f"sum([self.profile_deviation_weight*(self.{power_variable} - " - f"self._P_external)**2]))),obj_std)") - return cost_func diff --git a/flexibility_quantification/generate_flex_agents.py b/flexibility_quantification/generate_flex_agents.py deleted file mode 100644 index 43f5f010..00000000 --- a/flexibility_quantification/generate_flex_agents.py +++ /dev/null @@ -1,605 +0,0 @@ -import inspect -import logging -from copy import deepcopy -from agentlib.utils import custom_injection, load_config -from agentlib.core.errors import ConfigurationError -from flexibility_quantification.data_structures.flexquant import ( - FlexQuantConfig, - FlexibilityIndicatorConfig, - FlexibilityMarketConfig, -) -import flexibility_quantification.data_structures.globals as glbs -from flexibility_quantification.data_structures.mpcs import BaseMPCData, BaselineMPCData -import flexibility_quantification.utils.config_management as cmng -from flexibility_quantification.modules.flexibility_indicator import ( - FlexibilityIndicatorModuleConfig, -) -from flexibility_quantification.modules.flexibility_market import ( - FlexibilityMarketModuleConfig, -) -from agentlib_mpc.modules.mpc_full import BaseMPCConfig -from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable -from agentlib_mpc.models.casadi_model import CasadiModelConfig -from agentlib.core.agent import AgentConfig -from agentlib.core.module import BaseModuleConfig -import ast -import atexit -import os -from typing import Union, List -from pydantic import FilePath -from pathlib import Path -import black - - -class FlexAgentGenerator: - orig_mpc_module_config: BaseMPCConfig - baseline_mpc_module_config: BaseMPCConfig - pos_flex_mpc_module_config: BaseMPCConfig - neg_flex_mpc_module_config: BaseMPCConfig - indicator_module_config: FlexibilityIndicatorModuleConfig - market_module_config: FlexibilityMarketModuleConfig - - def __init__( - self, - flex_config: Union[str, FilePath, FlexQuantConfig], - mpc_agent_config: Union[str, FilePath, AgentConfig], - ): - if isinstance(flex_config, str or FilePath): - self.flex_config_file_name = os.path.basename(flex_config) - else: - # provide default name for json - self.flex_config_file_name = "flex_config.json" - # load configs - self.flex_config = load_config.load_config( - flex_config, config_type=FlexQuantConfig - ) - - # original mpc agent - self.orig_mpc_agent_config = load_config.load_config( - mpc_agent_config, config_type=AgentConfig - ) - # baseline agent - self.baseline_mpc_agent_config = self.orig_mpc_agent_config.__deepcopy__() - # pos agent - self.pos_flex_mpc_agent_config = self.orig_mpc_agent_config.__deepcopy__() - # neg agent - self.neg_flex_mpc_agent_config = self.orig_mpc_agent_config.__deepcopy__() - - # original mpc module - self.orig_mpc_module_config = cmng.get_module( - config=self.orig_mpc_agent_config, - module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - ) - # baseline module - self.baseline_mpc_module_config = cmng.get_module( - config=self.baseline_mpc_agent_config, - module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - ) - # pos module - self.pos_flex_mpc_module_config = cmng.get_module( - config=self.pos_flex_mpc_agent_config, - module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - ) - # neg module - self.neg_flex_mpc_module_config = cmng.get_module( - config=self.neg_flex_mpc_agent_config, - module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - ) - # load indicator config - self.indicator_config = load_config.load_config( - self.flex_config.indicator_config, config_type=FlexibilityIndicatorConfig - ) - # load indicator module config - self.indicator_agent_config = load_config.load_config( - self.flex_config.indicator_config.agent_config, config_type=AgentConfig - ) - self.indicator_module_config = cmng.get_module( - config=self.indicator_agent_config, module_type=cmng.INDICATOR_CONFIG_TYPE - ) - # load market config - if self.flex_config.market_config: - self.market_config = load_config.load_config( - self.flex_config.market_config, config_type=FlexibilityMarketConfig - ) - # load market module config - self.market_agent_config = load_config.load_config( - self.market_config.agent_config, config_type=AgentConfig - ) - self.market_module_config = cmng.get_module( - config=self.market_agent_config, module_type=cmng.MARKET_CONFIG_TYPE - ) - else: - self.flex_config.market_time = 0 - - def generate_flex_agents( - self, - ) -> [ - BaseMPCConfig, - BaseMPCConfig, - BaseMPCConfig, - FlexibilityIndicatorModuleConfig, - FlexibilityMarketModuleConfig, - ]: - """Generates the configs and the python module for the flexibility agents. - Power variable must be defined in the mpc config. - - """ - - # adapt modules to include necessary communication variables and dump jsons of the agents including the adapted module configs - indicator_module_config = self.adapt_indicator_config( - module_config=self.indicator_module_config - ) - self.append_module_and_dump_agent( - module=indicator_module_config, - agent=self.indicator_agent_config, - module_type=cmng.INDICATOR_CONFIG_TYPE, - config_name=self.flex_config.indicator_config.name_of_created_file, - ) - if self.flex_config.market_config: - market_module_config = self.adapt_market_config( - module_config=self.market_module_config - ) - self.append_module_and_dump_agent( - module=market_module_config, - agent=self.market_agent_config, - module_type=cmng.MARKET_CONFIG_TYPE, - config_name=self.market_config.name_of_created_file, - ) - - # check if the power variable exists in the mpc config - if self.flex_config.baseline_config_generator_data.power_variable not in [ - output.name for output in self.baseline_mpc_module_config.outputs - ]: - raise ConfigurationError( - f"Given power variable {self.flex_config.baseline_config_generator_data.power_variable} is not defined as output in baseline mpc config." - ) - # check if the comfort variable exists in the mpc slack variables - if self.flex_config.baseline_config_generator_data.comfort_variable: - file_path = self.baseline_mpc_module_config.optimization_backend["model"]["type"]["file"] - class_name = self.baseline_mpc_module_config.optimization_backend["model"]["type"]["class_name"] - # Get the class - dynamic_class = cmng.get_class_from_file(file_path, class_name) - if self.flex_config.baseline_config_generator_data.comfort_variable not in [ - state.name for state in dynamic_class().states - ]: - raise ConfigurationError( - f"Given comfort variable {self.flex_config.baseline_config_generator_data.comfort_variable} is not defined as state in baseline mpc config." - ) - # check if the energy storage variable exists in the mpc config - if indicator_module_config.correct_costs.enable_energy_costs_correction: - if indicator_module_config.correct_costs.stored_energy_variable not in [ - output.name for output in self.baseline_mpc_module_config.outputs - ]: - raise ConfigurationError( - f"The stored energy variable {indicator_module_config.correct_costs.stored_energy_variable} is not defined in baseline mpc config." - f"It must be defined in the base MPC model and config as output if the correction of costs is enabled" - ) - - # adapt modules to include necessary communication variables - baseline_mpc_config = self.adapt_mpc_module_config( - module_config=self.baseline_mpc_module_config, - mpc_dataclass=self.flex_config.baseline_config_generator_data, - ) - pf_mpc_config = self.adapt_mpc_module_config( - module_config=self.pos_flex_mpc_module_config, - mpc_dataclass=self.flex_config.shadow_mpc_config_generator_data.pos_flex, - ) - nf_mpc_config = self.adapt_mpc_module_config( - module_config=self.neg_flex_mpc_module_config, - mpc_dataclass=self.flex_config.shadow_mpc_config_generator_data.neg_flex, - ) - - # dump jsons of the agents including the adapted module configs - self.append_module_and_dump_agent( - module=baseline_mpc_config, - agent=self.baseline_mpc_agent_config, - module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - config_name=self.flex_config.baseline_config_generator_data.name_of_created_file, - ) - self.append_module_and_dump_agent( - module=pf_mpc_config, - agent=self.pos_flex_mpc_agent_config, - module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - config_name=self.flex_config.shadow_mpc_config_generator_data.pos_flex.name_of_created_file, - ) - self.append_module_and_dump_agent( - module=nf_mpc_config, - agent=self.neg_flex_mpc_agent_config, - module_type=cmng.get_orig_module_type(self.orig_mpc_agent_config), - config_name=self.flex_config.shadow_mpc_config_generator_data.neg_flex.name_of_created_file, - ) - - # generate python files for the shadow mpcs - self._generate_flex_model_definition() - - # save flex config to created flex files - with open(os.path.join(self.flex_config.path_to_flex_files, self.flex_config_file_name), "w") as f: - config_json = self.flex_config.model_dump_json(exclude_defaults=True) - f.write(config_json) - - # register the exit function if the corresponding flag is set - if self.flex_config.delete_files: - atexit.register(lambda: self._delete_created_files()) - return self.get_config_file_paths() - - def append_module_and_dump_agent( - self, - module: BaseModuleConfig, - agent: AgentConfig, - module_type: str, - config_name: str, - ): - """Appends the given module config to the given agent config and dumps the agent config to a - json file. The json file is named based on the config_name.""" - - # if module is not from the baseline, set a new agent id, based on module id - if module.type is not self.baseline_mpc_module_config.type: - agent.id = module.module_id - # get the module as a dict without default values - module_dict = cmng.to_dict_and_remove_unnecessary_fields(module=module) - # write given module to agent config - for i, agent_module in enumerate(agent.modules): - if ( - cmng.MODULE_TYPE_DICT[module_type] - is cmng.MODULE_TYPE_DICT[agent_module["type"]] - ): - agent.modules[i] = module_dict - - # create folder - Path(self.flex_config.path_to_flex_files).mkdir(parents=True, exist_ok=True) - # dump agent config - if agent.modules: - if self.flex_config.overwrite_files: - try: - Path( - os.path.join(self.flex_config.path_to_flex_files, config_name) - ).unlink() - except OSError: - pass - with open( - os.path.join(self.flex_config.path_to_flex_files, config_name), "w+" - ) as f: - module_json = agent.model_dump_json(exclude_defaults=True) - f.write(module_json) - else: - logging.error("Provided agent config does not contain any modules.") - - def get_config_file_paths(self) -> List[str]: - """Returns a list of paths with the created config files""" - paths = [ - os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.baseline_config_generator_data.name_of_created_file, - ), - os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.shadow_mpc_config_generator_data.pos_flex.name_of_created_file, - ), - os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.shadow_mpc_config_generator_data.neg_flex.name_of_created_file, - ), - os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.indicator_config.name_of_created_file, - ), - ] - if self.flex_config.market_config: - paths.append( - os.path.join( - self.flex_config.path_to_flex_files, - self.market_config.name_of_created_file, - ) - ) - return paths - - def _delete_created_files(self): - """Function to run at exit if the files are to be deleted""" - to_be_deleted = self.get_config_file_paths() - to_be_deleted.append( - os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config_file_name, - )) - # delete files - for file in to_be_deleted: - Path(file).unlink() - # also delete folder - Path(self.flex_config.path_to_flex_files).rmdir() - - def adapt_mpc_module_config( - self, module_config: BaseMPCConfig, mpc_dataclass: BaseMPCData - ) -> BaseMPCConfig: - """Adapts the mpc module config for automated flexibility quantification. - Things adapted among others are: - - the file name/path of the mpc config file - - names of the control variables for the shadow mpcs - - reduce communicated variables of shadow mpcs to outputs - - add the power variable to the outputs - - add parameters for the activation and quantification of flexibility - - """ - # allow the module config to be changed - module_config.model_config["frozen"] = False - - module_config.module_id = mpc_dataclass.module_id - - # append the new weights as parameter to the MPC or update its value - parameter_dict = { - parameter.name: parameter for parameter in module_config.parameters - } - for weight in mpc_dataclass.weights: - if weight.name in parameter_dict: - parameter_dict[weight.name].value = weight.value - else: - module_config.parameters.append(weight) - - # set new MPC type - module_config.type = mpc_dataclass.module_types[ - cmng.get_orig_module_type(self.orig_mpc_agent_config) - ] - # set new id (needed for plotting) - module_config.module_id = mpc_dataclass.module_id - # update optimization backend to use the created mpc files and classes - module_config.optimization_backend["model"]["type"] = { - "file": os.path.join( - self.flex_config.path_to_flex_files, - mpc_dataclass.created_flex_mpcs_file, - ), - "class_name": mpc_dataclass.class_name, - } - # update results file with suffix - module_config.optimization_backend["results_file"] = ( - module_config.optimization_backend["results_file"].replace( - ".csv", mpc_dataclass.results_suffix - ) - ) - # change cia backend to custom backend of flexquant - if module_config.optimization_backend["type"] == "casadi_cia": - module_config.optimization_backend["type"] = "casadi_cia_cons" - module_config.optimization_backend["market_time"] = ( - self.flex_config.market_time - ) - - # add the control signal of the baseline to outputs (used during market time) - # and as inputs for the shadow mpcs - if type(mpc_dataclass) is not BaselineMPCData: - for control in module_config.controls: - module_config.inputs.append( - MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, - ) - ) - # also include binary controls - if hasattr(module_config, "binary_controls"): - for control in module_config.binary_controls: - module_config.inputs.append( - MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, - ) - ) - - # only communicate outputs for the shadow mpcs - module_config.shared_variable_fields = ["outputs"] - else: - for control in module_config.controls: - module_config.outputs.append( - MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, - ) - ) - # also include binary controls - if hasattr(module_config, "binary_controls"): - for control in module_config.binary_controls: - module_config.outputs.append( - MPCVariable( - name=glbs.full_trajectory_prefix - + control.name - + glbs.full_trajectory_suffix, - value=control.value, - ) - ) - module_config.set_outputs = True - # add outputs for the power variables, for easier handling create a lookup dict - output_dict = {output.name: output for output in module_config.outputs} - if ( - self.flex_config.baseline_config_generator_data.power_variable - in output_dict - ): - output_dict[ - self.flex_config.baseline_config_generator_data.power_variable - ].alias = mpc_dataclass.power_alias - else: - module_config.outputs.append( - MPCVariable( - name=self.flex_config.baseline_config_generator_data.power_variable, - alias=mpc_dataclass.power_alias, - ) - ) - # add or change alias for stored energy variable - if self.indicator_module_config.correct_costs.enable_energy_costs_correction: - output_dict[ - self.indicator_module_config.correct_costs.stored_energy_variable - ].alias = mpc_dataclass.stored_energy_alias - - - module_config.inputs.extend(mpc_dataclass.config_inputs_appendix) - # CONFIG_PARAMETERS_APPENDIX only includes dummy values - # overwrite dummy values with values from flex config and append it to module config - for var in mpc_dataclass.config_parameters_appendix: - if var.name in self.flex_config.model_fields: - var.value = getattr(self.flex_config, var.name) - if var.name in self.flex_config.baseline_config_generator_data.model_fields: - var.value = getattr(self.flex_config.baseline_config_generator_data, var.name) - module_config.parameters.extend(mpc_dataclass.config_parameters_appendix) - - # freeze the config again - module_config.model_config["frozen"] = True - - return module_config - - def adapt_indicator_config( - self, module_config: FlexibilityIndicatorModuleConfig - ) -> FlexibilityIndicatorModuleConfig: - """Adapts the indicator module config for automated flexibility quantification.""" - # allow the module config to be changed - module_config.model_config["frozen"] = False - for parameter in module_config.parameters: - if parameter.name == glbs.PREP_TIME: - parameter.value = self.flex_config.prep_time - if parameter.name == glbs.MARKET_TIME: - parameter.value = self.flex_config.market_time - if parameter.name == glbs.FLEX_EVENT_DURATION: - parameter.value = self.flex_config.flex_event_duration - if parameter.name == "time_step": - parameter.value = self.baseline_mpc_module_config.time_step - if parameter.name == "prediction_horizon": - parameter.value = self.baseline_mpc_module_config.prediction_horizon - # set power unit - module_config.power_unit = ( - self.flex_config.baseline_config_generator_data.power_unit - ) - module_config.results_file = Path( - Path(self.orig_mpc_module_config.optimization_backend["results_file"]).parent, - self.indicator_config.name_of_created_file.replace(".json", ".csv"), - ) - module_config.model_config["frozen"] = True - return module_config - - def adapt_market_config( - self, module_config: FlexibilityMarketModuleConfig - ) -> FlexibilityMarketModuleConfig: - """Adapts the market module config for automated flexibility quantification.""" - # allow the module config to be changed - module_config.model_config["frozen"] = False - for field in module_config.__fields__: - if field in self.market_module_config.__fields__.keys(): - module_config.__setattr__( - field, getattr(self.market_module_config, field) - ) - module_config.results_file = Path( - Path(self.orig_mpc_module_config.optimization_backend["results_file"]).parent, - self.market_config.name_of_created_file.replace(".json", ".csv"), - ) - module_config.model_config["frozen"] = True - return module_config - - def _generate_flex_model_definition(self): - """Generates a python module for negative and positive flexibility agents from - the Baseline MPC model - - """ - from flexibility_quantification.utils.parsing import ( - SetupSystemModifier, - add_import_to_tree, - ) - import astor - - output_file = os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.baseline_config_generator_data.created_flex_mpcs_file, - ) - opt_backend = self.orig_mpc_module_config.optimization_backend["model"]["type"] - - # Extract the config class of the casadi model to check cost functions - if self.orig_mpc_module_config.optimization_backend["type"] == "casadi_ml": - config_class = inspect.get_annotations(custom_injection(opt_backend))["config"] - ml_model_sources = self.orig_mpc_module_config.optimization_backend["model"]["ml_model_sources"] - config_instance = config_class(ml_model_sources=ml_model_sources) - else: - config_class = inspect.get_annotations(custom_injection(opt_backend))["config"] - config_instance = config_class() - - self.check_variables_in_casadi_config( - config_instance, - self.flex_config.shadow_mpc_config_generator_data.neg_flex.flex_cost_function, - ) - self.check_variables_in_casadi_config( - config_instance, - self.flex_config.shadow_mpc_config_generator_data.pos_flex.flex_cost_function, - ) - - # parse mpc python file - with open(opt_backend["file"], "r") as f: - source = f.read() - tree = ast.parse(source) - - # create modifiers for python file - modifier_base = SetupSystemModifier( - mpc_data=self.flex_config.baseline_config_generator_data, - controls=self.baseline_mpc_module_config.controls, - binary_controls=self.baseline_mpc_module_config.binary_controls if hasattr(self.baseline_mpc_module_config, "binary_controls") else None, - ) - modifier_pos = SetupSystemModifier( - mpc_data=self.flex_config.shadow_mpc_config_generator_data.pos_flex, - controls=self.pos_flex_mpc_module_config.controls, - binary_controls=self.pos_flex_mpc_module_config.binary_controls if hasattr(self.pos_flex_mpc_module_config, "binary_controls") else None, - ) - modifier_neg = SetupSystemModifier( - mpc_data=self.flex_config.shadow_mpc_config_generator_data.neg_flex, - controls=self.neg_flex_mpc_module_config.controls, - binary_controls=self.neg_flex_mpc_module_config.binary_controls if hasattr(self.neg_flex_mpc_module_config, "binary_controls") else None, - ) - # run the modification - modified_tree_base = modifier_base.visit(deepcopy(tree)) - modified_tree_pos = modifier_pos.visit(deepcopy(tree)) - modified_tree_neg = modifier_neg.visit(deepcopy(tree)) - # combine modifications to one file - modified_tree = ast.Module(body=[], type_ignores=[]) - modified_tree.body.extend( - modified_tree_base.body + modified_tree_pos.body + modified_tree_neg.body - ) - modified_source = astor.to_source(modified_tree) - # Use black to format the generated code - formatted_code = black.format_str(modified_source, mode=black.FileMode()) - - if self.flex_config.overwrite_files: - try: - Path( - os.path.join( - self.flex_config.path_to_flex_files, - self.flex_config.baseline_config_generator_data.created_flex_mpcs_file, - ) - ).unlink() - except OSError: - pass - - with open(output_file, "w") as f: - f.write(formatted_code) - - def check_variables_in_casadi_config(self, config: CasadiModelConfig, expr: str): - """Check if all variables in the expression are defined in the config. - - Args: - config (CasadiModelConfig): casadi model config. - expr (str): The expression to check. - - Raises: - ValueError: If any variable in the expression is not defined in the config. - - """ - variables_in_config = set(config.get_variable_names()) - variables_in_cost_function = set(ast.walk(ast.parse(expr))) - variables_in_cost_function = { - node.attr - for node in variables_in_cost_function - if isinstance(node, ast.Attribute) - } - variables_newly_created = set( - weight.name - for weight in self.flex_config.shadow_mpc_config_generator_data.weights - ) - unknown_vars = ( - variables_in_cost_function - variables_in_config - variables_newly_created - ) - if unknown_vars: - raise ValueError(f"Unknown variables in new cost function: {unknown_vars}") diff --git a/flexibility_quantification/modules/shadow_mpc.py b/flexibility_quantification/modules/shadow_mpc.py deleted file mode 100644 index a79a87a5..00000000 --- a/flexibility_quantification/modules/shadow_mpc.py +++ /dev/null @@ -1,133 +0,0 @@ -from agentlib_mpc.modules import mpc_full, minlp_mpc -from flexibility_quantification.utils.data_handling import strip_multi_index, fill_nans, MEAN, INTERPOLATE -from flexibility_quantification.data_structures.globals import ( - full_trajectory_prefix, - full_trajectory_suffix, -) -from typing import Dict, Union -from agentlib.core.datamodels import AgentVariable -from agentlib_mpc.data_structures.mpc_datamodels import Results - - - -class FlexibilityShadowMPC(mpc_full.MPC): - - config: mpc_full.MPCConfig - - def __init__(self, *args, **kwargs): - # create instance variable - self._full_controls: Dict[str, Union[AgentVariable, None]] = {} - super().__init__(*args, **kwargs) - - def register_callbacks(self): - for control_var in self.config.controls: - self.agent.data_broker.register_callback( - name=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", - alias=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", - callback=self.calc_flex_callback, - ) - for input_var in self.config.inputs: - if input_var.name.replace(full_trajectory_prefix, "", 1).replace( - full_trajectory_suffix, "" - ) in [control_var.name for control_var in self.config.controls]: - self._full_controls[input_var.name] = input_var - - super().register_callbacks() - - def calc_flex_callback(self, inp, name): - """set the control trajectories before calculating the flexibility offer. - self.model should account for flexibility in its cost function - - """ - # during provision dont calculate flex - if self.get("in_provision").value: - return - - # do not trigger callback on self set variables - if self.agent.config.id == inp.source.agent_id: - return - - vals = strip_multi_index(inp.value) - if vals.isna().any(): - vals = fill_nans(series=vals, method=MEAN) - - # the MPC Predictions starts at t=env.now not t=0 - vals.index += self.env.time - self._full_controls[name].value = vals - self.set(name, vals) - # make sure all controls are set - if all(x.value is not None for x in self._full_controls.values()): - self.do_step() - for name in self._full_controls.keys(): - self._full_controls[name].value = None - - def process(self): - # the shadow mpc should only be run after the results of the baseline are sent - yield self.env.event() - - def set_actuation(self, solution: Results): - """Takes the solution from optimization backend and sends the first - step to AgentVariables.""" - self.logger.info("Sending optimal control values to data_broker.") - tolerance = 1e-5 - for control in self.var_ref.controls: - ub = self.get(control).ub - lb = self.get(control).lb - # take the first entry of the control trajectory - actuation = solution.df.variable[control].dropna() - self.set(control, actuation) - -class FlexibilityShadowMINLPMPC(minlp_mpc.MINLPMPC): - - config: minlp_mpc.MINLPMPCConfig - - def __init__(self, *args, **kwargs): - # create instance variable - self._full_controls: Dict[str, Union[AgentVariable, None]] = {} - super().__init__(*args, **kwargs) - - def register_callbacks(self): - for control_var in self.config.controls + self.config.binary_controls: - self.agent.data_broker.register_callback( - name=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", - alias=f"{full_trajectory_prefix}{control_var.name}{full_trajectory_suffix}", - callback=self.calc_flex_callback, - ) - for input_var in self.config.inputs: - if input_var.name.replace(full_trajectory_prefix, "", 1).replace( - full_trajectory_suffix, "" - ) in [control_var.name for control_var in self.config.controls + self.config.binary_controls]: - self._full_controls[input_var.name] = input_var - - super().register_callbacks() - - def calc_flex_callback(self, inp, name): - """set the control trajectories before calculating the flexibility offer. - self.model should account for flexibility in its cost function - - """ - # during provision dont calculate flex - if self.get("in_provision").value: - return - - # do not trigger callback on self set variables - if self.agent.config.id == inp.source.agent_id: - return - - vals = strip_multi_index(inp.value) - if vals.isna().any(): - vals = fill_nans(vals, method=MEAN) - - # the MPC Predictions starts at t=env.now not t=0 - vals.index += self.env.time - self._full_controls[name].value = vals - self.set(name, vals) - # make sure all controls are set - if all(x.value is not None for x in self._full_controls.values()): - self.do_step() - for name in self._full_controls.keys(): - self._full_controls[name].value = None - - def process(self): - # the shadow mpc should only be run after the results of the baseline are sent - yield self.env.event() diff --git a/flexibility_quantification/utils/data_handling.py b/flexibility_quantification/utils/data_handling.py deleted file mode 100644 index 0aa55392..00000000 --- a/flexibility_quantification/utils/data_handling.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import Literal -import pandas as pd -from agentlib_mpc.utils import TimeConversionTypes, TIME_CONVERSION - - -MEAN: str = "mean" -INTERPOLATE: str = "interpolate" -FillNansMethods = Literal[MEAN, INTERPOLATE] - - -def fill_nans(series: pd.Series, method: FillNansMethods) -> pd.Series: - """ - Fill NaN values in the series with the given method. - - Implemented methods: - - mean: fill NaN values with the mean of the following values. - - interpolate: interpolate missing values. - """ - #ignore lags from casadi_ml models - series = series[series.index >= 0] - if method == MEAN: - series = _set_mean_values(series=series) - elif method == INTERPOLATE: - # Interpolate missing values - series = series.interpolate(method="index", limit_direction="both") - - if series.isna().any(): - raise ValueError(f"NaN values are still present in the series after filling them with the method {method}\n{series}") - return series - - -def _set_mean_values(series: pd.Series) -> pd.Series: - """ Fills intervals including the nan with the mean of the following values. """ - def _get_intervals_for_mean(s: pd.Series) -> list[pd.Interval]: - intervals = [] - start = None - for index, value in s.items(): - if pd.isna(value): - if pd.isna(start): - start = index - else: - end = index - intervals.append(pd.Interval(left=start, right=end, closed="left")) - start = end - return intervals - - for interval in _get_intervals_for_mean(series): - interval_index = (interval.left <= series.index) & (series.index < interval.right) - series[interval.left] = series[interval_index].mean(skipna=True) - - # remove last entry if nan, e.g. with collocation - if pd.isna(series.iloc[-1]): - series = series.iloc[:-1] - - return series - - -def strip_multi_index(series: pd.Series) -> pd.Series: - # Convert the index (communicated as string) into a MultiIndex - if isinstance(series.index[0], str): - series.index = series.index.map(lambda x: eval(x)) - series.index = pd.MultiIndex.from_tuples(series.index) - # vals is multicolumn so get rid of first value (start time of predictions) - series.index = series.index.get_level_values(1).astype(float) - return series - - -def convert_timescale_of_index(df: pd.DataFrame, from_unit: TimeConversionTypes, to_unit: TIME_CONVERSION) -> pd.DataFrame: - """ Convert the timescale of a dataframe index (from seconds) to the given time unit - - Keyword arguments: - results -- The dictionary of the results with the dataframes - time_unit -- The time unit to convert the index to - """ - time_conversion_factor = TIME_CONVERSION[from_unit] / TIME_CONVERSION[to_unit] - if isinstance(df.index, pd.MultiIndex): - df.index = pd.MultiIndex.from_arrays( - [df.index.get_level_values(level) * time_conversion_factor for level in range(df.index.nlevels)] - ) - else: - df.index = df.index * time_conversion_factor - return df From da6e785060df290238ad9a3a7e0503773ff1f3c8 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Tue, 10 Feb 2026 12:30:29 +0100 Subject: [PATCH 15/25] allow multiple shooting note: not properly tested so far and did not check market with multiple shooting --- .../data_structures/flex_kpis.py | 89 +++++++++------ .../data_structures/flex_results.py | 2 +- agentlib_flexquant/data_structures/globals.py | 2 +- agentlib_flexquant/generate_flex_agents.py | 101 ++++++++++-------- agentlib_flexquant/modules/baseline_mpc.py | 4 +- .../modules/flexibility_indicator.py | 52 +++++---- .../modules/flexibility_market.py | 10 +- agentlib_flexquant/utils/config_management.py | 2 +- 8 files changed, 159 insertions(+), 103 deletions(-) diff --git a/agentlib_flexquant/data_structures/flex_kpis.py b/agentlib_flexquant/data_structures/flex_kpis.py index bafae678..3bada80a 100644 --- a/agentlib_flexquant/data_structures/flex_kpis.py +++ b/agentlib_flexquant/data_structures/flex_kpis.py @@ -1,4 +1,4 @@ -""" +"""" Module for representing and calculating flexibility KPIs. It defines Pydantic models for scalar and time-series KPIs, and provides methods to compute power, energy, and cost metrics for positive and negative flexibility scenarios. @@ -106,7 +106,7 @@ def integrate(self, time_unit: TimeConversionTypes = "seconds") -> float: if self.integration_method == LINEAR: # Linear integration: apply the trapezoidal rule, which assumes # the function changes linearly between sample points - return np.trapz(self.value.values, + return np.trapezoid(self.value.values, self.value.index) / TIME_CONVERSION[time_unit] if self.integration_method == CONSTANT: # Constant integration: use a step-wise constant approach by @@ -200,7 +200,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. @@ -215,7 +215,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 """ @@ -227,10 +228,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 @@ -245,7 +246,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() @@ -303,27 +304,42 @@ 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" - ) + power_flex_offer = self.power_flex_offer.value.iloc[:-1] + + # Only drop collocation points if using collocation method + if time_grid_info and time_grid_info.get("type") == "collocation": + power_flex_offer = power_flex_offer.drop( + time_grid_info["grid"], errors="ignore" + ) + 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" - ) + + # Only drop collocation points if using collocation method + if time_grid_info and time_grid_info.get("type") == "collocation": + power_flex_offer_integration.value = power_flex_offer_integration.value.drop( + time_grid_info["grid"], errors="ignore" + ) + # Calculate the average and stores the original value power_flex_offer_avg = power_flex_offer_integration.avg() @@ -352,9 +368,14 @@ 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.") @@ -363,9 +384,13 @@ def _calculate_energy_flex(self, mpc_time_grid, collocation_time_grid: list = No 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" - ) + + # Only drop collocation points if using collocation method + if time_grid_info and time_grid_info.get("type") == "collocation": + power_flex_offer_integration.value = power_flex_offer_integration.value.drop( + time_grid_info["grid"], errors="ignore" + ) + # Calculate the energy flex and stores the original value energy_flex = power_flex_offer_integration.integrate(time_unit="hours") @@ -378,7 +403,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. @@ -388,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 """ @@ -405,9 +430,12 @@ 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" - ) + + # Only drop collocation points if using collocation method + 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" + ) # Calculate series self.electricity_costs_series.value = ( @@ -605,7 +633,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 for the positive and negative flexibility. @@ -613,7 +641,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 """ self.kpis_pos.calculate( @@ -627,7 +656,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, @@ -640,7 +669,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 @@ -657,4 +686,4 @@ def reset_time_grid(self): Reset the common time grid. This should be called between different flexibility calculations. """ - self._common_time_grid = None + self._common_time_grid = None \ No newline at end of file diff --git a/agentlib_flexquant/data_structures/flex_results.py b/agentlib_flexquant/data_structures/flex_results.py index 1ceae378..ca5c5658 100644 --- a/agentlib_flexquant/data_structures/flex_results.py +++ b/agentlib_flexquant/data_structures/flex_results.py @@ -13,7 +13,7 @@ from agentlib.core.agent import AgentConfig from agentlib.modules.simulation.simulator import SimulatorConfig from agentlib.utils import load_config -from agentlib_mpc.modules.mpc import BaseMPCConfig +from agentlib_mpc.modules.mpc.mpc import BaseMPCConfig from agentlib_mpc.utils import TimeConversionTypes from agentlib_mpc.utils.analysis import load_mpc, load_mpc_stats, load_sim diff --git a/agentlib_flexquant/data_structures/globals.py b/agentlib_flexquant/data_structures/globals.py index 36823d33..14d96bdc 100644 --- a/agentlib_flexquant/data_structures/globals.py +++ b/agentlib_flexquant/data_structures/globals.py @@ -27,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" diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index 12429d9c..cc5311a9 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -30,7 +30,7 @@ from agentlib.utils import custom_injection, load_config from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable from agentlib_mpc.models.casadi_model import CasadiModelConfig -from agentlib_mpc.modules.mpc_full import MPCConfig +from agentlib_mpc.modules.mpc.mpc_full import MPCConfig from agentlib_mpc.optimization_backends.casadi_.basic import DirectCollocation from agentlib_mpc.data_structures.casadi_utils import CasadiDiscretizationOptions @@ -374,6 +374,7 @@ def adapt_mpc_module_config( self.flex_config.baseline_config_generator_data.power_variable) module_config_flex_dict["storage_variable_name"] = ( self.indicator_module_config.correct_costs.stored_energy_variable) + del module_config_flex_dict['r_del_u'] module_config_flex = cmng.MODULE_TYPE_DICT[module_config.type]( **module_config_flex_dict, _agent_id=agent_id ) @@ -582,11 +583,11 @@ def adapt_indicator_module_config( parameter.value = self.baseline_mpc_module_config.time_step if parameter.name == glbs.PREDICTION_HORIZON: parameter.value = self.baseline_mpc_module_config.prediction_horizon - if parameter.name == glbs.COLLOCATION_TIME_GRID: + if parameter.name == glbs.TIME_GRID_INFO: dis_op = self.baseline_mpc_module_config.optimization_backend[ "discretization_options" ] - parameter.value = self.get_collocation_time_grid( + parameter.value = self.get_time_grid( discretization_options=dis_op ) # set power unit @@ -612,11 +613,11 @@ def adapt_market_module_config( self.flex_config.results_directory / module_config.results_file.name ) for parameter in module_config.parameters: - if parameter.name == glbs.COLLOCATION_TIME_GRID: + if parameter.name == glbs.TIME_GRID_INFO: dis_op = self.baseline_mpc_module_config.optimization_backend[ "discretization_options" ] - parameter.value = self.get_collocation_time_grid( + parameter.value = self.get_time_grid( discretization_options=dis_op ) if parameter.name == glbs.TIME_STEP: @@ -646,30 +647,40 @@ def adapt_and_dump_flex_config(self): config_json = self.flex_config.model_dump_json(exclude_defaults=True) f.write(config_json) - def get_collocation_time_grid(self, discretization_options: dict): - """Get the mpc output collocation grid over the horizon""" - # get the mpc time grid configuration + def get_time_grid(self, discretization_options: dict): + """Get the mpc output collocation grid over the horizon. + + Returns a dict with 'type' and 'grid' keys. + """ time_step = self.baseline_mpc_module_config.time_step prediction_horizon = self.baseline_mpc_module_config.prediction_horizon - # get the collocation configuration - collocation_method = discretization_options["collocation_method"] - collocation_order = discretization_options["collocation_order"] - # get the collocation points - options = CasadiDiscretizationOptions( - collocation_order=collocation_order, collocation_method=collocation_method - ) - collocation_points = DirectCollocation(options= - options)._collocation_polynomial().root - # compute the mpc output collocation grid - discretization_points = np.arange(0, time_step * prediction_horizon, time_step) - collocation_time_grid = ( - discretization_points[:, None] + collocation_points * time_step - ).ravel() - collocation_time_grid = collocation_time_grid[ - ~np.isin(collocation_time_grid, discretization_points) - ] - collocation_time_grid = collocation_time_grid.tolist() - return collocation_time_grid + + # Check if using multiple shooting + if discretization_options.get("method") == "multiple_shooting": + grid = np.arange(0, (prediction_horizon + 1) * time_step, time_step) + return {"type": "multiple_shooting", "grid": grid.tolist()} + + # For collocation, compute the time grid + if "collocation_method" not in discretization_options: + return {"type": "none", "grid": []} + + else: + collocation_method = discretization_options["collocation_method"] + collocation_order = discretization_options["collocation_order"] + # get the collocation points + options = CasadiDiscretizationOptions( + collocation_order=collocation_order, collocation_method=collocation_method + ) + collocation_points = DirectCollocation(options=options)._collocation_polynomial().root + # compute the mpc output collocation grid + discretization_points = np.arange(0, time_step * prediction_horizon, time_step) + time_grid = ( + discretization_points[:, None] + collocation_points * time_step + ).ravel() + time_grid = time_grid[ + ~np.isin(time_grid, discretization_points) + ] + return {"type": "collocation", "grid": time_grid.tolist()} def _generate_flex_model_definition(self): """Generate a python module for negative and positive flexibility agents @@ -782,7 +793,7 @@ def run_config_validations(self): """Function to validate integrity of user-supplied flex config. Since the validation depends on interactions between multiple configurations, - it is performed within this function rather than using Pydantic’s built-in + it is performed within this function rather than using Pydantic's built-in validators for individual configurations. The following checks are performed: @@ -790,8 +801,8 @@ def run_config_validations(self): 2. Ensures the specified comfort variable exists in the MPC model states. 3. Validates that the stored energy variable exists in MPC outputs if energy cost correction is enabled. - 4. Verifies the supported collocation method is used; otherwise, - switches to 'legendre' and raises a warning. + 4. Verifies a supported discretization method is used (collocation or multiple shooting); + if collocation is used, validates the collocation method. 5. Ensures that the sum of prep time, market time, and flex event duration does not exceed the prediction horizon. 6. Ensures market time equals the MPC model time step if market config is @@ -840,20 +851,26 @@ def run_config_validations(self): f"if the correction of costs is enabled." ) - # raise warning if unsupported collocation method is used and change - # to supported method - if ( - "collocation_method" - not in self.baseline_mpc_module_config.optimization_backend["discretization_options"] - ): + # validate discretization method (collocation or multiple shooting) + discretization_options = self.baseline_mpc_module_config.optimization_backend.get( + "discretization_options", {} + ) + + # Check if using multiple shooting or collocation + # Multiple shooting typically doesn't require collocation_method + is_multiple_shooting = discretization_options.get("method") == "multiple_shooting" + has_collocation_method = "collocation_method" in discretization_options + + if not is_multiple_shooting and not has_collocation_method: raise ConfigurationError( - "Please use collocation as discretization method and define the " - "collocation_method in the mpc config" + "Please specify a valid discretization method. Either use multiple shooting " + "(set method='multiple_shooting' in discretization_options) or use collocation " + "with a defined collocation_method in the mpc config." ) - else: - collocation_method = self.baseline_mpc_module_config.optimization_backend[ - "discretization_options" - ]["collocation_method"] + + # If using collocation, validate the collocation method + if has_collocation_method: + collocation_method = discretization_options["collocation_method"] if collocation_method != "legendre": self.logger.warning( "Collocation method %s is not supported. Switching to " diff --git a/agentlib_flexquant/modules/baseline_mpc.py b/agentlib_flexquant/modules/baseline_mpc.py index e81b2686..643b4dd4 100644 --- a/agentlib_flexquant/modules/baseline_mpc.py +++ b/agentlib_flexquant/modules/baseline_mpc.py @@ -1,7 +1,7 @@ """ Defines MPC and MINLP-MPC for baseline flexibility quantification. """ -from agentlib_mpc.modules import minlp_mpc, mpc_full +from agentlib_mpc.modules.mpc import minlp_mpc, mpc_full import os import math import numpy as np @@ -11,7 +11,7 @@ from collections.abc import Iterable import agentlib_flexquant.data_structures.globals as glbs from agentlib import AgentVariable -from agentlib_mpc.modules import mpc_full, minlp_mpc +from agentlib_mpc.modules.mpc import mpc_full, minlp_mpc from agentlib_mpc.data_structures.mpc_datamodels import Results, InitStatus from agentlib_flexquant.data_structures.globals import (full_trajectory_suffix, base_vars_to_communicate_suffix) diff --git a/agentlib_flexquant/modules/flexibility_indicator.py b/agentlib_flexquant/modules/flexibility_indicator.py index 73318deb..0e3a7cc3 100644 --- a/agentlib_flexquant/modules/flexibility_indicator.py +++ b/agentlib_flexquant/modules/flexibility_indicator.py @@ -1,4 +1,4 @@ -""" +"""" Flexibility indicator module for calculating and distributing energy flexibility offers. This module processes power and energy profiles from baseline and shadow MPCs to @@ -15,7 +15,7 @@ import numpy as np import pandas as pd from pydantic import BaseModel, ConfigDict, Field, model_validator -from agentlib_flexquant.utils.data_handling import fill_nans, MEAN +from agentlib_flexquant.utils.data_handling import fill_nans, MEAN, INTERPOLATE import agentlib_flexquant.data_structures.globals as glbs from agentlib_flexquant.data_structures.flex_kpis import ( @@ -300,9 +300,9 @@ class FlexibilityIndicatorModuleConfig(agentlib.BaseModuleConfig): description="timestep of the mpc solution"), agentlib.AgentVariable(name=glbs.PREDICTION_HORIZON, unit="-", description="prediction horizon of the mpc solution"), - agentlib.AgentVariable(name=glbs.COLLOCATION_TIME_GRID, - alias=glbs.COLLOCATION_TIME_GRID, - description="Time grid of the mpc model output") + agentlib.AgentVariable(name=glbs.TIME_GRID_INFO, + alias=glbs.TIME_GRID_INFO, + description="Time grid info with 'type' and 'grid' keys") ] results_file: Optional[Path] = Field( @@ -510,9 +510,11 @@ def write_results(self, df: pd.DataFrame, ts: float, n: int) -> pd.DataFrame: values = self.data.stored_energy_profile_flex_pos elif name == self.config.price_variable: values = self.data.electricity_price_series - elif name == glbs.COLLOCATION_TIME_GRID: - value = self.get(name).value - values = pd.Series(index=value, data=value) + elif name == glbs.TIME_GRID_INFO: + time_grid_info = self.get(name).value + # Store the grid as a series for results + grid = time_grid_info.get("grid", []) if time_grid_info else [] + values = pd.Series(index=grid, data=grid) if grid else pd.Series() else: values = self.get(name).value @@ -571,18 +573,25 @@ def calc_and_send_offer(self): """Calculate the flexibility KPIs for current predictions, send the flex offer and set the outputs, write and save the results.""" # Calculate the flexibility KPIs for current predictions - collocation_time_grid = self.get(glbs.COLLOCATION_TIME_GRID).value + time_grid_info = self.get(glbs.TIME_GRID_INFO).value self.data.calculate( enable_energy_costs_correction= self.config.correct_costs.enable_energy_costs_correction, calculate_flex_cost=self.config.calculate_costs.calculate_flex_costs, integration_method=self.config.integration_method, - collocation_time_grid=collocation_time_grid) + time_grid_info=time_grid_info) + + # get the grid from time_grid_info + time_grid = time_grid_info.get("grid", []) if time_grid_info else [] # get the full index during flex event including mpc_time_grid index and the - # collocation index - full_index = np.sort(np.concatenate([collocation_time_grid, - self.data.mpc_time_grid])) + is_collocation = time_grid_info and time_grid_info.get("type") == "collocation" + + if is_collocation: + full_index = np.sort(np.unique(np.concatenate([time_grid,self.data.mpc_time_grid]))) + else: + full_index = self.data.mpc_time_grid + flex_begin = self.get(glbs.MARKET_TIME).value + self.get(glbs.PREP_TIME).value flex_end = flex_begin + self.get(glbs.FLEX_EVENT_DURATION).value full_flex_offer_index = full_index[(full_index >= flex_begin) & @@ -591,16 +600,17 @@ def calc_and_send_offer(self): # reindex the power profiles to not send the simulation points to the market, # but only the values on the collocation points and the forward mean of them base_power_profile = self.data.power_profile_base.reindex( - collocation_time_grid).reindex(full_flex_offer_index) + time_grid).reindex(full_flex_offer_index) pos_diff_profile = self.data.kpis_pos.power_flex_offer.value.reindex( - collocation_time_grid).reindex(full_flex_offer_index) + time_grid).reindex(full_flex_offer_index) neg_diff_profile = self.data.kpis_neg.power_flex_offer.value.reindex( - collocation_time_grid).reindex(full_flex_offer_index) + time_grid).reindex(full_flex_offer_index) - # fill the mpc_time_grid with forward mean - base_power_profile = fill_nans(base_power_profile, method=MEAN) - pos_diff_profile = fill_nans(pos_diff_profile, method=MEAN) - neg_diff_profile = fill_nans(neg_diff_profile, method=MEAN) + if is_collocation: + # fill the mpc_time_grid with forward mean + base_power_profile = fill_nans(base_power_profile, method=MEAN) + pos_diff_profile = fill_nans(pos_diff_profile, method=MEAN) + neg_diff_profile = fill_nans(neg_diff_profile, method=MEAN) # Send flex offer self.send_flex_offer( @@ -717,4 +727,4 @@ def check_power_end_deviation(self, tol: float): ) self.set(kpis_neg.power_flex_within_boundary.get_kpi_identifier(), False) else: - self.set(kpis_neg.power_flex_within_boundary.get_kpi_identifier(), True) + self.set(kpis_neg.power_flex_within_boundary.get_kpi_identifier(), True) \ No newline at end of file diff --git a/agentlib_flexquant/modules/flexibility_market.py b/agentlib_flexquant/modules/flexibility_market.py index 51a54714..243619b0 100644 --- a/agentlib_flexquant/modules/flexibility_market.py +++ b/agentlib_flexquant/modules/flexibility_market.py @@ -43,7 +43,7 @@ class FlexibilityMarketModuleConfig(agentlib.BaseModuleConfig): ] parameters: list[AgentVariable] = [ - AgentVariable(name=glbs.COLLOCATION_TIME_GRID, alias=glbs.COLLOCATION_TIME_GRID, + AgentVariable(name=glbs.TIME_GRID_INFO, alias=glbs.TIME_GRID_INFO, description="Time grid of the mpc model output"), AgentVariable(name=glbs.TIME_STEP, unit="s", description="Time step of the mpc") ] @@ -183,10 +183,10 @@ def random_flexibility_callback(self, inp: AgentVariable, name: str): self.config.market_specs.accepted_offer_sample_points) if flex_power_feedback_method == glbs.COLLOCATION: profile = profile.reindex( - self.get(glbs.COLLOCATION_TIME_GRID).value) + self.get(glbs.TIME_GRID_INFO).value) elif flex_power_feedback_method == glbs.CONSTANT: index_to_keep = ~np.isin( - profile.index, self.get(glbs.COLLOCATION_TIME_GRID).value) + profile.index, self.get(glbs.TIME_GRID_INFO).value) profile = profile.get(index_to_keep) helper_indices = [i - 1 for i in profile.index[1:]] new_index = sorted(set(profile.index.tolist() + @@ -243,10 +243,10 @@ def single_flexibility_callback(self, inp: AgentVariable, name: str): self.config.market_specs.accepted_offer_sample_points) if flex_power_feedback_method == glbs.COLLOCATION: profile = profile.reindex(self.get( - glbs.COLLOCATION_TIME_GRID).value) + glbs.TIME_GRID_INFO).value) elif flex_power_feedback_method == glbs.CONSTANT: index_to_keep = ~np.isin(profile.index, - self.get(glbs.COLLOCATION_TIME_GRID).value) + self.get(glbs.TIME_GRID_INFO).value) profile = profile.get(index_to_keep) helper_indices = [i - 1 for i in profile.index[1:]] new_index = sorted(set(profile.index.tolist() + diff --git a/agentlib_flexquant/utils/config_management.py b/agentlib_flexquant/utils/config_management.py index f4a85162..bd0e1fa1 100644 --- a/agentlib_flexquant/utils/config_management.py +++ b/agentlib_flexquant/utils/config_management.py @@ -17,7 +17,7 @@ all_module_types.pop("agentlib_mpc.ann_trainer") all_module_types.pop("agentlib_mpc.gpr_trainer") all_module_types.pop("agentlib_mpc.linreg_trainer") -all_module_types.pop("agentlib_mpc.ann_simulator") +all_module_types.pop("agentlib_mpc.ml_simulator") all_module_types.pop("agentlib_mpc.set_point_generator") # remove clone since not used all_module_types.pop("clonemap") From 6d4a1b41d7b180cb81c43cdd70e4fbcc100b2e66 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Tue, 24 Feb 2026 09:57:22 +0100 Subject: [PATCH 16/25] minor import changes --- agentlib_flexquant/data_structures/flex_kpis.py | 2 +- .../data_structures/flex_results.py | 4 ++-- agentlib_flexquant/data_structures/mpcs.py | 2 +- agentlib_flexquant/generate_flex_agents.py | 16 ++++++---------- agentlib_flexquant/modules/baseline_mpc.py | 3 +-- agentlib_flexquant/modules/shadow_mpc.py | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/agentlib_flexquant/data_structures/flex_kpis.py b/agentlib_flexquant/data_structures/flex_kpis.py index 3bada80a..ef81447a 100644 --- a/agentlib_flexquant/data_structures/flex_kpis.py +++ b/agentlib_flexquant/data_structures/flex_kpis.py @@ -106,7 +106,7 @@ def integrate(self, time_unit: TimeConversionTypes = "seconds") -> float: if self.integration_method == LINEAR: # Linear integration: apply the trapezoidal rule, which assumes # the function changes linearly between sample points - return np.trapezoid(self.value.values, + return np.trapz(self.value.values, self.value.index) / TIME_CONVERSION[time_unit] if self.integration_method == CONSTANT: # Constant integration: use a step-wise constant approach by diff --git a/agentlib_flexquant/data_structures/flex_results.py b/agentlib_flexquant/data_structures/flex_results.py index ca5c5658..d7cc13d0 100644 --- a/agentlib_flexquant/data_structures/flex_results.py +++ b/agentlib_flexquant/data_structures/flex_results.py @@ -9,11 +9,11 @@ from typing import Any, Dict, Optional, Type, Union import pandas as pd -from pydantic import BaseModel, FilePath +from pydantic import FilePath from agentlib.core.agent import AgentConfig from agentlib.modules.simulation.simulator import SimulatorConfig from agentlib.utils import load_config -from agentlib_mpc.modules.mpc.mpc import BaseMPCConfig +from agentlib_mpc.modules import BaseMPCConfig from agentlib_mpc.utils import TimeConversionTypes from agentlib_mpc.utils.analysis import load_mpc, load_mpc_stats, load_sim diff --git a/agentlib_flexquant/data_structures/mpcs.py b/agentlib_flexquant/data_structures/mpcs.py index 3883d855..e8945bc5 100644 --- a/agentlib_flexquant/data_structures/mpcs.py +++ b/agentlib_flexquant/data_structures/mpcs.py @@ -8,7 +8,7 @@ """ import pydantic from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable -from pydantic import ConfigDict, model_validator, field_serializer +from pydantic import model_validator, field_serializer, Field import agentlib_flexquant.data_structures.globals as glbs import agentlib_flexquant.utils.config_management as cmng diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index cc5311a9..9113f170 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -1,19 +1,15 @@ """Generate agents for flexibility quantification. - -This module provides the FlexAgentGenerator class that creates and configures flexibility agents. +This module provides the FlexAgentGenerator class that creates and configures +flexibility agents. The agents created include the baseline, positive and negative flexibility agents, -the flexibility indicator and market agents. The agents are created based on the flex config and -the MPC config. +the flexibility indicator and market agents. The agents are created based on the +flex config and the MPC config. """ import ast import atexit import inspect -import json import logging import os -from copy import deepcopy -from pathlib import Path -from typing import Union import astor import black @@ -21,7 +17,7 @@ import numpy as np from copy import deepcopy from pathlib import Path -from typing import List, Union +from typing import Union from pydantic import FilePath from agentlib.core.agent import AgentConfig from agentlib.core.datamodels import AgentVariable @@ -30,7 +26,7 @@ from agentlib.utils import custom_injection, load_config from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable from agentlib_mpc.models.casadi_model import CasadiModelConfig -from agentlib_mpc.modules.mpc.mpc_full import MPCConfig +from agentlib_mpc.modules.mpc_full import MPCConfig from agentlib_mpc.optimization_backends.casadi_.basic import DirectCollocation from agentlib_mpc.data_structures.casadi_utils import CasadiDiscretizationOptions diff --git a/agentlib_flexquant/modules/baseline_mpc.py b/agentlib_flexquant/modules/baseline_mpc.py index 643b4dd4..e2b21a73 100644 --- a/agentlib_flexquant/modules/baseline_mpc.py +++ b/agentlib_flexquant/modules/baseline_mpc.py @@ -1,7 +1,6 @@ """ Defines MPC and MINLP-MPC for baseline flexibility quantification. """ -from agentlib_mpc.modules.mpc import minlp_mpc, mpc_full import os import math import numpy as np @@ -11,7 +10,7 @@ from collections.abc import Iterable import agentlib_flexquant.data_structures.globals as glbs from agentlib import AgentVariable -from agentlib_mpc.modules.mpc import mpc_full, minlp_mpc +from agentlib_mpc.modules import mpc_full, minlp_mpc from agentlib_mpc.data_structures.mpc_datamodels import Results, InitStatus from agentlib_flexquant.data_structures.globals import (full_trajectory_suffix, base_vars_to_communicate_suffix) diff --git a/agentlib_flexquant/modules/shadow_mpc.py b/agentlib_flexquant/modules/shadow_mpc.py index f3b8ea9c..fcb047f6 100644 --- a/agentlib_flexquant/modules/shadow_mpc.py +++ b/agentlib_flexquant/modules/shadow_mpc.py @@ -11,7 +11,7 @@ from typing import Dict, Union, Optional from collections.abc import Iterable from agentlib.core.datamodels import AgentVariable, Source -from agentlib_mpc.modules.mpc import mpc_full, minlp_mpc +from agentlib_mpc.modules import mpc_full, minlp_mpc from agentlib_mpc.data_structures.mpc_datamodels import Results from agentlib_flexquant.utils.data_handling import fill_nans, MEAN from agentlib_flexquant.data_structures.globals import (full_trajectory_suffix, From 16220f612beb71a42c7aa03067905f401cd57f5f Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Tue, 24 Feb 2026 15:33:29 +0100 Subject: [PATCH 17/25] new numpy version adjust example for linear regression / add to tests --- .../flexibility_agent_config.json | 9 +- .../flex_configs/flexibility_market.json | 5 +- .../main_one_room_flex.py | 34 ++-- .../OneRoom_SimpleLinRegMPC/plot_results.py | 9 +- .../data_structures/flex_kpis.py | 2 +- agentlib_flexquant/generate_flex_agents.py | 12 +- .../modules/flexibility_market.py | 9 +- agentlib_flexquant/utils/parsing.py | 3 +- .../mpc_and_sim/simple_model.json | 2 +- requirements.txt | 1 + tests/test_oneRoom_SimpleLinRegMPC.py | 146 ++++++++++++++++++ 11 files changed, 184 insertions(+), 48 deletions(-) create mode 100644 tests/test_oneRoom_SimpleLinRegMPC.py diff --git a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json index bcddabe1..0aeb0fd7 100644 --- a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json +++ b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json @@ -12,7 +12,7 @@ }, { "module_id": "FlexibilityIndicator", - "type": "flexibility_quantification.flexibility_indicator", + "type": "agentlib_flexquant.flexibility_indicator", "price_variable": "r_pel", "parameters": [ { @@ -35,12 +35,6 @@ "name": "prediction_horizon", "value": 48 } - ], - "inputs": [ - { - "name": "r_pel", - "alias": "r_pel" - } ] } ] @@ -62,7 +56,6 @@ "flex_cost_function": "sum([self.s_T * self.T_slack ** 2, -self.s_P * self.P_el])" } }, - "path_to_flex_files": "created_flex_files", "delete_files": false, "overwrite_files": true } \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json index b7b68870..f724e15c 100644 --- a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json +++ b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json @@ -8,14 +8,13 @@ }, { "module_id": "FlexibilityMarket", - "type": "flexibility_quantification.flexibility_market", - "time_step": 900, + "type": "agentlib_flexquant.flexibility_market", "market_specs": { "type": "single", "cooldown": 10, "minimum_average_flex": 0, "options": { - "start_time": 9000, + "offer_acceptance_time": 9000, "direction": "positive" } } diff --git a/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py b/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py index f3673d2e..feee5be3 100644 --- a/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py +++ b/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py @@ -1,18 +1,17 @@ import logging -from flexibility_quantification.generate_flex_agents import FlexAgentGenerator +from agentlib_flexquant.generate_flex_agents import FlexAgentGenerator from agentlib.utils.multi_agent_system import LocalMASAgency -from flexibility_quantification.utils.interactive import Dashboard, CustomBound +from agentlib_flexquant.utils.interactive import Dashboard, CustomBound from plot_results import plot_results # Set the log-level logging.basicConfig(level=logging.WARN) -until = 21600 +until = 7200 -ENV_CONFIG = {"rt": False, "factor": 0.01, "t_sample": 60} +ENV_CONFIG = {"rt": False, "factor": 0.01, "t_sample": 900} -def run_example(until=until): - results = [] +def run_example(until=until, with_dashboard=False): mpc_config = "mpc_and_sim/simple_model.json" sim_config = "mpc_and_sim/simple_sim.json" predictor_config = "predictor/predictor_config.json" @@ -32,19 +31,20 @@ def run_example(until=until): results = mas.get_results(cleanup=False) plot_results(results_data=results) # Alternative plotscript using matplotlib, - Dashboard( - flex_config="flex_configs/flexibility_agent_config.json", - simulator_agent_config="mpc_and_sim/simple_sim.json", - results=results - ).show( - custom_bounds=CustomBound( - for_variable="T", - lb_name="T_lower", - ub_name="T_upper" + if with_dashboard: + Dashboard( + flex_config="flex_configs/flexibility_agent_config.json", + simulator_agent_config="mpc_and_sim/simple_sim.json", + results=results + ).show( + custom_bounds=CustomBound( + for_variable="T", + lb_name="T_lower", + ub_name="T_upper" + ) ) - ) return results if __name__ == "__main__": - run_example(until) + run_example(until, with_dashboard=True) diff --git a/Examples/OneRoom_SimpleLinRegMPC/plot_results.py b/Examples/OneRoom_SimpleLinRegMPC/plot_results.py index f02d89f1..327a5052 100644 --- a/Examples/OneRoom_SimpleLinRegMPC/plot_results.py +++ b/Examples/OneRoom_SimpleLinRegMPC/plot_results.py @@ -2,7 +2,7 @@ import matplotlib.pyplot as plt import agentlib_mpc.utils.plotting.basic as mpcplot from agentlib_mpc.utils.analysis import mpc_at_time_step -from flexibility_quantification.data_structures.flex_results import Results +from agentlib_flexquant.data_structures.flex_results import Results def plot_results(results_data: dict = None): @@ -162,8 +162,8 @@ def plot_results(results_data: dict = None): # flexibility # get only the first prediction time of each time step - energy_flex_neg = res.df_indicator.xs("energyflex_neg", axis=1).droplevel(1).dropna() - energy_flex_pos = res.df_indicator.xs("energyflex_pos", axis=1).droplevel(1).dropna() + energy_flex_neg = res.df_indicator.xs("negative_energy_flex", axis=1).droplevel(1).dropna() + energy_flex_pos = res.df_indicator.xs("positive_energy_flex", axis=1).droplevel(1).dropna() fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=1) ax1 = axs[0] ax1.set_ylabel(r"$\epsilon$ in kWh") @@ -182,5 +182,4 @@ def plot_results(results_data: dict = None): for ax in axs: mpcplot.make_grid(ax) ax.set_xlim(0, 3600 * 6) - # - plt.show() + diff --git a/agentlib_flexquant/data_structures/flex_kpis.py b/agentlib_flexquant/data_structures/flex_kpis.py index 3ffa1337..670f138e 100644 --- a/agentlib_flexquant/data_structures/flex_kpis.py +++ b/agentlib_flexquant/data_structures/flex_kpis.py @@ -106,7 +106,7 @@ def integrate(self, time_unit: TimeConversionTypes = "seconds") -> float: if self.integration_method == LINEAR: # Linear integration: apply the trapezoidal rule, which assumes # the function changes linearly between sample points - return np.trapezoid(self.value.values, + return np.trapz(self.value.values, self.value.index) / TIME_CONVERSION[time_unit] if self.integration_method == CONSTANT: # Constant integration: use a step-wise constant approach by diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index 3168657a..7cc7616c 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -749,15 +749,13 @@ def _generate_flex_model_definition(self): opt_backend = self.orig_mpc_module_config.optimization_backend["model"]["type"] # Extract the config class of the casadi model to check cost functions - config_class = inspect.get_annotations(custom_injection(opt_backend))["config"] - # Get custom module fields provided by the user and add them + config_class = inspect.get_annotations(custom_injection(opt_backend))["config"] + # Get custom module fields provided by the user and add them model_fields = self.baseline_mpc_module_config.optimization_backend["model"] _ = model_fields.pop("type") - if self.orig_mpc_module_config.optimization_backend["type"] == "casadi_ml": - ml_model_sources = self.orig_mpc_module_config.optimization_backend["model"]["ml_model_sources"] - config_instance = config_class(**model_fields, ml_model_sources=ml_model_sources) - else: - config_instance = config_class(**model_fields) + + config_instance = config_class(**model_fields) + self.check_variables_in_casadi_config( config_instance, self.flex_config.shadow_mpc_config_generator_data.neg_flex.flex_cost_function + diff --git a/agentlib_flexquant/modules/flexibility_market.py b/agentlib_flexquant/modules/flexibility_market.py index 243619b0..d17416af 100644 --- a/agentlib_flexquant/modules/flexibility_market.py +++ b/agentlib_flexquant/modules/flexibility_market.py @@ -183,10 +183,10 @@ def random_flexibility_callback(self, inp: AgentVariable, name: str): self.config.market_specs.accepted_offer_sample_points) if flex_power_feedback_method == glbs.COLLOCATION: profile = profile.reindex( - self.get(glbs.TIME_GRID_INFO).value) + self.get(glbs.TIME_GRID_INFO).value['grid']) elif flex_power_feedback_method == glbs.CONSTANT: index_to_keep = ~np.isin( - profile.index, self.get(glbs.TIME_GRID_INFO).value) + profile.index, self.get(glbs.TIME_GRID_INFO).value['grid']) profile = profile.get(index_to_keep) helper_indices = [i - 1 for i in profile.index[1:]] new_index = sorted(set(profile.index.tolist() + @@ -242,11 +242,10 @@ def single_flexibility_callback(self, inp: AgentVariable, name: str): flex_power_feedback_method = ( self.config.market_specs.accepted_offer_sample_points) if flex_power_feedback_method == glbs.COLLOCATION: - profile = profile.reindex(self.get( - glbs.TIME_GRID_INFO).value) + profile = profile.reindex(self.get(glbs.TIME_GRID_INFO).value['grid']) elif flex_power_feedback_method == glbs.CONSTANT: index_to_keep = ~np.isin(profile.index, - self.get(glbs.TIME_GRID_INFO).value) + self.get(glbs.TIME_GRID_INFO).value['grid']) profile = profile.get(index_to_keep) helper_indices = [i - 1 for i in profile.index[1:]] new_index = sorted(set(profile.index.tolist() + diff --git a/agentlib_flexquant/utils/parsing.py b/agentlib_flexquant/utils/parsing.py index 4b0c9d80..847ad46d 100644 --- a/agentlib_flexquant/utils/parsing.py +++ b/agentlib_flexquant/utils/parsing.py @@ -1,6 +1,7 @@ import ast +import logging from string import Template - +from typing import Optional, Union from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable diff --git a/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json b/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json index 477a159d..36abe29a 100644 --- a/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json +++ b/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json @@ -72,7 +72,7 @@ { "name": "P_el", "alias": "P_el" - } + }, { "name": "E_out", "alias": "E_out" diff --git a/requirements.txt b/requirements.txt index 436075db..7a7e5c8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ pathlib astor==0.8.1 black pre-commit +numpy >=1.26.4 # Building the docs sphinx>=6.1.3 diff --git a/tests/test_oneRoom_SimpleLinRegMPC.py b/tests/test_oneRoom_SimpleLinRegMPC.py new file mode 100644 index 00000000..d8a564da --- /dev/null +++ b/tests/test_oneRoom_SimpleLinRegMPC.py @@ -0,0 +1,146 @@ +import pytest +import pandas as pd +import os +import sys +from pathlib import Path +import importlib.util +import json +from util import module_cleanup, round_floats_in_structure + +# Add the project root to the Python path to allow for absolute imports +# This helps in locating the agentlib_flexquant package if needed +root_path = Path(__file__).parent.parent +sys.path.insert(0, str(root_path)) + + +def create_dataframe_summary(df: pd.DataFrame, precision: int = 6) -> dict: + """Create a robust, compact summary of a DataFrame for snapshotting. + + This summary is designed to be insensitive to minor floating-point differences + while being highly sensitive to meaningful data changes. + + Args: + df: The pandas DataFrame to summarize. + precision: The number of decimal places to round float values to. + + Returns: + A dictionary containing the summary. + + """ + if df is None or df.empty: + return {"error": "DataFrame is empty or None"} + + # Get descriptive statistics and round them to handle float precision issues + summary_stats = df.describe().round(precision) + + # Convert the stats DataFrame to a dictionary. This may have tuple keys. + stats_dict_raw = summary_stats.to_dict() + + # Create a new dictionary, converting any tuple keys into strings. + # e.g., ('lower', 'P_el') becomes 'lower.P_el' + stats_dict_clean = { + ".".join(map(str, k)) if isinstance(k, tuple) else str(k): v + for k, v in stats_dict_raw.items() + } + + # Create the final summary object + summary = { + "shape": df.shape, + "columns": df.columns.tolist(), + "index_start": str(tuple(float(x) for x in df.index.min())), + "index_end": str(tuple(float(x) for x in df.index.max())), + "statistics": stats_dict_clean, + "head_5_rows": df.head(5).round(precision).to_dict(orient='split'), + "tail_5_rows": df.tail(5).round(precision).to_dict(orient='split'), + } + return summary + + +def assert_frame_matches_summary_snapshot(snapshot, df: pd.DataFrame, + snapshot_name: str): + """Assert that a DataFrame's summary matches a stored snapshot. + + This function creates a summary of the dataframe and uses pytest-snapshot + to compare it against a stored version. + + """ + # Create a summary of the dataframe + summary = create_dataframe_summary(df) + + # Round all numbers in the summary to handle cross-platform differences + rounded_summary = round_floats_in_structure(summary, precision=4) + + # Convert the summary dictionary to a formatted JSON string + summary_json = json.dumps(rounded_summary, indent=2, sort_keys=True) + + # Use snapshot.assert_match on the small, stable JSON string + snapshot.assert_match(summary_json, snapshot_name) + + +def run_example_from_path(example_path: Path): + """Dynamically import and run the 'run_example' function from a script + in the specified directory. + + This function robustly handles changing the working directory AND the + Python import path, ensuring the script can find both its local files + and its local modules. + + """ + run_script_path = example_path / 'main_one_room_flex.py' + if not run_script_path.is_file(): + raise FileNotFoundError( + f"Could not find the run script at {run_script_path}. " + "Please ensure it is named 'run.py' or adjust the test code." + ) + + # --- SETUP: Store original paths before changing them --- + original_cwd = Path.cwd() + original_sys_path = sys.path[:] # Create a copy of the sys.path list + + module_name = f"agentlib_flexquant.tests.examples.{example_path.name}" + + try: + # --- STEP 1: Change CWD for file access (e.g., config.json) --- + os.chdir(example_path) + + # --- STEP 2: Add example dir to sys.path for module imports --- + sys.path.insert(0, str(example_path)) + + # Dynamically import the run_example function from the script + spec = importlib.util.spec_from_file_location(module_name, run_script_path) + run_module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = run_module + spec.loader.exec_module(run_module) + + if not hasattr(run_module, 'run_example'): + raise AttributeError( + "The 'run.py' script must contain a 'run_example' function.") + + # Execute the function and get the results + results = run_module.run_example(until=3600) + return results + + finally: + # --- TEARDOWN: Always restore original paths to avoid side-effects --- + os.chdir(original_cwd) + sys.path[:] = original_sys_path # Restore the original sys.path + + +def test_oneroom_simple_mpc(snapshot, module_cleanup): + """Unit test for the oneroom_simpleMPC example using snapshot testing. + + This test runs the example via its own run script and compares the + full resulting dataframes against stored snapshots. + + """ + # Define the path to the example directory + example_path = root_path / 'examples' / 'OneRoom_SimpleLinRegMPC' + + # Run the example and get the results object + res = run_example_from_path(example_path) + + # Extract the full resulting dataframes as requested + df_neg_flex_res = res["NegFlexMPC"]["NegFlexMPC"] + df_pos_flex_res = res["PosFlexMPC"]["PosFlexMPC"] + df_baseline_res = res["Baseline"]["Baseline"] + df_indicator_res = res["FlexibilityIndicator"]["FlexibilityIndicator"] From 3f8c3775dea7e8b7dcd0391019361c0754113548 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Tue, 24 Feb 2026 20:24:20 +0100 Subject: [PATCH 18/25] move example --- .../flexibility_agent_config.json | 61 ------ .../flex_configs/flexibility_market.json | 26 --- .../main_one_room_flex.py | 50 ----- .../mpc_and_sim/Trainer/evaluation_T.png | Bin 65260 -> 0 bytes .../mpc_and_sim/Trainer/ml_model.json | 1 - .../mpc_and_sim/simple_model.json | 98 ---------- .../mpc_and_sim/simple_model.py | 118 ----------- .../mpc_and_sim/simple_model_sim.py | 125 ------------ .../mpc_and_sim/simple_sim.json | 30 --- .../OneRoom_SimpleLinRegMPC/plot_results.py | 185 ------------------ .../predictor/predictor_config.json | 26 --- .../predictor/simple_predictor.py | 53 ----- 12 files changed, 773 deletions(-) delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/evaluation_T.png delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/plot_results.py delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json delete mode 100644 Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py diff --git a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json deleted file mode 100644 index 0aeb0fd7..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "prep_time": 900, - "flex_event_duration": 7200, - "market_time": 900, - "indicator_config": { - "agent_config": { - "id": "FlexibilityIndicator", - "modules": [ - { - "module_id": "Ag1Com", - "type": "local_broadcast" - }, - { - "module_id": "FlexibilityIndicator", - "type": "agentlib_flexquant.flexibility_indicator", - "price_variable": "r_pel", - "parameters": [ - { - "name": "prep_time", - "value": 900 - }, - { - "name": "market_time", - "value": 900 - }, - { - "name": "flex_event_duration", - "value": 7200 - }, - { - "name": "time_step", - "value": 900 - }, - { - "name": "prediction_horizon", - "value": 48 - } - ] - } - ] - }, - "name_of_created_file": "indicator.json" - }, - "market_config": "flex_configs/flexibility_market.json", - "baseline_config_generator_data": { - "power_variable": "P_el", - "power_unit": "kW", - "profile_deviation_weight": 100 - }, - "shadow_mpc_config_generator_data": { - "weights": [{"name": "s_P", "value": 10}], - "pos_flex": { - "flex_cost_function": "sum([self.s_T * self.T_slack ** 2, self.s_P * self.P_el])" - }, - "neg_flex": { - "flex_cost_function": "sum([self.s_T * self.T_slack ** 2, -self.s_P * self.P_el])" - } - }, - "delete_files": false, - "overwrite_files": true -} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json b/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json deleted file mode 100644 index f724e15c..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "agent_config": { - "id": "FlexibilityMarket", - "modules": [ - { - "module_id": "Ag1Com", - "type": "local_broadcast" - }, - { - "module_id": "FlexibilityMarket", - "type": "agentlib_flexquant.flexibility_market", - "market_specs": { - "type": "single", - "cooldown": 10, - "minimum_average_flex": 0, - "options": { - "offer_acceptance_time": 9000, - "direction": "positive" - } - } - } - ] - - }, - "name_of_created_file": "market.json" - } \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py b/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py deleted file mode 100644 index feee5be3..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -from agentlib_flexquant.generate_flex_agents import FlexAgentGenerator -from agentlib.utils.multi_agent_system import LocalMASAgency -from agentlib_flexquant.utils.interactive import Dashboard, CustomBound -from plot_results import plot_results - -# Set the log-level -logging.basicConfig(level=logging.WARN) -until = 7200 - -ENV_CONFIG = {"rt": False, "factor": 0.01, "t_sample": 900} - - -def run_example(until=until, with_dashboard=False): - mpc_config = "mpc_and_sim/simple_model.json" - sim_config = "mpc_and_sim/simple_sim.json" - predictor_config = "predictor/predictor_config.json" - flex_config = "flex_configs/flexibility_agent_config.json" - agent_configs = [sim_config, predictor_config] - - config_list = FlexAgentGenerator( - flex_config=flex_config, mpc_agent_config=mpc_config - ).generate_flex_agents() - agent_configs.extend(config_list) - - mas = LocalMASAgency( - agent_configs=agent_configs, env=ENV_CONFIG, variable_logging=False - ) - - mas.run(until=until) - results = mas.get_results(cleanup=False) - - plot_results(results_data=results) # Alternative plotscript using matplotlib, - if with_dashboard: - Dashboard( - flex_config="flex_configs/flexibility_agent_config.json", - simulator_agent_config="mpc_and_sim/simple_sim.json", - results=results - ).show( - custom_bounds=CustomBound( - for_variable="T", - lb_name="T_lower", - ub_name="T_upper" - ) - ) - return results - - -if __name__ == "__main__": - run_example(until, with_dashboard=True) diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/evaluation_T.png b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/evaluation_T.png deleted file mode 100644 index 97f99021826f68ef53399ba5f6622d048ce11809..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65260 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

bR)kM0R!If9MTs8Go8f<#tU zb_}#Gcmuj0>7i{EYI_Pv{1Z|8?c1~P3E*HD85nLuTP*woS~t$at_3nld}t!Q11dH& zw7_>^pWjso|8UP5J#JLQg{E<)7wG0ODd@QqhflN`=ia+_j|4Uul1Sp)LGN+M$B#^z z9w5Up-nylV&rl1eAJjp!;1G3^EDWv7(H4~dLqL-k$V)Mbwd*oJg)NJdGisxOZ! z%g7zrF13T6Pobw5H!!rd34MRf9`2jns%%;=Tir?Wbi3tt1?UM2(1s5kpR+L_@W9y< zd1g=S=KfsoOng8Y?!agM=N>9K=5GmJ7IuCJmiY!cdf$Xxk!fBQMY5rz2Ob%A6MPJK z`}X5L774U5kxJsDXJMe0;0RLJPY56 z41W6&3k`M^MV1CuGhmH9b-b0Gt80QK!M@xlSH|m^HPca$j!#5Hks#d=P%1NY1qsl! zE?0g_eff%z^Bfa4Hg;`tCy&9O87es2;7+E_tw4nYLr7M)Mi5{#ujE3d%lANS^tfR5+tw(iDGu8&)(-bwDKS-uR z$K1=4=2sm`G(0rQ-SdAAW#Hl#n+rB>LXc_6Oaxbd76!7l-9#hlZa+-d9oS?>QbR3* zcJ$FquySyaOD8DcGu%;E{{of=NBZD1ynsEp_IlOMzbIa0RYL5%oN@6iZhY_j<&3bf zFy$iSY&DvUi|?e-pqp8`WBHw>pOow2ZgI`i8-iMFa6#q|aQGT9O`$#{b9*sRSp6)C zUTji--{LI~dMO2R2Nv%x7#Zwbzras2<#^?G?WYs6i)E4 zw{5lL51g2ID){(IbTmg0ojB)7xFw|I41j35+Yh@j@4^`rFqKgc2>J3;}t!RRD;LL5mx&Tra)Z%KRUCvSZ`42GQ72}?xW`7k- z#UTAIh}7jB)%88%3eDO9UiAgqKQfj@i3xwj^MV^cyB6LnIhM=TpO6SyKbo)6Qe;OT zsSTox{g99Lw*jA|zuX4C0z3yQAGrob7^r*cQ}ZG zF4OBSjkwdstKF!gq`AfGn^oR+rUTq|H%cP!y3A7e3vu){ry|6Cqfr*4TL##cSrbmG{7Bw zeSPQ|;KY>e&851YY|epeOF@CcmiPLfqrA`~B%qDBw2TOWl)MAZW4FIZF8;r9_10lo zd|$ZtLzfatODZ7JDyVdWgp_n6f;5Pfbc3`=2!gb9mvl)Z(kLM!prj(*yledWJLg=l z{}>m@Gc$YkUU9F_T?vnQ0sex6IswfD<;_Qd{m#z|nmvtRG=aq6B1r2&-Yzb0#m0o{ z@`pLu)neyrp>$GmH*p5B3|}qn%Lu;e_F))tQJ-DVEjhquhndS4wHW201SzpJm{;4oGfrg?5mOmrYUOS<1lc8WLG$z<88%4A>-O7Zjfv6z5UCQ<6`gq*m?foO<2oayAQx?i*f!%vvq+b5sgcZ zQ9J|vf$D;$DaltZ`EC#C6}M6&BF{gaS2^?#0NZy$6Vl`k^efc8Z=?0?v+D(p zA!3Y}P%rc=FuOPCiu?3^LPFYVCdIJXQuy{70e{Lj ze|^p|$4I96J#}?GZ0S|Jd`^2H-sOTB<Y%3?SZ3v|s6-&*~sON%}C zlA0hi1|SxY%n)+Jf0vWjTyCKdbN0t)bBocq1yCb98=*w>&9r{at5(sL{a>d@TBvf& z6>>HJ1^3jW#TyBULEgFKyySC-9=hL$gb{oHwbA>7ma;PKgN@|gO1?{|Ob_sS7b%?f zt7)mIl<-&-TE_i4Cbkk*2m6?_#5QwTCCyJ>lrUk}l{_@#|M~P+k}Si82|?cFJv-S# z`w8(c4DY8*u_M#MQhYw51)`YnE_QWKxTPkj(N^i52eYyV_>8Hro?uuB{iq%T$>1SH zY#&H*z9G{c1sa9OG(gBVO&E80&ZnydY8YgqGI5ewP6Cw7FkK(E{;L_0O*m!soZE;P z&g6GSC*!tWNco&V*sBoK87-@6Ndx6O5b9s@xO~Z&hjQjD>IpZ4c0iQr7;ev8K+*Lx z36-C3NFabB^*$q%z1Xh@jYL%0g2kPG71&J*VbpuVTc2?l8wI6NmyGYl>Sx$0?o*+P zsPt_m>U%!Dn8{!FNLZGC6tgCv;m#vY=z!)6&E7OmV((KNEf2->WfPU^3n7e4#ElJ3 zA+8?#vRiD|`V@PPS}b|!D&Gkkop2F;Y&jsrH)@f0pD<`^dy7Q^T3y%xWe`a*UG?OO z6Q2(G$qAc}vaK_%(5Z9;q(RH>naWBZyh|-YCS7PgmTcWaUP&lu<&{`)m>=9A+q4@Q zIBG1%N*G*uZ;Z}`iT>`6?uELz7aBTc3`1HQOvJe^)1O8e0%exEsr9AQOL=;nQ`JIh zpLvSMrdy4_>soNHUBmtDLTfEVh>D||;>LN8YIS14sGrrF7j<(hj_~1RjgO}wY;u@K zNAOtsjcz3JIhT;(O_&=Tf;|WZ;o4y=AeIRYg$%Vpp?n^@NfZ-ykYNp|vIc#fu5jpE z+Y;54ad8|7baio3;l4qG-}iHI@%>xL*TWsz(u$UJ5+U6^e(zi-MI%yT`!_0*b)xUr z7p6jyjSR`u0S1BOBxHt_hY%gI&m;{RoMs-iHkeWb3jIOn@;qf`3PQh_D~jgKiCJg$ zN@C&RJYVm$*Ui1PHUzCkFM0xHPlVi_ z)2}0LM}>+0wxZ~_-j{HmZ80)27*C6Y(h^Cm4r@+*@^zN01<6phsfKYl#Z-aurCpG}wzOOC3in{4#P^k?hMF#y z?%vYh`ZYr99q`-VWi$1Y+ok+jptSG-Xv)k|;W1cfJIqV4!Y) zum2sJ&)v3Fm4m~q4F&sef~dOfAALd7&pxGm7`dEuPvucrLd<(7S*ZZdfH`Sjg-hsP z%NUqG1eS@Tn;2lW*SBCs7CfTD(WQpSR^!`#Owd>Qm@giT8WlCeK8PCwwJrW6fd3zqla3x4A17 z!+z(^9z+rKbaJuk3+Q{h$XL`;H!Sfk5RK|)d<4DYAHXr_cWNN@h;gE&HFXWm+CGu( z{x7MRwmQa9SPRUQajON~O8GJiF~Jrd^E5IB10B&HsrmJl`jwrB0aawx0o6Mg45foX z$i}TccE~zk3%YV4`7bir2nkOo90_Gs<7^yyHHa<{0eePs0Mmrz%dpf=?Ctj`lmx?e zhN|NbzHi4<8C7$M-e70cx1rK|w=vGAn{h0g#Z~TDgwD(eMgP!Zi&rUB-=rYdj=&~m z^;EUdWH7=P4s(&vsr_D5t6WAyvr=D z7QByOvJ<#DmO*zQ=WYk0QqSLB%(%7tD{_!__*I>#SK+Przy5VQWg=&*Y9fI%CP!Hb zGthTM5!=@hjiX^9LZ$_E^6hdMDYvL~n_D;)y0501M5zk8`!xrVh7m7hDrL|6l*j9c z7yj5+>gs(&LGzI3qBNa~L$e_xCGh(HP% z0kuM8auw(b2z7H4Ua(Bq$l{6quGH>gk&__(=sp)5alO+UO>*HHI3gV{s!}at4Fl2M zY-QZRtF5kqeRyjPPXCH147@@w>iMw}$ueBAE5uwlRq~{ULmoF4&m7y_C_pwKlO--D zhLR5pN0V#ktljC1kfnFVwL=qoIfxtXu#M(JS}kW>mCr2^GTvwo05OdyXdMtW z!s4;Bew>q=tMz;UPEvTl1GaAfBEs%IYv{a(kV_ZCIB&0MF9?gZE=`VZJzTPGDYz6f zGKHS!=O1O&N*c=eh6~-HsM^ek)DYJXWZYu7uP;)Q;sol2cPQYuk~^^Z;Cx`CyyeF8 z7N#1z2W~B3Sx9v@unrm~gP?-z$x~+w>d;Sy^cP2Cg+d~L`CBjWuP&X*_Gi%|V}mR7 z0S?Z*#AJW>dZAXZ}aWMY`#*ymTjfc8gQAIlr%6ioFapr zGk!XLO1K8A%1XlY=Cz?ooY`wQKsU7{T*E2qrc{N% z4_v;6ng|M0T}aoW{{uD1S62SYSE#QrZclPFa`FW(&JjY7)l?jiB!ca`GlJ2s*vut>-krVD% ze2aC~kfq6pB^s*pi4q3ts^G~AAa)ClVfvX~@MKW7)QJik zW3T2F>r$%wThd_}llqqbzQf z*o332Dd+jnH%DeS4QM+kzRK|twlDQ)5_)R$^(>hb=w9R6CvJ}; z_u!sa|L@8jZ-CUW=hT!>^O(22*Y-GFh=*1>7CmRRpGv0=`&x`wzK90rSPwcX2h$DD z4}}-QVP3_zLj3GTC>f7ugk%gG#Mg%h*}@<#LVo5<`E2J+5JfIRn3of>4~4#vWSB-$ zul?60a-v1z#{K!0j9D_GEd0;Um9L~&|B7TmBl(={cH4!XJtler>!^U8_P2!ERgq15 zKmDbg8uzSYLb?%FGK(QrQeVC+3Min!_+$EQo;;(BPy7`VkEM;F?2QqkiOcil?KDm% z>8!`M-+tcrjm~+J2?^V6*_rbmO#HEF<5vFcZvdZV|6glTJw(IhddJqJbP^u3Pbmh< zhL$F94ExZRT)QqGOHC;U0~KXXCLWix+3bCLr}qVMYJ+LDGOk)Qt!&a4|Ctj4o;RE- zhX<0jD&nO@hAs(S<*p_-F{CL>Zc-2BtMZsPZ;PBrO0w_trrxFKlyw!LRc=iViib95 z$bOp+!)n7Ry|I0lJJcE(eDP*)J|2}m6lQ|CRHA>KE@y2s*IOJuvm&U3@pZ>{nl4Hh zJFE&)Wj!I74A!r(xLgt7+NtE9w%yEuP}krPsGc85LRXv&Hv+GlG_$sytJ_p$S7<>nm?TsS|-VD%c^dju|NO2K3D-gFlYqZco};?l2m}Jap`u z=Z7S3?%(TB(9wtE#ejK>o|^dycYI69doz?~c3OAFiNtF{c^*j+%gj@LD_{$fZ!N8+ zV)Rl>MAvfi+OcmDZeo$B&pNwHM{U?~(7RvaF|pv3}zocATL1GG3&Fe{^ps0X|H10We zF7mO7M*KcijQ@EZm|Nlh?KYVL10u_>KsOL#4Vlm~Ggk99o^+TI!F|iRo?f|pC&kFl z3!)|fE+8}XWy*Mx75sYlNT0GfJODbM3bL#!=@Wfk>R<79EbmZyaFIR3p@~K%e9DG5 zz0IttXqA=z1Z2z%&-o`7VFKvNm4C}=SL!h9#gxBiBcCl>#jec$Dd{D2MQoIqUM1a4 z*g@{m>|r>N@OV;O$B7ky8952D{#laNE$d3|OGN72h5fWzR7Q~bEI_1y1g?2>P|7;c z-Rl1Ojj${(p0QOe_>vr6Q#dLSv$cDY0@0pCE2e~wyjRXpRtj{@)kDMMp^*!&WB8Qu z+5@x@FvE_Kid8j3i&iD7r0uQm>Z%A`uL910LN5aoa$CGD(Kkjp3m*S(1psCihT;=+ zx#jUG+3Ie8w`C(R)UT89{`H$_E#LE|Yg~NqC&@IQN_oEG_1lGg(R$5>&envaT8B!Qb+}l&hyUZJAj8;Q;@Zj`uDGMGGgMR+e~jNvZ#r-j3hvG~mo1dQ!& zW1&aU`?F-W$oerKuNO`YG(KwLfU}qUf2*BE4?D3qA^yQ(x@J)3I&3a&{zT<>8PUkS zZg@VxUM7t+j%9ki&4vDc*0eN~xs4&@+UJKC~?>e_-vB6CB8 z|L^;6b+MI+CgURV4b_YXhl;Fe`O3yCFBCz#=G#7fMOyBlk-qa^1K!X1a*@Eo4rm2$=Rnl|1zKZ}G`b9$}b9O#1y#Y=jWBL7b44Db4j zb7cywN8bGk1DLw!8ho6b7ZFOSFg3s&!PnN7Abp|Jf_ZD8KTn_f%Q1fq>@ZhLz?eU#3I&V_!EE4m|I`?qZ zJe5b~*q^s@h~(cVsIS4R^a~d;Q2GO(Lh=>S zDh0|UeExO{#cF=aV#l|mbK@T93=G}SP&qi7PtZGh)Sj)~ShdAa!%QP-|B&2p)GkQc zBV4lVqVH%cg~kIj*Dg36jsoDeDZEoo1KG>>@81h({z4i~kRjTV@oL!4UV7AsNrMm^ zMh(%?MP}Wv!C5?D=zJ}97-j}Ur5KO`%k1Ywn3$OX_k-qj?4Gcd217iM>Y$-?YwaQd8L%MF zr9lo)_w(n^g|`qmyTOklCCxucqbOg4q`MiiI#-j!IP$o$N{z7{Qy%LdcBrpeZJG%5oD6SFOcOs$CR*t+adQnrwf_#Dzh99ezF6{A>rn8H+6L=+^OhH+!7cEz$XD*ZTt?*%VZ{3h?>9S?CIuCTzxEu;z*pbaHW&Lxj{ut z?kcGfF2k51ZbA9KCV0lWgoslnX^sRQp07Q@?W4Q57et2l9f_?W?3yTI3`!F{!6uB4 z?x`avi)%5xHckk!5r07cUJ4wn*WqAYSA5Nb21R!cCjnXnz>hF#p(JW|3@W}l;|7V0 zR*(Xebg2QT!Om?@N4e!7@KN+D0UcBU?Kd@f-$dfgEsa(too{BhI&5MX(3aoDZu$_k z`1@GoNz#YcVE!&j4MeX7IpI(XmPCk7bW1$ZBb_SdEjkI<8YNn#$20jDnWPoe>2`u)Wp0bI4$~!ice-TT_xEnHB1a%gK zG4dGh3%px~7&P@9EMAIfJvkq$#|)m2CE z(8GAZmjK}<&hG;6Y#z$zU|zuA43%#7JEF+>l0Le!87Y8?gMoU*)W)G<$ky83O;I~H zB3tqz=f!d)2zg}N66rzi_0_#`bI*d;{m6{JEDq|mJTi8Z=8l>P>^Enh^mrjPobv~V ztY@aho(Axg`nVP)Eff|fZDbcI9bh)9Km@z4$CTWp?Q6vD{eER>x6KFd`A(Erg${x$ znyGk;Q?9-f>(!X zzQ0m#2g2b2Bp*Vml$MdfMxG>q;4%Q#Q{7C#h}jiI%Z~FNF|2@On-F+#fq>HW^b|ty z${@^H0VtzF@h3p&Ek=FK?p*CK>S;@d@<962moLqXd|hKIHNd^J#9fN>haPwZL$iL)1fCecSa+Azpj zoSwReoNk8Fz=rV=LV3Drw;i05xAml`vb(0jVLMpwx8J>(bWzpnf!7GJ^$^4L16W}r zu=d}P1yHzhPgxXSBW4@~*JL7)@j8*?_1j24qq;d7 zRxrZ!MEL?)4erOq&FzMhZlt|&)BD5?K)c_+wFOj*z+!kQ2*nXb`n5`|*mWT440`U+ zHGgUD*VXo(79n-ZH;9_%W+MK?bwHU)r}yIWQU8Y1b%Mgit}Pv%nxA5wt#1mmf(G&5 z+%|=`GH=ho$MW7{`hbm8(id%ZQ|7lXT;06WXSUaR5rnMoINWcO@_XFc);{Wg-~dRV z2V6_5nQ#A(LBZc2@>C~$zMKLE#=8D-E?~^Ap(gwfU|~GNb@+l1!Ju~1a&;S;EMK&% zn0p{afhvduOwE;9D6r9Bah%V{{s_zVZq(IO5=68HfsmAo zRRlx?B+F~BEzNwcOX+Doe8V^xjSd>5f1;=u6e%gW1UFOeGX>&J?EfX=VhrheKET2M z+>H8GRx+i2ru1SdZJNg#9_R9^F%<3N4L6DI(43N^jIlTM##ohPpNwhz?%|N zDjdE3ebjQe4m5E6P%}&8Zn9Pl(p97Gu-83MLfN@bM@zfpU z7{=WUPwEW9MNO~DF$WUl9{|TN zi|C{@N+tkpsqffSMzcnGX??qZ)vHR=PzaQrHjvtW-{jk8TT_SBSalMoZx(_Co5k|; zF-6#X?#FKXsP3wlHxm=owp_^iLVd#j)&rOO!s(@qvyWE1cD8kEB@IKF(gZN6j19sC z@&Cq&Q7Cw0*|kbAQJ9#R-~4cBDi|bOpvSr#GIpJ{RkG6dP0B8zI@i&Cu3(+1zcfAxK9pRY&Yx&h<_RGe zv_bi(f5{pdgVi8JfOo1Jk60$1*~CVPm8DN9rj<*Am~QZtd0p_-pNT!zwIrt`49fPE z*5_LN+9=*&{XR1ckSM|6(u4CC!Gj5?4_?&1ELk*kxaWp9K{L<|{e|C-Comz-US`Fv zH$jDZ{6y=7Vs{I;6h5635-_w}3KM<{hgmIcixiR2>u}J!Oomj%RJ00JMEqIN$o;nY z`RO7{%U{p4bFjwwKfSf73AKy6adNG`1HYDM>qrc5e2EwWuuO{dlBMupk1A2h#rh~d zJe$a4cj2eSiSue@Mwb;=kI~QW8xU6JT#ytH_9eeKWwd6-j~JU^6VXLPUb*`nLJ@>Z z2XN*x%TX2-RA4dfnszlqM1WHY`{EJVejYhI$i4Ok&&|A6 z1w+@bUlpVLW7rFP5R>Y<8*h`|lXS+SyDN-W#x(^|`H|md67_Lv*uC?=nqcT1+$27% zXbwTAx-$y&bgCVj>Q%e)w@7Zf3w5~qckb1p5~bv-Fx_GIAWg(za6q@>F&_9cT?p^I zYQ$bzRHW!#G;U=O)d1KjCMKq_Eh}yUAP|%qHpRSt9SV9Lgj3G-yX*k;8h-}Cv_hQ| zMmJao2U@MtWbQ34;WSC$;i5F|1(%Ydv*Aj;mR}TPlK35jdiW$z=rptArkL`o&vXCX zDr938g_KX@5wQG_fDX`8fK+(_HD{DBfc=rN{0y(Z{Sm-}0_V;%h}s-jbS1p~g?Hfe zeu#kg0lNvHXMccvsprwVfCgkBsl#afMQG2^Sw%B2D=H&BDWww#Ho-*=kOW zW5V;0$@tgcW`X#1A$>vE6fseNal1FxRIxIgg@cHEffis2XEw;AyFlkORAZ|LxNSr* zeE=N;2e7(9U|gS}|3apB3@7VfTr!+FpR27Y5N|ak+CgCZ|Dv7kwDa_;DpH7lK7jVC z^s_FU)W00bhbqX-aTw^=wm~<0GI#qEvu zmZYu7L#hnx6`2qIjXp3`Gt_XStg*Eqz215ToAXu36ojBV;)I|l$PAyE0Ui;#1f)*{ zELj^M;m+4X90!{VsA8O@1M~rhiU7og_h$k?1@s-31UPyHOI`u$9J4|K;(&r=^RvdF z{*Nub0)l@Ov^mxZ$|>?m55B2it$1|3-KNM>-yRq9c_A8Vg9BS}d7PH>tv3lO#XRK8 zXLKDxDubnoOUs2NnimnEas3%YUyFY5c4UExg5$+vkYOU=cdltyaHoTpsS7B1XUE%r^PfKzEnZM7*fQny?k*TU1T*y`+lN4Ejf2;!NLE#k; zb9B4~@NV6lW{?f~w;3ZVA7W{bAkF(u&VP=RLznxWku|b$L*+YUJr8E%k$ttt(;_aV zZZ}n#buSO2khcZTPz8@+EHhy7XY{X^7)Y0^2#7CxkM+;GU(rctAXCTFe40<7s3xWE zI?kk|W^h-XMGImVgjE8VK`exT1=Jl#!FY^YQ9>u19hCPRzEv9{R*WyZQ^r3!CfqRd znfY{92c><*q?IAbcE)(4Any{~APRhvur8L1hU0e9zC04+JT#nXp6I9;NH~T>I#RG6 znwL!TP5pQ21yNU6dYr$tqD9aOl7oJgurxO}_Zr|qfYQhZ?Ibx*0DX}C3WU0dlvDBh zek(_~@=xRg6(rEM0G*h0U$2ce^;}@MJH}4l$v%CKe7f{QGCIzQYk3@_OE261tMkfu zf8|(nWaQRD$|Y9|@}V;w548Y<3j*d>CdTH;ZcVn-IK|Kx9}~U_b_H3iQ?Hz^H(Rnfl>?AfT}>Z*A?HfH znq4TvWaTFj$aj#9Md)zx)mAm)J^{FfmOL6dVWX*kwzep9SsVXkXCJP-xCqY zv>k`~$+as!Z8-`AXScQ}|C~>eLwn8EBK(N{AzoMF4^1}%7^n(AC5R0KwUN3-?!WwI z_s7qHQm06`(NcWQvp97KhZ0!`s3E}hhoBN4fC@$AaPjgCprOFw&<$9?<&H~zg-z=4 zk*}gAT2;P=oCgCTAW-~w)q~!Ni*V@Rd{u7rr}!7satQ{Wf1Sgk5cMHq-=l4plfw$? z!s*0kmRCrPHvJ@!C;LLb#zJGWMo!q2O@jXW~|O zcrW`$c2-i4tUET3$tye&a^XHwk%?gJ%S9mssiWEhxI4Qqe@EnzkSFrB0@dTbA)C1w z{BYG&C%7R%C|c?U{GiJhrC6p5bV8JH2C?3_(FQ{Ap%$O>!ZHcu*}Z(v&Sbw(bgw~c z!Mu!=yfu^4kM0uE+>(m=;?08<1 zCY6*VlZ%gU@Wr5TUvV>u8j-T^@W@>nh~TkBnPN zww&K=>F7d(ZDFUyDE_x+p3M+3&?zad)C7FJ%5|4lU%)X>Mbom0I-ClvJ)*{3m}U2O z2)S}gC=!FM58&AaAmkjW-N z((}nMdF}83ci!RS<6GjTE&6#iaD192uKEF)mtTJrEYK$96%^`nued4KcnhQ|5mLWV zWP!+DtZ*D11$@Dfsw&<(BZQV)nFvW72yuF>w|9Q#gF^uVAgf0H!BvYBMx)?s8h^o8 zrwBf2DsF(a?i1zmp@2$VW<=Iw`zjfq^jrQToE7}!@_uih&xxmj& z-y9M_wX%nOJs6(ByuM?_@#TQ%(fO%x*t3e4$W-p^-3#4eRR@FDN$roTTWN+-rTWCL zE01};D=6NJ*MeDs%9p9H!e`pUc?{|=XF}BQV)i!+b7$w4kLv@LYhmGGC=^@uB^HZ} zEwp4_+lV(NA3ki${(1DrGJ^Y~w0XN-cJr}7*tvF4_)2csd9MA_vxWVV{!jnU7{U8o z>xS#obER7G@1sC_sf!{;eE`-dn{CU}1(~{2czT7rT8@qtCJXa@__DgXxqD!o{jDdgMa&Q%GoaE;1Ej5Cm=H;yhVp5HsiQfjOdVu%O74R0TUxcU$3Aczd`hz$Nwbzm-p4@_NovUoF;>dlg=^hFwj7# zI}2>Q_!_Vtm|NNd+Wh1A9~EAq2i z>VYQ>MPzt{P`l}$h?iC@k4Nq5%xI;TU_VK}-ae%q= z>U_|3>Gmc`>A~aD@b;=#k2-o-pFTDYM8*sJ=C`qAPPD!?-}}?raZVPp|BGy@Q6JHT ze}C&?PH-WOyYbhl+RpnY??UN>)FY^FN+@38m$-1{j{b!@)4ab6^hM1m)A( zzkh!|>2TAnht`q7dMLf^(LpH5Uje)MfcDe#Q=(8=`c%fQXdjh`_a519Ya6veur7}I z$=>!~(t9GxGIBkVl>c`NmlD(U7uj0o;jkE`Mqnr+x|#OF7oQfjpQ13|;1T_Q2bD_p zh5wJHJkX##WGx(`V3{m5V@@`Ec8TAJS!^h1w&_uShm=xXzy5JUJe}Y~Vc_Sd0+?6v zh=6jc09HU73S3pen5IVF>%}UYx6x%)uNat>)W@VlA)O=`{fB4>O5KW2iezydne7|b z=BUiV)L4VC+Q_dl1z|#f!i>)x7-JmQXfk4*LHZ8=nPwLzH)T3DgZM^HYiOR{KmFK* zTZ`{ctSxAngj!{?JdItgFCqBk0bY$992dY%0)_;TGq(J&@7D(;LfK8^9D3MjQrxjt zlexJR-tlHIE$F`}fstDjR~j!4#ejw-A7A~l4YQSej4eoZX+9{BIJ`}lXxDrSZc zWH7moArA#v)O+(Jsli+CZK!l%bozb}xi*UsF36Y+X_r6OC8KfuRrmn-#f{GV za%U|>z34lAn0huK!XaKR?E0=dID`E3dR2W0j{x#&0{bhl$y-iF%wnwn^Vq~eMbPkH zf`*osWf0th$=WU1^FGni@j6A_B9Rj;NXUj+vIzh+;VZLr-AHtM*yF(bm@8$Bx7yqS zZNgWR4_zD7y;kw~XYKys^1>YW2+IX6&nq7Hk97p>-0zEWbCUSF>&+W3+(kZ1LGzkO zk27!y5ua-SnRfwOcoPyw1QO|Bfmnt zZ4LZtrsF1kCEA>%spPY+7X2Bz$E^trl%xD03e~mmjg3M&Jf(q;7ZW}W6$G0JnM{j(X{6O&bxevM6RC!4c(iZq#8jkHy4c*KbE`%JCk@Odpr@q;ALP)e1R zaI0f})|$mEalh7j5@m@j(OF!`aI|36{I#%xVh_9^A3y*O1IFL+HxNShK$_?|bx{mb z+$dOmDtVm#`4=`z$lRU1!Z$`QGq?Y#GPck-FPMF09aSJ^u@);Nb&o^$#X;kWR6`T3 zqzY4bAQ*&g`a|${hbZrumuP%i%1!fE9%RvfKf&)x ziO3R)J-W__dp$lRIY3I@-oV;c@6xs0p;{Mghb=&Nj15B;1gQo&IWf||l6uhK)PxrdqU*tf|qGOAKt&MY;O+|76(w_m1n+5qWfzhYr)tDD% zb0X}uB&gQ^G!0*_cz1J{Ncs9GEPKYWK2X&caLJR!8`OGkPmV}a;$w*Mg$4Kscb5-L zY>iD2{AOk$qIN=ot1T=yuF{a8px$Wkg!xs&3F*(mdQ_mKK23F{&&9v0W($FPxcn}6 zIc%tgsOIv)r)#fqmDKjLf7*H5Y zlyO$A+D)eiM+XcHthhmHJcML-B}*&xPa1HBR_Bx66YukL2?^5XXf!G?!6zXZ_xdYo z88_LhF8@1Zk*+L!0W7u{zKs^U1{p_s(LdoU?qg4T;e2wSIdgZH05xI5Ib0{$tidO? z`Z9}^U@hWN013L7SOy-t??Dbq+KOJAsddsQU~cCHVK<(_lCI8sb<~;TM-FDplZ?W> z;|0zE+|9jUwG7`*3$a|85`xa3z$;j4pw--MLaHH|)=&b@)J2Bn5a&~5Cx?_8Fta3J zdjrDzZ-~Q}jSUC574)CMYlj#_08(2E$yCzQQBeN`_OUqP=yvTIc&@2{(Y(Yj)51=( zN}DgYKx|yp{+T0GSzYwYWS3)4W_|X-fF)|;NO>>lJCq)2j_wis_&oPg#lFH^Qyle^ z3<8Q>rbcSate!=pjqxK)BH3omSDUzUV}=GIen8{MLQ8Mt&3dB z8NYpbMw9hR+2GW2`we|Jb81ftFFi&|jqwI4^uakFI`O|%pEn@HD8Q1yMY!LHzZICR z|8U@hraR)z2~E9n<2H1J?F=Slra?E6X=~`T!dUjldFNNXfiO1+uxbX7k^mQs5kpgI z66kP@&%_OLHW1Dd>um$VozKIP?L>GuFGaSW9x4T*RMgcm1BNXq8tYRGasnuvc+kFl zsV+}tQ^hA}WB}kAP|so_WYPyPAH2ru%RmovJXdn~s_h?3f9B-&5#~<-iBL)$Q7|}5 zSa_KdqHlOqi2O!(6ZqQmU>=HT+=O)svqNDpMw|&};XR)-A+Sga0i$(%2$S1rU1$6N z`_Dgii0%tp8nk`DL74(dNRr{__P?7uvI@3DqkMiT_GQCp^0jpTxb&<3YM7Y)71rW9 z$GV4jh1!~~s#*qZ2F{Z;ne0UugQ`n18CG@0!~E5;F5!;g;l-dcI>+`79G;Rme$3wQ zI^Gp}o|5G8bLLsBLSOf@=3KqILBYYR(C!F}j-CXA(w$CX@b0!c__k}YMjIz*mAZDUA1woHE8s!gYo*= zR$|LCpo?pwe}gdt;%mY*3&7-La8OHHi@0)E32}xR867<}s@#}e@FDk!m zGm)5`N4<-3sfW>2jzpw1B0D8?&rvWXN4K;kDUB;6hbK*$Uu2GZvYMlpa!Fh1m``Ek zg<-AtG?zLfp+d>$+l9N;>^ipd|4kIw#9ox(QlFl+FPcca@`_o`$X9(W}}C=5*_Pt`7N1@Iuv7 z>~#cl5DpG|_u8xOwfKE)VUGBok?qu+fa2soJ^7wk>>svR$4FV**UGcXY5H&Is1;q8 z)shamVfBrng@LV1kLY>SI2OA%SAQ(K%i~DN-YxW;E&u#NoH=$m`BTgK3ZdivI+xQR z)oQ#hxc(tt+&bOz#uEvzdXttELWLIotT+A`YkRUo@0w&!-r9e4&Gx??O%L^ehn}8| zOe^4)0R}}-F<1q^x>wU16R_3rJ(3!xZv=zE%1=o!r0)&>TI@VLWhMd_s~C0-i1K&1 z{2NAVE%eFyhx^_IQBIOzkhfk0B~^UA4yM+f{a`LWH+xel5syAaH8MyXwq`>kycLCcT&RY zAC~`A(Wi4(ouWFt_bZ5upIvuKC)4~S^q~No*Tks&si(@4x_}2^2!^^Fc7;1CjTTz4!JNqzWQ%x-+^qVwNtQZzr7n zol5Qbn~DAk4l1gk3p7Q``J?K@k5|$ZeyLe0#dz2>p61v+>eYFCOXlsV2brduRP?Ek zEXe{qw2jNrsZ>%9m?)GlIkI^0of1;Z$tYE7H(6PS@~N~Y=1SDw?9oja{j{%2YdMhi z=ejZ_X^P`gQftjy)qY9P9k~Y@6QgYTJL^Tu%NAZoE4fqPfFN@44TICDSs%`4EpWfv z`!!n#gnetU3`WeacVS1V&f_7@EB>A&(24e7#C9^Ffup!_7PoTm_rtx4(keH7y_~%r z?q)iY8O;T9dX3fX$+&65-k|}H4?eRQdI>*5s%qEFqz@Wz5r)u|5Svj5Xnb?&*Yo60 z({u4$a%R^q7!KuZZ9QcMJSO;smkZg%j8yNNIzEdV zV^NRwiEQA)5i*+!fB#d2^D~cvpi8OxPLyKKxv^#RQtZ>Jhe}crv=(N1N-eY(HZp%7 z(#gH0_9XA4xb$63eVTQ5ZB*~_)`%IiO~Kx;z;O+uela-ve18NY4-enT>d!R_E9rJd zep(nbc+UN&}a8@@9uQ8-J#b1EKB6|C&@9vr?^b@rxxX6 z9ASAyqR$3JOgy2j{A#RzdPJU)M9J&266!a${_^;3Y6eG$2c_0Q1KTpsK9d`y9W4-upPiq`=RVXQY=d zWddO%=iR#menU?VQPCFh5+e1S#r2}^{!70&-!Pdje!N%Y&!E80{VPXD5XZ_(_xcU7 zDY7A@*mc@d`CxzIkYv!6nowE3&AJY`Eep^+}c=>uo}-kWD}0)LS?q;4cSW1aoWDHskzK^aN zgbC*;zev)JY`D#UH4Tq(h?>MPL(VyzaCBkv?eN%a9$$aO4!BflOd5I=Lo>y>mpO46 zdW)m5FRRRZ07W4eJmIG7uXiD~mUPjwP*5%2^n#kT27pAERqzfRRX%LUtz zp8^rMc}-ghqzP3g>vyI`?ie%>9(+>MrQhrirfg2JX_?YxrpIwvW7J7#3>jVQ@2!z6 zGAO*$SGTP!E&Z7+KXfoP{`q1VSc^4wVwFXWyYBbqN`^#z{O*W7ZrP}m`w$quR-^AC zY1_@r&E*p`*6MZONusQxV+iuHtV`--=DhY(!LFnjG?f=AdBpC{I=uqHp%9o6KM+Y% zhL#L??!H0{29do3bQjwMbpY_m1D_=wu;Zv-&J1^gW2XR|55d%T1ROD{9`{~qI@#*q zJ9M3SDx2h;@@$YFx!5~4pVPrjM+!{0$|3q*Uw~lR_OmjkRna@={A5X^Th9obKFw4g zc~3K56wObi8`LRiExxiCMw{LEagDu6#xXHdC0{`?IqI?WR4Eac;+|ls+u^q;3590U z%HjxS>BoYL%R}N{UMkfY+`JW(5W&%VpRIh<@+qZ$7XEKC+^2P8P+K!aONQUB@N+1D zjKn_9EC{(71<1|7fkFRFFgVLcJ|k$L-N7Vg_j@?Erkgh;aeZFGTwF@fp(JbOW@Wv9 ze)3b4&->)yRyX{g??5bN1Raf|>AACaEvFmy-rW!Kmkcf3HOL;?_cg!2Yahb_(V=)) z{hfP zSLDsZ*Z6Q*yi5{Y>2%r&InX0o3BF=C1bVc0V6bEj=obOcgEEeOHL$}PYjCws_1gPF zE$ZC}g5r(+Gx#!1DEf{=0jU~v6Hq!f0?DBoz*`_E9fvQEq!rWzE6kFzA6lT9tp>Lo z;q8Ijy5*08z){`kkqkj{)=1|V`xIkYRWL56ywP-}kNvPuG?roKCm)NoKgYf0BI)6` zFlgP2J)Y1y3X%>TGChCh75nlkjdbCG zzaKg!C3Ez#i>>5xLj2DJjvEuNvaW;>4&f1rXqtK0DMyjpx$8ivmj^M=gL}?l`hF(6 zBa{EXJ3?(f@HzubUDTf&0!s=}@U4LVQ-SI+yeR_E9dm}4as8Uo8){%1eN@-Onf z9T7yYe}tp4=0z4nz7)hQj`dF0Pur~FvO6Kmm*`KdGo`p28iKF56lCUEly6IT^kq|g zY-H;zqltC4`Jr*mfuAGj`PPP656RS-| za))ANIY?hzL zrHQfmHSr?#rh-b9`e<*d4#>MN=m8M>zbQPR`@sT6lGD=T|qmzTDUY6A`Dm{?wdURPS^{Slr?Rl%rilu=6|IIPaYh<>~=^%O1uow1L z;K54eTTMmd3N?8fyU%-}vOhWcS!*|5OGVe%U)0z$%vJou70fqNRK~w9rCn4mNuy<~ zooeM!?nw++VnG=Qq?!+X?38I5R&sh1lWB3!)7V{Ngd40Mr!zlO$|l^4()@{mMSb`@ z*zaa)qYC#qx8jR8(`yl%1Uo8&YSORKTOMsPC*EM0btz`L(PPSM*Q<(U7Y+GcC}eZd zWD)kIoQ$^2$}#PNI}bIzn`bmg!-!n89kCnM-E|22t;%wOomF+v_J2ey1t3qOg8>$d zjJhGuLr2%!G^hsaZl{2i}V#-hPW&g0GmhHJkwzw9zx8XFspv)%3@=mJv2Q}0xKd8di%c($LKV|*XVDzz^ zfTtLKOs)-v^P-ag87b*l(VDd!A5k&uH$wQHblAmF7RE~-@3Y~C4-IVE3B);Oe<;-8 z=H4--(oyPNx#frz%qo;u40hpfyxI=J#(1kTeFfn=ePfZ2OQ){M3)OGtS`N+&mZgzs zB#sRPY__+VKIGhaY0H$a=;kaxClW@dmVWw0wDAA#n-}>%V9Qz{0eL#t-~T2~i&xlv zl{e>;yKczcl1ZOy0_=uuNKBQGHByso+Qb20in92HeC?(9k3($vi?LF*98I-0IWEOV zsp`wB2}^O)s?W8TVpSUk4Rpu0B>sMb(%)}bf^VqH=DJ-gwm$&ss~=Y{;-BmI4CuVm z>JVTZr(%K%rHc$lbibQ(@^#skEBuPrg$}eOVH^*B=?cRQ%9C2j{F9S5ciXkt@Z6;# zn_I4YrlKLwGCk_=4`vSUQi$DmNLEWrw&T{* zujW(DB|FP=VKGR^mSEQ+)0lA-NW~{4oB&ftUbC&o2~3d0qQr!e9S{G&vBFG5{|x-snfgb3Xn{YKHC%PoOv!Q=}H0Lg^IFL@b ztuA+e#pB1Mw;akcbCuX1`Y5pBzdxh;TAWwr$vn98cxAP5EOS3@6j?i{bZI)_-u5_r2cje8Mq7)p~H$$FiY z9T4I4kqL&TUjBQhNr)ZMEaLkJl4(rl&7zhLBYuQ@u*e-{zILPAk&d8>27<!6T`$DYk8j435B3GaP2t=}0@ zxKe1Hm+>9N?r}JIi~mdPV{ZP4w*ntU5)xl!Aw|!d42r+fd#H`B|MyOU+yuEm3l`*m z_4D%!i;OJZbb}aYy&GwE@96UuZ&}&MN%vn0kJhMl^L&mA)|qfKpTlDxx~fq{Q=XZ6 z!Js@~-IPFbN{js)euAocw^ARF>o^o3+#inXeTDbmcThR9oHwR4fFNTv zT~n+idg^q#qUun<0pQHqPk;G!^S*qv;TP#8DUG;z3BNa|-`I*4{HLs%+~PM!VHEVcUv|B8UQ# zktC9716U+cD3IKWWC0~-Ft><+g@B+0L2^<7k!&L8SdvweWXS>o-`ItH-gDl2p6}1S zJm>VIShd62Yp*ru7-No^ma7PH>3zp)979!0-ysOP3m~w-C*ZD=1=$kO;l2SbGYLem z+xms(0L9vmlYkzlwa>ImG6rj1k?HN16me3B7E-5QED$TvjaoeTL}EU6<>90KM=AQ- zY4c{Cc{3{&2Xp{{A?6^a69nnkC>p^Yj&O#PW^`DtWVI?F;JQ6_pbt3*g?qgXk&Kg@FP zGlBc%43cI1oQ7E4jY7k>Zevktj`7OTy4!xPk0F)!wkbE>g&eg51 z0JGgx=-~!D+exDDq523F#-kkvsFf8J4P-6-*z4EVZrv}T5s7^WP2R?xYS8i7emFII z2!^IqRQ))>m~V)dp*TC=m`;3g@=Q3#T>sX|kriV`lk!*}q$FX&0}gzP)`?Q{T}J5( zUT4hq?M)i~r8tF;xMZ_+Iu2#cxB%(p?>z*1BI5k*ui_6MPID(+y&2{wI5`1KP%~`}Nna_?=Ctck=m@CvOtnOv4@9%oWd^p+YL2 zoHEheA^z@N^GvK$kj$`oA8qBbcW!honDr8~R97vW+`OrG#+AqFkmL8$4n zzswPr=3PQfI*|q`?%>-EESHC@MM44Xn}054sr-CpXtI*yE7IsBbvNcS7ZL{|B(kV` z*R5Ya1ZdbQbJzo_ILb3{O^JBvf+F0A*Gxh}2U4R2D3A9^_a<2%&`m;wy;vJjBpZJA84_F)z!uncHP^!Co2nM<`Dj z>M&6sf;V!^XyL%re5U7;E2#mU)Gb6#ys*EvP@+aYpuxmzEU==(aX7DHidaLZrOBI5 zM9(aR9m~0Kkf)%qFJSH3a9Vi2IBt_ZJ*?6BEy;djv7wf>NZ!8#zzIxa#xMMd2{QF9 zsTUOjXBvbx5ue3OrJ%a$xu@%e2}HT*S#k)bGE^lbu5Eec*$m`PlvZz zDWVCsWv1nR8TZ#M8G}&q4d;lFlR1Cq7U)PC@hD#NP&atl@(|jhdizP@ zMwh{te;h3*KVlD=XiiFJUmOMp!cCq_zoc@67(uz7gS{59s>hz@b=y_GY)W;@mdI{L zlTq%FVzQQ=d;b0_q;s~DYal{ajj0ijH)yljq#B=5pGH3ay>hZ1@3uL*u{V3#cvL+S zHb~#3zOACSuVT|ivYhf^?I7QxY3sj>uc?)5t1}dwSzy;wTv|})+j5snC+sWdimPgm zdX(?__1d{}Sz19;Uji!@a?Re~9pdFlNl5RT5d&#a{&K|lK5?1eai7hG=rfO^k~D+G zruF41taOxTFm%TZtd>FV%A@dpQsUKu8K)Z0JkN>=T zp-?H{uGoDx`F}!*NG#oP%7tXXQXk5qyA1K+Q#o*rdl(rhkZ7h*K`E7D_V2LH?#u=% zCaK58%R4RE@0hpbhfk6v;yy)v5{fK(^XAQ_QjL!BFGlQ(oqLS9Q+ppNCJznwUOAQ| z8V8YVo=RlxfLL5zotoR-i|Ea+3c+7-NeTDSqnd}q?I{bhqsbpXo}U?Qr;dL_nPdve zJlcCwhlVxk3qr#44ZD1|gkE2p)NHf$Q*I;$-chCBYdYAAA2>7rRwgKb_mnk ztrxAoz24S$@jiB!#%sT9n4U@+zt?5z>sfP^C-3>!;fMXgZHMEPpDG|0;&b`x@A`Sz zX2$HaR%(Efxp+~Pd}U3=xj+BBcKiu_cvxWCV|uo)hC={a}!ATJ@6F0q#_Ng;Tyo$`=C1H1H1?Is#JhZjWJe zsvme~#)N4*Em2FUb*(jd*8o1!*mWQ_PKMJMy2$5-OWqPcsNg?*Zx>a z0F_X@kxofoE-5W-pHn+!M0r-=HfI5RW0sohtW3%DTJaq1)Xh|nE$tz$#@5!B?XI98 zN6Cjrdv+LIRQCP(7YnJlFGYs?g2d@bLKJ26P##LH2DEml{E2-7Y1WcIq_XOm+RE0&7D(RwkL0M9CY&0$q4PuWOMe-x3Y zbwIkh@ti?RaCJ%e@1}ov>e;-qY5O|op!ogjjn?T=v3}WFX-odE)b`~LB_STolp`1| zloN+OmCkz{ZnQq^yWRRdB$L@tkLqp?gNqAugGL7ry{qQiz0}rW+s9Bcc;(TbH)@WA z=t;n6mtN%UTmHqajr5M8pWiDyN_n_*DxF6_^sVY0lFQ`wp6VJQfQDjT5;hN8#Z97I z*k9dwaA{U%mKaymm>(giXu*<4XnMPcH2Ym3_v=AB^h)1nI~RbULMG-esb zZ-{@v!HJKNW~DVA3o;s2p;^2QKM>eWMXLWyzki#H8i9g*davtjF5iDzej*KhuuI%0 z%MUv&e?+{WmEIb_N?lo=lU>Zhiy?WQq@*N__PDbzAmt7W4mR=S+O!QYuh|_v*r&!t zMZLk(M)@%@uVf-IeSNX=y{CLJDrP-dZe);79X}6B>1tl5XDQm;Rx(Y^yW>xrHYo%1 z?i@#_-<}C)U(7No<{~2;(6iKMg;nDyM`W$(AAh_G#VfccvX*+eRKuW*Y^x0B?%U>^ zqy-6;M@QbC7D9l|AugWtP7rZgYvCAf+R#MVUuGe}<43&$i`DaVbr++Gj_lH2tNX%Z zH;)D87-y7|I!xylzt(Q-A24F24r!T$28GF}e?UZP#T`<){8UkY~{J1@LUEjmg}kRKL5(yKqd=&524Q9Yt% zXqV~&PQJR(;}liaw)gM5{{GpenK06J1`skz0e|@Lr}1OIsZd(aVS=PVt&ak&2Ka5GwxBY2=w1G_u##8i3U@ZYk>jr zjXUYcP=ug6%wE*&9N*q|e-KG&$)o5ug8ck7g?B83IgET!Loeb64Ex*g|5}!4^Foxd z5kWmdpqIW}WbvE_1jdh7-ND)veh*=SM)FxTdj{wQ~s_c zJEBjFUH9Qve~Q$9D~pU?AV+6|nWPSpfzu_$^NW+L&N?%jt;IqSPN2akj**`DutSkV zs&CdZOvQR>j)gx=)~m3~b(rryx_Y7EcKY*hNEW}sD5wcO4CxpKeTQbU>u46$zAs2C z$!ld9R_e{Tg&C~F!_ZSxaib~7S**}SUX?YZc7S|al047KyiVa{g4Jo?ls`^=`sJcT zxx^Dz@n3d+OiYeW&+QstDlTpxxbxwwnp*X}8K;pZZQ+&qEuVK07yhQDmzA39FwD(6 zqQ3)M*G%2MsH-r=kOw2^$6qjguCKp;D2r;)2mrs|sRoW9nb2UJQc%!pYY`uK5;I=3 zVnEfiyN?ddY2c`IP!s5IYamaLf8B-+;!h~j+JYL9nxskJR8}V zYL+$Z#kGcpUA#FHct!XasaEs8+`*NR<4b`UX06CP;{%;<^i3|vt2xaXWC!Fz7{ zEul-cp2(p$%L@1+PEIIpfMB`u+k*Oge zPSRPUd12_!c|Vh#`Y};*whMDSgbS*=RuviX{U{ z)L4qz_Lj+iWvsiB5};N@m&_K)Zk(4t`D+ft&i&EfE~h_9i*D6#5Xi`y>~P_pUby5g z1Nj7Ni^wNHZno?4cJBS-pmy+W`u)*dfrr&an2EEgjT2|{_xf=H)+ao3=g}{~x^Uj4 zh-#fbA%C1d(|L_xxCUoyP?cXh2H_bJ=w3 z=f>hpn~rJDRrYOqV644q%N7k-l@TxU!$oGuJsM1<`J6iZZHwv8O*fg_-n>OMDI>1H zyGJj1TaI_ngFgEOh6iE>=~6B(LJ@0S)-D+(QzY53RQL5>(i+%=0|rosgVHK1~7F$Hu@AMgY`wQE|AbEExC1jjgSVO3Da~?z889UV4{XSvo<^ zFfD(b>FVx&)RzT#qbql zW78m)$XeS9#^0YYXKi_E*n|d7al1W$MUjEjMfu}i4pb#f?mRC$_u_ckht_bfT1vT9Qg$+LfBLHMxVH65t4^`i!c6>=*awAGF_Wi-;zkah*81yF z8go?@4O)>~I@G&X=eM4jiVUX^-*_|gXfDpW<;yXLH0#Iywq8f_`^{m>xZ?0&0z*Pa zfg8`3Y~DL3RPUVVdKW!^{^=T3hp%laHAwgt)m+UlP4K2u(#!W?WhrxJ*QTeX`gMDV z>y17#6Se;eLnYtLnPb#J%GF$lvSS+O&nNgm+@7SHZEYKz6mFbv;6l$q*0ZZ*IH+)G zAEQ&m%2kHyPv^>8b-Rvp9Y~-*v^tIpIG{;N@8n5!RWk+)sfujRc~hqC%+WC=2}gJ` zI%a)>56LHaESeR0Cx4f4)E6L*SRa>q-F0TxQjB1j6rUU>*-nnq9&VlUcb=VQC5pRT z1`k4chpSiC%lnnwNU4rG5kD=9_$c^7kb^e=@1NrX(RIU5=jQ_xG#j zyc)s2<__+h_UP^$)%fl-AJi~2_!1;Io%e!5**cMH4S8(qKiE^qNixA2uj=yN9dh`v zoikhV45c?nT}J(F+TGyTzrTGml4MV+QoS2wQ2IIG(7}fEe$7D%ec~u|B8PA4$?f3G zk$V1mmRzo_R;XlAhhwEN zm$OS{4mUaP1Y41Fyl=~I+>N(u2O81HhJe}%g8Qy;9^*X!`SS(aV@#CaLTQIA-F-aG z>AV1Hi5HCMi5>fod znx1}mUs20l)UWB?nE2+69DK%-w9*aGQafVhNlwu!D)rQ5ne6^#5V@wh)0&wNA%Uqt zfh)nCG2Ln0sdq8wFEGq)S7w)7m)Z@3pLN+<+jL&-yk*?xV%Mj>Fd4&8(bo6SJ-GV? zppC68N0qT#N7vO&I!QnvM@RR``9#^erWhQNXLxE|AM+HmYU2roqF`}TU(GS86zqLX zc4Av6BoCt^(yl*0%| zPPQ0P2 z*U85 zYgSo9E7oM-MZH%vKfYoxvu|a&J-G0>`SDwF*No4NqqO;_t-1TL$r(Iq}`Vb&h=yXlt+g1)!6aPh{L~KcrSv}I6m*IEY}=TZ>+qj-JG~c zTbJ%_D#!d2ijk`7ksqC{#Dftv{`Kl032Z;e(hvwiJf52dk1)I|XRx z@pHPV_eQX|a_!X7)DUBBA)!r-$6o7H61P}T(W={1;@2GR6x_)*5s_m#vyk1+zRvs( zk0xc>ej(=_8ym(;zhBb+IpEX*sF8m} zq;LCra4_UZa(=EM?R#%nSa)AhK2=GVoFVnxu)9!EQIUIUA;+lXp`=?f5#lwy>Q!Ex z5BQ`EV62Khqau0rUg>_&&S$Xe{N#+!!<6FYSUb~scNuXedQy`8tR|_pQ{?2idf&9w z7ezn0tS+i;D=kg(?%7G4a-u46(!!Sf%$_f8s^osMZNH}ZgQtIn*)9Iv=CGG`X{W)8 zT^7~f_2}{AX?3!`TcfVASlOH~QJnWQ$ zyKop?{b0g61aGeFPAa@Q*kJXq0njOE$U7A5Y0ycRFB-4WDDT|iZ*qxKOdwr(@#)j2dyn=i z>xVh0L34V}T1}H&M|(TyhHeLJPCaL0eQhi>Pm_q6KOYWHz9aB_=WB->`{jZ(n2!CF z7wTl4t>G%t0`RuVUQ`y`R+oP-k=bbFFPoSzW}wkRy~h?LtDc+`wUu@~Oj3p-4 zaJO{`o0U?{Lx-{Xu&GGKz6i^Oh@`m&+f4A2)_2r}DD?Z2Cij}|x@7)kR=>inrPJVin!? zDu`{B5T;hLAA+bQ1MsTF{KunhdqTO5l^{4`Cuwaf%r^Ojwgl6O9;aPa-?i1rw1t>V znUNt!O>2g+K+U-Ev9X=BI(R53G?Z&Kpx#%-x! zW~GUcP;rNa8{aRG(>kN8Vg2|YIS?)T@xFZu*Ga7NjQ$GZ$-xzrOPi!8`z|du_a&uR zYhexS$7+R2p9xE+@6Rt7ujNl1xtvmAVoE5Z9mKW5u z*Ie^-$nhC=%rPFU&SVgB-U)$Bw}lt z+fe9!yw%W)JMqUVXGRw_fohv#?sR^w384?)`E)fNji1vzd8SX-kGp+{nr+iZ-L$3A zrP9{EWgw4@w5~1v-5*MaVqdcEqs>z@sZ@#HN!NmfvApq20MSd|-fwHmbw~#M+jr#{ z!Bd7qWF2djj` z{e=pnt-3tp%Qrwm@kn)JGl=tWqX}7Kp)~#4GMuK~wNHS1X-uO$Y>cGB7ls zE;wmi(atwfAf7(_xcDBAhhe#dE?%g<2z8-+?nHb2)Felu1OY$QCz7}3B=KMuKBVB+ z1Up^MbZRmfilHjuT2Qlp7B#)Gt!(%aFxy7i!8u1^XNVMt`SY55h`3*pzCn^+$$(SG zXnFlR6#+}9gyylh?2oj+-4btH#-}v7Q1N6OVkx$mH7<-{k3u-8FQm6+P-aP! z>FHTl8Y^`Q=6Fcw6t|T>*^ziv$xo4Tu%_tact?Y90W>I^_BqHD_tAn=vpB!SfXY!v zOSrL7o`Iha2S%>a5!2JYJh1XKr#tl=%Aur&cl2g4QBs5kKzN%~eXS1@8Qc}SvTWf( zA0I3pKi%*Si`9<^mXZCbZ${7djrpEmD5!B9T{^xRO4j8w&9PEZvJ(Y5M2P}d+9;Hg zbBxtbjE1v_S$e2l3L3c2DBae@B{=Z$Wr>jQ1A)WsU29%Iiv^Kn_O`h(X~oVJJDZ%* zJjJYXp;NSn5+YguK;N!=ZzJewn{@&i7mNy0hFXLkIrbFd0PY1`71Iau=-HcNsK@8%^Fla6yehGqv z_yozM*oOhE-B|80zcAJ+d;)>7JViUx>;d=#m`^-jURo6U_y`na6-*J#E*fnJ>t0zt zD(u(+o>A}0+gmgi0|Orh`Bqp_w`-uw_pS%>l+dfC2Qp@Us8`?gv#MUVKegcs_*N^RDUW{%NSp{x6ea4b>hGB2Oyc6FDV1KpWdfy1XwirQk_PK{}+Md4yay! z1TW=Qbad)+8BJky3fOlv|hhJhg=ruZ(% z6z~kC_&7Lb;^R>9_gu%8u86C})C~>OcNdzBKL-qIf zhwVTLX06CyZ@eY4wRG8?4)gLS80ZBiCGJCFQ}{vNvx%*Qg`3^7Nu2 z)-oy6zJJv1Qe1gph=u=C>amsR34P%k41&uqh4KzcynENWj;6Ko?onH%3@jcFqa*Ud zIz2*p1O|XtJGhyXYQsDBnuW*1z)-Q~5k(NPvSBAF2e0V<>@dC~`Z>aho%BzAz-wn(eSpl*uue$SrL1`m)Bz$On2JkF30WoT zVmgP`@{<4qzciI)kWD|WA3PiMM6jQzr51FI>I-~mB;Kbge$ITK0m@xFlB3qfd`w{? zB5@^Sty+`}lr!v!L+Pt-Y9YB~br)RuBRE{~{Ax~aSvPbd4Lh?rEIVd{A7yts>Lx#V zOHOf~Hz@y{9~WGDaS-LV>R3?sJ-wrnKHVU1`AM#za56G%N&6b5vCZ<~AqD?S0_weqOPLon z_KkilI2S(I=uq=kd7X>z+Ps;7fxjrOE}e>3*OT4%M`~>kuD=_hM-1bkKNmjxss}MK zB+yT65V1WKXBfHnT)NrH+F8W*Vqa%yvsJFgEQ^&2Y3Qi85``#L@_;~%TsZ!dkZkgn zwv_P+S+i^0=ld{6X+Zut&6RyAJ#CBO8K{6+DEXDk!8mG?UYzaF z=+zCwQ)P~mI`mxKhk_mzI}Ej&4(~m4TYp%Ig7HziQgn^TGZwSHFzSkG8)&l(C3{QL zZr;oQ0j{V?)gDH5$9S2CWW9UIO19T>KRg>et$B9)JY7$HVprtO74qv%9`q|>WVIIF z3#F>3WM{TvBhSc4@qQ!!_ysL7r1KL%(0Y9S-|DbHL&Zeong6Z-c=!oJXvMze@ctS5 zAvO68;V#a%-2J5HF{y&SBDJ`ck+IjVgvL_R9V`6ZcK-&zWr~mIZ`x||HUDxgX4+HE zkks(1H~RW`I`d<)14!D6R+dy5@%E;ezcK(M1zFvBtq1=j3foUcu0bw_c6)RQdswMg z4}hGjSdL3zZpnSM|A(JP(P%(N@QLwTAIxIKj^~&&GQ8S>qQavO9{h(0mtpVTwt&vn zS)1?&s}@f`u%LyM_@J4Y!?Tt*HV0<$M1?Q8!w*0dx$1(+#23Z(p;hHS<60VyPglpg z-8Sow;%l1a@5WHJG27m!Mq#ed<|Zw?uwuEt_oy6^^KMgi%YFw&@r0=R$0I6X_M2CU zPdc+#sF%D;xkiX|pBXx;WNHVu-CRFdeBn+oo>-Yx}Qw&~sJHr=-w_pH8tzZ~kdkWa#Q znXt%?$|XOmFtG}10n&uAP#Tq>r+upK!*4lZ*S;|7X&ZWdnR9h<$g1e^+21>t&+&Pe z!z#2bXDL_KyyYQQK;Ftt4eA#LEIlsn-a6K9Fn72{%eh7%?|{zJ3%0NBy#jB;lOFYt zE}Kry@;LE5ZOWeCqvBNy&l2Va6%9vEmF@FPizu7_8loO;)U+gfuOgFp&J64IfVM$W z{j&;L`St4b*V%eJjRP|z*muH>@5_&SSu%;q&y*LlX0GY~sNpI&`Ql^C&6`5$qJ1f; zlRkrz@80dpT21z%W^n(Pepa7JHh#HOe(v)F5r?tBr*`z(n1mczTk@*}cqyKVMGRo8cu5c$dQ$(XI^lSnMK%yzYFIZ(OH$~Dd;*dm@O4bT z)Er+L3fX=A*`Z(1549(lDTr6e@A(yJr^eq&4gcY6ArYtSMuWR|3Gh$3+9l?YUAasg z9Ph1WbO~I?)8t=CdMM?jA9mVEnXVN2@RQYzwStDg0?AMx^F1G*23WzJy%W*m-;j4g zGqdUs+H)kzLH25|(s73UGijl33AuT<>RkpdgXq@~y!Q3?1-Ypf6?zPJP#4ssA{r`r zf8NFpR;@#ie-dvb`NOaLn{VEWFJsuFnR=94C;F?1OqQLF#AwG90o=~CLXGw>M43+H z7%sUV_(k)L@ziFwd!|?T^k1cP^q9K7bQu2ofsgt6%AKoc*^9H_g_!#Q=D&~s%$BPX z_PH@?RQmmzqkZ1cC8B_(&4%&mA>RDLyw&Gd$BCKARg|lnlhYTmi&^oVIC?WiuU;;iReRnZ$-uR9-oV0p(lPHPLn*Ts=SOAL}*nOHq zuaWigk}v(PspD{cHIwx2_YO$DOExvX7*Z2HGm=rOkvFF4VDcBhsAN6Y^Wi_QFiQMD zuf;_?euE2dummQ3et*09^>cTR>NiQMzpq9Fbxfe89(-Zc`n0O=q)hDg99&p#<#5`xiwX!J@&Xg> z_n(~K+7`>(n;V2ltrKq>wNDJ8rYcS4J6H$+IsBik{fZRS%qP$OvSDz@NHmj;>*A*} z+uCI7KV1~g9}`_&&gf2-H76uVPWfD)Ts@;G2J!Fz{Ke;alfVD<&+nDf{_Su0@RqY` ztH@cD$@w%|nE^Z@P^`RqI6h`pn!pAnQ2CnrftGV)ojUo>FW|18j=l&(P=^6zQ-=bN zDBF!c!*ZWlS|_{1_8v_1JDWOJ7d<(?tf2t(=FoPXZ8fcw4hCL8PS!MUP9hV(Ac|wn zp$)s=aboq9O?Nc++k!lRD9i%ct;A?%V^fZusl!kc3DvWNikI*u8cI&*hDRp>OuYYG zX9y3UxUp3xiO!--sN_-H*vS)6Icvzt^@(=@nXqrsHXZz0A43#cLH5&(U+p#Ik&!V# zfjRW-jc~)YXf>i;fp>5^mn}t8-?=aev7V}U-%dh=MR|sO$pa=-TRT^`ZMvomTqs!! zRS8SSgoQIUy3r7HI`6h)V<*y{JYnKH|NM<-SGG+z$H*#7PiN8ddP~ma3k2{md4R3!%4mbzD#B41r@JxJOs zYVokYHqvll7%j?bJ@bt#cNt&0bee*YIScCXMd&Zvfy?ldD$s^&k(DIrIHCq?Q#LLh z9wh)QlTa}{gT!d~&9z@BJ|<6*id6PYoTd?BAWsNhxEN$?rK zSHT%SVB3G}iX3p9U^dV^_%08QN&(OlyJj3~fm44<%M9 zAos3D5yyQfDAX6Bo0O-B!@FRFs>dG^DrOS_AR<)QG^{3Evb|KoY3#B|%@cEE>|OaT zC4IK7r3-W8Hbl~jU!e|0oDq8kP)~DHGcNp?((jT6VXasup{U<*o7Qi49X| zZLlx0R%`g`POt4Nb44qy=@`++fI%UE{XZ15g8lFq=8%!;l6gg_KyhqbGe9p6$TNXF z;4df*5-Kk*$8EtWTm|>a#mNc1GHM{010Ovy1B4+UQ2SF2ijUvDdlw0)G(Ju!Y4P#$ znwp~}djg+4xlAa!F>=E*VFdgU8fxy<1rVj?Kc(pZJY0y>HAV$f4T}ovPw-dnF{-E; zw~!*zH6**??S!zH5V!Tr5r}^-ykkJrB*LV32qjek#@eFRhyUYEWFxI^qB@B6`e4pL zkg+KJzS{nCQ6jmBlTJulgEQ>K4%O1VJS#?O0BW2-cqYuWIR0FuUH+Kk1Y-|y&%PjQ z^%e9s{1rO|^yu*@mXXNb!N{mRJO|^rvrgR}*=&PohSH3T?)<>=X^P}nsp2X-juJ-^ zB<|=j88>$Q;>C+2rNxpAO&hdE4&V&mrB{Q0xg|@WRpOjo{pTpKo)}8)#8tDEr*r&V zKZjUYLwtfccS&MB8$xlx`PUc6+%mh`e!lSYI#!?OKYTceMI)+Wis)8PR7}lI!2=_Q zHE(qx^tH6BUxlJ+#|MahJfb7kK-Wo6%sMn@zG=;5FAyOqjmHAHW#E=60dhOAe2+swV#;{U37LY=ApQcMBh$3`OJ!}^E%t4pgH znMtMdTF+Yq$^UvZ)(92<#IblhR&xOF+zUAu4?O~W{y!V3H(1SPZ^hkDy!r>@IgkAe z$>{LS-`A}cqSEc%Ao?GFW7Ya4`qOIOBG>a*&c%QG8zLV$&=^gheqPkCyjrUX*>}ys6trr{s>GIPsHorK>yr|G$Atp@y0Elj*!O!sgfi_KMm3 z$CIFC#Gs(q$^&03P}p*YothXP-5UnUgn!+`zF?lC z*Z@rP(A^R)z~Z7N;##VOS-2N;T=_Ws+r-V4>6;YBUs!2NykN>dF#44g`rKAx2RIf<5j%Nf$nPU|GH z7u1=TjXP+KL~ZI?eS5rfFXmj7ZhvhaLj=+2I~k}Ua-)(#@^pP^#Of*&wY=R{)H_x8 zRoV7ks#{lQgSR{}`>t@F7;>9a2oqU!gFT#xgVX9v(UCT>Z5FP)Y4N}`U{j!Q7Mx@# zA_KGAVUzW99^4N|j@Y?#XZ6RnP^#vwP_EfwB~24fPy2R`AqGEF#2w!j1?rft7)FbK zDLiTSUizQ$zBHxvBMUV}Hkp<>C3_mpv@DAOuEjsL&P`0rTfLvU%P^!>3o&ptkI}n_ z)4TuzG97F2_;>TW?C!_qtygR#yk3iT@MJblU`>kSTZ@kxR`t3LDd0=kg(3E=C4Lf#2V0ixL$YDAJ8 zyL|;+7e}+tC4xBCL(eQC)`eM23>pP#6VyY5OFtu8GLl>AW#6A=zoDaD z?kVYP1!;N0)DrHT0IOFeveUO6NdghkI;m;N;y=hczpLg z;yCYKVZaPAV3!dWRiW0VddC^53tb*_`Wdk3nnG{bFyfm;9FNk_MVUQE{_-xbtmMf~ zMQg9t0aSRul|lir3E-f9w8|C*wU_SUb?5f&Fvus!A5j;umWb%{QlVOW4FA?QDf)RW z?%&>SFo8w&?2GM(U1fIJ(th>7f&KmlhNF%)N;BKGO;9s$P zhYqu?5r$KvZXCysee6ILNI!HFBFIbyi8{v3yLgYlWvd9}=b2d6-I#9rH}$fvAjE1H zTP9eDerMDI40h&aKe)t1f2TFmTz$v%AAE07iDN^Q@MNIH8m)M8Im0|@hb=&^Fcg=! z${vGGShxR4mL)ap)2F`0=c9Z4xn+gh5W?&CKS7zx$D&IL z3JM{TmY*>4M}v()^ddZDFqnf4`SWy#Vg{{`-9<{X^FBOj&ud-aK9(%|SLS&bu+6Ug zih$5`$)s81=6FcLnc}$)B+Hha>oI1C;VE~0e1s@fq{HEtP~{i*!AD?fkU4&V{$W&TRf0+CTt4?m%Z9R~*MWa(J&Y!Mrs&zvz&XFkt&-p&v zTJhnO3|}@Q?cmm22TRCFaW~M!>`OFjCHe*>i}GhO6G6F$fHVxVjQ#A+M02I+ti2a9_m6>l#!bRCS{)DA*5d2|Y_Or=n1fkFCUM^46rOBf_&S6A1Z(P#1T?Mfb? z-xcSt_sC7KoH7k}2%c~)Xx^0b-Sh5(1`F$M;w~rViXqNG6`a`~8nnqm**V@qs8S=RR=KPPNFp0*c?_D+bB zc%*7y@-8}q1xI#7tHao z&2sd?kv$g1y;dx{cZcXZhAz2+j0a;MgDRN$B~|V1n3r*wmz+o{&(*I8%6z`LgIhYg zS3q3%)n)TK&(Gh9>g>fkCmfc?PftjPMn)Q%3=e#9%agFWYQxeU%&UD-mfs^C(cB9p z)%(#`2=8FMC{-^8O~@4~n5oa8g_!dkXz4W|@FctzY_w8xKsy$lyXN{PKEnr89ZWjm zrqb!PvefL!(k)Q0M!;quMF_i<_$RhLC^Jlend5MF(UMiHR|;mhOe$u$rxf;CtIk|9 zdK+i}T_Vc-G4VKsgw7e2`ZZ(O)wv+~V~c_cf@e95He?NKBt%7SK?5OM-r`BO|F!gP zBDvRtO#Z5~@T6hxl1iHeR0vC6XJ2Rg3lIVvh)UQ<7cN}zTrpMs5^?f9>`eSYrlUyd zx8KxIF;1j*gv^5S43~=vUp4gUm~u0;8dr*}jYr%-&E@mlX13G?cEB=#CEEGIwO@;Ou0(SX{V@kO4PBhU-fap=3 zLi{VtQvMWmmBpUXjeh9Qpb*qDbf36|R6_%fD(LM)_(g1!`sTf#tvXyXyCYY=BQW6% z?(7u{;GSye_NV|3h}4@Kq38yBraHVY3=9nHBv)*o)9`)|GQWejEpcdNoa4KS)0Rz} zk|uv(MPIyXZEbyHdShw!LjGaP_FZ$c6Gjyexd?eA8wqXMTOt8y2n}E&vSQ9OFU!8C zRpgoLVBoNb8cSpAR6KpUjxb0eBCT-`6$isfH@V@XXfgq7jq}8#BCyfDMUhz5ZO~X4 z&)72^P9_%LKE2&5<@_AlAqi9<=K#iS1Qs`guHY81dY;W8Zjt>4Yj%4o1cmZmKYCb3 zsl@0IAt6L&RBh96qD+^lX$_Ndd5O7f7Fk+*rD>;dj>Q2swhx|qwqeGn6n8Zwr^qp@ z>CU*EX?evrHrn$Z)ucovKIzn{0xWJzLZ=4U$4DY6DDC*^B@rL)7={2O9@FMLUf6nU zM1*?N5?74){&DwW*id2boOjDGVNSk_PlxT!`=`)1b&mT#9s07!KBPP7ot%kiqdG#m z-BSjNj-npg&5U0|Y;qPJ)@O1C2oITA)d=X#(A7Ns`F^Yz5?C>!;?#F#WMou&2&fqj zO+}!iMn`*UH=cs2G~6dC&xok>-fjU+M8Fl&A#Q)DupT^koTwp&5wkrk1%z%5JYKyx zE^%?~RZMA{*Y_JW62s_4{(eqL{`|0mM zdDHKd!eKRmDKm}ThzEK;WEiN!t|MN8F@>v3$4Q>)Zc@x7gtLuTB*Nh>lZ{fV2~Iw~ zYMB!0y9-UIoDtyv`!YX}oLJ zb&{g{Mf2JBh%Hz+5x0&wrF`#7Q^h!!-r2zHEa$bd*v*aw)t*wgG^>kQpVcsj`Og*9 z@!JI?53@aC9ZGP1P3Z%wcfB)s`Mo7e$rR*yzQ8TY$Tr~|O zSP>$hV19j_{*~K7cQ9oVdo@3^hwMTasE>Sy!bT(oLAwI<(U}gg`?JaM*r8?S16K{B zZ87HR=uxh9{P^`}(M4x{LiiPN1BU1>oVMf)@0g$n-AU)cZn1erzrDf<4+x6`l01d+ z=;iab20Rel&APxhi+pJIcfL-5nr7XS-JXk9K?4}ZcA1rH8<-QmF9{H&v{ugES`%B-<+J*_l;xE4T-Bu`wR$PoQ9h6z+xm_yG3!j~I-N zTegT$lrdgF;b$a0VffoR*ACgU#sx{Zd1ZNYVe>ZVe@z`0MV0o;^uUM+18Yh6C2@#| z;GEDr9yLhMw*KYhy+&d_86^FN?eZgZM5h)Fa*=ISGT}rX??W6 zd#D^ONI!meFSJb;v+Gx0)X_3t2&XK5;{4_A>c)tXBjsP;~prw$ilA|I!_U^Sv%N^nt z2DCw>j~N%kj?ldE!gjQFn(y2Im1p0p88q+sxSBs=MK=MNXo3~5pO~K; zfBWqSMHy5#QcRUw9*8WC5J6{W3*PW*Y$|35a7|@?d^;iRI&HT25grB*bN+e4!?n(r ze7INWHg-xDY`#a)`Uf*CZDP@29(m#;L@$lXa0{WQOWO31h^axaR0DRf_6o0sne`N0 z#2yhDGU0-v94^tk+&XM8QB4@cU(m(ubU?J$0MDz@#4H96qJ206N13-^2|8Nu3+g)2 z=b_O8cK-CO@y#1ICLUqq;Q~#g=h$i-GYYRNzL-NNkZLz%q^o?1}XFTkVvkx8b#O@L6 zgNI@c_=s>$LMt*60y;oSy))rN5R5jG3Eo0}JaHs)1pFN#t0WdABmoaKG*TwCiWY*b ziQj(Z-yb3@wG zPm8@MUUPtIg#qLzLV5%Mnm`fGVx5NS*dDN#c5p0GH7*!q5sNk9AK(D_rH+;Dndrjr z?lJ{vhTsZFX9lkc7aS)Li4OsOA;2@qvDYWyj+m}V%o*~(ftD((Q04zMXt@es{y#)P z|KosBZ7`SQ&<NKYV8otl!*U7m`F`>6j0N$i;k7}~Z)nQQefY2ffoDqCs}Wol z92hMz>-I`IrqG-hEAg!a4seNKxdf!Yl@BpIVJ+f`4_btK!B}w!d4kzT)hwE!JkaCA zq%YLd@Unh#BoN#|rYk{Ry6aX|9ifb2>u{p+!u0cIk|Hnvd2tF4U1GnVKVYX!?TU>=q}X?Q46!8F=MM--_M za8%?VoE#kT#2owW)5=uyu1bbAFo!NCkmx8f=mWB6qXziv_09ZhczKDF0(%(- zK~9BxkLA)lmlbUI!(ga_I1ZT%0Y4F0E5P_fG#4x2mL<}k7Gh}t%#51-S;E}{9`^QV zp-8AUTmhV9T`cx-9Toui-uj3zWYEdN@{@pNiI!CoX*HJJEDK9m?&ab&Ns_X`JA(P) zc`HvC*}S^j+*mUa`y+oc$G(o&QDClvaLxyikpO``t%nm<0D-xD)CHK&Pxc|p^$0bx z8>neU0R}G#s8BiqlOutB-r)$$4m+Ng8HTQpIlV?X4cMYexCGg*zbeuG6&2e6(ohzV zysE~2sRji)#G1G&}QOt2E|bd{{4+IXp_1m^Z+Ct;X{p`&V;omXw58(_C$DoRdE@9HfGOi-z3U&)sZs9B}~YcJt3NnD;Q zUQ36!>vlh%MjMYG@9@TE+Cp`3lrrKEOQT2d3u?(nB-anBSq!~=n=MR%gF=ju-eAY< z`jvH20t3J~_XI%4#{tB~oe;L9SBcC1_;%w3Z{=wrtP)2kRd72z+nH&)JP~Hg$6VE?3o#6HtcvGS3KFhXHNnkaD7Vegmb2CUxi{iA~?wcOk$YT82b_Z z6I=nGvXP+oaE0kx@`x7+RzF@a|Vq15bMX7pDr(1)M`gxGC?Y?HXT^9_jc)weBYXJBT*0=x9xsc2FE)&PS*;KOHl(aEmW zxQKIved)LFz2$+UrNC8A#aacua-X+F4`&huQ>~4=mIZ4*n>qGlz{`*^2@*4+ zr4yhu7z~#nL9{|5KHGyoH-9a_icTpZyg^iCRZS~+YX^F%pZ2jUJWjZ01t!Z`aw z_xt^PKF{;K-|y%BC>vHzT>$D&El`A6ej72}%JqCj$5mdr+Te=5 z%x+1;Je6R+czm7{6S(OrW8@vXeCaaSh47%H5SP~^7^XXQ{unn~t-TWildqzs9wU{< zr+V&XrA!FU6IviF?udVRkx4jhpiUj%Z-QhSIn~@=IoW?UCT?+*?UC}0 zlu)G`{Z1*4aJ-956TNG*z%0K4ieJGEJL-cik^E}FiQB`3Z~s1Id{<7})wBb(eqzdS zXU07=f*WS&ewW{Bxf^1a8GYN!&S+_nJsd@_A{^1PPWq6f4bqnCh>B-Y<}b~bbgJIC zp1bL+WL?w7_YZ{@1Xv7QXZ^l}uNK+FqwC%EVKhhG;S!(0^6cPXyBy|3*Hf#3-Fx8}VwKVNb}%e{6JnOewgR4eMx zz5EcbT|OXpk}z~}_K4NH24S7}0((HIsIap@Eg|T|ESu6m=iV);%am)rpu1jdzQa>z zkML9yQ3ZAi0}AVh&vPU@2?Yjrf~sqeS+tyQG$jhOnn&6r=q`Eaq|^rNniG80NnZXnLSad# zo$wiAXL^pjuyt1G^2&!-EBPbba6$T3KnO3Lsk{hkd0vySf*E|ZBAIn)A&G(AYH0{; z!1xs^JaeaOF0U={G^1}s-~gu@bY?Xt7b9boh%TPpWMx`_1Y^KIXg(@a#Xx0Re>$i> zocens)LzHRRZrcPgZ1-qnym#O(`~7>OazIITSI@i_Ya{hXrOBjDpx5L_klok!Ps_F z!Ydr_)A?yC6?;QUGA3p@^9;naj#aWOi#+Ho8Kz^S9Z=B11$M9;?WnFWv5nra;Z4r% z0rBybq@pj@Hi&z(rZivDQj)|uEcX1wD-UYgCnF=Rw1J9I_z7%WB^ zWEd(n>BsM$A19<9%an{ diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json deleted file mode 100644 index 976e0381..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json +++ /dev/null @@ -1 +0,0 @@ -{"dt":900.0,"input":{"mDot":{"name":"mDot","lag":3},"load":{"name":"load","lag":2},"T_in":{"name":"T_in","lag":1}},"output":{"T":{"name":"T","lag":2,"output_type":"difference","recursive":true}},"agentlib_mpc_hash":"5302c96","training_info":null,"model_type":"LinReg","parameters":{"coef":[[-98.39137379499294,-39.21682530199981,-0.5848461265173618,2.59314258954646e-8,6.637570493239764e-9,-0.503047937143595,-0.492892772393688,0.3401290832302948]],"intercept":[187.30625067405674],"n_features_in":8,"rank":6,"singular":[35.99361869005735,18.9771165622424,0.2417850576555883,0.21421375073807045,0.023474240917668355,1.309867755231031e-12,4.219908371740855e-19,2.1045347182571248e-19]}} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json deleted file mode 100644 index f86feb28..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "id": "FlexModel", - "modules": [ - { - "module_id": "Ag1Com", - "type": "local_broadcast" - }, - { - "module_id": "BaselineMPC", - "type": "agentlib_mpc.mpc", - "optimization_backend": { - "type": "casadi_ml", - "model": { - "type": { - "file": "mpc_and_sim/simple_model.py", - "class_name": "BaselineMPCModel" - }, - "ml_model_sources": ["mpc_and_sim/Trainer/ml_model.json"] - }, - "discretization_options": { - "method": "multiple_shooting" - }, - "solver": { - "name": "ipopt", - "options": { - "ipopt": { - "max_iter": 100, - "tol": 1e-4 - } - } - }, - "results_file": "results/mpc.csv", - "save_results": true, - "overwrite_result_file": true - }, - "time_step": 900, - "prediction_horizon": 48, - "set_outputs": true, - "parameters": [ - { - "name": "s_T", - "value": 250 - }, - { - "name": "r_mDot", - "value": 1 - } - ], - "inputs": [ - { - "name": "load", - "value": 150 - }, - { - "name": "T_upper", - "value": 294.15 - }, - { - "name": "T_lower", - "value": 292.15 - }, - { - "name": "T_in", - "value": 280.15 - } - ], - "outputs": [ - { - "name": "T_out", - "alias": "T_out" - }, - { - "name": "P_el", - "alias": "P_el" - }, - { - "name": "Time" - } - ], - "controls": [ - { - "name": "mDot", - "value": 0.02, - "ub": 0.05, - "lb": 0 - } - ], - "states": [ - { - "name": "T", - "value": 298.16, - "ub": 303.15, - "lb": 288.15 - } - ] - } - ] -} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py deleted file mode 100644 index 21574e3e..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py +++ /dev/null @@ -1,118 +0,0 @@ -from agentlib_mpc.models.casadi_model import ( - CasadiModel, - CasadiInput, - CasadiState, - CasadiParameter, - CasadiOutput, - CasadiModelConfig, -) -from typing import List -from math import inf -from agentlib_mpc.models.casadi_ml_model import CasadiMLModel, CasadiMLModelConfig - - -class BaselineMPCModelConfig(CasadiMLModelConfig): - inputs: List[CasadiInput] = [ - # controls - CasadiInput( - name="mDot", value=0.0225, unit="kg/s", description="Air mass flow into zone" - ), - # disturbances - CasadiInput( - name="load", value=150, unit="W", description="Heat " "load into zone" - ), - CasadiInput( - name="T_in", value=280.15, unit="K", description="Inflow air temperature" - ), - # settings - CasadiInput( - name="T_upper", - value=294.15, - unit="K", - description="Upper boundary (soft) for T.", - ), - CasadiInput( - name="T_lower", - value=292.15, - unit="K", - description="Upper boundary (soft) for T.", - ), - ] - - states: List[CasadiState] = [ - # differential - CasadiState( - name="T", value=293.15, unit="K", description="Temperature of zone" - ), - # algebraic - # slack variables - CasadiState( - name="T_slack", - value=0, - unit="K", - description="Slack variable of temperature of zone", - ), - - ] - parameters: List[CasadiParameter] = [ - CasadiParameter( - name="cp", - value=1000, - unit="J/kg*K", - description="thermal capacity of the air", - ), - CasadiParameter( - name="C", value=100000, unit="J/K", description="thermal capacity of zone" - ), - CasadiParameter( - name="s_T", - value=1, - unit="-", - description="Weight for T in constraint function", - ), - CasadiParameter( - name="r_mDot", - value=1, - unit="-", - description="Weight for mDot in objective function", - ), - - ] - outputs: List[CasadiOutput] = [ - CasadiOutput(name="T_out", unit="K", description="Temperature of zone"), - CasadiOutput( - name="P_el", - unit="W", - description="The power input to the system", - ), - CasadiOutput(name="Time", unit="s", description="Test casadi time") - ] - -class BaselineMPCModel(CasadiMLModel): - config: BaselineMPCModelConfig - - def setup_system(self): - # Define ode - self.T_out.alg = self.T - self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in) / 1000 - self.Time.alg = self.time - - # Constraints: List[(lower bound, function, upper bound)] - self.constraints = [ - # soft constraints - (self.T_lower, self.T + self.T_slack, inf), - (-inf, self.T - self.T_slack, self.T_upper), - (0, self.T_slack, inf) - ] - # Objective function - objective = sum( - [ - self.r_mDot * self.mDot, - self.s_T * self.T_slack ** 2, - ] - ) - return objective - - - - diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py deleted file mode 100644 index aa5af3c2..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py +++ /dev/null @@ -1,125 +0,0 @@ -from agentlib_mpc.models.casadi_model import ( - CasadiModel, - CasadiInput, - CasadiState, - CasadiParameter, - CasadiOutput, - CasadiModelConfig, -) -from typing import List -from math import inf -from agentlib_mpc.models.casadi_ml_model import CasadiMLModel, CasadiMLModelConfig - - -class BaselineMPCModelConfig(CasadiModelConfig): - inputs: List[CasadiInput] = [ - # controls - CasadiInput( - name="mDot", value=0.0225, unit="kg/s", description="Air mass flow into zone" - ), - # disturbances - CasadiInput( - name="load", value=150, unit="W", description="Heat " "load into zone" - ), - CasadiInput( - name="T_in", value=280.15, unit="K", description="Inflow air temperature" - ), - # settings - CasadiInput( - name="T_upper", - value=294.15, - unit="K", - description="Upper boundary (soft) for T.", - ), - CasadiInput( - name="T_lower", - value=292.15, - unit="K", - description="Upper boundary (soft) for T.", - ), - - ] - - states: List[CasadiState] = [ - CasadiState(name="t_sim", value=0, unit="sec", description="simulation time"), - - # differential - CasadiState( - name="T", value=293.15, unit="K", description="Temperature of zone" - ), - # algebraic - # slack variables - CasadiState( - name="T_slack", - value=0, - unit="K", - description="Slack variable of temperature of zone", - ), - - ] - - parameters: List[CasadiParameter] = [ - CasadiParameter( - name="cp", - value=1000, - unit="J/kg*K", - description="thermal capacity of the air", - ), - CasadiParameter( - name="C", value=100000, unit="J/K", description="thermal capacity of zone" - ), - CasadiParameter( - name="s_T", - value=1, - unit="-", - description="Weight for T in constraint function", - ), - CasadiParameter( - name="r_mDot", - value=1, - unit="-", - description="Weight for mDot in objective function", - ), - - ] - outputs: List[CasadiOutput] = [ - CasadiOutput(name="T_out", unit="K", description="Temperature of zone"), - CasadiOutput( - name="P_el", - unit="W", - description="The power input to the system", - ), - CasadiOutput(name="Time", unit="s", description="Test casadi time") - ] - -class BaselineMPCModel(CasadiModel): - config: BaselineMPCModelConfig - - def setup_system(self): - # Define ode - self.T.ode = ( - self.cp * self.mDot / self.C * (self.T_in - self.T) + self.load / self.C - ) - self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in)/1000 - self.Time.alg = self.time - - # Define ae - self.T_out.alg = self.T # math operation to get the symbolic variable - # Constraints: List[(lower bound, function, upper bound)] - self.constraints = [ - # soft constraints - (self.T_lower, self.T + self.T_slack, inf), - (-inf, self.T - self.T_slack, self.T_upper), - (0, self.T_slack, inf) - ] - # Objective function - objective = sum( - [ - self.r_mDot * self.mDot, - self.s_T * self.T_slack**2, - ] - ) - return objective - - - diff --git a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json b/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json deleted file mode 100644 index cb1fada9..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "id": "SimAgent", - "modules": [ - { - "module_id": "Ag1Com", - "type": "local_broadcast" - }, - { - "module_id": "room", - "type": "simulator", - "model": { - "type": {"file": "mpc_and_sim/simple_model_sim.py", "class_name": "BaselineMPCModel"}, - "states": [{"name": "T", "value": 298}] - - }, - "t_sample": 10, - "update_inputs_on_callback": false, - "save_results": true, - "result_filename": "results/sim_room.csv", - "overwrite_result_file": true, - "outputs": [ - {"name": "T_out", "alias": "T"}, - {"name": "P_el","alias": "P_el_sim"} - ], - "inputs": [ - {"name": "mDot", "value": 0.02, "alias": "mDot"} - ] - } - ] -} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/plot_results.py b/Examples/OneRoom_SimpleLinRegMPC/plot_results.py deleted file mode 100644 index 327a5052..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/plot_results.py +++ /dev/null @@ -1,185 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import agentlib_mpc.utils.plotting.basic as mpcplot -from agentlib_mpc.utils.analysis import mpc_at_time_step -from agentlib_flexquant.data_structures.flex_results import Results - - -def plot_results(results_data: dict = None): - """ - Example how plotting with matplotlib and mpcplot from agentlib_mpc works - """ - if results_data is None: - res = Results( - flex_config="flex_configs/flexibility_agent_config.json", - simulator_agent_config="mpc_and_sim/simple_sim.json", - results="results" - ) - else: - res = Results( - flex_config="flex_configs/flexibility_agent_config.json", - simulator_agent_config="mpc_and_sim/simple_sim.json", - results=results_data - ) - - fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=2) - (ax1, ax2) = axs - # load - ax1.set_ylabel(r"$\dot{Q}_{Room}$ in W") - res.df_simulation["load"].plot(ax=ax1) - # T_in - ax2.set_ylabel("$T_{in}$ in K") - res.df_simulation["T_in"].plot(ax=ax2) - x_ticks = np.arange(0, 3600 * 6 + 1, 3600) - x_tick_labels = [int(tick / 3600) for tick in x_ticks] - ax2.set_xticks(x_ticks) - ax2.set_xticklabels(x_tick_labels) - ax2.set_xlabel("Time in hours") - for ax in axs: - mpcplot.make_grid(ax) - ax.set_xlim(0, 3600 * 6) - - # room temp - fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=1) - ax1 = axs[0] - # T out - ax1.set_ylabel("$T_{room}$ in K") - res.df_simulation["T_upper"].plot(ax=ax1, color="0.5") - res.df_simulation["T_lower"].plot(ax=ax1, color="0.5") - res.df_simulation["T_out"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) - mpc_at_time_step( - data=res.df_neg_flex, time_step=9000, variable="T" - ).plot(ax=ax1, label="neg", linestyle="--", color=mpcplot.EBCColors.red) - mpc_at_time_step( - data=res.df_pos_flex, time_step=9000, variable="T" - ).plot(ax=ax1, label="pos", linestyle="--", color=mpcplot.EBCColors.blue) - mpc_at_time_step( - data=res.df_baseline, time_step=9900, variable="T" - ).plot(ax=ax1, label="base", linestyle="--", color=mpcplot.EBCColors.dark_grey) - - ax1.legend() - ax1.vlines(9000, ymin=0, ymax=500, colors="black") - ax1.vlines(9900, ymin=0, ymax=500, colors="black") - ax1.vlines(10800, ymin=0, ymax=500, colors="black") - ax1.vlines(18000, ymin=0, ymax=500, colors="black") - - ax1.set_ylim(289, 299) - x_ticks = np.arange(0, 3600 * 6 + 1, 3600) - x_tick_labels = [int(tick / 3600) for tick in x_ticks] - ax1.set_xticks(x_ticks) - ax1.set_xticklabels(x_tick_labels) - ax1.set_xlabel("Time in hours") - for ax in axs: - mpcplot.make_grid(ax) - ax.set_xlim(0, 3600 * 6) - - # predictions - fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=2) - (ax1, ax2) = axs - # P_el - ax1.set_ylabel("$P_{el}$ in kW") - res.df_simulation["P_el"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) - mpc_at_time_step( - data=res.df_neg_flex, time_step=9000, variable="P_el" - ).ffill().plot( - ax=ax1, - drawstyle="steps-post", - label="neg", - linestyle="--", - color=mpcplot.EBCColors.red, - ) - mpc_at_time_step( - data=res.df_pos_flex, time_step=9000, variable="P_el" - ).ffill().plot( - ax=ax1, - drawstyle="steps-post", - label="pos", - linestyle="--", - color=mpcplot.EBCColors.blue, - ) - mpc_at_time_step( - data=res.df_baseline, time_step=9000, variable="P_el" - ).ffill().plot( - ax=ax1, - drawstyle="steps-post", - label="base", - linestyle="--", - color=mpcplot.EBCColors.dark_grey, - ) - ax1.legend() - ax1.vlines(9000, ymin=-1000, ymax=5000, colors="black") - ax1.vlines(9900, ymin=-1000, ymax=5000, colors="black") - ax1.vlines(10800, ymin=-1000, ymax=5000, colors="black") - ax1.vlines(18000, ymin=-1000, ymax=5000, colors="black") - ax1.set_ylim(-0.1, 1) - - # mdot - ax2.set_ylabel(r"$\dot{m}$ in kg/s") - res.df_simulation["mDot"].plot(ax=ax2, color=mpcplot.EBCColors.dark_grey) - mpc_at_time_step( - data=res.df_neg_flex, time_step=9000, variable="mDot" - ).ffill().plot( - ax=ax2, - drawstyle="steps-post", - label="neg", - linestyle="--", - color=mpcplot.EBCColors.red, - ) - mpc_at_time_step( - data=res.df_pos_flex, time_step=9000, variable="mDot" - ).ffill().plot( - ax=ax2, - drawstyle="steps-post", - label="pos", - linestyle="--", - color=mpcplot.EBCColors.blue, - ) - mpc_at_time_step( - data=res.df_baseline, time_step=9900, variable="mDot" - ).ffill().plot( - ax=ax2, - drawstyle="steps-post", - label="base", - linestyle="--", - color=mpcplot.EBCColors.dark_grey, - ) - ax2.legend() - ax2.vlines(9000, ymin=0, ymax=500, colors="black") - ax2.vlines(9900, ymin=0, ymax=500, colors="black") - ax2.vlines(10800, ymin=0, ymax=500, colors="black") - ax2.vlines(18000, ymin=0, ymax=500, colors="black") - - ax2.set_ylim(0, 0.06) - - x_ticks = np.arange(0, 3600 * 6 + 1, 3600) - x_tick_labels = [int(tick / 3600) for tick in x_ticks] - ax2.set_xticks(x_ticks) - ax2.set_xticklabels(x_tick_labels) - ax2.set_xlabel("Time in hours") - for ax in axs: - mpcplot.make_grid(ax) - ax.set_xlim(0, 3600 * 6) - - # flexibility - # get only the first prediction time of each time step - energy_flex_neg = res.df_indicator.xs("negative_energy_flex", axis=1).droplevel(1).dropna() - energy_flex_pos = res.df_indicator.xs("positive_energy_flex", axis=1).droplevel(1).dropna() - fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=1) - ax1 = axs[0] - ax1.set_ylabel(r"$\epsilon$ in kWh") - energy_flex_neg.plot(ax=ax1, label="neg") - energy_flex_pos.plot(ax=ax1, label="pos") - energy_flex_neg.plot(ax=ax1, label="neg", color=mpcplot.EBCColors.red) - energy_flex_pos.plot(ax=ax1, label="pos", color=mpcplot.EBCColors.blue) - - ax1.legend() - - x_ticks = np.arange(0, 3600 * 6 + 1, 3600) - x_tick_labels = [int(tick / 3600) for tick in x_ticks] - ax1.set_xticks(x_ticks) - ax1.set_xticklabels(x_tick_labels) - ax1.set_xlabel("Time in hours") - for ax in axs: - mpcplot.make_grid(ax) - ax.set_xlim(0, 3600 * 6) - diff --git a/Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json b/Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json deleted file mode 100644 index baa0c6ad..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "myPredictorAgent", - "modules": [ - { - "module_id": "Ag4Com", - "type": "local_broadcast" - }, - { - "module_id": "MyPredictor", - "type": { - "file": "predictor/simple_predictor.py", - "class_name": "PredictorModule" - }, - "parameters": [ - { - "name": "time_step", - "value": 900 - }, - { - "name": "prediction_horizon", - "value": 49 - } - ] - } - ] -} \ No newline at end of file diff --git a/Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py b/Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py deleted file mode 100644 index 9bb3a2ea..00000000 --- a/Examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py +++ /dev/null @@ -1,53 +0,0 @@ -import agentlib as al -import numpy as np -import pandas as pd -from agentlib.core import Agent -from typing import List -import json -import csv -from datetime import datetime - -class PredictorModuleConfig(al.BaseModuleConfig): - """Module that outputs a prediction of the heat load at a specified - interval.""" - outputs: al.AgentVariables = [ - al.AgentVariable( - name="r_pel", unit="ct/kWh", type="pd.Series", description="Weight for P_el in objective function" - ), - ] - parameters: al.AgentVariables = [ - al.AgentVariable( - name="time_step", value=900, description="Sampling time for prediction." - ), - al.AgentVariable( - name="prediction_horizon", - value=8, - description="Number of sampling points for prediction.", - ) - ] - - - shared_variable_fields:List[str] = ["outputs"] - - -class PredictorModule(al.BaseModule): - """Module that outputs a prediction of the heat load at a specified - interval.""" - - config: PredictorModuleConfig - - def register_callbacks(self): - pass - - def process(self): - while True: - sample_time = self.env.config.t_sample - ts = self.get("time_step").value - k = self.get("prediction_horizon").value - now = self.env.now - - grid = np.arange(now, now + k * ts + 1, sample_time) - p_traj = pd.Series([1 for i in grid], index=list(grid)) - self.set("r_pel", p_traj) - - yield self.env.timeout(sample_time) From 099df9fd56323f208e32b76b0b746bfecdd11e6b Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Tue, 24 Feb 2026 20:26:31 +0100 Subject: [PATCH 19/25] move example --- .../flexibility_agent_config.json | 61 ++++++ .../flex_configs/flexibility_market.json | 26 +++ .../main_one_room_flex.py | 50 +++++ .../mpc_and_sim/Trainer/ml_model.json | 1 + .../mpc_and_sim/simple_model.json | 98 ++++++++++ .../mpc_and_sim/simple_model.py | 118 +++++++++++ .../mpc_and_sim/simple_model_sim.py | 125 ++++++++++++ .../mpc_and_sim/simple_sim.json | 30 +++ .../OneRoom_SimpleLinRegMPC/plot_results.py | 185 ++++++++++++++++++ .../predictor/predictor_config.json | 26 +++ .../predictor/simple_predictor.py | 53 +++++ 11 files changed, 773 insertions(+) create mode 100644 examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json create mode 100644 examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json create mode 100644 examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py create mode 100644 examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json create mode 100644 examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json create mode 100644 examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py create mode 100644 examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py create mode 100644 examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json create mode 100644 examples/OneRoom_SimpleLinRegMPC/plot_results.py create mode 100644 examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json create mode 100644 examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py diff --git a/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json new file mode 100644 index 00000000..0aeb0fd7 --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json @@ -0,0 +1,61 @@ +{ + "prep_time": 900, + "flex_event_duration": 7200, + "market_time": 900, + "indicator_config": { + "agent_config": { + "id": "FlexibilityIndicator", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "FlexibilityIndicator", + "type": "agentlib_flexquant.flexibility_indicator", + "price_variable": "r_pel", + "parameters": [ + { + "name": "prep_time", + "value": 900 + }, + { + "name": "market_time", + "value": 900 + }, + { + "name": "flex_event_duration", + "value": 7200 + }, + { + "name": "time_step", + "value": 900 + }, + { + "name": "prediction_horizon", + "value": 48 + } + ] + } + ] + }, + "name_of_created_file": "indicator.json" + }, + "market_config": "flex_configs/flexibility_market.json", + "baseline_config_generator_data": { + "power_variable": "P_el", + "power_unit": "kW", + "profile_deviation_weight": 100 + }, + "shadow_mpc_config_generator_data": { + "weights": [{"name": "s_P", "value": 10}], + "pos_flex": { + "flex_cost_function": "sum([self.s_T * self.T_slack ** 2, self.s_P * self.P_el])" + }, + "neg_flex": { + "flex_cost_function": "sum([self.s_T * self.T_slack ** 2, -self.s_P * self.P_el])" + } + }, + "delete_files": false, + "overwrite_files": true +} \ No newline at end of file diff --git a/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json new file mode 100644 index 00000000..f724e15c --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_market.json @@ -0,0 +1,26 @@ +{ + "agent_config": { + "id": "FlexibilityMarket", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "FlexibilityMarket", + "type": "agentlib_flexquant.flexibility_market", + "market_specs": { + "type": "single", + "cooldown": 10, + "minimum_average_flex": 0, + "options": { + "offer_acceptance_time": 9000, + "direction": "positive" + } + } + } + ] + + }, + "name_of_created_file": "market.json" + } \ No newline at end of file diff --git a/examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py b/examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py new file mode 100644 index 00000000..feee5be3 --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/main_one_room_flex.py @@ -0,0 +1,50 @@ +import logging +from agentlib_flexquant.generate_flex_agents import FlexAgentGenerator +from agentlib.utils.multi_agent_system import LocalMASAgency +from agentlib_flexquant.utils.interactive import Dashboard, CustomBound +from plot_results import plot_results + +# Set the log-level +logging.basicConfig(level=logging.WARN) +until = 7200 + +ENV_CONFIG = {"rt": False, "factor": 0.01, "t_sample": 900} + + +def run_example(until=until, with_dashboard=False): + mpc_config = "mpc_and_sim/simple_model.json" + sim_config = "mpc_and_sim/simple_sim.json" + predictor_config = "predictor/predictor_config.json" + flex_config = "flex_configs/flexibility_agent_config.json" + agent_configs = [sim_config, predictor_config] + + config_list = FlexAgentGenerator( + flex_config=flex_config, mpc_agent_config=mpc_config + ).generate_flex_agents() + agent_configs.extend(config_list) + + mas = LocalMASAgency( + agent_configs=agent_configs, env=ENV_CONFIG, variable_logging=False + ) + + mas.run(until=until) + results = mas.get_results(cleanup=False) + + plot_results(results_data=results) # Alternative plotscript using matplotlib, + if with_dashboard: + Dashboard( + flex_config="flex_configs/flexibility_agent_config.json", + simulator_agent_config="mpc_and_sim/simple_sim.json", + results=results + ).show( + custom_bounds=CustomBound( + for_variable="T", + lb_name="T_lower", + ub_name="T_upper" + ) + ) + return results + + +if __name__ == "__main__": + run_example(until, with_dashboard=True) diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json new file mode 100644 index 00000000..976e0381 --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/Trainer/ml_model.json @@ -0,0 +1 @@ +{"dt":900.0,"input":{"mDot":{"name":"mDot","lag":3},"load":{"name":"load","lag":2},"T_in":{"name":"T_in","lag":1}},"output":{"T":{"name":"T","lag":2,"output_type":"difference","recursive":true}},"agentlib_mpc_hash":"5302c96","training_info":null,"model_type":"LinReg","parameters":{"coef":[[-98.39137379499294,-39.21682530199981,-0.5848461265173618,2.59314258954646e-8,6.637570493239764e-9,-0.503047937143595,-0.492892772393688,0.3401290832302948]],"intercept":[187.30625067405674],"n_features_in":8,"rank":6,"singular":[35.99361869005735,18.9771165622424,0.2417850576555883,0.21421375073807045,0.023474240917668355,1.309867755231031e-12,4.219908371740855e-19,2.1045347182571248e-19]}} \ No newline at end of file diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json new file mode 100644 index 00000000..f86feb28 --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json @@ -0,0 +1,98 @@ +{ + "id": "FlexModel", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "BaselineMPC", + "type": "agentlib_mpc.mpc", + "optimization_backend": { + "type": "casadi_ml", + "model": { + "type": { + "file": "mpc_and_sim/simple_model.py", + "class_name": "BaselineMPCModel" + }, + "ml_model_sources": ["mpc_and_sim/Trainer/ml_model.json"] + }, + "discretization_options": { + "method": "multiple_shooting" + }, + "solver": { + "name": "ipopt", + "options": { + "ipopt": { + "max_iter": 100, + "tol": 1e-4 + } + } + }, + "results_file": "results/mpc.csv", + "save_results": true, + "overwrite_result_file": true + }, + "time_step": 900, + "prediction_horizon": 48, + "set_outputs": true, + "parameters": [ + { + "name": "s_T", + "value": 250 + }, + { + "name": "r_mDot", + "value": 1 + } + ], + "inputs": [ + { + "name": "load", + "value": 150 + }, + { + "name": "T_upper", + "value": 294.15 + }, + { + "name": "T_lower", + "value": 292.15 + }, + { + "name": "T_in", + "value": 280.15 + } + ], + "outputs": [ + { + "name": "T_out", + "alias": "T_out" + }, + { + "name": "P_el", + "alias": "P_el" + }, + { + "name": "Time" + } + ], + "controls": [ + { + "name": "mDot", + "value": 0.02, + "ub": 0.05, + "lb": 0 + } + ], + "states": [ + { + "name": "T", + "value": 298.16, + "ub": 303.15, + "lb": 288.15 + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py new file mode 100644 index 00000000..21574e3e --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py @@ -0,0 +1,118 @@ +from agentlib_mpc.models.casadi_model import ( + CasadiModel, + CasadiInput, + CasadiState, + CasadiParameter, + CasadiOutput, + CasadiModelConfig, +) +from typing import List +from math import inf +from agentlib_mpc.models.casadi_ml_model import CasadiMLModel, CasadiMLModelConfig + + +class BaselineMPCModelConfig(CasadiMLModelConfig): + inputs: List[CasadiInput] = [ + # controls + CasadiInput( + name="mDot", value=0.0225, unit="kg/s", description="Air mass flow into zone" + ), + # disturbances + CasadiInput( + name="load", value=150, unit="W", description="Heat " "load into zone" + ), + CasadiInput( + name="T_in", value=280.15, unit="K", description="Inflow air temperature" + ), + # settings + CasadiInput( + name="T_upper", + value=294.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + CasadiInput( + name="T_lower", + value=292.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + ] + + states: List[CasadiState] = [ + # differential + CasadiState( + name="T", value=293.15, unit="K", description="Temperature of zone" + ), + # algebraic + # slack variables + CasadiState( + name="T_slack", + value=0, + unit="K", + description="Slack variable of temperature of zone", + ), + + ] + parameters: List[CasadiParameter] = [ + CasadiParameter( + name="cp", + value=1000, + unit="J/kg*K", + description="thermal capacity of the air", + ), + CasadiParameter( + name="C", value=100000, unit="J/K", description="thermal capacity of zone" + ), + CasadiParameter( + name="s_T", + value=1, + unit="-", + description="Weight for T in constraint function", + ), + CasadiParameter( + name="r_mDot", + value=1, + unit="-", + description="Weight for mDot in objective function", + ), + + ] + outputs: List[CasadiOutput] = [ + CasadiOutput(name="T_out", unit="K", description="Temperature of zone"), + CasadiOutput( + name="P_el", + unit="W", + description="The power input to the system", + ), + CasadiOutput(name="Time", unit="s", description="Test casadi time") + ] + +class BaselineMPCModel(CasadiMLModel): + config: BaselineMPCModelConfig + + def setup_system(self): + # Define ode + self.T_out.alg = self.T + self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in) / 1000 + self.Time.alg = self.time + + # Constraints: List[(lower bound, function, upper bound)] + self.constraints = [ + # soft constraints + (self.T_lower, self.T + self.T_slack, inf), + (-inf, self.T - self.T_slack, self.T_upper), + (0, self.T_slack, inf) + ] + # Objective function + objective = sum( + [ + self.r_mDot * self.mDot, + self.s_T * self.T_slack ** 2, + ] + ) + return objective + + + + diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py new file mode 100644 index 00000000..aa5af3c2 --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py @@ -0,0 +1,125 @@ +from agentlib_mpc.models.casadi_model import ( + CasadiModel, + CasadiInput, + CasadiState, + CasadiParameter, + CasadiOutput, + CasadiModelConfig, +) +from typing import List +from math import inf +from agentlib_mpc.models.casadi_ml_model import CasadiMLModel, CasadiMLModelConfig + + +class BaselineMPCModelConfig(CasadiModelConfig): + inputs: List[CasadiInput] = [ + # controls + CasadiInput( + name="mDot", value=0.0225, unit="kg/s", description="Air mass flow into zone" + ), + # disturbances + CasadiInput( + name="load", value=150, unit="W", description="Heat " "load into zone" + ), + CasadiInput( + name="T_in", value=280.15, unit="K", description="Inflow air temperature" + ), + # settings + CasadiInput( + name="T_upper", + value=294.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + CasadiInput( + name="T_lower", + value=292.15, + unit="K", + description="Upper boundary (soft) for T.", + ), + + ] + + states: List[CasadiState] = [ + CasadiState(name="t_sim", value=0, unit="sec", description="simulation time"), + + # differential + CasadiState( + name="T", value=293.15, unit="K", description="Temperature of zone" + ), + # algebraic + # slack variables + CasadiState( + name="T_slack", + value=0, + unit="K", + description="Slack variable of temperature of zone", + ), + + ] + + parameters: List[CasadiParameter] = [ + CasadiParameter( + name="cp", + value=1000, + unit="J/kg*K", + description="thermal capacity of the air", + ), + CasadiParameter( + name="C", value=100000, unit="J/K", description="thermal capacity of zone" + ), + CasadiParameter( + name="s_T", + value=1, + unit="-", + description="Weight for T in constraint function", + ), + CasadiParameter( + name="r_mDot", + value=1, + unit="-", + description="Weight for mDot in objective function", + ), + + ] + outputs: List[CasadiOutput] = [ + CasadiOutput(name="T_out", unit="K", description="Temperature of zone"), + CasadiOutput( + name="P_el", + unit="W", + description="The power input to the system", + ), + CasadiOutput(name="Time", unit="s", description="Test casadi time") + ] + +class BaselineMPCModel(CasadiModel): + config: BaselineMPCModelConfig + + def setup_system(self): + # Define ode + self.T.ode = ( + self.cp * self.mDot / self.C * (self.T_in - self.T) + self.load / self.C + ) + self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in)/1000 + self.Time.alg = self.time + + # Define ae + self.T_out.alg = self.T # math operation to get the symbolic variable + # Constraints: List[(lower bound, function, upper bound)] + self.constraints = [ + # soft constraints + (self.T_lower, self.T + self.T_slack, inf), + (-inf, self.T - self.T_slack, self.T_upper), + (0, self.T_slack, inf) + ] + # Objective function + objective = sum( + [ + self.r_mDot * self.mDot, + self.s_T * self.T_slack**2, + ] + ) + return objective + + + diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json new file mode 100644 index 00000000..cb1fada9 --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json @@ -0,0 +1,30 @@ +{ + "id": "SimAgent", + "modules": [ + { + "module_id": "Ag1Com", + "type": "local_broadcast" + }, + { + "module_id": "room", + "type": "simulator", + "model": { + "type": {"file": "mpc_and_sim/simple_model_sim.py", "class_name": "BaselineMPCModel"}, + "states": [{"name": "T", "value": 298}] + + }, + "t_sample": 10, + "update_inputs_on_callback": false, + "save_results": true, + "result_filename": "results/sim_room.csv", + "overwrite_result_file": true, + "outputs": [ + {"name": "T_out", "alias": "T"}, + {"name": "P_el","alias": "P_el_sim"} + ], + "inputs": [ + {"name": "mDot", "value": 0.02, "alias": "mDot"} + ] + } + ] +} \ No newline at end of file diff --git a/examples/OneRoom_SimpleLinRegMPC/plot_results.py b/examples/OneRoom_SimpleLinRegMPC/plot_results.py new file mode 100644 index 00000000..327a5052 --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/plot_results.py @@ -0,0 +1,185 @@ +import numpy as np +import matplotlib.pyplot as plt +import agentlib_mpc.utils.plotting.basic as mpcplot +from agentlib_mpc.utils.analysis import mpc_at_time_step +from agentlib_flexquant.data_structures.flex_results import Results + + +def plot_results(results_data: dict = None): + """ + Example how plotting with matplotlib and mpcplot from agentlib_mpc works + """ + if results_data is None: + res = Results( + flex_config="flex_configs/flexibility_agent_config.json", + simulator_agent_config="mpc_and_sim/simple_sim.json", + results="results" + ) + else: + res = Results( + flex_config="flex_configs/flexibility_agent_config.json", + simulator_agent_config="mpc_and_sim/simple_sim.json", + results=results_data + ) + + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=2) + (ax1, ax2) = axs + # load + ax1.set_ylabel(r"$\dot{Q}_{Room}$ in W") + res.df_simulation["load"].plot(ax=ax1) + # T_in + ax2.set_ylabel("$T_{in}$ in K") + res.df_simulation["T_in"].plot(ax=ax2) + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax2.set_xticks(x_ticks) + ax2.set_xticklabels(x_tick_labels) + ax2.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + + # room temp + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=1) + ax1 = axs[0] + # T out + ax1.set_ylabel("$T_{room}$ in K") + res.df_simulation["T_upper"].plot(ax=ax1, color="0.5") + res.df_simulation["T_lower"].plot(ax=ax1, color="0.5") + res.df_simulation["T_out"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) + mpc_at_time_step( + data=res.df_neg_flex, time_step=9000, variable="T" + ).plot(ax=ax1, label="neg", linestyle="--", color=mpcplot.EBCColors.red) + mpc_at_time_step( + data=res.df_pos_flex, time_step=9000, variable="T" + ).plot(ax=ax1, label="pos", linestyle="--", color=mpcplot.EBCColors.blue) + mpc_at_time_step( + data=res.df_baseline, time_step=9900, variable="T" + ).plot(ax=ax1, label="base", linestyle="--", color=mpcplot.EBCColors.dark_grey) + + ax1.legend() + ax1.vlines(9000, ymin=0, ymax=500, colors="black") + ax1.vlines(9900, ymin=0, ymax=500, colors="black") + ax1.vlines(10800, ymin=0, ymax=500, colors="black") + ax1.vlines(18000, ymin=0, ymax=500, colors="black") + + ax1.set_ylim(289, 299) + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax1.set_xticks(x_ticks) + ax1.set_xticklabels(x_tick_labels) + ax1.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + + # predictions + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=2) + (ax1, ax2) = axs + # P_el + ax1.set_ylabel("$P_{el}$ in kW") + res.df_simulation["P_el"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) + mpc_at_time_step( + data=res.df_neg_flex, time_step=9000, variable="P_el" + ).ffill().plot( + ax=ax1, + drawstyle="steps-post", + label="neg", + linestyle="--", + color=mpcplot.EBCColors.red, + ) + mpc_at_time_step( + data=res.df_pos_flex, time_step=9000, variable="P_el" + ).ffill().plot( + ax=ax1, + drawstyle="steps-post", + label="pos", + linestyle="--", + color=mpcplot.EBCColors.blue, + ) + mpc_at_time_step( + data=res.df_baseline, time_step=9000, variable="P_el" + ).ffill().plot( + ax=ax1, + drawstyle="steps-post", + label="base", + linestyle="--", + color=mpcplot.EBCColors.dark_grey, + ) + ax1.legend() + ax1.vlines(9000, ymin=-1000, ymax=5000, colors="black") + ax1.vlines(9900, ymin=-1000, ymax=5000, colors="black") + ax1.vlines(10800, ymin=-1000, ymax=5000, colors="black") + ax1.vlines(18000, ymin=-1000, ymax=5000, colors="black") + ax1.set_ylim(-0.1, 1) + + # mdot + ax2.set_ylabel(r"$\dot{m}$ in kg/s") + res.df_simulation["mDot"].plot(ax=ax2, color=mpcplot.EBCColors.dark_grey) + mpc_at_time_step( + data=res.df_neg_flex, time_step=9000, variable="mDot" + ).ffill().plot( + ax=ax2, + drawstyle="steps-post", + label="neg", + linestyle="--", + color=mpcplot.EBCColors.red, + ) + mpc_at_time_step( + data=res.df_pos_flex, time_step=9000, variable="mDot" + ).ffill().plot( + ax=ax2, + drawstyle="steps-post", + label="pos", + linestyle="--", + color=mpcplot.EBCColors.blue, + ) + mpc_at_time_step( + data=res.df_baseline, time_step=9900, variable="mDot" + ).ffill().plot( + ax=ax2, + drawstyle="steps-post", + label="base", + linestyle="--", + color=mpcplot.EBCColors.dark_grey, + ) + ax2.legend() + ax2.vlines(9000, ymin=0, ymax=500, colors="black") + ax2.vlines(9900, ymin=0, ymax=500, colors="black") + ax2.vlines(10800, ymin=0, ymax=500, colors="black") + ax2.vlines(18000, ymin=0, ymax=500, colors="black") + + ax2.set_ylim(0, 0.06) + + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax2.set_xticks(x_ticks) + ax2.set_xticklabels(x_tick_labels) + ax2.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + + # flexibility + # get only the first prediction time of each time step + energy_flex_neg = res.df_indicator.xs("negative_energy_flex", axis=1).droplevel(1).dropna() + energy_flex_pos = res.df_indicator.xs("positive_energy_flex", axis=1).droplevel(1).dropna() + fig, axs = mpcplot.make_fig(style=mpcplot.Style(use_tex=False), rows=1) + ax1 = axs[0] + ax1.set_ylabel(r"$\epsilon$ in kWh") + energy_flex_neg.plot(ax=ax1, label="neg") + energy_flex_pos.plot(ax=ax1, label="pos") + energy_flex_neg.plot(ax=ax1, label="neg", color=mpcplot.EBCColors.red) + energy_flex_pos.plot(ax=ax1, label="pos", color=mpcplot.EBCColors.blue) + + ax1.legend() + + x_ticks = np.arange(0, 3600 * 6 + 1, 3600) + x_tick_labels = [int(tick / 3600) for tick in x_ticks] + ax1.set_xticks(x_ticks) + ax1.set_xticklabels(x_tick_labels) + ax1.set_xlabel("Time in hours") + for ax in axs: + mpcplot.make_grid(ax) + ax.set_xlim(0, 3600 * 6) + diff --git a/examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json b/examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json new file mode 100644 index 00000000..baa0c6ad --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/predictor/predictor_config.json @@ -0,0 +1,26 @@ +{ + "id": "myPredictorAgent", + "modules": [ + { + "module_id": "Ag4Com", + "type": "local_broadcast" + }, + { + "module_id": "MyPredictor", + "type": { + "file": "predictor/simple_predictor.py", + "class_name": "PredictorModule" + }, + "parameters": [ + { + "name": "time_step", + "value": 900 + }, + { + "name": "prediction_horizon", + "value": 49 + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py b/examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py new file mode 100644 index 00000000..9bb3a2ea --- /dev/null +++ b/examples/OneRoom_SimpleLinRegMPC/predictor/simple_predictor.py @@ -0,0 +1,53 @@ +import agentlib as al +import numpy as np +import pandas as pd +from agentlib.core import Agent +from typing import List +import json +import csv +from datetime import datetime + +class PredictorModuleConfig(al.BaseModuleConfig): + """Module that outputs a prediction of the heat load at a specified + interval.""" + outputs: al.AgentVariables = [ + al.AgentVariable( + name="r_pel", unit="ct/kWh", type="pd.Series", description="Weight for P_el in objective function" + ), + ] + parameters: al.AgentVariables = [ + al.AgentVariable( + name="time_step", value=900, description="Sampling time for prediction." + ), + al.AgentVariable( + name="prediction_horizon", + value=8, + description="Number of sampling points for prediction.", + ) + ] + + + shared_variable_fields:List[str] = ["outputs"] + + +class PredictorModule(al.BaseModule): + """Module that outputs a prediction of the heat load at a specified + interval.""" + + config: PredictorModuleConfig + + def register_callbacks(self): + pass + + def process(self): + while True: + sample_time = self.env.config.t_sample + ts = self.get("time_step").value + k = self.get("prediction_horizon").value + now = self.env.now + + grid = np.arange(now, now + k * ts + 1, sample_time) + p_traj = pd.Series([1 for i in grid], index=list(grid)) + self.set("r_pel", p_traj) + + yield self.env.timeout(sample_time) From 6ea896c3cda6cac4b0ed770cb03c3255bfa3b304 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Wed, 25 Feb 2026 12:28:13 +0100 Subject: [PATCH 20/25] adjust snapshots of ci tests for new time grid formulation add snapshots for linreg example --- .../data_structures/flex_kpis.py | 2 +- .../mpc_and_sim/simple_model.json | 3 - .../mpc_and_sim/simple_model.py | 4 +- .../mpc_and_sim/simple_model_sim.py | 21 +- .../mpc_and_sim/simple_sim.json | 4 +- .../mpc_and_sim/simple_model.json | 4 - .../mpc_and_sim/simple_model.py | 22 +- requirements.txt | 1 - setup.py | 3 + .../oneroom_cia_indicator_summary.json | 26 +- .../SimpleBuilding_indicator_summary.json | 26 +- .../oneroom_simpleMPC_baseline_summary.json | 1048 ++++++++++++++++ .../oneroom_simpleMPC_indicator_summary.json | 873 +++++++++++++ .../oneroom_simpleMPC_neg_flex_summary.json | 1080 +++++++++++++++++ .../oneroom_simpleMPC_pos_flex_summary.json | 1080 +++++++++++++++++ .../oneroom_simpleMPC_indicator_summary.json | 26 +- tests/test_oneRoom_SimpleLinRegMPC.py | 28 + 17 files changed, 4167 insertions(+), 84 deletions(-) create mode 100644 tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_baseline_summary.json create mode 100644 tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json create mode 100644 tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_neg_flex_summary.json create mode 100644 tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_pos_flex_summary.json diff --git a/agentlib_flexquant/data_structures/flex_kpis.py b/agentlib_flexquant/data_structures/flex_kpis.py index 670f138e..3ffa1337 100644 --- a/agentlib_flexquant/data_structures/flex_kpis.py +++ b/agentlib_flexquant/data_structures/flex_kpis.py @@ -106,7 +106,7 @@ def integrate(self, time_unit: TimeConversionTypes = "seconds") -> float: if self.integration_method == LINEAR: # Linear integration: apply the trapezoidal rule, which assumes # the function changes linearly between sample points - return np.trapz(self.value.values, + return np.trapezoid(self.value.values, self.value.index) / TIME_CONVERSION[time_unit] if self.integration_method == CONSTANT: # Constant integration: use a step-wise constant approach by diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json index f86feb28..7ea32d87 100644 --- a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.json @@ -72,9 +72,6 @@ { "name": "P_el", "alias": "P_el" - }, - { - "name": "Time" } ], "controls": [ diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py index 21574e3e..300f120f 100644 --- a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model.py @@ -84,8 +84,7 @@ class BaselineMPCModelConfig(CasadiMLModelConfig): name="P_el", unit="W", description="The power input to the system", - ), - CasadiOutput(name="Time", unit="s", description="Test casadi time") + ) ] class BaselineMPCModel(CasadiMLModel): @@ -95,7 +94,6 @@ def setup_system(self): # Define ode self.T_out.alg = self.T self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in) / 1000 - self.Time.alg = self.time # Constraints: List[(lower bound, function, upper bound)] self.constraints = [ diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py index aa5af3c2..88694d31 100644 --- a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_model_sim.py @@ -8,7 +8,6 @@ ) from typing import List from math import inf -from agentlib_mpc.models.casadi_ml_model import CasadiMLModel, CasadiMLModelConfig class BaselineMPCModelConfig(CasadiModelConfig): @@ -88,8 +87,7 @@ class BaselineMPCModelConfig(CasadiModelConfig): name="P_el", unit="W", description="The power input to the system", - ), - CasadiOutput(name="Time", unit="s", description="Test casadi time") + ) ] class BaselineMPCModel(CasadiModel): @@ -101,25 +99,10 @@ def setup_system(self): self.cp * self.mDot / self.C * (self.T_in - self.T) + self.load / self.C ) self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in)/1000 - self.Time.alg = self.time # Define ae self.T_out.alg = self.T # math operation to get the symbolic variable - # Constraints: List[(lower bound, function, upper bound)] - self.constraints = [ - # soft constraints - (self.T_lower, self.T + self.T_slack, inf), - (-inf, self.T - self.T_slack, self.T_upper), - (0, self.T_slack, inf) - ] - # Objective function - objective = sum( - [ - self.r_mDot * self.mDot, - self.s_T * self.T_slack**2, - ] - ) - return objective + diff --git a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json index cb1fada9..91545afb 100644 --- a/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json +++ b/examples/OneRoom_SimpleLinRegMPC/mpc_and_sim/simple_sim.json @@ -13,8 +13,8 @@ "states": [{"name": "T", "value": 298}] }, - "t_sample": 10, - "update_inputs_on_callback": false, + "t_sample_communication": 10, + "t_sample_simulation": 10, "save_results": true, "result_filename": "results/sim_room.csv", "overwrite_result_file": true, diff --git a/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json b/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json index 36abe29a..3a2ea4f6 100644 --- a/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json +++ b/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.json @@ -76,10 +76,6 @@ { "name": "E_out", "alias": "E_out" - }, -{ - "name": "Time" - } ], "controls": [ diff --git a/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py b/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py index d06df1c1..d55f0358 100644 --- a/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py +++ b/examples/OneRoom_SimpleMPC/mpc_and_sim/simple_model.py @@ -51,7 +51,7 @@ class BaselineMPCModelConfig(CasadiModelConfig): unit="K", description="Slack variable of temperature of zone", ), - + ] parameters: list[CasadiParameter] = [ @@ -85,25 +85,23 @@ class BaselineMPCModelConfig(CasadiModelConfig): name="P_el", unit="W", description="The power input to the system", - ), - CasadiOutput(name="Time", unit="s", description="Test casadi time") + ) ] class BaselineMPCModel(CasadiModel): config: BaselineMPCModelConfig - + def setup_system(self): # Define ode self.T.ode = ( - self.cp * self.mDot / self.C * (self.T_in - self.T) + self.load / self.C + self.cp * self.mDot / self.C * (self.T_in - self.T) + self.load / self.C ) - self.Time.alg = self.time # Define ae self.P_el.alg = self.cp * self.mDot * (self.T - self.T_in) / 1000 self.T_out.alg = self.T # math operation to get the symbolic variable - self.E_out.alg = - self.T * self.C / (3600*1000) # stored electrical energy in kWh + self.E_out.alg = - self.T * self.C / (3600 * 1000) # stored electrical energy in kWh # Constraints: list[(lower bound, function, upper bound)] self.constraints = [ @@ -115,9 +113,9 @@ def setup_system(self): # Objective function objective = sum( - [ - self.r_mDot * self.mDot, - self.s_T * self.T_slack**2, - ] - ) + [ + self.r_mDot * self.mDot, + self.s_T * self.T_slack ** 2, + ] + ) return objective diff --git a/requirements.txt b/requirements.txt index 7a7e5c8a..436075db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ pathlib astor==0.8.1 black pre-commit -numpy >=1.26.4 # Building the docs sphinx>=6.1.3 diff --git a/setup.py b/setup.py index 484153c8..0849418f 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,9 @@ author="", author_email="", description="Flexibility quantification setup based on agentlib_mpc", + extras_require={ + 'ml': ['agentlib_mpc[ml] @ git+https://github.com/RWTH-EBC/AgentLib-MPC.git@quickfix-custom-objectives'], + }, packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3.8", diff --git a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json index e809ccd1..1c9ee9f5 100644 --- a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json +++ b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json @@ -33,7 +33,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "head_5_rows": { @@ -71,7 +71,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ @@ -351,16 +351,6 @@ "min": 0.0, "std": 151.3 }, - "collocation_time_grid": { - "25%": 1231.7, - "50%": 2400.0, - "75%": 3568.3, - "count": 384.0, - "max": 4736.6, - "mean": 2400.0, - "min": 63.4, - "std": 1387.4 - }, "flex_event_duration": { "25%": 2400.0, "50%": 2400.0, @@ -611,6 +601,16 @@ "min": 10.0, "std": 0.0 }, + "time_grid_info": { + "25%": 1231.7, + "50%": 2400.0, + "75%": 3568.3, + "count": 384.0, + "max": 4736.6, + "mean": 2400.0, + "min": 63.4, + "std": 1387.4 + }, "time_step_mpc": { "25%": 300.0, "50%": 300.0, @@ -657,7 +657,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ diff --git a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json index 9b08ebe2..4860ae51 100644 --- a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json +++ b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json @@ -33,7 +33,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "head_5_rows": { @@ -71,7 +71,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ @@ -351,16 +351,6 @@ "min": 0.0, "std": 24.10322 }, - "collocation_time_grid": { - "25%": 10895.09619, - "50%": 21600.0, - "75%": 32304.90381, - "count": 384.0, - "max": 43009.80762, - "mean": 21600.0, - "min": 190.19238, - "std": 12487.03557 - }, "flex_event_duration": { "25%": 7200.0, "50%": 7200.0, @@ -611,6 +601,16 @@ "min": 1.0, "std": 0.0 }, + "time_grid_info": { + "25%": 10895.09619, + "50%": 21600.0, + "75%": 32304.90381, + "count": 384.0, + "max": 43009.80762, + "mean": 21600.0, + "min": 190.19238, + "std": 12487.03557 + }, "time_step_mpc": { "25%": 900.0, "50%": 900.0, @@ -657,7 +657,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ diff --git a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_baseline_summary.json b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_baseline_summary.json new file mode 100644 index 00000000..32df27cf --- /dev/null +++ b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_baseline_summary.json @@ -0,0 +1,1048 @@ +{ + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "_P_external" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "rel_start" + ], + [ + "parameter", + "rel_end" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "profile_deviation_weight" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "head_5_rows": { + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "_P_external" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "rel_start" + ], + [ + "parameter", + "rel_end" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "profile_deviation_weight" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "data": [ + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + 298.16, + 0.02, + 298.16, + 303.15, + 288.15, + 0.02, + 0.05, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + 298.16, + 0.02, + 298.16, + 303.15, + 288.15, + 0.02, + 0.05, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + 1000.0, + 100000.0, + 250.0, + 1.0, + 100.0, + 298.16, + NaN, + 298.16, + 303.15, + 288.15, + 0.0411, + 0.05, + 0.0, + 4.01, + Infinity, + -Infinity, + 298.16, + 0.7401, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.012, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1683, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0096, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1347, + Infinity, + Infinity, + -Infinity, + -Infinity + ] + ], + "index": [ + [ + 0.0, + -1800.0 + ], + [ + 0.0, + -900.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 900.0 + ], + [ + 0.0, + 1800.0 + ] + ] + }, + "index_end": "(2700.0, 43200.0)", + "index_start": "(0.0, -1800.0)", + "shape": [ + 204, + 30 + ], + "statistics": { + "lower.P_el": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.T": { + "25%": 288.15, + "50%": 288.15, + "75%": 288.15, + "count": 204.0, + "max": 288.15, + "mean": 288.15, + "min": 288.15, + "std": 0.0 + }, + "lower.T_out": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.T_slack": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.mDot": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.C": { + "25%": 100000.0, + "50%": 100000.0, + "75%": 100000.0, + "count": 4.0, + "max": 100000.0, + "mean": 100000.0, + "min": 100000.0, + "std": 0.0 + }, + "parameter.T": { + "25%": 293.8146, + "50%": 296.1162, + "75%": 298.0245, + "count": 12.0, + "max": 298.16, + "mean": 295.9453, + "min": 293.636, + "std": 2.2255 + }, + "parameter.T_in": { + "25%": 280.15, + "50%": 280.15, + "75%": 280.15, + "count": 200.0, + "max": 280.15, + "mean": 280.15, + "min": 280.15, + "std": 0.0 + }, + "parameter.T_lower": { + "25%": 292.15, + "50%": 292.15, + "75%": 292.15, + "count": 200.0, + "max": 292.15, + "mean": 292.15, + "min": 292.15, + "std": 0.0 + }, + "parameter.T_upper": { + "25%": 294.15, + "50%": 294.15, + "75%": 294.15, + "count": 200.0, + "max": 294.15, + "mean": 294.15, + "min": 294.15, + "std": 0.0 + }, + "parameter._P_external": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.cp": { + "25%": 1000.0, + "50%": 1000.0, + "75%": 1000.0, + "count": 4.0, + "max": 1000.0, + "mean": 1000.0, + "min": 1000.0, + "std": 0.0 + }, + "parameter.in_provision": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.load": { + "25%": 150.0, + "50%": 150.0, + "75%": 150.0, + "count": 200.0, + "max": 150.0, + "mean": 150.0, + "min": 150.0, + "std": 0.0 + }, + "parameter.mDot": { + "25%": 0.0088, + "50%": 0.02, + "75%": 0.0411, + "count": 8.0, + "max": 0.0411, + "mean": 0.0236, + "min": 0.0076, + "std": 0.0153 + }, + "parameter.profile_deviation_weight": { + "25%": 100.0, + "50%": 100.0, + "75%": 100.0, + "count": 4.0, + "max": 100.0, + "mean": 100.0, + "min": 100.0, + "std": 0.0 + }, + "parameter.r_mDot": { + "25%": 1.0, + "50%": 1.0, + "75%": 1.0, + "count": 4.0, + "max": 1.0, + "mean": 1.0, + "min": 1.0, + "std": 0.0 + }, + "parameter.rel_end": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.rel_start": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.s_T": { + "25%": 250.0, + "50%": 250.0, + "75%": 250.0, + "count": 4.0, + "max": 250.0, + "mean": 250.0, + "min": 250.0, + "std": 0.0 + }, + "upper.P_el": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.T": { + "25%": 303.15, + "50%": 303.15, + "75%": 303.15, + "count": 204.0, + "max": 303.15, + "mean": 303.15, + "min": 303.15, + "std": 0.0 + }, + "upper.T_out": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.T_slack": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.mDot": { + "25%": 0.05, + "50%": 0.05, + "75%": 0.05, + "count": 200.0, + "max": 0.05, + "mean": 0.05, + "min": 0.05, + "std": 0.0 + }, + "variable.P_el": { + "25%": 0.1461, + "50%": 0.1461, + "75%": 0.1461, + "count": 192.0, + "max": 0.7401, + "mean": 0.1459, + "min": 0.0, + "std": 0.0481 + }, + "variable.T": { + "25%": 294.15, + "50%": 294.15, + "75%": 294.15, + "count": 204.0, + "max": 298.16, + "mean": 294.2758, + "min": 293.636, + "std": 0.681 + }, + "variable.T_out": { + "25%": 294.15, + "50%": 294.15, + "75%": 294.15, + "count": 192.0, + "max": 298.16, + "mean": 294.1675, + "min": 293.6765, + "std": 0.2925 + }, + "variable.T_slack": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0001, + "count": 192.0, + "max": 4.01, + "mean": 0.0215, + "min": 0.0, + "std": 0.2895 + }, + "variable.mDot": { + "25%": 0.0104, + "50%": 0.0104, + "75%": 0.0104, + "count": 200.0, + "max": 0.0411, + "mean": 0.0109, + "min": 0.0, + "std": 0.0047 + } + }, + "tail_5_rows": { + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "_P_external" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "rel_start" + ], + [ + "parameter", + "rel_end" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "profile_deviation_weight" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "data": [ + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.0, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 295.1767, + 303.15, + 288.15, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ] + ], + "index": [ + [ + 2700.0, + 39600.0 + ], + [ + 2700.0, + 40500.0 + ], + [ + 2700.0, + 41400.0 + ], + [ + 2700.0, + 42300.0 + ], + [ + 2700.0, + 43200.0 + ] + ] + } +} \ No newline at end of file diff --git a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json new file mode 100644 index 00000000..9684577d --- /dev/null +++ b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json @@ -0,0 +1,873 @@ +{ + "columns": [ + "_P_el_base", + "_P_el_neg", + "_P_el_pos", + "_E_stored_base", + "_E_stored_neg", + "_E_stored_pos", + "r_pel", + "negative_power_flex_full", + "positive_power_flex_full", + "negative_power_flex_offer", + "positive_power_flex_offer", + "negative_power_flex_offer_min", + "positive_power_flex_offer_min", + "negative_power_flex_offer_max", + "positive_power_flex_offer_max", + "negative_power_flex_offer_avg", + "positive_power_flex_offer_avg", + "negative_power_flex_within_boundary", + "positive_power_flex_within_boundary", + "negative_energy_flex", + "positive_energy_flex", + "negative_costs", + "positive_costs", + "negative_corrected_costs", + "positive_corrected_costs", + "negative_costs_rel", + "positive_costs_rel", + "negative_corrected_costs_rel", + "positive_corrected_costs_rel", + "prep_time", + "market_time", + "flex_event_duration", + "prediction_horizon", + "time_grid_info", + "time_step_mpc" + ], + "head_5_rows": { + "columns": [ + "_P_el_base", + "_P_el_neg", + "_P_el_pos", + "_E_stored_base", + "_E_stored_neg", + "_E_stored_pos", + "r_pel", + "negative_power_flex_full", + "positive_power_flex_full", + "negative_power_flex_offer", + "positive_power_flex_offer", + "negative_power_flex_offer_min", + "positive_power_flex_offer_min", + "negative_power_flex_offer_max", + "positive_power_flex_offer_max", + "negative_power_flex_offer_avg", + "positive_power_flex_offer_avg", + "negative_power_flex_within_boundary", + "positive_power_flex_within_boundary", + "negative_energy_flex", + "positive_energy_flex", + "negative_costs", + "positive_costs", + "negative_corrected_costs", + "positive_corrected_costs", + "negative_costs_rel", + "positive_costs_rel", + "negative_corrected_costs_rel", + "positive_corrected_costs_rel", + "prep_time", + "market_time", + "flex_event_duration", + "prediction_horizon", + "time_grid_info", + "time_step_mpc" + ], + "data": [ + [ + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 1.0, + 0.0, + 0.0, + NaN, + NaN, + -0.1505, + 0.0, + 0.287, + 0.1146, + 0.0198, + 0.0234, + true, + true, + 0.0396, + 0.0467, + 0.0327, + 0.0102, + 0.0327, + 0.0102, + 0.8254, + 0.2172, + 0.8254, + 0.2172, + 900.0, + 900.0, + 7200.0, + 48.0, + 0.0, + 900.0 + ], + [ + 0.1683, + 0.1678, + 0.4532, + NaN, + NaN, + NaN, + 1.0, + 0.0, + -0.2849, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 900.0, + NaN + ], + [ + 0.1347, + 0.4198, + 0.0201, + NaN, + NaN, + NaN, + 1.0, + 0.2851, + 0.1146, + 0.2851, + 0.1146, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 1800.0, + NaN + ], + [ + 0.1505, + 0.0, + 0.0593, + NaN, + NaN, + NaN, + 1.0, + -0.1505, + 0.0912, + -0.1505, + 0.0912, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 2700.0, + NaN + ], + [ + 0.1444, + 0.318, + 0.1059, + NaN, + NaN, + NaN, + 1.0, + 0.1736, + 0.0385, + 0.1736, + 0.0385, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 3600.0, + NaN + ] + ], + "index": [ + [ + 0.0, + 0.0 + ], + [ + 0.0, + 900.0 + ], + [ + 0.0, + 1800.0 + ], + [ + 0.0, + 2700.0 + ], + [ + 0.0, + 3600.0 + ] + ] + }, + "index_end": "(2700.0, 44100.0)", + "index_start": "(0.0, 0.0)", + "shape": [ + 200, + 35 + ], + "statistics": { + "_E_stored_base": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 0.0, + "max": NaN, + "mean": NaN, + "min": NaN, + "std": NaN + }, + "_E_stored_neg": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 0.0, + "max": NaN, + "mean": NaN, + "min": NaN, + "std": NaN + }, + "_E_stored_pos": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 0.0, + "max": NaN, + "mean": NaN, + "min": NaN, + "std": NaN + }, + "_P_el_base": { + "25%": 0.1461, + "50%": 0.1461, + "75%": 0.1461, + "count": 188.0, + "max": 0.1683, + "mean": 0.143, + "min": 0.0, + "std": 0.0213 + }, + "_P_el_neg": { + "25%": 0.1461, + "50%": 0.1461, + "75%": 0.1461, + "count": 188.0, + "max": 0.4335, + "mean": 0.1452, + "min": 0.0, + "std": 0.0872 + }, + "_P_el_pos": { + "25%": 0.1461, + "50%": 0.1461, + "75%": 0.1461, + "count": 188.0, + "max": 0.4532, + "mean": 0.1432, + "min": 0.0, + "std": 0.0501 + }, + "flex_event_duration": { + "25%": 7200.0, + "50%": 7200.0, + "75%": 7200.0, + "count": 4.0, + "max": 7200.0, + "mean": 7200.0, + "min": 7200.0, + "std": 0.0 + }, + "market_time": { + "25%": 900.0, + "50%": 900.0, + "75%": 900.0, + "count": 4.0, + "max": 900.0, + "mean": 900.0, + "min": 900.0, + "std": 0.0 + }, + "negative_corrected_costs": { + "25%": 0.026, + "50%": 0.0325, + "75%": 0.0327, + "count": 4.0, + "max": 0.033, + "mean": 0.0263, + "min": 0.0071, + "std": 0.0128 + }, + "negative_corrected_costs_rel": { + "25%": 0.6538, + "50%": 0.8155, + "75%": 0.8232, + "count": 4.0, + "max": 0.8254, + "mean": 0.6615, + "min": 0.1896, + "std": 0.3147 + }, + "negative_costs": { + "25%": 0.026, + "50%": 0.0325, + "75%": 0.0327, + "count": 4.0, + "max": 0.033, + "mean": 0.0263, + "min": 0.0071, + "std": 0.0128 + }, + "negative_costs_rel": { + "25%": 0.6538, + "50%": 0.8155, + "75%": 0.8232, + "count": 4.0, + "max": 0.8254, + "mean": 0.6615, + "min": 0.1896, + "std": 0.3147 + }, + "negative_energy_flex": { + "25%": 0.0391, + "50%": 0.0398, + "75%": 0.04, + "count": 4.0, + "max": 0.0401, + "mean": 0.0393, + "min": 0.0376, + "std": 0.0011 + }, + "negative_power_flex_full": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 192.0, + "max": 0.2874, + "mean": 0.0022, + "min": -0.1505, + "std": 0.0837 + }, + "negative_power_flex_offer": { + "25%": -0.1461, + "50%": -0.099, + "75%": 0.2603, + "count": 36.0, + "max": 0.2874, + "mean": 0.0248, + "min": -0.1505, + "std": 0.1905 + }, + "negative_power_flex_offer_avg": { + "25%": 0.0195, + "50%": 0.0199, + "75%": 0.02, + "count": 4.0, + "max": 0.02, + "mean": 0.0197, + "min": 0.0188, + "std": 0.0006 + }, + "negative_power_flex_offer_max": { + "25%": 0.2873, + "50%": 0.2874, + "75%": 0.2874, + "count": 4.0, + "max": 0.2874, + "mean": 0.2873, + "min": 0.287, + "std": 0.0002 + }, + "negative_power_flex_offer_min": { + "25%": -0.1474, + "50%": -0.1463, + "75%": -0.1462, + "count": 4.0, + "max": -0.1461, + "mean": -0.1473, + "min": -0.1505, + "std": 0.0021 + }, + "positive_corrected_costs": { + "25%": 0.0021, + "50%": 0.0099, + "75%": 0.0102, + "count": 4.0, + "max": 0.0104, + "mean": 0.0024, + "min": -0.0204, + "std": 0.0152 + }, + "positive_corrected_costs_rel": { + "25%": 0.0487, + "50%": 0.2113, + "75%": 0.2189, + "count": 4.0, + "max": 0.2243, + "mean": 0.0564, + "min": -0.4214, + "std": 0.3186 + }, + "positive_costs": { + "25%": 0.0021, + "50%": 0.0099, + "75%": 0.0102, + "count": 4.0, + "max": 0.0104, + "mean": 0.0024, + "min": -0.0204, + "std": 0.0152 + }, + "positive_costs_rel": { + "25%": 0.0487, + "50%": 0.2113, + "75%": 0.2189, + "count": 4.0, + "max": 0.2243, + "mean": 0.0564, + "min": -0.4214, + "std": 0.3186 + }, + "positive_energy_flex": { + "25%": 0.0465, + "50%": 0.0466, + "75%": 0.0472, + "count": 4.0, + "max": 0.0484, + "mean": 0.0471, + "min": 0.0465, + "std": 0.0009 + }, + "positive_power_flex_full": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 192.0, + "max": 0.142, + "mean": -0.0002, + "min": -0.2866, + "std": 0.0443 + }, + "positive_power_flex_offer": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.04, + "count": 36.0, + "max": 0.142, + "mean": 0.0278, + "min": -0.0026, + "std": 0.0449 + }, + "positive_power_flex_offer_avg": { + "25%": 0.0233, + "50%": 0.0233, + "75%": 0.0236, + "count": 4.0, + "max": 0.0242, + "mean": 0.0235, + "min": 0.0233, + "std": 0.0005 + }, + "positive_power_flex_offer_max": { + "25%": 0.117, + "50%": 0.1179, + "75%": 0.124, + "count": 4.0, + "max": 0.142, + "mean": 0.1231, + "min": 0.1146, + "std": 0.0127 + }, + "positive_power_flex_offer_min": { + "25%": -0.0006, + "50%": 0.0, + "75%": 0.0, + "count": 4.0, + "max": 0.0, + "mean": -0.0006, + "min": -0.0026, + "std": 0.0013 + }, + "prediction_horizon": { + "25%": 48.0, + "50%": 48.0, + "75%": 48.0, + "count": 4.0, + "max": 48.0, + "mean": 48.0, + "min": 48.0, + "std": 0.0 + }, + "prep_time": { + "25%": 900.0, + "50%": 900.0, + "75%": 900.0, + "count": 4.0, + "max": 900.0, + "mean": 900.0, + "min": 900.0, + "std": 0.0 + }, + "r_pel": { + "25%": 1.0, + "50%": 1.0, + "75%": 1.0, + "count": 200.0, + "max": 1.0, + "mean": 1.0, + "min": 1.0, + "std": 0.0 + }, + "time_grid_info": { + "25%": 10800.0, + "50%": 21600.0, + "75%": 32400.0, + "count": 196.0, + "max": 43200.0, + "mean": 21600.0, + "min": 0.0, + "std": 12760.516 + }, + "time_step_mpc": { + "25%": 900.0, + "50%": 900.0, + "75%": 900.0, + "count": 4.0, + "max": 900.0, + "mean": 900.0, + "min": 900.0, + "std": 0.0 + } + }, + "tail_5_rows": { + "columns": [ + "_P_el_base", + "_P_el_neg", + "_P_el_pos", + "_E_stored_base", + "_E_stored_neg", + "_E_stored_pos", + "r_pel", + "negative_power_flex_full", + "positive_power_flex_full", + "negative_power_flex_offer", + "positive_power_flex_offer", + "negative_power_flex_offer_min", + "positive_power_flex_offer_min", + "negative_power_flex_offer_max", + "positive_power_flex_offer_max", + "negative_power_flex_offer_avg", + "positive_power_flex_offer_avg", + "negative_power_flex_within_boundary", + "positive_power_flex_within_boundary", + "negative_energy_flex", + "positive_energy_flex", + "negative_costs", + "positive_costs", + "negative_corrected_costs", + "positive_corrected_costs", + "negative_costs_rel", + "positive_costs_rel", + "negative_corrected_costs_rel", + "positive_corrected_costs_rel", + "prep_time", + "market_time", + "flex_event_duration", + "prediction_horizon", + "time_grid_info", + "time_step_mpc" + ], + "data": [ + [ + 0.1461, + 0.1461, + 0.1461, + NaN, + NaN, + NaN, + 1.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 40500.0, + NaN + ], + [ + 0.1461, + 0.1461, + 0.1461, + NaN, + NaN, + NaN, + 1.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 41400.0, + NaN + ], + [ + 0.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + 1.0, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 42300.0, + NaN + ], + [ + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 1.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 43200.0, + NaN + ], + [ + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 1.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ] + ], + "index": [ + [ + 2700.0, + 40500.0 + ], + [ + 2700.0, + 41400.0 + ], + [ + 2700.0, + 42300.0 + ], + [ + 2700.0, + 43200.0 + ], + [ + 2700.0, + 44100.0 + ] + ] + } +} \ No newline at end of file diff --git a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_neg_flex_summary.json b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_neg_flex_summary.json new file mode 100644 index 00000000..7068732b --- /dev/null +++ b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_neg_flex_summary.json @@ -0,0 +1,1080 @@ +{ + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "mDot_full" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "prep_time" + ], + [ + "parameter", + "flex_event_duration" + ], + [ + "parameter", + "market_time" + ], + [ + "parameter", + "s_P" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "head_5_rows": { + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "mDot_full" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "prep_time" + ], + [ + "parameter", + "flex_event_duration" + ], + [ + "parameter", + "market_time" + ], + [ + "parameter", + "s_P" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "data": [ + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.02, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 298.16, + 0.02, + 298.16, + 303.15, + 288.15, + 0.02, + 0.05, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.02, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 298.16, + 0.02, + 298.16, + 303.15, + 288.15, + 0.02, + 0.05, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0411, + 0.0, + 1000.0, + 100000.0, + 250.0, + 1.0, + 900.0, + 7200.0, + 900.0, + 10.0, + 298.16, + NaN, + 298.16, + 303.15, + 288.15, + 0.0411, + 0.05, + 0.0, + 4.01, + Infinity, + -Infinity, + 298.16, + 0.7401, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.012, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.012, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1678, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0096, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.1532, + 303.15, + 288.15, + 0.03, + 0.05, + 0.0, + 0.0032, + Infinity, + -Infinity, + 294.1532, + 0.4198, + Infinity, + Infinity, + -Infinity, + -Infinity + ] + ], + "index": [ + [ + 0.0, + -1800.0 + ], + [ + 0.0, + -900.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 900.0 + ], + [ + 0.0, + 1800.0 + ] + ] + }, + "index_end": "(2700.0, 43200.0)", + "index_start": "(0.0, -1800.0)", + "shape": [ + 204, + 31 + ], + "statistics": { + "lower.P_el": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.T": { + "25%": 288.15, + "50%": 288.15, + "75%": 288.15, + "count": 204.0, + "max": 288.15, + "mean": 288.15, + "min": 288.15, + "std": 0.0 + }, + "lower.T_out": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.T_slack": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.mDot": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.C": { + "25%": 100000.0, + "50%": 100000.0, + "75%": 100000.0, + "count": 4.0, + "max": 100000.0, + "mean": 100000.0, + "min": 100000.0, + "std": 0.0 + }, + "parameter.T": { + "25%": 294.15, + "50%": 294.15, + "75%": 298.0245, + "count": 12.0, + "max": 298.16, + "mean": 295.408, + "min": 293.6765, + "std": 2.005 + }, + "parameter.T_in": { + "25%": 280.15, + "50%": 280.15, + "75%": 280.15, + "count": 200.0, + "max": 280.15, + "mean": 280.15, + "min": 280.15, + "std": 0.0 + }, + "parameter.T_lower": { + "25%": 292.15, + "50%": 292.15, + "75%": 292.15, + "count": 200.0, + "max": 292.15, + "mean": 292.15, + "min": 292.15, + "std": 0.0 + }, + "parameter.T_upper": { + "25%": 294.15, + "50%": 294.15, + "75%": 294.15, + "count": 200.0, + "max": 294.15, + "mean": 294.15, + "min": 294.15, + "std": 0.0 + }, + "parameter.cp": { + "25%": 1000.0, + "50%": 1000.0, + "75%": 1000.0, + "count": 4.0, + "max": 1000.0, + "mean": 1000.0, + "min": 1000.0, + "std": 0.0 + }, + "parameter.flex_event_duration": { + "25%": 7200.0, + "50%": 7200.0, + "75%": 7200.0, + "count": 4.0, + "max": 7200.0, + "mean": 7200.0, + "min": 7200.0, + "std": 0.0 + }, + "parameter.in_provision": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.load": { + "25%": 150.0, + "50%": 150.0, + "75%": 150.0, + "count": 200.0, + "max": 150.0, + "mean": 150.0, + "min": 150.0, + "std": 0.0 + }, + "parameter.mDot": { + "25%": 0.0101, + "50%": 0.02, + "75%": 0.0411, + "count": 8.0, + "max": 0.0411, + "mean": 0.024, + "min": 0.0093, + "std": 0.0148 + }, + "parameter.mDot_full": { + "25%": 0.0104, + "50%": 0.0104, + "75%": 0.0104, + "count": 200.0, + "max": 0.0411, + "mean": 0.0103, + "min": 0.0, + "std": 0.004 + }, + "parameter.market_time": { + "25%": 900.0, + "50%": 900.0, + "75%": 900.0, + "count": 4.0, + "max": 900.0, + "mean": 900.0, + "min": 900.0, + "std": 0.0 + }, + "parameter.prep_time": { + "25%": 900.0, + "50%": 900.0, + "75%": 900.0, + "count": 4.0, + "max": 900.0, + "mean": 900.0, + "min": 900.0, + "std": 0.0 + }, + "parameter.r_mDot": { + "25%": 1.0, + "50%": 1.0, + "75%": 1.0, + "count": 4.0, + "max": 1.0, + "mean": 1.0, + "min": 1.0, + "std": 0.0 + }, + "parameter.s_P": { + "25%": 10.0, + "50%": 10.0, + "75%": 10.0, + "count": 4.0, + "max": 10.0, + "mean": 10.0, + "min": 10.0, + "std": 0.0 + }, + "parameter.s_T": { + "25%": 250.0, + "50%": 250.0, + "75%": 250.0, + "count": 4.0, + "max": 250.0, + "mean": 250.0, + "min": 250.0, + "std": 0.0 + }, + "upper.P_el": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.T": { + "25%": 303.15, + "50%": 303.15, + "75%": 303.15, + "count": 204.0, + "max": 303.15, + "mean": 303.15, + "min": 303.15, + "std": 0.0 + }, + "upper.T_out": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.T_slack": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.mDot": { + "25%": 0.05, + "50%": 0.05, + "75%": 0.05, + "count": 200.0, + "max": 0.05, + "mean": 0.05, + "min": 0.05, + "std": 0.0 + }, + "variable.P_el": { + "25%": 0.1461, + "50%": 0.1461, + "75%": 0.1461, + "count": 192.0, + "max": 0.7401, + "mean": 0.1483, + "min": 0.0, + "std": 0.0964 + }, + "variable.T": { + "25%": 294.1488, + "50%": 294.15, + "75%": 294.15, + "count": 204.0, + "max": 298.16, + "mean": 294.0222, + "min": 292.1471, + "std": 0.8286 + }, + "variable.T_out": { + "25%": 294.1488, + "50%": 294.15, + "75%": 294.15, + "count": 192.0, + "max": 298.16, + "mean": 293.935, + "min": 292.1471, + "std": 0.6572 + }, + "variable.T_slack": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0002, + "count": 192.0, + "max": 4.01, + "mean": 0.0213, + "min": 0.0, + "std": 0.2894 + }, + "variable.mDot": { + "25%": 0.0104, + "50%": 0.0104, + "75%": 0.0104, + "count": 200.0, + "max": 0.0411, + "mean": 0.0111, + "min": 0.0, + "std": 0.0075 + } + }, + "tail_5_rows": { + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "mDot_full" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "prep_time" + ], + [ + "parameter", + "flex_event_duration" + ], + [ + "parameter", + "market_time" + ], + [ + "parameter", + "s_P" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "data": [ + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.0, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 295.1767, + 303.15, + 288.15, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ] + ], + "index": [ + [ + 2700.0, + 39600.0 + ], + [ + 2700.0, + 40500.0 + ], + [ + 2700.0, + 41400.0 + ], + [ + 2700.0, + 42300.0 + ], + [ + 2700.0, + 43200.0 + ] + ] + } +} \ No newline at end of file diff --git a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_pos_flex_summary.json b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_pos_flex_summary.json new file mode 100644 index 00000000..90cac4e2 --- /dev/null +++ b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_pos_flex_summary.json @@ -0,0 +1,1080 @@ +{ + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "mDot_full" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "prep_time" + ], + [ + "parameter", + "flex_event_duration" + ], + [ + "parameter", + "market_time" + ], + [ + "parameter", + "s_P" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "head_5_rows": { + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "mDot_full" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "prep_time" + ], + [ + "parameter", + "flex_event_duration" + ], + [ + "parameter", + "market_time" + ], + [ + "parameter", + "s_P" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "data": [ + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.02, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 298.16, + 0.02, + 298.16, + 303.15, + 288.15, + 0.02, + 0.05, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.02, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 298.16, + 0.02, + 298.16, + 303.15, + 288.15, + 0.02, + 0.05, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0411, + 0.0, + 1000.0, + 100000.0, + 250.0, + 1.0, + 900.0, + 7200.0, + 900.0, + 10.0, + 298.16, + NaN, + 298.16, + 303.15, + 288.15, + 0.0411, + 0.05, + 0.0, + 4.01, + Infinity, + -Infinity, + 298.16, + 0.7401, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.012, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0324, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.4532, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0096, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 292.1478, + 303.15, + 288.15, + 0.0017, + 0.05, + 0.0, + 0.0022, + Infinity, + -Infinity, + 292.1478, + 0.0201, + Infinity, + Infinity, + -Infinity, + -Infinity + ] + ], + "index": [ + [ + 0.0, + -1800.0 + ], + [ + 0.0, + -900.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 900.0 + ], + [ + 0.0, + 1800.0 + ] + ] + }, + "index_end": "(2700.0, 43200.0)", + "index_start": "(0.0, -1800.0)", + "shape": [ + 204, + 31 + ], + "statistics": { + "lower.P_el": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.T": { + "25%": 288.15, + "50%": 288.15, + "75%": 288.15, + "count": 204.0, + "max": 288.15, + "mean": 288.15, + "min": 288.15, + "std": 0.0 + }, + "lower.T_out": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.T_slack": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": -Infinity, + "mean": -Infinity, + "min": -Infinity, + "std": NaN + }, + "lower.mDot": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.C": { + "25%": 100000.0, + "50%": 100000.0, + "75%": 100000.0, + "count": 4.0, + "max": 100000.0, + "mean": 100000.0, + "min": 100000.0, + "std": 0.0 + }, + "parameter.T": { + "25%": 294.15, + "50%": 294.15, + "75%": 298.0245, + "count": 12.0, + "max": 298.16, + "mean": 295.408, + "min": 293.6765, + "std": 2.005 + }, + "parameter.T_in": { + "25%": 280.15, + "50%": 280.15, + "75%": 280.15, + "count": 200.0, + "max": 280.15, + "mean": 280.15, + "min": 280.15, + "std": 0.0 + }, + "parameter.T_lower": { + "25%": 292.15, + "50%": 292.15, + "75%": 292.15, + "count": 200.0, + "max": 292.15, + "mean": 292.15, + "min": 292.15, + "std": 0.0 + }, + "parameter.T_upper": { + "25%": 294.15, + "50%": 294.15, + "75%": 294.15, + "count": 200.0, + "max": 294.15, + "mean": 294.15, + "min": 294.15, + "std": 0.0 + }, + "parameter.cp": { + "25%": 1000.0, + "50%": 1000.0, + "75%": 1000.0, + "count": 4.0, + "max": 1000.0, + "mean": 1000.0, + "min": 1000.0, + "std": 0.0 + }, + "parameter.flex_event_duration": { + "25%": 7200.0, + "50%": 7200.0, + "75%": 7200.0, + "count": 4.0, + "max": 7200.0, + "mean": 7200.0, + "min": 7200.0, + "std": 0.0 + }, + "parameter.in_provision": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 200.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, + "parameter.load": { + "25%": 150.0, + "50%": 150.0, + "75%": 150.0, + "count": 200.0, + "max": 150.0, + "mean": 150.0, + "min": 150.0, + "std": 0.0 + }, + "parameter.mDot": { + "25%": 0.0101, + "50%": 0.02, + "75%": 0.0411, + "count": 8.0, + "max": 0.0411, + "mean": 0.024, + "min": 0.0093, + "std": 0.0148 + }, + "parameter.mDot_full": { + "25%": 0.0104, + "50%": 0.0104, + "75%": 0.0104, + "count": 200.0, + "max": 0.0411, + "mean": 0.0103, + "min": 0.0, + "std": 0.004 + }, + "parameter.market_time": { + "25%": 900.0, + "50%": 900.0, + "75%": 900.0, + "count": 4.0, + "max": 900.0, + "mean": 900.0, + "min": 900.0, + "std": 0.0 + }, + "parameter.prep_time": { + "25%": 900.0, + "50%": 900.0, + "75%": 900.0, + "count": 4.0, + "max": 900.0, + "mean": 900.0, + "min": 900.0, + "std": 0.0 + }, + "parameter.r_mDot": { + "25%": 1.0, + "50%": 1.0, + "75%": 1.0, + "count": 4.0, + "max": 1.0, + "mean": 1.0, + "min": 1.0, + "std": 0.0 + }, + "parameter.s_P": { + "25%": 10.0, + "50%": 10.0, + "75%": 10.0, + "count": 4.0, + "max": 10.0, + "mean": 10.0, + "min": 10.0, + "std": 0.0 + }, + "parameter.s_T": { + "25%": 250.0, + "50%": 250.0, + "75%": 250.0, + "count": 4.0, + "max": 250.0, + "mean": 250.0, + "min": 250.0, + "std": 0.0 + }, + "upper.P_el": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.T": { + "25%": 303.15, + "50%": 303.15, + "75%": 303.15, + "count": 204.0, + "max": 303.15, + "mean": 303.15, + "min": 303.15, + "std": 0.0 + }, + "upper.T_out": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.T_slack": { + "25%": NaN, + "50%": NaN, + "75%": NaN, + "count": 192.0, + "max": Infinity, + "mean": Infinity, + "min": Infinity, + "std": NaN + }, + "upper.mDot": { + "25%": 0.05, + "50%": 0.05, + "75%": 0.05, + "count": 200.0, + "max": 0.05, + "mean": 0.05, + "min": 0.05, + "std": 0.0 + }, + "variable.P_el": { + "25%": 0.1461, + "50%": 0.1461, + "75%": 0.1461, + "count": 192.0, + "max": 0.7401, + "mean": 0.1463, + "min": 0.0, + "std": 0.0657 + }, + "variable.T": { + "25%": 294.15, + "50%": 294.15, + "75%": 294.15, + "count": 204.0, + "max": 298.16, + "mean": 294.1729, + "min": 292.1478, + "std": 0.6642 + }, + "variable.T_out": { + "25%": 294.15, + "50%": 294.15, + "75%": 294.15, + "count": 192.0, + "max": 298.16, + "mean": 294.0952, + "min": 292.1478, + "std": 0.4444 + }, + "variable.T_slack": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0001, + "count": 192.0, + "max": 4.01, + "mean": 0.0212, + "min": 0.0, + "std": 0.2894 + }, + "variable.mDot": { + "25%": 0.0104, + "50%": 0.0104, + "75%": 0.0104, + "count": 200.0, + "max": 0.0411, + "mean": 0.011, + "min": 0.0, + "std": 0.0056 + } + }, + "tail_5_rows": { + "columns": [ + [ + "parameter", + "load" + ], + [ + "parameter", + "T_upper" + ], + [ + "parameter", + "T_lower" + ], + [ + "parameter", + "T_in" + ], + [ + "parameter", + "mDot_full" + ], + [ + "parameter", + "in_provision" + ], + [ + "parameter", + "cp" + ], + [ + "parameter", + "C" + ], + [ + "parameter", + "s_T" + ], + [ + "parameter", + "r_mDot" + ], + [ + "parameter", + "prep_time" + ], + [ + "parameter", + "flex_event_duration" + ], + [ + "parameter", + "market_time" + ], + [ + "parameter", + "s_P" + ], + [ + "parameter", + "T" + ], + [ + "parameter", + "mDot" + ], + [ + "variable", + "T" + ], + [ + "upper", + "T" + ], + [ + "lower", + "T" + ], + [ + "variable", + "mDot" + ], + [ + "upper", + "mDot" + ], + [ + "lower", + "mDot" + ], + [ + "variable", + "T_slack" + ], + [ + "upper", + "T_slack" + ], + [ + "lower", + "T_slack" + ], + [ + "variable", + "T_out" + ], + [ + "variable", + "P_el" + ], + [ + "upper", + "T_out" + ], + [ + "upper", + "P_el" + ], + [ + "lower", + "T_out" + ], + [ + "lower", + "P_el" + ] + ], + "data": [ + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0104, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.1461, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + 150.0, + 294.15, + 292.15, + 280.15, + 0.0, + 0.0, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 294.15, + 303.15, + 288.15, + 0.0, + 0.05, + 0.0, + 0.0, + Infinity, + -Infinity, + 294.15, + 0.0, + Infinity, + Infinity, + -Infinity, + -Infinity + ], + [ + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + 295.1767, + 303.15, + 288.15, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN, + NaN + ] + ], + "index": [ + [ + 2700.0, + 39600.0 + ], + [ + 2700.0, + 40500.0 + ], + [ + 2700.0, + 41400.0 + ], + [ + 2700.0, + 42300.0 + ], + [ + 2700.0, + 43200.0 + ] + ] + } +} \ No newline at end of file diff --git a/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json b/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json index addd2487..6b60bb0e 100644 --- a/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json +++ b/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json @@ -33,7 +33,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "head_5_rows": { @@ -71,7 +71,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ @@ -351,16 +351,6 @@ "min": 0.0, "std": 0.0697 }, - "collocation_time_grid": { - "25%": 10895.0962, - "50%": 21600.0, - "75%": 32304.9038, - "count": 384.0, - "max": 43009.8076, - "mean": 21600.0, - "min": 190.1924, - "std": 12487.0356 - }, "flex_event_duration": { "25%": 7200.0, "50%": 7200.0, @@ -631,6 +621,16 @@ "min": 10.0, "std": 0.0 }, + "time_grid_info": { + "25%": 10895.0962, + "50%": 21600.0, + "75%": 32304.9038, + "count": 384.0, + "max": 43009.8076, + "mean": 21600.0, + "min": 190.1924, + "std": 12487.0356 + }, "time_step_mpc": { "25%": 900.0, "50%": 900.0, @@ -677,7 +677,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ diff --git a/tests/test_oneRoom_SimpleLinRegMPC.py b/tests/test_oneRoom_SimpleLinRegMPC.py index d8a564da..68f22088 100644 --- a/tests/test_oneRoom_SimpleLinRegMPC.py +++ b/tests/test_oneRoom_SimpleLinRegMPC.py @@ -6,6 +6,12 @@ import importlib.util import json from util import module_cleanup, round_floats_in_structure +from agentlib.core.errors import OptionalDependencyError + +try: + import keras #check ML dependency +except ImportError: + raise OptionalDependencyError(used_object='agentlib_mpc[ml]', dependency_name="agentlib_mpc[ml]", dependency_install="pip install 'agentlib_mpc[ml] @ git+https://github.com/RWTH-EBC/AgentLib-MPC.git@quickfix-custom-objectives'") # Add the project root to the Python path to allow for absolute imports # This helps in locating the agentlib_flexquant package if needed @@ -144,3 +150,25 @@ def test_oneroom_simple_mpc(snapshot, module_cleanup): df_pos_flex_res = res["PosFlexMPC"]["PosFlexMPC"] df_baseline_res = res["Baseline"]["Baseline"] df_indicator_res = res["FlexibilityIndicator"]["FlexibilityIndicator"] + + # Assert that a summary of each result DataFrame matches its snapshot + assert_frame_matches_summary_snapshot( + snapshot, + df_neg_flex_res, + 'oneroom_simpleMPC_neg_flex_summary.json' + ) + assert_frame_matches_summary_snapshot( + snapshot, + df_pos_flex_res, + 'oneroom_simpleMPC_pos_flex_summary.json' + ) + assert_frame_matches_summary_snapshot( + snapshot, + df_baseline_res, + 'oneroom_simpleMPC_baseline_summary.json' + ) + assert_frame_matches_summary_snapshot( + snapshot, + df_indicator_res, + 'oneroom_simpleMPC_indicator_summary.json' + ) From 6f8f0dcb6cf40fc7ad06d4793176bebeb6744092 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Wed, 25 Feb 2026 13:25:18 +0100 Subject: [PATCH 21/25] adjust ci workflow minor changes --- .github/workflows/ci.yml | 1 + agentlib_flexquant/data_structures/flex_kpis.py | 2 +- agentlib_flexquant/data_structures/globals.py | 6 ------ agentlib_flexquant/generate_flex_agents.py | 4 +++- agentlib_flexquant/modules/flexibility_indicator.py | 4 ++-- agentlib_flexquant/modules/shadow_mpc.py | 1 - 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a720232..01176488 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,3 +20,4 @@ jobs: CREATE_PAGES_ON_FAILURE: true EXECUTE_TESTS: true EXECUTE_COVERAGE_TEST: true + EXTRA_REQUIREMENTS: '["ml"]' diff --git a/agentlib_flexquant/data_structures/flex_kpis.py b/agentlib_flexquant/data_structures/flex_kpis.py index 3ffa1337..5b434a93 100644 --- a/agentlib_flexquant/data_structures/flex_kpis.py +++ b/agentlib_flexquant/data_structures/flex_kpis.py @@ -1,4 +1,4 @@ -"""" +""" Module for representing and calculating flexibility KPIs. It defines Pydantic models for scalar and time-series KPIs, and provides methods to compute power, energy, and cost metrics for positive and negative flexibility scenarios. diff --git a/agentlib_flexquant/data_structures/globals.py b/agentlib_flexquant/data_structures/globals.py index 14d96bdc..0cec19d5 100644 --- a/agentlib_flexquant/data_structures/globals.py +++ b/agentlib_flexquant/data_structures/globals.py @@ -42,12 +42,6 @@ "self.market_time.sym), obj_flex, obj_std))" ) -SHADOW_MPC_COST_FUNCTION = ("return ca.if_else(self.time < self.prep_time.sym + " - "self.market_time.sym, obj_std, ca.if_else(self.time < " - "(self.prep_time.sym + self.flex_event_duration.sym + " - "self.market_time.sym), obj_flex, obj_std))") - - def return_baseline_cost_function(power_variable: str, comfort_variable: str) -> str: """Return baseline cost function diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index 7cc7616c..c01a2e1d 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -764,6 +764,8 @@ def _generate_flex_model_definition(self): if self.flex_config.shadow_mpc_config_generator_data.neg_flex.flex_cost_function_appendix else ""), shadow_mpc_type="neg_flex" ) + # The " + " is just there to simplify the validation, it does not affect + # the generated code self.check_variables_in_casadi_config( config_instance, self.flex_config.shadow_mpc_config_generator_data.pos_flex.flex_cost_function + @@ -1049,7 +1051,7 @@ def adapt_sim_results_path(self, simulator_agent_config: Union[str, Path], with open(Path(str(simulator_agent_config.stem) + save_name_suffix + ".json"), "w", encoding="utf-8") as f: json.dump(sim_config, f, indent=4) - return simulator_agent_config + return Path(str(simulator_agent_config.stem) + save_name_suffix + ".json") except Exception as e: raise Exception(f"Could not adapt and create a new simulation config " f"due to: {e}. " diff --git a/agentlib_flexquant/modules/flexibility_indicator.py b/agentlib_flexquant/modules/flexibility_indicator.py index 0e3a7cc3..1e7db118 100644 --- a/agentlib_flexquant/modules/flexibility_indicator.py +++ b/agentlib_flexquant/modules/flexibility_indicator.py @@ -1,4 +1,4 @@ -"""" +""" Flexibility indicator module for calculating and distributing energy flexibility offers. This module processes power and energy profiles from baseline and shadow MPCs to @@ -15,7 +15,7 @@ import numpy as np import pandas as pd from pydantic import BaseModel, ConfigDict, Field, model_validator -from agentlib_flexquant.utils.data_handling import fill_nans, MEAN, INTERPOLATE +from agentlib_flexquant.utils.data_handling import fill_nans, MEAN import agentlib_flexquant.data_structures.globals as glbs from agentlib_flexquant.data_structures.flex_kpis import ( diff --git a/agentlib_flexquant/modules/shadow_mpc.py b/agentlib_flexquant/modules/shadow_mpc.py index 4cd25b44..3844277b 100644 --- a/agentlib_flexquant/modules/shadow_mpc.py +++ b/agentlib_flexquant/modules/shadow_mpc.py @@ -10,7 +10,6 @@ from collections.abc import Iterable from agentlib.core.datamodels import AgentVariable, Source from agentlib_mpc.modules.mpc import mpc_full, minlp_mpc -from agentlib_mpc.data_structures.mpc_datamodels import Results from agentlib_flexquant.utils.data_handling import fill_nans, MEAN from agentlib_flexquant.data_structures.globals import (full_trajectory_suffix, base_vars_to_communicate_suffix) From 9d92f57d31bb53a7ab596df379f6fe38f1750918 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Wed, 25 Feb 2026 14:21:31 +0100 Subject: [PATCH 22/25] change r_del_u handling --- agentlib_flexquant/generate_flex_agents.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index c01a2e1d..d92a0f29 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -374,15 +374,14 @@ def adapt_mpc_module_config( self.flex_config.baseline_config_generator_data.power_variable) module_config_flex_dict["storage_variable_name"] = ( self.indicator_module_config.correct_costs.stored_energy_variable) - del module_config_flex_dict['r_del_u'] module_config_flex = cmng.MODULE_TYPE_DICT[module_config.type]( **module_config_flex_dict, _agent_id=agent_id ) # HOTFIX due to AgentLib-MPC bug. Needs to be adapted after Objectives # in AgentLib-MPC are fixed. - #if module_config_flex.r_del_u is None: - # module_config_flex = module_config_flex.model_copy(update={"r_del_u": {}}) + if module_config_flex.r_del_u is None: + module_config_flex = module_config_flex.model_copy(update={"r_del_u": {}}) # allow the module config to be changed module_config_flex.model_config["frozen"] = False From 758861beb8294a40b7ebb1f83150132530802c96 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Thu, 26 Mar 2026 15:59:36 +0100 Subject: [PATCH 23/25] include review --- agentlib_flexquant/generate_flex_agents.py | 27 +++---------------- .../modules/flexibility_indicator.py | 2 +- .../flexibility_agent_config.json | 3 --- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index d92a0f29..cab76e6f 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -381,7 +381,7 @@ def adapt_mpc_module_config( # HOTFIX due to AgentLib-MPC bug. Needs to be adapted after Objectives # in AgentLib-MPC are fixed. if module_config_flex.r_del_u is None: - module_config_flex = module_config_flex.model_copy(update={"r_del_u": {}}) + module_config_flex = module_config_flex.model_copy(update={"r_del_u": {}}) # allow the module config to be changed module_config_flex.model_config["frozen"] = False @@ -716,11 +716,6 @@ def get_time_grid(self, discretization_options: dict): if discretization_options.get("method") == "multiple_shooting": grid = np.arange(0, (prediction_horizon + 1) * time_step, time_step) return {"type": "multiple_shooting", "grid": grid.tolist()} - - # For collocation, compute the time grid - if "collocation_method" not in discretization_options: - return {"type": "none", "grid": []} - else: collocation_method = discretization_options["collocation_method"] collocation_order = discretization_options["collocation_order"] @@ -923,26 +918,12 @@ def run_config_validations(self): f"if the correction of costs is enabled." ) - - # validate discretization method (collocation or multiple shooting) + # validate discretization method (collocation or multiple shooting) discretization_options = self.baseline_mpc_module_config.optimization_backend.get( - "discretization_options", {} - ) - - # Check if using multiple shooting or collocation - # Multiple shooting typically doesn't require collocation_method - is_multiple_shooting = discretization_options.get("method") == "multiple_shooting" - has_collocation_method = "collocation_method" in discretization_options - - if not is_multiple_shooting and not has_collocation_method: - raise ConfigurationError( - "Please specify a valid discretization method. Either use multiple shooting " - "(set method='multiple_shooting' in discretization_options) or use collocation " - "with a defined collocation_method in the mpc config." - ) + "discretization_options", {}) # If using collocation, validate the collocation method - if has_collocation_method: + if "collocation_method" in discretization_options: collocation_method = discretization_options["collocation_method"] if collocation_method != "legendre": self.logger.warning( diff --git a/agentlib_flexquant/modules/flexibility_indicator.py b/agentlib_flexquant/modules/flexibility_indicator.py index 1e7db118..ed9098e7 100644 --- a/agentlib_flexquant/modules/flexibility_indicator.py +++ b/agentlib_flexquant/modules/flexibility_indicator.py @@ -588,7 +588,7 @@ def calc_and_send_offer(self): is_collocation = time_grid_info and time_grid_info.get("type") == "collocation" if is_collocation: - full_index = np.sort(np.unique(np.concatenate([time_grid,self.data.mpc_time_grid]))) + full_index = np.sort(np.concatenate([time_grid,self.data.mpc_time_grid])) else: full_index = self.data.mpc_time_grid diff --git a/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json index 0aeb0fd7..f42b9f7f 100644 --- a/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json +++ b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json @@ -1,7 +1,4 @@ { - "prep_time": 900, - "flex_event_duration": 7200, - "market_time": 900, "indicator_config": { "agent_config": { "id": "FlexibilityIndicator", From 647f780cacca08296e34c4694f53af4034dc09f7 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Fri, 27 Mar 2026 13:35:47 +0100 Subject: [PATCH 24/25] update examples and tests --- .../data_structures/flex_kpis.py | 1 - .../flexibility_agent_config.json | 15 +- .../oneroom_cia_indicator_summary.json | 26 +- .../oneroom_simpleMPC_indicator_summary.json | 225 ++++++++++-------- .../oneroom_simpleMPC_indicator_summary.json | 26 +- 5 files changed, 153 insertions(+), 140 deletions(-) diff --git a/agentlib_flexquant/data_structures/flex_kpis.py b/agentlib_flexquant/data_structures/flex_kpis.py index 6bc6ce14..79a30252 100644 --- a/agentlib_flexquant/data_structures/flex_kpis.py +++ b/agentlib_flexquant/data_structures/flex_kpis.py @@ -456,7 +456,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) diff --git a/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json index 4a26e253..ae68f835 100644 --- a/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json +++ b/examples/OneRoom_SimpleLinRegMPC/flex_configs/flexibility_agent_config.json @@ -1,4 +1,7 @@ { + "prep_time": 900, + "flex_event_duration": 7200, + "market_time": 900, "indicator_config": { "agent_config": { "id": "FlexibilityIndicator", @@ -12,18 +15,6 @@ "type": "agentlib_flexquant.flexibility_indicator", "price_variable": "r_pel", "parameters": [ - { - "name": "prep_time", - "value": 900 - }, - { - "name": "market_time", - "value": 900 - }, - { - "name": "flex_event_duration", - "value": 7200 - }, { "name": "time_step", "value": 900 diff --git a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json index 2bbac190..fe3d9c00 100644 --- a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json +++ b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json @@ -34,7 +34,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "head_5_rows": { @@ -73,7 +73,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ @@ -368,16 +368,6 @@ "min": 0.0, "std": 0.0 }, - "collocation_time_grid": { - "25%": 1231.7, - "50%": 2400.0, - "75%": 3568.3, - "count": 384.0, - "max": 4736.6, - "mean": 2400.0, - "min": 63.4, - "std": 1387.4 - }, "flex_event_duration": { "25%": 2400.0, "50%": 2400.0, @@ -628,6 +618,16 @@ "min": 10.0, "std": 0.0 }, + "time_grid_info": { + "25%": 1231.7, + "50%": 2400.0, + "75%": 3568.3, + "count": 384.0, + "max": 4736.6, + "mean": 2400.0, + "min": 63.4, + "std": 1387.4 + }, "time_step_mpc": { "25%": 300.0, "50%": 300.0, @@ -675,7 +675,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ diff --git a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json index 9684577d..54dc6540 100644 --- a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json +++ b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json @@ -7,6 +7,7 @@ "_E_stored_neg", "_E_stored_pos", "r_pel", + "c_pel_feed_in", "negative_power_flex_full", "positive_power_flex_full", "negative_power_flex_offer", @@ -45,6 +46,7 @@ "_E_stored_neg", "_E_stored_pos", "r_pel", + "c_pel_feed_in", "negative_power_flex_full", "positive_power_flex_full", "negative_power_flex_offer", @@ -82,7 +84,8 @@ NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, 0.0, 0.0, NaN, @@ -97,14 +100,14 @@ true, 0.0396, 0.0467, - 0.0327, - 0.0102, - 0.0327, - 0.0102, - 0.8254, - 0.2172, - 0.8254, - 0.2172, + 0.3265, + 0.1015, + 0.3265, + 0.1015, + 8.2537, + 2.1717, + 8.2537, + 2.1717, 900.0, 900.0, 7200.0, @@ -119,7 +122,8 @@ NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, 0.0, -0.2849, NaN, @@ -156,7 +160,8 @@ NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, 0.2851, 0.1146, 0.2851, @@ -193,7 +198,8 @@ NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, -0.1505, 0.0912, -0.1505, @@ -230,7 +236,8 @@ NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, 0.1736, 0.0385, 0.1736, @@ -284,11 +291,11 @@ ] ] }, - "index_end": "(2700.0, 44100.0)", + "index_end": "(2700.0, 43200.0)", "index_start": "(0.0, 0.0)", "shape": [ - 200, - 35 + 196, + 36 ], "statistics": { "_E_stored_base": { @@ -351,6 +358,16 @@ "min": 0.0, "std": 0.0501 }, + "c_pel_feed_in": { + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, + "count": 196.0, + "max": 0.0, + "mean": 0.0, + "min": 0.0, + "std": 0.0 + }, "flex_event_duration": { "25%": 7200.0, "50%": 7200.0, @@ -372,44 +389,44 @@ "std": 0.0 }, "negative_corrected_costs": { - "25%": 0.026, - "50%": 0.0325, - "75%": 0.0327, + "25%": 0.2603, + "50%": 0.3249, + "75%": 0.3273, "count": 4.0, - "max": 0.033, - "mean": 0.0263, - "min": 0.0071, - "std": 0.0128 + "max": 0.3298, + "mean": 0.2627, + "min": 0.0714, + "std": 0.1276 }, "negative_corrected_costs_rel": { - "25%": 0.6538, - "50%": 0.8155, - "75%": 0.8232, + "25%": 6.5378, + "50%": 8.1551, + "75%": 8.2325, "count": 4.0, - "max": 0.8254, - "mean": 0.6615, - "min": 0.1896, - "std": 0.3147 + "max": 8.2537, + "mean": 6.6151, + "min": 1.8965, + "std": 3.1466 }, "negative_costs": { - "25%": 0.026, - "50%": 0.0325, - "75%": 0.0327, + "25%": 0.2603, + "50%": 0.3249, + "75%": 0.3273, "count": 4.0, - "max": 0.033, - "mean": 0.0263, - "min": 0.0071, - "std": 0.0128 + "max": 0.3298, + "mean": 0.2627, + "min": 0.0714, + "std": 0.1276 }, "negative_costs_rel": { - "25%": 0.6538, - "50%": 0.8155, - "75%": 0.8232, + "25%": 6.5378, + "50%": 8.1551, + "75%": 8.2325, "count": 4.0, - "max": 0.8254, - "mean": 0.6615, - "min": 0.1896, - "std": 0.3147 + "max": 8.2537, + "mean": 6.6151, + "min": 1.8965, + "std": 3.1466 }, "negative_energy_flex": { "25%": 0.0391, @@ -472,44 +489,44 @@ "std": 0.0021 }, "positive_corrected_costs": { - "25%": 0.0021, - "50%": 0.0099, - "75%": 0.0102, + "25%": 0.0207, + "50%": 0.0986, + "75%": 0.1022, "count": 4.0, - "max": 0.0104, - "mean": 0.0024, - "min": -0.0204, - "std": 0.0152 + "max": 0.1043, + "mean": 0.0243, + "min": -0.2041, + "std": 0.1523 }, "positive_corrected_costs_rel": { - "25%": 0.0487, - "50%": 0.2113, - "75%": 0.2189, + "25%": 0.4874, + "50%": 2.1132, + "75%": 2.1894, "count": 4.0, - "max": 0.2243, - "mean": 0.0564, - "min": -0.4214, - "std": 0.3186 + "max": 2.2425, + "mean": 0.5636, + "min": -4.2144, + "std": 3.1863 }, "positive_costs": { - "25%": 0.0021, - "50%": 0.0099, - "75%": 0.0102, + "25%": 0.0207, + "50%": 0.0986, + "75%": 0.1022, "count": 4.0, - "max": 0.0104, - "mean": 0.0024, - "min": -0.0204, - "std": 0.0152 + "max": 0.1043, + "mean": 0.0243, + "min": -0.2041, + "std": 0.1523 }, "positive_costs_rel": { - "25%": 0.0487, - "50%": 0.2113, - "75%": 0.2189, + "25%": 0.4874, + "50%": 2.1132, + "75%": 2.1894, "count": 4.0, - "max": 0.2243, - "mean": 0.0564, - "min": -0.4214, - "std": 0.3186 + "max": 2.2425, + "mean": 0.5636, + "min": -4.2144, + "std": 3.1863 }, "positive_energy_flex": { "25%": 0.0465, @@ -592,13 +609,13 @@ "std": 0.0 }, "r_pel": { - "25%": 1.0, - "50%": 1.0, - "75%": 1.0, - "count": 200.0, - "max": 1.0, - "mean": 1.0, - "min": 1.0, + "25%": 10.0, + "50%": 10.0, + "75%": 10.0, + "count": 196.0, + "max": 10.0, + "mean": 10.0, + "min": 10.0, "std": 0.0 }, "time_grid_info": { @@ -631,6 +648,7 @@ "_E_stored_neg", "_E_stored_pos", "r_pel", + "c_pel_feed_in", "negative_power_flex_full", "positive_power_flex_full", "negative_power_flex_offer", @@ -668,7 +686,8 @@ NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, 0.0, 0.0, NaN, @@ -695,7 +714,7 @@ NaN, NaN, NaN, - 40500.0, + 39600.0, NaN ], [ @@ -705,7 +724,8 @@ NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, 0.0, 0.0, NaN, @@ -732,17 +752,18 @@ NaN, NaN, NaN, - 41400.0, + 40500.0, NaN ], [ - 0.0, - 0.0, - 0.0, + 0.1461, + 0.1461, + 0.1461, NaN, NaN, NaN, - 1.0, + 10.0, + 0.0, 0.0, 0.0, NaN, @@ -769,19 +790,20 @@ NaN, NaN, NaN, - 42300.0, + 41400.0, NaN ], [ + 0.0, + 0.0, + 0.0, NaN, NaN, NaN, - NaN, - NaN, - NaN, - 1.0, - NaN, - NaN, + 10.0, + 0.0, + 0.0, + 0.0, NaN, NaN, NaN, @@ -806,7 +828,7 @@ NaN, NaN, NaN, - 43200.0, + 42300.0, NaN ], [ @@ -816,8 +838,8 @@ NaN, NaN, NaN, - 1.0, - NaN, + 10.0, + 0.0, NaN, NaN, NaN, @@ -844,10 +866,15 @@ NaN, NaN, NaN, + 43200.0, NaN ] ], "index": [ + [ + 2700.0, + 39600.0 + ], [ 2700.0, 40500.0 @@ -863,10 +890,6 @@ [ 2700.0, 43200.0 - ], - [ - 2700.0, - 44100.0 ] ] } diff --git a/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json b/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json index 38a7265c..677cf185 100644 --- a/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json +++ b/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json @@ -34,7 +34,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "head_5_rows": { @@ -73,7 +73,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ @@ -368,16 +368,6 @@ "min": 0.0, "std": 0.0 }, - "collocation_time_grid": { - "25%": 10895.0962, - "50%": 21600.0, - "75%": 32304.9038, - "count": 384.0, - "max": 43009.8076, - "mean": 21600.0, - "min": 190.1924, - "std": 12487.0356 - }, "flex_event_duration": { "25%": 7200.0, "50%": 7200.0, @@ -648,6 +638,16 @@ "min": 10.0, "std": 0.0 }, + "time_grid_info": { + "25%": 10895.0962, + "50%": 21600.0, + "75%": 32304.9038, + "count": 384.0, + "max": 43009.8076, + "mean": 21600.0, + "min": 190.1924, + "std": 12487.0356 + }, "time_step_mpc": { "25%": 900.0, "50%": 900.0, @@ -695,7 +695,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ From 733c708188b87f365157c2e9965fbc300e427ca1 Mon Sep 17 00:00:00 2001 From: sarahleidolf Date: Thu, 9 Apr 2026 10:27:08 +0200 Subject: [PATCH 25/25] Update snapshots after assumption "Set the first value of power_flex to zero" was removed add model initialization in config validations, otherwise ML-model is not found --- .../data_structures/flex_kpis.py | 24 ++- agentlib_flexquant/generate_flex_agents.py | 13 +- .../oneroom_cia_indicator_summary.json | 28 +-- .../SimpleBuilding_indicator_summary.json | 194 +++++++++--------- .../oneroom_simpleMPC_indicator_summary.json | 116 +++++------ .../oneroom_simpleMPC_indicator_summary.json | 30 +-- 6 files changed, 211 insertions(+), 194 deletions(-) diff --git a/agentlib_flexquant/data_structures/flex_kpis.py b/agentlib_flexquant/data_structures/flex_kpis.py index 79a30252..c41d2575 100644 --- a/agentlib_flexquant/data_structures/flex_kpis.py +++ b/agentlib_flexquant/data_structures/flex_kpis.py @@ -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", @@ -293,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 @@ -323,13 +323,14 @@ def _calculate_power_flex_stats( # Calculate characteristic values # max and min of power flex offer - power_flex_offer = self.power_flex_offer.value.iloc[:-1] + 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": - power_flex_offer = power_flex_offer.drop( + 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() @@ -337,9 +338,9 @@ def _calculate_power_flex_stats( # 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 + 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 @@ -381,7 +382,7 @@ def _calculate_energy_flex(self, mpc_time_grid, time_grid_info: dict = None): # 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 + series=self.power_flex_offer_prepared, mpc_time_grid=mpc_time_grid ) # Calculate the energy flex and stores the original value @@ -448,6 +449,11 @@ def _calculate_costs( power_flex_full_integration = self._get_series_for_integration( series=self.power_flex_full, mpc_time_grid=mpc_time_grid ) + 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 delta_cost = delta_cost.reindex(power_flex_full_integration.value.index) diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index 6c991c92..6cc8109d 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -902,8 +902,19 @@ def run_config_validations(self): class_name = mod_type["class_name"] # Get the class dynamic_class = cmng.get_class_from_file(file_path, class_name) + + model_config = self.baseline_mpc_module_config.optimization_backend.get("model", {}) + model_kwargs = {k: v for k, v in model_config.items() if k != "type"} + + try: + model_instance = dynamic_class(**model_kwargs) + except Exception as e: + self.logger.warning( + f"Could not instantiate model class {class_name} with full config " + ) + if self.flex_config.baseline_config_generator_data.comfort_variable not in [ - state.name for state in dynamic_class().states + state.name for state in model_instance.states ]: raise ConfigurationError( f"Given comfort variable " diff --git a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json index fe3d9c00..e08d6309 100644 --- a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json +++ b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_indicator_summary.json @@ -86,8 +86,8 @@ NaN, 10.0, 0.0, - 0.0, - 0.0, + NaN, + NaN, NaN, NaN, 0.0, @@ -442,21 +442,21 @@ "25%": 0.0, "50%": 0.0, "75%": 500.0, - "count": 972.0, + "count": 960.0, "max": 500.0, - "mean": 134.6, + "mean": 136.3, "min": -500.0, - "std": 263.4 + "std": 264.6 }, "negative_power_flex_offer": { "25%": 0.0, "50%": 500.0, "75%": 500.0, - "count": 492.0, + "count": 300.0, "max": 500.0, - "mean": 309.3, + "mean": 303.2, "min": -500.0, - "std": 240.4 + "std": 244.2 }, "negative_power_flex_offer_avg": { "25%": 248.1, @@ -542,21 +542,21 @@ "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 972.0, + "count": 960.0, "max": 500.0, - "mean": 11.6, + "mean": 11.7, "min": -500.0, - "std": 140.8 + "std": 141.7 }, "positive_power_flex_offer": { "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 492.0, + "count": 300.0, "max": 500.0, - "mean": 51.0, + "mean": 47.6, "min": -500.0, - "std": 144.8 + "std": 150.6 }, "positive_power_flex_offer_avg": { "25%": 5.3, diff --git a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json index c45c37fd..2d0f21ab 100644 --- a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json +++ b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json @@ -34,7 +34,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "head_5_rows": { @@ -73,7 +73,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ @@ -86,8 +86,8 @@ NaN, 1.0, 0.0, - 0.0, - 0.0, + NaN, + NaN, NaN, NaN, 41.42016, @@ -100,14 +100,14 @@ true, 90.74214, 11.64628, - 93.72299, - 15.9482, - 93.72299, - 15.9482, - 1.03285, - 1.36938, - 1.03285, - 1.36938, + 94.63477, + -1.61884, + 94.63477, + -1.61884, + 1.0429, + -0.139, + 1.0429, + -0.139, 900.0, 900.0, 7200.0, @@ -368,16 +368,6 @@ "min": 0.0, "std": 0.0 }, - "collocation_time_grid": { - "25%": 10895.09619, - "50%": 21600.0, - "75%": 32304.90381, - "count": 384.0, - "max": 43009.80762, - "mean": 21600.0, - "min": 190.19238, - "std": 12487.03557 - }, "flex_event_duration": { "25%": 7200.0, "50%": 7200.0, @@ -399,44 +389,44 @@ "std": 0.0 }, "negative_corrected_costs": { - "25%": 78.6882, - "50%": 79.95716, - "75%": 84.02288, + "25%": 79.60304, + "50%": 80.91256, + "75%": 84.99916, "count": 4.0, - "max": 93.72299, - "mean": 82.75392, - "min": 77.37836, - "std": 7.44416 + "max": 94.63477, + "mean": 83.68963, + "min": 78.29862, + "std": 7.43446 }, "negative_corrected_costs_rel": { - "25%": 1.04608, - "50%": 1.05136, - "75%": 1.05225, + "25%": 1.05832, + "50%": 1.06394, + "75%": 1.0645, "count": 4.0, - "max": 1.05227, - "mean": 1.04696, - "min": 1.03285, - "std": 0.00945 + "max": 1.06475, + "mean": 1.05888, + "min": 1.0429, + "std": 0.01067 }, "negative_costs": { - "25%": 78.6882, - "50%": 79.95716, - "75%": 84.02288, + "25%": 79.60304, + "50%": 80.91256, + "75%": 84.99916, "count": 4.0, - "max": 93.72299, - "mean": 82.75392, - "min": 77.37836, - "std": 7.44416 + "max": 94.63477, + "mean": 83.68963, + "min": 78.29862, + "std": 7.43446 }, "negative_costs_rel": { - "25%": 1.04608, - "50%": 1.05136, - "75%": 1.05225, + "25%": 1.05832, + "50%": 1.06394, + "75%": 1.0645, "count": 4.0, - "max": 1.05227, - "mean": 1.04696, - "min": 1.03285, - "std": 0.00945 + "max": 1.06475, + "mean": 1.05888, + "min": 1.0429, + "std": 0.01067 }, "negative_energy_flex": { "25%": 74.77996, @@ -452,21 +442,21 @@ "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 580.0, + "count": 576.0, "max": 133.29934, - "mean": 6.8486, + "mean": 6.89616, "min": -59.19998, - "std": 23.89503 + "std": 23.97114 }, "negative_power_flex_offer": { - "25%": 36.45976, - "50%": 38.16392, - "75%": 46.2347, - "count": 100.0, + "25%": 35.77786, + "50%": 37.80566, + "75%": 45.97691, + "count": 36.0, "max": 133.29934, - "mean": 46.45813, + "mean": 39.30288, "min": -59.19998, - "std": 34.54558 + "std": 42.25591 }, "negative_power_flex_offer_avg": { "25%": 37.38998, @@ -499,44 +489,44 @@ "std": 5.3469 }, "positive_corrected_costs": { - "25%": 16.17708, - "50%": 16.41002, - "75%": 16.62597, + "25%": -1.34791, + "50%": -1.22117, + "75%": -1.17686, "count": 4.0, - "max": 16.8039, - "mean": 16.39303, - "min": 15.9482, - "std": 0.37253 + "max": -1.15322, + "mean": -1.3036, + "min": -1.61884, + "std": 0.21466 }, "positive_corrected_costs_rel": { - "25%": 1.37956, - "50%": 1.38457, - "75%": 1.38672, + "25%": -0.11238, + "50%": -0.10132, + "75%": -0.09897, "count": 4.0, - "max": 1.38836, - "mean": 1.38172, - "min": 1.36938, - "std": 0.00852 + "max": -0.09851, + "mean": -0.11003, + "min": -0.139, + "std": 0.01944 }, "positive_costs": { - "25%": 16.17708, - "50%": 16.41002, - "75%": 16.62597, + "25%": -1.34791, + "50%": -1.22117, + "75%": -1.17686, "count": 4.0, - "max": 16.8039, - "mean": 16.39303, - "min": 15.9482, - "std": 0.37253 + "max": -1.15322, + "mean": -1.3036, + "min": -1.61884, + "std": 0.21466 }, "positive_costs_rel": { - "25%": 1.37956, - "50%": 1.38457, - "75%": 1.38672, + "25%": -0.11238, + "50%": -0.10132, + "75%": -0.09897, "count": 4.0, - "max": 1.38836, - "mean": 1.38172, - "min": 1.36938, - "std": 0.00852 + "max": -0.09851, + "mean": -0.11003, + "min": -0.139, + "std": 0.01944 }, "positive_energy_flex": { "25%": 11.69175, @@ -552,21 +542,21 @@ "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 580.0, + "count": 576.0, "max": 59.13035, - "mean": -1.35667, + "mean": -1.36609, "min": -144.49196, - "std": 22.69287 + "std": 22.77138 }, "positive_power_flex_offer": { - "25%": -2.04346, - "50%": 1.11256, - "75%": 9.24657, - "count": 100.0, + "25%": -2.04518, + "50%": 1.11201, + "75%": 9.24185, + "count": 36.0, "max": 59.13035, - "mean": 9.60406, + "mean": 6.83617, "min": -27.77965, - "std": 20.29285 + "std": 22.06284 }, "positive_power_flex_offer_avg": { "25%": 5.84588, @@ -628,6 +618,16 @@ "min": 1.0, "std": 0.0 }, + "time_grid_info": { + "25%": 10895.09619, + "50%": 21600.0, + "75%": 32304.90381, + "count": 384.0, + "max": 43009.80762, + "mean": 21600.0, + "min": 190.19238, + "std": 12487.03557 + }, "time_step_mpc": { "25%": 900.0, "50%": 900.0, @@ -675,7 +675,7 @@ "market_time", "flex_event_duration", "prediction_horizon", - "collocation_time_grid", + "time_grid_info", "time_step_mpc" ], "data": [ diff --git a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json index 54dc6540..491c91d1 100644 --- a/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json +++ b/tests/snapshots/test_oneRoom_SimpleLinRegMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json @@ -86,8 +86,8 @@ NaN, 10.0, 0.0, - 0.0, - 0.0, + NaN, + NaN, NaN, NaN, -0.1505, @@ -101,13 +101,13 @@ 0.0396, 0.0467, 0.3265, - 0.1015, + -0.2546, 0.3265, - 0.1015, + -0.2546, 8.2537, - 2.1717, + -5.4467, 8.2537, - 2.1717, + -5.4467, 900.0, 900.0, 7200.0, @@ -389,44 +389,44 @@ "std": 0.0 }, "negative_corrected_costs": { - "25%": 0.2603, - "50%": 0.3249, + "25%": 0.2861, + "50%": 0.3262, "75%": 0.3273, "count": 4.0, "max": 0.3298, - "mean": 0.2627, - "min": 0.0714, - "std": 0.1276 + "mean": 0.2873, + "min": 0.167, + "std": 0.0802 }, "negative_corrected_costs_rel": { - "25%": 6.5378, - "50%": 8.1551, + "25%": 7.2199, + "50%": 8.1864, "75%": 8.2325, "count": 4.0, "max": 8.2537, - "mean": 6.6151, - "min": 1.8965, - "std": 3.1466 + "mean": 7.2659, + "min": 4.4373, + "std": 1.8863 }, "negative_costs": { - "25%": 0.2603, - "50%": 0.3249, + "25%": 0.2861, + "50%": 0.3262, "75%": 0.3273, "count": 4.0, "max": 0.3298, - "mean": 0.2627, - "min": 0.0714, - "std": 0.1276 + "mean": 0.2873, + "min": 0.167, + "std": 0.0802 }, "negative_costs_rel": { - "25%": 6.5378, - "50%": 8.1551, + "25%": 7.2199, + "50%": 8.1864, "75%": 8.2325, "count": 4.0, "max": 8.2537, - "mean": 6.6151, - "min": 1.8965, - "std": 3.1466 + "mean": 7.2659, + "min": 4.4373, + "std": 1.8863 }, "negative_energy_flex": { "25%": 0.0391, @@ -442,11 +442,11 @@ "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 192.0, + "count": 188.0, "max": 0.2874, "mean": 0.0022, "min": -0.1505, - "std": 0.0837 + "std": 0.0846 }, "negative_power_flex_offer": { "25%": -0.1461, @@ -489,44 +489,44 @@ "std": 0.0021 }, "positive_corrected_costs": { - "25%": 0.0207, - "50%": 0.0986, - "75%": 0.1022, + "25%": -0.3022, + "50%": -0.2566, + "75%": -0.2545, "count": 4.0, - "max": 0.1043, - "mean": 0.0243, - "min": -0.2041, - "std": 0.1523 + "max": -0.254, + "mean": -0.3, + "min": -0.4329, + "std": 0.0886 }, "positive_corrected_costs_rel": { - "25%": 0.4874, - "50%": 2.1132, - "75%": 2.1894, + "25%": -6.4027, + "50%": -5.5097, + "75%": -5.4584, "count": 4.0, - "max": 2.2425, - "mean": 0.5636, - "min": -4.2144, - "std": 3.1863 + "max": -5.4467, + "mean": -6.3514, + "min": -8.9394, + "std": 1.7261 }, "positive_costs": { - "25%": 0.0207, - "50%": 0.0986, - "75%": 0.1022, + "25%": -0.3022, + "50%": -0.2566, + "75%": -0.2545, "count": 4.0, - "max": 0.1043, - "mean": 0.0243, - "min": -0.2041, - "std": 0.1523 + "max": -0.254, + "mean": -0.3, + "min": -0.4329, + "std": 0.0886 }, "positive_costs_rel": { - "25%": 0.4874, - "50%": 2.1132, - "75%": 2.1894, + "25%": -6.4027, + "50%": -5.5097, + "75%": -5.4584, "count": 4.0, - "max": 2.2425, - "mean": 0.5636, - "min": -4.2144, - "std": 3.1863 + "max": -5.4467, + "mean": -6.3514, + "min": -8.9394, + "std": 1.7261 }, "positive_energy_flex": { "25%": 0.0465, @@ -542,11 +542,11 @@ "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 192.0, + "count": 188.0, "max": 0.142, "mean": -0.0002, "min": -0.2866, - "std": 0.0443 + "std": 0.0448 }, "positive_power_flex_offer": { "25%": 0.0, diff --git a/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json b/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json index 677cf185..b94e217f 100644 --- a/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json +++ b/tests/snapshots/test_oneRoom_SimpleMPC/test_oneroom_simple_mpc/oneroom_simpleMPC_indicator_summary.json @@ -86,8 +86,8 @@ NaN, 10.0, 0.0, - 0.0, - 0.0, + NaN, + NaN, NaN, NaN, -0.0599, @@ -442,21 +442,21 @@ "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 580.0, + "count": 576.0, "max": 0.228, "mean": 0.0, "min": -0.15, - "std": 0.0453 + "std": 0.0455 }, "negative_power_flex_offer": { - "25%": -0.0182, + "25%": -0.0234, "50%": 0.0, "75%": 0.0, - "count": 100.0, - "max": 0.228, - "mean": 0.0247, + "count": 36.0, + "max": 0.2122, + "mean": 0.0118, "min": -0.15, - "std": 0.0908 + "std": 0.0995 }, "negative_power_flex_offer_avg": { "25%": 0.0319, @@ -552,21 +552,21 @@ "25%": 0.0, "50%": 0.0, "75%": 0.0, - "count": 580.0, + "count": 576.0, "max": 0.15, "mean": -0.0, "min": -0.2753, - "std": 0.0532 + "std": 0.0534 }, "positive_power_flex_offer": { "25%": 0.0, "50%": 0.0, - "75%": 0.0132, - "count": 100.0, + "75%": 0.0033, + "count": 36.0, "max": 0.15, - "mean": 0.028, + "mean": 0.0154, "min": -0.1416, - "std": 0.0721 + "std": 0.0833 }, "positive_power_flex_offer_avg": { "25%": 0.035,