|
| 1 | +# Copyright 2008, 2021 European Space Agency |
| 2 | +# |
| 3 | +# This file is part of pyoptgra, a pygmo affiliated library. |
| 4 | +# |
| 5 | +# This Source Code Form is available under two different licenses. |
| 6 | +# You may choose to license and use it under version 3 of the |
| 7 | +# GNU General Public License or under the |
| 8 | +# ESA Software Community Licence (ESCL) 2.4 Weak Copyleft. |
| 9 | +# We explicitly reserve the right to release future versions of |
| 10 | +# Pyoptgra and Optgra under different licenses. |
| 11 | +# If copies of GPL3 and ESCL 2.4 were not distributed with this |
| 12 | +# file, you can obtain them at https://www.gnu.org/licenses/gpl-3.0.txt |
| 13 | +# and https://essr.esa.int/license/european-space-agency-community-license-v2-4-weak-copyleft |
| 14 | + |
| 15 | +import multiprocessing as mp |
| 16 | +from typing import Any, Callable, List, Tuple |
| 17 | + |
| 18 | +__all__ = ["get_optimize_with_timeout_function"] |
| 19 | + |
| 20 | + |
| 21 | +def _run_optimize( |
| 22 | + func: Callable[..., Tuple[List[float], List[float], int]], |
| 23 | + args: tuple, |
| 24 | + kwargs: dict, |
| 25 | + return_dict: Any, |
| 26 | +) -> None: |
| 27 | + """Worker process that runs the C++ optimizer and stores its result.""" |
| 28 | + try: |
| 29 | + result = func(*args, **kwargs) |
| 30 | + return_dict["result"] = result |
| 31 | + except Exception as e: |
| 32 | + return_dict["error"] = str(e) |
| 33 | + |
| 34 | + |
| 35 | +def get_optimize_with_timeout_function( |
| 36 | + optimize_func: Callable[..., Tuple[List[float], List[float], int]], |
| 37 | + timeout_seconds: float, |
| 38 | + x_timeout: List[float], |
| 39 | + fitness_func: Callable, |
| 40 | +) -> Callable[..., Tuple[List[float], List[float], int]]: |
| 41 | + """ |
| 42 | + Wrap the Pybind11-based `optimize` function with a timeout safeguard. |
| 43 | +
|
| 44 | + Parameters |
| 45 | + ---------- |
| 46 | + optimize_func : callable |
| 47 | + The Pybind11-bound `optimize` function to execute. |
| 48 | + Must return a tuple `(x_opt, f_opt, status)`. |
| 49 | + timeout_seconds : float |
| 50 | + Maximum runtime in seconds before the optimizer process is terminated. |
| 51 | + x_timeout : List[float] |
| 52 | + Decision vector to return on timeout |
| 53 | + fitness_func : callable |
| 54 | + Fitness function to return on timeout |
| 55 | +
|
| 56 | + Returns |
| 57 | + ------- |
| 58 | + callable |
| 59 | + A wrapped version of `optimize_func` with the same signature. |
| 60 | + When called: |
| 61 | + * Returns `(x_opt, multipliers, status)` if the optimizer completes. |
| 62 | + * Returns `([], [], 5)` if the optimizer exceeds the timeout. |
| 63 | +
|
| 64 | + Notes |
| 65 | + ----- |
| 66 | + - The wrapped function runs the optimizer in a separate process using |
| 67 | + :mod:`multiprocessing` to allow safe termination if the Fortran backend hangs. |
| 68 | + - Status code `5` indicates a timeout occurred. |
| 69 | + - This approach ensures that the main Python process remains responsive |
| 70 | + and that no hanging Fortran thread blocks program exit. |
| 71 | + """ |
| 72 | + |
| 73 | + def wrapped_optimize(*args, **kwargs) -> Tuple[List[float], List[float], int]: |
| 74 | + manager = mp.Manager() |
| 75 | + return_dict = manager.dict() |
| 76 | + |
| 77 | + process = mp.Process(target=_run_optimize, args=(optimize_func, args, kwargs, return_dict)) |
| 78 | + process.start() |
| 79 | + process.join(timeout_seconds) |
| 80 | + |
| 81 | + if process.is_alive(): |
| 82 | + print( |
| 83 | + f"⚠️ Optimization timed out after {timeout_seconds} seconds — terminating process." |
| 84 | + ) |
| 85 | + process.terminate() |
| 86 | + process.join() |
| 87 | + # Return timeout status code instead of raising |
| 88 | + return (x_timeout, fitness_func(x_timeout), 5) |
| 89 | + |
| 90 | + if "error" in return_dict: |
| 91 | + print(f"⚠️ Optimizer process failed: {return_dict['error']}") |
| 92 | + return (x_timeout, fitness_func(x_timeout), 5) |
| 93 | + |
| 94 | + return return_dict.get("result", (x_timeout, fitness_func(x_timeout), 5)) |
| 95 | + |
| 96 | + return wrapped_optimize |
0 commit comments