Coverage for agentlib_flexquant/data_structures/flexquant.py: 93%
71 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-08-01 15:10 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-08-01 15:10 +0000
1from enum import Enum
2from pathlib import Path
3from typing import List, Optional, Union
5import pydantic
6from agentlib.core.agent import AgentConfig
7from agentlib.core.errors import ConfigurationError
8from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable
9from pydantic import ConfigDict, model_validator
11from agentlib_flexquant.data_structures.mpcs import (
12 BaselineMPCData,
13 NFMPCData,
14 PFMPCData
15)
18class ForcedOffers(Enum):
19 positive = "positive"
20 negative = "negative"
23class ShadowMPCConfigGeneratorConfig(pydantic.BaseModel):
24 """Class defining the options to initialize the shadow mpc config generation."""
25 model_config = ConfigDict(
26 json_encoders={MPCVariable: lambda v: v.dict()},
27 extra='forbid'
28 )
29 weights: List[MPCVariable] = pydantic.Field(
30 default=[],
31 description="Name and value of weights",
32 )
33 pos_flex: PFMPCData = pydantic.Field(
34 default=None,
35 description="Data for PF-MPC"
36 )
37 neg_flex: NFMPCData = pydantic.Field(
38 default=None,
39 description="Data for NF-MPC"
40 )
41 @model_validator(mode="after")
42 def assign_weights_to_flex(self):
43 if self.pos_flex is None:
44 raise ValueError("Missing required field: 'pos_flex' specifying the pos flex cost function.")
45 if self.neg_flex is None:
46 raise ValueError("Missing required field: 'neg_flex' specifying the neg flex cost function.")
47 if self.weights:
48 self.pos_flex.weights = self.weights
49 self.neg_flex.weights = self.weights
50 return self
53class FlexibilityMarketConfig(pydantic.BaseModel):
54 """Class defining the options to initialize the market."""
55 model_config = ConfigDict(
56 extra='forbid'
57 )
58 agent_config: AgentConfig
59 name_of_created_file: str = pydantic.Field(
60 default="flexibility_market.json",
61 description="Name of the config that is created by the generator",
62 )
65class FlexibilityIndicatorConfig(pydantic.BaseModel):
66 """Class defining the options for the flexibility indicators."""
67 model_config = ConfigDict(
68 json_encoders={Path: str, AgentConfig: lambda v: v.model_dump()},
69 extra='forbid'
70 )
71 agent_config: AgentConfig
72 name_of_created_file: str = pydantic.Field(
73 default="indicator.json",
74 description="Name of the config that is created by the generator",
75 )
76 @model_validator(mode="after")
77 def check_file_extension(self):
78 if self.name_of_created_file:
79 file_path = Path(self.name_of_created_file)
80 if file_path.suffix != ".json":
81 raise ConfigurationError(
82 f"Invalid file extension for name_of_created_file: '{self.name_of_created_file}'. "
83 f"Expected a '.json' file."
84 )
85 return self
88class FlexQuantConfig(pydantic.BaseModel):
89 """Class defining the options to initialize the FlexQuant generation."""
90 model_config = ConfigDict(
91 json_encoders={Path: str},
92 extra='forbid'
93 )
94 prep_time: int = pydantic.Field(
95 default=1800,
96 ge=0,
97 unit="s",
98 description="Preparation time before the flexibility event",
99 )
100 flex_event_duration: int = pydantic.Field(
101 default=7200,
102 ge=0,
103 unit="s",
104 description="Flexibility event duration",
105 )
106 market_time: int = pydantic.Field(
107 default=900,
108 ge=0,
109 unit="s",
110 description="Time for market interaction",
111 )
112 indicator_config: Union[FlexibilityIndicatorConfig, Path] = pydantic.Field(
113 description="Path to the file or dict of flexibility indicator config",
114 )
115 market_config: Optional[Union[FlexibilityMarketConfig, Path]] = pydantic.Field(
116 default=None,
117 description="Path to the file or dict of market config",
118 )
119 baseline_config_generator_data: BaselineMPCData = pydantic.Field(
120 description="Baseline generator data config file or dict",
121 )
122 shadow_mpc_config_generator_data: ShadowMPCConfigGeneratorConfig = pydantic.Field(
123 description="Shadow mpc generator data config file or dict",
124 )
125 flex_base_directory_path: Optional[Path] = pydantic.Field(
126 default_factory=lambda: Path.cwd() / "flex_output_data",
127 description="Base path where generated flex data is stored",
128 )
129 flex_files_directory: Path = pydantic.Field(
130 default=Path("created_flex_files"),
131 description="Directory where generated files (jsons) should be stored",
132 )
133 results_directory: Path = pydantic.Field(
134 default=Path("results"),
135 description="Directory where generated result files (CSVs) should be stored",
136 )
137 delete_files: bool = pydantic.Field(
138 default=True,
139 description="If generated files should be deleted afterwards",
140 )
141 overwrite_files: bool = pydantic.Field(
142 default=False,
143 description="If generated files should be overwritten by new files",
144 )
146 @model_validator(mode="after")
147 def check_config_file_extension(self):
148 """
149 Validates that the indicator and market config file paths have a '.json' extension.
151 Raises:
152 ValueError: If either file does not have the expected '.json' extension.
153 """
154 if isinstance(self.indicator_config, Path) and self.indicator_config.suffix != ".json":
155 raise ValueError(
156 f"Invalid file extension for indicator config: '{self.indicator_config}'. "
157 f"Expected a '.json' file."
158 )
159 if isinstance(self.market_config, Path) and self.market_config.suffix != ".json":
160 raise ValueError(
161 f"Invalid file extension for market config: '{self.market_config}'. "
162 f"Expected a '.json' file."
163 )
164 return self
166 @model_validator(mode="after")
167 def adapt_paths_and_create_directory(self):
168 """
169 Adjusts and ensures the directory structure for flex file generation and results storage.
171 This method:
172 - Updates `flex_files_directory` and `results_directory` paths so they are relative to
173 the base flex directory, using only the directory names (ignoring any user-supplied paths).
174 - Creates the base, flex files, and results directories if they do not already exist.
175 """
176 # adapt paths and use only names for user supplied data
177 self.flex_files_directory = (
178 self.flex_base_directory_path
179 / self.flex_files_directory.name
180 )
181 self.results_directory = (
182 self.flex_base_directory_path
183 / self.results_directory.name
184 )
185 # create directories if not already existing
186 self.flex_base_directory_path.mkdir(parents=True, exist_ok=True)
187 self.flex_files_directory.mkdir(parents=True, exist_ok=True)
188 self.results_directory.mkdir(parents=True, exist_ok=True)
189 return self