Source code for agentlib_flexquant.data_structures.flexquant

"""
Pydantic data models for FlexQuant configuration and validation.
"""
# from enum import Enum
from pathlib import Path
from typing import Optional, Union

import pydantic
from pydantic import field_validator, ConfigDict, model_validator, Field, BaseModel
from agentlib.core.agent import AgentConfig
from agentlib.core.errors import ConfigurationError
from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable

from agentlib_flexquant.data_structures.mpcs import (
    BaselineMPCData,
    NFMPCData,
    PFMPCData,
)


# class ForcedOffers(Enum):
#     positive = "positive"
#     negative = "negative"


[docs]class ShadowMPCConfigGeneratorConfig(BaseModel): """Class defining the options to initialize the shadow mpc config generation.""" model_config = ConfigDict( json_encoders={MPCVariable: lambda v: v.dict()}, extra="forbid" ) weights: list[MPCVariable] = Field( default=[], description="Name and value of weights", ) pos_flex: PFMPCData = Field(default=None, description="Data for PF-MPC") neg_flex: NFMPCData = Field(default=None, description="Data for NF-MPC")
[docs] @model_validator(mode="after") 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." ) if self.neg_flex is None: raise ValueError( "Missing required field: 'neg_flex' specifying the neg flex cost function." ) if self.weights: self.pos_flex.weights = self.weights self.neg_flex.weights = self.weights return self
[docs]class FlexibilityMarketConfig(BaseModel): """Class defining the options to initialize the market.""" model_config = ConfigDict(extra="forbid") agent_config: AgentConfig name_of_created_file: str = Field( default="flexibility_market.json", description="Name of the config that is created by the generator", )
[docs]class FlexibilityIndicatorConfig(BaseModel): """Class defining the options for the flexibility indicators.""" model_config = ConfigDict( json_encoders={Path: str, AgentConfig: lambda v: v.model_dump()}, extra="forbid" ) agent_config: AgentConfig name_of_created_file: str = Field( default="indicator.json", description="Name of the config that is created by the generator", )
[docs] @model_validator(mode="after") def check_file_extension(self): """Validate that name_of_created_file has a .json extension.""" if self.name_of_created_file: file_path = Path(self.name_of_created_file) if file_path.suffix != ".json": raise ConfigurationError( f"Invalid file extension for " f"name_of_created_file: '{self.name_of_created_file}'. " f"Expected a '.json' file." ) return self
[docs]class FlexQuantConfig(BaseModel): """Class defining the options to initialize the FlexQuant generation.""" model_config = ConfigDict(json_encoders={Path: str}, extra="forbid") prep_time: int = Field( default=1800, ge=0, unit="s", description="Preparation time before the flexibility event", ) flex_event_duration: int = Field( default=7200, ge=0, unit="s", description="Flexibility event duration", ) market_time: int = Field( default=900, ge=0, unit="s", description="Time for market interaction", ) indicator_config: Union[FlexibilityIndicatorConfig, Path] = Field( description="Path to the file or dict of flexibility indicator config", ) market_config: Optional[Union[FlexibilityMarketConfig, Path]] = Field( default=None, description="Path to the file or dict of market config", ) baseline_config_generator_data: BaselineMPCData = Field( description="Baseline generator data config file or dict", ) shadow_mpc_config_generator_data: ShadowMPCConfigGeneratorConfig = Field( description="Shadow mpc generator data config file or dict", ) 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." "Set to 0 to skip simulation", ) flex_base_directory_path: Optional[Path] = Field( default_factory=lambda: Path.cwd() / "flex_output_data", description="Base path where generated flex data is stored", ) flex_files_directory: Path = Field( default=Path("created_flex_files"), description="Directory where generated files (jsons) should be stored", ) results_directory: Path = Field( default=Path("results"), description="Directory where generated result files (CSVs) should be stored", ) delete_files: bool = Field( default=True, description="If generated files should be deleted afterwards", ) overwrite_files: bool = Field( default=False, description="If generated files should be overwritten by new files", )
[docs] @model_validator(mode="after") def check_config_file_extension(self): """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. """ if ( isinstance(self.indicator_config, Path) and self.indicator_config.suffix != ".json" ): raise ValueError( f"Invalid file extension for indicator config: '{self.indicator_config}'. " f"Expected a '.json' file." ) if ( isinstance(self.market_config, Path) and self.market_config.suffix != ".json" ): raise ValueError( f"Invalid file extension for market config: '{self.market_config}'. " f"Expected a '.json' file." ) return self
[docs] @field_validator('casadi_sim_time_step', mode='after') @classmethod def is_none_negative_integer(cls, value: int) -> int: if value < 0: raise ValueError(f'{value} is not a non-negative integer') return value
[docs] @model_validator(mode="after") def adapt_paths_and_create_directory(self): """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. """ # adapt paths and use only names for user supplied data self.flex_files_directory = ( self.flex_base_directory_path / self.flex_files_directory.name ) self.results_directory = ( self.flex_base_directory_path / self.results_directory.name ) # create directories if not already existing self.flex_base_directory_path.mkdir(parents=True, exist_ok=True) self.flex_files_directory.mkdir(parents=True, exist_ok=True) self.results_directory.mkdir(parents=True, exist_ok=True) return self