Coverage for agentlib_flexquant/data_structures/mpcs.py: 99%

101 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2026-03-26 09:43 +0000

1""" 

2Data models for MPC configurations in flexibility quantification. 

3 

4This module defines Pydantic data models that encapsulate configuration parameters 

5for baseline, positive flexibility, and negative flexibility MPC controllers used 

6in flexquant. The models handle file paths, module configurations, variable 

7mappings, and optimization weights for MPC implementations. 

8""" 

9import pydantic 

10from agentlib_mpc.data_structures.mpc_datamodels import MPCVariable 

11from pydantic import model_validator, field_serializer, Field 

12 

13import agentlib_flexquant.data_structures.globals as glbs 

14import agentlib_flexquant.utils.config_management as cmng 

15 

16excluded_fields = [ 

17 "rdf_class", 

18 "source", 

19 "type", 

20 "timestamp", 

21 "description", 

22 "unit", 

23 "clip", 

24 "interpolation_method", 

25 "allowed_values", 

26 ] 

27 

28default_parameters = [ 

29 MPCVariable( 

30 name=glbs.PREP_TIME, 

31 value=0, 

32 unit="s", 

33 type="int", 

34 description="Preparation time before switching objective", 

35 ), 

36 MPCVariable( 

37 name=glbs.FLEX_EVENT_DURATION, 

38 value=0, 

39 unit="s", 

40 type="int", 

41 description="Duration of the flexibility event", 

42 ), 

43 MPCVariable( 

44 name=glbs.MARKET_TIME, 

45 value=0, 

46 unit="s", 

47 type="int", 

48 description="Market time associated with the objective switch", 

49 ), 

50 ] 

51 

52default_inputs = [ 

53 MPCVariable( 

54 name=glbs.PROVISION_VAR_NAME, 

55 value=False, 

56 type="bool", 

57 description="Flag indicating whether flexibility should be provisioned", 

58 ), 

59 ] 

60 

61def _ensure_defaults_in_appendix( 

62 data: dict, 

63 field_name: str, 

64 default_variables: list[MPCVariable], 

65) -> dict: 

66 """ 

67 Helper to ensure that all default_variables are present in 

68 data[field_name], based on their ``name`` attribute. 

69 """ 

70 provided_variables = data.get(field_name, []) 

71 provided_variables = [MPCVariable(**var) if isinstance(var, dict) else var for var in provided_variables] 

72 provided_names = {param.name for param in provided_variables} 

73 for default_var in default_variables: 

74 if default_var.name not in provided_names: 

75 provided_variables.append(default_var) 

76 

77 data[field_name] = provided_variables 

78 return data 

79 

80 

81class BaseMPCData(pydantic.BaseModel): 

82 """Base class containing necessary data for the code creation 

83 of the different mpcs 

84 

85 """ 

86 

87 # files and paths 

88 created_flex_mpcs_file: str = "flex_agents.py" 

89 name_of_created_file: str 

90 results_suffix: str 

91 # modules 

92 module_types: dict 

93 class_name: str 

94 module_id: str 

95 agent_id: str 

96 # variables 

97 power_alias: str 

98 stored_energy_alias: str 

99 config_inputs_appendix: list[MPCVariable] = Field( 

100 default=[], 

101 description="Inputs, which are appended to the MPCs' config " 

102 "(.json file and ConfigClass).") 

103 config_parameters_appendix: list[MPCVariable] = Field( 

104 default=[], 

105 description="Parameters, which are appended to the MPCs' config " 

106 "(.json file and ConfigClass).") 

107 

108 @field_serializer('config_inputs_appendix', 'config_parameters_appendix') 

109 def serialize_mpc_variables(self, variables: list[MPCVariable], _info): 

110 return [v.dict(exclude=excluded_fields) for v in variables] 

111 

112 

113class BaselineMPCData(BaseMPCData): 

114 """Data class for Baseline MPC""" 

115 

116 # files and paths 

117 results_suffix: str = "_base.csv" 

118 name_of_created_file: str = "baseline.json" 

