Custom Objective¶
In some black-box optimization problems with multiple objectives, not all objective functions are black-box. For such problems, you can either:
Over-black-boxing:
Treat all of the objective functions (both black-box and non-black-box) involved as black-box, orCustom objective:
Formulate non-black-box functions to a polynomial solvable as an optimization problem (e.g. QUBO).
Here, let us consider the wing optimization problem introduced in Numerical simulations in 2. Black-Box Function.
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.
For optimization, a typical code snippet to prepare an optimizer would look somthing like this:
from datetime import timedelta
from amplify import FixstarsClient
from amplify_bbopt import (
DatasetGenerator,
KernelQAOptimizer,
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=5),
wing_height: float = RealVariable(bounds=(1, 5), nbins=5),
wing_angle: float = RealVariable(bounds=(0, 45), nbins=5),
) -> 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
data = DatasetGenerator(objective_lift_drag).generate(num_samples=5)
# Set up solver client
client = FixstarsClient()
client.parameters.timeout = timedelta(milliseconds=2000) # 2 seconds
# client.token = "xxxxxxxxxxx" # Enter your Amplify API token.
optimizer = KernelQAOptimizer(data, objective_lift_drag, client)
amplify-bbopt | 2024/10/04 05:23:05 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:05 | INFO | #0/5 initial data for objective_lift_drag
amplify-bbopt | 2024/10/04 05:23:05 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:05 | INFO | #1/5 initial data for objective_lift_drag
amplify-bbopt | 2024/10/04 05:23:05 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:05 | INFO | #2/5 initial data for objective_lift_drag
amplify-bbopt | 2024/10/04 05:23:05 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:05 | INFO | #3/5 initial data for objective_lift_drag
amplify-bbopt | 2024/10/04 05:23:05 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:05 | INFO | #4/5 initial data for objective_lift_drag
In this example, suppose that you want to minimize the cross-sectional area of the wing as well (hence this is multiple-objective optimization). It is reasonable that the cross-sectional area of the wing is approximately proportional to wing_width * wing_height
. Thus, you have an additional objective function to minimize, which is not black-box (as you know the formulation: wing_width * wing_height
).
Over-black-boxing¶
One way to deal with such an additional non-black-box opbjective function is to add wing_width * wing_height
to the return value of the objective_list_drag()
function as:
return -lift / drag + wing_width * wing_height # value to minimize
This is a typical example of “over-black-boxing” method mentioned above: You treat the additional objective function as black-box although it is actually now. As long as you use over-black-boxing, you can solve this kind of optimization problems (with black-box and non-black-box objective functions) as usual multiple-objective optimization problems.
Custom objective¶
Another way, which may be more desirable, is to formulate the non-black-box part of the objective as a custom objective.
Here, you are expected to use the Amplify SDK’s variables that represent Amplify-BBOpt’s variables involved in the black-box function objective_lift_drag()
directly to construct the custom objective. This is because (as already described), in annealing-based optimizers (e.g. FMQAOptimizer
, KernelQAOptimizer
), a surrogate model for a black-box objective function is converted to a QUBO model using the Amplify SDK (such a QUBO model has a type of amplify.Model
). Amplify-BBOpt then passes the QUBO model to the Amplify SDK, which requests the external optimization solver to solve QUBO model.
The idea of using a custom objective is to add additional objective function, which can be formulated explicitly, to the converted QUBO model, before it is passed to the Amplify SDK.
To perform such formulation, you first need to retrieve the Amplify SDK variables that represent Amplify-BBOpt variables. You can retrieve such Amplify SDK variables in various ways:
Retrieving the Amplify SDK’s expression of a Amplify-BBOpt’s variable¶
You can retrieve an Amplify SDK’s polynomial that expresses a Amplify-BBOpt’s variable in the black-box function. In the above wing optimization example, the Amplify SDK’s polynomials for the real variables wing_width
, wing_height
and wing_angle
can be retrieved as follows. The obtained polynomial is the representation of Amplify-BBOpt’s various variables using the Amplify SDK’s variables. You can see each polynomial represents a real value with corresponding bounds.
wing_width_poly = optimizer.objective.variables.wing_width.to_amplify_poly()
print(wing_width_poly)
wing_height_poly = optimizer.objective.variables.wing_height.to_amplify_poly()
print(wing_height_poly)
wing_angle_poly = optimizer.objective.variables.wing_angle.to_amplify_poly()
print(wing_angle_poly)
4.75 q0_0 + 4.75 q0_1 + 4.75 q0_2 + 4.75 q0_3 + 1
q1_0 + q1_1 + q1_2 + q1_3 + 1
11.25 q2_0 + 11.25 q2_1 + 11.25 q2_2 + 11.25 q2_3
Retrieving individual variables of the Amplify SDK¶
You can also retrieve individual variables of the Amplify SDK that constitute Amplify-BBOpt’s variables in the following two ways:
amplify_variable_1 = optimizer.objective.variables.amplify_variables
print(amplify_variable_1)
amplify_variable_2 = optimizer.objective.variables.poly_array
print(amplify_variable_2)
{'wing_width': PolyArray([q0_0, q0_1, q0_2, q0_3]), 'wing_height': PolyArray([q1_0, q1_1, q1_2, q1_3]), 'wing_angle': PolyArray([q2_0, q2_1, q2_2, q2_3])}
[q0_0, q0_1, q0_2, q0_3, q1_0, q1_1, q1_2, q1_3, q2_0, q2_1, q2_2, q2_3]
As you can see, both amplify_variable_1
and amplify_variable_2
obtain the same Amplify SDK’s variables. With amplify_variable_1
you can obtain the Amplify SDK’s variables in a dictionary by Amplify-BBOpt’s variable. The second result amplify_variable_2
is simply a list of all amplify SDK’s variables used in the optimizer.
Using these Amplify SDK’s variables, you can directly formulate the additional objective function, and such a formulated objective (custom objective) can be set in the optimizer using optimizer.custom_amplify_objective
as follows.
optimizer.custom_amplify_objective = wing_width_poly * wing_height_poly
print(f"{optimizer.custom_amplify_objective=}")
optimizer.custom_amplify_objective=Poly(4.75 q0_0 q1_0 + 4.75 q0_0 q1_1 + 4.75 q0_0 q1_2 + 4.75 q0_0 q1_3 + 4.75 q0_1 q1_0 + 4.75 q0_1 q1_1 + 4.75 q0_1 q1_2 + 4.75 q0_1 q1_3 + 4.75 q0_2 q1_0 + 4.75 q0_2 q1_1 + 4.75 q0_2 q1_2 + 4.75 q0_2 q1_3 + 4.75 q0_3 q1_0 + 4.75 q0_3 q1_1 + 4.75 q0_3 q1_2 + 4.75 q0_3 q1_3 + 4.75 q0_0 + 4.75 q0_1 + 4.75 q0_2 + 4.75 q0_3 + q1_0 + q1_1 + q1_2 + q1_3 + 1)
Finally you can start optimization cycle as usual.
optimizer.optimize(num_cycles=5)
amplify-bbopt | 2024/10/04 05:23:05 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:05 | INFO | #1/5 optimization cycle, constraint wt: 7.92e+00
amplify-bbopt | 2024/10/04 05:23:05 | INFO | model corrcoef: 0.986, beta: 0.0
amplify-bbopt | 2024/10/04 05:23:08 | INFO | num_iterations: 31
amplify-bbopt | 2024/10/04 05:23:08 | INFO | y_hat=-9.645e-01, y_custom=1.000e+00, best objective=3.549e-02
amplify-bbopt | 2024/10/04 05:23:08 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:08 | INFO | #2/5 optimization cycle, constraint wt: 7.92e+00
amplify-bbopt | 2024/10/04 05:23:08 | INFO | model corrcoef: 0.979, beta: 0.0
amplify-bbopt | 2024/10/04 05:23:12 | INFO | num_iterations: 31
amplify-bbopt | 2024/10/04 05:23:12 | INFO | modifying solution (6, is_frequent=False), {'wing_width': np.float64(1.0), 'wing_height': np.float64(1.0), 'wing_angle': np.float64(22.5)} --> {'wing_width': np.float64(15.25), 'wing_height': np.float64(4.0), 'wing_angle': np.float64(22.5)}.
amplify-bbopt | 2024/10/04 05:23:12 | INFO | y_hat=-3.188e+00, y_custom=6.100e+01, best objective=3.549e-02
amplify-bbopt | 2024/10/04 05:23:12 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:12 | INFO | #3/5 optimization cycle, constraint wt: 7.92e+00
amplify-bbopt | 2024/10/04 05:23:12 | INFO | model corrcoef: 0.981, beta: 0.0
amplify-bbopt | 2024/10/04 05:23:16 | INFO | num_iterations: 31
amplify-bbopt | 2024/10/04 05:23:16 | INFO | modifying solution (1, is_frequent=False), {'wing_width': np.float64(1.0), 'wing_height': np.float64(1.0), 'wing_angle': np.float64(22.5)} --> {'wing_width': np.float64(10.5), 'wing_height': np.float64(5.0), 'wing_angle': np.float64(11.25)}.
amplify-bbopt | 2024/10/04 05:23:16 | INFO | y_hat=-1.953e+00, y_custom=5.250e+01, best objective=3.549e-02
amplify-bbopt | 2024/10/04 05:23:16 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:16 | INFO | #4/5 optimization cycle, constraint wt: 7.92e+00
amplify-bbopt | 2024/10/04 05:23:16 | INFO | model corrcoef: 0.977, beta: 0.0
amplify-bbopt | 2024/10/04 05:23:20 | INFO | num_iterations: 31
amplify-bbopt | 2024/10/04 05:23:20 | INFO | modifying solution (1, is_frequent=False), {'wing_width': np.float64(1.0), 'wing_height': np.float64(1.0), 'wing_angle': np.float64(22.5)} --> {'wing_width': np.float64(20.0), 'wing_height': np.float64(4.0), 'wing_angle': np.float64(0.0)}.
amplify-bbopt | 2024/10/04 05:23:20 | INFO | y_hat=-4.000e+00, y_custom=8.000e+01, best objective=3.549e-02
amplify-bbopt | 2024/10/04 05:23:20 | INFO | ----------------------------------------
amplify-bbopt | 2024/10/04 05:23:20 | INFO | #5/5 optimization cycle, constraint wt: 8.00e+00
amplify-bbopt | 2024/10/04 05:23:20 | INFO | model corrcoef: 0.981, beta: 0.0
amplify-bbopt | 2024/10/04 05:23:23 | INFO | num_iterations: 30
amplify-bbopt | 2024/10/04 05:23:23 | INFO | modifying solution (1, is_frequent=False), {'wing_width': np.float64(1.0), 'wing_height': np.float64(1.0), 'wing_angle': np.float64(22.5)} --> {'wing_width': np.float64(5.75), 'wing_height': np.float64(5.0), 'wing_angle': np.float64(22.5)}.
amplify-bbopt | 2024/10/04 05:23:23 | INFO | y_hat=-1.100e+00, y_custom=2.875e+01, best objective=3.549e-02
optimizer.best_objective
np.float64(0.03549492547106681)
optimizer.best_solution
{'wing_width': np.float64(1.0),
'wing_height': np.float64(1.0),
'wing_angle': np.float64(22.5)}
As described in “6. Visualization”, you can fetch the optimization history with an optimizer instance’s method fetch_history
. This contains all variables and corresponding output values of black-box and custom objective functions as well as their total values for each sample obtained at every optimization cycles.
print(optimizer.fetch_history().history_df)
wing_width wing_height wing_angle objective (blackbox) objective (custom) objective (total) elapsed time (s) de duplication
Sample #0 20.00 4.0 22.50 -3.958577 80.00 76.041423 0.000000 True
Sample #1 5.75 2.0 0.00 -2.513661 11.50 8.986339 0.000000 True
Sample #2 1.00 1.0 0.00 -0.952381 1.00 0.047619 0.000000 True
Sample #3 20.00 4.0 45.00 -3.574700 80.00 76.425300 0.000000 True
Sample #4 10.50 4.0 45.00 -2.173969 42.00 39.826031 0.000000 True
Sample #5 1.00 1.0 22.50 -0.964505 1.00 0.035495 3.702525 False
Sample #6 15.25 4.0 22.50 -3.187966 61.00 57.812034 7.800413 True
Sample #7 10.50 5.0 11.25 -1.953480 52.50 50.546520 11.458776 True
Sample #8 20.00 4.0 0.00 -4.000000 80.00 76.000000 14.939886 True
Sample #9 5.75 5.0 22.50 -1.100177 28.75 27.649823 18.210232 True