3. Constraint¶

Let us recap the below example mentioned in “2. Black-Box Function”.

Numerical simulations

You want to design an optimal wing profile to maximize the lift and minimize the drag. In this case, your black-box function would be a fluid flow simulation that returns the negative lift-drag ratio of a given wing profile setting.

In this example, the objective is to find an input set that minimizes the negative lift-drag ratio, maximizing the lift while reducing the drag.

The objective function for this problem may be implemented as follows. Note that the below code defines the three decision variables using the “@blackbox” decorator as described in “2. Black-Box Function”.

from amplify_bbopt import RealVariable, blackbox

from utils.pseudo_simulators import (
    pseudo_wing_simulator as wing_simulator,
)


@blackbox
def objective_lift_drag(
    wing_width: float = RealVariable(bounds=(1, 20), nbins=100),
    wing_height: float = RealVariable(bounds=(1, 5), nbins=20),
    wing_angle: float = RealVariable(bounds=(0, 45), nbins=20),
) -> float:
    """
    This black-box function executes wing_simulator() and returns
    the negative lift-drag ratio for a given wing's width, height, and angle.
    """
    lift, drag = wing_simulator(wing_width, wing_height, wing_angle)
    return -lift / drag  # value to minimize

Often, you may need to impose some constraints on the set variables during black-box optimization. In the black-box optimization problem considering the black-box function above, for example, the decision variables may need to meet the following constraints:

  • wing_width + wing_height must be less than or equal to 10.

  • wing_width - wing_height must be greater than or equal to 4.

There are two ways to implement such constraints: “soft” and “hard” constraints.

Soft constraint¶

The first method is to use a soft constraint. In the above black-box function, instead of returning -lift / drag directly, you can add a penalty to the objective function when the given decision variables do not meet the constraints.

This method is called “soft” since the solutions violating the constraints are still regarded as feasible from an optimizer perspective — the constraints are a part of the objective function and not explicitly defined as constraints with Amplify-BBOpt. Note that in this method, the intensity of the penalty needs to be set appropriately.

Below is an example of the same black-box function with such soft constraints and unity penalty.

@blackbox
def objective_lift_drag_soft_constraint(
    wing_width: float = RealVariable(bounds=(1, 20), nbins=100),
    wing_height: float = RealVariable(bounds=(1, 5), nbins=20),
    wing_angle: float = RealVariable(bounds=(0, 45), nbins=20),
) -> float:
    lift, drag = wing_simulator(wing_width, wing_height, wing_angle)
    value = -lift / drag

    # Add a penalty (=1) when the given variables violate the constraint.
    if wing_width + wing_height > 10:
        value += 1

    # Add a penalty (=1) when the given variables violate the constraint.
    if wing_width - wing_height < 4:
        value += 1

    return value

Hard constraint¶

The second method is called hard constraint. In this method, you do not modify your black-box function. Instead, you additionally specify and add the constraints to your black-box function class instance using the following Amplify-BBOpt functions.

Method

Constraint

equal_to(left, right)

left == right

less_equal(left, right)

left <= right

greater_equal(left, right)

left >= right

clamp(left, right)

right[0] <= left <= right[1]

Here, left is the left-hand side of the equality and inequality constraints and consists of a polynomial based on the decision variables. right is the right-hand side of the constraints and is always a value (a tuple for clamp to specify lower and upper bounds of an inequality equation).

In the above example of the wing simulation, the constraints can be implemented as follows:

from amplify_bbopt import greater_equal, less_equal

variables = objective_lift_drag.variables

constraint_sum = less_equal(variables.wing_width + variables.wing_height, 10)

constraint_sub = greater_equal(variables.wing_width - variables.wing_height, 4)

The defined constraints are the instances of Constraint. You can check the created constraints as follows:

print(constraint_sum)
print(constraint_sub)
constraint: wing_width + wing_height <= 10
constraint: wing_width - wing_height >= 4

You can add these constraints to the black-box function class instance using the add_constraint method. You can add more than one constraint by using add_constraint repeatedly. You can also view the added constraints associated with the black-box function class instance.

# Add constraints to the black-box function class instance
objective_lift_drag.add_constraint(constraint_sum)
objective_lift_drag.add_constraint(constraint_sub)

# Display all constraints associated with the black-box function class instance
print(objective_lift_drag.constraints)
--------------------
constraint: wing_width + wing_height <= 10 (weight: 1.0)
constraint: wing_width - wing_height >= 4 (weight: 1.0)

Now, Amplify-BBOpt knows that these variables must hold the added constraints for your black-box function, and any input set violating the constraint is regarded as infeasible. Hence, we call this type of constraint a “hard” constraint.

Attention

Currently, you can only specify a first-order polynomial of the decision variables as left of the hard constraint. For example, wing_width + wing_height is a first-order polynomial and can be a left in the above constraint functions (you can also multiply a value). However, wing_width * wing_height is second order, and Amplify-BBOpt cannot handle such a high order polynomial as left for now. If you need to consider a second or higher order polynomial, use the “soft” constraint approach or manually construct your constraint instead.

Caution

Using an equal_to constraint for real variables requires some care. Most likely, Amplify-BBOpt discretizes between the bounds a real variable can take into a number of small bins. This means the value you expect the real variable would yield may not be included in the discretized values of the variable.

real = RealVariable(bounds=[0, 1], nbins=3)
print(real)
c = equal_to(2 * real, 0.2) # this would never satisfied.

To circumvent this situation, you can:

  1. Adjust your variable bounds or nbins appropriately, or

  2. Use the soft constraint approach with an allowable error.

Caution

Using inequality constraints (less_equal, greater_equal and clamp) for real variables requires some care. Most likely, Amplify-BBOpt passes such inequality constraints to Amplify SDK as is, which, as of today, cannot take inequality constraints with real variables. If this happens, Amplify SDK spits the following error message:

ValueError: Model conversion failed: conversion from real to other types of variables is not supported.

Should this happen, you can set the penalty_formulation property of the corresponding constraint as “Relaxation”:

real = RealVariable(bounds=[0, 2], nbins=5)
print(real)
c = less_equal(real, 1)
c.penalty_formulation = "Relaxation"