119 # modules 

120 module_types: dict = cmng.BASELINE_MODULE_TYPE_DICT 

121 class_name: str = "BaselineMPCModel" 

122 module_id: str = "Baseline" 

123 agent_id: str = "Baseline" 

124 # variables 

125 power_alias: str = glbs.POWER_ALIAS_BASE 

126 stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_BASE 

127 power_variable: str = pydantic.Field( 

128 default="P_el", 

129 description=( 

130 "Name of the variable representing the electrical " 

131 "power in the baseline config" 

132 ), 

133 ) 

134 profile_deviation_weight: float = pydantic.Field( 

135 default=0, 

136 description="Weight of soft constraint for deviation from " 

137 "accepted flexible profile", 

138 ) 

139 power_unit: str = pydantic.Field( 

140 default="kW", description="Unit of the power variable" 

141 ) 

142 comfort_variable: str = pydantic.Field( 

143 default=None, 

144 description=( 

145 "Name of the slack variable representing the thermal " 

146 "comfort in the baseline config" 

147 ), 

148 ) 

149 profile_comfort_weight: float = pydantic.Field( 

150 default=1, description="Weight of soft constraint for discomfort", 

151 ) 

152 config_inputs_appendix: list[MPCVariable] = [ 

153 MPCVariable(name=glbs.ACCEPTED_POWER_VAR_NAME, value=0, unit="W"), 

154 MPCVariable(name=glbs.PROVISION_VAR_NAME, value=False), 

155 MPCVariable(name=glbs.RELATIVE_EVENT_START_TIME_VAR_NAME, value=0, unit="s"), 

156 MPCVariable(name=glbs.RELATIVE_EVENT_END_TIME_VAR_NAME, value=0, unit="s"), 

157 ] 

158 

159 config_parameters_appendix: list[MPCVariable] = [] 

160 

161 

162 @field_serializer('config_inputs_appendix', 'config_parameters_appendix') 

163 def serialize_mpc_variables(self, variables: list[MPCVariable], _info): 

164 return [v.dict(exclude=excluded_fields) for v in variables] 

165 

166 @model_validator(mode="after") 

167 def update_config_parameters_appendix(self) -> "BaselineMPCData": 

168 """Update the config parameters appendix with profile deviation and comfort 

169 weights. 

170 

171 Adds the profile deviation weight parameter and optionally the profile 

172 comfort weight parameter (if comfort_variable is enabled) to the 

173 config_parameters_appendix list as MPCVariable instances. 

174 """ 

175 self.config_parameters_appendix = [ 

176 MPCVariable( 

177 name=glbs.PROFILE_DEVIATION_WEIGHT, 

178 value=self.profile_deviation_weight, 

179 unit="-", 

180 description=( 

181 "Weight of soft constraint for deviation from accepted " 

182 "flexible profile" 

183 ), 

184 ) 

185 ] 

186 if self.comfort_variable: 

187 self.config_parameters_appendix.append( 

188 MPCVariable( 

189 name=glbs.PROFILE_COMFORT_WEIGHT, 

190 value=self.profile_comfort_weight, 

191 unit="-", 

192 description="Weight of soft constraint for discomfort", 

193 ) 

194 ) 

195 return self 

196 

197 

198class PFMPCData(BaseMPCData): 

199 """Data class for PF-MPC""" 

200 

201 # files and paths 

202 results_suffix: str = "_pos_flex.csv" 

203 name_of_created_file: str = "pos_flex.json" 

204 # modules 

205 module_types: dict = cmng.SHADOW_MODULE_TYPE_DICT 

206 class_name: str = "PosFlexModel" 

207 module_id: str = "PosFlexMPC" 

208 agent_id: str = "PosFlexMPC" 

209 # variables 

210 power_alias: str = glbs.POWER_ALIAS_POS 

211 stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_POS 

212 flex_cost_function: str = pydantic.Field( 

213 default=None, description="Cost function of the PF-MPC during the event", 

214 ) 

