2. Black-Box Function¶
In black-box optimization, you aim to find a set of input values to an objective function to minimize the output value from the function. Here, the objective function is often a black-box function.
A black-box function is a function whose shape or gradient is unknown. It can be complex numerical simulations and measurements with/without postprocessing. Therefore, typical optimization methods of gradient descent and mathematical optimization cannot be used since you have no knowledge about the gradient or formulation of the function.
Here are some examples of preparing a black-box function for optimization.
Note
If your problem is a maximization problem, multiply your black-box function return value by -1 to convert the maximization problem to a minimization problem.
Examples of (black-box) objective 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. Your black-box function would look something like this:
from utils.pseudo_simulators import (
pseudo_wing_simulator as wing_simulator,
)
def objective_lift_drag(
wing_width: float,
wing_height: float,
wing_angle: float,
) -> 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
Experimental measurements¶
You want to design a “meta”-material to minimize the permittivity by synthesizing several from 10 raw materials. In this case, your black-box function would be an experiment that measures the permittivity of the meta-material created by synthesizing the chosen raw materials.
def objective_permittivity(choice: list[bool]) -> float:
"""This black-box function takes a choice of raw materials.
The variable "choice" is a list of bool to express the selection of
i-th raw material. The user is expected to perform material synthesis
based on the given choice and enter the measured permittivity, which is
then returned from this function to the optimizer.
"""
message = (
f"Synthesize based on {choice=} and enter the measured permittivity"
)
permittivity = input(message)
return float(permittivity) # value to minimize
Inverse problems¶
You can also use black-box optimization for inverse problems of complex phenomena.
For example, there is an output of a complex simulation, but you do not know its input parameters to reproduce the same output (target output). In this case, your black-box function would be a function that returns the difference (distance) between the target output and the simulation output based on the new “potentially-optimal” input.
import math
from utils.pseudo_simulators import (
pseudo_complex_calculation as complex_calculation,
)
target_output = (5.0, 6.0, 6.0)
def objective_distance(x0: bool, x1: int, x2: float) -> float:
"""This black-box function takes a set of input (x0, x1, x2), executes
complex_calculation(), and returns the distance between the obtained output
and the target output.
"""
output = complex_calculation(x0, x1, x2)
return math.dist(target_output, output) # value to minimize
Note
A more general and friendlier example of an inverse problem is Coca-Cola’s secret formula. In this case, your decision variables may be amounts of \(N_I\) number of ingredients in the syrup and \(N_C\) number of processing conditions. Your objective function would be an experiment where 100 testers taste the syrup based on the new “potentially optimal” formula and rate how close the taste is to the actual Coca-Cola (0 is the closest, 10 is the farthest). The return value could be the average of 100 testers’ ratings. The optimizer tries to find a formula for which the average rating is almost the smallest.
Associating variables with black-box function¶
Since the input to your black-box function and decision variables must have a one-to-one relation, it would be convenient to associate the variables and function closely. In Amplify-BBOpt, the blackbox
decorator helps you associate the decision variables with your black-box function in different ways. Using the black-box function example mentioned in the numerical simulation above, here are some examples.
Method 1¶
Use the @blackbox
decorator and define decision variables as input arguments’ default values.
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
Method 2¶
Use the @blackbox
decorator and define decision variables with Annotated
.
from typing import Annotated
from amplify_bbopt import RealVariable, blackbox
from utils.pseudo_simulators import (
pseudo_wing_simulator as wing_simulator,
)
@blackbox
def objective_lift_drag(
wing_width: Annotated[float, RealVariable(bounds=(1, 20), nbins=100)],
wing_height: Annotated[float, RealVariable(bounds=(1, 5), nbins=20)],
wing_angle: Annotated[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
Attributes of the BlackBoxFuncBase
class¶
The black-box function constructed in the above methods 1 and 2 using @blackbox
decorator is also an instance of “black-box function class” inheriting BlackBoxFuncBase
. The class has the following attributes.
Attribute |
Data type |
Details |
---|---|---|
variables |
A class handles all the set variables with different types |
|
constraints |
A class handles all the set constraints |
|
name |
|
The name of an black-box objective function |
We will describe constraints next, so do not worry if you are unsure now.
Via the variables
attribute you can access individual variables you set along with the black-box function. For example, for the above class instance objective_lift_drag
(for any of the above methods), you can access the variables wing_width
, wing_height
and wing_angle
as follows (for variable’s attributes see “1. Decision Variable”):
wing_width = objective_lift_drag.variables.wing_width
print(f"{wing_width.type=}")
print(f"{wing_width.bounds=}")
print(f"{wing_width.method=}")
wing_width.type=<class 'float'>
wing_width.bounds=(1, 20)
wing_width.method='domain_wall'
The variables
attribute is also useful for obtaining information about the variables at once. For example, for the above class instance objective_lift_drag
(for any of the above methods), the following line outputs information about all the variables associated with your black-box function.
print(objective_lift_drag.variables)
type nbins len method nvars i=0 i=1 i=2 i=3 i=4 i=5 i=6 i=7 i=8 i=9 i=10 i=11 i=12 i=13 i=14 i=15 i=16 i=17 i=18 i=19 i=20 i=21 i=22 i=23 i=24 i=25 i=26 i=27 i=28 i=29 i=30 i=31 i=32 i=33 i=34 i=35 i=36 i=37 i=38 i=39 i=40 i=41 i=42 i=43 i=44 i=45 i=46 i=47 i=48 \
wing_width float 100 1 domain_wall 99 1.0 1.1919191919191918 1.3838383838383839 1.5757575757575757 1.7676767676767677 1.9595959595959596 2.1515151515151514 2.3434343434343434 2.5353535353535355 2.727272727272727 2.919191919191919 3.111111111111111 3.3030303030303028 3.494949494949495 3.686868686868687 3.8787878787878785 4.070707070707071 4.262626262626263 4.454545454545454 4.646464646464646 4.838383838383838 5.03030303030303 5.222222222222222 5.4141414141414135 5.6060606060606055 5.797979797979798 5.98989898989899 6.181818181818182 6.373737373737374 6.565656565656565 6.757575757575757 6.949494949494949 7.141414141414141 7.333333333333333 7.525252525252525 7.717171717171717 7.909090909090908 8.1010101010101 8.292929292929292 8.484848484848484 8.676767676767676 8.868686868686869 9.06060606060606 9.252525252525253 9.444444444444445 9.636363636363635 9.828282828282827 10.020202020202019 10.212121212121211
wing_height float 20 1 domain_wall 19 1.0 1.2105263157894737 1.4210526315789473 1.631578947368421 1.8421052631578947 2.052631578947368 2.263157894736842 2.473684210526316 2.6842105263157894 2.894736842105263 3.1052631578947367 3.3157894736842106 3.526315789473684 3.7368421052631575 3.9473684210526314 4.157894736842105 4.368421052631579 4.578947368421052 4.789473684210526 5.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
wing_angle float 20 1 domain_wall 19 0.0 2.3684210526315788 4.7368421052631575 7.105263157894736 9.473684210526315 11.842105263157894 14.210526315789473 16.57894736842105 18.94736842105263 21.31578947368421 23.684210526315788 26.052631578947366 28.421052631578945 30.789473684210524 33.1578947368421 35.526315789473685 37.89473684210526 40.263157894736835 42.63157894736842 45.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
i=49 i=50 i=51 i=52 i=53 i=54 i=55 i=56 i=57 i=58 i=59 i=60 i=61 i=62 i=63 i=64 i=65 i=66 i=67 i=68 i=69 i=70 i=71 i=72 i=73 i=74 i=75 i=76 i=77 i=78 i=79 i=80 i=81 i=82 i=83 i=84 i=85 i=86 i=87 i=88 i=89 i=90 i=91 i=92 i=93 i=94 i=95 i=96 i=97 \
wing_width 10.404040404040403 10.595959595959595 10.787878787878787 10.97979797979798 11.171717171717171 11.363636363636363 11.555555555555555 11.747474747474747 11.93939393939394 12.13131313131313 12.323232323232322 12.515151515151514 12.707070707070706 12.898989898989898 13.09090909090909 13.282828282828282 13.474747474747474 13.666666666666666 13.858585858585858 14.05050505050505 14.242424242424242 14.434343434343434 14.626262626262625 14.818181818181817 15.010101010101009 15.2020202020202 15.393939393939393 15.585858585858585 15.777777777777777 15.969696969696969 16.16161616161616 16.353535353535353 16.545454545454547 16.737373737373737 16.929292929292927 17.12121212121212 17.31313131313131 17.505050505050505 17.696969696969695 17.88888888888889 18.08080808080808 18.27272727272727 18.464646464646464 18.656565656565654 18.848484848484848 19.040404040404038 19.232323232323232 19.424242424242422 19.616161616161616
wing_height - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
wing_angle - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
i=98 i=99
wing_width 19.808080808080806 20.0
wing_height - -
wing_angle - -
The output shows the following information for each of the defined variables. You can see that each non-binary variable ranges and is discretized as described above.
value type (
type
),number of discretization points (
nbins
)length (
len
)encoding method (
method
)[1]number of the Amplify-SDK’s variables (
nvars
)[1]discretization (value at each discretization point \(i\) )
A similar human-readable string representation is prepared for objective_lift_drag.constraints
. We will describe this next.