Xopt basic example¶
Xopt optimization problems can be defined via one of two methods:
- a yaml text file (for limiting the amount of python script writing and/or setting up simulation runs)
- a simple python script (for those who prefer to use python directly)
Here we will demonstrate how both of these techniques can be used to solve a relatively simple constrained optimization problem.
$n=2$ variables: $x_i \in [0, \pi], i=1,2$
Objective:
- $f(x) = \sum_i x_i$
Constraint:
- $g(x) = -x_1^2 -x_2^2 + 1 \le 0$
Xopt Components¶
The definition of the Xopt object requires 3 parts, listed below:
- The
Evaluator
object, which evaluates input points using the arbitrary function specified by thefunction
property. - The
Generator
object, which, when given data that has been evaluated, generates future points to evaluate using the evaluator. - The
VOCS
(variables, objectives, constraints, statics) object, which specifies the input domain, the objectives, constraints and constants passed to the evaluator function.
Import¶
from xopt import Evaluator
from xopt import VOCS
from xopt import Xopt
from xopt.generators import list_available_generators
from xopt.generators import get_generator
import math
Defining Xopt components using python¶
We first examine how one would create and configure and Xopt optimization run using python. This can also be done via a YAML file (see the next section).
Define the objective function and the evaluator¶
Note that the objective function takes in a dict of variable values and returns a dict of objective return values. The keys of the input and output dictionaries must contain the keys we will specify in VOCS (see below).
def evaluate_function(inputs: dict) -> dict:
objective_value = inputs["x1"] ** 2 + inputs["x2"] ** 2
constraint_value = -(inputs["x1"] ** 2) - inputs["x2"] ** 2 + 1
return {"f": objective_value, "g": constraint_value}
evaluator = Evaluator(function=evaluate_function)
Define VOCS¶
Here we define the names and ranges of input parameters, the names and settings of objectives, and the names and settings of constraints. Note that the keys here should be referenced in the evaluate function above.
vocs = VOCS(
variables={"x1": [0, math.pi], "x2": [0, math.pi]},
objectives={"f": "MINIMIZE"},
constraints={"g": ["LESS_THAN", 0]},
)
Define the Generator¶
First lets see which generators are available for use.
list_available_generators()
['random', 'mggpo', 'neldermead', 'latin_hypercube', 'upper_confidence_bound', 'mobo', 'bayesian_exploration', 'time_dependent_upper_confidence_bound', 'expected_improvement', 'multi_fidelity', 'cnsga', 'extremum_seeking', 'rcds']
Here we will use the simplest generator that is defined by Xopt, random number generation.
# get the docstring for the random generator
print(get_generator("random").__doc__)
# use the get generator method to get the random number generator
generator = get_generator("random")(vocs=vocs)
Random number generator.
Combine into Xopt object¶
X = Xopt(vocs=vocs, generator=generator, evaluator=evaluator)
Defining Xopt object from yaml file¶
Alternatively, it might be more useful to define the Xopt object from a text file or YAML string. We replicate the code above with the YAML file below.
# Make a proper input file.
YAML = """
evaluator:
function: __main__.evaluate_function
generator:
name: random
vocs:
variables:
x1: [0, 3.14159]
x2: [0, 3.14159]
objectives: {f: MINIMIZE}
constraints:
g: [LESS_THAN, 0]
"""
# create Xopt object.
X_from_yaml = Xopt.from_yaml(YAML)
Introspection¶
Objects in Xopt can be printed to a string or dumped to a text file for easy introspection of attributes and current configuration.
# Convenient representation of the state.
X
Xopt ________________________________ Version: 2.4.6.dev5+ga295b108.d20250107 Data size: 0 Config as YAML: dump_file: null evaluator: function: __main__.evaluate_function function_kwargs: {} max_workers: 1 vectorized: false generator: name: random supports_batch_generation: true supports_multi_objective: true max_evaluations: null serialize_inline: false serialize_torch: false strict: true vocs: constants: {} constraints: g: - LESS_THAN - 0.0 objectives: f: MINIMIZE observables: [] variables: x1: - 0.0 - 3.141592653589793 x2: - 0.0 - 3.141592653589793
Evaluating randomly generated or fixed inputs.¶
The main Xopt object has a variety of means for evaluating random or fixed points.
This is often used to initialize optimization, but can be used independently of any
generator. Results from evaluations are stored in the data
attribute. Data can also
be explictly added to the Xopt object (and by extension the generator attached to
the xopt object by calling X.add_data()
.
# randomly evaluate some points and add data to Xopt object
X.random_evaluate(5)
x1 | x2 | f | g | xopt_runtime | xopt_error | |
---|---|---|---|---|---|---|
0 | 2.057846 | 1.345458 | 6.044988 | -5.044988 | 0.000006 | False |
1 | 1.468658 | 1.130505 | 3.434998 | -2.434998 | 0.000003 | False |
2 | 2.732492 | 0.615452 | 7.845294 | -6.845294 | 0.000002 | False |
3 | 0.599672 | 2.008051 | 4.391875 | -3.391875 | 0.000002 | False |
4 | 0.453927 | 1.094627 | 1.404259 | -0.404259 | 0.000002 | False |
# evaluate some points additionally
points = {"x1": [1.0, 0.5, 2.25], "x2": [0, 1.75, 0.6]}
X.evaluate_data(points)
x1 | x2 | f | g | xopt_runtime | xopt_error | |
---|---|---|---|---|---|---|
0 | 1.00 | 0.00 | 1.0000 | 0.0000 | 0.000003 | False |
1 | 0.50 | 1.75 | 3.3125 | -2.3125 | 0.000002 | False |
2 | 2.25 | 0.60 | 5.4225 | -4.4225 | 0.000002 | False |
# examine the data stored in Xopt
X.data
x1 | x2 | f | g | xopt_runtime | xopt_error | |
---|---|---|---|---|---|---|
0 | 2.057846 | 1.345458 | 6.044988 | -5.044988 | 0.000006 | False |
1 | 1.468658 | 1.130505 | 3.434998 | -2.434998 | 0.000003 | False |
2 | 2.732492 | 0.615452 | 7.845294 | -6.845294 | 0.000002 | False |
3 | 0.599672 | 2.008051 | 4.391875 | -3.391875 | 0.000002 | False |
4 | 0.453927 | 1.094627 | 1.404259 | -0.404259 | 0.000002 | False |
5 | 1.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000003 | False |
6 | 0.500000 | 1.750000 | 3.312500 | -2.312500 | 0.000002 | False |
7 | 2.250000 | 0.600000 | 5.422500 | -4.422500 | 0.000002 | False |
Optimization¶
Xopt conducts a single iteration of optimization by calling X.step()
. Inside this
function Xopt will generate a point (or set of points) using the generator object,
then send the point to be evaluated by the evaluator. Results will be stored in the
data attribute.
# Take one step (generate a single point)
X.step()
# examine the results
X.data
x1 | x2 | f | g | xopt_runtime | xopt_error | |
---|---|---|---|---|---|---|
0 | 2.057846 | 1.345458 | 6.044988 | -5.044988 | 0.000006 | False |
1 | 1.468658 | 1.130505 | 3.434998 | -2.434998 | 0.000003 | False |
2 | 2.732492 | 0.615452 | 7.845294 | -6.845294 | 0.000002 | False |
3 | 0.599672 | 2.008051 | 4.391875 | -3.391875 | 0.000002 | False |
4 | 0.453927 | 1.094627 | 1.404259 | -0.404259 | 0.000002 | False |
5 | 1.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000003 | False |
6 | 0.500000 | 1.750000 | 3.312500 | -2.312500 | 0.000002 | False |
7 | 2.250000 | 0.600000 | 5.422500 | -4.422500 | 0.000002 | False |
8 | 1.763594 | 0.820817 | 3.784005 | -2.784005 | 0.000003 | False |
# take a couple of steps and examine the results
for _ in range(10):
X.step()
X.data
x1 | x2 | f | g | xopt_runtime | xopt_error | |
---|---|---|---|---|---|---|
0 | 2.057846 | 1.345458 | 6.044988 | -5.044988 | 0.000006 | False |
1 | 1.468658 | 1.130505 | 3.434998 | -2.434998 | 0.000003 | False |
2 | 2.732492 | 0.615452 | 7.845294 | -6.845294 | 0.000002 | False |
3 | 0.599672 | 2.008051 | 4.391875 | -3.391875 | 0.000002 | False |
4 | 0.453927 | 1.094627 | 1.404259 | -0.404259 | 0.000002 | False |
5 | 1.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000003 | False |
6 | 0.500000 | 1.750000 | 3.312500 | -2.312500 | 0.000002 | False |
7 | 2.250000 | 0.600000 | 5.422500 | -4.422500 | 0.000002 | False |
8 | 1.763594 | 0.820817 | 3.784005 | -2.784005 | 0.000003 | False |
9 | 0.555516 | 1.794332 | 3.528224 | -2.528224 | 0.000003 | False |
10 | 0.544649 | 2.154640 | 4.939115 | -3.939115 | 0.000003 | False |
11 | 0.895511 | 2.364227 | 6.391509 | -5.391509 | 0.000003 | False |
12 | 0.609695 | 2.626823 | 7.271925 | -6.271925 | 0.000003 | False |
13 | 1.400714 | 0.472096 | 2.184874 | -1.184874 | 0.000003 | False |
14 | 0.124040 | 0.895613 | 0.817508 | 0.182492 | 0.000003 | False |
15 | 3.060395 | 1.426571 | 11.401121 | -10.401121 | 0.000002 | False |
16 | 2.395042 | 2.499610 | 11.984274 | -10.984274 | 0.000002 | False |
17 | 1.244689 | 0.641559 | 1.960848 | -0.960848 | 0.000002 | False |
18 | 2.171908 | 1.197979 | 6.152339 | -5.152339 | 0.000002 | False |
Find and evaluate the best point from X.data
¶
idx, val, params = X.vocs.select_best(X.data)
print(f"best objective value {val}")
print(f"best point {params}")
X.evaluate_data(params)
best objective value [1.] best point {'x1': 1.0, 'x2': 0.0}
x1 | x2 | f | g | xopt_runtime | xopt_error | |
---|---|---|---|---|---|---|
0 | 1.0 | 0.0 | 1.0 | 0.0 | 0.000003 | False |
Visualization¶
Finally, we can visualize the objectives and variables to monitor optimization or visualize the results
# view objective values
X.data.plot(y=X.vocs.objective_names)
# view variables values
X.data.plot(*X.vocs.variable_names, kind="scatter")
# you can also normalize the variables
X.vocs.normalize_inputs(X.data).plot(*X.vocs.variable_names, kind="scatter")
<Axes: xlabel='x1', ylabel='x2'>