Skip to content

Commit

Permalink
Allow specifying nonlinear constraints with comparison operator
Browse files Browse the repository at this point in the history
  • Loading branch information
schmoelder committed Mar 20, 2024
1 parent eee63cd commit 9a67b46
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 68 deletions.
9 changes: 5 additions & 4 deletions CADETProcess/fractionation/fractionationOptimizer.py
Expand Up @@ -225,11 +225,12 @@ def _setup_optimization_problem(

purity = Purity()
purity.n_metrics = frac.component_system.n_comp
constraint_bounds = -np.array(purity_required, ndmin=1)
constraint_bounds = constraint_bounds.tolist()
opt.add_nonlinear_constraint(
purity, n_nonlinear_constraints=len(constraint_bounds),
bounds=constraint_bounds, requires=frac_evaluator
purity,
n_nonlinear_constraints=len(purity_required),
bounds=purity_required,
comparison_operator='ge',
requires=frac_evaluator,
)

for evt in frac.events:
Expand Down
31 changes: 17 additions & 14 deletions CADETProcess/optimization/axAdapater.py
Expand Up @@ -116,24 +116,28 @@ def run(self, trial: BaseTrial) -> Dict[str, Any]:
# Calculate nonlinear constraints
# Explore if adding a small amount of noise to the result helps BO
if self.optimization_problem.n_nonlinear_constraints > 0:
nonlincon_fun = self.optimization_problem.evaluate_nonlinear_constraints_population
nonlincon_cv_fun = self.optimization_problem.evaluate_nonlinear_constraints_violation_population
nonlincon_labels = self.optimization_problem.nonlinear_constraint_labels

G = nonlincon_fun(X, untransform=True, parallelization_backend=self.parallelization_backend)
CV = nonlincon_cv_fun(
X,
untransform=True,
parallelization_backend=self.parallelization_backend
)

else:
G = None
CV = None
nonlincon_labels = None

# Update trial information with results.
trial_metadata = self.get_metadata(
trial, F, objective_labels, G, nonlincon_labels
trial, F, objective_labels, CV, nonlincon_labels
)

return trial_metadata

@staticmethod
def get_metadata(trial, F, objective_labels, G, nonlincon_labels):
def get_metadata(trial, F, objective_labels, CV, nonlincon_labels):
trial_metadata = {"name": str(trial.index)}
trial_metadata.update({"arms": {}})

Expand All @@ -142,13 +146,13 @@ def get_metadata(trial, F, objective_labels, G, nonlincon_labels):
metric: f_metric[i]
for metric, f_metric in zip(objective_labels, F.T)
}
g_dict = {}
if G is not None:
g_dict = {
metric: g_metric[i]
for metric, g_metric in zip(nonlincon_labels, G.T)
cv_dict = {}
if CV is not None:
cv_dict = {
metric: cv_metric[i]
for metric, cv_metric in zip(nonlincon_labels, CV.T)
}
trial_metadata["arms"].update({arm: {**f_dict, **g_dict}})
trial_metadata["arms"].update({arm: {**f_dict, **cv_dict}})

return trial_metadata

Expand Down Expand Up @@ -237,16 +241,15 @@ def _setup_objectives(self):
def _setup_outcome_constraints(self):
"""Parse nonliear constraint functions from optimization problem."""
nonlincon_names = self.optimization_problem.nonlinear_constraint_labels
bounds = self.optimization_problem.nonlinear_constraints_bounds

outcome_constraints = []
for name, bound in zip(nonlincon_names, bounds):
for name in nonlincon_names:
ax_metric = CADETProcessMetric(name=name)

nonlincon = OutcomeConstraint(
metric=ax_metric,
op=ComparisonOp.LEQ,
bound=bound,
bound=0.0,
relative=False,
)
outcome_constraints.append(nonlincon)
Expand Down
30 changes: 27 additions & 3 deletions CADETProcess/optimization/optimizationProblem.py
Expand Up @@ -1179,6 +1179,7 @@ def add_nonlinear_constraint(
bad_metrics=None,
evaluation_objects=-1,
bounds=0,
comparison_operator='le',
labels=None,
requires=None,
*args, **kwargs):
Expand All @@ -1203,6 +1204,10 @@ def add_nonlinear_constraint(
Upper limits of constraint function.
If only one value is given, the same value is assumed for all
constraints. The default is 0.
comparison_operator : {'ge', 'le'}, optional
Comparator to define whether metric should be greater or equal to, or less
than or equal to the specified bounds.
The default is 'le' (lower or equal).
labels : str, optional
Names of the individual metrics.
requires : {None, Evaluator, list}, optional
Expand Down Expand Up @@ -1274,8 +1279,9 @@ def add_nonlinear_constraint(
nonlincon = NonlinearConstraint(
nonlincon,
name,
bounds=bounds,
n_nonlinear_constraints=n_nonlinear_constraints,
bounds=bounds,
comparison_operator=comparison_operator,
bad_metrics=bad_metrics,
evaluation_objects=evaluation_objects,
evaluators=evaluators,
Expand Down Expand Up @@ -1381,8 +1387,17 @@ def evaluate_nonlinear_constraints_violation(self, x, force=False):
"""
self.logger.debug(f'Evaluate nonlinear constraints violation at {x}.')

factors = []
for constr in self.nonlinear_constraints:
factor = -1 if constr.comparison_operator == 'ge' else 1
factors += constr.n_total_metrics * [factor]

g = self._evaluate_individual(self.nonlinear_constraints, x, force=False)
cv = np.array(g) - np.array(self.nonlinear_constraints_bounds)
g_transformed = np.multiply(factors, g)

bounds_transformed = np.multiply(factors, self.nonlinear_constraints_bounds)

cv = g_transformed - bounds_transformed

return cv

Expand Down Expand Up @@ -3723,10 +3738,19 @@ class NonlinearConstraint(Metric):

minimize = Bool(default=True)
nonlinear_constraint = Metric.func
comparison_operator = Switch(valid=['le', 'ge'], default='le')
n_nonlinear_constraints = Metric.n_metrics

def __init__(self, *args, n_nonlinear_constraints=1, bounds=0, **kwargs):
def __init__(
self,
*args,
n_nonlinear_constraints=1,
bounds=0,
comparison_operator='le',
**kwargs
):
self.bounds = bounds
self.comparison_operator = comparison_operator

super().__init__(*args, n_metrics=n_nonlinear_constraints, **kwargs)

Expand Down
61 changes: 15 additions & 46 deletions CADETProcess/optimization/scipyAdapter.py
Expand Up @@ -164,17 +164,13 @@ def get_bounds(self, optimization_problem):
)

def get_constraint_objects(self, optimization_problem):
"""Defines the constraints of the optimization_problem and resturns
them into a list.
First defines the lincon, the linequon and the nonlincon constraints.
Returns the constrainst in a list.
"""Return constraints as objets.
Returns
-------
constraint_objects : list
List containing a sorted list of all constraints of an
optimization_problem, if they're not None.
List containing lists of all constraint types of the optimization_problem.
If type of constraints is not defined, it is replaced with None.
See Also
--------
Expand All @@ -191,16 +187,13 @@ def get_constraint_objects(self, optimization_problem):
return [con for con in constraints if con is not None]

def get_lincon_obj(self, optimization_problem):
"""Returns the optimized linear constraint as an object.
Sets the lower and upper bounds of the optimization_problem and returns
optimized linear constraints. Keep_feasible is set to True.
"""Return the linear constraints as an object.
Returns
-------
lincon_obj : LinearConstraint
Linear Constraint object with optimized upper and lower bounds of b
of the optimization_problem.
Linear Constraint object with lower and upper bounds of b of the
optimization_problem.
See Also
--------
Expand All @@ -219,20 +212,13 @@ def get_lincon_obj(self, optimization_problem):
)

def get_lineqcon_obj(self, optimization_problem):
"""Returns the optimized linear equality constraints as an object.
Checks the length of the beq first, before setting the bounds of the
constraint. Sets the lower and upper bounds of the
optimization_problem and returns optimized linear equality constraints.
Keep_feasible is set to True.
"""Return the linear equality constraints as an object.
Returns
-------
None: bool
If the length of the beq of the optimization_problem is equal zero.
lineqcon_obj : LinearConstraint
Linear equality Constraint object with optimized upper and lower
bounds of beq of the optimization_problem.
Linear equality Constraint object with lower and upper bounds of beq of the
optimization_problem.
See Also
--------
Expand All @@ -252,29 +238,12 @@ def get_lineqcon_obj(self, optimization_problem):
)

def get_nonlincon_obj(self, optimization_problem):
"""Returns the optimized nonlinear constraints as an object.
Checks the length of the nonlinear_constraints first, before setting
the bounds of the constraint. Tries to set the bounds from the list
nonlinear_constraints from the optimization_problem for the lower
bounds and sets the upper bounds for the length of the
nonlinear_constraints list. If a TypeError is excepted it sets the
lower bound by the first entry of the nonlinear_constraints list and
the upper bound to infinity. Then a local variable named
finite_diff_rel_step is defined. After setting the bounds it returns
the optimized nonlinear constraints as an object with the
finite_diff_rel_step and the jacobian matrix. The jacobian matrix is
got by calling the method nonlinear_constraint_jacobian from the
optimization_problem. Keep_feasible is set to True.
"""Return the optimized nonlinear constraints as an object.
Returns
-------
None: bool
If the length of the nonlinear_constraints of the
optimization_problem is equal zero.
nonlincon_obj : NonlinearConstraint
Linear equality Constraint object with optimized upper and lower
bounds of beq of the optimization_problem.
nonlincon_obj : list
Nonlinear constraint violation objects with bounds the optimization_problem.
See Also
--------
Expand Down Expand Up @@ -305,11 +274,11 @@ def makeConstraint(i):
in the main loop.
"""
constr = optimize.NonlinearConstraint(
lambda x: opt.evaluate_nonlinear_constraints(x, untransform=True)[i],
lb=-np.inf, ub=opt.nonlinear_constraints_bounds[i],
lambda x: opt.evaluate_nonlinear_constraints_violation(x, untransform=True)[i],
lb=-np.inf, ub=0,
finite_diff_rel_step=self.finite_diff_rel_step,
keep_feasible=True
)
)
return constr

constraints = []
Expand Down
2 changes: 1 addition & 1 deletion CADETProcess/performance.py
Expand Up @@ -361,7 +361,7 @@ class Purity(PerformanceIndicator):
"""

def _evaluate(self, performance):
return - performance.purity
return performance.purity


class Concentration(PerformanceIndicator):
Expand Down

0 comments on commit 9a67b46

Please sign in to comment.