Coverage for agentlib_flexquant/modules/shadow_mpc.py: 71%
278 statements
« prev ^ index » next coverage.py v7.4.4, created at 2026-03-26 09:43 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2026-03-26 09:43 +0000
1"""
2Defines shadow MPC and MINLP-MPC for positive/negative flexibility quantification.
3"""
4import os
5import math
6import numpy as np
7import pandas as pd
8from pydantic import Field
9from typing import Dict, Union, Optional
10from collections.abc import Iterable
11from agentlib.core.datamodels import AgentVariable, Source
12from agentlib_mpc.modules.mpc import mpc_full, minlp_mpc
13from agentlib_flexquant.utils.data_handling import fill_nans, MEAN
14from agentlib_flexquant.data_structures.globals import (full_trajectory_suffix,
15 base_vars_to_communicate_suffix)
16import agentlib_flexquant.data_structures.globals as glbs
17from agentlib_flexquant.optimization_backends.constrained_cia import ConstrainedCasADiCIABackend
20class FlexibilityShadowMPCConfig(mpc_full.MPCConfig):
22 baseline_input_names: list[str] = Field(default=[])
23 custom_input_names: list[Dict] = Field(default=[])
24 baseline_state_names: list[str] = Field(default=[])
25 full_control_names: list[str] = Field(default=[])
28 baseline_agent_id: str = ""
30 casadi_sim_time_step: int = Field(
31 default=0,
32 description="Time step for simulation with Casadi simulator. "
33 "Value is read from FlexQuantConfig",
34 )
35 power_variable_name: str = Field(
36 default=None, description="Name of the power variable in the "
37 "shadow mpc model."
38 )
39 storage_variable_name: Optional[str] = Field(
40 default=None, description="Name of the storage variable in the "
41 "shadow mpc model."
42 )
45class FlexibilityShadowMPC(mpc_full.MPC):
46 """Shadow MPC for calculating positive/negative flexibility offers."""
48 config: FlexibilityShadowMPCConfig
50 def __init__(self, *args, **kwargs):
51 # initialize flex_results with None
52 self.flex_results = None
54 super().__init__(*args, **kwargs)
56 # setup look up dict to track incoming inputs and states
57 # (maps name as str to actual AgentVariable)
58 input_names_list = [var["name"] for var in self.config.custom_input_names]
59 self._track_base_comm_vars_dict: Dict[str, Union[AgentVariable, None]] = {}
60 for comm_var in self.config.inputs + self.config.states:
61 if (comm_var.name in self.config.full_control_names or
62 comm_var.name + base_vars_to_communicate_suffix in
63 self.config.baseline_input_names or
64 comm_var.name + base_vars_to_communicate_suffix in
65 self.config.baseline_state_names or
66 comm_var.name in input_names_list):
67 comm_var.value = None
68 self._track_base_comm_vars_dict[comm_var.name] = comm_var.copy(deep=True)
69 # set up necessary components if simulation is enabled
70 if self.config.casadi_sim_time_step > 0:
71 # generate a separate simulation model for integration to ensure
72 # the model used in MPC optimization remains unaffected
73 self.flex_model = type(self.model)(dt=self.config.casadi_sim_time_step)
74 # generate the filename for the simulation results
75 self.res_file_flex = self.config.optimization_backend["results_file"].replace(
76 "_flex", "_sim_flex"
77 )
78 # clear the casadi simulator result at the first time step if already exists
79 try:
80 os.remove(self.res_file_flex)
81 except FileNotFoundError:
82 pass
84 def set_output(self, solution):
85 """Takes the solution from optimization backend and sends it to AgentVariables."""
86 # Output must be defined in the config as "type"="pd.Series"
87 if not self.config.set_outputs:
88 return
89 self.logger.info("Sending optimal output values to data_broker.")
90 df = solution.df
91 self.sim_flex_model(solution)
92 if self.flex_results is not None:
93 for output in self.var_ref.outputs:
94 if output not in [
95 self.config.power_variable_name,
96 self.config.storage_variable_name,
97 ]:
98 series = df.variable[output]
99 self.set(output, series)
100 # send the power and storage variable value from simulation results
101 upsampled_output_power = self.flex_results[self.config.power_variable_name]
102 self.set(self.config.power_variable_name, upsampled_output_power)
103 if self.config.storage_variable_name is not None:
104 upsampled_output_storage = self.flex_results[self.config.storage_variable_name]
105 self.set(self.config.storage_variable_name, upsampled_output_storage.dropna())
106 else:
107 for output in self.var_ref.outputs:
108 series = df.variable[output]
109 self.set(output, series)
111 def sim_flex_model(self, solution):
112 """simulate the flex model over the preditcion horizon and save results"""
114 # return if sim_time_step is not a positive integer and system is in provision
115 if not (self.config.casadi_sim_time_step > 0 and not self.get(glbs.PROVISION_VAR_NAME).value):
116 return
118 # read the defined simulation time step
119 sim_time_step = self.config.casadi_sim_time_step
120 mpc_time_step = self.config.time_step
122 # set the horizon length and the number of simulation steps
123 total_horizon_time = int(self.config.prediction_horizon * self.config.time_step)
124 n_simulation_steps = math.ceil(total_horizon_time / sim_time_step)
126 # read the current optimization result
127 result_df = solution.df
129 # initialize the flex sim results Dataframe
130 self._initialize_flex_results(
131 n_simulation_steps, total_horizon_time, sim_time_step, result_df
132 )
134 # Update model parameters and initial states
135 self._update_model_parameters()
136 self._update_initial_states(result_df)
138 # Run simulation
139 self._run_simulation(
140 n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time
141 )
143 # set index of flex results to the same as mpc result
144 store_results_df = self.flex_results.copy(deep=True)
145 store_results_df.index = self.flex_results.index.tolist()
147 # save results
148 if not os.path.exists(self.res_file_flex):
149 store_results_df.to_csv(self.res_file_flex)
150 else:
151 store_results_df.to_csv(self.res_file_flex, mode="a", header=False)
153 # set the flex results format same as mpc result while updating Agentvariable
154 self.flex_results.index = self.flex_results.index.get_level_values(1)
156 def register_callbacks(self):
157 for control_var in self.config.controls:
158 self.agent.data_broker.register_callback(
159 name=control_var.name + full_trajectory_suffix,
160 alias=control_var.name + full_trajectory_suffix,
161 callback=self.calc_flex_callback,
162 source=Source(agent_id=self.config.baseline_agent_id, module_id=None)
163 )
164 for base_inputs in self.config.baseline_input_names:
165 self.agent.data_broker.register_callback(
166 name=base_inputs.removesuffix(base_vars_to_communicate_suffix), # update MPC variable
167 alias=base_inputs,
168 callback=self.calc_flex_callback,
169 source=Source(agent_id=self.config.baseline_agent_id, module_id=None)
170 )
171 for custom_inputs in self.config.custom_input_names:
172 self.agent.data_broker.register_callback(
173 name=custom_inputs["name"],
174 alias=custom_inputs["alias"],
175 callback=self.calc_flex_callback
176 )
177 for base_states in self.config.baseline_state_names:
178 self.agent.data_broker.register_callback(
179 name=base_states.removesuffix(base_vars_to_communicate_suffix), # update MPC variable
180 alias=base_states,
181 callback=self.calc_flex_callback,
182 source=Source(agent_id=self.config.baseline_agent_id, module_id=None)
183 )
184 super().register_callbacks()
186 def calc_flex_callback(self, inp: AgentVariable, name: str):
187 """Ensure that all control trajectories and Baseline inputs/states
188 have been set before starting the calculation.
190 """
191 # during provision do not calculate flex
192 if self.get(glbs.PROVISION_VAR_NAME).value:
193 return
195 # do not trigger callback on self set variables
196 if self.agent.config.id == inp.source.agent_id:
197 return
199 # get the value of the input
200 vals = inp.value
202 if inp.name in self.config.full_control_names:
203 if vals.isna().any():
204 vals = fill_nans(series=vals, method=MEAN)
205 # add time shift env.time to the incoming variable to adapt to mpc output,
206 # which starts at t=0
207 if vals.index[0] == 0:
208 self.logger.info(f"The incoming variable {inp.name} starts with a time "
209 f"index of 0. Adding the current environment time.")
210 vals.index += self.env.time
212 # update value in the tracking dictionary
213 self._track_base_comm_vars_dict[name].value = vals
214 # set value
215 self.set(name, vals)
217 # make sure all necessary inputs are set
218 if all(x.value is not None for x in self._track_base_comm_vars_dict.values()):
219 self.do_step()
220 for _, comm_var in self._track_base_comm_vars_dict.items():
221 comm_var.value = None
223 def process(self):
224 # the shadow mpc should only be run after the results of the baseline are sent
225 yield self.env.event()
227 def _initialize_flex_results(
228 self, n_simulation_steps, horizon_length, sim_time_step, result_df
229 ):
230 """Initialize the flex results dataframe with the correct dimension
231 and index and fill with existing results from optimization
233 """
235 # create MultiIndex for collocation points
236 index_coll = pd.MultiIndex.from_arrays(
237 [[self.env.now] * len(result_df.index), result_df.index],
238 names=["time_step", "time"]
239 # Match the names with multi_index but note they're reversed
240 )
241 # create Multiindex for full simulation sample times
242 index_full_sample = pd.MultiIndex.from_tuples(
243 zip(
244 [self.env.now] * (n_simulation_steps + 1),
245 range(0, horizon_length + sim_time_step, sim_time_step),
246 ),
247 names=["time_step", "time"],
248 )
249 # merge indexes
250 new_index = index_coll.union(index_full_sample).sort_values()
251 # initialize the flex results with correct dimension
252 self.flex_results = pd.DataFrame(np.nan,
253 index=new_index,
254 columns=self.var_ref.outputs)
256 # Get the optimization outputs and create a series for fixed
257 # optimization outputs with the correct MultiIndex format
258 opti_outputs = result_df.variable[self.config.power_variable_name]
259 fixed_opti_output = pd.Series(
260 opti_outputs.values,
261 index=index_coll,
262 )
263 # fill the output value at the time step where it already exists
264 # in optimization output
265 for idx in fixed_opti_output.index:
266 if idx in self.flex_results.index:
267 self.flex_results.loc[idx, self.config.power_variable_name] = (
268 fixed_opti_output)[idx]
270 def _update_model_parameters(self):
271 """update the value of module parameters with value from config,
272 since creating a model just reads the value in the model class
273 but not the config.
275 """
277 for par in self.config.parameters:
278 self.flex_model.set(par.name, par.value)
280 def _update_initial_states(self, result_df):
281 """set the initial value of states"""
283 # get state values from the mpc optimization result
284 state_values = result_df.variable[self.var_ref.states]
285 # update state values with last measurement
286 for state, value in zip(self.var_ref.states, state_values.iloc[0]):
287 self.flex_model.set(state, value)
289 def _run_simulation(
290 self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time
291 ):
292 """simulate with flex model over the prediction horizon
294 """
296 # get control and input values from the mpc optimization result
297 control_values = result_df.variable[self.var_ref.controls].dropna()
298 input_values = result_df.parameter[self.var_ref.inputs].dropna()
300 # Get the simulation time step index
301 sim_time_index = np.arange(0, (n_simulation_steps + 1) * sim_time_step, sim_time_step)
303 # Reindex the controls and inputs to sim_time_index
304 control_values_full = control_values.copy().reindex(sim_time_index, method="ffill")
305 input_values_full = input_values.copy().reindex(sim_time_index, method="nearest")
307 for i in range(0, n_simulation_steps):
308 current_sim_time = i * sim_time_step
310 # Apply control and input values from the appropriate MPC step
311 for control, value in zip(
312 self.var_ref.controls, control_values_full.loc[current_sim_time]
313 ):
314 self.flex_model.set(control, value)
316 for input_var, value in zip(
317 self.var_ref.inputs, input_values_full.loc[current_sim_time]
318 ):
319 # change the type of iterable input, since casadi model can't deal with iterable
320 if issubclass(eval(self.flex_model.get(input_var).type), Iterable):
321 self.flex_model.get(input_var).type = type(value).__name__
322 self.flex_model.set(input_var, value)
324 # do integration
325 # reduce the simulation time step so that the total horizon time will not be exceeded
326 if current_sim_time + sim_time_step <= total_horizon_time:
327 t_sample = sim_time_step
328 else:
329 t_sample = total_horizon_time - current_sim_time
330 self.flex_model.do_step(t_start=0, t_sample=t_sample)
332 # save output
333 for output in self.var_ref.outputs:
334 self.flex_results.loc[
335 (self.env.now, current_sim_time + t_sample), output
336 ] = self.flex_model.get_output(output).value
339class FlexibilityShadowMINLPMPCConfig(minlp_mpc.MINLPMPCConfig):
341 baseline_input_names: list[str] = Field(default=[])
342 custom_input_names: list[Dict] = Field(default=[])
343 baseline_state_names: list[str] = Field(default=[])
344 full_control_names: list[str] = Field(default=[])
346 baseline_agent_id: str = ""
348 casadi_sim_time_step: int = Field(
349 default=0,
350 description="Time step for simulation with Casadi simulator. "
351 "Value is read from FlexQuantConfig",
352 )
353 power_variable_name: str = Field(
354 default=None, description="Name of the power variable in the "
355 "shadow mpc model."
356 )
357 storage_variable_name: Optional[str] = Field(
358 default=None, description="Name of the storage variable in the "
359 "shadow mpc model."
360 )
363class FlexibilityShadowMINLPMPC(minlp_mpc.MINLPMPC):
364 """Shadow MINLP-MPC for calculating positive/negatives flexibility offers.
366 """
368 config: FlexibilityShadowMINLPMPCConfig
370 def __init__(self, *args, **kwargs):
371 # initialize flex_results with None
372 self.flex_results = None
374 super().__init__(*args, **kwargs)
376 # setup look up dict to track incoming inputs and states
377 # (maps name as str to actual AgentVariable)
378 input_names_list = [var["name"] for var in self.config.custom_input_names]
379 self._track_base_comm_vars_dict: Dict[str, Union[AgentVariable, None]] = {}
380 for comm_var in self.config.inputs + self.config.states:
381 if (comm_var.name in self.config.full_control_names or
382 comm_var.name + base_vars_to_communicate_suffix in
383 self.config.baseline_input_names or
384 comm_var.name + base_vars_to_communicate_suffix in
385 self.config.baseline_state_names or
386 comm_var.name in input_names_list):
387 comm_var.value = None
388 self._track_base_comm_vars_dict[comm_var.name] = comm_var.copy(deep=True)
389 # set up necessary components if simulation is enabled
390 if self.config.casadi_sim_time_step > 0:
391 # generate a separate simulation model for integration to ensure
392 # the model used in MPC optimization remains unaffected
393 self.flex_model = type(self.model)(dt=self.config.casadi_sim_time_step)
394 # generate the filename for the simulation results
395 self.res_file_flex = self.config.optimization_backend["results_file"].replace(
396 "_flex", "_sim_flex"
397 )
398 # clear the casadi simulator result at the first time step if already exists
399 try:
400 os.remove(self.res_file_flex)
401 except FileNotFoundError:
402 pass
404 def register_callbacks(self):
405 for control_var in self.config.controls + self.config.binary_controls:
406 self.agent.data_broker.register_callback(
407 name=control_var.name + full_trajectory_suffix,
408 alias=control_var.name + full_trajectory_suffix,
409 callback=self.calc_flex_callback,
410 source=Source(agent_id=self.config.baseline_agent_id, module_id=None)
411 )
412 for base_inputs in self.config.baseline_input_names:
413 self.agent.data_broker.register_callback(
414 name=base_inputs.removesuffix(base_vars_to_communicate_suffix), # update MPC variable
415 alias=base_inputs,
416 callback=self.calc_flex_callback,
417 source=Source(agent_id=self.config.baseline_agent_id, module_id=None)
418 )
419 for custom_inputs in self.config.custom_input_names:
420 self.agent.data_broker.register_callback(
421 name=custom_inputs["name"],
422 alias=custom_inputs["alias"],
423 callback=self.calc_flex_callback
424 )
425 for base_states in self.config.baseline_state_names:
426 self.agent.data_broker.register_callback(
427 name=base_states.removesuffix(base_vars_to_communicate_suffix), # update MPC variable
428 alias=base_states,
429 callback=self.calc_flex_callback,
430 source=Source(agent_id=self.config.baseline_agent_id, module_id=None)
431 )
433 super().register_callbacks()
435 def calc_flex_callback(self, inp: AgentVariable, name: str):
436 """Ensure that all control trajectories and Baseline inputs/states
437 have been set before starting the calculation.
439 """
440 # during provision do not calculate flex
441 if self.get(glbs.PROVISION_VAR_NAME).value:
442 return
444 # do not trigger callback on self set variables
445 if self.agent.config.id == inp.source.agent_id:
446 return
448 # get the value of the input
449 vals = inp.value
451 if inp.name in self.config.full_control_names:
452 if vals.isna().any():
453 vals = fill_nans(series=vals, method=MEAN)
454 # set full controls to custom cia backend to constrain during market time
455 if isinstance(self.optimization_backend, ConstrainedCasADiCIABackend):
456 self.optimization_backend.config.full_controls_dict[inp.name] = vals
457 # add time shift env.now to the mpc prediction index if it starts at t=0
458 if vals.index[0] == 0:
459 vals.index += self.env.time
461 # update value in the tracking dictionary
462 self._track_base_comm_vars_dict[name].value = vals
463 # set value
464 self.set(name, vals)
466 # make sure all necessary inputs are set
467 if all(x.value is not None for x in self._track_base_comm_vars_dict.values()):
468 self.do_step()
469 for _, comm_var in self._track_base_comm_vars_dict.items():
470 comm_var.value = None
472 def process(self):
473 # the shadow mpc should only be run after the results of the baseline are sent
474 yield self.env.event()
476 def set_output(self, solution):
477 """Takes the solution from optimization backend and sends
478 it to AgentVariables.
480 """
481 # Output must be defined in the config as "type"="pd.Series"
482 if not self.config.set_outputs:
483 return
484 self.logger.info("Sending optimal output values to data_broker.")
486 # simulate with the casadi simulator
487 self.sim_flex_model(solution)
489 df = solution.df
490 if self.flex_results is not None:
491 for output in self.var_ref.outputs:
492 if output not in [
493 self.config.power_variable_name,
494 self.config.storage_variable_name,
495 ]:
496 series = df.variable[output]
497 self.set(output, series)
498 # send the power and storage variable value from simulation results
499 upsampled_output_power = self.flex_results[self.config.power_variable_name]
500 self.set(self.config.power_variable_name, upsampled_output_power)
501 if self.config.storage_variable_name is not None:
502 upsampled_output_storage = self.flex_results[self.config.storage_variable_name]
503 self.set(self.config.storage_variable_name, upsampled_output_storage.dropna())
504 else:
505 for output in self.var_ref.outputs:
506 series = df.variable[output]
507 self.set(output, series)
509 def sim_flex_model(self, solution):
510 """simulate the flex model over the preditcion horizon and save results
512 """
514 # return if sim_time_step is not a positive integer and system is in provision
515 if not (self.config.casadi_sim_time_step > 0 and not self.get(glbs.PROVISION_VAR_NAME).value):
516 return
518 # read the defined simulation time step
519 sim_time_step = self.config.casadi_sim_time_step
520 mpc_time_step = self.config.time_step
522 # set the horizon length and the number of simulation steps
523 total_horizon_time = int(self.config.prediction_horizon * self.config.time_step)
524 n_simulation_steps = math.ceil(total_horizon_time / sim_time_step)
526 # read the current optimization result
527 result_df = solution.df
529 # initialize the flex sim results Dataframe
530 self._initialize_flex_results(
531 n_simulation_steps, total_horizon_time, sim_time_step, result_df
532 )
534 # Update model parameters and initial states
535 self._update_model_parameters()
536 self._update_initial_states(result_df)
538 # Run simulation
539 self._run_simulation(
540 n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time
541 )
543 # set index of flex results to the same as mpc result
544 store_results_df = self.flex_results.copy(deep=True)
545 store_results_df.index = self.flex_results.index.tolist()
547 # save results
548 if not os.path.exists(self.res_file_flex):
549 store_results_df.to_csv(self.res_file_flex)
550 else:
551 store_results_df.to_csv(self.res_file_flex, mode="a", header=False)
553 # set the flex results format same as mpc result while updating Agentvariable
554 self.flex_results.index = self.flex_results.index.get_level_values(1)
556 def _initialize_flex_results(
557 self, n_simulation_steps, horizon_length, sim_time_step, result_df
558 ):
559 """Initialize the flex results dataframe with the correct dimension
560 and index and fill with existing results from optimization
562 """
564 # create MultiIndex for collocation points
565 index_coll = pd.MultiIndex.from_arrays(
566 [[self.env.now] * len(result_df.index), result_df.index],
567 names=["time_step", "time"]
568 # Match the names with multi_index but note they're reversed
569 )
570 # create Multiindex for full simulation sample times
571 index_full_sample = pd.MultiIndex.from_tuples(
572 zip(
573 [self.env.now] * (n_simulation_steps + 1),
574 range(0, horizon_length + sim_time_step, sim_time_step),
575 ),
576 names=["time_step", "time"],
577 )
578 # merge indexes
579 new_index = index_coll.union(index_full_sample).sort_values()
580 # initialize the flex results with correct dimension
581 self.flex_results = pd.DataFrame(np.nan, index=new_index, columns=self.var_ref.outputs)
583 # Get the optimization outputs and create a series for fixed optimization outputs with the
584 # correct MultiIndex format
585 opti_outputs = result_df.variable[self.config.power_variable_name]
586 fixed_opti_output = pd.Series(
587 opti_outputs.values,
588 index=index_coll,
589 )
590 # fill the output value at the time step where it already exists in optimization output
591 for idx in fixed_opti_output.index:
592 if idx in self.flex_results.index:
593 self.flex_results.loc[idx, self.config.power_variable_name] = fixed_opti_output[idx]
595 def _update_model_parameters(self):
596 """update the value of module parameters with value from config,
597 since creating a model just reads the value in the model class but not the config
598 """
600 for par in self.config.parameters:
601 self.flex_model.set(par.name, par.value)
603 def _update_initial_states(self, result_df):
604 """set the initial value of states"""
606 # get state values from the mpc optimization result
607 state_values = result_df.variable[self.var_ref.states]
608 # update state values with last measurement
609 for state, value in zip(self.var_ref.states, state_values.iloc[0]):
610 self.flex_model.set(state, value)
612 def _run_simulation(
613 self, n_simulation_steps, sim_time_step, mpc_time_step, result_df, total_horizon_time
614 ):
615 """simulate with flex model over the prediction horizon"""
617 # get control and input values from the mpc optimization result
618 control_values = result_df.variable[
619 [*self.var_ref.controls, *self.var_ref.binary_controls]
620 ].dropna()
621 input_values = result_df.parameter[self.var_ref.inputs].dropna()
623 # Get the simulation time step index
624 sim_time_index = np.arange(0, (n_simulation_steps + 1) * sim_time_step, sim_time_step)
626 # Reindex the controls and inputs to sim_time_index
627 control_values_full = control_values.copy().reindex(sim_time_index, method="ffill")
628 input_values_full = input_values.copy().reindex(sim_time_index, method="nearest")
630 for i in range(0, n_simulation_steps):
631 current_sim_time = i * sim_time_step
633 # Apply control and input values from the appropriate MPC step
634 for control, value in zip(
635 self.var_ref.controls,
636 control_values_full.loc[current_sim_time, self.var_ref.controls],
637 ):
638 self.flex_model.set(control, value)
640 for binary_control, value in zip(
641 self.var_ref.binary_controls,
642 control_values_full.loc[current_sim_time, self.var_ref.binary_controls],
643 ):
644 self.flex_model.set(binary_control, value)
646 for input_var, value in zip(
647 self.var_ref.inputs, input_values_full.loc[current_sim_time]
648 ):
649 # change the type of iterable input, since casadi model can't deal with iterable
650 if issubclass(eval(self.flex_model.get(input_var).type), Iterable):
651 self.flex_model.get(input_var).type = type(value).__name__
652 self.flex_model.set(input_var, value)
654 # do integration
655 # reduce the simulation time step so that the total horizon time will not be exceeded
656 if current_sim_time + sim_time_step <= total_horizon_time:
657 t_sample = sim_time_step
658 else:
659 t_sample = total_horizon_time - current_sim_time
660 self.flex_model.do_step(t_start=0, t_sample=t_sample)
662 # save output
663 for output in self.var_ref.outputs:
664 self.flex_results.loc[
665 (self.env.now, current_sim_time + t_sample), output
666 ] = self.flex_model.get_output(output).value