215 flex_cost_function_appendix: str = pydantic.Field( 

216 default=None, description="Cost function appendix of the PF-MPC added " 

217 "to the Baseline cost function", 

218 ) 

219 # initialize market parameters with dummy values (0) 

220 config_parameters_appendix: list[MPCVariable] = pydantic.Field( 

221 default=[], description="Parameters, which need to be appended to the shadow MPCs" 

222 ) 

223 config_inputs_appendix: list[MPCVariable] = pydantic.Field( 

224 default=[], description="Inputs, which need to be appended to the shadow MPCs" 

225 ) 

226 

227 

228 @field_serializer('config_inputs_appendix', 'config_parameters_appendix') 

229 def serialize_mpc_variables(self, variables: list[MPCVariable], _info): 

230 return [v.dict(exclude=excluded_fields) for v in variables] 

231 

232 @model_validator(mode="before") 

233 @classmethod 

234 def add_defaults_to_appendix(cls, data): 

235 """ 

236 Ensure that all required framework variables are included in both 

237 ``config_inputs_appendix`` and ``config_parameters_appendix``. 

238 If any default framework input or parameter (e.g., PROVISION_VAR_NAME) 

239 is missing, it is appended to the corresponding list. 

240 """ 

241 

242 data = _ensure_defaults_in_appendix( 

243 data=data, 

244 field_name="config_inputs_appendix", 

245 default_variables=default_inputs, 

246 ) 

247 

248 data = _ensure_defaults_in_appendix( 

249 data=data, 

250 field_name="config_parameters_appendix", 

251 default_variables=default_parameters, 

252 ) 

253 return data 

254 

255 

256class NFMPCData(BaseMPCData): 

257 """Data class for NF-MPC""" 

258 

259 # files and paths 

260 results_suffix: str = "_neg_flex.csv" 

261 name_of_created_file: str = "neg_flex.json" 

262 # modules 

263 module_types: dict = cmng.SHADOW_MODULE_TYPE_DICT 

264 class_name: str = "NegFlexModel" 

265 module_id: str = "NegFlexMPC" 

266 agent_id: str = "NegFlexMPC" 

267 # variables 

268 power_alias: str = glbs.POWER_ALIAS_NEG 

269 stored_energy_alias: str = glbs.STORED_ENERGY_ALIAS_NEG 

270 flex_cost_function: str = pydantic.Field( 

271 default=None, description="Cost function of the NF-MPC during the event", 

272 ) 

273 flex_cost_function_appendix: str = pydantic.Field( 

274 default=None, description="Cost function appendix of the NF-MPC added " 

275 "to the Baseline cost function", 

276 ) 

277 # initialize market parameters with dummy values (0) 

278 config_parameters_appendix: list[MPCVariable] = pydantic.Field( 

279 default=[], description="Parameters, which need to be appended to the shadow MPCs" 

280 ) 

281 config_inputs_appendix: list[MPCVariable] = pydantic.Field( 

282 default=[], description = "Inputs, which need to be appended to the shadow MPCs" 

283 ) 

284 

285 

286 @field_serializer('config_inputs_appendix', 'config_parameters_appendix') 

287 def serialize_mpc_variables(self, variables: list[MPCVariable], _info): 

288 return [v.dict(exclude=excluded_fields) for v in variables] 

289 

290 

291 @model_validator(mode="before") 

292 @classmethod 

293 def add_defaults_to_appendix(cls, data): 

294 """ 

295 Ensures that all required framework input and parameter variables are included in 

296 ``config_inputs_appendix`` and ``config_parameters_appendix``. If any default 

297 framework input or parameter (e.g., PROVISION_VAR_NAME) is missing, it is appended 

298 to the corresponding list. 

299 """ 

300 

301 data = _ensure_defaults_in_appendix( 

302 data=data, 

303 field_name="config_inputs_appendix", 

304 default_variables=default_inputs, 

305 ) 

306 

307 data = _ensure_defaults_in_appendix( 

308 data=data, 

309 field_name="config_parameters_appendix", 

310 default_variables=default_parameters, 

311 ) 

312 return data