The Ax Service API is designed to allow the user to control scheduling of trials and data computation while having an easy to use interface with Ax.
The user iteratively:
from ax.service.ax_client import AxClient
from ax.utils.measurement.synthetic_functions import hartmann6
from ax.utils.notebook.plotting import render, init_notebook_plotting
init_notebook_plotting()
[INFO 01-18 03:36:42] ipy_plotting: Injecting Plotly library into cell. Do not overwrite or delete cell.
Create a client object to interface with Ax APIs. By default this runs locally without storage.
ax_client = AxClient()
[INFO 01-18 03:36:42] ax.service.ax_client: Starting optimization with verbose logging. To disable logging, set the `verbose_logging` argument to `False`. Note that float values in the logs are rounded to 2 decimal points.
An experiment consists of a search space (parameters and parameter constraints) and optimization configuration (objective name, minimization setting, and outcome constraints). Note that:
name
, parameters
, and objective_name
arguments are required.parameters
have the following required keys: "name" - parameter name, "type" - parameter type ("range", "choice" or "fixed"), "bounds" for range parameters, "values" for choice parameters, and "value" for fixed parameters.parameters
can optionally include "value_type" ("int", "float", "bool" or "str"), "log_scale" flag for range parameters, and "is_ordered" flag for choice parameters.parameter_constraints
should be a list of strings of form "p1 >= p2" or "p1 + p2 <= some_bound".outcome_constraints
should be a list of strings of form "constrained_metric <= some_bound".ax_client.create_experiment(
name="hartmann_test_experiment",
parameters=[
{
"name": "x1",
"type": "range",
"bounds": [0.0, 1.0],
"value_type": "float", # Optional, defaults to inference from type of "bounds".
"log_scale": False, # Optional, defaults to False.
},
{
"name": "x2",
"type": "range",
"bounds": [0.0, 1.0],
},
{
"name": "x3",
"type": "range",
"bounds": [0.0, 1.0],
},
{
"name": "x4",
"type": "range",
"bounds": [0.0, 1.0],
},
{
"name": "x5",
"type": "range",
"bounds": [0.0, 1.0],
},
{
"name": "x6",
"type": "range",
"bounds": [0.0, 1.0],
},
],
objective_name="hartmann6",
minimize=True, # Optional, defaults to False.
parameter_constraints=["x1 + x2 <= 2.0"], # Optional.
outcome_constraints=["l2norm <= 1.25"], # Optional.
)
[INFO 01-18 03:36:42] ax.modelbridge.dispatch_utils: Using Bayesian Optimization generation strategy: GenerationStrategy(name='Sobol+GPEI', steps=[Sobol for 6 arms, GPEI for subsequent arms], generated 0 arm(s) so far). Iterations after 6 will take longer to generate due to model-fitting.
When using Ax a service, evaluation of parameterizations suggested by Ax is done either locally or, more commonly, using an external scheduler. Below is a dummy evaluation function that outputs data for two metrics "hartmann6" and "l2norm". Note that all returned metrics correspond to either the objective_name
set on experiment creation or the metric names mentioned in outcome_constraints
.
import numpy as np
def evaluate(parameters):
x = np.array([parameters.get(f"x{i+1}") for i in range(6)])
# In our case, standard error is 0, since we are computing a synthetic function.
return {"hartmann6": (hartmann6(x), 0.0), "l2norm": (np.sqrt((x ** 2).sum()), 0.0)}
Result of the evaluation should generally be a mapping of the format: {metric_name -> (mean, SEM)}
. If there is only one metric in the experiment – the objective – then evaluation function can return a single tuple of mean and SEM, in which case Ax will assume that evaluation corresponds to the objective. It can also return only the mean as a float, in which case Ax will treat SEM as unknown and use a model that can infer it.
For more details on evaluation function, refer to the "Trial Evaluation" section in the Ax docs at ax.dev
With the experiment set up, we can start the optimization loop.
At each step, the user queries the client for a new trial then submits the evaluation of that trial back to the client.
Note that Ax auto-selects an appropriate optimization algorithm based on the search space. For more advance use cases that require a specific optimization algorithm, pass a generation_strategy
argument into the AxClient
constructor. Note that when Bayesian Optimization is used, generating new trials may take a few minutes.
for i in range(25):
parameters, trial_index = ax_client.get_next_trial()
# Local evaluation here can be replaced with deployment to external system.
ax_client.complete_trial(trial_index=trial_index, raw_data=evaluate(parameters))
[INFO 01-18 03:36:42] ax.service.ax_client: Generated new trial 0 with parameters {'x1': 0.42, 'x2': 0.97, 'x3': 0.95, 'x4': 0.84, 'x5': 0.38, 'x6': 0.92}. [INFO 01-18 03:36:42] ax.service.ax_client: Completed trial 0 with data: {'hartmann6': (-0.02, 0.0), 'l2norm': (1.93, 0.0)}. [INFO 01-18 03:36:42] ax.service.ax_client: Generated new trial 1 with parameters {'x1': 0.35, 'x2': 0.99, 'x3': 0.09, 'x4': 0.06, 'x5': 0.99, 'x6': 0.8}. [INFO 01-18 03:36:42] ax.service.ax_client: Completed trial 1 with data: {'hartmann6': (-0.0, 0.0), 'l2norm': (1.65, 0.0)}. [INFO 01-18 03:36:42] ax.service.ax_client: Generated new trial 2 with parameters {'x1': 0.32, 'x2': 0.94, 'x3': 0.72, 'x4': 0.58, 'x5': 0.02, 'x6': 0.35}. [INFO 01-18 03:36:42] ax.service.ax_client: Completed trial 2 with data: {'hartmann6': (-0.71, 0.0), 'l2norm': (1.4, 0.0)}. [INFO 01-18 03:36:42] ax.service.ax_client: Generated new trial 3 with parameters {'x1': 0.19, 'x2': 0.25, 'x3': 0.94, 'x4': 0.78, 'x5': 0.98, 'x6': 0.47}. [INFO 01-18 03:36:42] ax.service.ax_client: Completed trial 3 with data: {'hartmann6': (-0.01, 0.0), 'l2norm': (1.67, 0.0)}. [INFO 01-18 03:36:42] ax.service.ax_client: Generated new trial 4 with parameters {'x1': 0.22, 'x2': 0.08, 'x3': 0.32, 'x4': 0.87, 'x5': 0.88, 'x6': 0.17}. [INFO 01-18 03:36:42] ax.service.ax_client: Completed trial 4 with data: {'hartmann6': (-0.01, 0.0), 'l2norm': (1.31, 0.0)}. [INFO 01-18 03:36:42] ax.service.ax_client: Generated new trial 5 with parameters {'x1': 0.04, 'x2': 0.83, 'x3': 0.83, 'x4': 0.08, 'x5': 0.52, 'x6': 0.95}. [INFO 01-18 03:36:42] ax.service.ax_client: Completed trial 5 with data: {'hartmann6': (-0.12, 0.0), 'l2norm': (1.6, 0.0)}. [INFO 01-18 03:36:46] ax.service.ax_client: Generated new trial 6 with parameters {'x1': 0.31, 'x2': 0.97, 'x3': 0.56, 'x4': 0.51, 'x5': 0.0, 'x6': 0.21}. [INFO 01-18 03:36:46] ax.service.ax_client: Completed trial 6 with data: {'hartmann6': (-1.6, 0.0), 'l2norm': (1.28, 0.0)}. [INFO 01-18 03:36:50] ax.service.ax_client: Generated new trial 7 with parameters {'x1': 0.27, 'x2': 0.91, 'x3': 0.53, 'x4': 0.48, 'x5': 0.0, 'x6': 0.14}. [INFO 01-18 03:36:50] ax.service.ax_client: Completed trial 7 with data: {'hartmann6': (-1.85, 0.0), 'l2norm': (1.2, 0.0)}. [INFO 01-18 03:36:54] ax.service.ax_client: Generated new trial 8 with parameters {'x1': 0.27, 'x2': 0.92, 'x3': 0.45, 'x4': 0.45, 'x5': 0.0, 'x6': 0.09}. [INFO 01-18 03:36:54] ax.service.ax_client: Completed trial 8 with data: {'hartmann6': (-1.93, 0.0), 'l2norm': (1.15, 0.0)}. [INFO 01-18 03:36:58] ax.service.ax_client: Generated new trial 9 with parameters {'x1': 0.24, 'x2': 0.99, 'x3': 0.52, 'x4': 0.4, 'x5': 0.0, 'x6': 0.03}. [INFO 01-18 03:36:58] ax.service.ax_client: Completed trial 9 with data: {'hartmann6': (-1.37, 0.0), 'l2norm': (1.21, 0.0)}. [INFO 01-18 03:37:03] ax.service.ax_client: Generated new trial 10 with parameters {'x1': 0.3, 'x2': 0.86, 'x3': 0.4, 'x4': 0.49, 'x5': 0.01, 'x6': 0.13}. [INFO 01-18 03:37:03] ax.service.ax_client: Completed trial 10 with data: {'hartmann6': (-2.11, 0.0), 'l2norm': (1.11, 0.0)}. [INFO 01-18 03:37:07] ax.service.ax_client: Generated new trial 11 with parameters {'x1': 0.24, 'x2': 0.83, 'x3': 0.38, 'x4': 0.57, 'x5': 0.0, 'x6': 0.09}. [INFO 01-18 03:37:07] ax.service.ax_client: Completed trial 11 with data: {'hartmann6': (-1.87, 0.0), 'l2norm': (1.11, 0.0)}. [INFO 01-18 03:37:11] ax.service.ax_client: Generated new trial 12 with parameters {'x1': 0.39, 'x2': 0.82, 'x3': 0.41, 'x4': 0.47, 'x5': 0.0, 'x6': 0.11}. [INFO 01-18 03:37:11] ax.service.ax_client: Completed trial 12 with data: {'hartmann6': (-2.53, 0.0), 'l2norm': (1.11, 0.0)}. [INFO 01-18 03:37:15] ax.service.ax_client: Generated new trial 13 with parameters {'x1': 0.47, 'x2': 0.77, 'x3': 0.43, 'x4': 0.46, 'x5': 0.0, 'x6': 0.06}. [INFO 01-18 03:37:15] ax.service.ax_client: Completed trial 13 with data: {'hartmann6': (-2.35, 0.0), 'l2norm': (1.11, 0.0)}. [INFO 01-18 03:37:18] ax.service.ax_client: Generated new trial 14 with parameters {'x1': 0.39, 'x2': 0.75, 'x3': 0.42, 'x4': 0.39, 'x5': 0.0, 'x6': 0.1}. [INFO 01-18 03:37:18] ax.service.ax_client: Completed trial 14 with data: {'hartmann6': (-1.83, 0.0), 'l2norm': (1.03, 0.0)}. [INFO 01-18 03:37:22] ax.service.ax_client: Generated new trial 15 with parameters {'x1': 0.41, 'x2': 0.85, 'x3': 0.41, 'x4': 0.51, 'x5': 0.0, 'x6': 0.07}. [INFO 01-18 03:37:22] ax.service.ax_client: Completed trial 15 with data: {'hartmann6': (-2.97, 0.0), 'l2norm': (1.15, 0.0)}. [INFO 01-18 03:37:27] ax.service.ax_client: Generated new trial 16 with parameters {'x1': 0.44, 'x2': 0.89, 'x3': 0.4, 'x4': 0.57, 'x5': 0.0, 'x6': 0.03}. [INFO 01-18 03:37:27] ax.service.ax_client: Completed trial 16 with data: {'hartmann6': (-3.09, 0.0), 'l2norm': (1.21, 0.0)}. [INFO 01-18 03:37:30] ax.service.ax_client: Generated new trial 17 with parameters {'x1': 0.42, 'x2': 0.87, 'x3': 0.47, 'x4': 0.57, 'x5': 0.0, 'x6': 0.0}. [INFO 01-18 03:37:30] ax.service.ax_client: Completed trial 17 with data: {'hartmann6': (-3.1, 0.0), 'l2norm': (1.21, 0.0)}. [INFO 01-18 03:37:34] ax.service.ax_client: Generated new trial 18 with parameters {'x1': 0.43, 'x2': 0.87, 'x3': 0.42, 'x4': 0.55, 'x5': 0.06, 'x6': 0.0}. [INFO 01-18 03:37:34] ax.service.ax_client: Completed trial 18 with data: {'hartmann6': (-3.07, 0.0), 'l2norm': (1.19, 0.0)}. [INFO 01-18 03:37:35] ax.service.ax_client: Generated new trial 19 with parameters {'x1': 0.44, 'x2': 0.89, 'x3': 0.45, 'x4': 0.52, 'x5': 0.0, 'x6': 0.0}. [INFO 01-18 03:37:35] ax.service.ax_client: Completed trial 19 with data: {'hartmann6': (-2.95, 0.0), 'l2norm': (1.21, 0.0)}. [INFO 01-18 03:37:39] ax.service.ax_client: Generated new trial 20 with parameters {'x1': 0.41, 'x2': 0.86, 'x3': 0.41, 'x4': 0.6, 'x5': 0.02, 'x6': 0.03}. [INFO 01-18 03:37:39] ax.service.ax_client: Completed trial 20 with data: {'hartmann6': (-3.13, 0.0), 'l2norm': (1.2, 0.0)}. [INFO 01-18 03:37:39] ax.service.ax_client: Generated new trial 21 with parameters {'x1': 0.47, 'x2': 0.8, 'x3': 0.2, 'x4': 0.85, 'x5': 0.35, 'x6': 0.04}. [INFO 01-18 03:37:39] ax.service.ax_client: Completed trial 21 with data: {'hartmann6': (-1.29, 0.0), 'l2norm': (1.32, 0.0)}. [INFO 01-18 03:37:40] ax.service.ax_client: Generated new trial 22 with parameters {'x1': 0.41, 'x2': 0.85, 'x3': 0.36, 'x4': 0.57, 'x5': 0.0, 'x6': 0.0}. [INFO 01-18 03:37:40] ax.service.ax_client: Completed trial 22 with data: {'hartmann6': (-3.07, 0.0), 'l2norm': (1.17, 0.0)}. [INFO 01-18 03:37:41] ax.service.ax_client: Generated new trial 23 with parameters {'x1': 0.43, 'x2': 0.88, 'x3': 0.72, 'x4': 0.66, 'x5': 0.15, 'x6': 0.0}. [INFO 01-18 03:37:41] ax.service.ax_client: Completed trial 23 with data: {'hartmann6': (-2.89, 0.0), 'l2norm': (1.39, 0.0)}. [INFO 01-18 03:37:42] ax.service.ax_client: Generated new trial 24 with parameters {'x1': 0.04, 'x2': 0.84, 'x3': 0.05, 'x4': 0.32, 'x5': 0.86, 'x6': 0.8}. [INFO 01-18 03:37:42] ax.service.ax_client: Completed trial 24 with data: {'hartmann6': (-0.0, 0.0), 'l2norm': (1.48, 0.0)}.
To view all trials in a data frame at any point during optimization:
ax_client.get_trials_data_frame().sort_values('trial_index')
/home/travis/virtualenv/python3.6.7/lib/python3.6/site-packages/pandas/core/reshape/merge.py:617: UserWarning: merging between different levels can give an unintended result (2 levels on the left, 1 on the right)
arm_name | hartmann6 | l2norm | trial_index | x1 | x2 | x3 | x4 | x5 | x6 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0_0 | -0.0237176 | 1.93291 | 0 | 0.424880 | 0.968682 | 0.952429 | 0.844571 | 3.827628e-01 | 9.221341e-01 |
3 | 1_0 | -0.00139386 | 1.65048 | 1 | 0.351940 | 0.985043 | 0.087487 | 0.064516 | 9.872298e-01 | 8.021760e-01 |
15 | 2_0 | -0.706655 | 1.39991 | 2 | 0.324077 | 0.940471 | 0.715466 | 0.576476 | 2.418129e-02 | 3.541575e-01 |
18 | 3_0 | -0.0113239 | 1.66571 | 3 | 0.188291 | 0.252210 | 0.941311 | 0.775871 | 9.815478e-01 | 4.733413e-01 |
19 | 4_0 | -0.0097572 | 1.31359 | 4 | 0.218321 | 0.083273 | 0.317531 | 0.870771 | 8.839930e-01 | 1.743592e-01 |
20 | 5_0 | -0.122768 | 1.59811 | 5 | 0.035168 | 0.832064 | 0.825524 | 0.082991 | 5.226894e-01 | 9.480583e-01 |
21 | 6_0 | -1.59697 | 1.28184 | 6 | 0.308396 | 0.967262 | 0.556293 | 0.506896 | 0.000000e+00 | 2.145123e-01 |
22 | 7_0 | -1.84699 | 1.19724 | 7 | 0.273695 | 0.911516 | 0.529915 | 0.475865 | 1.063916e-08 | 1.426371e-01 |
23 | 8_0 | -1.92844 | 1.1519 | 8 | 0.269509 | 0.915591 | 0.452171 | 0.451904 | 4.445262e-03 | 8.510050e-02 |
24 | 9_0 | -1.36789 | 1.21368 | 9 | 0.242275 | 0.989032 | 0.524883 | 0.399264 | 1.799034e-17 | 3.495734e-02 |
1 | 10_0 | -2.11288 | 1.11454 | 10 | 0.295624 | 0.857789 | 0.401107 | 0.489763 | 1.375390e-02 | 1.344188e-01 |
2 | 11_0 | -1.87024 | 1.10781 | 11 | 0.239273 | 0.833527 | 0.382639 | 0.565868 | 5.627377e-17 | 9.271207e-02 |
4 | 12_0 | -2.52856 | 1.10513 | 12 | 0.387362 | 0.817502 | 0.412716 | 0.468994 | 8.787075e-17 | 1.125173e-01 |
5 | 13_0 | -2.3546 | 1.10579 | 13 | 0.469598 | 0.774606 | 0.428228 | 0.463287 | 4.524198e-17 | 6.491297e-02 |
6 | 14_0 | -1.8329 | 1.02623 | 14 | 0.388343 | 0.753307 | 0.417754 | 0.386430 | 4.228920e-12 | 1.049372e-01 |
7 | 15_0 | -2.97119 | 1.14977 | 15 | 0.412175 | 0.848841 | 0.409701 | 0.509200 | 4.069093e-13 | 6.645956e-02 |
8 | 16_0 | -3.08879 | 1.21008 | 16 | 0.440641 | 0.887102 | 0.398171 | 0.569030 | 1.133831e-18 | 2.914861e-02 |
9 | 17_0 | -3.09757 | 1.21273 | 17 | 0.418652 | 0.866243 | 0.466978 | 0.571835 | 5.376449e-19 | 2.141938e-03 |
10 | 18_0 | -3.0734 | 1.19469 | 18 | 0.426727 | 0.871729 | 0.419691 | 0.552510 | 6.204681e-02 | 3.425596e-03 |
11 | 19_0 | -2.95494 | 1.20795 | 19 | 0.439340 | 0.889338 | 0.452232 | 0.520278 | 7.532006e-18 | 2.502241e-18 |
12 | 20_0 | -3.13087 | 1.20079 | 20 | 0.414879 | 0.862800 | 0.410556 | 0.596397 | 1.606970e-02 | 2.906198e-02 |
13 | 21_0 | -1.29293 | 1.32498 | 21 | 0.473875 | 0.803599 | 0.201302 | 0.847604 | 3.531386e-01 | 3.980996e-02 |
14 | 22_0 | -3.06919 | 1.16652 | 22 | 0.413430 | 0.854961 | 0.358489 | 0.574780 | 0.000000e+00 | 0.000000e+00 |
16 | 23_0 | -2.89349 | 1.38556 | 23 | 0.425087 | 0.875474 | 0.717182 | 0.659251 | 1.538223e-01 | 0.000000e+00 |
17 | 24_0 | -0.00343404 | 1.48192 | 24 | 0.036716 | 0.841991 | 0.046683 | 0.320709 | 8.560150e-01 | 8.049785e-01 |
Once it's complete, we can access the best parameters found, as well as the corresponding metric values.
best_parameters, values = ax_client.get_best_parameters()
best_parameters
{'x1': 0.41487931700070046, 'x2': 0.8627996265696116, 'x3': 0.4105563552715978, 'x4': 0.5963972686396043, 'x5': 0.016069695424268303, 'x6': 0.0290619818012548}
means, covariances = values
means
{'l2norm': 1.2007899506093045, 'hartmann6': -3.13087254787905}
For comparison, Hartmann6 minimum:
hartmann6.fmin
-3.32237
Here we arbitrarily select "x1" and "x2" as the two parameters to plot for both metrics, "hartmann6" and "l2norm".
render(ax_client.get_contour_plot())
[INFO 01-18 03:37:42] ax.service.ax_client: Retrieving contour plot with parameter 'x1' on X-axis and 'x2' on Y-axis, for metric 'hartmann6'. Ramaining parameters are affixed to the middle of their range.
We can also retrieve a contour plot for the other metric, "l2norm" –– say, we are interested in seeing the response surface for parameters "x3" and "x4" for this one.
render(ax_client.get_contour_plot(param_x="x3", param_y="x4", metric_name="l2norm"))
[INFO 01-18 03:37:44] ax.service.ax_client: Retrieving contour plot with parameter 'x3' on X-axis and 'x4' on Y-axis, for metric 'l2norm'. Ramaining parameters are affixed to the middle of their range.
Here we plot the optimization trace, showing the progression of finding the point with the optimal objective:
render(ax_client.get_optimization_trace(objective_optimum=hartmann6.fmin)) # Objective_optimum is optional.
We can serialize the state of optimization to JSON and save it to a .json
file or save it to the SQL backend. For the former:
ax_client.save_to_json_file() # For custom filepath, pass `filepath` argument.
[INFO 01-18 03:37:46] ax.service.ax_client: Saved JSON-serialized state of optimization to `ax_client_snapshot.json`.
restored_ax_client = AxClient.load_from_json_file() # For custom filepath, pass `filepath` argument.
[INFO 01-18 03:37:47] ax.service.ax_client: Starting optimization with verbose logging. To disable logging, set the `verbose_logging` argument to `False`. Note that float values in the logs are rounded to 2 decimal points.
To store state of optimization to an SQL backend, first follow setup instructions on Ax website.
Having set up the SQL backend, pass DBSettings
to AxClient
on instantiation (note that SQLAlchemy
dependency will have to be installed – for installation, refer to optional dependencies on Ax website):
from ax.storage.sqa_store.structs import DBSettings
# URL is of the form "dialect+driver://username:password@host:port/database".
db_settings = DBSettings(url="postgresql+psycopg2://sarah:c82i94d@ocalhost:5432/foobar")
# Instead of URL, can provide a `creator function`; can specify custom encoders/decoders if necessary.
new_ax = AxClient(db_settings=db_settings)
[INFO 01-18 03:37:47] ax.service.ax_client: Starting optimization with verbose logging. To disable logging, set the `verbose_logging` argument to `False`. Note that float values in the logs are rounded to 2 decimal points.
When valid DBSettings
are passed into AxClient
, a unique experiment name is a required argument (name
) to ax_client.create_experiment
. The state of the optimization is auto-saved any time it changes (i.e. a new trial is added or completed, etc).
To reload an optimization state later, instantiate AxClient
with the same DBSettings
and use ax_client.load_experiment_from_database(experiment_name="my_experiment")
.
Evaluation failure: should any optimization iterations fail during evaluation, log_trial_failure
will ensure that the same trial is not proposed again.
_, trial_index = ax_client.get_next_trial()
ax_client.log_trial_failure(trial_index=trial_index)
[INFO 01-18 03:37:47] ax.service.ax_client: Generated new trial 25 with parameters {'x1': 0.99, 'x2': 0.12, 'x3': 0.9, 'x4': 0.1, 'x5': 0.37, 'x6': 0.14}. [INFO 01-18 03:37:47] ax.service.ax_client: Registered failure of trial 25.
Adding custom trials: should there be need to evaluate a specific parameterization, attach_trial
will add it to the experiment.
ax_client.attach_trial(parameters={"x1": 0.9, "x2": 0.9, "x3": 0.9, "x4": 0.9, "x5": 0.9, "x6": 0.9})
[INFO 01-18 03:37:47] ax.service.ax_client: Attached custom parameterization {'x1': 0.9, 'x2': 0.9, 'x3': 0.9, 'x4': 0.9, 'x5': 0.9, 'x6': 0.9} as trial 26.
({'x1': 0.9, 'x2': 0.9, 'x3': 0.9, 'x4': 0.9, 'x5': 0.9, 'x6': 0.9}, 26)
Need to run many trials in parallel: for optimal results and optimization efficiency, we strongly recommend sequential optimization (generating a few trials, then waiting for them to be completed with evaluation data). However, if your use case needs to dispatch many trials in parallel before they are updated with data and you are running into the "All trials for current model have been generated, but not enough data has been observed to fit next model" error, instantiate AxClient
as AxClient(enforce_sequential_optimization=False)
.
Total runtime of script: 1 minutes, 8.24 seconds.