Running optimizations¶
The Optimization scan mode turns a scan into a closed feedback loop: instead of sweeping a variable over a fixed grid, an optimization algorithm proposes device setpoints, the scanner sets them and acquires shots, an evaluator turns that data into a number (the objective), and the algorithm uses it to decide where to look next. This page is a practical guide to configuring and running one — what the config file means, which algorithm to pick, how the objective is computed, and the pitfalls worth knowing before your first run.
It is operator-focused. For the underlying classes see the Base Optimizer and Base Evaluator API references; the optimization layer wraps Xopt for the algorithms.
How the loop works¶
Every optimization is assembled from four pieces, all declared in one YAML file:
| Piece | What it is | Config key |
|---|---|---|
| VOCS | The problem: which variables to tune (with bounds) and what to optimize | vocs |
| Generator | The strategy: the algorithm that proposes the next setpoints | generator |
| Evaluator | The objective: turns acquired shot data into a number (or observables) | evaluator |
| Optimizer | The glue that runs the loop and talks to the scan engine | the file as a whole |
Each scan step then runs this cycle:
- The generator proposes values for the
vocsvariables (the first couple of steps are random, to seed the model). - The scan engine sets those devices and acquires the configured number of shots.
- The evaluator reads the just-acquired bin of data and computes the
objective
f(and/or observables). - The result is fed back to the generator, which proposes the next point.
The loop repeats until you stop the scan. Optionally, at the end the scanner can move the devices to the best observed setpoint.
The optimization config file¶
You point the GUI's Optimization mode at a single YAML file. Here is a complete, annotated example — a Bayesian optimization that maximizes camera counts on one diagnostic:
# VOCS — the optimization problem
vocs:
variables:
U_Hexapod:ypos: [-22.1, -21.5] # device:variable -> [min, max]
objectives:
f: MINIMIZE # the evaluator returns "f"; MINIMIZE or MAXIMIZE
constraints: {} # optional, usually empty
# Evaluator — how to turn acquired data into "f"
evaluator:
module: geecs_scanner.optimization.evaluators.beam_sum_counts_evaluator
class: MaxCountsEvaluator
kwargs:
analyzers: # diagnostic-driven data source
- diagnostic: UC_TC_Output # a diagnostic config (configs repo)
scan:
mode: per_bin # average the bin, then analyze once
# Generator — the algorithm
generator:
name: bayes_turbo_standard
Top-level keys¶
| Key | Required | Meaning |
|---|---|---|
vocs |
yes | Variables (device:variable: [min, max]), objectives (f: MINIMIZE/MAXIMIZE), optional constraints, optional observables (for BAX) |
evaluator |
yes | module + class of the evaluator, plus kwargs passed to it |
generator |
yes | name of the algorithm (see Choosing a generator) |
xopt_config_overrides |
no | Per-generator tuning, keyed by generator name (e.g. beta, BAX probe settings) |
seed_dump_files |
no | Paths to prior xopt_dump.yaml files to warm-start the model |
move_to_best_on_finish |
no | If true, move devices to the best observed setpoint when the scan ends |
device_requirements |
no | Usually omit — auto-generated from the evaluator's analyzers |
Variable names are GEECS device:variable strings
Use the exact device:variable form, e.g. U_Hexapod:ypos or
U_EMQTripletBipolar:Current_Limit.Ch1. Names with spaces, colons, and
dots are fine.
Choosing a generator¶
Set generator.name to one of:
| Name | Use it when |
|---|---|
random |
Baseline / debugging — uniform random sampling, no model |
bayes_default |
Good default Bayesian optimization (Expected Improvement) for a single smooth objective |
bayes_ucb |
Noise-robust alternative to EI (Upper Confidence Bound). Tune exploration with beta (default 2.0) |
bayes_ucb_explore |
Pure exploration / surrogate-building (UCB with high beta) — maximizes model knowledge, not the objective |
bayes_turbo_standard |
Higher-dimensional or noisier problems — EI inside a TuRBO trust region that localizes the search |
bayes_turbo_ucb |
TuRBO trust region with UCB instead of EI (configurable beta) |
bayes_cheetah |
Specialized surrogate-driven BO (requires the optional cheetah package) |
multipoint_bax_alignment / _l2 |
Beam alignment via BAX — observables-only, minimizes a virtual slope objective |
Per-generator knobs go under xopt_config_overrides, keyed by the generator
name:
generator:
name: bayes_ucb
xopt_config_overrides:
bayes_ucb:
beta: 4.0 # more exploration
Defining the objective: evaluators¶
The evaluator is the only piece you may need to write Python for, and even then
it is small. It reads the just-acquired bin of shot data and returns a
dictionary. There are two data sources, set in the evaluator kwargs:
analyzers— diagnostic-driven. Each entry names a diagnostic (the same YAML diagnostics ScanAnalysis uses) that runs against the acquired images and produces namespaced scalars likeUC_TopView_x_fwhm. Entries are a bare stem (- UC_TopView) or a dict with overrides (- {diagnostic: UC_FROG, scan: {mode: per_bin}}).scalars— column names read directly from the current-bin data frame (s-file columns), used verbatim.
A subclass implements one or both hooks:
from geecs_scanner.optimization.base_evaluator import BaseEvaluator
class MyObjective(BaseEvaluator):
def compute_objective(self, scalars: dict, bin_number: int) -> float:
# scalars holds analyzer outputs + raw scalar columns for this bin
return -scalars["UC_TopView_total_counts"] # minimize -> maximize counts
The built-in evaluators in geecs_scanner.optimization.evaluators cover the
common cases:
| Class | Objective |
|---|---|
MaxCountsEvaluator |
Total counts on a camera diagnostic |
BeamSizeEvaluator |
Beam size (FWHM) |
BeamPositionEvaluator |
Beam centroid position |
BeamPositionSimulationEvaluator |
Synthetic centroid from a ray-tracing model — no camera needed (used to exercise the loop on hardware) |
EBeamSourceOpt |
E-beam source optimization (mag-spec) |
device_requirements is auto-generated
You normally do not write a device_requirements block. The optimizer
derives it from the diagnostics in analyzers, so the cameras those
diagnostics need are saved automatically. (See the gotcha below for the one
case this doesn't cover.)
BAX alignment¶
BAX (Bayesian Algorithm Execution) is a different kind of generator used for alignment: rather than optimizing a measured objective directly, it builds a model of one or more observables and computes a virtual objective from them — here, minimizing the slope of a beam-position observable as a "measurement" variable is swept.
The key differences from a normal optimization:
- The VOCS is observables-only — it declares
observables, notobjectives. The optimization target is the algorithm's virtual objective, so the generator requires zero objectives. - The evaluator returns observables (e.g.
x_CoM), not anf. - The probe behavior is configured under
xopt_config_overrides.
A worked BAX-alignment config (driving real correctors, with a synthetic centroid so the full control loop can be exercised without the beam diagnostic):
vocs:
variables:
U_S1V:Current: [-4, 4] # control (the BAX "S")
U_EMQTripletBipolar:Current_Limit.Ch1: [1.2, 1.7] # measurement (the BAX "M")
observables:
- x_CoM # no `objectives:` block
evaluator:
module: geecs_scanner.optimization.evaluators.beam_position_evaluator_simulation
class: BeamPositionSimulationEvaluator
kwargs:
control_variable_name: U_S1V:Current
measurement_variable_name: U_EMQTripletBipolar:Current_Limit.Ch1
generator:
name: multipoint_bax_alignment_l2
xopt_config_overrides:
multipoint_bax_alignment_l2:
control_names: ["U_S1V:Current"]
measurement_name: "U_EMQTripletBipolar:Current_Limit.Ch1"
observable_names: ["x_CoM"]
probe_nominal: 1.5
probe_grid_absolute: [-0.25, 0.0, +0.25]
n_control_mesh: 21
mesh_measurement: true
n_measurement_mesh: 5
n_monte_carlo_samples: 32
use_low_noise_prior: false
Seeding and finishing¶
- Warm-start a run from previous data by listing prior dumps under
seed_dump_files. Each scan writes anxopt_dump.yamlinto its scan folder; point a new run at one (or several) to seed the model. Compatibility is checked: variable, objective, and observable names must match. - Move to best by setting
move_to_best_on_finish: true. At scan end the scanner moves devices to the best observed (feasible, non-errored) setpoint. For observables-only BAX runs there is no single "best objective" row, so the devices fall back to their initial state.
Gotchas¶
BAX VOCS must be observables-only
A multipoint_bax_alignment* generator requires no objectives: block —
only observables:. Leaving an objectives: block in a BAX config will fail
at generator construction. (Conversely, every non-BAX generator requires an
objective.)
Simulation / observables-only runs still need data logged
Evaluators read the acquired bin of shot data. If your scan has no save
devices, nothing is logged and the evaluator fails with a cryptic
KeyError: 'Elapsed Time'. A synthetic evaluator (e.g.
BeamPositionSimulationEvaluator) does not pull in any cameras, so you must
add the control/measurement devices to your save elements yourself so
their setpoints are recorded.
Diagnostics must match the current schema
Diagnostics referenced under analyzers are validated against the current
ImageAnalysis diagnostic schema (extra="forbid"). A stale diagnostic YAML
— camera fields at the top level, or a missing image_analyzer — fails with
a DiagnosticAnalysisConfig validation error. Update the diagnostic in the
configs repo to the current image: / scan: shape.
Where to next¶
- Base Optimizer API — the wrapper class and its methods
- Base Evaluator API — writing evaluators
- Generator Factory API — the full generator registry
- Basic Optimization Setup — runnable example notebook