Defining the Objective Function¶

Example of a black-box objective function¶

In black-box optimization, the objective function is typically black-box with a complex input–output mapping, such as numerical simulations or experimental measurements (and their post-processing). Amplify-BBOpt searches for input values that minimize the output of such a black-box objective function.

Below is an example of a typical black-box function.

Simulation-based objective¶

In the following example, we consider an optimization problem for wing design that aims to maximize the lift-to-drag ratio (\(L/D\)), defined as the ratio of lift (\(L\)) to drag (\(D\)). Since Amplify-BBOpt treats optimization as a minimization problem, this black-box function returns the negative value of the lift-to-drag ratio, and the search is performed to minimize this negative value.

import WingSimulator  # Example simulator

def my_blackbox_func_simulation(
    wing_width: float,
    wing_height: float,
    wing_angle: float,
) -> float:
    """Returns the negative value of the lift-to-drag ratio obtained from the simulation."""
    simulator = WingSimulator(wing_width, wing_height, wing_angle)
    lift_force, drag_force = simulator.simulate()

    return -lift_force / drag_force

Experiment-based objective¶

If the wing design optimization is conducted based on experimental measurements instead of simulations, the black-box objective function can be implemented as follows. In this case, Amplify-BBOpt proposes new experimental conditions. You then perform the experiment according to those conditions, input the measured (and post-processed) results into Amplify-BBOpt, and the next round of optimization proceeds based on that data.

def my_blackbox_func_experiment(
    wing_width: float,
    wing_height: float,
    wing_angle: float,
) -> float:
    """Prompts for experimental conditions, accepts measured results, and returns the negative lift-to-drag ratio."""
    print(f"Please conduct the experiment under the following conditions: {wing_width=}, {wing_height=}, {wing_angle=}")
    lift_force = input("Enter the measured lift force: ")
    drag_force = input("Enter the measured drag force: ")
    return -lift_force / drag_force

Inverse-problem-based objective¶

A problem that involves determining the input values for an experiment or simulation to achieve a desired output is called an inverse problem. Black-box optimization can also be applied to such inverse problems.

In the case of the wing design example above, if the goal is to design a wing that achieves a target lift-to-drag ratio specified in advance, the black-box objective function can be implemented as follows:

import WingSimulator  # Example simulator

# Predefined target lift-to-drag ratio (L/D ratio)
target_lift_drag_ratio = 0.1

def my_blackbox_func_inverse(
    wing_width: float,
    wing_height: float,
    wing_angle: float,
) -> float:
    """Returns the squared difference between the simulated lift-to-drag ratio and the target value."""
    simulator = WingSimulator(wing_width, wing_height, wing_angle)
    lift_force, drag_force = simulator.simulate()

    return (target_lift_drag_ratio - lift_force / drag_force) ** 2

Defining a black-box function with @blackbox¶

In Amplify-BBOpt, functions that are treated as black-box functions should be decorated with @blackbox. This decorator enables linking between the decision variables defined as described in Creating Decision Variables and the arguments of the black-box function.

Basic definition¶

The following example shows how to implement the previously mentioned black-box function. As illustrated, you can simultaneously define decision variables and link them to the black-box function by assigning the variable definitions as default argument values.

1from amplify_bbopt import RealVariable, blackbox
2
3@blackbox
4def my_blackbox_func_simulation(
5    wing_width: float = RealVariable(bounds=(1, 20)),
6    wing_height: float = RealVariable(bounds=(1, 5)),
7    wing_angle: float = RealVariable(bounds=(0, 45)),
8) -> float:
9    # ...process of black-box function...

Note

When linking variables as default argument values, some environments may show type-checking warnings from tools such as mypy or Pylance (for example, in lines 5–7 in the above code). Although these warnings do not affect execution, you can suppress them by appending # type: ignore at the end of the warning lines if desired.

Defining a black-box function with more complex arguments¶

Even when different types of decision variables are mixed, you can still define a black-box function and link its arguments to the corresponding decision variables using the @blackbox decorator in the same way as in the previous examples.

from amplify_bbopt import BinaryVariable, IntegerVariable, RealVariable, blackbox

@blackbox
def my_blackbox_complex(
    x: float = RealVariable(bounds=(1, 20)),  # Real decision variable
    i: int = IntegerVariable(bounds=(1, 5)),  # Integer decision variable
    q: int = BinaryVariable(),  # Binary decision variable
    y: list[float] = [RealVariable(bounds=(1, 20)) for _ in range(3)],  # Real decision variable list
) -> float:
    # ...Process of black-box function...

Tip

Linking with Annotated

Alternatively, you can link decision variables using Annotated, a standard feature available in Python 3.12 and later.

from amplify_bbopt import RealVariable, blackbox
from typing import Annotated

@blackbox
def my_blackbox_func_simulation(
    wing_width: Annotated[float, RealVariable(bounds=(1, 20))],
    wing_height: Annotated[float, RealVariable(bounds=(1, 5))],
    wing_angle: Annotated[float, RealVariable(bounds=(0, 45))],
) -> float:
    # ...Process of black-box function...

Hint

Effect of the @blackbox decorator

A black-box function defined with the @blackbox decorator is treated as an instance of a black-box function class that inherits from BlackBoxFuncBase. This class provides the following attributes:

Attribute name

Description

name

Name of the black-box function object

variables

Decision variables considered in the black-box function

In particular, by using variables, you can access the decision variable objects associated with the black-box function directly from the function instance itself. This is especially useful when implementing polynomial expressions related to constraints (hard constraints) described later.

import WingSimulator  # Example simulator
from amplify_bbopt import RealVariable, blackbox

@blackbox
def my_blackbox_func(
    wing_width: float = RealVariable(bounds=(1, 20)),
    wing_height: float = RealVariable(bounds=(1, 5)),
    wing_angle: float = RealVariable(bounds=(0, 45)),
) -> float:
    """Returns the negative value of the lift-to-drag ratio obtained from the simulation."""
    simulator = WingSimulator(wing_width, wing_height, wing_angle)
    lift_force, drag_force = simulator.simulate()

    return -lift_force / drag_force


# The decision variable objects can be accessed as attributes of the black-box function class.
variables = my_blackbox_func.variables
print(variables.wing_width)  # wing_width
print(variables.wing_height)  # wing_height
print(variables.wing_angle)  # wing_angle

# Although it is an instance of a class, it can be executed like a regular function.
my_blackbox_func(15, 2, 25)