diff --git a/agentlib_flexquant/data_structures/flex_results.py b/agentlib_flexquant/data_structures/flex_results.py index 4c976c1f..1ceae378 100644 --- a/agentlib_flexquant/data_structures/flex_results.py +++ b/agentlib_flexquant/data_structures/flex_results.py @@ -66,13 +66,14 @@ def load_market(file_path: Union[str, FilePath]) -> pd.DataFrame: class Results: """ Loads the results for the baseline, positive and negative flexibility, - the indicator, market and simulator results/data. Additionally the MPC stats are loaded. + the indicator, market and simulator results/data. Additionally the MPC stats + are loaded. Results can be loaded either from a user-specified custom base path or from the (default) base path specified in the flex config. - Loaded results are stored in pandas DataFrames which can be used for further processing, - e.g. plotting and analysis. + Loaded results are stored in pandas DataFrames which can be used for further + processing, e.g. plotting and analysis. """ # Configs: @@ -186,7 +187,8 @@ def _get_config_filenames(self): if self.flex_config.market_config: if isinstance(self.flex_config.market_config, Union[str, Path]): self.config_filename_market = load_config.load_config( - config=self.flex_config.market_config, config_type=FlexibilityMarketConfig + config=self.flex_config.market_config, + config_type=FlexibilityMarketConfig ).name_of_created_file else: # is dict self.config_filename_market = FlexibilityMarketConfig.model_validate( @@ -194,6 +196,7 @@ def _get_config_filenames(self): def _load_agent_module_configs(self): """Load agent and module configs.""" + files_found = [] for file_path in Path(self.flex_config.flex_files_directory).rglob( "*.json" ): @@ -203,8 +206,10 @@ def _load_agent_module_configs(self): ) self.baseline_module_config = cmng.get_module( config=self.baseline_agent_config, - module_type=self._get_flexquant_mpc_module_type(self.baseline_agent_config), + module_type= + self._get_flexquant_mpc_module_type(self.baseline_agent_config), ) + files_found.append(self.config_filename_baseline) elif file_path.name in self.config_filename_pos_flex: self.pos_flex_agent_config = load_config.load_config( @@ -212,8 +217,10 @@ def _load_agent_module_configs(self): ) self.pos_flex_module_config = cmng.get_module( config=self.pos_flex_agent_config, - module_type=self._get_flexquant_mpc_module_type(self.pos_flex_agent_config), + module_type= + self._get_flexquant_mpc_module_type(self.pos_flex_agent_config), ) + files_found.append(self.config_filename_pos_flex) elif file_path.name in self.config_filename_neg_flex: self.neg_flex_agent_config = load_config.load_config( @@ -221,8 +228,10 @@ def _load_agent_module_configs(self): ) self.neg_flex_module_config = cmng.get_module( config=self.neg_flex_agent_config, - module_type=self._get_flexquant_mpc_module_type(self.neg_flex_agent_config), + module_type= + self._get_flexquant_mpc_module_type(self.neg_flex_agent_config), ) + files_found.append(self.config_filename_neg_flex) elif file_path.name in self.config_filename_indicator: self.indicator_agent_config = load_config.load_config( @@ -232,6 +241,7 @@ def _load_agent_module_configs(self): config=self.indicator_agent_config, module_type=cmng.INDICATOR_CONFIG_TYPE, ) + files_found.append(self.config_filename_indicator) elif ( self.flex_config.market_config @@ -243,16 +253,18 @@ def _load_agent_module_configs(self): self.market_module_config = cmng.get_module( config=self.market_agent_config, module_type=cmng.MARKET_CONFIG_TYPE ) - else: - import warnings - warnings.warn(f"The file {file_path.name} is not listed in any of the known " - f"filenames of the created files: {self.config_filename_baseline}, " - f"{self.config_filename_pos_flex}, {self.config_filename_neg_flex}, " - f"{self.config_filename_indicator}" + - (f", {self.config_filename_market}" if - self.flex_config.market_config else "") + - ". This can cause trouble in loading the files. " - "Please check the filenames.") + files_found.append(self.config_filename_market) + files_needed = [self.config_filename_baseline, + self.config_filename_pos_flex, self.config_filename_neg_flex, + self.config_filename_indicator] + if self.flex_config.market_config: + files_needed.append(self.config_filename_market) + difference = list(set(files_needed) - set(files_found)) + if difference: + import warnings + warnings.warn(f"The files {difference} have not been found in the " + f"given Path. This will most likely cause problems " + f"later on. Please check the filenames.") def _load_simulator_config(self, simulator_agent_config): """Load simulator agent and module config separately. @@ -281,11 +293,12 @@ def _load_simulator_config(self, simulator_agent_config): self.simulator_agent_config = AgentConfig.model_validate(sim_config) # instantiate sim module config by skipping validation for result_filename # to prevent file deletion, if overwrite_result_file in sim config is true - self.simulator_module_config = self.create_simulator_config_with_skipped_validation( - sim_config_class=SimulatorConfig, - sim_config=sim_module_config, - skip_fields=["result_filename"], - ) + self.simulator_module_config = ( + self.create_simulator_config_with_skipped_validation( + sim_config_class=SimulatorConfig, + sim_config=sim_module_config, + skip_fields=["result_filename"], + )) def _get_flexquant_mpc_module_type(self, agent_config: AgentConfig) -> str: """Get the mpc module type from agent_config. @@ -300,12 +313,15 @@ def _get_flexquant_mpc_module_type(self, agent_config: AgentConfig) -> str: """ for module in agent_config.modules: - if module['type'] in [cmng.BASELINEMPC_CONFIG_TYPE, cmng.BASELINEMINLPMPC_CONFIG_TYPE, - cmng.SHADOWMPC_CONFIG_TYPE, cmng.SHADOWMINLPMPC_CONFIG_TYPE]: + if module['type'] in [cmng.BASELINEMPC_CONFIG_TYPE, + cmng.BASELINEMINLPMPC_CONFIG_TYPE, + cmng.SHADOWMPC_CONFIG_TYPE, + cmng.SHADOWMINLPMPC_CONFIG_TYPE]: return module['type'] - raise ModuleNotFoundError(f'There is no matching mpc module type in Agentlib_FlexQuant for ' - f'modules in agent {agent_config.id}.') + raise ModuleNotFoundError(f'There is no matching mpc module type in ' + f'Agentlib_FlexQuant for modules in agent ' + f'{agent_config.id}.') def _resolve_sim_results_path( self, sim_result_filename: str, results_path: Union[str, Path] @@ -340,8 +356,8 @@ def _resolve_sim_results_path( if not sim_results_path.is_absolute() and sim_results_path.exists(): return sim_results_path - # Strategy 3: Try in results directory (handles both relative paths and just filenames) - # (fallback for helper function usage) + # Strategy 3: Try in results directory (handles both relative paths + # and just filenames) (fallback for helper function usage) results_dir_path = results_path / sim_results_path.name if results_dir_path.exists(): return results_dir_path @@ -514,8 +530,9 @@ def get_intersection_mpcs_sim(self) -> dict[str, dict[str, str]]: """Get the intersection of the MPCs and the simulator variables. Returns: - dictionary with the following structure: Key: variable alias (from baseline) - Value: {module id: variable name} + dictionary with the following structure: + Key: variable alias (from baseline) + Value: {module id: variable name} """ id_alias_name_dict = {} @@ -552,11 +569,13 @@ def create_simulator_config_with_skipped_validation( sim_config: Dict[str, Any], skip_fields: Optional[list[str]] = None, ) -> SimulatorConfig: - """Create a Pydantic model instance while skipping validation for specified fields. + """Create a Pydantic model instance while skipping validation for + specified fields. - This function allows partial validation of a model's config dictionary by validating - all fields except those listed in `skip_fields`. Skipped fields are set on the instance - after construction without triggering their validators. + This function allows partial validation of a model's config dictionary + by validating all fields except those listed in `skip_fields`. + Skipped fields are set on the instance after construction without + triggering their validators. Args: sim_config_class: The Pydantic model class to instantiate. @@ -565,17 +584,20 @@ def create_simulator_config_with_skipped_validation( These fields will be manually set after instantiation. Returns: - SimulatorConfig: An instance of the model_class with validated and skipped fields assigned. + SimulatorConfig: An instance of the model_class with validated and + skipped fields assigned. """ if skip_fields is None: skip_fields = [] # Separate data into validated and skipped fields validated_fields = { - field: value for field, value in sim_config.items() if field not in skip_fields + field: value for field, value in sim_config.items() if + field not in skip_fields } skipped_fields = { - field: value for field, value in sim_config.items() if field in skip_fields + field: value for field, value in sim_config.items() if + field in skip_fields } # Create instance with validation for non-skipped fields if validated_fields: @@ -596,8 +618,8 @@ def create_simulator_config_with_skipped_validation( def __deepcopy__(self, memo: Dict[int, Any]) -> "Results": """Custom deepcopy implementation that handles Pydantic models with bypassed validation. - Needed, if an Results object should be copied with copy.deepcopy, without - deleting the simulator results due to its üydantic validators. + Needed, if a Results object should be copied with copy.deepcopy, without + deleting the simulator results due to its pydantic validators. """ # Create a new instance of the same class new_instance = self.__class__.__new__(self.__class__) diff --git a/agentlib_flexquant/data_structures/flexquant.py b/agentlib_flexquant/data_structures/flexquant.py index 2cd04447..afbc7cf4 100644 --- a/agentlib_flexquant/data_structures/flexquant.py +++ b/agentlib_flexquant/data_structures/flexquant.py @@ -24,7 +24,6 @@ "description", "unit", "clip", - "shared", "interpolation_method", "allowed_values", ] @@ -47,11 +46,13 @@ def assign_weights_to_flex(self): """Validate flexibility cost function fields and assign weights to them.""" if self.pos_flex is None: raise ValueError( - "Missing required field: 'pos_flex' specifying the pos flex cost function." + "Missing required field: 'pos_flex' specifying the pos flex " + "cost function." ) if self.neg_flex is None: raise ValueError( - "Missing required field: 'neg_flex' specifying the neg flex cost function." + "Missing required field: 'neg_flex' specifying the neg flex " + "cost function." ) if self.weights: self.pos_flex.weights = self.weights @@ -143,10 +144,10 @@ class FlexQuantConfig(BaseModel): ) casadi_sim_time_step: int = Field( default=0, - description="Simulate over the prediction horizon with a defined resolution using Casadi " - "simulator. " - "Only use it when the power depends on the states. Don't use it when power " - "itself is the control variable." + description="Simulate over the prediction horizon with a defined resolution " + "using Casadi simulator. " + "Only use it when the power depends on the states. " + "Don't use it when power itself is the control variable." "Set to 0 to skip simulation", ) flex_base_directory_path: Optional[Path] = Field( @@ -171,7 +172,8 @@ class FlexQuantConfig(BaseModel): @model_validator(mode="after") def check_config_file_extension(self): - """Validate that the indicator and market config file paths have a '.json' extension. + """Validate that the indicator and market config file paths have a '.json' + extension. Raises: ValueError: If either file does not have the expected '.json' extension. @@ -182,7 +184,8 @@ def check_config_file_extension(self): and self.indicator_config.suffix != ".json" ): raise ValueError( - f"Invalid file extension for indicator config: '{self.indicator_config}'. " + f"Invalid file extension for indicator " + f"config: '{self.indicator_config}'. " f"Expected a '.json' file." ) if ( @@ -190,7 +193,8 @@ def check_config_file_extension(self): and self.market_config.suffix != ".json" ): raise ValueError( - f"Invalid file extension for market config: '{self.market_config}'. " + f"Invalid file extension for market " + f"config: '{self.market_config}'. " f"Expected a '.json' file." ) return self @@ -204,12 +208,16 @@ def is_none_negative_integer(cls, value: int) -> int: @model_validator(mode="after") def adapt_paths_and_create_directory(self): - """Adjust and ensure the directory structure for flex file generation and results storage. + """Adjust and ensure the directory structure for flex file generation and + results storage. This method: - - Updates `flex_files_directory` and `results_directory` paths, so they are relative to - the base flex directory, using only the directory names (ignoring any user-supplied paths). - - Creates the base, flex files, and results directories if they do not already exist. + - Updates `flex_files_directory` and `results_directory` paths, so they are + relative to + the base flex directory, using only the directory names (ignoring any + user-supplied paths). + - Creates the base, flex files, and results directories if they do not + already exist. """ # adapt paths and use only names for user supplied data diff --git a/agentlib_flexquant/data_structures/globals.py b/agentlib_flexquant/data_structures/globals.py index e99da14b..fced1ce3 100644 --- a/agentlib_flexquant/data_structures/globals.py +++ b/agentlib_flexquant/data_structures/globals.py @@ -23,9 +23,13 @@ STORED_ENERGY_ALIAS_NEG = "_E_stored_neg" STORED_ENERGY_ALIAS_POS = "_E_stored_pos" full_trajectory_suffix: str = "_full" -full_trajectory_prefix: str = "_" +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 @@ -51,19 +55,19 @@ def return_baseline_cost_function(power_variable: str, comfort_variable: str) -> """ 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"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._P_external)**2, " + 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"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._P_external)**2]))),obj_std)" + f"self.{ACCEPTED_POWER_VAR_NAME})**2]))),obj_std)" ) return cost_func diff --git a/agentlib_flexquant/data_structures/mpcs.py b/agentlib_flexquant/data_structures/mpcs.py index b316bf7d..3883d855 100644 --- a/agentlib_flexquant/data_structures/mpcs.py +++ b/agentlib_flexquant/data_structures/mpcs.py @@ -21,7 +21,6 @@ "description", "unit", "clip", - "shared", "interpolation_method", "allowed_values", ] @@ -38,6 +37,7 @@ class BaseMPCData(pydantic.BaseModel): module_types: dict class_name: str module_id: str + agent_id: str # variables power_alias: str stored_energy_alias: str @@ -62,6 +62,7 @@ class BaselineMPCData(BaseMPCData): module_types: dict = cmng.BASELINE_MODULE_TYPE_DICT class_name: str = "BaselineMPCModel" module_id: str = "Baseline" + agent_id: str = "Baseline" # variables power_alias: str = glbs.POWER_ALIAS_BASE stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_BASE @@ -88,10 +89,10 @@ class BaselineMPCData(BaseMPCData): default=1, description="Weight of soft constraint for discomfort", ) config_inputs_appendix: list[MPCVariable] = [ - MPCVariable(name="_P_external", value=0, unit="W"), - MPCVariable(name="in_provision", value=False), - MPCVariable(name="rel_start", value=0, unit="s"), - MPCVariable(name="rel_end", value=0, unit="s"), + MPCVariable(name=glbs.ACCEPTED_POWER_VAR_NAME, value=0, unit="W"), + MPCVariable(name=glbs.PROVISION_VAR_NAME, value=False), + MPCVariable(name=glbs.RELATIVE_EVENT_START_TIME_VAR_NAME, value=0, unit="s"), + MPCVariable(name=glbs.RELATIVE_EVENT_END_TIME_VAR_NAME, value=0, unit="s"), ] config_parameters_appendix: list[MPCVariable] = [] @@ -144,6 +145,7 @@ class PFMPCData(BaseMPCData): module_types: dict = cmng.SHADOW_MODULE_TYPE_DICT class_name: str = "PosFlexModel" module_id: str = "PosFlexMPC" + agent_id: str = "PosFlexMPC" # variables power_alias: str = glbs.POWER_ALIAS_POS stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_POS @@ -157,7 +159,7 @@ class PFMPCData(BaseMPCData): MPCVariable(name=glbs.FLEX_EVENT_DURATION, value=0, unit="s") ] config_inputs_appendix: list[MPCVariable] = [ - MPCVariable(name="in_provision", value=False), + MPCVariable(name=glbs.PROVISION_VAR_NAME, value=False), ] weights: list[MPCVariable] = pydantic.Field( default=[], description="Name and value of weights", @@ -178,6 +180,7 @@ class NFMPCData(BaseMPCData): module_types: dict = cmng.SHADOW_MODULE_TYPE_DICT class_name: str = "NegFlexModel" module_id: str = "NegFlexMPC" + agent_id: str = "NegFlexMPC" # variables power_alias: str = glbs.POWER_ALIAS_NEG stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_NEG @@ -191,7 +194,7 @@ class NFMPCData(BaseMPCData): MPCVariable(name=glbs.FLEX_EVENT_DURATION, value=0, unit="s") ] config_inputs_appendix: list[MPCVariable] = [ - MPCVariable(name="in_provision", value=False), + MPCVariable(name=glbs.PROVISION_VAR_NAME, value=False), ] weights: list[MPCVariable] = pydantic.Field( default=[], description="Name and value of weights", diff --git a/agentlib_flexquant/generate_flex_agents.py b/agentlib_flexquant/generate_flex_agents.py index c87782f2..ecdefeee 100644 --- a/agentlib_flexquant/generate_flex_agents.py +++ b/agentlib_flexquant/generate_flex_agents.py @@ -93,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( @@ -167,42 +175,47 @@ def generate_flex_agents(self) -> list[str]: 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.baseline_mpc_agent_config.id, + 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.pos_flex_mpc_agent_config.id, + 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.neg_flex_mpc_agent_config.id, + 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) + 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, @@ -248,9 +261,6 @@ def append_module_and_dump_agent( 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 @@ -262,7 +272,8 @@ def append_module_and_dump_agent( if agent.modules: if self.flex_config.overwrite_files: try: - Path(os.path.join(self.flex_config.flex_files_directory, config_name)).unlink() + Path(os.path.join(self.flex_config.flex_files_directory, + config_name)).unlink() except OSError: pass with open( @@ -280,15 +291,18 @@ def get_config_file_paths(self) -> list[str]: paths = [ os.path.join( self.flex_config.flex_files_directory, - self.flex_config.baseline_config_generator_data.name_of_created_file, + self.flex_config.baseline_config_generator_data. + name_of_created_file, ), os.path.join( self.flex_config.flex_files_directory, - self.flex_config.shadow_mpc_config_generator_data.pos_flex.name_of_created_file, + self.flex_config.shadow_mpc_config_generator_data.pos_flex. + name_of_created_file, ), os.path.join( self.flex_config.flex_files_directory, - self.flex_config.shadow_mpc_config_generator_data.neg_flex.name_of_created_file, + self.flex_config.shadow_mpc_config_generator_data.neg_flex. + name_of_created_file, ), os.path.join( self.flex_config.flex_files_directory, @@ -334,9 +348,10 @@ def adapt_mpc_module_config( 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 + 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 @@ -350,16 +365,15 @@ def adapt_mpc_module_config( 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 + # 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_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 ) @@ -370,7 +384,8 @@ def adapt_mpc_module_config( 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_flex.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 @@ -387,7 +402,8 @@ def adapt_mpc_module_config( ), "class_name": mpc_dataclass.class_name, } - # extract filename from results file and update it with suffix and parent directory + # 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) @@ -396,9 +412,11 @@ def adapt_mpc_module_config( # change cia backend to custom backend of flexquant 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 + 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 + # 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( @@ -408,8 +426,11 @@ def adapt_mpc_module_config( type="pd.Series", ) ) - # change the alias of control variable in shadow mpc to prevent it from triggering - # the wrong callback + # 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_flex, "binary_controls"): @@ -421,13 +442,44 @@ def adapt_mpc_module_config( type="pd.Series", ) ) - # change the alias of control variable in shadow mpc to prevent it from - # triggering the wrong callback + # 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_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: - # add full_controls trajectory as AgentVariable to the config of Baseline mpc + # 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( @@ -445,6 +497,25 @@ def adapt_mpc_module_config( shared=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_flex.outputs} @@ -468,7 +539,8 @@ def adapt_mpc_module_config( # 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) @@ -484,7 +556,10 @@ def adapt_mpc_module_config( def adapt_indicator_module_config( self, module_config: FlexibilityIndicatorModuleConfig ) -> FlexibilityIndicatorModuleConfig: - """Adapt 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( @@ -508,14 +583,15 @@ def adapt_indicator_module_config( if parameter.name == glbs.PREDICTION_HORIZON: parameter.value = self.baseline_mpc_module_config.prediction_horizon if parameter.name == glbs.COLLOCATION_TIME_GRID: - discretization_options = self.baseline_mpc_module_config.optimization_backend[ + dis_op = self.baseline_mpc_module_config.optimization_backend[ "discretization_options" ] parameter.value = self.get_collocation_time_grid( - discretization_options=discretization_options + discretization_options=dis_op ) # set power unit - module_config.power_unit = self.flex_config.baseline_config_generator_data.power_unit + module_config.power_unit = ( + self.flex_config.baseline_config_generator_data.power_unit) module_config.results_file = ( self.flex_config.results_directory / module_config.results_file.name ) @@ -530,17 +606,18 @@ def adapt_market_module_config( 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.__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: - discretization_options = self.baseline_mpc_module_config.optimization_backend[ + dis_op = self.baseline_mpc_module_config.optimization_backend[ "discretization_options" ] parameter.value = self.get_collocation_time_grid( - discretization_options=discretization_options + discretization_options=dis_op ) if parameter.name == glbs.TIME_STEP: parameter.value = self.baseline_mpc_module_config.time_step @@ -581,7 +658,8 @@ def get_collocation_time_grid(self, discretization_options: dict): options = CasadiDiscretizationOptions( collocation_order=collocation_order, collocation_method=collocation_method ) - collocation_points = DirectCollocation(options=options)._collocation_polynomial().root + 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 = ( @@ -682,21 +760,24 @@ 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 + 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 + 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. + 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. @@ -707,7 +788,8 @@ def run_config_validations(self): 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. + 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 @@ -716,20 +798,18 @@ def run_config_validations(self): """ # 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]: + 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 = self.baseline_mpc_module_config.optimization_backend["model"]["type"][ - "file" - ] - class_name = self.baseline_mpc_module_config.optimization_backend["model"]["type"][ - "class_name" - ] + 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 [ @@ -754,14 +834,15 @@ 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 + # 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" + "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[ @@ -769,7 +850,8 @@ def run_config_validations(self): ]["collocation_method"] if collocation_method != "legendre": self.logger.warning( - "Collocation method %s is not supported. Switching to method legendre.", + "Collocation method %s is not supported. Switching to " + "method legendre.", collocation_method, ) self.baseline_mpc_module_config.optimization_backend["discretization_options"][ @@ -793,7 +875,8 @@ def run_config_validations(self): 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"]: + 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." @@ -855,12 +938,12 @@ def adapt_sim_results_path(self, simulator_agent_config: Union[str, Path], 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, - ) + (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 + # 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 ) @@ -870,5 +953,7 @@ def adapt_sim_results_path(self, simulator_agent_config: Union[str, Path], 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 due to: {e}. " - f"Please check {simulator_agent_config} and '{save_name_suffix}'") + 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}'") diff --git a/agentlib_flexquant/modules/baseline_mpc.py b/agentlib_flexquant/modules/baseline_mpc.py index 5fd93d31..e81b2686 100644 --- a/agentlib_flexquant/modules/baseline_mpc.py +++ b/agentlib_flexquant/modules/baseline_mpc.py @@ -12,15 +12,18 @@ import agentlib_flexquant.data_structures.globals as glbs from agentlib import AgentVariable from agentlib_mpc.modules import mpc_full, minlp_mpc -from agentlib_mpc.data_structures.mpc_datamodels import Results -from agentlib_flexquant.data_structures.globals import full_trajectory_suffix +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) class FlexibilityBaselineMPCConfig(mpc_full.MPCConfig): - # define an AgentVariable list for the full control trajectory, since use MPCVariable output - # affects the optimization result + # define an AgentVariable list for the full control trajectory full_controls: list[AgentVariable] = Field(default=[]) + # define an AgentVariable list for the variables to communicate to the shadow MPCs + vars_to_communicate: list[AgentVariable] = Field(default=[]) + casadi_sim_time_step: int = Field( default=0, description="Time step for simulation with Casadi" @@ -28,10 +31,12 @@ class FlexibilityBaselineMPCConfig(mpc_full.MPCConfig): "FlexQuantConfig", ) power_variable_name: str = Field( - default=None, description="Name of the power variable in the " "baseline mpc model." + default=None, description="Name of the power variable in the " + "baseline mpc model." ) storage_variable_name: Optional[str] = Field( - default=None, description="Name of the storage v" "ariable in the baseline " "mpc model." + default=None, description="Name of the storage variable in the " + "baseline mpc model." ) @@ -42,19 +47,23 @@ class FlexibilityBaselineMPC(mpc_full.MPC): def __init__(self, config, agent): super().__init__(config, agent) - # initialize a control mapping dictionary which maps the full control names to the control - # names + # initialize a control mapping dictionary which maps the names of the + # incoming AgentVariables (with suffix) to the names without suffix self._controls_name_mapping: Dict[str, str] = {} + self._vars_to_com_name_mapping: Dict[str, str] = {} for full_control in self.config.full_controls: - # add full_control to the variables dictionary, so that the set function can be applied - # to it - self._variables_dict[full_control.name] = full_control # fill the mapping dictionary self._controls_name_mapping[full_control.name] = full_control.name.replace( full_trajectory_suffix, "" ) + for vars_to_com in self.config.vars_to_communicate: + # fill the mapping dictionary + self._vars_to_com_name_mapping[vars_to_com.name] = vars_to_com.name.replace( + base_vars_to_communicate_suffix, "" + ) + # initialize flex_results with None self.flex_results = None # set up necessary components if simulation is enabled @@ -64,14 +73,38 @@ def __init__(self, config, agent): 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( - "mpc", "mpc_sim" + "_base", "_sim_base" ) # clear the casadi simulator result at the first time step if already exists try: os.remove(self.res_file_flex) - except: + except FileNotFoundError: pass + def do_step(self): + """ + Performs an MPC step. + """ + if not self.init_status == InitStatus.ready: + self.logger.warning("Skipping step, optimization_backend is not ready.") + return + + self.pre_computation_hook() + + # get new values from data_broker + updated_vars = self.collect_variables_for_optimization() + + # solve optimization problem with up-to-date values from data_broker + result = self.optimization_backend.solve(self.env.time, updated_vars) + + # Set variables in data_broker + self.set_actuation(result) + # Set variables, so that shadow MPCs are initialized with the + # same values as the Baseline + self.set_vars_for_shadow() + self.set_output(result) + self._remove_old_values_from_history() + def pre_computation_hook(self): """Calculate relative start and end times for flexibility provision. @@ -79,11 +112,25 @@ def pre_computation_hook(self): events based on the external power profile timestamps and current environment time. """ - if self.get("in_provision").value: - self.set("rel_start", self.get("_P_external").value.index[0] - self.env.time) + if self.get(glbs.PROVISION_VAR_NAME).value: + self.set(glbs.RELATIVE_EVENT_START_TIME_VAR_NAME, + self.get(glbs.ACCEPTED_POWER_VAR_NAME).value.index[0] - + self.env.time) # the provision profile gives a value for the start of a time step. # For the end of the flex interval add time step! - self.set("rel_end", self.get("_P_external").value.index[-1] - self.env.time) + self.set(glbs.RELATIVE_EVENT_END_TIME_VAR_NAME, + self.get(glbs.ACCEPTED_POWER_VAR_NAME).value.index[-1] - + self.env.time) + + def set_vars_for_shadow(self): + """Sets the variables of the Baseline MPC needed by the shadow MPCs + with a predefined suffix. + This essentially sends the same inputs and states the Baseline used + for optimization to the Shadow MPC, ensuring synchronisation. + """ + for vars_to_com in self.config.vars_to_communicate: + vars_name = self._vars_to_com_name_mapping[vars_to_com.name] + self.set(vars_to_com.name, self.get_value(vars_name)) def set_actuation(self, solution: Results): super().set_actuation(solution) @@ -94,7 +141,10 @@ def set_actuation(self, solution: Results): self.set(full_control.name, solution.df.variable[control].ffill()) def set_output(self, solution): - """Takes the solution from optimization backend and sends it to AgentVariables.""" + """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 @@ -119,18 +169,23 @@ def set_output(self, solution): 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()) + 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""" + """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("in_provision").value): + 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 @@ -155,7 +210,8 @@ def sim_flex_model(self, solution): # Run simulation self._run_simulation( - n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + 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 @@ -176,6 +232,7 @@ def _initialize_flex_results( ): """Initialize the flex results dataframe with the correct dimension and index and fill with existing results from optimization + """ # create MultiIndex for collocation points @@ -195,23 +252,28 @@ def _initialize_flex_results( # 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) + 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 + # 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 + # 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] + 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 + since creating a model just reads the value in the model class but + not the config + """ for par in self.config.parameters: @@ -227,20 +289,26 @@ def _update_initial_states(self, result_df): self.flex_model.set(state, value) def _run_simulation( - self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, + total_horizon_time ): - """simulate with flex model over the prediction horizon""" + """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) + 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") + 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 @@ -254,13 +322,15 @@ def _run_simulation( 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 + # 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 + # 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: @@ -275,61 +345,93 @@ def _run_simulation( class FlexibilityBaselineMINLPMPCConfig(minlp_mpc.MINLPMPCConfig): - - # define an AgentVariable list for the full control trajectory, since use MPCVariable output - # affects the optimization result + # define an AgentVariable list for the full control trajectory full_controls: list[AgentVariable] = Field(default=[]) + # define an AgentVariable list for the variables to communicate to the shadow MPCs + vars_to_communicate: list[AgentVariable] = Field(default=[]) + casadi_sim_time_step: int = Field( default=0, - description="Time step for simulation with Casadi simulator. Value is read from " - "FlexQuantConfig", + 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 baseline mpc model." + default=None, description="Name of the power variable in the " + "baseline mpc model." ) storage_variable_name: Optional[str] = Field( - default=None, description="Name of the storage variable in the baseline mpc model." + default=None, description="Name of the storage variable in the " + "baseline mpc model." ) class FlexibilityBaselineMINLPMPC(minlp_mpc.MINLPMPC): - """MINLP-MPC for baseline flexibility quantification with mixed-integer optimization.""" + """MINLP-MPC for baseline flexibility quantification with mixed-integer + optimization. + + """ config: FlexibilityBaselineMINLPMPCConfig def __init__(self, config, agent): super().__init__(config, agent) - # initialize a control mapping dictionary which maps the full control names to the control - # names + # initialize a control mapping dictionary which maps the names of the + # incoming AgentVariables (with suffix) to the names without suffix self._controls_name_mapping: Dict[str, str] = {} + self._vars_to_com_name_mapping: Dict[str, str] = {} for full_control in self.config.full_controls: - # add full_control to the variables dictionary, so that the set function can be applied - # to it - self._variables_dict[full_control.name] = full_control # fill the mapping dictionary self._controls_name_mapping[full_control.name] = full_control.name.replace( full_trajectory_suffix, "" ) + for vars_to_com in self.config.vars_to_communicate: + # fill the mapping dictionary + self._vars_to_com_name_mapping[vars_to_com.name] = vars_to_com.name.replace( + base_vars_to_communicate_suffix, "" + ) + # initialize flex_results with None self.flex_results = None # set up necessary components if simulation is enabled if self.config.casadi_sim_time_step > 0: - # generate a separate flex_model for integration to ensure the model used in MPC - # optimization remains unaffected + # generate a separate flex_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( - "mpc", "mpc_sim" - ) + self.res_file_flex = self.config.optimization_backend[ + "results_file"].replace("_base", "_sim_base") # clear the casadi simulator result at the first time step if already exists try: os.remove(self.res_file_flex) - except: + except FileNotFoundError: pass + def do_step(self): + """ + Performs an MPC step. + """ + if not self.init_status == InitStatus.ready: + self.logger.warning("Skipping step, optimization_backend is not ready.") + return + + self.pre_computation_hook() + + # get new values from data_broker + updated_vars = self.collect_variables_for_optimization() + + # solve optimization problem with up-to-date values from data_broker + result = self.optimization_backend.solve(self.env.time, updated_vars) + + # Set variables in data_broker + self.set_actuation(result) + # Set variables, so that shadow MPCs are initialized + # with the same values as the Baseline + self.set_vars_for_shadow() + self.set_output(result) + def pre_computation_hook(self): """Calculate relative start and end times for flexibility provision. @@ -337,17 +439,27 @@ def pre_computation_hook(self): events based on the external power profile timestamps and current environment time. """ - if self.get("in_provision").value: - timestep = ( - self.get("_P_external").value.index[1] - self.get("_P_external").value.index[0] - ) - self.set("rel_start", self.get("_P_external").value.index[0] - self.env.time) + if self.get(glbs.PROVISION_VAR_NAME).value: + timestep = (self.get(glbs.ACCEPTED_POWER_VAR_NAME).value.index[1] - + self.get(glbs.ACCEPTED_POWER_VAR_NAME).value.index[0]) + self.set(glbs.RELATIVE_EVENT_START_TIME_VAR_NAME, + self.get(glbs.ACCEPTED_POWER_VAR_NAME).value.index[0] - + self.env.time) # the provision profile gives a value for the start of a time step. # For the end of the flex interval add time step! - self.set( - "rel_end", - self.get("_P_external").value.index[-1] - self.env.time + timestep, - ) + self.set(glbs.RELATIVE_EVENT_END_TIME_VAR_NAME, + self.get(glbs.ACCEPTED_POWER_VAR_NAME).value.index[-1] - + self.env.time + timestep,) + + def set_vars_for_shadow(self): + """Sets the variables of the Baseline MPC needed by the shadow MPCs + with a predefined suffix. + This essentially sends the same inputs and states the Baseline used + for optimization to the Shadow MPC, ensuring synchronisation. + """ + for vars_to_com in self.config.vars_to_communicate: + vars_name = self._vars_to_com_name_mapping[vars_to_com.name] + self.set(vars_to_com.name, self.get_value(vars_name)) def set_actuation(self, solution: Results): super().set_actuation(solution) @@ -358,7 +470,10 @@ def set_actuation(self, solution: Results): self.set(full_control.name, solution.df.variable[control].ffill()) def set_output(self, solution): - """Takes the solution from optimization backend and sends it to AgentVariables.""" + """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 @@ -377,21 +492,27 @@ def set_output(self, solution): 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] + 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()) + 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""" + """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("in_provision").value): + 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 @@ -416,7 +537,8 @@ def sim_flex_model(self, solution): # Run simulation self._run_simulation( - n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + 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 @@ -435,8 +557,10 @@ def sim_flex_model(self, solution): 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""" + """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( @@ -455,23 +579,28 @@ def _initialize_flex_results( # 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) + 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 + # 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 + # 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] + 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 + since creating a model just reads the value in the model class but + not the config + """ for par in self.config.parameters: @@ -487,9 +616,12 @@ def _update_initial_states(self, result_df): self.flex_model.set(state, value) def _run_simulation( - self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time + self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, + total_horizon_time ): - """simulate with flex model over the prediction horizon""" + """simulate with flex model over the prediction horizon + + """ # get control and input values from the mpc optimization result control_values = result_df.variable[ @@ -498,11 +630,14 @@ def _run_simulation( 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) + 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") + 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 @@ -523,13 +658,15 @@ def _run_simulation( 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 + # 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 + # 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: diff --git a/agentlib_flexquant/modules/flexibility_indicator.py b/agentlib_flexquant/modules/flexibility_indicator.py index 8048a96a..73318deb 100644 --- a/agentlib_flexquant/modules/flexibility_indicator.py +++ b/agentlib_flexquant/modules/flexibility_indicator.py @@ -26,14 +26,17 @@ class InputsForCorrectFlexCosts(BaseModel): - """Configuration for flexibility cost correction.""" + """Configuration for flexibility cost correction. + + """ enable_energy_costs_correction: bool = Field( name="enable_energy_costs_correction", description=( - "Variable determining whether to correct the costs of the flexible energy " - "Define the variable for stored electrical energy in the base MPC model and " - "config as output if the correction of costs is enabled" + "Variable determining whether to correct the costs of the " + "flexible energy. Define the variable for stored electrical " + "energy in the base MPC model and config as output if the " + "correction of costs is enabled" ), default=False, ) @@ -47,13 +50,16 @@ class InputsForCorrectFlexCosts(BaseModel): stored_energy_variable: Optional[str] = Field( name="stored_energy_variable", default=None, - description="Name of the variable representing the stored electrical energy in the " - "baseline config" + description="Name of the variable representing the stored electrical energy " + "in the baseline config" ) class InputsForCalculateFlexCosts(BaseModel): - """Configuration for flexibility cost calculation with optional constant pricing.""" + """Configuration for flexibility cost calculation with optional constant + pricing. + + """ use_constant_electricity_price: bool = Field( default=False, description="Use constant electricity price" @@ -74,11 +80,12 @@ def validate_constant_price(self): ): raise ValueError( ( - f"Constant electricity price must have a valid value in float if it is " - f"to be used for calculation. " + f"Constant electricity price must have a valid value in " + f"float if it is to be used for calculation. " f'Received "use_constant_electricity_price": true, ' f'"const_electricity_price": {self.const_electricity_price}. ' - f'Please specify them correctly in "calculate_costs" field in flex config.' + f'Please specify them correctly in "calculate_costs" ' + f'field in flex config.' ) ) return self @@ -91,7 +98,9 @@ def validate_constant_price(self): class FlexibilityIndicatorModuleConfig(agentlib.BaseModuleConfig): """Configuration for flexibility indicator module with power/energy inputs, - KPI outputs, and cost calculation settings.""" + KPI outputs, and cost calculation settings. + + """ model_config = ConfigDict(extra="forbid") @@ -291,7 +300,8 @@ 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, + agentlib.AgentVariable(name=glbs.COLLOCATION_TIME_GRID, + alias=glbs.COLLOCATION_TIME_GRID, description="Time grid of the mpc model output") ] @@ -363,7 +373,8 @@ def register_callbacks(self): name=var.name, alias=var.name, callback=self.callback ) self.agent.data_broker.register_callback( - name="in_provision", alias="in_provision", callback=self.callback + name=glbs.PROVISION_VAR_NAME, alias=glbs.PROVISION_VAR_NAME, + callback=self.callback ) def process(self): @@ -373,7 +384,7 @@ def process(self): def callback(self, inp, name): """Handle incoming data by storing power/energy profiles and triggering flexibility calculations when all required inputs are available.""" - if name == "in_provision": + if name == glbs.PROVISION_VAR_NAME: self.in_provision = inp.value if self.in_provision: self._set_inputs_to_none() @@ -388,14 +399,16 @@ def callback(self, inp, name): elif name == glbs.STORED_ENERGY_ALIAS_BASE: self.data.stored_energy_profile_base = self.data.unify_inputs(inp.value) elif name == glbs.STORED_ENERGY_ALIAS_NEG: - self.data.stored_energy_profile_flex_neg = self.data.unify_inputs(inp.value) + self.data.stored_energy_profile_flex_neg = ( + self.data.unify_inputs(inp.value)) elif name == glbs.STORED_ENERGY_ALIAS_POS: - self.data.stored_energy_profile_flex_pos = self.data.unify_inputs(inp.value) + self.data.stored_energy_profile_flex_pos = ( + self.data.unify_inputs(inp.value)) elif name == self.config.price_variable: if not self.config.calculate_costs.use_constant_electricity_price: # price comes from predictor - self.data.electricity_price_series = self.data.unify_inputs(inp.value, - mpc=False) + self.data.electricity_price_series = ( + self.data.unify_inputs(inp.value, mpc=False)) # set the constant electricity price series if given if ( @@ -408,7 +421,8 @@ def callback(self, inp, name): grid = np.arange(0, n * ts + ts, ts) # fill the electricity_price_series with values self.data.electricity_price_series = pd.Series( - [self.config.calculate_costs.const_electricity_price for i in grid], index=grid) + [self.config.calculate_costs.const_electricity_price + for i in grid], index=grid) necessary_input_for_calc_flex = [ self.data.power_profile_base, @@ -423,8 +437,9 @@ def callback(self, inp, name): len(necessary_input_for_calc_flex) == 4): # align the index of price variable to the index of inputs from mpc; # electricity price signal is usually steps - necessary_input_for_calc_flex[-1] = self.data.electricity_price_series.reindex( - self.data.power_profile_base.index).ffill() + necessary_input_for_calc_flex[-1] = ( + self.data.electricity_price_series.reindex( + self.data.power_profile_base.index).ffill()) if self.config.correct_costs.enable_energy_costs_correction: necessary_input_for_calc_flex.extend( @@ -558,7 +573,8 @@ def calc_and_send_offer(self): # Calculate the flexibility KPIs for current predictions collocation_time_grid = self.get(glbs.COLLOCATION_TIME_GRID).value self.data.calculate( - enable_energy_costs_correction=self.config.correct_costs.enable_energy_costs_correction, + 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) diff --git a/agentlib_flexquant/modules/flexibility_market.py b/agentlib_flexquant/modules/flexibility_market.py index 538d61cc..51a54714 100644 --- a/agentlib_flexquant/modules/flexibility_market.py +++ b/agentlib_flexquant/modules/flexibility_market.py @@ -21,21 +21,22 @@ class FlexibilityMarketModuleConfig(agentlib.BaseModuleConfig): outputs: list[AgentVariable] = [ AgentVariable( - name="_P_external", alias="_P_external", description="External Power IO" + name=glbs.ACCEPTED_POWER_VAR_NAME, alias=glbs.ACCEPTED_POWER_VAR_NAME, + description="External Power IO" ), AgentVariable( - name="rel_start", - alias="rel_start", + name=glbs.RELATIVE_EVENT_START_TIME_VAR_NAME, + alias=glbs.RELATIVE_EVENT_START_TIME_VAR_NAME, description="relative start time of the flexibility event", ), AgentVariable( - name="rel_end", - alias="rel_end", + name=glbs.RELATIVE_EVENT_END_TIME_VAR_NAME, + alias=glbs.RELATIVE_EVENT_END_TIME_VAR_NAME, description="relative end time of the flexibility event", ), AgentVariable( - name="in_provision", - alias="in_provision", + name=glbs.PROVISION_VAR_NAME, + alias=glbs.PROVISION_VAR_NAME, description="Set if the system is in provision", value=False, ), @@ -75,8 +76,8 @@ class FlexibilityMarketModule(agentlib.BaseModule): # DataFrame for flex offer. Multiindex: (time_step, time). # Columns: pos_price, neg_price, status flex_offer_df: pd.DataFrame = None - # absolute end time of a flexibility event (now + relative end time of the flexibility - # event on the mpc horizon) + # absolute end time of a flexibility event (now + relative end time + # of the flexibility event on the mpc horizon) abs_flex_event_end: Union[int, float] = 0 def set_random_seed(self, random_seed: int): @@ -142,14 +143,15 @@ def random_flexibility_callback(self, inp: AgentVariable, name: str): A higher rate means that more positive offers will be accepted. Constraints: - cooldown: during $cooldown steps after a flexibility event no offer is accepted + cooldown: during $cooldown steps after a flexibility event no offer is + accepted minimum_average_flex: min amount of flexibility to be accepted, to account for the model error """ offer = inp.value # check if there is a flexibility provision and the cooldown is finished - if not self.get("in_provision").value and self.cooldown_ticker == 0: + if not self.get(glbs.PROVISION_VAR_NAME).value and self.cooldown_ticker == 0: if ( self.random_generator.random() < self.config.market_specs.options.offer_acceptance_rate @@ -177,21 +179,24 @@ def random_flexibility_callback(self, inp: AgentVariable, name: str): if profile is not None: # reindex the profile to the mpc output time grid - flex_power_feedback_method = self.config.market_specs.accepted_offer_sample_points + 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.COLLOCATION_TIME_GRID).value) + profile = profile.reindex( + self.get(glbs.COLLOCATION_TIME_GRID).value) elif flex_power_feedback_method == glbs.CONSTANT: - index_to_keep = ~np.isin(profile.index, - self.get(glbs.COLLOCATION_TIME_GRID).value) + index_to_keep = ~np.isin( + profile.index, self.get(glbs.COLLOCATION_TIME_GRID).value) profile = profile.get(index_to_keep) helper_indices = [i - 1 for i in profile.index[1:]] - new_index = sorted(set(profile.index.tolist() + helper_indices))[:-1] + new_index = sorted(set(profile.index.tolist() + + helper_indices))[:-1] profile = profile.reindex(new_index).ffill() profile = profile.dropna() profile.index += self.env.time - self.set("_P_external", profile) + self.set(glbs.ACCEPTED_POWER_VAR_NAME, profile) self.abs_flex_event_end = profile.index[-1] - self.set("in_provision", True) + self.set(glbs.PROVISION_VAR_NAME, True) self.cooldown_ticker = self.config.market_specs.cooldown elif self.cooldown_ticker > 0: @@ -205,7 +210,8 @@ def single_flexibility_callback(self, inp: AgentVariable, name: str): profile = None t_sample = self.get(glbs.TIME_STEP).value acceptance_time_lower = ( - self.env.config.offset + self.config.market_specs.options.offer_acceptance_time + self.env.config.offset + + self.config.market_specs.options.offer_acceptance_time ) acceptance_time_upper = ( self.env.config.offset @@ -214,7 +220,7 @@ def single_flexibility_callback(self, inp: AgentVariable, name: str): ) if ( acceptance_time_lower <= self.env.now < acceptance_time_upper - and not self.get("in_provision").value + and not self.get(glbs.PROVISION_VAR_NAME).value ): if self.config.market_specs.options.direction == "positive": if ( @@ -233,21 +239,24 @@ def single_flexibility_callback(self, inp: AgentVariable, name: str): if profile is not None: # reindex the profile to the mpc output time grid - flex_power_feedback_method = self.config.market_specs.accepted_offer_sample_points + 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.COLLOCATION_TIME_GRID).value) + profile = profile.reindex(self.get( + glbs.COLLOCATION_TIME_GRID).value) elif flex_power_feedback_method == glbs.CONSTANT: index_to_keep = ~np.isin(profile.index, self.get(glbs.COLLOCATION_TIME_GRID).value) profile = profile.get(index_to_keep) helper_indices = [i - 1 for i in profile.index[1:]] - new_index = sorted(set(profile.index.tolist() + helper_indices))[:-1] + new_index = sorted(set(profile.index.tolist() + + helper_indices))[:-1] profile = profile.reindex(new_index).ffill() profile = profile.dropna() profile.index += self.env.time - self.set("_P_external", profile) + self.set(glbs.ACCEPTED_POWER_VAR_NAME, profile) self.abs_flex_event_end = profile.index[-1] - self.set("in_provision", True) + self.set(glbs.PROVISION_VAR_NAME, True) self.write_results(offer) @@ -270,5 +279,5 @@ def process(self): while True: # End the provision at the appropriate time if self.abs_flex_event_end <= self.env.time: - self.set("in_provision", False) + self.set(glbs.PROVISION_VAR_NAME, False) yield self.env.timeout(self.env.config.t_sample) diff --git a/agentlib_flexquant/modules/shadow_mpc.py b/agentlib_flexquant/modules/shadow_mpc.py index 816c0018..d161f290 100644 --- a/agentlib_flexquant/modules/shadow_mpc.py +++ b/agentlib_flexquant/modules/shadow_mpc.py @@ -10,29 +10,34 @@ from pydantic import Field from typing import Dict, Union, Optional from collections.abc import Iterable -from agentlib.core.datamodels import AgentVariable +from agentlib.core.datamodels import AgentVariable, Source from agentlib_mpc.modules import mpc_full, minlp_mpc from agentlib_flexquant.utils.data_handling import fill_nans, MEAN -from agentlib_flexquant.data_structures.globals import ( - full_trajectory_prefix, - full_trajectory_suffix, - STORED_ENERGY_ALIAS_NEG, - STORED_ENERGY_ALIAS_POS, -) +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", + 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." + 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." + default=None, description="Name of the storage variable in the " + "shadow mpc model." ) @@ -42,24 +47,35 @@ class FlexibilityShadowMPC(mpc_full.MPC): 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 flex_model for integration to ensure the model used in MPC - # optimization remains unaffected + # 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( - "mpc", "mpc_sim" + "_flex", "_sim_flex" ) # clear the casadi simulator result at the first time step if already exists try: os.remove(self.res_file_flex) - except: + except FileNotFoundError: pass def set_output(self, solution): @@ -93,7 +109,7 @@ 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("in_provision").value): + 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 @@ -137,25 +153,34 @@ def sim_flex_model(self, solution): def register_callbacks(self): for control_var in self.config.controls: self.agent.data_broker.register_callback( - name=f"{control_var.name+full_trajectory_suffix}", - alias=f"{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: - adapted_name = input_var.name.replace(full_trajectory_suffix, "") - if adapted_name 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: AgentVariable, name: str): - """Set the control trajectories before calculating the flexibility offer. - - self.model should account for flexibility in its cost function. + """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 @@ -164,18 +189,24 @@ def calc_flex_callback(self, inp: AgentVariable, name: str): # get the value of the input vals = inp.value - 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 mapping dictionary - self._full_controls[name].value = vals - # make sure all controls are set - if all(x.value is not None for x in self._full_controls.values()): + + 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 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 _, control_var in self._full_controls.items(): - control_var.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 @@ -184,8 +215,10 @@ def process(self): 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""" + """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( @@ -204,23 +237,29 @@ def _initialize_flex_results( # 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) + 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 + # 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 + # 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] + 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 + since creating a model just reads the value in the model class + but not the config. + """ for par in self.config.parameters: @@ -238,7 +277,9 @@ def _update_initial_states(self, result_df): 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""" + """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() @@ -285,70 +326,98 @@ def _run_simulation( 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", + 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." + 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." + 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.""" + """Shadow MINLP-MPC for calculating positive/negatives flexibility offers. + + """ 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 flex_model for integration to ensure the model used in MPC - # optimization remains unaffected + # 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( - "mpc", "mpc_sim" + "_flex", "_sim_flex" ) # clear the casadi simulator result at the first time step if already exists try: os.remove(self.res_file_flex) - except: + 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"{control_var.name}{full_trajectory_suffix}", - alias=f"{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: - adapted_name = input_var.name.replace(full_trajectory_suffix, "") - if adapted_name 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: AgentVariable, name: str): - """Set the control trajectories before calculating the flexibility offer. - - self.model should account for flexibility in its cost function + """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 @@ -357,28 +426,34 @@ def calc_flex_callback(self, inp: AgentVariable, name: str): # get the value of the input vals = inp.value - 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 mapping dictionary - self._full_controls[name].value = vals - # update the value of the variable in the model if we want to limit the binary control in - # the market time during optimization - self.model.set(name, vals) - # make sure all controls are set - if all(x.value is not None for x in self._full_controls.values()): + + 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 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 _, control_var in self._full_controls.items(): - control_var.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.""" + """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 @@ -408,10 +483,12 @@ def set_output(self, solution): self.set(output, series) def sim_flex_model(self, solution): - """simulate the flex model over the preditcion horizon and save results""" + """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("in_provision").value): + 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 @@ -455,8 +532,10 @@ def sim_flex_model(self, solution): 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""" + """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( diff --git a/agentlib_flexquant/utils/config_management.py b/agentlib_flexquant/utils/config_management.py index 4ee475a5..f4a85162 100644 --- a/agentlib_flexquant/utils/config_management.py +++ b/agentlib_flexquant/utils/config_management.py @@ -79,7 +79,8 @@ def get_module_type_matching_dict(dictionary: dict) -> (dict, dict): return baseline_matches, shadow_matches -BASELINE_MODULE_TYPE_DICT, SHADOW_MODULE_TYPE_DICT = get_module_type_matching_dict(MODULE_NAME_DICT) +BASELINE_MODULE_TYPE_DICT, SHADOW_MODULE_TYPE_DICT = ( + get_module_type_matching_dict(MODULE_NAME_DICT)) def get_orig_module_type(config: AgentConfig) -> str: @@ -141,10 +142,12 @@ def check_bounds(parameter): # update every variable with a dict excluding the defined fields if "parameters" in parent_dict: parent_dict["parameters"] = [ - parameter.dict(exclude=check_bounds(parameter)) for parameter in module.parameters + parameter.dict(exclude=check_bounds(parameter)) for + parameter in module.parameters ] if "inputs" in parent_dict: - parent_dict["inputs"] = [input.dict(exclude=check_bounds(input)) for input in module.inputs] + parent_dict["inputs"] = [input.dict(exclude=check_bounds(input)) for + input in module.inputs] if "outputs" in parent_dict: parent_dict["outputs"] = [ output.dict(exclude=check_bounds(output)) for output in module.outputs @@ -159,14 +162,24 @@ def check_bounds(parameter): for binary_control in module.binary_controls ] if "states" in parent_dict: - parent_dict["states"] = [state.dict(exclude=check_bounds(state)) for state in module.states] + parent_dict["states"] = [state.dict(exclude=check_bounds(state)) for + state in module.states] if "full_controls" in parent_dict: parent_dict["full_controls"] = [ full_control.dict( - exclude=(lambda ex: ex.remove("shared") or ex)(check_bounds(full_control)) + exclude=(lambda ex: + ex.remove("shared") or ex)(check_bounds(full_control)) ) for full_control in module.full_controls ] + if "vars_to_communicate" in parent_dict: + parent_dict["vars_to_communicate"] = [ + var_to_communicate.dict( + exclude=(lambda ex: + ex.remove("shared") or ex)(check_bounds(var_to_communicate)) + ) + for var_to_communicate in module.vars_to_communicate + ] return parent_dict diff --git a/agentlib_flexquant/utils/parsing.py b/agentlib_flexquant/utils/parsing.py index 73a726a7..15d175f0 100644 --- a/agentlib_flexquant/utils/parsing.py +++ b/agentlib_flexquant/utils/parsing.py @@ -9,9 +9,12 @@ MARKET_TIME, PREP_TIME, SHADOW_MPC_COST_FUNCTION, - full_trajectory_prefix, full_trajectory_suffix, return_baseline_cost_function, + PROVISION_VAR_NAME, + ACCEPTED_POWER_VAR_NAME, + RELATIVE_EVENT_START_TIME_VAR_NAME, + RELATIVE_EVENT_END_TIME_VAR_NAME ) from agentlib_flexquant.data_structures.mpcs import ( BaselineMPCData, @@ -284,7 +287,7 @@ def modify_config_class_shadow(self, node: ast.ClassDef): ) ) body.value.elts.append( - add_input("in_provision", False, "-", "provision flag", "bool") + add_input(PROVISION_VAR_NAME, False, "-", "provision flag", "bool") ) # add the flex variables and the weights if body.target.id == "parameters": @@ -334,7 +337,7 @@ def modify_config_class_baseline(self, node: ast.ClassDef): value_list = self.get_leftmost_list(body.value) value_list.elts.append( add_input( - "_P_external", + ACCEPTED_POWER_VAR_NAME, 0, "W", "External power profile to be provided", @@ -343,7 +346,7 @@ def modify_config_class_baseline(self, node: ast.ClassDef): ) value_list.elts.append( add_input( - "in_provision", + PROVISION_VAR_NAME, False, "-", "Flag signaling if the flexibility is in provision", @@ -352,7 +355,7 @@ def modify_config_class_baseline(self, node: ast.ClassDef): ) value_list.elts.append( add_input( - "rel_start", + RELATIVE_EVENT_START_TIME_VAR_NAME, 0, "s", "relative start time of the flexibility event", @@ -361,7 +364,7 @@ def modify_config_class_baseline(self, node: ast.ClassDef): ) value_list.elts.append( add_input( - "rel_end", + RELATIVE_EVENT_END_TIME_VAR_NAME, 0, "s", "relative end time of the flexibility event", diff --git a/examples/OneRoom_CIA/main_cia_flex.py b/examples/OneRoom_CIA/main_cia_flex.py index c84025f2..c417abeb 100644 --- a/examples/OneRoom_CIA/main_cia_flex.py +++ b/examples/OneRoom_CIA/main_cia_flex.py @@ -1,6 +1,7 @@ import matplotlib.pyplot as plt from matplotlib.ticker import FormatStrFormatter import matplotlib + matplotlib.use("Agg") from agentlib.utils.multi_agent_system import LocalMASAgency import numpy as np @@ -67,15 +68,19 @@ def run_example(until=until, with_plots=False, with_dashboard=False): # T out ax1.set_ylabel("$T_{room}$ in K") results["SimAgent"]["room"]["T_upper"].plot(ax=ax1, color="0.5") - results["SimAgent"]["room"]["T_out"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) + results["SimAgent"]["room"]["T_out"].plot(ax=ax1, + color=mpcplot.EBCColors.dark_grey) mpc_at_time_step( - data=results["myMPCAgent"]["Baseline"], time_step=time_of_activation, variable="T" + data=results["Baseline"]["Baseline"], time_step=time_of_activation, + variable="T" ).plot(ax=ax1, label="base", linestyle="--", color=mpcplot.EBCColors.dark_grey) mpc_at_time_step( - data=results["NegFlexMPC"]["NegFlexMPC"], time_step=time_of_activation, variable="T" + data=results["NegFlexMPC"]["NegFlexMPC"], time_step=time_of_activation, + variable="T" ).plot(ax=ax1, label="neg", linestyle="--", color=mpcplot.EBCColors.red) mpc_at_time_step( - data=results["PosFlexMPC"]["PosFlexMPC"], time_step=time_of_activation, variable="T" + data=results["PosFlexMPC"]["PosFlexMPC"], time_step=time_of_activation, + variable="T" ).plot(ax=ax1, label="pos", linestyle="--", color=mpcplot.EBCColors.blue) ax1.legend() @@ -99,9 +104,11 @@ def run_example(until=until, with_plots=False, with_dashboard=False): ax1 = axs[0] # P_el ax1.set_ylabel("$P_{el}$ in W") - results["SimAgent"]["room"]["P_el"].plot(ax=ax1, color=mpcplot.EBCColors.dark_grey) + results["SimAgent"]["room"]["P_el"].plot(ax=ax1, + color=mpcplot.EBCColors.dark_grey) mpc_at_time_step( - data=results["NegFlexMPC"]["NegFlexMPC"], time_step=time_of_activation, variable="P_el" + data=results["NegFlexMPC"]["NegFlexMPC"], time_step=time_of_activation, + variable="P_el" ).ffill().plot( ax=ax1, drawstyle="steps-post", @@ -110,7 +117,8 @@ def run_example(until=until, with_plots=False, with_dashboard=False): color=mpcplot.EBCColors.red, ) mpc_at_time_step( - data=results["PosFlexMPC"]["PosFlexMPC"], time_step=time_of_activation, variable="P_el" + data=results["PosFlexMPC"]["PosFlexMPC"], time_step=time_of_activation, + variable="P_el" ).ffill().plot( ax=ax1, drawstyle="steps-post", @@ -119,7 +127,8 @@ def run_example(until=until, with_plots=False, with_dashboard=False): color=mpcplot.EBCColors.blue, ) mpc_at_time_step( - data=results["myMPCAgent"]["Baseline"], time_step=time_of_activation, variable="P_el" + data=results["Baseline"]["Baseline"], time_step=time_of_activation, + variable="P_el" ).ffill().plot( ax=ax1, drawstyle="steps-post", @@ -136,8 +145,10 @@ def run_example(until=until, with_plots=False, with_dashboard=False): # flexibility # get only the first prediction time of each time step ind_res = results["FlexibilityIndicator"]["FlexibilityIndicator"] - energy_flex_neg = ind_res.xs("negative_energy_flex", axis=1).droplevel(1).dropna() - energy_flex_pos = ind_res.xs("positive_energy_flex", axis=1).droplevel(1).dropna() + energy_flex_neg = ind_res.xs("negative_energy_flex", axis=1).droplevel( + 1).dropna() + energy_flex_pos = ind_res.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("$epsilon$ in kWh") diff --git a/examples/OneRoom_CIA/predictor/simple_predictor.py b/examples/OneRoom_CIA/predictor/simple_predictor.py index 6d3756e9..fb3bfb66 100644 --- a/examples/OneRoom_CIA/predictor/simple_predictor.py +++ b/examples/OneRoom_CIA/predictor/simple_predictor.py @@ -1,10 +1,7 @@ import agentlib as al import numpy as np import pandas as pd -from agentlib.core import Agent -import json -import csv -from datetime import datetime + class PredictorModuleConfig(al.BaseModuleConfig): """Module that outputs a prediction of the heat load at a specified @@ -25,7 +22,7 @@ class PredictorModuleConfig(al.BaseModuleConfig): ) ] - shared_variable_fields:list[str] = ["outputs"] + shared_variable_fields: list[str] = ["outputs"] class PredictorModule(al.BaseModule): diff --git a/examples/OneRoom_SimpleMPC/main_one_room_flex.py b/examples/OneRoom_SimpleMPC/main_one_room_flex.py index 995f8e0e..ab2bbb23 100644 --- a/examples/OneRoom_SimpleMPC/main_one_room_flex.py +++ b/examples/OneRoom_SimpleMPC/main_one_room_flex.py @@ -25,12 +25,10 @@ def run_example(until=until, with_plots=False, with_dashboard=False): """ - 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] + agent_configs = [sim_config] config_list = FlexAgentGenerator( flex_config=flex_config, mpc_agent_config=mpc_config diff --git a/examples/OneRoom_SimpleMPC/predictor/predictor_config.json b/examples/OneRoom_SimpleMPC/predictor/predictor_config.json deleted file mode 100644 index dfe9275c..00000000 --- a/examples/OneRoom_SimpleMPC/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": 48 - } - ] - } - ] -} \ No newline at end of file diff --git a/examples/OneRoom_SimpleMPC/predictor/simple_predictor.py b/examples/OneRoom_SimpleMPC/predictor/simple_predictor.py deleted file mode 100644 index 6a2e08e3..00000000 --- a/examples/OneRoom_SimpleMPC/predictor/simple_predictor.py +++ /dev/null @@ -1,52 +0,0 @@ -import agentlib as al -import numpy as np -import pandas as pd -from agentlib.core import Agent -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) 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 91f90336..e809ccd1 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 @@ -87,23 +87,23 @@ 0.0, NaN, NaN, - -500.0, + 0.0, 0.0, 500.0, 500.0, - 187.5, + 250.0, 125.0, true, true, - 125.0, + 166.7, 83.3, - 1458.3, + 1250.0, -416.7, - 1458.3, + 1250.0, -416.7, - 11.7, + 7.5, -5.0, - 11.7, + 7.5, -5.0, 600.0, 300.0, @@ -337,9 +337,9 @@ "75%": 500.0, "count": 960.0, "max": 500.0, - "mean": 214.0, + "mean": 202.3, "min": 0.0, - "std": 239.4 + "std": 244.8 }, "_P_el_pos": { "25%": 0.0, @@ -347,9 +347,9 @@ "75%": 0.0, "count": 960.0, "max": 500.0, - "mean": 44.7, + "mean": 54.3, "min": 0.0, - "std": 140.4 + "std": 151.3 }, "collocation_time_grid": { "25%": 1231.7, @@ -382,54 +382,54 @@ "std": 0.0 }, "negative_corrected_costs": { - "25%": 1989.8, - "50%": 2001.9, - "75%": 2049.3, + "25%": 1781.5, + "50%": 1839.9, + "75%": 1928.3, "count": 12.0, - "max": 2110.9, - "mean": 1973.8, - "min": 1458.3, - "std": 174.9 + "max": 2033.1, + "mean": 1817.6, + "min": 1250.0, + "std": 212.1 }, "negative_corrected_costs_rel": { - "25%": 11.4, - "50%": 11.6, - "75%": 12.6, + "25%": 8.2, + "50%": 8.5, + "75%": 10.6, "count": 12.0, - "max": 14.5, - "mean": 12.0, - "min": 11.2, - "std": 0.9 + "max": 12.6, + "mean": 9.5, + "min": 7.5, + "std": 2.0 }, "negative_costs": { - "25%": 1989.8, - "50%": 2001.9, - "75%": 2049.3, + "25%": 1781.5, + "50%": 1839.9, + "75%": 1928.3, "count": 12.0, - "max": 2110.9, - "mean": 1973.8, - "min": 1458.3, - "std": 174.9 + "max": 2033.1, + "mean": 1817.6, + "min": 1250.0, + "std": 212.1 }, "negative_costs_rel": { - "25%": 11.4, - "50%": 11.6, - "75%": 12.6, + "25%": 8.2, + "50%": 8.5, + "75%": 10.6, "count": 12.0, - "max": 14.5, - "mean": 12.0, - "min": 11.2, - "std": 0.9 + "max": 12.6, + "mean": 9.5, + "min": 7.5, + "std": 2.0 }, "negative_energy_flex": { - "25%": 160.3, - "50%": 170.4, - "75%": 176.7, + "25%": 165.4, + "50%": 212.1, + "75%": 218.4, "count": 12.0, - "max": 182.0, - "mean": 165.0, - "min": 125.0, - "std": 18.0 + "max": 223.7, + "mean": 196.2, + "min": 158.9, + "std": 28.0 }, "negative_power_flex_full": { "25%": 0.0, @@ -437,9 +437,9 @@ "75%": 500.0, "count": 972.0, "max": 500.0, - "mean": 146.2, + "mean": 134.6, "min": -500.0, - "std": 270.7 + "std": 263.4 }, "negative_power_flex_offer": { "25%": 0.0, @@ -447,19 +447,19 @@ "75%": 500.0, "count": 492.0, "max": 500.0, - "mean": 305.2, + "mean": 309.3, "min": -500.0, - "std": 299.0 + "std": 240.4 }, "negative_power_flex_offer_avg": { - "25%": 240.5, - "50%": 255.6, - "75%": 265.1, + "25%": 248.1, + "50%": 318.1, + "75%": 327.6, "count": 12.0, - "max": 273.1, - "mean": 247.5, - "min": 187.5, - "std": 26.9 + "max": 335.6, + "mean": 294.3, + "min": 238.3, + "std": 42.0 }, "negative_power_flex_offer_max": { "25%": 500.0, @@ -472,64 +472,64 @@ "std": 0.0 }, "negative_power_flex_offer_min": { - "25%": -404.5, - "50%": -339.9, - "75%": -260.1, + "25%": 0.0, + "50%": 0.0, + "75%": 0.0, "count": 12.0, "max": 0.0, - "mean": -301.5, + "mean": -70.3, "min": -500.0, - "std": 175.7 + "std": 167.5 }, "positive_corrected_costs": { - "25%": -416.7, - "50%": -295.0, - "75%": -183.8, + "25%": -220.4, + "50%": -129.4, + "75%": -37.7, "count": 12.0, - "max": -78.0, - "mean": -283.1, - "min": -470.8, - "std": 138.9 + "max": 0.0, + "mean": -156.3, + "min": -416.7, + "std": 153.8 }, "positive_corrected_costs_rel": { "25%": -10.0, - "50%": -8.9, - "75%": -7.2, + "50%": -6.9, + "75%": -3.1, "count": 12.0, - "max": -5.0, - "mean": -8.6, + "max": 0.0, + "mean": -5.9, "min": -10.0, - "std": 1.7 + "std": 4.0 }, "positive_costs": { - "25%": -416.7, - "50%": -295.0, - "75%": -183.8, + "25%": -220.4, + "50%": -129.4, + "75%": -37.7, "count": 12.0, - "max": -78.0, - "mean": -283.1, - "min": -470.8, - "std": 138.9 + "max": 0.0, + "mean": -156.3, + "min": -416.7, + "std": 153.8 }, "positive_costs_rel": { "25%": -10.0, - "50%": -8.9, - "75%": -7.2, + "50%": -6.9, + "75%": -3.1, "count": 12.0, - "max": -5.0, - "mean": -8.6, + "max": 0.0, + "mean": -5.9, "min": -10.0, - "std": 1.7 + "std": 4.0 }, "positive_energy_flex": { - "25%": 26.4, - "50%": 33.4, - "75%": 42.9, + "25%": 3.5, + "50%": 17.0, + "75%": 30.4, "count": 12.0, "max": 83.3, - "mean": 34.7, - "min": 7.8, - "std": 19.8 + "mean": 24.3, + "min": -1.1, + "std": 28.6 }, "positive_power_flex_full": { "25%": 0.0, @@ -537,9 +537,9 @@ "75%": 0.0, "count": 972.0, "max": 500.0, - "mean": 21.0, + "mean": 11.6, "min": -500.0, - "std": 109.7 + "std": 140.8 }, "positive_power_flex_offer": { "25%": 0.0, @@ -547,39 +547,39 @@ "75%": 0.0, "count": 492.0, "max": 500.0, - "mean": 52.0, - "min": -250.0, - "std": 137.5 + "mean": 51.0, + "min": -500.0, + "std": 144.8 }, "positive_power_flex_offer_avg": { - "25%": 39.6, - "50%": 50.1, - "75%": 64.4, + "25%": 5.3, + "50%": 25.6, + "75%": 45.6, "count": 12.0, "max": 125.0, - "mean": 52.0, - "min": 11.7, - "std": 29.6 + "mean": 36.4, + "min": -1.7, + "std": 42.8 }, "positive_power_flex_offer_max": { "25%": 315.4, - "50%": 339.9, - "75%": 404.5, + "50%": 359.5, + "75%": 432.8, "count": 12.0, "max": 500.0, - "mean": 366.6, + "mean": 378.3, "min": 250.0, - "std": 77.8 + "std": 86.7 }, "positive_power_flex_offer_min": { - "25%": 0.0, - "50%": 0.0, + "25%": -417.3, + "50%": -78.2, "75%": 0.0, "count": 12.0, "max": 0.0, - "mean": -33.9, - "min": -250.0, - "std": 81.6 + "mean": -200.6, + "min": -500.0, + "std": 226.3 }, "prediction_horizon": { "25%": 16.0, diff --git a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_neg_flex_summary.json b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_neg_flex_summary.json index 3674c96b..d476790e 100644 --- a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_neg_flex_summary.json +++ b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_neg_flex_summary.json @@ -402,13 +402,13 @@ NaN, NaN, NaN, - 1.0, - 1.0, - 1.0, + 0.0, + 0.0, + 0.0, 297.1, 303.1, 288.1, - 250.0, + 0.0, 500.0, 0.0, NaN, @@ -441,17 +441,17 @@ NaN, NaN, NaN, - 297.0, + 297.2, 303.1, 288.1, NaN, NaN, NaN, - -1.9, + -2.1, Infinity, -Infinity, - 297.0, - 250.0, + 297.2, + 0.0, Infinity, Infinity, -Infinity, @@ -534,7 +534,7 @@ "75%": 1.0, "count": 192.0, "max": 1.0, - "mean": 0.5, + "mean": 0.4, "min": 0.0, "std": 0.5 }, @@ -754,7 +754,7 @@ "75%": 1.0, "count": 192.0, "max": 1.0, - "mean": 0.5, + "mean": 0.4, "min": 0.0, "std": 0.5 }, @@ -774,29 +774,29 @@ "75%": 500.0, "count": 384.0, "max": 500.0, - "mean": 214.0, + "mean": 202.3, "min": 0.0, - "std": 239.6 + "std": 245.0 }, "variable.T": { - "25%": 291.1, - "50%": 293.1, - "75%": 294.9, + "25%": 291.2, + "50%": 292.8, + "75%": 294.6, "count": 588.0, "max": 298.2, - "mean": 293.1, - "min": 289.4, - "std": 2.2 + "mean": 293.0, + "min": 289.6, + "std": 2.0 }, "variable.T_out": { - "25%": 291.1, - "50%": 293.1, - "75%": 294.9, + "25%": 291.2, + "50%": 292.7, + "75%": 294.6, "count": 384.0, - "max": 298.1, - "mean": 293.1, - "min": 289.5, - "std": 2.2 + "max": 297.9, + "mean": 293.0, + "min": 289.7, + "std": 2.0 }, "variable.T_slack": { "25%": 0.0, @@ -804,8 +804,8 @@ "75%": 0.0, "count": 384.0, "max": 0.0, - "mean": -0.2, - "min": -2.9, + "mean": -0.1, + "min": -2.8, "std": 0.5 }, "variable.cooler_on": { @@ -814,7 +814,7 @@ "75%": 1.0, "count": 192.0, "max": 1.0, - "mean": 0.5, + "mean": 0.4, "min": 0.0, "std": 0.5 }, @@ -824,9 +824,9 @@ "75%": 500.0, "count": 192.0, "max": 500.0, - "mean": 214.0, + "mean": 202.3, "min": 0.0, - "std": 239.9 + "std": 245.3 } }, "tail_5_rows": { @@ -989,7 +989,7 @@ NaN, NaN, NaN, - 291.5, + 292.2, 303.1, 288.1, NaN, @@ -998,7 +998,7 @@ 0.0, Infinity, -Infinity, - 291.5, + 292.2, 0.0, Infinity, Infinity, @@ -1025,7 +1025,7 @@ 0.0, 0.0, 0.0, - 291.6, + 292.3, 303.1, 288.1, 0.0, @@ -1061,7 +1061,7 @@ NaN, NaN, NaN, - 291.7, + 292.4, 303.1, 288.1, NaN, @@ -1070,7 +1070,7 @@ 0.0, Infinity, -Infinity, - 291.7, + 292.4, 0.0, Infinity, Infinity, @@ -1097,7 +1097,7 @@ NaN, NaN, NaN, - 291.9, + 292.7, 303.1, 288.1, NaN, @@ -1106,7 +1106,7 @@ 0.0, Infinity, -Infinity, - 291.9, + 292.7, 0.0, Infinity, Infinity, @@ -1133,7 +1133,7 @@ NaN, NaN, NaN, - 292.0, + 292.8, 303.1, 288.1, NaN, diff --git a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_pos_flex_summary.json b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_pos_flex_summary.json index 42c195d3..17c561cc 100644 --- a/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_pos_flex_summary.json +++ b/tests/snapshots/test_OneRoom_CIA/test_oneroom_cia/oneroom_cia_pos_flex_summary.json @@ -774,39 +774,39 @@ "75%": 0.0, "count": 384.0, "max": 500.0, - "mean": 44.7, + "mean": 54.3, "min": 0.0, - "std": 140.5 + "std": 151.5 }, "variable.T": { - "25%": 295.1, - "50%": 296.9, - "75%": 298.6, + "25%": 294.8, + "50%": 296.4, + "75%": 298.2, "count": 588.0, "max": 301.3, - "mean": 297.0, - "min": 293.6, + "mean": 296.6, + "min": 293.4, "std": 2.0 }, "variable.T_out": { - "25%": 295.1, - "50%": 296.9, - "75%": 298.6, + "25%": 294.8, + "50%": 296.4, + "75%": 298.2, "count": 384.0, "max": 301.2, - "mean": 296.9, - "min": 293.7, - "std": 2.0 + "mean": 296.6, + "min": 293.5, + "std": 1.9 }, "variable.T_slack": { - "25%": -3.5, - "50%": -1.7, - "75%": -0.0, + "25%": -3.0, + "50%": -1.3, + "75%": 0.0, "count": 384.0, "max": 0.0, - "mean": -2.0, + "mean": -1.7, "min": -6.1, - "std": 1.8 + "std": 1.6 }, "variable.cooler_on": { "25%": 0.0, @@ -824,9 +824,9 @@ "75%": 0.0, "count": 192.0, "max": 500.0, - "mean": 44.7, + "mean": 54.3, "min": 0.0, - "std": 140.7 + "std": 151.7 } }, "tail_5_rows": { @@ -989,16 +989,16 @@ NaN, NaN, NaN, - 299.7, + 298.7, 303.1, 288.1, NaN, NaN, NaN, - -4.6, + -3.5, Infinity, -Infinity, - 299.7, + 298.7, 0.0, Infinity, Infinity, @@ -1025,7 +1025,7 @@ 0.0, 0.0, 0.0, - 299.8, + 298.8, 303.1, 288.1, 0.0, @@ -1061,16 +1061,16 @@ NaN, NaN, NaN, - 299.9, + 298.9, 303.1, 288.1, NaN, NaN, NaN, - -4.8, + -3.7, Infinity, -Infinity, - 299.9, + 298.9, 0.0, Infinity, Infinity, @@ -1097,16 +1097,16 @@ NaN, NaN, NaN, - 300.2, + 299.1, 303.1, 288.1, NaN, NaN, NaN, - -5.0, + -4.0, Infinity, -Infinity, - 300.2, + 299.1, 0.0, Infinity, Infinity, @@ -1133,7 +1133,7 @@ NaN, NaN, NaN, - 300.3, + 299.2, 303.1, 288.1, NaN, 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 63d3ad12..785faa83 100644 --- a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json +++ b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_indicator_summary.json @@ -87,24 +87,24 @@ 0.0, NaN, NaN, - 28.21667, - 5.04873, - 129.42772, + 41.42016, + -2.20626, + 133.29934, 59.13035, - 36.18018, - 16.24944, - false, - false, - 72.36037, - 32.49888, - -95.81378, - -174.22474, - -95.81378, - -174.22474, - -1.32412, - -5.36095, - -1.32412, - -5.36095, + 45.37107, + 5.82314, + true, + true, + 90.74214, + 11.64628, + 93.72299, + 15.9482, + 93.72299, + 15.9482, + 1.03285, + 1.36938, + 1.03285, + 1.36938, 900.0, 900.0, 7200.0, @@ -188,13 +188,13 @@ ], [ 59.4637, - 48.95195, + 52.16941, 200.0, NaN, NaN, NaN, 1.0, - -10.51174, + -7.29429, -140.5363, NaN, NaN, @@ -225,13 +225,13 @@ ], [ 59.4637, - 48.95195, + 52.16941, 200.0, NaN, NaN, NaN, NaN, - -10.51174, + -7.29429, -140.5363, NaN, NaN, @@ -332,24 +332,24 @@ "std": 6.81814 }, "_P_el_neg": { - "25%": 42.76229, - "50%": 45.23904, - "75%": 56.30088, + "25%": 44.42571, + "50%": 51.76071, + "75%": 59.19999, "count": 576.0, - "max": 188.55806, - "mean": 53.9549, + "max": 192.42969, + "mean": 57.90396, "min": 0.0, - "std": 26.26921 + "std": 25.92565 }, "_P_el_pos": { - "25%": 42.57377, - "50%": 44.9519, - "75%": 50.68764, + "25%": 44.33154, + "50%": 49.27794, + "75%": 59.19958, "count": 576.0, "max": 200.0, - "mean": 48.41227, + "mean": 52.37404, "min": 0.0, - "std": 24.22683 + "std": 24.01966 }, "collocation_time_grid": { "25%": 10895.09619, @@ -382,184 +382,184 @@ "std": 0.0 }, "negative_corrected_costs": { - "25%": 34.08003, - "50%": 78.25203, - "75%": 79.51469, + "25%": 78.68905, + "50%": 79.9033, + "75%": 83.94113, "count": 4.0, - "max": 80.68051, - "mean": 35.34269, - "min": -95.81378, - "std": 87.44806 + "max": 93.72299, + "mean": 82.72689, + "min": 77.37797, + "std": 7.45383 }, "negative_corrected_costs_rel": { - "25%": 0.4548, + "25%": 1.04404, "50%": 1.05, "75%": 1.05225, "count": 4.0, "max": 1.0523, - "mean": 0.45704, - "min": -1.32412, - "std": 1.18745 + "mean": 1.04629, + "min": 1.03285, + "std": 0.00921 }, "negative_costs": { - "25%": 34.08003, - "50%": 78.25203, - "75%": 79.51469, + "25%": 78.68905, + "50%": 79.9033, + "75%": 83.94113, "count": 4.0, - "max": 80.68051, - "mean": 35.34269, - "min": -95.81378, - "std": 87.44806 + "max": 93.72299, + "mean": 82.72689, + "min": 77.37797, + "std": 7.45383 }, "negative_costs_rel": { - "25%": 0.4548, + "25%": 1.04404, "50%": 1.05, "75%": 1.05225, "count": 4.0, "max": 1.0523, - "mean": 0.45704, - "min": -1.32412, - "std": 1.18745 + "mean": 1.04629, + "min": 1.03285, + "std": 0.00921 }, "negative_energy_flex": { - "25%": 73.24313, - "50%": 74.36528, - "75%": 75.64544, + "25%": 74.77924, + "50%": 76.0977, + "75%": 80.43718, "count": 4.0, - "max": 77.0022, - "mean": 74.52328, - "min": 72.36037, - "std": 2.02023 + "max": 90.74214, + "mean": 79.11873, + "min": 73.53738, + "std": 7.87707 }, "negative_power_flex_full": { - "25%": -0.20434, + "25%": 0.0, "50%": 0.0, "75%": 0.0, "count": 580.0, - "max": 129.42772, - "mean": 2.92562, + "max": 133.29934, + "mean": 6.84636, "min": -59.19998, - "std": 25.57299 + "std": 23.91844 }, "negative_power_flex_offer": { - "25%": 35.64256, - "50%": 37.2623, - "75%": 39.77599, + "25%": 36.45698, + "50%": 38.16419, + "75%": 46.2347, "count": 100.0, - "max": 129.42772, - "mean": 44.21704, + "max": 133.29934, + "mean": 46.48093, "min": -59.19998, - "std": 34.53061 + "std": 34.59776 }, "negative_power_flex_offer_avg": { - "25%": 36.62156, - "50%": 37.18264, - "75%": 37.82272, + "25%": 37.38962, + "50%": 38.04885, + "75%": 40.21859, "count": 4.0, - "max": 38.5011, - "mean": 37.26164, - "min": 36.18018, - "std": 1.01012 + "max": 45.37107, + "mean": 39.55936, + "min": 36.76869, + "std": 3.93854 }, "negative_power_flex_offer_max": { "25%": 121.31461, "50%": 123.5883, - "75%": 126.32554, + "75%": 127.29344, "count": 4.0, - "max": 129.42772, - "mean": 124.05184, + "max": 133.29934, + "mean": 125.01975, "min": 119.60305, - "std": 4.27875 + "std": 5.99422 }, "negative_power_flex_offer_min": { - "25%": 29.53893, - "50%": 30.39654, - "75%": 31.0218, + "25%": 30.60497, + "50%": 31.23019, + "75%": 34.09028, "count": 4.0, - "max": 31.64699, - "mean": 30.16418, - "min": 28.21667, - "std": 1.46595 + "max": 41.42016, + "mean": 33.46506, + "min": 29.97968, + "std": 5.3469 }, "positive_corrected_costs": { - "25%": -31.36609, + "25%": 16.17715, "50%": 16.40991, "75%": 16.6266, "count": 4.0, "max": 16.80733, - "mean": -31.1494, - "min": -174.22474, - "std": 95.38383 + "mean": 16.39384, + "min": 15.9482, + "std": 0.37374 }, "positive_corrected_costs_rel": { - "25%": -0.30303, + "25%": 1.37955, "50%": 1.38457, "75%": 1.38674, "count": 4.0, "max": 1.38835, - "mean": -0.30086, - "min": -5.36095, - "std": 3.37339 + "mean": 1.38172, + "min": 1.36938, + "std": 0.00852 }, "positive_costs": { - "25%": -31.36609, + "25%": 16.17715, "50%": 16.40991, "75%": 16.6266, "count": 4.0, "max": 16.80733, - "mean": -31.1494, - "min": -174.22474, - "std": 95.38383 + "mean": 16.39384, + "min": 15.9482, + "std": 0.37374 }, "positive_costs_rel": { - "25%": -0.30303, + "25%": 1.37955, "50%": 1.38457, "75%": 1.38674, "count": 4.0, "max": 1.38835, - "mean": -0.30086, - "min": -5.36095, - "std": 3.37339 + "mean": 1.38172, + "min": 1.36938, + "std": 0.00852 }, "positive_energy_flex": { - "25%": 11.88994, - "50%": 12.05211, - "75%": 17.2397, + "25%": 11.69185, + "50%": 11.82897, + "75%": 12.0015, "count": 4.0, - "max": 32.49888, - "mean": 17.07754, - "min": 11.70704, - "std": 10.28252 + "max": 12.15331, + "mean": 11.86438, + "min": 11.64628, + "std": 0.2333 }, "positive_power_flex_full": { "25%": 0.0, "50%": 0.0, - "75%": 9.2398, + "75%": 0.0, "count": 580.0, "max": 59.13035, - "mean": 2.57718, + "mean": -1.35673, "min": -144.52336, - "std": 24.28044 + "std": 22.69378 }, "positive_power_flex_offer": { - "25%": 0.0, - "50%": 5.04873, - "75%": 23.80017, + "25%": -2.04346, + "50%": 1.11256, + "75%": 9.24657, "count": 100.0, "max": 59.13035, - "mean": 12.02733, - "min": -27.75812, - "std": 19.97176 + "mean": 9.6044, + "min": -27.77965, + "std": 20.29337 }, "positive_power_flex_offer_avg": { - "25%": 5.94497, - "50%": 6.02605, - "75%": 8.61985, + "25%": 5.84593, + "50%": 5.91449, + "75%": 6.00075, "count": 4.0, - "max": 16.24944, - "mean": 8.53877, - "min": 5.85352, - "std": 5.14126 + "max": 6.07666, + "mean": 5.93219, + "min": 5.82314, + "std": 0.11665 }, "positive_power_flex_offer_max": { "25%": 54.41607, @@ -572,14 +572,14 @@ "std": 2.38915 }, "positive_power_flex_offer_min": { - "25%": -2.20267, - "50%": -2.19253, - "75%": -0.37583, + "25%": -2.20657, + "50%": -2.20366, + "75%": -2.1968, "count": 4.0, - "max": 5.04873, - "mean": -0.38596, + "max": -2.18401, + "mean": -2.19971, "min": -2.20751, - "std": 3.62314 + "std": 0.01083 }, "prediction_horizon": { "25%": 48.0, diff --git a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_neg_flex_summary.json b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_neg_flex_summary.json index 4eeb2f6f..d32938cd 100644 --- a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_neg_flex_summary.json +++ b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_neg_flex_summary.json @@ -262,7 +262,7 @@ [ 299.0, 290.0, - 278.21911, + 278.15, 22.30528, 0.0, NaN, @@ -276,7 +276,7 @@ NaN, NaN, NaN, - 293.1132, + 293.10961, 303.15, 288.15, NaN, @@ -292,7 +292,7 @@ [ 299.0, 290.0, - 278.40791, + 278.15, 22.30528, 0.0, NaN, @@ -306,7 +306,7 @@ NaN, NaN, NaN, - 290.73596, + 290.6956, 303.15, 288.15, NaN, @@ -336,10 +336,10 @@ NaN, NaN, NaN, - 290.03249, + 289.9688, 303.15, 288.15, - 48.95195, + 52.16941, 200.0, 0.0, NaN, @@ -352,7 +352,7 @@ [ 299.0, 290.0, - 278.54583, + 278.15, 59.4637, 0.0, NaN, @@ -366,16 +366,16 @@ NaN, NaN, NaN, - 289.87692, + 289.84393, 303.15, 288.15, NaN, NaN, NaN, - 0.12308, + 0.15606, Infinity, -Infinity, - 48.95195, + 52.16941, Infinity, -Infinity ] @@ -481,14 +481,14 @@ "std": 6.90908 }, "parameter.T_amb": { - "25%": 280.09446, - "50%": 281.68393, - "75%": 282.75412, + "25%": 278.15, + "50%": 280.64887, + "75%": 282.46092, "count": 384.0, "max": 283.14781, - "mean": 281.30817, + "mean": 280.51268, "min": 277.24276, - "std": 1.58859 + "std": 1.94832 }, "parameter.T_lower": { "25%": 290.01321, @@ -651,44 +651,44 @@ "std": 0.0 }, "variable.P_el": { - "25%": 42.76229, - "50%": 45.23904, - "75%": 55.51528, + "25%": 44.42569, + "50%": 51.21368, + "75%": 59.19999, "count": 384.0, - "max": 188.55806, - "mean": 53.87729, + "max": 192.42969, + "mean": 57.82493, "min": 0.0, - "std": 26.31269 + "std": 25.98125 }, "variable.P_in": { - "25%": 42.76229, - "50%": 45.23904, - "75%": 55.51528, + "25%": 44.42569, + "50%": 51.21368, + "75%": 59.19999, "count": 192.0, - "max": 188.55806, - "mean": 53.87729, + "max": 192.42969, + "mean": 57.82493, "min": 0.0, - "std": 26.34711 + "std": 26.01524 }, "variable.T_slack": { - "25%": 0.00978, - "50%": 0.01008, - "75%": 0.01027, + "25%": 0.0098, + "50%": 0.01, + "75%": 0.01026, "count": 384.0, "max": 0.66918, - "mean": 0.03485, + "mean": 0.03491, "min": 7e-05, - "std": 0.08786 + "std": 0.08784 }, "variable.T_zone": { "25%": 290.09006, "50%": 291.17409, "75%": 291.84067, "count": 580.0, - "max": 299.85458, - "mean": 292.12772, - "min": 289.45632, - "std": 2.95589 + "max": 299.8446, + "mean": 292.12167, + "min": 289.46697, + "std": 2.95357 } }, "tail_5_rows": { diff --git a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_pos_flex_summary.json b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_pos_flex_summary.json index e44e34c7..4cb5e8ac 100644 --- a/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_pos_flex_summary.json +++ b/tests/snapshots/test_SimpleBuilding/test_simplebuilding/SimpleBuilding_pos_flex_summary.json @@ -262,7 +262,7 @@ [ 299.0, 290.0, - 278.21911, + 278.15, 22.30528, 0.0, NaN, @@ -276,13 +276,13 @@ NaN, NaN, NaN, - 293.1132, + 293.10961, 303.15, 288.15, NaN, NaN, NaN, - 9e-05, + 4e-05, Infinity, -Infinity, 22.30528, @@ -292,7 +292,7 @@ [ 299.0, 290.0, - 278.40791, + 278.15, 22.30528, 0.0, NaN, @@ -306,13 +306,13 @@ NaN, NaN, NaN, - 290.73596, + 290.6956, 303.15, 288.15, NaN, NaN, NaN, - 9e-05, + 4e-05, Infinity, -Infinity, 22.30528, @@ -336,7 +336,7 @@ NaN, NaN, NaN, - 290.03249, + 289.9688, 303.15, 288.15, 200.0, @@ -352,7 +352,7 @@ [ 299.0, 290.0, - 278.54583, + 278.15, 59.4637, 0.0, NaN, @@ -366,13 +366,13 @@ NaN, NaN, NaN, - 292.60071, + 292.5097, 303.15, 288.15, NaN, NaN, NaN, - 9e-05, + 4e-05, Infinity, -Infinity, 200.0, @@ -481,14 +481,14 @@ "std": 6.90908 }, "parameter.T_amb": { - "25%": 280.09446, - "50%": 281.68393, - "75%": 282.75412, + "25%": 278.15, + "50%": 280.64887, + "75%": 282.46092, "count": 384.0, "max": 283.14781, - "mean": 281.30817, + "mean": 280.51268, "min": 277.24276, - "std": 1.58859 + "std": 1.94832 }, "parameter.T_lower": { "25%": 290.01321, @@ -651,44 +651,44 @@ "std": 0.0 }, "variable.P_el": { - "25%": 42.55033, - "50%": 44.91918, - "75%": 50.67189, + "25%": 44.32972, + "50%": 48.94716, + "75%": 59.19624, "count": 384.0, "max": 200.0, - "mean": 48.33466, + "mean": 52.29501, "min": 0.0, - "std": 24.25435 + "std": 24.05974 }, "variable.P_in": { - "25%": 42.55033, - "50%": 44.91918, - "75%": 50.67189, + "25%": 44.32972, + "50%": 48.94716, + "75%": 59.19624, "count": 192.0, "max": 200.0, - "mean": 48.33466, + "mean": 52.29501, "min": 0.0, - "std": 24.28607 + "std": 24.09121 }, "variable.T_slack": { - "25%": 0.00978, - "50%": 0.01008, + "25%": 0.00979, + "50%": 0.01, "75%": 0.01023, "count": 384.0, - "max": 0.51728, + "max": 0.51729, "mean": 0.02804, - "min": 9e-05, + "min": 4e-05, "std": 0.06318 }, "variable.T_zone": { "25%": 290.05558, - "50%": 290.73365, + "50%": 290.73294, "75%": 291.49585, "count": 580.0, "max": 300.70143, - "mean": 291.02684, - "min": 289.37205, - "std": 1.45947 + "mean": 291.02331, + "min": 289.36206, + "std": 1.45076 } }, "tail_5_rows": { diff --git a/tests/test_OneRoom_CIA.py b/tests/test_OneRoom_CIA.py index 33585bc4..66337042 100644 --- a/tests/test_OneRoom_CIA.py +++ b/tests/test_OneRoom_CIA.py @@ -142,7 +142,7 @@ def test_oneroom_cia(snapshot, module_cleanup): # 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["myMPCAgent"]["Baseline"] + df_baseline_res = res["Baseline"]["Baseline"] df_indicator_res = res["FlexibilityIndicator"]["FlexibilityIndicator"] # Assert that a summary of each result DataFrame matches its snapshot diff --git a/tests/test_SimpleBuilding.py b/tests/test_SimpleBuilding.py index f4555a88..70a4647a 100644 --- a/tests/test_SimpleBuilding.py +++ b/tests/test_SimpleBuilding.py @@ -144,7 +144,7 @@ def test_simplebuilding(snapshot, module_cleanup): # 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["FlexModel"]["Baseline"] + df_baseline_res = res["Baseline"]["Baseline"] df_indicator_res = res["FlexibilityIndicator"]["FlexibilityIndicator"] # Assert that a summary of each result DataFrame matches its snapshot diff --git a/tests/test_oneRoom_SimpleMPC.py b/tests/test_oneRoom_SimpleMPC.py index 72fe8eef..c90ed9e8 100644 --- a/tests/test_oneRoom_SimpleMPC.py +++ b/tests/test_oneRoom_SimpleMPC.py @@ -142,7 +142,7 @@ def test_oneroom_simple_mpc(snapshot, module_cleanup): # 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["FlexModel"]["Baseline"] + df_baseline_res = res["Baseline"]["Baseline"] df_indicator_res = res["FlexibilityIndicator"]["FlexibilityIndicator"] # Assert that a summary of each result DataFrame matches its snapshot