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 Luksan solvers, which PyHelios
373 does not build by default, so this is typically unavailable -- use :class:`Adam` or :class:`BOBYQA`.
376 max_iterations: int = 200
377 ftol_rel: float = 1e-6
378 xtol_rel: float = 1e-6
379 verify_gradients: bool =
False
380 fd_step: float = 1e-5
383 struct = paramopt_wrapper.PyHeliosLBFGS()
395 BOBYQA derivative-free local optimization. Requires NLopt.
397 Builds a local quadratic model from function values alone -- a good choice
398 for polishing a population-based result or for noisy black-box objectives.
401 max_iterations: int = 200
402 ftol_rel: float = 1e-6
403 xtol_rel: float = 1e-6
404 initial_step: float = 0.0
407 struct = paramopt_wrapper.PyHeliosBOBYQA()
418 SLSQP gradient-based optimization. Requires NLopt and a gradient.
420 Note that PyHelios does not currently expose the plugin's nonlinear
421 constraint support, so this behaves as an unconstrained local optimizer.
424 max_iterations: int = 200
425 ftol_rel: float = 1e-6
426 xtol_rel: float = 1e-6
429 struct = paramopt_wrapper.PyHeliosSLSQP()
436AlgorithmSettings = Union[GeneticAlgorithm, BayesianOptimization, CMAES,
437 Adam, LBFGS, BOBYQA, SLSQP]
447 GeneticAlgorithm: (
"GA", paramopt_wrapper.setGeneticAlgorithm,
False,
True),
448 BayesianOptimization: (
"BO", paramopt_wrapper.setBayesianOptimization,
False,
False),
449 CMAES: (
"CMAES", paramopt_wrapper.setCMAES,
False,
False),
450 Adam: (
"ADAM", paramopt_wrapper.setAdam,
True,
False),
451 LBFGS: (
"LBFGS", paramopt_wrapper.setLBFGS,
True,
False),
452 BOBYQA: (
"BOBYQA", paramopt_wrapper.setBOBYQA,
False,
False),
453 SLSQP: (
"SLSQP", paramopt_wrapper.setSLSQP,
True,
False),
457@dataclass(frozen=True)
460 One evaluation of a constrained simulation.
462 Returned by the callable passed to :meth:`ParameterOptimization.runConstrained`,
463 which computes the objective, the constraints, and every gradient in a single
464 pass. The optimizer caches this per parameter point, so a simulation that runs a
465 full Helios scene is evaluated once rather than once per constraint.
468 objective: Scalar cost to minimize
469 objective_gradient: Partial derivative of the objective for every parameter
470 constraints: Constraint values; constraint i is satisfied when its value is <= 0
471 constraint_gradients: One gradient mapping per constraint, in the same order
475 objective_gradient: Dict[str, float]
476 constraints: Sequence[float]
477 constraint_gradients: Sequence[Dict[str, float]]
480@dataclass(frozen=True)
483 Outcome of an optimization run.
485 Holds full :class:`Parameter` objects rather than bare floats so a result
486 can be fed straight back into another run, e.g. refining a CMA-ES result
490 parameters: Dict[str, Parameter]
494 def values(self) -> Dict[str, float]:
495 """The optimized values, as a plain name-to-value mapping."""
496 return {name: parameter.value
for name, parameter
in self.
parameters.items()}
499 """Get one optimized value by parameter name."""
504 objective: Callable[[Dict[str, float]], float],
505 objective_gradient: Callable[[Dict[str, float]], Dict[str, float]],
506 constraints: Sequence[tuple]) -> Callable[[Dict[str, float]], ConstrainedResult]:
508 Compose separate objective and constraint callables into one simulation.
510 A convenience for problems whose constraints really are independent functions.
511 Note that it calls every function at each parameter point, so it forfeits the
512 single-pass advantage of writing one combined simulation: if computing the
513 objective and the constraints shares expensive work -- as it does when they come
514 from one Helios simulation -- write a :class:`ConstrainedResult` directly instead.
517 objective: Callable receiving ``{name: value}`` and returning a scalar cost
518 objective_gradient: Callable returning ``{name: partial derivative}``
519 constraints: Sequence of ``(function, gradient)`` pairs, each satisfied when
520 ``function(params) <= 0``
523 A callable suitable for :meth:`ParameterOptimization.runConstrained`
526 >>> simulation = make_constrained_simulation(
527 ... lambda p: p["x"] ** 2,
528 ... lambda p: {"x": 2 * p["x"]},
529 ... [(lambda p: 1.0 - p["x"], lambda p: {"x": -1.0})])
531 if not callable(objective):
532 raise TypeError(f
"objective must be callable, got {type(objective).__name__}")
533 if not callable(objective_gradient):
535 f
"objective_gradient must be callable, got {type(objective_gradient).__name__}")
537 pairs = list(constraints)
538 for index, pair
in enumerate(pairs):
539 if not isinstance(pair, tuple)
or len(pair) != 2:
541 f
"constraints[{index}] must be a (function, gradient) tuple, "
542 f
"got {type(pair).__name__}")
543 if not all(callable(item)
for item
in pair):
544 raise TypeError(f
"Both entries of constraints[{index}] must be callable")
546 def simulation(params: Dict[str, float]) -> ConstrainedResult:
548 objective=objective(params),
549 objective_gradient=objective_gradient(params),
550 constraints=[function(params)
for function, _
in pairs],
551 constraint_gradients=[gradient(params)
for _, gradient
in pairs],
559 Optimize named model parameters against an objective function.
561 The objective receives a ``{name: value}`` dict and returns a scalar cost to
562 minimize. It may close over a :class:`~pyhelios.Context` and run a full
563 Helios simulation; the plugin itself takes no Context.
565 This class requires the native Helios library built with the
566 ``parameteroptimization`` plugin. Use it as a context manager so the C++
567 instance is released promptly.
570 >>> with ParameterOptimization() as opt:
571 ... opt.setAlgorithm(CMAES(max_evaluations=200, random_seed=1))
572 ... result = opt.run(objective, {"x": Parameter.continuous(0.0, -5.0, 5.0)})
573 ... print(result.fitness, result["x"])
578 Create a ParameterOptimization instance.
581 ParameterOptimizationError: If the plugin is unavailable in this build
586 registry = get_plugin_registry()
587 if not registry.is_plugin_available(
'parameteroptimization'):
588 available_plugins = registry.get_available_plugins()
590 "ParameterOptimization requires the 'parameteroptimization' plugin "
591 "which is not available.\n\n"
592 "To enable parameter optimization:\n"
593 "1. Rebuild PyHelios with all plugins:\n"
594 " build_scripts/build_helios --clean\n"
595 "2. Or select the plugin explicitly:\n"
596 " build_scripts/build_helios --plugins parameteroptimization\n\n"
597 "System requirements:\n"
598 " - Platforms: Windows, Linux, macOS\n"
599 " - Dependencies: none (NLopt is bundled)\n"
600 " - GPU: not required\n\n"
601 f
"Currently available plugins: {available_plugins}"
605 self.
optimizer = paramopt_wrapper.createParameterOptimization()
608 "Failed to create ParameterOptimization instance.")
609 except ParameterOptimizationError:
611 except Exception
as e:
613 f
"Failed to initialize ParameterOptimization: {e}")
616 """Context manager entry."""
619 def __exit__(self, exc_type, exc_value, traceback):
620 """Context manager exit with proper cleanup."""
623 paramopt_wrapper.destroyParameterOptimization(self.
optimizer)
624 logger.debug(
"ParameterOptimization destroyed successfully")
625 except Exception
as e:
626 logger.warning(f
"Error destroying ParameterOptimization: {e}")
631 """Destructor to ensure C++ resources freed even without 'with' statement."""
632 if hasattr(self,
'optimizer')
and self.
optimizer is not None:
634 paramopt_wrapper.destroyParameterOptimization(self.
optimizer)
636 except Exception
as e:
638 warnings.warn(f
"Error in ParameterOptimization.__del__: {e}")
641 """Get the native pointer for advanced operations."""
645 """Raise if this instance has already been destroyed."""
648 "ParameterOptimization has been destroyed and can no longer be used.")
657 Report which algorithms can run in this build.
659 L-BFGS, BOBYQA and SLSQP depend on NLopt, and L-BFGS additionally on the
660 LGPL Luksan solvers, which PyHelios disables by default.
663 Mapping of algorithm name to availability
665 names = [
"GA",
"BO",
"CMAES",
"ADAM",
"LBFGS",
"BOBYQA",
"SLSQP"]
666 return {name: paramopt_wrapper.isAlgorithmAvailable(name)
for name
in names}
668 def setAlgorithm(self, algorithm: AlgorithmSettings) ->
None:
670 Select the optimization algorithm and its hyperparameters.
672 If never called, the plugin picks a default based on the parameter types
673 and whether a gradient was supplied.
676 algorithm: One of the algorithm settings dataclasses
679 TypeError: If algorithm is not a recognized settings type
680 ParameterOptimizationError: If the algorithm is unavailable in this build
684 info = _ALGORITHM_INFO.get(type(algorithm))
687 f
"algorithm must be one of "
688 f
"{', '.join(cls.__name__ for cls in _ALGORITHM_INFO)}, "
689 f
"got {type(algorithm).__name__}")
691 native_name, setter, _, _ = info
695 if not paramopt_wrapper.isAlgorithmAvailable(native_name):
697 f
"The {type(algorithm).__name__} algorithm is not available in this build.\n\n"
698 f
"{native_name} is provided by NLopt. PyHelios builds NLopt without the "
699 f
"Luksan solvers by default, which makes L-BFGS unavailable.\n\n"
700 f
"Alternatives that are always available:\n"
701 f
" - Adam: gradient-based, noise-tolerant\n"
702 f
" - BOBYQA: derivative-free local optimization (needs NLopt)\n"
703 f
" - CMAES / GeneticAlgorithm: population-based global search\n\n"
704 f
"To enable it, change the set(HELIOS_NLOPT_LUKSAN OFF ...) line in "
705 f
"build_scripts/build_helios.py and rebuild from clean.")
708 setter(self.
optimizer, algorithm._to_struct())
709 except (ParameterOptimizationError, ValueError, TypeError):
711 except Exception
as e:
722 Enable or disable the plugin's progress printout to stdout.
725 enable: True to print progress during optimization
728 paramopt_wrapper.setPrintProgress(self.
optimizer, bool(enable))
732 Write the final result to a CSV file.
735 path: Output path ending in .csv or .txt; None disables writing
738 paramopt_wrapper.setResultFile(self.
optimizer, path)
742 Write per-generation progress to a CSV file.
745 path: Output path ending in .csv or .txt; None disables writing
748 paramopt_wrapper.setProgressFile(self.
optimizer, path)
752 Read the initial parameter set from a file.
754 Only the genetic algorithm consults this file.
757 path: Headerless CSV of "name,value,min,max" rows; None disables reading
760 paramopt_wrapper.setInputFile(self.
optimizer, path)
767 objective: Callable[[Dict[str, float]], float],
768 parameters: Mapping[str, Parameter],
769 gradient: Optional[Callable[[Dict[str, float]], Dict[str, float]]] =
None,
771 finite_difference: bool =
False,
772 fd_step: float = 0.0) -> OptimizationResult:
774 Run the optimization.
777 objective: Callable receiving {name: value} and returning a scalar cost
778 to minimize. Invoked once per candidate parameter set.
779 parameters: Parameters to optimize, keyed by name
780 gradient: Optional callable receiving {name: value} and returning
781 {name: partial derivative} for every parameter. Required by
782 Adam, L-BFGS, and SLSQP unless finite_difference is used.
783 finite_difference: Estimate the gradient by centered finite differences
784 instead of supplying one. Costs 2N extra objective
785 evaluations per gradient.
786 fd_step: Relative perturbation for finite differences; 0 uses the default
789 The optimized parameters and the objective value at the optimum
792 ValueError: If the arguments are invalid
793 TypeError: If objective or gradient is not callable
794 ParameterOptimizationError: If the optimization fails
797 An exception raised inside the objective aborts the run and is
798 re-raised here with its original traceback. Partial results are not
803 if not callable(objective):
804 raise TypeError(f
"objective must be callable, got {type(objective).__name__}")
805 if gradient
is not None and not callable(gradient):
806 raise TypeError(f
"gradient must be callable, got {type(gradient).__name__}")
807 if gradient
is not None and finite_difference:
809 "Pass either a gradient function or finite_difference=True, not both.")
813 algorithm = getattr(self,
'_algorithm',
None)
814 if algorithm
is not None:
815 _, _, needs_gradient, _ = _ALGORITHM_INFO[type(algorithm)]
817 if needs_gradient
and gradient
is None and not finite_difference:
819 f
"{type(algorithm).__name__} is a gradient-based algorithm and requires "
820 f
"a gradient. Either pass gradient=<callable>, or pass "
821 f
"finite_difference=True to estimate it numerically.")
827 "ParameterOptimization.run() was called from inside its own objective "
828 "function. The optimizer is not reentrant; use a separate instance.")
832 if gradient
is not None:
833 values, fitness = paramopt_wrapper.runOptimizationWithGradient(
834 self.
optimizer, specs, objective, gradient)
835 elif finite_difference:
836 values, fitness = paramopt_wrapper.runOptimizationWithFDGradient(
837 self.
optimizer, specs, objective, fd_step)
839 values, fitness = paramopt_wrapper.runOptimization(
847 name:
Parameter(value=values[name], min=parameter.min, max=parameter.max,
848 type=parameter.type, categories=parameter.categories)
849 for name, parameter
in parameters.items()
854 simulation: Callable[[Dict[str, float]], ConstrainedResult],
855 parameters: Mapping[str, Parameter],
857 constraint_count: int) -> OptimizationResult:
859 Run a constrained optimization: minimize f(x) subject to c_i(x) <= 0.
861 Requires ``setAlgorithm(SLSQP(...))``. SLSQP is the only algorithm in the
862 plugin that handles nonlinear inequality constraints, and it needs every
863 parameter to be ``FLOAT``.
865 The simulation returns the objective, the constraints, and all gradients
866 together. The optimizer caches each result, so the simulation runs once per
867 parameter point no matter how many constraints there are -- which is what
868 makes this practical for objectives that run a full Helios simulation.
871 simulation: Callable receiving ``{name: value}`` and returning a
872 :class:`ConstrainedResult`
873 parameters: Parameters to optimize, keyed by name
874 constraint_count: Number of constraints. Required, and fixed for the whole run.
877 The optimized parameters and the objective value at the optimum
880 ValueError: If the arguments are invalid, or the selected algorithm is
882 TypeError: If simulation is not callable
883 ParameterOptimizationError: If the optimization fails
886 Constraints are satisfied to the plugin's tolerance rather than exactly.
887 Check feasibility of the returned parameters if it matters.
890 >>> # minimize x^2 + y^2 subject to x + y >= 1
891 >>> def simulation(p):
892 ... return ConstrainedResult(
893 ... objective=p["x"] ** 2 + p["y"] ** 2,
894 ... objective_gradient={"x": 2 * p["x"], "y": 2 * p["y"]},
895 ... constraints=[1.0 - p["x"] - p["y"]],
896 ... constraint_gradients=[{"x": -1.0, "y": -1.0}],
898 >>> with ParameterOptimization() as opt:
899 ... opt.setAlgorithm(SLSQP())
900 ... result = opt.runConstrained(
902 ... {"x": Parameter.continuous(0.0, -5.0, 5.0),
903 ... "y": Parameter.continuous(0.0, -5.0, 5.0)},
904 ... constraint_count=1)
908 if not callable(simulation):
910 f
"simulation must be callable, got {type(simulation).__name__}")
911 if not isinstance(constraint_count, int)
or isinstance(constraint_count, bool):
913 f
"constraint_count must be an int, got {type(constraint_count).__name__}")
914 if constraint_count < 1:
916 f
"constraint_count must be at least 1, got {constraint_count}. "
917 f
"Use run() when there are no constraints.")
923 algorithm = getattr(self,
'_algorithm',
None)
924 if not isinstance(algorithm, SLSQP):
925 selected = type(algorithm).__name__
if algorithm
is not None else "none"
927 f
"Constrained optimization requires SLSQP, but the selected algorithm is "
928 f
"{selected}. Call setAlgorithm(SLSQP(...)) first.\n\n"
929 f
"SLSQP is the only algorithm in the plugin that supports nonlinear "
930 f
"inequality constraints. For the others, fold the constraint into the "
931 f
"objective as a penalty term and use run().")
937 "ParameterOptimization.runConstrained() was called from inside its own "
938 "simulation function. The optimizer is not reentrant; use a separate "
943 values, fitness = paramopt_wrapper.runOptimizationConstrained(
944 self.
optimizer, specs, simulation, constraint_count)
949 name:
Parameter(value=values[name], min=parameter.min, max=parameter.max,
950 type=parameter.type, categories=parameter.categories)
951 for name, parameter
in parameters.items()
958 Check the parameter mapping and flatten it for the wrapper.
960 Only conditions that are memory-safety preconditions, or that produce a
961 materially better message here than from C++, are checked. Bound
962 consistency (min == max, min > max, NaN bounds, empty categories) is left
963 to the plugin's own validation so the two cannot drift apart.
965 if not isinstance(parameters, Mapping):
967 f
"parameters must be a mapping of name to Parameter, "
968 f
"got {type(parameters).__name__}")
970 raise ValueError(
"parameters cannot be empty")
973 for name, parameter
in parameters.items():
974 if not isinstance(name, str):
976 f
"Parameter names must be strings, got {type(name).__name__}: {name!r}")
978 raise ValueError(
"Parameter names cannot be empty")
982 raise ValueError(f
"Parameter name {name!r} cannot contain a null character")
983 if not isinstance(parameter, Parameter):
985 f
"Parameter '{name}' must be a Parameter, "
986 f
"got {type(parameter).__name__}")
988 for attribute
in (
"value",
"min",
"max"):
989 attr_value = getattr(parameter, attribute)
990 if not isinstance(attr_value, (int, float))
or isinstance(attr_value, bool):
992 f
"Parameter '{name}' field '{attribute}' must be numeric, "
993 f
"got {type(attr_value).__name__}")
995 categories = tuple(parameter.categories
or ())
996 if parameter.type == ParameterType.CATEGORICAL
and not categories:
998 f
"Parameter '{name}' is CATEGORICAL and must define at least one "
999 f
"allowed value via 'categories'")
1000 for category
in categories:
1001 if not isinstance(category, (int, float))
or isinstance(category, bool):
1003 f
"Parameter '{name}' has a non-numeric category: {category!r}")
1004 if not math.isfinite(float(category)):
1006 f
"Parameter '{name}' has a non-finite category: {category!r}")
1010 "value": float(parameter.value),
1011 "min": float(parameter.min),
1012 "max": float(parameter.max),
1013 "type": int(parameter.type),
1014 "categories": [float(c)
for c
in categories],
1021 parameters: Mapping[str,
"Parameter"],
1022 algorithm:
"AlgorithmSettings") ->
None:
1024 Reject discrete parameters given to an algorithm that cannot handle them.
1026 Only the genetic algorithm implements INTEGER and CATEGORICAL parameters.
1027 The rest search a continuous space, so a discrete parameter would be
1028 optimized as a plain float and the result would not be a whole number, or
1029 not one of the allowed categories.
1031 helios-core enforces this for L-BFGS, Adam, BOBYQA and SLSQP, but not for
1032 CMA-ES or Bayesian optimization, where a CATEGORICAL parameter instead
1033 collapses to 0.0 with no diagnostic: its min and max are documented as
1034 ignored and so are conventionally left at zero, which those two algorithms
1035 read as the bounds [0, 0]. Checking here covers that gap on every core
1036 version, and reports the parameter and the remedy rather than leaving the
1037 message to the layer that happens to catch it first.
1039 info = _ALGORITHM_INFO.get(type(algorithm))
1040 if info
is None or info[3]:
1045 for name, parameter
in parameters.items()
1046 if isinstance(parameter, Parameter)
1047 and parameter.type != ParameterType.FLOAT
1052 listed =
", ".join(f
"'{name}' is {type_name}" for name, type_name
in discrete)
1054 f
"{type(algorithm).__name__} requires every parameter to be FLOAT, "
1055 f
"but {listed}.\n\n"
1056 f
"GeneticAlgorithm is the only algorithm that supports INTEGER and "
1057 f
"CATEGORICAL parameters. Either switch to it, or make the parameter "
1058 f
"continuous with Parameter.continuous(...).")
1061 """Check if the parameteroptimization plugin is available in this build."""
1062 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