0.1.33
Loading...
Searching...
No Matches
UParameterOptimizationWrapper.py
Go to the documentation of this file.
1"""
2Ctypes wrapper for ParameterOptimization C++ bindings.
3
4This module provides low-level ctypes bindings to interface with the native
5Helios ParameterOptimization plugin via the C++ wrapper layer.
6
7Unlike the other wrappers, this one passes Python callables into C++: the
8objective is invoked once per candidate parameter set, potentially thousands of
9times per run. Two mechanisms make that safe and are documented in detail at
10their definitions below -- the exception stash (a Python exception cannot
11propagate through C++ frames) and the callback keepalive (a garbage-collected
12CFUNCTYPE object leaves C++ calling into freed memory).
13"""
14
15import ctypes
16import math
17import sys
18from typing import Callable, Dict, List, Optional, Sequence, Tuple
19
20from ..plugins import helios_lib
21from ..exceptions import check_helios_error
22
23
24# Define the UParameterOptimization struct
25class UParameterOptimization(ctypes.Structure):
26 """Opaque structure for ParameterOptimization C++ class"""
27 pass
28
29
30# Return codes from the native run() entry points. Must match the
31# PYHELIOS_PARAMOPT_* macros in pyhelios_wrapper_parameteroptimization.h.
32PARAMOPT_OK = 0
33PARAMOPT_ERROR = -1
34PARAMOPT_CALLBACK_FAILED = -2
35
36# Parameter kinds. Must match PyHeliosParameterType.
37PARAM_FLOAT = 0
38PARAM_INTEGER = 1
39PARAM_CATEGORICAL = 2
40
41# Genetic algorithm operator selections. Must match PyHeliosCrossoverKind and
42# PyHeliosMutationKind.
43CROSSOVER_BLX_ALPHA = 0
44CROSSOVER_BLX_PCA = 1
45MUTATION_PER_GENE = 0
46MUTATION_ISOTROPIC = 1
47MUTATION_HYBRID = 2
48
49
50# Error checking callback
51def _check_error(result, func, args):
52 """Automatic error checking for all parameter optimization functions"""
53 check_helios_error(helios_lib.getLastErrorCode, helios_lib.getLastErrorMessage, helios_lib.clearError)
54 return result
55
56
57#=============================================================================
58# Structure mirrors
59#
60# These mirror the POD structs in pyhelios_wrapper_parameteroptimization.h.
61# _pack_ is deliberately not set: the C structs use natural alignment, so
62# forcing a packed layout here would silently misread every field after the
63# first padded one.
64#=============================================================================
65
66class PyHeliosParameterSpec(ctypes.Structure):
67 """One optimizable parameter, flattened for the C ABI."""
68 _fields_ = [
69 ("name", ctypes.c_char_p),
70 ("value", ctypes.c_float),
71 ("min", ctypes.c_float),
72 ("max", ctypes.c_float),
73 ("type", ctypes.c_int),
74 ("categories", ctypes.POINTER(ctypes.c_float)),
75 ("category_count", ctypes.c_uint),
76 ]
77
78
79class PyHeliosGeneticAlgorithm(ctypes.Structure):
80 """Genetic algorithm settings, with the variant members flattened."""
81 _fields_ = [
82 ("generations", ctypes.c_size_t),
83 ("population_size", ctypes.c_size_t),
84 ("crossover_rate", ctypes.c_float),
85 ("elitism_rate", ctypes.c_float),
86 ("random_seed", ctypes.c_uint),
87 ("crossover_kind", ctypes.c_int),
88 ("crossover_alpha", ctypes.c_float),
89 ("crossover_pca_update_interval", ctypes.c_size_t),
90 ("mutation_kind", ctypes.c_int),
91 ("mutation_rate", ctypes.c_float),
92 ("mutation_pca_update_interval", ctypes.c_size_t),
93 ("mutation_sigma_pca", ctypes.c_float),
94 ("mutation_gamma_cauchy", ctypes.c_float),
95 ("mutation_sigma_random", ctypes.c_float),
96 ("mutation_pca_gaussian_prob", ctypes.c_float),
97 ("mutation_pca_cauchy_prob", ctypes.c_float),
98 ]
99
100
101class PyHeliosBayesianOptimization(ctypes.Structure):
102 """Bayesian optimization settings."""
103 _fields_ = [
104 ("max_evaluations", ctypes.c_size_t),
105 ("initial_samples", ctypes.c_size_t),
106 ("ucb_kappa", ctypes.c_float),
107 ("max_gp_samples", ctypes.c_size_t),
108 ("acquisition_samples", ctypes.c_size_t),
109 ("random_seed", ctypes.c_uint),
111
112
113class PyHeliosCMAES(ctypes.Structure):
114 """CMA-ES settings."""
115 _fields_ = [
116 ("max_evaluations", ctypes.c_size_t),
117 ("lambda_", ctypes.c_size_t),
118 ("sigma", ctypes.c_float),
119 ("random_seed", ctypes.c_uint),
120 ]
121
122
123class PyHeliosLBFGS(ctypes.Structure):
124 """L-BFGS settings."""
125 _fields_ = [
126 ("max_iterations", ctypes.c_int),
127 ("ftol_rel", ctypes.c_double),
128 ("xtol_rel", ctypes.c_double),
129 ("verify_gradients", ctypes.c_int),
130 ("fd_step", ctypes.c_double),
131 ]
132
133
134class PyHeliosAdam(ctypes.Structure):
135 """AdamW settings."""
136 _fields_ = [
137 ("max_iterations", ctypes.c_int),
138 ("learning_rate", ctypes.c_float),
139 ("beta1", ctypes.c_float),
140 ("beta2", ctypes.c_float),
141 ("epsilon", ctypes.c_float),
142 ("weight_decay", ctypes.c_float),
143 ("ftol_rel", ctypes.c_double),
144 ("xtol_rel", ctypes.c_double),
145 ]
146
147
148class PyHeliosBOBYQA(ctypes.Structure):
149 """BOBYQA settings."""
150 _fields_ = [
151 ("max_iterations", ctypes.c_int),
152 ("ftol_rel", ctypes.c_double),
153 ("xtol_rel", ctypes.c_double),
154 ("initial_step", ctypes.c_double),
155 ]
157
158class PyHeliosSLSQP(ctypes.Structure):
159 """SLSQP settings."""
160 _fields_ = [
161 ("max_iterations", ctypes.c_int),
162 ("ftol_rel", ctypes.c_double),
163 ("xtol_rel", ctypes.c_double),
164 ]
165
166
167#=============================================================================
168# Callback types
170# CFUNCTYPE, never PYFUNCTYPE or WINFUNCTYPE. CFUNCTYPE acquires the GIL on
171# entry (PyGILState_Ensure) and releases it on return, which is exactly what is
172# needed when C++ -- running with the GIL released for the duration of the
173# foreign call -- re-enters Python. PYFUNCTYPE assumes the GIL is already held
174# and would crash if the optimizer ever evaluated candidates off the calling
175# thread; WINFUNCTYPE is stdcall and would corrupt the stack.
176#=============================================================================
177
178ObjectiveCallback = ctypes.CFUNCTYPE(
179 ctypes.c_float, # return: objective value
180 ctypes.POINTER(ctypes.c_float), # values, in sorted-name order
181 ctypes.c_uint, # n
182 ctypes.c_void_p, # user_data
183 ctypes.POINTER(ctypes.c_int), # error_flag (out)
184)
185
186GradientCallback = ctypes.CFUNCTYPE(
187 None,
188 ctypes.POINTER(ctypes.c_float), # values, in sorted-name order
189 ctypes.c_uint, # n
190 ctypes.POINTER(ctypes.c_float), # out_gradient
191 ctypes.c_void_p, # user_data
192 ctypes.POINTER(ctypes.c_int), # error_flag (out)
193)
194
195ConstrainedCallback = ctypes.CFUNCTYPE(
196 None,
197 ctypes.POINTER(ctypes.c_float), # values, in sorted-name order
198 ctypes.c_uint, # n
199 ctypes.POINTER(ctypes.c_float), # out_objective
200 ctypes.POINTER(ctypes.c_float), # out_obj_gradient
201 ctypes.POINTER(ctypes.c_float), # out_constraints
202 ctypes.POINTER(ctypes.c_float), # out_con_gradients, row-major [i * n + j]
203 ctypes.c_uint, # constraint_count
204 ctypes.c_void_p, # user_data
205 ctypes.POINTER(ctypes.c_int), # error_flag (out)
206)
207
208
209#=============================================================================
210# Function prototypes
211#=============================================================================
213try:
214 helios_lib.createParameterOptimization.argtypes = []
215 helios_lib.createParameterOptimization.restype = ctypes.POINTER(UParameterOptimization)
216 helios_lib.createParameterOptimization.errcheck = _check_error
217
218 helios_lib.destroyParameterOptimization.argtypes = [ctypes.POINTER(UParameterOptimization)]
219 helios_lib.destroyParameterOptimization.restype = None
220 # No errcheck: destructors do not fail.
222 helios_lib.parameterOptimizationAlgorithmAvailable.argtypes = [ctypes.c_char_p]
223 helios_lib.parameterOptimizationAlgorithmAvailable.restype = ctypes.c_int
224 # No errcheck: this is a pure query that never sets error state.
225
226 helios_lib.setParameterOptimizationGeneticAlgorithm.argtypes = [
227 ctypes.POINTER(UParameterOptimization), ctypes.POINTER(PyHeliosGeneticAlgorithm)]
228 helios_lib.setParameterOptimizationGeneticAlgorithm.restype = None
229 helios_lib.setParameterOptimizationGeneticAlgorithm.errcheck = _check_error
230
231 helios_lib.setParameterOptimizationBayesian.argtypes = [
232 ctypes.POINTER(UParameterOptimization), ctypes.POINTER(PyHeliosBayesianOptimization)]
233 helios_lib.setParameterOptimizationBayesian.restype = None
234 helios_lib.setParameterOptimizationBayesian.errcheck = _check_error
235
236 helios_lib.setParameterOptimizationCMAES.argtypes = [
237 ctypes.POINTER(UParameterOptimization), ctypes.POINTER(PyHeliosCMAES)]
238 helios_lib.setParameterOptimizationCMAES.restype = None
239 helios_lib.setParameterOptimizationCMAES.errcheck = _check_error
241 helios_lib.setParameterOptimizationAdam.argtypes = [
242 ctypes.POINTER(UParameterOptimization), ctypes.POINTER(PyHeliosAdam)]
243 helios_lib.setParameterOptimizationAdam.restype = None
244 helios_lib.setParameterOptimizationAdam.errcheck = _check_error
245
246 helios_lib.setParameterOptimizationLBFGS.argtypes = [
247 ctypes.POINTER(UParameterOptimization), ctypes.POINTER(PyHeliosLBFGS)]
248 helios_lib.setParameterOptimizationLBFGS.restype = None
249 helios_lib.setParameterOptimizationLBFGS.errcheck = _check_error
250
251 helios_lib.setParameterOptimizationBOBYQA.argtypes = [
252 ctypes.POINTER(UParameterOptimization), ctypes.POINTER(PyHeliosBOBYQA)]
253 helios_lib.setParameterOptimizationBOBYQA.restype = None
254 helios_lib.setParameterOptimizationBOBYQA.errcheck = _check_error
255
256 helios_lib.setParameterOptimizationSLSQP.argtypes = [
257 ctypes.POINTER(UParameterOptimization), ctypes.POINTER(PyHeliosSLSQP)]
258 helios_lib.setParameterOptimizationSLSQP.restype = None
259 helios_lib.setParameterOptimizationSLSQP.errcheck = _check_error
260
261 for _name, _struct in (
262 ("getParameterOptimizationGADefaults", PyHeliosGeneticAlgorithm),
263 ("getParameterOptimizationGAExplore", PyHeliosGeneticAlgorithm),
264 ("getParameterOptimizationGAExploit", PyHeliosGeneticAlgorithm),
265 ("getParameterOptimizationBayesianDefaults", PyHeliosBayesianOptimization),
266 ("getParameterOptimizationBayesianExplore", PyHeliosBayesianOptimization),
267 ("getParameterOptimizationBayesianExploit", PyHeliosBayesianOptimization),
268 ("getParameterOptimizationCMAESDefaults", PyHeliosCMAES),
269 ("getParameterOptimizationCMAESExplore", PyHeliosCMAES),
270 ("getParameterOptimizationCMAESExploit", PyHeliosCMAES),
271 ("getParameterOptimizationLBFGSDefaults", PyHeliosLBFGS),
272 ("getParameterOptimizationAdamDefaults", PyHeliosAdam),
273 ("getParameterOptimizationBOBYQADefaults", PyHeliosBOBYQA),
274 ("getParameterOptimizationSLSQPDefaults", PyHeliosSLSQP),
275 ):
276 _fn = getattr(helios_lib, _name)
277 _fn.argtypes = [ctypes.POINTER(_struct)]
278 _fn.restype = None
279
280 helios_lib.setParameterOptimizationPrintProgress.argtypes = [
281 ctypes.POINTER(UParameterOptimization), ctypes.c_int]
282 helios_lib.setParameterOptimizationPrintProgress.restype = None
283 helios_lib.setParameterOptimizationPrintProgress.errcheck = _check_error
284
285 for _name in ("setParameterOptimizationResultFile",
286 "setParameterOptimizationProgressFile",
287 "setParameterOptimizationInputFile"):
288 _fn = getattr(helios_lib, _name)
289 _fn.argtypes = [ctypes.POINTER(UParameterOptimization), ctypes.c_char_p]
290 _fn.restype = None
291 _fn.errcheck = _check_error
292
293 # The run() entry points deliberately do NOT get an errcheck callback. They
294 # signal a callback abort by returning PARAMOPT_CALLBACK_FAILED with the
295 # native error state left clean, so the wrapper can re-raise the Python
296 # exception the user's objective actually raised. An errcheck here would
297 # inspect that clean state and mask the real failure.
298 helios_lib.runParameterOptimization.argtypes = [
299 ctypes.POINTER(UParameterOptimization),
300 ctypes.POINTER(PyHeliosParameterSpec),
301 ctypes.c_uint,
302 ObjectiveCallback,
303 ctypes.c_void_p,
304 ctypes.POINTER(ctypes.c_float),
305 ctypes.POINTER(ctypes.c_float),
306 ]
307 helios_lib.runParameterOptimization.restype = ctypes.c_int
308
309 helios_lib.runParameterOptimizationWithGradient.argtypes = [
310 ctypes.POINTER(UParameterOptimization),
311 ctypes.POINTER(PyHeliosParameterSpec),
312 ctypes.c_uint,
313 ObjectiveCallback,
314 GradientCallback,
315 ctypes.c_void_p,
316 ctypes.POINTER(ctypes.c_float),
317 ctypes.POINTER(ctypes.c_float),
318 ]
319 helios_lib.runParameterOptimizationWithGradient.restype = ctypes.c_int
320
321 helios_lib.runParameterOptimizationWithFDGradient.argtypes = [
322 ctypes.POINTER(UParameterOptimization),
323 ctypes.POINTER(PyHeliosParameterSpec),
324 ctypes.c_uint,
325 ObjectiveCallback,
326 ctypes.c_float,
327 ctypes.c_void_p,
328 ctypes.POINTER(ctypes.c_float),
329 ctypes.POINTER(ctypes.c_float),
330 ]
331 helios_lib.runParameterOptimizationWithFDGradient.restype = ctypes.c_int
332
333 helios_lib.runParameterOptimizationConstrained.argtypes = [
334 ctypes.POINTER(UParameterOptimization),
335 ctypes.POINTER(PyHeliosParameterSpec),
336 ctypes.c_uint,
337 ConstrainedCallback,
338 ctypes.c_uint,
339 ctypes.c_void_p,
340 ctypes.POINTER(ctypes.c_float),
341 ctypes.POINTER(ctypes.c_float),
342 ]
343 helios_lib.runParameterOptimizationConstrained.restype = ctypes.c_int
344
345 _PARAMETEROPTIMIZATION_FUNCTIONS_AVAILABLE = True
346
347except AttributeError:
348 _PARAMETEROPTIMIZATION_FUNCTIONS_AVAILABLE = False
349
350
352 """Check if ParameterOptimization functions are available in this build"""
353 return _PARAMETEROPTIMIZATION_FUNCTIONS_AVAILABLE
354
355
356def _require_available() -> None:
357 """Raise an actionable error if the plugin was not built into the library."""
358 if not _PARAMETEROPTIMIZATION_FUNCTIONS_AVAILABLE:
359 raise NotImplementedError(
360 "ParameterOptimization functions not available in current Helios library. "
361 "Rebuild PyHelios with the parameteroptimization plugin enabled:\n"
362 " build_scripts/build_helios --clean"
363 )
364
365
366#=============================================================================
367# Callback plumbing
368#=============================================================================
369
370class _CallbackState:
371 """
372 Carries a Python exception raised inside a callback back to the caller.
373
374 A Python exception cannot propagate through the intervening C++ frames.
375 Worse, ctypes does not even let it escape the callback: it prints the
376 traceback via PyErr_WriteUnraisable and returns 0 to C++, and 0 is a
377 perfectly plausible objective value, so the optimizer would carry on and
378 return a confidently wrong answer.
379
380 Instead the trampoline catches everything, stores it here, and raises the
381 error flag. The native side unwinds and returns PARAMOPT_CALLBACK_FAILED,
382 and the caller re-raises the stored exception with its original traceback.
383 """
384
385 __slots__ = ("exception",)
386
387 def __init__(self):
388 self.exception: Optional[Tuple] = None
389
391def _make_objective_trampoline(objective: Callable[[Dict[str, float]], float],
392 names: Sequence[str],
393 state: _CallbackState) -> "ctypes._CFuncPtr":
394 """Wrap a Python objective as a C callback."""
395
396 def _impl(values_ptr, n, user_data, error_flag_ptr):
397 # Once a failure has been recorded, do not re-enter user code: NLopt may
398 # call back a further time or two before it observes its stop flag.
399 if state.exception is not None:
400 error_flag_ptr[0] = 1
401 return 0.0
402 try:
403 params = {names[i]: values_ptr[i] for i in range(n)}
404 # Coerce inside the try so a non-numeric return surfaces as the
405 # user's own TypeError rather than a ctypes conversion failure.
406 result = float(objective(params))
407 if not math.isfinite(result):
408 raise ValueError(
409 f"Objective function returned {result}, which is not a finite number. "
410 f"Non-finite objective values corrupt the optimizer's internal state "
411 f"without raising an error, so they are rejected here."
412 )
413 return result
414 except BaseException:
415 # BaseException, not Exception: a KeyboardInterrupt during a long
416 # run must abort the optimization rather than be swallowed by ctypes.
417 state.exception = sys.exc_info()
418 error_flag_ptr[0] = 1
419 return 0.0
420
421 return ObjectiveCallback(_impl)
422
423
424def _make_gradient_trampoline(gradient: Callable[[Dict[str, float]], Dict[str, float]],
425 names: Sequence[str],
426 state: _CallbackState) -> "ctypes._CFuncPtr":
427 """Wrap a Python gradient function as a C callback."""
428
429 def _impl(values_ptr, n, out_gradient_ptr, user_data, error_flag_ptr):
430 if state.exception is not None:
431 error_flag_ptr[0] = 1
432 return
433 try:
434 params = {names[i]: values_ptr[i] for i in range(n)}
435 result = gradient(params)
437 if not isinstance(result, dict):
438 raise TypeError(
439 f"Gradient function must return a dict mapping parameter name to "
440 f"partial derivative, got {type(result).__name__}"
441 )
442
443 # Checked here rather than left to C++ so the message names the
444 # offending parameters and the traceback points at the user's own
445 # gradient function.
446 missing = set(names) - set(result)
447 if missing:
448 raise ValueError(
449 f"Gradient function omitted parameter(s) {sorted(missing)}. "
450 f"It must return a partial derivative for every parameter: {sorted(names)}"
451 )
452 extra = set(result) - set(names)
453 if extra:
454 raise ValueError(
455 f"Gradient function returned unknown parameter(s) {sorted(extra)}. "
456 f"Expected exactly: {sorted(names)}"
457 )
458
459 for i, name in enumerate(names):
460 value = float(result[name])
461 if not math.isfinite(value):
462 raise ValueError(
463 f"Gradient for parameter '{name}' is {value}, which is not a finite number."
464 )
465 out_gradient_ptr[i] = value
466
467 except BaseException:
468 state.exception = sys.exc_info()
469 error_flag_ptr[0] = 1
470
471 return GradientCallback(_impl)
472
473
474def _write_gradient_dict(result, names: Sequence[str], out_ptr, offset: int, label: str) -> None:
475 """
476 Validate a {name: partial derivative} mapping and write it out positionally.
477
478 Shared by the objective and every constraint of a constrained simulation, so
479 all of them report a missing or unknown parameter the same way. `offset` is the
480 starting index in a flat row-major buffer.
481 """
482 if not isinstance(result, dict):
483 raise TypeError(
484 f"{label} must be a dict mapping parameter name to partial derivative, "
485 f"got {type(result).__name__}"
486 )
487
488 missing = set(names) - set(result)
489 if missing:
490 raise ValueError(
491 f"{label} omitted parameter(s) {sorted(missing)}. "
492 f"It must return a partial derivative for every parameter: {sorted(names)}"
493 )
494 extra = set(result) - set(names)
495 if extra:
496 raise ValueError(
497 f"{label} returned unknown parameter(s) {sorted(extra)}. "
498 f"Expected exactly: {sorted(names)}"
499 )
500
501 for i, name in enumerate(names):
502 value = float(result[name])
503 if not math.isfinite(value):
504 raise ValueError(
505 f"{label} for parameter '{name}' is {value}, which is not a finite number."
506 )
507 out_ptr[offset + i] = value
508
509
510def _make_constrained_trampoline(simulation: Callable[[Dict[str, float]], object],
511 names: Sequence[str],
512 constraint_count: int,
513 state: _CallbackState) -> "ctypes._CFuncPtr":
514 """Wrap a Python constrained simulation as a C callback."""
515
516 def _impl(values_ptr, n, out_objective_ptr, out_obj_gradient_ptr,
517 out_constraints_ptr, out_con_gradients_ptr, n_constraints,
518 user_data, error_flag_ptr):
519 if state.exception is not None:
520 error_flag_ptr[0] = 1
521 return
522 try:
523 params = {names[i]: values_ptr[i] for i in range(n)}
524 result = simulation(params)
525
526 for attribute in ("objective", "objective_gradient",
527 "constraints", "constraint_gradients"):
528 if not hasattr(result, attribute):
529 raise TypeError(
530 f"Constrained simulation must return a ConstrainedResult; got "
531 f"{type(result).__name__}, which has no '{attribute}' attribute."
532 )
533
534 objective = float(result.objective)
535 if not math.isfinite(objective):
536 raise ValueError(
537 f"Constrained simulation returned objective {objective}, which is not a "
538 f"finite number. Non-finite values corrupt the optimizer's internal "
539 f"state without raising an error, so they are rejected here."
540 )
541 out_objective_ptr[0] = objective
542
543 _write_gradient_dict(result.objective_gradient, names,
544 out_obj_gradient_ptr, 0, "Objective gradient")
545
546 constraints = list(result.constraints)
547 if len(constraints) != n_constraints:
548 raise ValueError(
549 f"Constrained simulation returned {len(constraints)} constraint value(s) "
550 f"but constraint_count={n_constraints} was declared. The count is fixed "
551 f"for the whole run and must not vary between evaluations."
552 )
553 gradients = list(result.constraint_gradients)
554 if len(gradients) != n_constraints:
555 raise ValueError(
556 f"Constrained simulation returned {len(gradients)} constraint gradient(s) "
557 f"but {n_constraints} constraint value(s). Every constraint needs exactly "
558 f"one gradient."
559 )
560
561 for i, value in enumerate(constraints):
562 value = float(value)
563 if not math.isfinite(value):
564 raise ValueError(
565 f"Constraint {i} is {value}, which is not a finite number."
566 )
567 out_constraints_ptr[i] = value
568
569 # Row-major, matching the C ABI: constraint i, parameter j at [i * n + j].
570 for i, gradient in enumerate(gradients):
571 _write_gradient_dict(gradient, names, out_con_gradients_ptr,
572 i * len(names), f"Gradient of constraint {i}")
573
574 except BaseException:
575 state.exception = sys.exc_info()
576 error_flag_ptr[0] = 1
577
578 return ConstrainedCallback(_impl)
579
580
581def _reraise(state: _CallbackState) -> None:
582 """Re-raise the exception a callback stashed, preserving its traceback."""
583 exc_info = state.exception
584 state.exception = None
585 if exc_info is None:
586 # The native side reported a callback abort but nothing was recorded.
587 # Surfacing this rather than returning a bogus result keeps the failure
588 # visible instead of silently producing an unoptimized answer.
589 raise RuntimeError(
590 "ParameterOptimization reported a callback failure but no Python exception "
591 "was recorded. This indicates an internal inconsistency in the callback bridge."
592 )
593 _, exc_value, exc_traceback = exc_info
594 raise exc_value.with_traceback(exc_traceback)
595
596
597def _build_parameter_array(parameters: List[dict]) -> Tuple[ctypes.Array, list]:
598 """
599 Build the C parameter array.
600
601 Returns the array along with a keepalive list holding every buffer the array
602 points into. The array stores borrowed pointers, so those buffers must
603 outlive the native call -- see the keepalive note in the run functions.
604 """
605 count = len(parameters)
606 array = (PyHeliosParameterSpec * count)()
607 keepalive: list = []
608
609 for i, spec in enumerate(parameters):
610 encoded_name = spec["name"].encode("utf-8")
611 keepalive.append(encoded_name)
612
613 array[i].name = encoded_name
614 array[i].value = spec["value"]
615 array[i].min = spec["min"]
616 array[i].max = spec["max"]
617 array[i].type = spec["type"]
618
619 categories = spec.get("categories") or ()
620 if categories:
621 category_array = (ctypes.c_float * len(categories))(*categories)
622 keepalive.append(category_array)
623 array[i].categories = category_array
624 array[i].category_count = len(categories)
625 else:
626 array[i].categories = None
627 array[i].category_count = 0
628
629 keepalive.append(array)
630 return array, keepalive
631
632
633#=============================================================================
634# Lifecycle
635#=============================================================================
636
638 """Create a ParameterOptimization instance."""
640 return helios_lib.createParameterOptimization()
641
642
643def destroyParameterOptimization(opt) -> None:
644 """Destroy a ParameterOptimization instance."""
645 if opt and _PARAMETEROPTIMIZATION_FUNCTIONS_AVAILABLE:
646 helios_lib.destroyParameterOptimization(opt)
647
648
649def isAlgorithmAvailable(algorithm_name: str) -> bool:
650 """
651 Check whether an algorithm can run in this build.
652
653 L-BFGS, BOBYQA and SLSQP depend on NLopt, and L-BFGS additionally on the
654 LGPL Luksan solvers that implement it.
655
656 Args:
657 algorithm_name: One of "GA", "BO", "CMAES", "LBFGS", "ADAM", "BOBYQA", "SLSQP"
658 """
659 if not _PARAMETEROPTIMIZATION_FUNCTIONS_AVAILABLE:
660 return False
661 return bool(helios_lib.parameterOptimizationAlgorithmAvailable(algorithm_name.encode("utf-8")))
662
663
664#=============================================================================
665# Algorithm selection
666#=============================================================================
667
668def setGeneticAlgorithm(opt, settings: PyHeliosGeneticAlgorithm) -> None:
669 """Select the genetic algorithm."""
671 helios_lib.setParameterOptimizationGeneticAlgorithm(opt, ctypes.byref(settings))
672
673
674def setBayesianOptimization(opt, settings: PyHeliosBayesianOptimization) -> None:
675 """Select Bayesian optimization."""
677 helios_lib.setParameterOptimizationBayesian(opt, ctypes.byref(settings))
678
679
680def setCMAES(opt, settings: PyHeliosCMAES) -> None:
681 """Select CMA-ES."""
683 helios_lib.setParameterOptimizationCMAES(opt, ctypes.byref(settings))
684
685
686def setAdam(opt, settings: PyHeliosAdam) -> None:
687 """Select AdamW."""
689 helios_lib.setParameterOptimizationAdam(opt, ctypes.byref(settings))
690
691
692def setLBFGS(opt, settings: PyHeliosLBFGS) -> None:
693 """Select L-BFGS."""
695 helios_lib.setParameterOptimizationLBFGS(opt, ctypes.byref(settings))
696
697
698def setBOBYQA(opt, settings: PyHeliosBOBYQA) -> None:
699 """Select BOBYQA."""
701 helios_lib.setParameterOptimizationBOBYQA(opt, ctypes.byref(settings))
702
703
704def setSLSQP(opt, settings: PyHeliosSLSQP) -> None:
705 """Select SLSQP."""
707 helios_lib.setParameterOptimizationSLSQP(opt, ctypes.byref(settings))
708
709
710#=============================================================================
711# Preset settings
712#=============================================================================
713
714def _fetch_preset(function_name: str, struct_type):
715 """Read a settings preset from the native library."""
717 settings = struct_type()
718 getattr(helios_lib, function_name)(ctypes.byref(settings))
719 return settings
720
721
722def getGeneticAlgorithmDefaults() -> PyHeliosGeneticAlgorithm:
723 """Get the plugin's default genetic algorithm settings."""
724 return _fetch_preset("getParameterOptimizationGADefaults", PyHeliosGeneticAlgorithm)
725
726
727def getGeneticAlgorithmExplore() -> PyHeliosGeneticAlgorithm:
728 """Get the exploration-biased genetic algorithm preset."""
729 return _fetch_preset("getParameterOptimizationGAExplore", PyHeliosGeneticAlgorithm)
730
731
732def getGeneticAlgorithmExploit() -> PyHeliosGeneticAlgorithm:
733 """Get the exploitation-biased genetic algorithm preset."""
734 return _fetch_preset("getParameterOptimizationGAExploit", PyHeliosGeneticAlgorithm)
736
737def getBayesianDefaults() -> PyHeliosBayesianOptimization:
738 """Get the plugin's default Bayesian optimization settings."""
739 return _fetch_preset("getParameterOptimizationBayesianDefaults", PyHeliosBayesianOptimization)
740
742def getBayesianExplore() -> PyHeliosBayesianOptimization:
743 """Get the exploration-biased Bayesian optimization preset."""
744 return _fetch_preset("getParameterOptimizationBayesianExplore", PyHeliosBayesianOptimization)
745
746
747def getBayesianExploit() -> PyHeliosBayesianOptimization:
748 """Get the exploitation-biased Bayesian optimization preset."""
749 return _fetch_preset("getParameterOptimizationBayesianExploit", PyHeliosBayesianOptimization)
750
751
752def getCMAESDefaults() -> PyHeliosCMAES:
753 """Get the plugin's default CMA-ES settings."""
754 return _fetch_preset("getParameterOptimizationCMAESDefaults", PyHeliosCMAES)
755
756
757def getCMAESExplore() -> PyHeliosCMAES:
758 """Get the exploration-biased CMA-ES preset."""
759 return _fetch_preset("getParameterOptimizationCMAESExplore", PyHeliosCMAES)
760
761
762def getCMAESExploit() -> PyHeliosCMAES:
763 """Get the exploitation-biased CMA-ES preset."""
764 return _fetch_preset("getParameterOptimizationCMAESExploit", PyHeliosCMAES)
765
766
767def getLBFGSDefaults() -> PyHeliosLBFGS:
768 """Get the plugin's default L-BFGS settings."""
769 return _fetch_preset("getParameterOptimizationLBFGSDefaults", PyHeliosLBFGS)
770
772def getAdamDefaults() -> PyHeliosAdam:
773 """Get the plugin's default Adam settings."""
774 return _fetch_preset("getParameterOptimizationAdamDefaults", PyHeliosAdam)
775
776
777def getBOBYQADefaults() -> PyHeliosBOBYQA:
778 """Get the plugin's default BOBYQA settings."""
779 return _fetch_preset("getParameterOptimizationBOBYQADefaults", PyHeliosBOBYQA)
780
781
782def getSLSQPDefaults() -> PyHeliosSLSQP:
783 """Get the plugin's default SLSQP settings."""
784 return _fetch_preset("getParameterOptimizationSLSQPDefaults", PyHeliosSLSQP)
785
786
787#=============================================================================
788# I/O configuration
789#=============================================================================
790
791def setPrintProgress(opt, enable: bool) -> None:
792 """Enable or disable the plugin's progress printout."""
794 helios_lib.setParameterOptimizationPrintProgress(opt, 1 if enable else 0)
795
796
797def setResultFile(opt, path: Optional[str]) -> None:
798 """Set the file the final result is written to (.csv or .txt)."""
800 helios_lib.setParameterOptimizationResultFile(opt, path.encode("utf-8") if path else None)
801
802
803def setProgressFile(opt, path: Optional[str]) -> None:
804 """Set the file per-generation progress is written to (.csv or .txt)."""
806 helios_lib.setParameterOptimizationProgressFile(opt, path.encode("utf-8") if path else None)
807
808
809def setInputFile(opt, path: Optional[str]) -> None:
810 """Set a file to read the initial parameter set from."""
812 helios_lib.setParameterOptimizationInputFile(opt, path.encode("utf-8") if path else None)
813
815#=============================================================================
816# Run
817#=============================================================================
818
819def _finish_run(rc: int, state: _CallbackState, names: Sequence[str],
820 out_values: ctypes.Array, out_fitness: ctypes.c_float) -> Tuple[Dict[str, float], float]:
821 """Translate a native return code into a result or an exception."""
822 if rc == PARAMOPT_CALLBACK_FAILED:
823 _reraise(state)
824 if rc != PARAMOPT_OK:
825 # Surfaces the C++ message as the appropriate HeliosError subclass.
826 check_helios_error(helios_lib.getLastErrorCode, helios_lib.getLastErrorMessage,
827 helios_lib.clearError)
828 raise RuntimeError("ParameterOptimization run failed without reporting an error message.")
830 values = {name: out_values[i] for i, name in enumerate(names)}
831 return values, out_fitness.value
832
833
834def runOptimization(opt, parameters: List[dict],
835 objective: Callable[[Dict[str, float]], float]) -> Tuple[Dict[str, float], float]:
836 """
837 Run a derivative-free optimization.
838
839 Args:
840 opt: ParameterOptimization instance pointer
841 parameters: Parameter specs as dicts with keys name/value/min/max/type/categories
842 objective: Callable receiving {name: value} and returning a scalar cost
843
844 Returns:
845 Tuple of ({name: optimized value}, fitness)
846 """
848 if not parameters:
849 raise ValueError("Parameter list cannot be empty")
850
851 # The native side orders everything by a lexicographic sort of the names.
852 names = sorted(spec["name"] for spec in parameters)
853 array, keepalive = _build_parameter_array(parameters)
855 state = _CallbackState()
856 # Bound to a local, never passed inline: the CFUNCTYPE object owns the
857 # trampoline's executable thunk, and if the only reference were a temporary
858 # it could be collected while C++ still holds the pointer.
859 objective_cb = _make_objective_trampoline(objective, names, state)
861 out_values = (ctypes.c_float * len(names))()
862 out_fitness = ctypes.c_float()
863
864 rc = helios_lib.runParameterOptimization(
865 opt, array, len(parameters), objective_cb, None,
866 out_values, ctypes.byref(out_fitness))
867
868 # Referenced after the call so nothing above can be collected early.
869 del keepalive, objective_cb
870
871 return _finish_run(rc, state, names, out_values, out_fitness)
872
873
874def runOptimizationWithGradient(opt, parameters: List[dict],
875 objective: Callable[[Dict[str, float]], float],
876 gradient: Callable[[Dict[str, float]], Dict[str, float]]
877 ) -> Tuple[Dict[str, float], float]:
878 """
879 Run an optimization with a user-supplied gradient.
880
881 Args:
882 opt: ParameterOptimization instance pointer
883 parameters: Parameter specs as dicts
884 objective: Callable receiving {name: value} and returning a scalar cost
885 gradient: Callable receiving {name: value} and returning {name: partial derivative}
886
887 Returns:
888 Tuple of ({name: optimized value}, fitness)
889 """
891 if not parameters:
892 raise ValueError("Parameter list cannot be empty")
893
894 names = sorted(spec["name"] for spec in parameters)
895 array, keepalive = _build_parameter_array(parameters)
896
897 state = _CallbackState()
898 objective_cb = _make_objective_trampoline(objective, names, state)
899 gradient_cb = _make_gradient_trampoline(gradient, names, state)
900
901 out_values = (ctypes.c_float * len(names))()
902 out_fitness = ctypes.c_float()
904 rc = helios_lib.runParameterOptimizationWithGradient(
905 opt, array, len(parameters), objective_cb, gradient_cb, None,
906 out_values, ctypes.byref(out_fitness))
907
908 del keepalive, objective_cb, gradient_cb
909
910 return _finish_run(rc, state, names, out_values, out_fitness)
911
912
913def runOptimizationWithFDGradient(opt, parameters: List[dict],
914 objective: Callable[[Dict[str, float]], float],
915 fd_step: float = 0.0) -> Tuple[Dict[str, float], float]:
916 """
917 Run an optimization with gradients estimated by finite differences.
918
919 Args:
920 opt: ParameterOptimization instance pointer
921 parameters: Parameter specs as dicts
922 objective: Callable receiving {name: value} and returning a scalar cost
923 fd_step: Relative perturbation factor; values <= 0 select the plugin default
924
925 Returns:
926 Tuple of ({name: optimized value}, fitness)
927 """
929 if not parameters:
930 raise ValueError("Parameter list cannot be empty")
931
932 names = sorted(spec["name"] for spec in parameters)
933 array, keepalive = _build_parameter_array(parameters)
934
935 state = _CallbackState()
936 objective_cb = _make_objective_trampoline(objective, names, state)
937
938 out_values = (ctypes.c_float * len(names))()
939 out_fitness = ctypes.c_float()
940
941 rc = helios_lib.runParameterOptimizationWithFDGradient(
942 opt, array, len(parameters), objective_cb, fd_step, None,
943 out_values, ctypes.byref(out_fitness))
945 del keepalive, objective_cb
946
947 return _finish_run(rc, state, names, out_values, out_fitness)
948
949
950def runOptimizationConstrained(opt, parameters: List[dict],
951 simulation: Callable[[Dict[str, float]], object],
952 constraint_count: int) -> Tuple[Dict[str, float], float]:
953 """
954 Run a constrained optimization: minimize f(x) subject to c_i(x) <= 0.
955
956 Requires SLSQP, enforced by the plugin.
957
958 Args:
959 opt: ParameterOptimization instance pointer
960 parameters: Parameter specs as dicts
961 simulation: Callable receiving {name: value} and returning a ConstrainedResult
962 constraint_count: Number of constraints; fixed for the whole run
963
964 Returns:
965 Tuple of ({name: optimized value}, fitness)
966 """
968 if not parameters:
969 raise ValueError("Parameter list cannot be empty")
970 if constraint_count < 1:
971 raise ValueError(
972 f"constraint_count must be at least 1, got {constraint_count}. "
973 f"Use runOptimization() or runOptimizationWithGradient() when there are "
974 f"no constraints.")
975
976 names = sorted(spec["name"] for spec in parameters)
977 array, keepalive = _build_parameter_array(parameters)
978
979 state = _CallbackState()
980 simulation_cb = _make_constrained_trampoline(simulation, names, constraint_count, state)
981
982 out_values = (ctypes.c_float * len(names))()
983 out_fitness = ctypes.c_float()
984
985 rc = helios_lib.runParameterOptimizationConstrained(
986 opt, array, len(parameters), simulation_cb, constraint_count, None,
987 out_values, ctypes.byref(out_fitness))
988
989 del keepalive, simulation_cb
990
991 return _finish_run(rc, state, names, out_values, out_fitness)
992
993
994# Mock mode functions for development
995if not _PARAMETEROPTIMIZATION_FUNCTIONS_AVAILABLE:
996 def mock_createParameterOptimization(*args, **kwargs):
997 raise RuntimeError(
998 "Mock mode: ParameterOptimization not available. "
999 "This would create a parameter optimization instance with native library."
1000 )
1001
1002 def mock_runParameterOptimization(*args, **kwargs):
1003 raise RuntimeError(
1004 "Mock mode: ParameterOptimization methods not available. "
1005 "This would run an optimization with native library."
1006 )
1007
1008 # Replace functions with mocks for development
1009 createParameterOptimization = mock_createParameterOptimization
1010 runOptimization = mock_runParameterOptimization
1011 runOptimizationWithGradient = mock_runParameterOptimization
1012 runOptimizationWithFDGradient = mock_runParameterOptimization
1013 runOptimizationConstrained = mock_runParameterOptimization
Genetic algorithm settings, with the variant members flattened.
Carries a Python exception raised inside a callback back to the caller.
_check_error(result, func, args)
Automatic error checking for all parameter optimization functions.
PyHeliosCMAES getCMAESDefaults()
Get the plugin's default CMA-ES settings.
PyHeliosGeneticAlgorithm getGeneticAlgorithmExploit()
Get the exploitation-biased genetic algorithm preset.
PyHeliosGeneticAlgorithm getGeneticAlgorithmExplore()
Get the exploration-biased genetic algorithm preset.
None setLBFGS(opt, PyHeliosLBFGS settings)
Select L-BFGS.
None setCMAES(opt, PyHeliosCMAES settings)
Select CMA-ES.
PyHeliosGeneticAlgorithm getGeneticAlgorithmDefaults()
Get the plugin's default genetic algorithm settings.
None _write_gradient_dict(result, Sequence[str] names, out_ptr, int offset, str label)
Validate a {name: partial derivative} mapping and write it out positionally.
None setResultFile(opt, Optional[str] path)
Set the file the final result is written to (.csv or .txt).
None setBayesianOptimization(opt, PyHeliosBayesianOptimization settings)
Select Bayesian optimization.
None setProgressFile(opt, Optional[str] path)
Set the file per-generation progress is written to (.csv or .txt).
PyHeliosCMAES getCMAESExplore()
Get the exploration-biased CMA-ES preset.
bool isParameterOptimizationAvailable()
Check if ParameterOptimization functions are available in this build.
"ctypes._CFuncPtr" _make_gradient_trampoline(Callable[[Dict[str, float]], Dict[str, float]] gradient, Sequence[str] names, _CallbackState state)
Wrap a Python gradient function as a C callback.
createParameterOptimization
Create a ParameterOptimization instance.
None _require_available()
Raise an actionable error if the plugin was not built into the library.
PyHeliosBayesianOptimization getBayesianExplore()
Get the exploration-biased Bayesian optimization preset.
Tuple[ctypes.Array, list] _build_parameter_array(List[dict] parameters)
Build the C parameter array.
None setSLSQP(opt, PyHeliosSLSQP settings)
Select SLSQP.
PyHeliosBayesianOptimization getBayesianDefaults()
Get the plugin's default Bayesian optimization settings.
Tuple[Dict[str, float], float] _finish_run(int rc, _CallbackState state, Sequence[str] names, ctypes.Array out_values, ctypes.c_float out_fitness)
Translate a native return code into a result or an exception.
None destroyParameterOptimization(opt)
Destroy a ParameterOptimization instance.
_fetch_preset(str function_name, struct_type)
Read a settings preset from the native library.
None setBOBYQA(opt, PyHeliosBOBYQA settings)
Select BOBYQA.
PyHeliosCMAES getCMAESExploit()
Get the exploitation-biased CMA-ES preset.
PyHeliosAdam getAdamDefaults()
Get the plugin's default Adam settings.
bool isAlgorithmAvailable(str algorithm_name)
Check whether an algorithm can run in this build.
PyHeliosSLSQP getSLSQPDefaults()
Get the plugin's default SLSQP settings.
None setGeneticAlgorithm(opt, PyHeliosGeneticAlgorithm settings)
Select the genetic algorithm.
PyHeliosBOBYQA getBOBYQADefaults()
Get the plugin's default BOBYQA settings.
None setAdam(opt, PyHeliosAdam settings)
Select AdamW.
None _reraise(_CallbackState state)
Re-raise the exception a callback stashed, preserving its traceback.
PyHeliosBayesianOptimization getBayesianExploit()
Get the exploitation-biased Bayesian optimization preset.
None setPrintProgress(opt, bool enable)
Enable or disable the plugin's progress printout.
PyHeliosLBFGS getLBFGSDefaults()
Get the plugin's default L-BFGS settings.
None setInputFile(opt, Optional[str] path)
Set a file to read the initial parameter set from.
"ctypes._CFuncPtr" _make_objective_trampoline(Callable[[Dict[str, float]], float] objective, Sequence[str] names, _CallbackState state)
Wrap a Python objective as a C callback.
"ctypes._CFuncPtr" _make_constrained_trampoline(Callable[[Dict[str, float]], object] simulation, Sequence[str] names, int constraint_count, _CallbackState state)
Wrap a Python constrained simulation as a C callback.