2High-level ParameterOptimization interface for PyHelios.
4This module provides a user-friendly interface to the parameter optimization
5plugin, which calibrates named model parameters against a user-supplied
6objective function using either population-based search (genetic algorithm,
7Bayesian optimization, CMA-ES) or local optimization (Adam, L-BFGS, BOBYQA).
10 >>> from pyhelios import ParameterOptimization, Parameter, GeneticAlgorithm
13 ... return (p["x"] - 3.0) ** 2 + (p["y"] + 1.0) ** 2
15 >>> with ParameterOptimization() as opt:
16 ... opt.setAlgorithm(GeneticAlgorithm(generations=100, random_seed=1))
17 ... result = opt.run(objective, {
18 ... "x": Parameter.continuous(0.0, -5.0, 5.0),
19 ... "y": Parameter.continuous(0.0, -5.0, 5.0),
21 >>> round(result["x"], 1), round(result["y"], 1)
27from dataclasses
import dataclass, field
28from enum
import IntEnum
29from typing
import Callable, Dict, Mapping, Optional, Sequence, Union
31from .plugins.registry
import get_plugin_registry
32from .wrappers
import UParameterOptimizationWrapper
as paramopt_wrapper
33from .exceptions
import HeliosError
35logger = logging.getLogger(__name__)
39 """Exception raised for ParameterOptimization-specific errors."""
44 """Kind of an optimizable parameter."""
46 FLOAT = paramopt_wrapper.PARAM_FLOAT
47 INTEGER = paramopt_wrapper.PARAM_INTEGER
48 CATEGORICAL = paramopt_wrapper.PARAM_CATEGORICAL
54 A single optimizable parameter.
58 min: Lower bound. Ignored for CATEGORICAL parameters.
59 max: Upper bound. Ignored for CATEGORICAL parameters.
61 categories: Allowed values, required for CATEGORICAL parameters
67 type: ParameterType = ParameterType.FLOAT
68 categories: Sequence[float] = ()
71 def continuous(cls, value: float, min: float, max: float) ->
"Parameter":
72 """Create a continuous parameter bounded by [min, max]."""
73 return cls(value=value, min=min, max=max, type=ParameterType.FLOAT)
76 def integer(cls, value: float, min: float, max: float) ->
"Parameter":
77 """Create an integer parameter bounded by [min, max]."""
78 return cls(value=value, min=min, max=max, type=ParameterType.INTEGER)
81 def categorical(cls, value: float, categories: Sequence[float]) ->
"Parameter":
82 """Create a parameter restricted to an explicit set of values."""
83 return cls(value=value, min=0.0, max=0.0,
84 type=ParameterType.CATEGORICAL, categories=tuple(categories))
91@dataclass(frozen=True)
93 """Component-wise blend crossover."""
97@dataclass(frozen=True)
99 """Blend crossover in PCA-transformed space, for non-separable problems."""
101 pca_update_interval: int = 5
104@dataclass(frozen=True)
106 """Per-gene Gaussian mutation."""
110@dataclass(frozen=True)
112 """Isotropic Gaussian mutation applied to all genes together."""
116@dataclass(frozen=True)
118 """Mixture of PCA-Gaussian, PCA-Cauchy, and random-direction mutation."""
120 pca_update_interval: int = 5
121 sigma_pca: float = 0.25
122 gamma_cauchy: float = 0.1
123 sigma_random: float = 0.3
124 pca_gaussian_prob: float = 0.70
125 pca_cauchy_prob: float = 0.20
128CrossoverOperator = Union[BLXAlphaCrossover, BLXPCACrossover]
129MutationOperator = Union[PerGeneMutation, IsotropicMutation, HybridMutation]
142 Genetic algorithm settings.
144 Handles continuous, integer, and categorical parameters, and needs no
145 gradient. A good default when the search space is large or poorly behaved.
148 generations: int = 100
149 population_size: int = 20
150 crossover_rate: float = 0.5
151 elitism_rate: float = 0.05
153 crossover: CrossoverOperator = field(default_factory=BLXAlphaCrossover)
154 mutation: MutationOperator = field(default_factory=PerGeneMutation)
158 if struct.crossover_kind == paramopt_wrapper.CROSSOVER_BLX_PCA:
160 alpha=struct.crossover_alpha,
161 pca_update_interval=struct.crossover_pca_update_interval)
165 if struct.mutation_kind == paramopt_wrapper.MUTATION_ISOTROPIC:
167 elif struct.mutation_kind == paramopt_wrapper.MUTATION_HYBRID:
169 rate=struct.mutation_rate,
170 pca_update_interval=struct.mutation_pca_update_interval,
171 sigma_pca=struct.mutation_sigma_pca,
172 gamma_cauchy=struct.mutation_gamma_cauchy,
173 sigma_random=struct.mutation_sigma_random,
174 pca_gaussian_prob=struct.mutation_pca_gaussian_prob,
175 pca_cauchy_prob=struct.mutation_pca_cauchy_prob)
180 generations=struct.generations,
181 population_size=struct.population_size,
182 crossover_rate=struct.crossover_rate,
183 elitism_rate=struct.elitism_rate,
184 random_seed=struct.random_seed,
189 def explore(cls) -> "GeneticAlgorithm":
190 """Exploration-biased preset: large population, high mutation."""
191 return cls.
_from_struct(paramopt_wrapper.getGeneticAlgorithmExplore())
194 def exploit(cls) -> "GeneticAlgorithm":
195 """Exploitation-biased preset: smaller population, low mutation."""
196 return cls.
_from_struct(paramopt_wrapper.getGeneticAlgorithmExploit())
199 struct = paramopt_wrapper.PyHeliosGeneticAlgorithm()
206 if isinstance(self.
crossover, BLXPCACrossover):
207 struct.crossover_kind = paramopt_wrapper.CROSSOVER_BLX_PCA
208 struct.crossover_alpha = self.
crossover.alpha
209 struct.crossover_pca_update_interval = self.
crossover.pca_update_interval
210 elif isinstance(self.
crossover, BLXAlphaCrossover):
211 struct.crossover_kind = paramopt_wrapper.CROSSOVER_BLX_ALPHA
212 struct.crossover_alpha = self.
crossover.alpha
213 struct.crossover_pca_update_interval = 5
216 f
"crossover must be BLXAlphaCrossover or BLXPCACrossover, "
217 f
"got {type(self.crossover).__name__}")
220 struct.mutation_pca_update_interval = 5
221 struct.mutation_sigma_pca = 0.25
222 struct.mutation_gamma_cauchy = 0.1
223 struct.mutation_sigma_random = 0.3
224 struct.mutation_pca_gaussian_prob = 0.70
225 struct.mutation_pca_cauchy_prob = 0.20
227 if isinstance(self.
mutation, HybridMutation):
228 struct.mutation_kind = paramopt_wrapper.MUTATION_HYBRID
229 struct.mutation_rate = self.
mutation.rate
230 struct.mutation_pca_update_interval = self.
mutation.pca_update_interval
231 struct.mutation_sigma_pca = self.
mutation.sigma_pca
232 struct.mutation_gamma_cauchy = self.
mutation.gamma_cauchy
233 struct.mutation_sigma_random = self.
mutation.sigma_random
234 struct.mutation_pca_gaussian_prob = self.
mutation.pca_gaussian_prob
235 struct.mutation_pca_cauchy_prob = self.
mutation.pca_cauchy_prob
236 elif isinstance(self.
mutation, IsotropicMutation):
237 struct.mutation_kind = paramopt_wrapper.MUTATION_ISOTROPIC
238 struct.mutation_rate = self.
mutation.rate
239 elif isinstance(self.
mutation, PerGeneMutation):
240 struct.mutation_kind = paramopt_wrapper.MUTATION_PER_GENE
241 struct.mutation_rate = self.
mutation.rate
244 f
"mutation must be PerGeneMutation, IsotropicMutation, or HybridMutation, "
245 f
"got {type(self.mutation).__name__}")
253 Bayesian optimization with a Gaussian process surrogate.
255 Suited to expensive objectives where the evaluation budget is small.
258 max_evaluations: int = 100
259 initial_samples: int = 0
260 ucb_kappa: float = 2.0
261 max_gp_samples: int = 200
262 acquisition_samples: int = 1000
268 max_evaluations=struct.max_evaluations,
269 initial_samples=struct.initial_samples,
270 ucb_kappa=struct.ucb_kappa,
271 max_gp_samples=struct.max_gp_samples,
272 acquisition_samples=struct.acquisition_samples,
273 random_seed=struct.random_seed)
276 def explore(cls) -> "BayesianOptimization":
277 """Exploration-biased preset: high kappa, many acquisition samples."""
278 return cls.
_from_struct(paramopt_wrapper.getBayesianExplore())
281 def exploit(cls) -> "BayesianOptimization":
282 """Exploitation-biased preset: low kappa."""
283 return cls.
_from_struct(paramopt_wrapper.getBayesianExploit())
286 struct = paramopt_wrapper.PyHeliosBayesianOptimization()
299 Covariance Matrix Adaptation Evolution Strategy.
301 Strong general-purpose choice for continuous, non-separable problems.
304 max_evaluations: int = 200
312 max_evaluations=struct.max_evaluations,
313 lambda_=struct.lambda_,
315 random_seed=struct.random_seed)
319 """Exploration-biased preset: large initial step size."""
320 return cls.
_from_struct(paramopt_wrapper.getCMAESExplore())
324 """Exploitation-biased preset: small initial step size."""
325 return cls.
_from_struct(paramopt_wrapper.getCMAESExploit())
328 struct = paramopt_wrapper.PyHeliosCMAES()
331 struct.sigma = self.
sigma
339 AdamW gradient-based optimization.
341 Noise-tolerant and dependency-free. Requires a gradient, either supplied
342 directly or estimated with ``finite_difference=True``.
345 max_iterations: int = 200
346 learning_rate: float = 0.01
349 epsilon: float = 1e-8
350 weight_decay: float = 0.0
351 ftol_rel: float = 1e-6
352 xtol_rel: float = 1e-6
355 struct = paramopt_wrapper.PyHeliosAdam()
358 struct.beta1 = self.
beta1
359 struct.beta2 = self.
beta2
370 L-BFGS gradient-based optimization.
372 Requires an NLopt build that includes the LGPL Luksan solvers. PyHelios
373 builds without them by default to keep the distributed library MIT-licensed,
374 so this is typically unavailable -- use :class:`Adam` or :class:`BOBYQA`.
377 max_iterations: int = 200
378 ftol_rel: float = 1e-6
379 xtol_rel: float = 1e-6
380 verify_gradients: bool =
False
381 fd_step: float = 1e-5
384 struct = paramopt_wrapper.PyHeliosLBFGS()
396 BOBYQA derivative-free local optimization. Requires NLopt.
398 Builds a local quadratic model from function values alone -- a good choice
399 for polishing a population-based result or for noisy black-box objectives.
402 max_iterations: int = 200
403 ftol_rel: float = 1e-6
404 xtol_rel: float = 1e-6
405 initial_step: float = 0.0
408 struct = paramopt_wrapper.PyHeliosBOBYQA()
419 SLSQP gradient-based optimization. Requires NLopt and a gradient.
421 Note that PyHelios does not currently expose the plugin's nonlinear
422 constraint support, so this behaves as an unconstrained local optimizer.
425 max_iterations: int = 200
426 ftol_rel: float = 1e-6
427 xtol_rel: float = 1e-6
430 struct = paramopt_wrapper.PyHeliosSLSQP()
437AlgorithmSettings = Union[GeneticAlgorithm, BayesianOptimization, CMAES,
438 Adam, LBFGS, BOBYQA, SLSQP]
448 GeneticAlgorithm: (
"GA", paramopt_wrapper.setGeneticAlgorithm,
False,
True),
449 BayesianOptimization: (
"BO", paramopt_wrapper.setBayesianOptimization,
False,
False),
450 CMAES: (
"CMAES", paramopt_wrapper.setCMAES,
False,
False),
451 Adam: (
"ADAM", paramopt_wrapper.setAdam,
True,
False),
452 LBFGS: (
"LBFGS", paramopt_wrapper.setLBFGS,
True,
False),
453 BOBYQA: (
"BOBYQA", paramopt_wrapper.setBOBYQA,
False,
False),
454 SLSQP: (
"SLSQP", paramopt_wrapper.setSLSQP,
True,
False),
458@dataclass(frozen=True)
461 One evaluation of a constrained simulation.
463 Returned by the callable passed to :meth:`ParameterOptimization.runConstrained`,
464 which computes the objective, the constraints, and every gradient in a single
465 pass. The optimizer caches this per parameter point, so a simulation that runs a
466 full Helios scene is evaluated once rather than once per constraint.
469 objective: Scalar cost to minimize
470 objective_gradient: Partial derivative of the objective for every parameter
471 constraints: Constraint values; constraint i is satisfied when its value is <= 0
472 constraint_gradients: One gradient mapping per constraint, in the same order
476 objective_gradient: Dict[str, float]
477 constraints: Sequence[float]
478 constraint_gradients: Sequence[Dict[str, float]]
481@dataclass(frozen=True)
484 Outcome of an optimization run.
486 Holds full :class:`Parameter` objects rather than bare floats so a result
487 can be fed straight back into another run, e.g. refining a CMA-ES result
491 parameters: Dict[str, Parameter]
495 def values(self) -> Dict[str, float]:
496 """The optimized values, as a plain name-to-value mapping."""
497 return {name: parameter.value
for name, parameter
in self.
parameters.items()}
500 """Get one optimized value by parameter name."""
505 objective: Callable[[Dict[str, float]], float],
506 objective_gradient: Callable[[Dict[str, float]], Dict[str, float]],
507 constraints: Sequence[tuple]) -> Callable[[Dict[str, float]], ConstrainedResult]:
509 Compose separate objective and constraint callables into one simulation.
511 A convenience for problems whose constraints really are independent functions.
512 Note that it calls every function at each parameter point, so it forfeits the
513 single-pass advantage of writing one combined simulation: if computing the
514 objective and the constraints shares expensive work -- as it does when they come
515 from one Helios simulation -- write a :class:`ConstrainedResult` directly instead.
518 objective: Callable receiving ``{name: value}`` and returning a scalar cost
519 objective_gradient: Callable returning ``{name: partial derivative}``
520 constraints: Sequence of ``(function, gradient)`` pairs, each satisfied when
521 ``function(params) <= 0``
524 A callable suitable for :meth:`ParameterOptimization.runConstrained`
527 >>> simulation = make_constrained_simulation(
528 ... lambda p: p["x"] ** 2,
529 ... lambda p: {"x": 2 * p["x"]},
530 ... [(lambda p: 1.0 - p["x"], lambda p: {"x": -1.0})])
532 if not callable(objective):
533 raise TypeError(f
"objective must be callable, got {type(objective).__name__}")
534 if not callable(objective_gradient):
536 f
"objective_gradient must be callable, got {type(objective_gradient).__name__}")
538 pairs = list(constraints)
539 for index, pair
in enumerate(pairs):
540 if not isinstance(pair, tuple)
or len(pair) != 2:
542 f
"constraints[{index}] must be a (function, gradient) tuple, "
543 f
"got {type(pair).__name__}")
544 if not all(callable(item)
for item
in pair):
545 raise TypeError(f
"Both entries of constraints[{index}] must be callable")
547 def simulation(params: Dict[str, float]) -> ConstrainedResult:
549 objective=objective(params),
550 objective_gradient=objective_gradient(params),
551 constraints=[function(params)
for function, _
in pairs],
552 constraint_gradients=[gradient(params)
for _, gradient
in pairs],
560 Optimize named model parameters against an objective function.
562 The objective receives a ``{name: value}`` dict and returns a scalar cost to
563 minimize. It may close over a :class:`~pyhelios.Context` and run a full
564 Helios simulation; the plugin itself takes no Context.
566 This class requires the native Helios library built with the
567 ``parameteroptimization`` plugin. Use it as a context manager so the C++
568 instance is released promptly.
571 >>> with ParameterOptimization() as opt:
572 ... opt.setAlgorithm(CMAES(max_evaluations=200, random_seed=1))
573 ... result = opt.run(objective, {"x": Parameter.continuous(0.0, -5.0, 5.0)})
574 ... print(result.fitness, result["x"])
579 Create a ParameterOptimization instance.
582 ParameterOptimizationError: If the plugin is unavailable in this build
587 registry = get_plugin_registry()
588 if not registry.is_plugin_available(
'parameteroptimization'):
589 available_plugins = registry.get_available_plugins()
591 "ParameterOptimization requires the 'parameteroptimization' plugin "
592 "which is not available.\n\n"
593 "To enable parameter optimization:\n"
594 "1. Rebuild PyHelios with all plugins:\n"
595 " build_scripts/build_helios --clean\n"
596 "2. Or select the plugin explicitly:\n"
597 " build_scripts/build_helios --plugins parameteroptimization\n\n"
598 "System requirements:\n"
599 " - Platforms: Windows, Linux, macOS\n"
600 " - Dependencies: none (NLopt is bundled)\n"
601 " - GPU: not required\n\n"
602 f
"Currently available plugins: {available_plugins}"
606 self.
optimizer = paramopt_wrapper.createParameterOptimization()
609 "Failed to create ParameterOptimization instance.")
610 except ParameterOptimizationError:
612 except Exception
as e:
614 f
"Failed to initialize ParameterOptimization: {e}")
617 """Context manager entry."""
620 def __exit__(self, exc_type, exc_value, traceback):
621 """Context manager exit with proper cleanup."""
624 paramopt_wrapper.destroyParameterOptimization(self.
optimizer)
625 logger.debug(
"ParameterOptimization destroyed successfully")
626 except Exception
as e:
627 logger.warning(f
"Error destroying ParameterOptimization: {e}")
632 """Destructor to ensure C++ resources freed even without 'with' statement."""
633 if hasattr(self,
'optimizer')
and self.
optimizer is not None:
635 paramopt_wrapper.destroyParameterOptimization(self.
optimizer)
637 except Exception
as e:
639 warnings.warn(f
"Error in ParameterOptimization.__del__: {e}")
642 """Get the native pointer for advanced operations."""
646 """Raise if this instance has already been destroyed."""
649 "ParameterOptimization has been destroyed and can no longer be used.")
658 Report which algorithms can run in this build.
660 L-BFGS, BOBYQA and SLSQP depend on NLopt, and L-BFGS additionally on the
661 LGPL Luksan solvers, which PyHelios disables by default.
664 Mapping of algorithm name to availability
666 names = [
"GA",
"BO",
"CMAES",
"ADAM",
"LBFGS",
"BOBYQA",
"SLSQP"]
667 return {name: paramopt_wrapper.isAlgorithmAvailable(name)
for name
in names}
669 def setAlgorithm(self, algorithm: AlgorithmSettings) ->
None:
671 Select the optimization algorithm and its hyperparameters.
673 If never called, the plugin picks a default based on the parameter types
674 and whether a gradient was supplied.
677 algorithm: One of the algorithm settings dataclasses
680 TypeError: If algorithm is not a recognized settings type
681 ParameterOptimizationError: If the algorithm is unavailable in this build
685 info = _ALGORITHM_INFO.get(type(algorithm))
688 f
"algorithm must be one of "
689 f
"{', '.join(cls.__name__ for cls in _ALGORITHM_INFO)}, "
690 f
"got {type(algorithm).__name__}")
692 native_name, setter, _, _ = info
696 if not paramopt_wrapper.isAlgorithmAvailable(native_name):
698 f
"The {type(algorithm).__name__} algorithm is not available in this build.\n\n"
699 f
"{native_name} is provided by NLopt. PyHelios builds NLopt without the "
700 f
"LGPL-licensed Luksan solvers so the distributed library stays "
701 f
"MIT-licensed, which makes L-BFGS unavailable.\n\n"
702 f
"Alternatives that are always available:\n"
703 f
" - Adam: gradient-based, noise-tolerant\n"
704 f
" - BOBYQA: derivative-free local optimization (needs NLopt)\n"
705 f
" - CMAES / GeneticAlgorithm: population-based global search\n\n"
706 f
"To enable it anyway, rebuild helios-core with -DHELIOS_NLOPT_LUKSAN=ON.")
709 setter(self.
optimizer, algorithm._to_struct())
710 except (ParameterOptimizationError, ValueError, TypeError):
712 except Exception
as e:
723 Enable or disable the plugin's progress printout to stdout.
726 enable: True to print progress during optimization
729 paramopt_wrapper.setPrintProgress(self.
optimizer, bool(enable))
733 Write the final result to a CSV file.
736 path: Output path ending in .csv or .txt; None disables writing
739 paramopt_wrapper.setResultFile(self.
optimizer, path)
743 Write per-generation progress to a CSV file.
746 path: Output path ending in .csv or .txt; None disables writing
749 paramopt_wrapper.setProgressFile(self.
optimizer, path)
753 Read the initial parameter set from a file.
755 Only the genetic algorithm consults this file.
758 path: Headerless CSV of "name,value,min,max" rows; None disables reading
761 paramopt_wrapper.setInputFile(self.
optimizer, path)
768 objective: Callable[[Dict[str, float]], float],
769 parameters: Mapping[str, Parameter],
770 gradient: Optional[Callable[[Dict[str, float]], Dict[str, float]]] =
None,
772 finite_difference: bool =
False,
773 fd_step: float = 0.0) -> OptimizationResult:
775 Run the optimization.
778 objective: Callable receiving {name: value} and returning a scalar cost
779 to minimize. Invoked once per candidate parameter set.
780 parameters: Parameters to optimize, keyed by name
781 gradient: Optional callable receiving {name: value} and returning
782 {name: partial derivative} for every parameter. Required by
783 Adam, L-BFGS, and SLSQP unless finite_difference is used.
784 finite_difference: Estimate the gradient by centered finite differences
785 instead of supplying one. Costs 2N extra objective
786 evaluations per gradient.
787 fd_step: Relative perturbation for finite differences; 0 uses the default
790 The optimized parameters and the objective value at the optimum
793 ValueError: If the arguments are invalid
794 TypeError: If objective or gradient is not callable
795 ParameterOptimizationError: If the optimization fails
798 An exception raised inside the objective aborts the run and is
799 re-raised here with its original traceback. Partial results are not
804 if not callable(objective):
805 raise TypeError(f
"objective must be callable, got {type(objective).__name__}")
806 if gradient
is not None and not callable(gradient):
807 raise TypeError(f
"gradient must be callable, got {type(gradient).__name__}")
808 if gradient
is not None and finite_difference:
810 "Pass either a gradient function or finite_difference=True, not both.")
814 algorithm = getattr(self,
'_algorithm',
None)
815 if algorithm
is not None:
816 _, _, needs_gradient, _ = _ALGORITHM_INFO[type(algorithm)]
818 if needs_gradient
and gradient
is None and not finite_difference:
820 f
"{type(algorithm).__name__} is a gradient-based algorithm and requires "
821 f
"a gradient. Either pass gradient=<callable>, or pass "
822 f
"finite_difference=True to estimate it numerically.")
828 "ParameterOptimization.run() was called from inside its own objective "
829 "function. The optimizer is not reentrant; use a separate instance.")
833 if gradient
is not None:
834 values, fitness = paramopt_wrapper.runOptimizationWithGradient(
835 self.
optimizer, specs, objective, gradient)
836 elif finite_difference:
837 values, fitness = paramopt_wrapper.runOptimizationWithFDGradient(
838 self.
optimizer, specs, objective, fd_step)
840 values, fitness = paramopt_wrapper.runOptimization(
848 name:
Parameter(value=values[name], min=parameter.min, max=parameter.max,
849 type=parameter.type, categories=parameter.categories)
850 for name, parameter
in parameters.items()
855 simulation: Callable[[Dict[str, float]], ConstrainedResult],
856 parameters: Mapping[str, Parameter],
858 constraint_count: int) -> OptimizationResult:
860 Run a constrained optimization: minimize f(x) subject to c_i(x) <= 0.
862 Requires ``setAlgorithm(SLSQP(...))``. SLSQP is the only algorithm in the
863 plugin that handles nonlinear inequality constraints, and it needs every
864 parameter to be ``FLOAT``.
866 The simulation returns the objective, the constraints, and all gradients
867 together. The optimizer caches each result, so the simulation runs once per
868 parameter point no matter how many constraints there are -- which is what
869 makes this practical for objectives that run a full Helios simulation.
872 simulation: Callable receiving ``{name: value}`` and returning a
873 :class:`ConstrainedResult`
874 parameters: Parameters to optimize, keyed by name
875 constraint_count: Number of constraints. Required, and fixed for the whole run: the buffers the simulation writes into are sized before the first call, so the count cannot be discovered by calling it.
878 The optimized parameters and the objective value at the optimum
881 ValueError: If the arguments are invalid, or the selected algorithm is
883 TypeError: If simulation is not callable
884 ParameterOptimizationError: If the optimization fails
887 Constraints are satisfied to the plugin's tolerance rather than exactly.
888 Check feasibility of the returned parameters if it matters.
891 >>> # minimize x^2 + y^2 subject to x + y >= 1
892 >>> def simulation(p):
893 ... return ConstrainedResult(
894 ... objective=p["x"] ** 2 + p["y"] ** 2,
895 ... objective_gradient={"x": 2 * p["x"], "y": 2 * p["y"]},
896 ... constraints=[1.0 - p["x"] - p["y"]],
897 ... constraint_gradients=[{"x": -1.0, "y": -1.0}],
899 >>> with ParameterOptimization() as opt:
900 ... opt.setAlgorithm(SLSQP())
901 ... result = opt.runConstrained(
903 ... {"x": Parameter.continuous(0.0, -5.0, 5.0),
904 ... "y": Parameter.continuous(0.0, -5.0, 5.0)},
905 ... constraint_count=1)
909 if not callable(simulation):
911 f
"simulation must be callable, got {type(simulation).__name__}")
912 if not isinstance(constraint_count, int)
or isinstance(constraint_count, bool):
914 f
"constraint_count must be an int, got {type(constraint_count).__name__}")
915 if constraint_count < 1:
917 f
"constraint_count must be at least 1, got {constraint_count}. "
918 f
"Use run() when there are no constraints.")
924 algorithm = getattr(self,
'_algorithm',
None)
925 if not isinstance(algorithm, SLSQP):
926 selected = type(algorithm).__name__
if algorithm
is not None else "none"
928 f
"Constrained optimization requires SLSQP, but the selected algorithm is "
929 f
"{selected}. Call setAlgorithm(SLSQP(...)) first.\n\n"
930 f
"SLSQP is the only algorithm in the plugin that supports nonlinear "
931 f
"inequality constraints. For the others, fold the constraint into the "
932 f
"objective as a penalty term and use run().")
938 "ParameterOptimization.runConstrained() was called from inside its own "
939 "simulation function. The optimizer is not reentrant; use a separate "
944 values, fitness = paramopt_wrapper.runOptimizationConstrained(
945 self.
optimizer, specs, simulation, constraint_count)
950 name:
Parameter(value=values[name], min=parameter.min, max=parameter.max,
951 type=parameter.type, categories=parameter.categories)
952 for name, parameter
in parameters.items()
959 Check the parameter mapping and flatten it for the wrapper.
961 Only conditions that are memory-safety preconditions, or that produce a
962 materially better message here than from C++, are checked. Bound
963 consistency (min == max, min > max, NaN bounds, empty categories) is left
964 to the plugin's own validation so the two cannot drift apart.
966 if not isinstance(parameters, Mapping):
968 f
"parameters must be a mapping of name to Parameter, "
969 f
"got {type(parameters).__name__}")
971 raise ValueError(
"parameters cannot be empty")
974 for name, parameter
in parameters.items():
975 if not isinstance(name, str):
977 f
"Parameter names must be strings, got {type(name).__name__}: {name!r}")
979 raise ValueError(
"Parameter names cannot be empty")
983 raise ValueError(f
"Parameter name {name!r} cannot contain a null character")
984 if not isinstance(parameter, Parameter):
986 f
"Parameter '{name}' must be a Parameter, "
987 f
"got {type(parameter).__name__}")
989 for attribute
in (
"value",
"min",
"max"):
990 attr_value = getattr(parameter, attribute)
991 if not isinstance(attr_value, (int, float))
or isinstance(attr_value, bool):
993 f
"Parameter '{name}' field '{attribute}' must be numeric, "
994 f
"got {type(attr_value).__name__}")
996 categories = tuple(parameter.categories
or ())
997 if parameter.type == ParameterType.CATEGORICAL
and not categories:
999 f
"Parameter '{name}' is CATEGORICAL and must define at least one "
1000 f
"allowed value via 'categories'")
1001 for category
in categories:
1002 if not isinstance(category, (int, float))
or isinstance(category, bool):
1004 f
"Parameter '{name}' has a non-numeric category: {category!r}")
1005 if not math.isfinite(float(category)):
1007 f
"Parameter '{name}' has a non-finite category: {category!r}")
1011 "value": float(parameter.value),
1012 "min": float(parameter.min),
1013 "max": float(parameter.max),
1014 "type": int(parameter.type),
1015 "categories": [float(c)
for c
in categories],
1022 parameters: Mapping[str,
"Parameter"],
1023 algorithm:
"AlgorithmSettings") ->
None:
1025 Reject discrete parameters given to an algorithm that cannot handle them.
1027 Only the genetic algorithm implements INTEGER and CATEGORICAL parameters.
1028 The rest search a continuous space, so a discrete parameter would be
1029 optimized as a plain float and the result would not be a whole number, or
1030 not one of the allowed categories.
1032 helios-core enforces this for L-BFGS, Adam, BOBYQA and SLSQP, but not for
1033 CMA-ES or Bayesian optimization, where a CATEGORICAL parameter instead
1034 collapses to 0.0 with no diagnostic: its min and max are documented as
1035 ignored and so are conventionally left at zero, which those two algorithms
1036 read as the bounds [0, 0]. Checking here covers that gap on every core
1037 version, and reports the parameter and the remedy rather than leaving the
1038 message to the layer that happens to catch it first.
1040 info = _ALGORITHM_INFO.get(type(algorithm))
1041 if info
is None or info[3]:
1046 for name, parameter
in parameters.items()
1047 if isinstance(parameter, Parameter)
1048 and parameter.type != ParameterType.FLOAT
1053 listed =
", ".join(f
"'{name}' is {type_name}" for name, type_name
in discrete)
1055 f
"{type(algorithm).__name__} requires every parameter to be FLOAT, "
1056 f
"but {listed}.\n\n"
1057 f
"GeneticAlgorithm is the only algorithm that supports INTEGER and "
1058 f
"CATEGORICAL parameters. Either switch to it, or make the parameter "
1059 f
"continuous with Parameter.continuous(...).")
1062 """Check if the parameteroptimization plugin is available in this build."""
1063 return get_plugin_registry().is_plugin_available(
'parameteroptimization')
AdamW gradient-based optimization.
Component-wise blend crossover.
Blend crossover in PCA-transformed space, for non-separable problems.
BOBYQA derivative-free local optimization.
Bayesian optimization with a Gaussian process surrogate.
"BayesianOptimization" _from_struct(cls, struct)
"BayesianOptimization" exploit(cls)
Exploitation-biased preset: low kappa.
"BayesianOptimization" explore(cls)
Exploration-biased preset: high kappa, many acquisition samples.
Covariance Matrix Adaptation Evolution Strategy.
"CMAES" _from_struct(cls, struct)
"CMAES" explore(cls)
Exploration-biased preset: large initial step size.
"CMAES" exploit(cls)
Exploitation-biased preset: small initial step size.
One evaluation of a constrained simulation.
Genetic algorithm settings.
"GeneticAlgorithm" exploit(cls)
Exploitation-biased preset: smaller population, low mutation.
MutationOperator mutation
CrossoverOperator crossover
"GeneticAlgorithm" _from_struct(cls, struct)
"GeneticAlgorithm" explore(cls)
Exploration-biased preset: large population, high mutation.
Mixture of PCA-Gaussian, PCA-Cauchy, and random-direction mutation.
Isotropic Gaussian mutation applied to all genes together.
L-BFGS gradient-based optimization.
Outcome of an optimization run.
float values
The optimized values, as a plain name-to-value mapping.
float __getitem__(self, str name)
Get one optimized value by parameter name.
Exception raised for ParameterOptimization-specific errors.
Optimize named model parameters against an objective function.
None setInputFile(self, Optional[str] path)
Read the initial parameter set from a file.
None _validate_parameter_types_for_algorithm(Mapping[str, "Parameter"] parameters, "AlgorithmSettings" algorithm)
Reject discrete parameters given to an algorithm that cannot handle them.
None _check_alive(self)
Raise if this instance has already been destroyed.
OptimizationResult run(self, Callable[[Dict[str, float]], float] objective, Mapping[str, Parameter] parameters, Optional[Callable[[Dict[str, float]], Dict[str, float]]] gradient=None, *, bool finite_difference=False, float fd_step=0.0)
Run the optimization.
list _validate_parameters(Mapping[str, Parameter] parameters)
Check the parameter mapping and flatten it for the wrapper.
None setResultFile(self, Optional[str] path)
Write the final result to a CSV file.
bool is_available(self)
Check if the parameteroptimization plugin is available in this build.
None setPrintProgress(self, bool enable)
Enable or disable the plugin's progress printout to stdout.
None setProgressFile(self, Optional[str] path)
Write per-generation progress to a CSV file.
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
None setAlgorithm(self, AlgorithmSettings algorithm)
Select the optimization algorithm and its hyperparameters.
Dict[str, bool] availableAlgorithms()
Report which algorithms can run in this build.
OptimizationResult runConstrained(self, Callable[[Dict[str, float]], ConstrainedResult] simulation, Mapping[str, Parameter] parameters, *, int constraint_count)
Run a constrained optimization: minimize f(x) subject to c_i(x) <= 0.
getNativePtr(self)
Get the native pointer for advanced operations.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
__enter__(self)
Context manager entry.
Kind of an optimizable parameter.
A single optimizable parameter.
"Parameter" continuous(cls, float value, float min, float max)
Create a continuous parameter bounded by [min, max].
"Parameter" categorical(cls, float value, Sequence[float] categories)
Create a parameter restricted to an explicit set of values.
"Parameter" integer(cls, float value, float min, float max)
Create an integer parameter bounded by [min, max].
Per-gene Gaussian mutation.
SLSQP gradient-based optimization.
Exception classes for PyHelios library.
make_constrained_simulation