Use the built-in optimization algorithms¤
This tutorial shows how to use the optimizer blocks that ship with f3dasm on a built-in benchmark function.
Optimization in f3dasm is expressed by composing blocks. There are two shapes of optimizer block:
- Ask/tell update-step blocks (e.g. TPE from Optuna). Chain with a data generator and wrap in a
LoopBlock:(update_step >> data_generator).loop(n_iterations). - One-shot blocks (scipy's
cg,lbfgsb,nelder_mead). Call once; scipy runs its own inner loop viamaxiter.
See the Optimization Loops tutorial for a deeper dive on the pattern. Here we focus on how to use the built-in factories.
Set-up¤
We'll minimize a simple 2D paraboloid so both ask/tell and one-shot optimizers apply without domain-shape gymnastics. First define a domain and a data generator.
from f3dasm import ExperimentData, create_sampler, datagenerator
from f3dasm.design import Domain
domain = Domain()
domain.add_float(name="x0", low=-5.0, high=5.0)
domain.add_float(name="x1", low=-5.0, high=5.0)
domain.add_output("y")
@datagenerator(output_names="y")
def f(x0: float, x1: float) -> float:
return (x0 - 1.5) ** 2 + (x1 + 2.0) ** 2
def initial_design(n: int = 10) -> ExperimentData:
data = ExperimentData(domain=domain)
data = create_sampler("latin", seed=42).call(data, n_samples=n)
f.arm(data)
return f.call(data)
Method 1: string-based factory create_optimizer¤
f3dasm.create_optimizer(name, **kwargs) returns an optimizer block. For ask/tell optimizers it returns the update step — you still chain with a data generator and wrap in a LoopBlock yourself.
Ask/tell: tpesampler¤
from f3dasm import create_optimizer
data = initial_design()
update_step = create_optimizer("tpesampler", output_name="y")
loop = (update_step >> f).loop(20)
loop.arm(data)
data = loop.call(data)
print(f"After TPE: {len(data)} rows")
One-shot: cg, lbfgsb, nelder_mead¤
Scipy optimizers expect the objective as f(x) with a single array argument, so they need a domain with an ArrayParameter named the same as input_name.
import numpy as np
scipy_domain = Domain()
scipy_domain.add_array(name="x", shape=(2,), low=-5.0, high=5.0)
scipy_domain.add_output("y")
@datagenerator(output_names="y")
def f_array(x: np.ndarray) -> float:
return float(np.sum((x - np.array([1.5, -2.0])) ** 2))
scipy_data = ExperimentData(domain=scipy_domain)
scipy_data = create_sampler("random", seed=0).call(scipy_data, n_samples=1)
f_array.arm(scipy_data)
scipy_data = f_array.call(scipy_data)
scipy_opt = create_optimizer(
"lbfgsb",
data_generator=f_array,
output_name="y",
input_name="x",
maxiter=50,
)
scipy_opt.arm(scipy_data)
scipy_data = scipy_opt.call(scipy_data)
print(f"After L-BFGS-B one-shot: {len(scipy_data)} rows")
Method 2: import the factory directly from f3dasm.optimization¤
The per-optimizer factories (tpesampler, cg, lbfgsb, nelder_mead) are also importable directly. This is equivalent to create_optimizer but gives you type-aware editor completion and makes the call explicit.
from f3dasm.optimization import tpesampler
data = initial_design()
update_step = tpesampler(output_name="y")
loop = (update_step >> f).loop(20)
loop.arm(data)
data = loop.call(data)
df_in, df_out = data.to_pandas()
best_idx = df_out["y"].idxmin()
print(
f"Best: x0={df_in.loc[best_idx, 'x0']:.3f}, "
f"x1={df_in.loc[best_idx, 'x1']:.3f}, "
f"y={df_out.loc[best_idx, 'y']:.4f}"
)
Next: Built-in Samplers