Variable Dependencies

Variable Dependencies#

In many optimization problems, a large number of variables must be considered simultaneously, leading to high complexity. For more advanced problems, reducing the degrees of freedom can greatly simplify the optimization process and lead to faster convergence and better results. One way to achieve this is to define dependencies between individual variables.

When defining dependencies, it is important that the optimizer is not exposed to them directly. Instead, the dependencies should be integrated into the model in a way that is transparent to the optimizer. Different mechanisms can be used to define dependencies, including linear combinations and custom functions. With linear combinations, variables are combined using weights or coefficients, while custom functions allow for more complex relationships between variables to be defined.

For example, consider a process where the same parameter is used in multiple unit operations. To reduce the number of variables that the optimizer needs to consider, it is possible to add a single variable, which is then set on both evaluation objects in pre-processing. In other cases, the ratio between model parameters may be essential for the optimization problem.

../../_images/transform_dependency.svg

For example, consider an OptimizationProblem where the ratio of two variables should be considered as a third variable. First, all variables need to be added.

from CADETProcess.optimization import OptimizationProblem
optimization_problem = OptimizationProblem('transform_demo')

optimization_problem.add_variable('var_0')
optimization_problem.add_variable('var_1')
optimization_problem.add_variable('var_2')
OptimizationVariable(name=var_2, evaluation_objects=[], parameter_path=None, lb=-inf, ub=inf)

To add the dependency, the dependent variable needs to be specified, as well as a list of the independent variables. Finally, a callable that takes the independent variables and returns the value of the dependent variable value needs to be added. For more information refer to add_variable_dependency().

def transform_fun(var_0, var_1):
    return var_0/var_1

optimization_problem.add_variable_dependency('var_2', ['var_0', 'var_1'], transform=transform_fun)

Note that generally bounds and linear constraints can still be specified independently for all variables.

Adsorption rates example#

For instance, consider an adsorption proces with an adsorption rate \(k_a\) and a desorption rate \(k_d\). Both influence the strength of the interaction as well as the dynamics of the interaction. By using the transformation \(k_{eq} = k_a / k_d\) to calculate the equilibrium constant and \(k_{kin} = 1 / k_d\) to calculate the kinetics constant, the values for the equilibrium and the kinetics of the reaction can be identified independently. First, the dependent variables \(k_a\) and \(k_d\) must be added as they are implemented in the underlying model.

optimization_problem.add_variable(
    name='adsorption_rate',
    parameter_path='flow_sheet.column.binding_model.adsorption_rate',
    lb=1e-3, ub=1e3,
    transform='auto',
    indices=[1]  # modify only the protein (component index 1) parameter
)

optimization_problem.add_variable(
    name='desorption_rate',
    parameter_path='flow_sheet.column.binding_model.desorption_rate',
    lb=1e-3, ub=1e3,
    transform='auto',
    indices=[1]
)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 1
----> 1 optimization_problem.add_variable(
      2     name='adsorption_rate',
      3     parameter_path='flow_sheet.column.binding_model.adsorption_rate',
      4     lb=1e-3, ub=1e3,
      5     transform='auto',
      6     indices=[1]  # modify only the protein (component index 1) parameter
      7 )
      9 optimization_problem.add_variable(
     10     name='desorption_rate',
     11     parameter_path='flow_sheet.column.binding_model.desorption_rate',
   (...)
     14     indices=[1]
     15 )

File ~/checkouts/readthedocs.org/user_builds/cadet-process/conda/latest/lib/python3.11/site-packages/CADETProcess/optimization/optimizationProblem.py:378, in OptimizationProblem.add_variable(self, name, evaluation_objects, parameter_path, lb, ub, transform, indices, pre_processing)
    376     parameter_path = name
    377 if parameter_path is not None and len(evaluation_objects) == 0:
--> 378     raise ValueError(
    379         "Cannot set parameter_path for variable without evaluation object "
    380     )
    382 var = OptimizationVariable(
    383     name, evaluation_objects, parameter_path,
    384     lb=lb, ub=ub, transform=transform,
    385     indices=indices,
    386     pre_processing=pre_processing,
    387 )
    389 self._variables.append(var)

ValueError: Cannot set parameter_path for variable without evaluation object 

Then, the independent variables \(k_{eq}\) and \(k_{kin}\) are added. To ensure, that CADET-Process does not try to write these variables into the CADET-Core model, where they do not have a place, evaluation_objects is set to None.

optimization_problem.add_variable(
    name='equilibrium_constant',
    evaluation_objects=None,
    lb=1e-4, ub=1e3,
    transform='auto',
    indices=[1]
)

optimization_problem.add_variable(
    name='kinetic_constant',
    evaluation_objects=None,
    lb=1e-4, ub=1e3,
    transform='auto',
    indices=[1]
)

Lasty, the dependency between the variables is added with the .add_variable_dependency() method.

optimization_problem.add_variable_dependency(
    dependent_variable="desorption_rate",
    independent_variables=["kinetic_constant", ],
    transform=lambda k_kin: 1 / k_kin
)

optimization_problem.add_variable_dependency(
    dependent_variable="adsorption_rate",
    independent_variables=["kinetic_constant", "equilibrium_constant"],
    transform=lambda k_kin, k_eq: k_eq / k_kin
)