![]() |
0.1.33
|
The ParameterOptimization plugin calibrates named model parameters against an objective function you supply. You describe the parameters and their bounds, write a function that scores a candidate parameter set, and the plugin searches for the set that minimizes that score.
The objective is an ordinary Python callable. It can run a full Helios simulation — build geometry, run radiation and energy balance, compare against measurements — and return a scalar error. That makes this plugin the natural way to fit model parameters to observed data.
Six algorithms are available, in two families:
Global search — no gradient required, handles rough or multi-modal objectives:
| Algorithm | Best for |
|---|---|
GeneticAlgorithm | Large or awkward search spaces; searches integer and categorical parameters natively |
CMAES | Continuous, non-separable problems; a strong general-purpose default |
BayesianOptimization | Expensive objectives where you can afford few evaluations |
Local search — refines a good starting point:
| Algorithm | Best for |
|---|---|
Adam | Gradient-based, noise-tolerant, no external dependency |
BOBYQA | Derivative-free polishing of a global-search result |
SLSQP | Gradient-based; the only algorithm supporting nonlinear constraints |
A common pattern is to run a global search first and polish the result with a local one — see Two-stage optimization below.
The plugin is part of the default build:
To check availability:
The objective receives a {name: value} dict and returns a float. The result exposes result["name"] for a single value, result.values for all of them as a plain dict, and result.parameters for the full Parameter objects with their bounds intact.
Replace run_my_helios_simulation with your own model. For a runnable example that drives a real Context, see docs/examples/parameteroptimization_sample.py.
Adam, L-BFGS, and SLSQP need a gradient. Supply one directly:
Or let the plugin estimate it numerically, at a cost of 2N extra objective evaluations per gradient:
Use GeneticAlgorithm for discrete parameters. It is the only algorithm that searches integer and categorical spaces correctly.
Every other algorithm (CMAES, BayesianOptimization, Adam, BOBYQA, SLSQP, L-BFGS) searches a continuous space and requires all parameters to be FLOAT. Passing an INTEGER or CATEGORICAL parameter to one of them raises ValueError naming the parameter and its type:
PyHelios performs this check itself. Older helios-core revisions do not reject discrete parameters in CMAES and BayesianOptimization, where a CATEGORICAL parameter would otherwise optimize to 0.0 — outside its own category list — with no diagnostic.
SLSQP solves problems with nonlinear inequality constraints — "maximize A subject
to E below a budget" — stated directly rather than approximated with a penalty. Each constraint is satisfied when its value is <= 0, so a requirement like x + y >= 1 is written 1 - x - y <= 0.
The simulation returns the objective, the constraints, and every gradient together as a ConstrainedResult. The optimizer caches each result per parameter point, so a simulation that runs a full Helios scene is evaluated once per point no matter how many constraints there are:
constraint_count is required and fixed for the run: the buffers the simulation writes into are sized before it is first called, so the count cannot be discovered by calling it.
Constraints are satisfied to a tolerance rather than exactly. If feasibility matters, check the returned parameters:
When the objective and constraints really are independent functions, make_constrained_simulation composes them — at the cost of calling every function at each parameter point, which forfeits the single-pass advantage above:
Global search finds the right basin; local search refines within it. Because the result carries full Parameter objects, it can be passed straight back in:
GeneticAlgorithm, BayesianOptimization, and CMAES offer explore() and exploit() presets, read directly from the native library so they always match the tuned upstream values:
Genetic algorithm operators can also be selected explicitly:
Output paths must end in .csv or .txt.
An exception raised inside your objective aborts the run and is re-raised with its original type and traceback, so it points at your own code:
KeyboardInterrupt works the same way, so Ctrl-C cleanly stops a long run.
Objective values are checked for finiteness. A NaN or infinity is rejected with a clear error rather than silently corrupting the optimizer's internal state:
Gradient functions must return an entry for every parameter; omissions and unknown keys are both reported by name.
Selecting an unavailable algorithm raises immediately, rather than failing part-way through a long run:
The optimization does not converge. Widen the bounds, increase generations or max_evaluations, or try explore() presets. If the objective is noisy, prefer GeneticAlgorithm or Adam over the derivative-free local methods.
Results differ between runs. The population-based algorithms seed from std::random_device by default. Set random_seed to any non-zero value for reproducibility.
Adam is a gradient-based algorithm and requires a gradient. Pass gradient=<callable> or finite_difference=True.
L-BFGS reports as unavailable. This is expected — see Limitations.
The run is slow. The objective dominates: with a genetic algorithm the plugin calls it up to generations x population_size times. Reduce the evaluation budget, or use BayesianOptimization, which is designed for expensive objectives.
Each objective evaluation crosses from C++ back into Python. That transition costs well under a microsecond and is irrelevant next to any real Helios simulation, but it means the total number of evaluations is what governs runtime. Budget accordingly:
GeneticAlgorithm: at most generations x population_size, and in practice roughly a third to a half of it — individuals carried forward unchanged are not re-evaluated. Treat the product as an upper bound when budgeting.CMAES / BayesianOptimization: exactly max_evaluationsAdam / BOBYQA / SLSQP: up to max_iterations, plus 2N per gradient when using finite_difference=TruerunConstrained: one simulation call per parameter point regardless of how many constraints there are — the result is cached across the optimizer's separate objective and constraint queriesL-BFGS is unavailable in default builds. NLopt's L-BFGS comes from its LGPL-2.1 Luksan sources, which PyHelios omits so the distributed library stays MIT-licensed. Adam covers the same gradient-based use case with better noise tolerance, and BOBYQA covers derivative-free local refinement.
Enabling it means accepting the LGPL obligations for anything you redistribute. The setting is written by the build script, not read from the CMake command line — a -DHELIOS_NLOPT_LUKSAN=ON flag is overridden — so change the set(HELIOS_NLOPT_LUKSAN
OFF ...) line emitted in build_scripts/build_helios.py and rebuild from clean.
Constraints require SLSQP and continuous parameters. No algorithm supports both discrete parameters and nonlinear constraints. For a discrete problem, fold the constraint into the objective as a penalty term and use run():
Note that a penalty does not guarantee the final solution is feasible — check it.
No live progress callback. The plugin reports progress via stdout (setPrintProgress) and CSV files (setProgressFile), not a callback hook. To track progress programmatically, count evaluations inside your own objective.
Partial results are not recoverable. If the objective raises, the run is torn down and no best-so-far value is returned.
One run at a time per instance. The optimizer is stateful and not reentrant; calling run() from inside its own objective raises. Use separate instances.