From 2e419f987854cb00b4d4b7b485bde4e371b05e23 Mon Sep 17 00:00:00 2001 From: Sebastian Gsell Date: Tue, 24 Jun 2025 12:46:59 +0200 Subject: [PATCH 01/20] Copy batch evaluator code over from optimagic --- src/tranquilo/batch_evaluators.py | 187 ++++++++++++++++++++++++++++++ src/tranquilo/decorators.py | 126 ++++++++++++++++++++ src/tranquilo/options.py | 9 ++ src/tranquilo/wrap_criterion.py | 4 +- 4 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 src/tranquilo/batch_evaluators.py create mode 100644 src/tranquilo/decorators.py diff --git a/src/tranquilo/batch_evaluators.py b/src/tranquilo/batch_evaluators.py new file mode 100644 index 0000000..437317a --- /dev/null +++ b/src/tranquilo/batch_evaluators.py @@ -0,0 +1,187 @@ +"""A collection of batch evaluators for process based parallelism. + +All batch evaluators have the same interface and any function with the same interface +can be used used as batch evaluator in optimagic. + +""" + +from joblib import Parallel, delayed + +try: + from pathos.pools import ProcessPool + + pathos_is_available = True +except ImportError: + pathos_is_available = False + +from typing import Any, Callable, Literal, TypeVar + +from tranquilo.config import DEFAULT_N_CORES as N_CORES +from tranquilo.decorators import catch, unpack +from tranquilo.options import ErrorHandling + +T = TypeVar("T") + + +def pathos_mp_batch_evaluator( + func: Callable[..., T], + arguments: list[Any], + *, + n_cores: int = N_CORES, + error_handling: ( + ErrorHandling | Literal["raise", "continue"] + ) = ErrorHandling.CONTINUE, + unpack_symbol: Literal["*", "**"] | None = None, +) -> list[T]: + """Batch evaluator based on pathos.multiprocess.ProcessPool. + + This uses a patched but older version of python multiprocessing that replaces + pickling with dill and can thus handle decorated functions. + + Args: + func (Callable): The function that is evaluated. + arguments (Iterable): Arguments for the functions. Their interperation + depends on the unpack argument. + n_cores (int): Number of cores used to evaluate the function in parallel. + Value below one are interpreted as one. If only one core is used, the + batch evaluator disables everything that could cause problems, i.e. in that + case func and arguments are never pickled and func is executed in the main + process. + error_handling (str): Can take the values "raise" (raise the error and stop all + tasks as soon as one task fails) and "continue" (catch exceptions and set + the traceback of the raised exception. + KeyboardInterrupt and SystemExit are always raised. + unpack_symbol (str or None). Can be "**", "*" or None. If None, func just takes + one argument. If "*", the elements of arguments are positional arguments for + func. If "**", the elements of arguments are keyword arguments for func. + + + Returns: + list: The function evaluations. + + """ + if not pathos_is_available: + raise NotImplementedError( + "To use the pathos_mp_batch_evaluator, install pathos with " + "conda install -c conda-forge pathos." + ) + + _check_inputs(func, arguments, n_cores, error_handling, unpack_symbol) + n_cores = int(n_cores) + + reraise = error_handling in [ + "raise", + ErrorHandling.RAISE, + ErrorHandling.RAISE_STRICT, + ] + + @unpack(symbol=unpack_symbol) + @catch(default="__traceback__", reraise=reraise) + def internal_func(*args: Any, **kwargs: Any) -> T: + return func(*args, **kwargs) + + if n_cores <= 1: + res = [internal_func(arg) for arg in arguments] + else: + p = ProcessPool(nodes=n_cores) + try: + res = p.map(internal_func, arguments) + except Exception as e: + p.terminate() + raise e + + return res + + +def joblib_batch_evaluator( + func: Callable[..., T], + arguments: list[Any], + *, + n_cores: int = N_CORES, + error_handling: ( + ErrorHandling | Literal["raise", "continue"] + ) = ErrorHandling.CONTINUE, + unpack_symbol: Literal["*", "**"] | None = None, +) -> list[T]: + """Batch evaluator based on joblib's Parallel. + + Args: + func (Callable): The function that is evaluated. + arguments (Iterable): Arguments for the functions. Their interperation + depends on the unpack argument. + n_cores (int): Number of cores used to evaluate the function in parallel. + Value below one are interpreted as one. If only one core is used, the + batch evaluator disables everything that could cause problems, i.e. in that + case func and arguments are never pickled and func is executed in the main + process. + error_handling (str): Can take the values "raise" (raise the error and stop all + tasks as soon as one task fails) and "continue" (catch exceptions and set + the output of failed tasks to the traceback of the raised exception. + KeyboardInterrupt and SystemExit are always raised. + unpack_symbol (str or None). Can be "**", "*" or None. If None, func just takes + one argument. If "*", the elements of arguments are positional arguments for + func. If "**", the elements of arguments are keyword arguments for func. + + + Returns: + list: The function evaluations. + + """ + _check_inputs(func, arguments, n_cores, error_handling, unpack_symbol) + n_cores = int(n_cores) if int(n_cores) >= 2 else 1 + + reraise = error_handling in [ + "raise", + ErrorHandling.RAISE, + ErrorHandling.RAISE_STRICT, + ] + + @unpack(symbol=unpack_symbol) + @catch(default="__traceback__", reraise=reraise) + def internal_func(*args: Any, **kwargs: Any) -> T: + return func(*args, **kwargs) + + if n_cores == 1: + res = [internal_func(arg) for arg in arguments] + else: + res = Parallel(n_jobs=n_cores)(delayed(internal_func)(arg) for arg in arguments) + + return res + + +def _check_inputs( + func: Callable[..., T], + arguments: list[Any], + n_cores: int, + error_handling: ErrorHandling | Literal["raise", "continue"], + unpack_symbol: Literal["*", "**"] | None, +) -> None: + if not callable(func): + raise TypeError("func must be callable.") + + try: + arguments = list(arguments) + except Exception as e: + raise ValueError("arguments must be list like.") from e + + try: + int(n_cores) + except Exception as e: + raise ValueError("n_cores must be an integer.") from e + + if unpack_symbol not in (None, "*", "**"): + raise ValueError( + f"unpack_symbol must be None, '*' or '**', not {unpack_symbol}" + ) + + if error_handling not in [ + "raise", + "continue", + ErrorHandling.RAISE, + ErrorHandling.CONTINUE, + ErrorHandling.RAISE_STRICT, + ]: + raise ValueError( + "error_handling must be 'raise' or 'continue' or ErrorHandling not " + f"{error_handling}" + ) diff --git a/src/tranquilo/decorators.py b/src/tranquilo/decorators.py new file mode 100644 index 0000000..296ea1f --- /dev/null +++ b/src/tranquilo/decorators.py @@ -0,0 +1,126 @@ +"""This module contains various decorators. + +There are two kinds of decorators defined in this module which consists of either two or +three nested functions. The former are decorators without and the latter with arguments. + +For more information on decorators, see this `guide +`_ on https://realpython.com + +which +provides a comprehensive overview. + +.. _guide: + +https://realpython.com/primer-on-python-decorators/ + +""" + +import sys +from traceback import format_exception + +import functools +import warnings + + +def catch( + func=None, + *, + exception=Exception, + exclude=(KeyboardInterrupt, SystemExit), + onerror=None, + default=None, + warn=True, + reraise=False, +): + """Catch and handle exceptions. + + This decorator can be used with and without additional arguments. + + Args: + exception (Exception or tuple): One or several exceptions that + are caught and handled. By default all Exceptions are + caught and handled. + exclude (Exception or tuple): One or several exceptionts that + are not caught. By default those are KeyboardInterrupt and + SystemExit. + onerror (None or Callable): Callable that takes an Exception + as only argument. This is called when an exception occurs. + default: Value that is returned when as the output of func when + an exception occurs. Can be one of the following: + - a constant + - "__traceback__", in this case a string with a traceback is returned. + - callable with the same signature as func. + warn (bool): If True, the exception is converted to a warning. + reraise (bool): If True, the exception is raised after handling it. + + """ + + def decorator_catch(func): + @functools.wraps(func) + def wrapper_catch(*args, **kwargs): + try: + res = func(*args, **kwargs) + except exclude: + raise + except exception as e: + if onerror is not None: + onerror(e) + + if reraise: + raise e + + tb = get_traceback() + + if warn: + msg = f"The following exception was caught:\n\n{tb}" + warnings.warn(msg) + + if default == "__traceback__": + res = tb + elif callable(default): + res = default(*args, **kwargs) + else: + res = default + return res + + return wrapper_catch + + if callable(func): + return decorator_catch(func) + else: + return decorator_catch + + +def unpack(func=None, symbol=None): + def decorator_unpack(func): + if symbol is None: + + @functools.wraps(func) + def wrapper_unpack(arg): + return func(arg) + + elif symbol == "*": + + @functools.wraps(func) + def wrapper_unpack(arg): + return func(*arg) + + elif symbol == "**": + + @functools.wraps(func) + def wrapper_unpack(arg): + return func(**arg) + + return wrapper_unpack + + if callable(func): + return decorator_unpack(func) + else: + return decorator_unpack + + +def get_traceback(): + tb = format_exception(*sys.exc_info()) + if isinstance(tb, list): + tb = "".join(tb) + return tb diff --git a/src/tranquilo/options.py b/src/tranquilo/options.py index c83eced..5158dca 100644 --- a/src/tranquilo/options.py +++ b/src/tranquilo/options.py @@ -1,4 +1,5 @@ from typing import NamedTuple +from enum import Enum from tranquilo.models import n_free_params import numpy as np @@ -256,3 +257,11 @@ def update_option_bundle(default_options, user_options=None): out = default_options._replace(**typed) return out + + +class ErrorHandling(Enum): + """Enum to specify the error handling strategy of the optimization algorithm.""" + + RAISE = "raise" + RAISE_STRICT = "raise_strict" + CONTINUE = "continue" diff --git a/src/tranquilo/wrap_criterion.py b/src/tranquilo/wrap_criterion.py index e432eae..0622a77 100644 --- a/src/tranquilo/wrap_criterion.py +++ b/src/tranquilo/wrap_criterion.py @@ -80,9 +80,9 @@ def process_batch_evaluator(batch_evaluator="joblib"): out = batch_evaluator elif isinstance(batch_evaluator, str): if batch_evaluator == "joblib": - from optimagic.batch_evaluators import joblib_batch_evaluator as out + from tranquilo.batch_evaluators import joblib_batch_evaluator as out elif batch_evaluator == "pathos": - from optimagic.batch_evaluators import pathos_mp_batch_evaluator as out + from tranquilo.batch_evaluators import pathos_mp_batch_evaluator as out else: raise ValueError( "Invalid batch evaluator requested. Currently only 'pathos' and " From 711556dc2275509a7d58ab116c46214d78ca5d09 Mon Sep 17 00:00:00 2001 From: Sebastian Gsell Date: Tue, 24 Jun 2025 13:10:41 +0200 Subject: [PATCH 02/20] Skip tests if optimatic is not installed --- src/tranquilo/config.py | 12 ++++++++++++ tests/subsolvers/test_gqtpar_lambdas.py | 9 +++++++-- tests/test_fit_models.py | 13 +++++++++++-- tests/test_tranquilo.py | 17 +++++++++++++---- tests/test_visualize.py | 6 ++++-- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/tranquilo/config.py b/src/tranquilo/config.py index 121780f..99dbfc6 100644 --- a/src/tranquilo/config.py +++ b/src/tranquilo/config.py @@ -18,6 +18,18 @@ CRITERION_PENALTY_CONSTANT = 100 +# ====================================================================================== +# Check Available Packages +# ====================================================================================== + +try: + import optimagic # noqa: F401 +except ImportError: + IS_OPTIMAGIC_INSTALLED = False +else: + IS_OPTIMAGIC_INSTALLED = True + + # ================================================================================= # Dashboard Defaults # ================================================================================= diff --git a/tests/subsolvers/test_gqtpar_lambdas.py b/tests/subsolvers/test_gqtpar_lambdas.py index 0c1a770..c8a0a05 100644 --- a/tests/subsolvers/test_gqtpar_lambdas.py +++ b/tests/subsolvers/test_gqtpar_lambdas.py @@ -1,7 +1,12 @@ -from optimagic.optimization.optimize import minimize -from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems +import pytest +from tranquilo.config import IS_OPTIMAGIC_INSTALLED +if IS_OPTIMAGIC_INSTALLED: + from optimagic.optimization.optimize import minimize + from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems + +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") def test_gqtpar_lambdas(): algo_options = { "disable_convergence": True, diff --git a/tests/test_fit_models.py b/tests/test_fit_models.py index 84b5324..b5427eb 100644 --- a/tests/test_fit_models.py +++ b/tests/test_fit_models.py @@ -1,9 +1,16 @@ import numpy as np import pytest -from optimagic.differentiation.derivatives import first_derivative, second_derivative +from numpy.testing import assert_array_almost_equal, assert_array_equal + from tranquilo.fit_models import _quadratic_features, get_fitter from tranquilo.region import Region -from numpy.testing import assert_array_almost_equal, assert_array_equal +from tranquilo.config import IS_OPTIMAGIC_INSTALLED + +if IS_OPTIMAGIC_INSTALLED: + from optimagic.differentiation.derivatives import ( + first_derivative, + second_derivative, + ) def aaae(x, y, decimal=None, case=None): @@ -91,6 +98,7 @@ def test_fit_against_truth_quadratic(fitter, quadratic_case): ) +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") @pytest.mark.parametrize("model", ["ols", "ridge", "tranquilo"]) def test_fit_ols_against_gradient(model, quadratic_case): options = {"l2_penalty_square": 0} @@ -116,6 +124,7 @@ def test_fit_ols_against_gradient(model, quadratic_case): aaae(gradient["derivative"], grad, case="gradient") +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") @pytest.mark.parametrize("model", ("ols", "ridge", "tranquilo", "powell")) def test_fit_ols_against_hessian(model, quadratic_case): options = {"l2_penalty_square": 0} diff --git a/tests/test_tranquilo.py b/tests/test_tranquilo.py index b966132..4339ee4 100644 --- a/tests/test_tranquilo.py +++ b/tests/test_tranquilo.py @@ -1,12 +1,16 @@ import itertools -import numpy as np import pytest -from optimagic.optimization.optimize import minimize -from tranquilo.tranquilo import _tranquilo from functools import partial +import numpy as np from numpy.testing import assert_array_almost_equal as aaae -from optimagic import mark + +from tranquilo.tranquilo import _tranquilo +from tranquilo.config import IS_OPTIMAGIC_INSTALLED + +if IS_OPTIMAGIC_INSTALLED: + from optimagic.optimization.optimize import minimize + from optimagic import mark tranquilo = partial( @@ -119,6 +123,7 @@ def test_internal_tranquilo_scalar_sphere_imprecise_defaults( # ====================================================================================== +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") def test_external_tranquilo_scalar_sphere_defaults(): res = minimize( criterion=lambda x: x @ x, @@ -172,6 +177,7 @@ def test_internal_tranquilo_ls_sphere_defaults( # ====================================================================================== +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") def test_external_tranquilo_ls_sphere_defaults(): res = minimize( criterion=mark.least_squares(lambda x: x), @@ -187,6 +193,7 @@ def test_external_tranquilo_ls_sphere_defaults(): # ====================================================================================== +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") @pytest.mark.parametrize("algo", ["tranquilo", "tranquilo_ls"]) def test_tranquilo_with_noise_handling_and_deterministic_function(algo): def _f(x): @@ -202,6 +209,7 @@ def _f(x): aaae(res.params, np.zeros(5), decimal=3) +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") @pytest.mark.slow() def test_tranquilo_ls_with_noise_handling_and_noisy_function(): rng = np.random.default_rng(123) @@ -230,6 +238,7 @@ def sum_of_squares(x): return {"value": contribs.sum(), "contributions": contribs, "root_contributions": x} +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") @pytest.mark.parametrize("algorithm", ["tranquilo", "tranquilo_ls"]) def test_tranquilo_with_binding_bounds(algorithm): res = minimize( diff --git a/tests/test_visualize.py b/tests/test_visualize.py index 6882164..bafbdf0 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -1,8 +1,10 @@ import pytest -from optimagic.optimization.optimize import minimize -from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems from tranquilo.visualize import visualize_tranquilo +from tranquilo.config import IS_OPTIMAGIC_INSTALLED +if IS_OPTIMAGIC_INSTALLED: + from optimagic.optimization.optimize import minimize + from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems cases = [] algo_options = { From accf58b89be2bc300cf60b62c3720cf3e74a6181 Mon Sep 17 00:00:00 2001 From: Sebastian Gsell Date: Tue, 24 Jun 2025 13:31:31 +0200 Subject: [PATCH 03/20] Remove optimagic from docstring --- src/tranquilo/batch_evaluators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tranquilo/batch_evaluators.py b/src/tranquilo/batch_evaluators.py index 437317a..a549c2a 100644 --- a/src/tranquilo/batch_evaluators.py +++ b/src/tranquilo/batch_evaluators.py @@ -1,7 +1,7 @@ """A collection of batch evaluators for process based parallelism. All batch evaluators have the same interface and any function with the same interface -can be used used as batch evaluator in optimagic. +can be used used as batch evaluator. """ From 6aed0f7aec0c5c90b0b3e9696d4d9c5f519aa118 Mon Sep 17 00:00:00 2001 From: Sebastian Gsell Date: Tue, 24 Jun 2025 13:39:08 +0200 Subject: [PATCH 04/20] Update pre-commit hooks --- .pre-commit-config.yaml | 26 +++++++++---------- .../subsolvers/fallback_subsolvers.py | 5 ++-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc34200..266b412 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: check-useless-excludes # - id: identity # Prints all files passed to pre-commits. Debugging. - repo: https://github.com/lyz-code/yamlfix - rev: 1.9.0 + rev: 1.17.0 hooks: - id: yamlfix - repo: local @@ -61,17 +61,17 @@ repos: rev: 1.13.0 hooks: - id: blacken-docs -# - repo: https://github.com/PyCQA/docformatter -# rev: v1.5.1 -# hooks: -# - id: docformatter -# args: -# - --in-place -# - --wrap-summaries -# - '88' -# - --wrap-descriptions -# - '88' -# - --blank + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.7 + hooks: + - id: docformatter + args: + - --in-place + - --wrap-summaries + - '88' + - --wrap-descriptions + - '88' + - --blank - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.0.261 hooks: @@ -91,7 +91,7 @@ repos: - id: nbqa-black - id: nbqa-ruff - repo: https://github.com/executablebooks/mdformat - rev: 0.7.16 + rev: 0.7.22 hooks: - id: mdformat additional_dependencies: diff --git a/src/tranquilo/subsolvers/fallback_subsolvers.py b/src/tranquilo/subsolvers/fallback_subsolvers.py index 5192aad..2283973 100644 --- a/src/tranquilo/subsolvers/fallback_subsolvers.py +++ b/src/tranquilo/subsolvers/fallback_subsolvers.py @@ -80,8 +80,9 @@ def robust_cube_solver_multistart(model, x_candidate): def robust_sphere_solver_inscribed_cube(model, x_candidate): """Robust sphere solver that uses a cube solver in an inscribed cube. - We let x be in the largest cube that is inscribed inside the unit sphere. Formula - is taken from http://tinyurl.com/4astpuwn. + We let x be in the largest cube that is inscribed inside the unit sphere. Formula is + taken from + http://tinyurl.com/4astpuwn. This solver cannot find solutions on the hull of the sphere. From 952fda3f7a8c93fcf5dd7c3722d164fe35bb75f0 Mon Sep 17 00:00:00 2001 From: Sebastian Gsell Date: Tue, 24 Jun 2025 16:44:24 +0200 Subject: [PATCH 05/20] Fix depreciation warnings --- src/tranquilo/aggregate_models.py | 2 +- src/tranquilo/sample_points.py | 4 +- .../subsolvers/fallback_subsolvers.py | 37 +++++++++------ tests/subsolvers/test_gqtpar_lambdas.py | 4 +- tests/test_fit_models.py | 4 +- tests/test_tranquilo.py | 46 ++++++++++--------- tests/test_visualize.py | 6 +-- 7 files changed, 58 insertions(+), 45 deletions(-) diff --git a/src/tranquilo/aggregate_models.py b/src/tranquilo/aggregate_models.py index d5ea4d6..adff6db 100644 --- a/src/tranquilo/aggregate_models.py +++ b/src/tranquilo/aggregate_models.py @@ -69,7 +69,7 @@ def aggregator_identity(vector_model): """ n_params = vector_model.linear_terms.size - intercept = float(vector_model.intercepts) + intercept = float(vector_model.intercepts[0]) linear_terms = vector_model.linear_terms.flatten() if vector_model.square_terms is None: square_terms = np.zeros((n_params, n_params)) diff --git a/src/tranquilo/sample_points.py b/src/tranquilo/sample_points.py index 5ff0745..292b608 100644 --- a/src/tranquilo/sample_points.py +++ b/src/tranquilo/sample_points.py @@ -374,7 +374,7 @@ def _minimal_pairwise_distance_on_hull( x = _project_onto_unit_hull(x, trustregion_shape=trustregion_shape) if existing_xs is not None: - sample = np.row_stack([x, existing_xs]) + sample = np.vstack([x, existing_xs]) n_existing_pairs = len(existing_xs) * (len(existing_xs) - 1) // 2 slc = slice(0, -n_existing_pairs) if n_existing_pairs else slice(None) else: @@ -416,7 +416,7 @@ def _determinant_on_hull(x, existing_xs, trustregion_shape, n_params): x = _project_onto_unit_hull(x, trustregion_shape=trustregion_shape) if existing_xs is not None: - sample = np.row_stack([x, existing_xs]) + sample = np.vstack([x, existing_xs]) else: sample = x diff --git a/src/tranquilo/subsolvers/fallback_subsolvers.py b/src/tranquilo/subsolvers/fallback_subsolvers.py index 2283973..742a34c 100644 --- a/src/tranquilo/subsolvers/fallback_subsolvers.py +++ b/src/tranquilo/subsolvers/fallback_subsolvers.py @@ -1,6 +1,6 @@ import numpy as np from functools import partial -from scipy.optimize import Bounds, NonlinearConstraint, minimize +from scipy.optimize import Bounds, minimize from tranquilo.exploration_sample import draw_exploration_sample @@ -184,17 +184,26 @@ def _grad(x, g, h): return crit, grad +# def _get_constraint(): +# def _constr_fun(x): +# return x @ x + +# def _constr_jac(x): +# return 2 * x + +# return NonlinearConstraint( +# fun=_constr_fun, +# lb=-np.inf, +# ub=1, +# jac=_constr_jac, +# keep_feasible=True, +# ) + + def _get_constraint(): - def _constr_fun(x): - return x @ x - - def _constr_jac(x): - return 2 * x - - return NonlinearConstraint( - fun=_constr_fun, - lb=-np.inf, - ub=1, - jac=_constr_jac, - keep_feasible=True, - ) + """Constraint enforcing ||x||^2 <= 1 as a simple inequality for SLSQP.""" + return { + "type": "ineq", + "fun": lambda x: 1 - x @ x, + "jac": lambda x: -2 * x, + } diff --git a/tests/subsolvers/test_gqtpar_lambdas.py b/tests/subsolvers/test_gqtpar_lambdas.py index c8a0a05..5be7840 100644 --- a/tests/subsolvers/test_gqtpar_lambdas.py +++ b/tests/subsolvers/test_gqtpar_lambdas.py @@ -10,7 +10,7 @@ def test_gqtpar_lambdas(): algo_options = { "disable_convergence": True, - "stopping_max_iterations": 30, + "stopping_maxiter": 30, "sample_filter": "keep_all", "sampler": "random_hull", "subsolver_options": {"k_hard": 0.001, "k_easy": 0.001}, @@ -18,7 +18,7 @@ def test_gqtpar_lambdas(): problem_info = get_benchmark_problems("more_wild")["freudenstein_roth_good_start"] minimize( - criterion=problem_info["inputs"]["fun"], + fun=problem_info["inputs"]["fun"], params=problem_info["inputs"]["params"], algo_options=algo_options, algorithm="tranquilo", diff --git a/tests/test_fit_models.py b/tests/test_fit_models.py index b5427eb..fa2887c 100644 --- a/tests/test_fit_models.py +++ b/tests/test_fit_models.py @@ -121,7 +121,7 @@ def test_fit_ols_against_gradient(model, quadratic_case): grad = a + hess @ quadratic_case["x0"] gradient = first_derivative(quadratic_case["func"], quadratic_case["x0"]) - aaae(gradient["derivative"], grad, case="gradient") + aaae(gradient.derivative, grad, case="gradient") @pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") @@ -143,7 +143,7 @@ def test_fit_ols_against_hessian(model, quadratic_case): ) hessian = second_derivative(quadratic_case["func"], quadratic_case["x0"]) hess = got.square_terms.reshape((4, 4)) - aaae(hessian["derivative"], hess, case="hessian") + aaae(hessian.derivative, hess, case="hessian") def test_quadratic_features(): diff --git a/tests/test_tranquilo.py b/tests/test_tranquilo.py index 4339ee4..1882eea 100644 --- a/tests/test_tranquilo.py +++ b/tests/test_tranquilo.py @@ -126,7 +126,7 @@ def test_internal_tranquilo_scalar_sphere_imprecise_defaults( @pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") def test_external_tranquilo_scalar_sphere_defaults(): res = minimize( - criterion=lambda x: x @ x, + fun=lambda x: x @ x, params=np.arange(4), algorithm="tranquilo", ) @@ -180,7 +180,7 @@ def test_internal_tranquilo_ls_sphere_defaults( @pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") def test_external_tranquilo_ls_sphere_defaults(): res = minimize( - criterion=mark.least_squares(lambda x: x), + fun=mark.least_squares(lambda x: x), params=np.arange(5), algorithm="tranquilo_ls", ) @@ -194,15 +194,18 @@ def test_external_tranquilo_ls_sphere_defaults(): @pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") -@pytest.mark.parametrize("algo", ["tranquilo", "tranquilo_ls"]) -def test_tranquilo_with_noise_handling_and_deterministic_function(algo): - def _f(x): - return {"root_contributions": x, "value": x @ x} - +@pytest.mark.parametrize( + "algorithm, criterion", + [ + ("tranquilo", mark.scalar(lambda x: x @ x)), + ("tranquilo_ls", mark.least_squares(lambda x: x)), + ], +) +def test_tranquilo_with_noise_handling_and_deterministic_function(algorithm, criterion): res = minimize( - criterion=_f, + fun=criterion, params=np.arange(5), - algorithm=algo, + algorithm=algorithm, algo_options={"noisy": True}, ) @@ -214,12 +217,13 @@ def _f(x): def test_tranquilo_ls_with_noise_handling_and_noisy_function(): rng = np.random.default_rng(123) + @mark.least_squares def _f(x): x_n = x + rng.normal(0, 0.05, size=x.shape) - return {"root_contributions": x_n, "value": x_n @ x_n} + return x_n res = minimize( - criterion=_f, + fun=_f, params=np.ones(3), algorithm="tranquilo_ls", algo_options={"noisy": True, "n_evals_per_point": 10}, @@ -233,19 +237,19 @@ def _f(x): # ====================================================================================== -def sum_of_squares(x): - contribs = x**2 - return {"value": contribs.sum(), "contributions": contribs, "root_contributions": x} - - @pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") -@pytest.mark.parametrize("algorithm", ["tranquilo", "tranquilo_ls"]) -def test_tranquilo_with_binding_bounds(algorithm): +@pytest.mark.parametrize( + "algorithm, criterion", + [ + ("tranquilo", mark.scalar(lambda x: x @ x)), + ("tranquilo_ls", mark.least_squares(lambda x: x)), + ], +) +def test_tranquilo_with_binding_bounds(algorithm, criterion): res = minimize( - criterion=sum_of_squares, + fun=criterion, params=np.array([3, 2, -3]), - lower_bounds=np.array([1, -np.inf, -np.inf]), - upper_bounds=np.array([np.inf, np.inf, -1]), + bounds=[(1, np.inf), (-np.inf, np.inf), (-np.inf, -1)], algorithm=algorithm, collect_history=True, skip_checks=True, diff --git a/tests/test_visualize.py b/tests/test_visualize.py index bafbdf0..67084e5 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -12,13 +12,13 @@ "sampler": "random_hull", "sphere_subsolver": "gqtpar_fast", "sample_filter": "keep_all", - "stopping_max_iterations": 10, + "stopping_maxiter": 10, }, "optimal_hull": { "sampler": "optimal_hull", "sphere_subsolver": "gqtpar_fast", "sample_filter": "keep_all", - "stopping_max_iterations": 10, + "stopping_maxiter": 10, }, } for problem in ["rosenbrock_good_start", "watson_6_good_start"]: @@ -29,7 +29,7 @@ results = {} for s, options in algo_options.items(): results[s] = minimize( - criterion=fun, + fun=fun, params=start_params, algo_options=options, algorithm=algorithm, From 729e1abcbb41a2f0f6f47303be81b939c533aede Mon Sep 17 00:00:00 2001 From: Sebastian Gsell Date: Tue, 24 Jun 2025 18:19:11 +0200 Subject: [PATCH 06/20] Add note on former scipy warning --- .../subsolvers/fallback_subsolvers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/tranquilo/subsolvers/fallback_subsolvers.py b/src/tranquilo/subsolvers/fallback_subsolvers.py index 742a34c..109b58b 100644 --- a/src/tranquilo/subsolvers/fallback_subsolvers.py +++ b/src/tranquilo/subsolvers/fallback_subsolvers.py @@ -184,7 +184,17 @@ def _grad(x, g, h): return crit, grad +def _get_constraint(): + """Constraint enforcing ||x||^2 <= 1 as a simple inequality for SLSQP.""" + return { + "type": "ineq", + "fun": lambda x: 1 - x @ x, + "jac": lambda x: -2 * x, + } + + # def _get_constraint(): +# """Raises scipy warning.""" # def _constr_fun(x): # return x @ x @@ -198,12 +208,3 @@ def _grad(x, g, h): # jac=_constr_jac, # keep_feasible=True, # ) - - -def _get_constraint(): - """Constraint enforcing ||x||^2 <= 1 as a simple inequality for SLSQP.""" - return { - "type": "ineq", - "fun": lambda x: 1 - x @ x, - "jac": lambda x: -2 * x, - } From f5ad15996f856fc20cdaf8f32322d815af58df49 Mon Sep 17 00:00:00 2001 From: Sebastian Gsell Date: Mon, 30 Jun 2025 13:27:56 +0200 Subject: [PATCH 07/20] Replace OptimizeResult with dummy object for attribute check --- src/tranquilo/visualize.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/tranquilo/visualize.py b/src/tranquilo/visualize.py index ca2092b..a8db4f2 100644 --- a/src/tranquilo/visualize.py +++ b/src/tranquilo/visualize.py @@ -8,11 +8,12 @@ from plotly import graph_objects as go from plotly.subplots import make_subplots -from optimagic.optimization.optimize_result import OptimizeResult from tranquilo.clustering import cluster from tranquilo.geometry import log_d_quality_calculator from tranquilo.volume import get_radius_after_volume_scaling +from typing import Any, Protocol, runtime_checkable + def visualize_tranquilo(results, iterations): """Plot diagnostic information of optimization result in given iteration(s). @@ -56,7 +57,7 @@ def visualize_tranquilo(results, iterations): if isinstance(iterations, int): iterations = {case: iterations for case in results} results = {case: _process_results(results[case]) for case in results} - elif isinstance(results, OptimizeResult): + elif isinstance(results, OptimizeResultLike): results = _process_results(results) results = {f"iteration {i}": results for i in iterations} iterations = {f"iteration {iteration}": iteration for iteration in iterations} @@ -588,3 +589,13 @@ def _get_model_indices(xs, state): for point in state.model_points: model_indices = np.concatenate([model_indices, _find_index(xs, point)]) return model_indices.astype(int) + + +@runtime_checkable +class OptimizeResultLike(Protocol): + """Runtime-checkable stand-in for optimagic's OptimizeResult object.""" + + algorithm: str + history: Any + params: Any + algorithm_output: dict From 75a10c4f2cf39ab254caaafde56fa5fea21cac19 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 17:17:26 +0100 Subject: [PATCH 08/20] Use most recent batch evaluators --- src/tranquilo/batch_evaluators.py | 129 ++++++++++++++++++++++++++++-- src/tranquilo/config.py | 14 ++-- src/tranquilo/decorators.py | 3 + src/tranquilo/wrap_criterion.py | 22 +---- 4 files changed, 133 insertions(+), 35 deletions(-) diff --git a/src/tranquilo/batch_evaluators.py b/src/tranquilo/batch_evaluators.py index a549c2a..d75dc5a 100644 --- a/src/tranquilo/batch_evaluators.py +++ b/src/tranquilo/batch_evaluators.py @@ -1,7 +1,10 @@ """A collection of batch evaluators for process based parallelism. +The following module is taken from optimagic/batch_evaluators.py and slightly modified +to fit into tranquilo. + All batch evaluators have the same interface and any function with the same interface -can be used used as batch evaluator. +can be used used as batch evaluator in optimagic. """ @@ -14,23 +17,38 @@ except ImportError: pathos_is_available = False -from typing import Any, Callable, Literal, TypeVar +import threading +from typing import Any, Callable, Literal, TypeVar, Protocol, cast from tranquilo.config import DEFAULT_N_CORES as N_CORES from tranquilo.decorators import catch, unpack from tranquilo.options import ErrorHandling +BatchEvaluatorLiteral = Literal["joblib", "pathos", "threading"] + T = TypeVar("T") +class BatchEvaluator(Protocol): + def __call__( + self, + func: Callable[..., T], + arguments: list[Any], + n_cores: int = 1, + error_handling: ErrorHandling + | Literal["raise", "continue"] = ErrorHandling.CONTINUE, + unpack_symbol: Literal["*", "**"] | None = None, + ) -> list[T]: + pass + + def pathos_mp_batch_evaluator( func: Callable[..., T], arguments: list[Any], *, n_cores: int = N_CORES, - error_handling: ( - ErrorHandling | Literal["raise", "continue"] - ) = ErrorHandling.CONTINUE, + error_handling: ErrorHandling + | Literal["raise", "continue"] = ErrorHandling.CONTINUE, unpack_symbol: Literal["*", "**"] | None = None, ) -> list[T]: """Batch evaluator based on pathos.multiprocess.ProcessPool. @@ -98,9 +116,8 @@ def joblib_batch_evaluator( arguments: list[Any], *, n_cores: int = N_CORES, - error_handling: ( - ErrorHandling | Literal["raise", "continue"] - ) = ErrorHandling.CONTINUE, + error_handling: ErrorHandling + | Literal["raise", "continue"] = ErrorHandling.CONTINUE, unpack_symbol: Literal["*", "**"] | None = None, ) -> list[T]: """Batch evaluator based on joblib's Parallel. @@ -149,6 +166,79 @@ def internal_func(*args: Any, **kwargs: Any) -> T: return res +def threading_batch_evaluator( + func: Callable[..., T], + arguments: list[Any], + *, + n_cores: int = N_CORES, + error_handling: ErrorHandling + | Literal["raise", "continue"] = ErrorHandling.CONTINUE, + unpack_symbol: Literal["*", "**"] | None = None, +) -> list[T]: + """Batch evaluator based on Python's threading. + + Args: + func (Callable): The function that is evaluated. + arguments (Iterable): Arguments for the functions. Their interperation + depends on the unpack argument. + n_cores (int): Number of threads used to evaluate the function in parallel. + Value below one are interpreted as one. + error_handling (str): Can take the values "raise" (raise the error and stop all + tasks as soon as one task fails) and "continue" (catch exceptions and set + the output of failed tasks to the traceback of the raised exception. + KeyboardInterrupt and SystemExit are always raised. + unpack_symbol (str or None). Can be "**", "*" or None. If None, func just takes + one argument. If "*", the elements of arguments are positional arguments for + func. If "**", the elements of arguments are keyword arguments for func. + + Returns: + list: The function evaluations. + + """ + _check_inputs(func, arguments, n_cores, error_handling, unpack_symbol) + n_cores = int(n_cores) if int(n_cores) >= 2 else 1 + + reraise = error_handling in [ + "raise", + ErrorHandling.RAISE, + ErrorHandling.RAISE_STRICT, + ] + + @unpack(symbol=unpack_symbol) + @catch(default="__traceback__", reraise=reraise) + def internal_func(*args: Any, **kwargs: Any) -> T: + return func(*args, **kwargs) + + if n_cores == 1: + res = [internal_func(arg) for arg in arguments] + else: + results = [None] * len(arguments) + threads = [] + errors = [] + error_lock = threading.Lock() + + def thread_func(index: int, arg: Any) -> None: + try: + results[index] = internal_func(arg) + except Exception as e: + with error_lock: + errors.append(e) + + for i, arg in enumerate(arguments): + thread = threading.Thread(target=thread_func, args=(i, arg)) + threads.append(thread) + thread.start() + + for thread in threads: + thread.join() + + if errors: + raise errors[0] + + res = cast(list[T], results) + return res + + def _check_inputs( func: Callable[..., T], arguments: list[Any], @@ -185,3 +275,26 @@ def _check_inputs( "error_handling must be 'raise' or 'continue' or ErrorHandling not " f"{error_handling}" ) + + +def process_batch_evaluator( + batch_evaluator: BatchEvaluatorLiteral | BatchEvaluator = "joblib", +) -> BatchEvaluator: + if callable(batch_evaluator): + out = batch_evaluator + elif isinstance(batch_evaluator, str): + if batch_evaluator == "joblib": + out = cast(BatchEvaluator, joblib_batch_evaluator) + elif batch_evaluator == "pathos": + out = cast(BatchEvaluator, pathos_mp_batch_evaluator) + elif batch_evaluator == "threading": + out = cast(BatchEvaluator, threading_batch_evaluator) + else: + raise ValueError( + "Invalid batch evaluator requested. Currently only 'pathos', 'joblib', " + "and 'threading' are supported." + ) + else: + raise TypeError("batch_evaluator must be a callable or string.") + + return out diff --git a/src/tranquilo/config.py b/src/tranquilo/config.py index 99dbfc6..406da02 100644 --- a/src/tranquilo/config.py +++ b/src/tranquilo/config.py @@ -1,4 +1,5 @@ from pathlib import Path +import importlib.util import plotly.express as px @@ -22,12 +23,13 @@ # Check Available Packages # ====================================================================================== -try: - import optimagic # noqa: F401 -except ImportError: - IS_OPTIMAGIC_INSTALLED = False -else: - IS_OPTIMAGIC_INSTALLED = True + +def _is_installed(module_name: str) -> bool: + """Return True if the given module is installed, otherwise False.""" + return importlib.util.find_spec(module_name) is not None + + +IS_OPTIMAGIC_INSTALLED = _is_installed("optimagic") # ================================================================================= diff --git a/src/tranquilo/decorators.py b/src/tranquilo/decorators.py index 296ea1f..1054821 100644 --- a/src/tranquilo/decorators.py +++ b/src/tranquilo/decorators.py @@ -1,5 +1,8 @@ """This module contains various decorators. +The following module is taken from optimagic/decorators.py and slightly modified +to fit into tranquilo. + There are two kinds of decorators defined in this module which consists of either two or three nested functions. The former are decorators without and the latter with arguments. diff --git a/src/tranquilo/wrap_criterion.py b/src/tranquilo/wrap_criterion.py index 0622a77..3ce05d9 100644 --- a/src/tranquilo/wrap_criterion.py +++ b/src/tranquilo/wrap_criterion.py @@ -1,6 +1,7 @@ import functools import numpy as np +from tranquilo.batch_evaluators import process_batch_evaluator def get_wrapped_criterion(criterion, batch_evaluator, n_cores, history): @@ -71,24 +72,3 @@ def wrapper_criterion(eval_info): ) return wrapper_criterion - - -def process_batch_evaluator(batch_evaluator="joblib"): - batch_evaluator = "joblib" if batch_evaluator is None else batch_evaluator - - if callable(batch_evaluator): - out = batch_evaluator - elif isinstance(batch_evaluator, str): - if batch_evaluator == "joblib": - from tranquilo.batch_evaluators import joblib_batch_evaluator as out - elif batch_evaluator == "pathos": - from tranquilo.batch_evaluators import pathos_mp_batch_evaluator as out - else: - raise ValueError( - "Invalid batch evaluator requested. Currently only 'pathos' and " - "'joblib' are supported." - ) - else: - raise TypeError("batch_evaluator must be a callable or string.") - - return out From 19d2b15ac3fa209cda4525732068278fe8c025ec Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 18:06:30 +0100 Subject: [PATCH 09/20] Add argument to tranquilo --- src/tranquilo/batch_evaluators.py | 300 ------------------ src/tranquilo/decorators.py | 129 -------- src/tranquilo/process_arguments.py | 24 +- .../subsolvers/fallback_subsolvers.py | 34 +- src/tranquilo/visualize.py | 2 +- src/tranquilo/wrap_criterion.py | 49 ++- tests/test_process_arguments.py | 2 +- tests/test_tranquilo.py | 6 +- tests/test_visualize.py | 7 - tests/test_wrap_criterion.py | 12 +- 10 files changed, 68 insertions(+), 497 deletions(-) delete mode 100644 src/tranquilo/batch_evaluators.py delete mode 100644 src/tranquilo/decorators.py diff --git a/src/tranquilo/batch_evaluators.py b/src/tranquilo/batch_evaluators.py deleted file mode 100644 index d75dc5a..0000000 --- a/src/tranquilo/batch_evaluators.py +++ /dev/null @@ -1,300 +0,0 @@ -"""A collection of batch evaluators for process based parallelism. - -The following module is taken from optimagic/batch_evaluators.py and slightly modified -to fit into tranquilo. - -All batch evaluators have the same interface and any function with the same interface -can be used used as batch evaluator in optimagic. - -""" - -from joblib import Parallel, delayed - -try: - from pathos.pools import ProcessPool - - pathos_is_available = True -except ImportError: - pathos_is_available = False - -import threading -from typing import Any, Callable, Literal, TypeVar, Protocol, cast - -from tranquilo.config import DEFAULT_N_CORES as N_CORES -from tranquilo.decorators import catch, unpack -from tranquilo.options import ErrorHandling - -BatchEvaluatorLiteral = Literal["joblib", "pathos", "threading"] - -T = TypeVar("T") - - -class BatchEvaluator(Protocol): - def __call__( - self, - func: Callable[..., T], - arguments: list[Any], - n_cores: int = 1, - error_handling: ErrorHandling - | Literal["raise", "continue"] = ErrorHandling.CONTINUE, - unpack_symbol: Literal["*", "**"] | None = None, - ) -> list[T]: - pass - - -def pathos_mp_batch_evaluator( - func: Callable[..., T], - arguments: list[Any], - *, - n_cores: int = N_CORES, - error_handling: ErrorHandling - | Literal["raise", "continue"] = ErrorHandling.CONTINUE, - unpack_symbol: Literal["*", "**"] | None = None, -) -> list[T]: - """Batch evaluator based on pathos.multiprocess.ProcessPool. - - This uses a patched but older version of python multiprocessing that replaces - pickling with dill and can thus handle decorated functions. - - Args: - func (Callable): The function that is evaluated. - arguments (Iterable): Arguments for the functions. Their interperation - depends on the unpack argument. - n_cores (int): Number of cores used to evaluate the function in parallel. - Value below one are interpreted as one. If only one core is used, the - batch evaluator disables everything that could cause problems, i.e. in that - case func and arguments are never pickled and func is executed in the main - process. - error_handling (str): Can take the values "raise" (raise the error and stop all - tasks as soon as one task fails) and "continue" (catch exceptions and set - the traceback of the raised exception. - KeyboardInterrupt and SystemExit are always raised. - unpack_symbol (str or None). Can be "**", "*" or None. If None, func just takes - one argument. If "*", the elements of arguments are positional arguments for - func. If "**", the elements of arguments are keyword arguments for func. - - - Returns: - list: The function evaluations. - - """ - if not pathos_is_available: - raise NotImplementedError( - "To use the pathos_mp_batch_evaluator, install pathos with " - "conda install -c conda-forge pathos." - ) - - _check_inputs(func, arguments, n_cores, error_handling, unpack_symbol) - n_cores = int(n_cores) - - reraise = error_handling in [ - "raise", - ErrorHandling.RAISE, - ErrorHandling.RAISE_STRICT, - ] - - @unpack(symbol=unpack_symbol) - @catch(default="__traceback__", reraise=reraise) - def internal_func(*args: Any, **kwargs: Any) -> T: - return func(*args, **kwargs) - - if n_cores <= 1: - res = [internal_func(arg) for arg in arguments] - else: - p = ProcessPool(nodes=n_cores) - try: - res = p.map(internal_func, arguments) - except Exception as e: - p.terminate() - raise e - - return res - - -def joblib_batch_evaluator( - func: Callable[..., T], - arguments: list[Any], - *, - n_cores: int = N_CORES, - error_handling: ErrorHandling - | Literal["raise", "continue"] = ErrorHandling.CONTINUE, - unpack_symbol: Literal["*", "**"] | None = None, -) -> list[T]: - """Batch evaluator based on joblib's Parallel. - - Args: - func (Callable): The function that is evaluated. - arguments (Iterable): Arguments for the functions. Their interperation - depends on the unpack argument. - n_cores (int): Number of cores used to evaluate the function in parallel. - Value below one are interpreted as one. If only one core is used, the - batch evaluator disables everything that could cause problems, i.e. in that - case func and arguments are never pickled and func is executed in the main - process. - error_handling (str): Can take the values "raise" (raise the error and stop all - tasks as soon as one task fails) and "continue" (catch exceptions and set - the output of failed tasks to the traceback of the raised exception. - KeyboardInterrupt and SystemExit are always raised. - unpack_symbol (str or None). Can be "**", "*" or None. If None, func just takes - one argument. If "*", the elements of arguments are positional arguments for - func. If "**", the elements of arguments are keyword arguments for func. - - - Returns: - list: The function evaluations. - - """ - _check_inputs(func, arguments, n_cores, error_handling, unpack_symbol) - n_cores = int(n_cores) if int(n_cores) >= 2 else 1 - - reraise = error_handling in [ - "raise", - ErrorHandling.RAISE, - ErrorHandling.RAISE_STRICT, - ] - - @unpack(symbol=unpack_symbol) - @catch(default="__traceback__", reraise=reraise) - def internal_func(*args: Any, **kwargs: Any) -> T: - return func(*args, **kwargs) - - if n_cores == 1: - res = [internal_func(arg) for arg in arguments] - else: - res = Parallel(n_jobs=n_cores)(delayed(internal_func)(arg) for arg in arguments) - - return res - - -def threading_batch_evaluator( - func: Callable[..., T], - arguments: list[Any], - *, - n_cores: int = N_CORES, - error_handling: ErrorHandling - | Literal["raise", "continue"] = ErrorHandling.CONTINUE, - unpack_symbol: Literal["*", "**"] | None = None, -) -> list[T]: - """Batch evaluator based on Python's threading. - - Args: - func (Callable): The function that is evaluated. - arguments (Iterable): Arguments for the functions. Their interperation - depends on the unpack argument. - n_cores (int): Number of threads used to evaluate the function in parallel. - Value below one are interpreted as one. - error_handling (str): Can take the values "raise" (raise the error and stop all - tasks as soon as one task fails) and "continue" (catch exceptions and set - the output of failed tasks to the traceback of the raised exception. - KeyboardInterrupt and SystemExit are always raised. - unpack_symbol (str or None). Can be "**", "*" or None. If None, func just takes - one argument. If "*", the elements of arguments are positional arguments for - func. If "**", the elements of arguments are keyword arguments for func. - - Returns: - list: The function evaluations. - - """ - _check_inputs(func, arguments, n_cores, error_handling, unpack_symbol) - n_cores = int(n_cores) if int(n_cores) >= 2 else 1 - - reraise = error_handling in [ - "raise", - ErrorHandling.RAISE, - ErrorHandling.RAISE_STRICT, - ] - - @unpack(symbol=unpack_symbol) - @catch(default="__traceback__", reraise=reraise) - def internal_func(*args: Any, **kwargs: Any) -> T: - return func(*args, **kwargs) - - if n_cores == 1: - res = [internal_func(arg) for arg in arguments] - else: - results = [None] * len(arguments) - threads = [] - errors = [] - error_lock = threading.Lock() - - def thread_func(index: int, arg: Any) -> None: - try: - results[index] = internal_func(arg) - except Exception as e: - with error_lock: - errors.append(e) - - for i, arg in enumerate(arguments): - thread = threading.Thread(target=thread_func, args=(i, arg)) - threads.append(thread) - thread.start() - - for thread in threads: - thread.join() - - if errors: - raise errors[0] - - res = cast(list[T], results) - return res - - -def _check_inputs( - func: Callable[..., T], - arguments: list[Any], - n_cores: int, - error_handling: ErrorHandling | Literal["raise", "continue"], - unpack_symbol: Literal["*", "**"] | None, -) -> None: - if not callable(func): - raise TypeError("func must be callable.") - - try: - arguments = list(arguments) - except Exception as e: - raise ValueError("arguments must be list like.") from e - - try: - int(n_cores) - except Exception as e: - raise ValueError("n_cores must be an integer.") from e - - if unpack_symbol not in (None, "*", "**"): - raise ValueError( - f"unpack_symbol must be None, '*' or '**', not {unpack_symbol}" - ) - - if error_handling not in [ - "raise", - "continue", - ErrorHandling.RAISE, - ErrorHandling.CONTINUE, - ErrorHandling.RAISE_STRICT, - ]: - raise ValueError( - "error_handling must be 'raise' or 'continue' or ErrorHandling not " - f"{error_handling}" - ) - - -def process_batch_evaluator( - batch_evaluator: BatchEvaluatorLiteral | BatchEvaluator = "joblib", -) -> BatchEvaluator: - if callable(batch_evaluator): - out = batch_evaluator - elif isinstance(batch_evaluator, str): - if batch_evaluator == "joblib": - out = cast(BatchEvaluator, joblib_batch_evaluator) - elif batch_evaluator == "pathos": - out = cast(BatchEvaluator, pathos_mp_batch_evaluator) - elif batch_evaluator == "threading": - out = cast(BatchEvaluator, threading_batch_evaluator) - else: - raise ValueError( - "Invalid batch evaluator requested. Currently only 'pathos', 'joblib', " - "and 'threading' are supported." - ) - else: - raise TypeError("batch_evaluator must be a callable or string.") - - return out diff --git a/src/tranquilo/decorators.py b/src/tranquilo/decorators.py deleted file mode 100644 index 1054821..0000000 --- a/src/tranquilo/decorators.py +++ /dev/null @@ -1,129 +0,0 @@ -"""This module contains various decorators. - -The following module is taken from optimagic/decorators.py and slightly modified -to fit into tranquilo. - -There are two kinds of decorators defined in this module which consists of either two or -three nested functions. The former are decorators without and the latter with arguments. - -For more information on decorators, see this `guide -`_ on https://realpython.com - -which -provides a comprehensive overview. - -.. _guide: - -https://realpython.com/primer-on-python-decorators/ - -""" - -import sys -from traceback import format_exception - -import functools -import warnings - - -def catch( - func=None, - *, - exception=Exception, - exclude=(KeyboardInterrupt, SystemExit), - onerror=None, - default=None, - warn=True, - reraise=False, -): - """Catch and handle exceptions. - - This decorator can be used with and without additional arguments. - - Args: - exception (Exception or tuple): One or several exceptions that - are caught and handled. By default all Exceptions are - caught and handled. - exclude (Exception or tuple): One or several exceptionts that - are not caught. By default those are KeyboardInterrupt and - SystemExit. - onerror (None or Callable): Callable that takes an Exception - as only argument. This is called when an exception occurs. - default: Value that is returned when as the output of func when - an exception occurs. Can be one of the following: - - a constant - - "__traceback__", in this case a string with a traceback is returned. - - callable with the same signature as func. - warn (bool): If True, the exception is converted to a warning. - reraise (bool): If True, the exception is raised after handling it. - - """ - - def decorator_catch(func): - @functools.wraps(func) - def wrapper_catch(*args, **kwargs): - try: - res = func(*args, **kwargs) - except exclude: - raise - except exception as e: - if onerror is not None: - onerror(e) - - if reraise: - raise e - - tb = get_traceback() - - if warn: - msg = f"The following exception was caught:\n\n{tb}" - warnings.warn(msg) - - if default == "__traceback__": - res = tb - elif callable(default): - res = default(*args, **kwargs) - else: - res = default - return res - - return wrapper_catch - - if callable(func): - return decorator_catch(func) - else: - return decorator_catch - - -def unpack(func=None, symbol=None): - def decorator_unpack(func): - if symbol is None: - - @functools.wraps(func) - def wrapper_unpack(arg): - return func(arg) - - elif symbol == "*": - - @functools.wraps(func) - def wrapper_unpack(arg): - return func(*arg) - - elif symbol == "**": - - @functools.wraps(func) - def wrapper_unpack(arg): - return func(**arg) - - return wrapper_unpack - - if callable(func): - return decorator_unpack(func) - else: - return decorator_unpack - - -def get_traceback(): - tb = format_exception(*sys.exc_info()) - if isinstance(tb, list): - tb = "".join(tb) - return tb diff --git a/src/tranquilo/process_arguments.py b/src/tranquilo/process_arguments.py index 579fd65..d9dfcbb 100644 --- a/src/tranquilo/process_arguments.py +++ b/src/tranquilo/process_arguments.py @@ -36,12 +36,13 @@ def process_arguments( # functype, will be partialled out functype, - # problem description - criterion, - x, + # problem description - either batch_fun or fun must be provided + batch_fun=None, + x=None, lower_bounds=None, upper_bounds=None, *, + fun=None, # basic options noisy=False, # convergence options @@ -58,7 +59,6 @@ def process_arguments( stopping_max_iterations=200, stopping_max_time=np.inf, # single advanced options - batch_evaluator="joblib", n_cores=1, batch_size=None, sample_size=None, @@ -89,6 +89,18 @@ def process_arguments( infinity_handler="relative", residualize=None, ): + # Handle either batch_fun or fun being provided + if batch_fun is None and fun is None: + raise ValueError("Either batch_fun or fun must be provided.") + if batch_fun is not None and fun is not None: + raise ValueError("Only one of batch_fun or fun should be provided.") + + # If fun is provided, wrap it into a simple batch_fun + if fun is not None: + + def batch_fun(x_list, n_cores, batch_size): + return [fun(x) for x in x_list] + # warning for things that do not work well yet if noisy and functype == "scalar": msg = ( @@ -177,9 +189,9 @@ def process_arguments( history = History(functype=functype) history.add_xs(x) evaluate_criterion = get_wrapped_criterion( - criterion=criterion, - batch_evaluator=batch_evaluator, + batch_fun=batch_fun, n_cores=n_cores, + batch_size=batch_size, history=history, ) _bounds = Bounds(lower_bounds, upper_bounds) diff --git a/src/tranquilo/subsolvers/fallback_subsolvers.py b/src/tranquilo/subsolvers/fallback_subsolvers.py index 109b58b..281225d 100644 --- a/src/tranquilo/subsolvers/fallback_subsolvers.py +++ b/src/tranquilo/subsolvers/fallback_subsolvers.py @@ -1,6 +1,6 @@ import numpy as np from functools import partial -from scipy.optimize import Bounds, minimize +from scipy.optimize import Bounds, minimize, NonlinearConstraint from tranquilo.exploration_sample import draw_exploration_sample @@ -185,26 +185,18 @@ def _grad(x, g, h): def _get_constraint(): - """Constraint enforcing ||x||^2 <= 1 as a simple inequality for SLSQP.""" - return { - "type": "ineq", - "fun": lambda x: 1 - x @ x, - "jac": lambda x: -2 * x, - } + """Raises scipy warning.""" + def _constr_fun(x): + return x @ x -# def _get_constraint(): -# """Raises scipy warning.""" -# def _constr_fun(x): -# return x @ x + def _constr_jac(x): + return 2 * x -# def _constr_jac(x): -# return 2 * x - -# return NonlinearConstraint( -# fun=_constr_fun, -# lb=-np.inf, -# ub=1, -# jac=_constr_jac, -# keep_feasible=True, -# ) + return NonlinearConstraint( + fun=_constr_fun, + lb=-np.inf, + ub=1, + jac=_constr_jac, + keep_feasible=True, + ) diff --git a/src/tranquilo/visualize.py b/src/tranquilo/visualize.py index a8db4f2..4591e4f 100644 --- a/src/tranquilo/visualize.py +++ b/src/tranquilo/visualize.py @@ -125,7 +125,7 @@ def _plot_criterion(history, state, color_dict, fig, row, col): x=np.arange(len(history)), showlegend=False, line_color="#C0C0C0", - name="Criterion", + name="criterion", mode="lines", ), row=row, diff --git a/src/tranquilo/wrap_criterion.py b/src/tranquilo/wrap_criterion.py index 3ce05d9..6011979 100644 --- a/src/tranquilo/wrap_criterion.py +++ b/src/tranquilo/wrap_criterion.py @@ -1,34 +1,33 @@ -import functools - import numpy as np -from tranquilo.batch_evaluators import process_batch_evaluator +from tranquilo.history import History -def get_wrapped_criterion(criterion, batch_evaluator, n_cores, history): - """Wrap the criterion function to do get parallelization and history handling. +def get_wrapped_criterion(batch_fun, n_cores: int, batch_size: int, history: History): + """Wrap the batch function to handle tranquilo's history management. Notes ----- The wrapped criterion function takes a dict mapping x_indices to required numbers of evaluations as only argument. It evaluates the criterion function in parallel and - saves the resulting function evaluations in the history. + saves the resulting function evaluations in the tranquilo history. The wrapped criterion function does not return anything. Args: - criterion (function): The criterion function to wrap. - batch_evaluator (function): The batch evaluator to use. - n_cores (int): The number of cores to use. - history (History): The tranquilo history. + batch_fun (callable): A function that takes (x_list, n_cores, batch_size) and + returns a list of function values. When called from optimagic, this is + InternalOptimizationProblem.batch_fun which handles parallelization and + error handling internally. + n_cores: The number of cores to use. + batch_size: The batch size for parallel evaluation. + history: The tranquilo history. Returns: callable: The wrapped criterion function. """ - batch_evaluator = process_batch_evaluator(batch_evaluator) - @functools.wraps(criterion) def wrapper_criterion(eval_info): if not isinstance(eval_info, dict): raise ValueError("eval_info must be a dict.") @@ -42,28 +41,22 @@ def wrapper_criterion(eval_info): xs = history.get_xs(x_indices) xs = np.repeat(xs, repetitions, axis=0) - arguments = list(xs) - - effective_n_cores = min(n_cores, len(arguments)) + x_list = list(xs) - raw_evals = batch_evaluator( - criterion, - arguments=arguments, - n_cores=effective_n_cores, - error_handling="continue", - ) + effective_n_cores = min(n_cores, len(x_list)) - # The batch evaluator replaces exceptions with their traceback (str) when - # error_handling="continue". We replace these cases with infinity. - raw_evals_with_replaced_traceback = [ - np.inf if isinstance(x, str) else x for x in raw_evals - ] + # Call the batch function directly - it handles parallelization and error + # handling internally. When called from optimagic, this also populates + # optimagic's history automatically. + raw_evals = batch_fun(x_list, effective_n_cores, batch_size) # replace NaNs but keep infinite values. NaNs would be problematic in many - # places, infs are only a problem in model fitting and will be handled there + # places, infs are only a problem in model fitting and will be handled there. + # Note: when using optimagic's batch_fun, errors are already handled and + # replaced with penalty values, so we don't need to check for tracebacks. clipped_evals = [ np.nan_to_num(critval, nan=np.inf, posinf=np.inf, neginf=-np.inf) - for critval in raw_evals_with_replaced_traceback + for critval in raw_evals ] history.add_evals( diff --git a/tests/test_process_arguments.py b/tests/test_process_arguments.py index 7841593..98b3129 100644 --- a/tests/test_process_arguments.py +++ b/tests/test_process_arguments.py @@ -22,7 +22,7 @@ def test_process_arguments_scalar_deterministic(): res = process_arguments( functype="scalar", - criterion=lambda x: x @ x, + fun=lambda x: x @ x, x=np.array([-3, 1, 2]), radius_options={"initial_radius": 1.0}, ) diff --git a/tests/test_tranquilo.py b/tests/test_tranquilo.py index 1882eea..1df2f46 100644 --- a/tests/test_tranquilo.py +++ b/tests/test_tranquilo.py @@ -72,7 +72,7 @@ def test_internal_tranquilo_scalar_sphere_defaults( model_type, ): res = tranquilo( - criterion=lambda x: x @ x, + fun=lambda x: x @ x, x=np.arange(4), sample_filter=sample_filter, model_fitter=model_fitter, @@ -109,7 +109,7 @@ def test_internal_tranquilo_scalar_sphere_imprecise_defaults( model_type, ): res = tranquilo( - criterion=lambda x: x @ x, + fun=lambda x: x @ x, x=np.arange(4), sample_filter=sample_filter, model_fitter=model_fitter, @@ -163,7 +163,7 @@ def test_internal_tranquilo_ls_sphere_defaults( model_type, ): res = tranquilo_ls( - criterion=lambda x: x, + fun=lambda x: x, x=np.arange(5), sample_filter=sample_filter, model_fitter=model_fitter, diff --git a/tests/test_visualize.py b/tests/test_visualize.py index 67084e5..eb5aba3 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -37,13 +37,6 @@ cases.append(results) -skip_reason = ( - "History collection of tranquilo and tranquilo_ls is disabled in optimagic, but" - "visualize_tranquilo requires a history." -) - - -@pytest.mark.skip(reason=skip_reason) @pytest.mark.parametrize("results", cases) def test_visualize_tranquilo(results): visualize_tranquilo(results, 5) diff --git a/tests/test_wrap_criterion.py b/tests/test_wrap_criterion.py index c5af149..545a7af 100644 --- a/tests/test_wrap_criterion.py +++ b/tests/test_wrap_criterion.py @@ -9,6 +9,15 @@ TEST_CASES = list(itertools.product(["scalar", "least_squares", "likelihood"], [1, 2])) +def _make_batch_fun(criterion): + """Convert a simple criterion function to a batch_fun for testing.""" + + def batch_fun(x_list, n_cores, batch_size): + return [criterion(x) for x in x_list] + + return batch_fun + + @pytest.mark.parametrize("functype, n_evals", TEST_CASES) def test_wrapped_criterion(functype, n_evals): # set up criterion (all should have same results) @@ -19,6 +28,7 @@ def test_wrapped_criterion(functype, n_evals): } criterion = func_dict[functype] + batch_fun = _make_batch_fun(criterion) # set up history history = History(functype=functype) @@ -29,7 +39,7 @@ def test_wrapped_criterion(functype, n_evals): assert history.get_n_fun() == 2 wrapped_criterion = get_wrapped_criterion( - criterion=criterion, batch_evaluator="joblib", n_cores=1, history=history + batch_fun=batch_fun, n_cores=1, batch_size=1, history=history ) # set up params and expected results From 5babb281dfdbe2d9a91ce089ca2fbff672e1aff1 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 18:13:32 +0100 Subject: [PATCH 10/20] Tackle a few warnings --- src/tranquilo/visualize.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tranquilo/visualize.py b/src/tranquilo/visualize.py index 4591e4f..fd5ee10 100644 --- a/src/tranquilo/visualize.py +++ b/src/tranquilo/visualize.py @@ -84,8 +84,8 @@ def visualize_tranquilo(results, iterations): result = results[case] iteration = iterations[case] state = result.algorithm_output["states"][iteration] - params_history = np.array(result.history["params"]) - criterion_history = np.array(result.history["criterion"]) + params_history = np.array(result.history.params) + criterion_history = np.array(result.history.fun) fig = _plot_sample_points( params_history, state, color_dict, fig, row=1, col=i + 1 ) @@ -320,7 +320,7 @@ def _plot_fekete_criterion(res, fig, row, col, iteration): def _plot_clusters_points_ratio(res, iteration, fig, row, col): dim = res.params.shape[0] - history = np.array(res.history["params"]) + history = np.array(res.history.params) states = res.algorithm_output["states"] colors = [ "rgb(251,106,74)", @@ -423,7 +423,7 @@ def _plot_distances_from_center(history, state, fig, col, rows): def _get_fekete_criterion(res): states = res.algorithm_output["states"][1:] - history = np.array(res.history["params"]) + history = np.array(res.history.params) out = [np.nan] + [ log_d_quality_calculator( @@ -517,7 +517,7 @@ def disable_legend_if_duplicate(trace): def _process_results(result): """Add model indices to states of optimization result.""" result = deepcopy(result) - xs = np.array(result.history["params"]) + xs = np.array(result.history.params) if result.algorithm in ["nag_pybobyqa", "nag_dfols"]: for i in range(1, len(result.algorithm_output["states"])): state = result.algorithm_output["states"][i] From 219c4bbfdf18a9115d0647c36ca2a7146379b117 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 18:25:53 +0100 Subject: [PATCH 11/20] Make plotly an optional dependency --- .envs/testenv-linux.yml | 2 +- .envs/testenv-others.yml | 2 +- environment.yml | 2 +- setup.cfg | 4 +--- src/tranquilo/config.py | 6 +----- src/tranquilo/visualize.py | 16 ++++++++++++---- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.envs/testenv-linux.yml b/.envs/testenv-linux.yml index 61e82b9..17ab824 100644 --- a/.envs/testenv-linux.yml +++ b/.envs/testenv-linux.yml @@ -10,7 +10,7 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - optimagic # run, tests + - optimagic>=0.5.0 # run, tests - numba # run, tests - numpy>=1.17.0 # run, tests - pandas # run, tests diff --git a/.envs/testenv-others.yml b/.envs/testenv-others.yml index 84f89f1..dd799fb 100644 --- a/.envs/testenv-others.yml +++ b/.envs/testenv-others.yml @@ -9,7 +9,7 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - optimagic # run, tests + - optimagic>=0.5.0 # run, tests - numba # run, tests - numpy>=1.17.0 # run, tests - pandas # run, tests diff --git a/environment.yml b/environment.yml index 4d7bf32..5c14934 100644 --- a/environment.yml +++ b/environment.yml @@ -14,7 +14,7 @@ dependencies: - pytest-xdist # dev, tests - setuptools_scm # dev - toml # dev - - optimagic # run, tests + - optimagic>=0.5.0 # run, tests - numba # run, tests - numpy>=1.17.0 # run, tests - pandas # run, tests diff --git a/setup.cfg b/setup.cfg index 7c8eb2c..bee0f33 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,11 +31,9 @@ packages = find: install_requires = numba numpy>=1.17.0 - optimagic>=0.5.1 pandas - plotly scipy>=1.2.1 -python_requires = >=3.8 +python_requires = >=3.10 include_package_data = True package_dir = =src diff --git a/src/tranquilo/config.py b/src/tranquilo/config.py index 406da02..cc1f864 100644 --- a/src/tranquilo/config.py +++ b/src/tranquilo/config.py @@ -1,8 +1,6 @@ from pathlib import Path import importlib.util -import plotly.express as px - DOCS_DIR = Path(__file__).parent.parent / "docs" EXAMPLE_DIR = Path(__file__).parent / "examples" @@ -10,9 +8,6 @@ TEST_FIXTURES_DIR = Path(__file__).parent.parent.parent / "tests" / "fixtures" -PLOTLY_TEMPLATE = "simple_white" -PLOTLY_PALETTE = px.colors.qualitative.Set2 - DEFAULT_N_CORES = 1 CRITERION_PENALTY_SLOPE = 0.1 @@ -30,6 +25,7 @@ def _is_installed(module_name: str) -> bool: IS_OPTIMAGIC_INSTALLED = _is_installed("optimagic") +IS_PLOTLY_INSTALLED = _is_installed("plotly") # ================================================================================= diff --git a/src/tranquilo/visualize.py b/src/tranquilo/visualize.py index fd5ee10..99f42b4 100644 --- a/src/tranquilo/visualize.py +++ b/src/tranquilo/visualize.py @@ -2,18 +2,21 @@ import numpy as np import pandas as pd -import plotly.express as px from numba import njit -from plotly import figure_factory as ff -from plotly import graph_objects as go -from plotly.subplots import make_subplots from tranquilo.clustering import cluster from tranquilo.geometry import log_d_quality_calculator from tranquilo.volume import get_radius_after_volume_scaling +from tranquilo.config import IS_PLOTLY_INSTALLED from typing import Any, Protocol, runtime_checkable +if IS_PLOTLY_INSTALLED: + import plotly.express as px + from plotly import figure_factory as ff + from plotly import graph_objects as go + from plotly.subplots import make_subplots + def visualize_tranquilo(results, iterations): """Plot diagnostic information of optimization result in given iteration(s). @@ -53,6 +56,11 @@ def visualize_tranquilo(results, iterations): iteration. """ + if not IS_PLOTLY_INSTALLED: + raise ImportError( + "Plotly is not installed. Please install plotly to use visualize_tranquilo." + ) + results = deepcopy(results) if isinstance(iterations, int): iterations = {case: iterations for case in results} From 8be21afd3b3763f4208a05c34f9253f460c0a5c0 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 18:42:01 +0100 Subject: [PATCH 12/20] Switch to new packaging structure --- .envs/testenv-linux.yml | 4 +- .envs/testenv-others.yml | 4 +- environment.yml | 11 +-- pyproject.toml | 199 ++++++++++++++++++++++++++++---------- setup.cfg | 47 --------- setup.py | 4 - src/tranquilo/__init__.py | 7 ++ 7 files changed, 165 insertions(+), 111 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.envs/testenv-linux.yml b/.envs/testenv-linux.yml index 17ab824..bc7c237 100644 --- a/.envs/testenv-linux.yml +++ b/.envs/testenv-linux.yml @@ -10,8 +10,10 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - optimagic>=0.5.0 # run, tests + - pytest-timeout # dev, tests + - joblib # run, tests - numba # run, tests + - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests - pandas # run, tests - plotly # run, tests diff --git a/.envs/testenv-others.yml b/.envs/testenv-others.yml index dd799fb..3009c85 100644 --- a/.envs/testenv-others.yml +++ b/.envs/testenv-others.yml @@ -9,8 +9,10 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - optimagic>=0.5.0 # run, tests + - pytest-timeout # dev, tests + - joblib # run, tests - numba # run, tests + - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests - pandas # run, tests - plotly # run, tests diff --git a/environment.yml b/environment.yml index 5c14934..202cb3a 100644 --- a/environment.yml +++ b/environment.yml @@ -6,16 +6,16 @@ channels: dependencies: - python=3.10 # dev - jupyterlab # dev, docs - - nb_black # dev, docs - - pdbpp # dev - pip # dev, tests, docs - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - setuptools_scm # dev + - pytest-timeout # dev, tests + - hatch # dev - toml # dev - - optimagic>=0.5.0 # run, tests + - joblib # run, tests - numba # run, tests + - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests - pandas # run, tests - plotly # run, tests @@ -27,8 +27,7 @@ dependencies: - sphinx-panels # docs - sphinxcontrib-bibtex # docs - pip: # dev, tests, docs - - black # dev - - blackcellmagic # dev - kaleido # dev, tests - pre-commit # dev + - pdbp # dev - -e . # dev diff --git a/pyproject.toml b/pyproject.toml index ed4bf3b..88d7271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,78 +1,164 @@ +# ====================================================================================== +# Project metadata +# ====================================================================================== +[project] +name = "tranquilo" +description = "Trust-region optimizer for scalar, least-square and noisy problems." +requires-python = ">=3.10" +dependencies = [ + "joblib", + "numba", + "numpy>=1.17.0", + "pandas", + "scipy>=1.2.1", +] +dynamic = ["version"] +keywords = [ + "optimization", + "dfo", + "derivative free optimization", + "noisy optimization", + "parallel optimization", + "numerical optimization", + "trust region", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", +] +authors = [ + { name = "Janos Gabler", email = "janos.gabler@gmail.com" }, +] +maintainers = [ + { name = "Janos Gabler", email = "janos.gabler@gmail.com" }, + { name = "Tim Mensinger", email = "mensingertim@gmail.com" }, +] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.license] +text = "MIT" + +[project.urls] +Repository = "https://github.com/OpenSourceEconomics/tranquilo" +Github = "https://github.com/OpenSourceEconomics/tranquilo" +Tracker = "https://github.com/OpenSourceEconomics/tranquilo/issues" + +[project.optional-dependencies] +plotly = ["plotly"] + + +# ====================================================================================== +# Build system configuration +# ====================================================================================== [build-system] -requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"] -build-backend = "setuptools.build_meta" +requires = ["hatchling", "hatch_vcs"] +build-backend = "hatchling.build" + +[tool.hatch.build.hooks.vcs] +version-file = "src/tranquilo/_version.py" + +[tool.hatch.build.targets.sdist] +exclude = ["tests"] +only-packages = true +[tool.hatch.build.targets.wheel] +only-include = ["src"] +sources = ["src"] -[tool.setuptools_scm] -write_to = "src/tranquilo/_version.py" +[tool.hatch.version] +source = "vcs" +[tool.hatch.metadata] +allow-direct-references = true + +# ====================================================================================== +# Ruff configuration +# ====================================================================================== [tool.ruff] -target-version = "py37" +target-version = "py310" fix = true +[tool.ruff.lint] select = [ - # pyflakes - "F", - # pycodestyle - "E", - "W", - # flake8-2020 - "YTT", - # flake8-bugbear - "B", - # flake8-quotes - "Q", - # pylint - "PLE", "PLR", "PLW", - # misc lints - "PIE", - # tidy imports - "TID", - # implicit string concatenation - "ISC", + # isort + "I", + # pyflakes + "F", + # pycodestyle + "E", + "W", + # flake8-2020 + "YTT", + # flake8-bugbear + "B", + # flake8-quotes + "Q", + # pylint + "PLE", + "PLR", + "PLW", + # misc lints + "PIE", + # tidy imports + "TID", + # implicit string concatenation + "ISC", ] extend-ignore = [ - - # allow module import not at top of file, important for notebooks - "E402", - # do not assign a lambda expression, use a def - "E731", - # Too many arguments to function call - "PLR0913", - # Too many returns - "PLR0911", - # Too many branches - "PLR0912", - # Too many statements - "PLR0915", - # Magic number - "PLR2004", - # Consider `elif` instead of `else` then `if` to remove indentation level - "PLR5501", - # For calls to warnings.warn(): No explicit `stacklevel` keyword argument found - "B028", + # allow module import not at top of file, important for notebooks + "E402", + # do not assign a lambda expression, use a def + "E731", + # Too many arguments to function call + "PLR0913", + # Too many returns + "PLR0911", + # Too many branches + "PLR0912", + # Too many statements + "PLR0915", + # Magic number + "PLR2004", + # Consider `elif` instead of `else` then `if` to remove indentation level + "PLR5501", + # For calls to warnings.warn(): No explicit `stacklevel` keyword argument found + "B028", + # Incompatible with formatting + "ISC001", ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "docs/source/conf.py" = ["E501", "ERA001", "DTZ005"] "docs/source/*" = ["B018"] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "google" -[tool.nbqa.config] -black = "pyproject.toml" - -[tool.nbqa.mutate] -black = 1 +# ====================================================================================== +# Pytest configuration +# ====================================================================================== [tool.pytest.ini_options] filterwarnings = [ - "ignore:delta_grad == 0.0", # UserWarning in test_poisedness.py - "ignore:Jupyter is migrating", # DeprecationWarning from jupyter client + "ignore:delta_grad == 0.0", + "ignore:Jupyter is migrating", "ignore:Noisy scalar functions are experimental", + "ignore:Parallelization together with", ] markers = [ "wip: Tests that are work-in-progress.", @@ -81,6 +167,15 @@ markers = [ norecursedirs = ["docs", ".envs"] +# ====================================================================================== +# Misc configuration +# ====================================================================================== +[tool.nbqa.config] +black = "pyproject.toml" + +[tool.nbqa.mutate] +black = 1 + [tool.yamlfix] line_length = 88 sequence_style = "block_style" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index bee0f33..0000000 --- a/setup.cfg +++ /dev/null @@ -1,47 +0,0 @@ -[metadata] -name = tranquilo -description = Trust-region optimizer for scalar, least-square and noisy problems -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/OpenSourceEconomics/tranquilo -author = Janos Gabler -author_email = janos.gabler@gmail.com -license = MIT -license_file = LICENSE -classifiers = - Development Status :: 3 - Alpha - Intended Audience :: Science/Research - License :: OSI Approved :: MIT License - Operating System :: MacOS :: MacOS X - Operating System :: Microsoft :: Windows - Operating System :: POSIX - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Topic :: Scientific/Engineering -keywords = - optimization - dfo - derivative free optimization - noisy optimization - parallel optimization - numerical optimization - -[options] -packages = find: -install_requires = - numba - numpy>=1.17.0 - pandas - scipy>=1.2.1 -python_requires = >=3.10 -include_package_data = True -package_dir = - =src -zip_safe = False - -[options.packages.find] -where = src - -[check-manifest] -ignore = - src/tranquilo/_version.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 7f1a176..0000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup - -if __name__ == "__main__": - setup() diff --git a/src/tranquilo/__init__.py b/src/tranquilo/__init__.py index e69de29..80f05db 100644 --- a/src/tranquilo/__init__.py +++ b/src/tranquilo/__init__.py @@ -0,0 +1,7 @@ +try: + from ._version import version as __version__ +except ImportError: + # package is not installed + __version__ = "unknown" + +__all__ = ["__version__"] From e9f5798452263ed48b94985c7f22feb762a17c21 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 18:49:12 +0100 Subject: [PATCH 13/20] Update pre-commit hooks --- .envs/update_envs.py | 4 +- .pre-commit-config.yaml | 67 ++++++------------- src/tranquilo/acceptance_decision.py | 1 + src/tranquilo/config.py | 2 +- src/tranquilo/estimate_variance.py | 7 +- src/tranquilo/exploration_sample.py | 3 +- src/tranquilo/filter_points.py | 2 +- src/tranquilo/fit_models.py | 2 +- src/tranquilo/get_component.py | 4 +- src/tranquilo/history.py | 2 +- src/tranquilo/models.py | 8 +-- src/tranquilo/options.py | 5 +- src/tranquilo/process_arguments.py | 9 +-- src/tranquilo/sample_points.py | 4 +- src/tranquilo/solve_subproblem.py | 12 ++-- .../subsolvers/_conjugate_gradient.py | 1 + .../subsolvers/_conjugate_gradient_fast.py | 1 + src/tranquilo/subsolvers/_steihaug_toint.py | 9 ++- .../subsolvers/_steihaug_toint_fast.py | 9 ++- src/tranquilo/subsolvers/_trsbox.py | 1 + src/tranquilo/subsolvers/_trsbox_fast.py | 1 + src/tranquilo/subsolvers/bntr.py | 5 +- src/tranquilo/subsolvers/bntr_fast.py | 11 +-- .../subsolvers/fallback_subsolvers.py | 5 +- src/tranquilo/subsolvers/gqtpar.py | 5 +- src/tranquilo/subsolvers/gqtpar_fast.py | 3 +- src/tranquilo/subsolvers/linear_subsolvers.py | 7 +- src/tranquilo/tranquilo.py | 4 +- src/tranquilo/utilities.py | 3 +- src/tranquilo/visualize.py | 7 +- src/tranquilo/volume.py | 1 + src/tranquilo/wrap_criterion.py | 1 + tests/subsolvers/test_bntr_fast.py | 8 +-- tests/subsolvers/test_fallback_solvers.py | 4 +- tests/subsolvers/test_gqtpar_fast.py | 5 +- tests/subsolvers/test_gqtpar_lambdas.py | 3 +- .../subsolvers/test_minimize_trust_region.py | 5 +- tests/test_acceptance_decision.py | 14 ++-- tests/test_acceptance_sample_size.py | 3 +- tests/test_adjust_radius.py | 1 + tests/test_aggregate_models.py | 3 +- tests/test_bounds.py | 1 + tests/test_clustering.py | 3 +- tests/test_estimate_variance.py | 3 +- tests/test_exploration_sample.py | 2 +- tests/test_filter_points.py | 12 ++-- tests/test_fit_models.py | 2 +- tests/test_get_component.py | 4 +- tests/test_handle_infinity.py | 3 +- tests/test_history.py | 5 +- tests/test_models.py | 7 +- tests/test_options.py | 4 +- tests/test_poisedness.py | 6 +- tests/test_process_arguments.py | 14 ++-- tests/test_region.py | 9 +-- tests/test_rho_noise.py | 7 +- tests/test_sample_points.py | 5 +- tests/test_solve_subproblem.py | 7 +- tests/test_tranquilo.py | 8 +-- tests/test_utilities.py | 7 +- tests/test_visualize.py | 5 +- tests/test_volume.py | 5 +- tests/test_weighting.py | 1 + tests/test_wrap_criterion.py | 3 +- 64 files changed, 205 insertions(+), 170 deletions(-) diff --git a/.envs/update_envs.py b/.envs/update_envs.py index 8422b58..8fb7bdd 100644 --- a/.envs/update_envs.py +++ b/.envs/update_envs.py @@ -40,7 +40,9 @@ def main(): docs_env.append(" - -e ../") # add local installation # write environments - for name, env in zip(["linux", "others"], [test_env_linux, test_env_others]): + for name, env in zip( + ["linux", "others"], [test_env_linux, test_env_others], strict=False + ): Path(f".envs/testenv-{name}.yml").write_text("\n".join(env) + "\n") diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 266b412..03e1270 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: check-useless-excludes # - id: identity # Prints all files passed to pre-commits. Debugging. - repo: https://github.com/lyz-code/yamlfix - rev: 1.17.0 + rev: 1.19.0 hooks: - id: yamlfix - repo: local @@ -18,7 +18,7 @@ repos: always_run: true require_serial: true - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: check-added-large-files args: @@ -46,21 +46,12 @@ repos: - --branch - main - id: trailing-whitespace + exclude: docs/ - id: check-ast - - id: check-docstring-first - repo: https://github.com/adrienverge/yamllint.git - rev: v1.30.0 + rev: v1.37.1 hooks: - id: yamllint - - repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - language_version: python3.10 - - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 - hooks: - - id: blacken-docs - repo: https://github.com/PyCQA/docformatter rev: v1.7.7 hooks: @@ -72,58 +63,44 @@ repos: - --wrap-descriptions - '88' - --blank - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.261 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.0 hooks: + # Run the linter. - id: ruff - # args: - # - --verbose - # - repo: https://github.com/kynan/nbstripout - # rev: 0.6.1 - # hooks: - # - id: nbstripout - # args: - # - --extra-keys - # - metadata.kernelspec metadata.language_info.version metadata.vscode - - repo: https://github.com/nbQA-dev/nbQA - rev: 1.7.0 - hooks: - - id: nbqa-black - - id: nbqa-ruff + types_or: + - python + - pyi + - jupyter + args: + - --fix + # Run the formatter. + - id: ruff-format + types_or: + - python + - pyi + - jupyter - repo: https://github.com/executablebooks/mdformat rev: 0.7.22 hooks: - id: mdformat additional_dependencies: - mdformat-gfm - - mdformat-black + - mdformat-ruff args: - --wrap - '88' files: (README\.md) - repo: https://github.com/executablebooks/mdformat - rev: 0.7.16 + rev: 0.7.22 hooks: - id: mdformat additional_dependencies: - mdformat-myst - - mdformat-black + - mdformat-ruff args: - --wrap - '88' files: (docs/.) - - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.2.0 - hooks: - - id: setup-cfg-fmt - - repo: https://github.com/mgedmin/check-manifest - rev: '0.49' - hooks: - - id: check-manifest - args: - - --no-build-isolation - additional_dependencies: - - setuptools-scm - - toml ci: autoupdate_schedule: monthly diff --git a/src/tranquilo/acceptance_decision.py b/src/tranquilo/acceptance_decision.py index 534b339..331be1e 100644 --- a/src/tranquilo/acceptance_decision.py +++ b/src/tranquilo/acceptance_decision.py @@ -4,6 +4,7 @@ also do own function evaluations and decide to accept a different point. """ + from typing import NamedTuple import numpy as np diff --git a/src/tranquilo/config.py b/src/tranquilo/config.py index cc1f864..13df451 100644 --- a/src/tranquilo/config.py +++ b/src/tranquilo/config.py @@ -1,5 +1,5 @@ -from pathlib import Path import importlib.util +from pathlib import Path DOCS_DIR = Path(__file__).parent.parent / "docs" diff --git a/src/tranquilo/estimate_variance.py b/src/tranquilo/estimate_variance.py index 4a74e33..d95e0be 100644 --- a/src/tranquilo/estimate_variance.py +++ b/src/tranquilo/estimate_variance.py @@ -1,12 +1,11 @@ """Estimate the variance or covariance matrix of the noise in the objective function.""" - import numpy as np from tranquilo.get_component import get_component from tranquilo.history import History -from tranquilo.region import Region from tranquilo.options import VarianceEstimatorOptions +from tranquilo.region import Region def get_variance_estimator(fitter, user_options): @@ -48,14 +47,14 @@ def _estimate_variance_classic( if model_type == "scalar": samples = list(history.get_fvals(valid_indices).values()) out = 0.0 - for weight, sample in zip(weights, samples): + for weight, sample in zip(weights, samples, strict=False): out += weight * np.var(sample, ddof=1) else: samples = list(history.get_fvecs(valid_indices).values()) dim = samples[0].shape[1] out = np.zeros((dim, dim)) - for weight, sample in zip(weights, samples): + for weight, sample in zip(weights, samples, strict=False): out += weight * np.cov(sample, rowvar=False, ddof=1) return out diff --git a/src/tranquilo/exploration_sample.py b/src/tranquilo/exploration_sample.py index 27b00c7..f32c59d 100644 --- a/src/tranquilo/exploration_sample.py +++ b/src/tranquilo/exploration_sample.py @@ -1,5 +1,6 @@ import numpy as np from scipy.stats import qmc, triang + from tranquilo.utilities import get_rng @@ -46,7 +47,7 @@ def draw_exploration_sample( if sampling_distribution not in valid_distributions: raise ValueError(f"Unsupported distribution: {sampling_distribution}") - for name, bound in zip(["lower", "upper"], [lower, upper]): + for name, bound in zip(["lower", "upper"], [lower, upper], strict=False): if not np.isfinite(bound).all(): raise ValueError( f"multistart optimization requires finite {name}_bounds or " diff --git a/src/tranquilo/filter_points.py b/src/tranquilo/filter_points.py index 318d3b1..08f681a 100644 --- a/src/tranquilo/filter_points.py +++ b/src/tranquilo/filter_points.py @@ -3,8 +3,8 @@ from tranquilo.clustering import cluster from tranquilo.get_component import get_component -from tranquilo.volume import get_radius_after_volume_scaling from tranquilo.options import FilterOptions +from tranquilo.volume import get_radius_after_volume_scaling def get_sample_filter(sample_filter="keep_all", user_options=None): diff --git a/src/tranquilo/fit_models.py b/src/tranquilo/fit_models.py index 151ae5c..65e432d 100644 --- a/src/tranquilo/fit_models.py +++ b/src/tranquilo/fit_models.py @@ -6,13 +6,13 @@ from tranquilo.get_component import get_component from tranquilo.handle_infinity import get_infinity_handler -from tranquilo.options import FitterOptions from tranquilo.models import ( VectorModel, add_models, move_model, n_second_order_terms, ) +from tranquilo.options import FitterOptions def get_fitter( diff --git a/src/tranquilo/get_component.py b/src/tranquilo/get_component.py index 8707ff7..94f5737 100644 --- a/src/tranquilo/get_component.py +++ b/src/tranquilo/get_component.py @@ -3,8 +3,8 @@ import warnings from functools import partial -from tranquilo.utilities import propose_alternatives from tranquilo.options import update_option_bundle +from tranquilo.utilities import propose_alternatives def get_component( @@ -213,7 +213,7 @@ def _add_redundant_argument_handling(func, signature, warn): @functools.wraps(func) def _wrapper_add_redundant_argument_handling(*args, **kwargs): - _kwargs = {**dict(zip(signature[: len(args)], args)), **kwargs} + _kwargs = {**dict(zip(signature[: len(args)], args, strict=False)), **kwargs} _redundant = {k: v for k, v in _kwargs.items() if k not in signature} _valid = {k: v for k, v in _kwargs.items() if k in signature} diff --git a/src/tranquilo/history.py b/src/tranquilo/history.py index 7f81fef..9f74afc 100644 --- a/src/tranquilo/history.py +++ b/src/tranquilo/history.py @@ -104,7 +104,7 @@ def add_evals(self, x_indices, evals): f_indices = np.arange(self.n_fun, self.n_fun + n_new_points) - for x_index, f_index in zip(x_indices, f_indices): + for x_index, f_index in zip(x_indices, f_indices, strict=False): self.index_mapper[x_index].append(f_index) self.n_fun += n_new_points diff --git a/src/tranquilo/models.py b/src/tranquilo/models.py index 50b63da..2a133cf 100644 --- a/src/tranquilo/models.py +++ b/src/tranquilo/models.py @@ -9,9 +9,9 @@ class VectorModel: intercepts: np.ndarray # shape (n_residuals,) linear_terms: np.ndarray # shape (n_residuals, n_params) - square_terms: Union[ - np.ndarray, None - ] = None # shape (n_residuals, n_params, n_params) + square_terms: Union[np.ndarray, None] = ( + None # shape (n_residuals, n_params, n_params) + ) # scale and shift correspond to effective_radius and effective_center of the region # on which the model was fitted @@ -98,7 +98,7 @@ def add_models(model1, model2): Union[ScalarModel, VectorModel]: The sum of the two models. """ - if type(model1) != type(model2): + if isinstance(model1, type(model2)): raise TypeError("Models must be of the same type.") if not np.allclose(model1.shift, model2.shift): diff --git a/src/tranquilo/options.py b/src/tranquilo/options.py index 5158dca..491bad5 100644 --- a/src/tranquilo/options.py +++ b/src/tranquilo/options.py @@ -1,9 +1,10 @@ -from typing import NamedTuple from enum import Enum -from tranquilo.models import n_free_params +from typing import NamedTuple import numpy as np +from tranquilo.models import n_free_params + def get_default_stagnation_options(noisy, batch_size): if noisy: diff --git a/src/tranquilo/process_arguments.py b/src/tranquilo/process_arguments.py index d9dfcbb..266b3e7 100644 --- a/src/tranquilo/process_arguments.py +++ b/src/tranquilo/process_arguments.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np from tranquilo.acceptance_decision import get_acceptance_decider @@ -9,28 +11,27 @@ from tranquilo.history import History from tranquilo.options import ( ConvOptions, + NoiseAdaptationOptions, StopOptions, get_default_acceptance_decider, get_default_aggregator, get_default_batch_size, get_default_model_fitter, - get_default_residualize, get_default_model_type, get_default_n_evals_at_start, get_default_n_evals_per_point, get_default_radius_options, + get_default_residualize, + get_default_sample_filter, get_default_sample_size, get_default_search_radius_factor, get_default_stagnation_options, - get_default_sample_filter, update_option_bundle, - NoiseAdaptationOptions, ) from tranquilo.region import Region from tranquilo.sample_points import get_sampler from tranquilo.solve_subproblem import get_subsolver from tranquilo.wrap_criterion import get_wrapped_criterion -import warnings def process_arguments( diff --git a/src/tranquilo/sample_points.py b/src/tranquilo/sample_points.py index 292b608..fa905e7 100644 --- a/src/tranquilo/sample_points.py +++ b/src/tranquilo/sample_points.py @@ -1,13 +1,13 @@ +import functools from functools import partial import numpy as np +from scipy.optimize import Bounds, minimize from scipy.spatial.distance import pdist from scipy.special import gammainc, logsumexp -from scipy.optimize import minimize, Bounds from tranquilo.get_component import get_component from tranquilo.options import SamplerOptions -import functools def get_sampler(sampler, user_options=None): diff --git a/src/tranquilo/solve_subproblem.py b/src/tranquilo/solve_subproblem.py index f08ed81..d0d1837 100644 --- a/src/tranquilo/solve_subproblem.py +++ b/src/tranquilo/solve_subproblem.py @@ -4,24 +4,24 @@ import numpy as np from tranquilo.get_component import get_component +from tranquilo.options import SubsolverOptions from tranquilo.subsolvers.bntr import ( bntr, ) from tranquilo.subsolvers.bntr_fast import ( bntr_fast, ) -from tranquilo.subsolvers.gqtpar import ( - gqtpar, -) -from tranquilo.subsolvers.gqtpar_fast import gqtpar_fast -from tranquilo.options import SubsolverOptions from tranquilo.subsolvers.fallback_subsolvers import ( robust_cube_solver, + robust_cube_solver_multistart, robust_sphere_solver_inscribed_cube, robust_sphere_solver_norm_constraint, robust_sphere_solver_reparametrized, - robust_cube_solver_multistart, ) +from tranquilo.subsolvers.gqtpar import ( + gqtpar, +) +from tranquilo.subsolvers.gqtpar_fast import gqtpar_fast def get_subsolver(sphere_solver, cube_solver, retry_with_fallback, user_options=None): diff --git a/src/tranquilo/subsolvers/_conjugate_gradient.py b/src/tranquilo/subsolvers/_conjugate_gradient.py index 7f07cd5..78868d1 100644 --- a/src/tranquilo/subsolvers/_conjugate_gradient.py +++ b/src/tranquilo/subsolvers/_conjugate_gradient.py @@ -1,4 +1,5 @@ """Implementation of the Conjugate Gradient algorithm.""" + import numpy as np diff --git a/src/tranquilo/subsolvers/_conjugate_gradient_fast.py b/src/tranquilo/subsolvers/_conjugate_gradient_fast.py index 6e46215..499487a 100644 --- a/src/tranquilo/subsolvers/_conjugate_gradient_fast.py +++ b/src/tranquilo/subsolvers/_conjugate_gradient_fast.py @@ -1,4 +1,5 @@ """Implementation of the Conjugate Gradient algorithm.""" + import numpy as np from numba import njit diff --git a/src/tranquilo/subsolvers/_steihaug_toint.py b/src/tranquilo/subsolvers/_steihaug_toint.py index ad64c81..1033e67 100644 --- a/src/tranquilo/subsolvers/_steihaug_toint.py +++ b/src/tranquilo/subsolvers/_steihaug_toint.py @@ -1,4 +1,5 @@ """Implementation of the Steihaug-Toint Conjugate Gradient algorithm.""" + import numpy as np @@ -188,7 +189,13 @@ def _take_step_to_trustregion_boundary(x_candidate, p, dp, radius_sq, norm_d, no def _check_convergence( - rnorm, rnorm0, abstol, ttol, divtol, converged, diverged # noqa: ARG001 + rnorm, + rnorm0, + abstol, + ttol, + divtol, + converged, + diverged, # noqa: ARG001 ): """Check for convergence.""" if rnorm <= ttol: diff --git a/src/tranquilo/subsolvers/_steihaug_toint_fast.py b/src/tranquilo/subsolvers/_steihaug_toint_fast.py index 1e87fcf..aa34124 100644 --- a/src/tranquilo/subsolvers/_steihaug_toint_fast.py +++ b/src/tranquilo/subsolvers/_steihaug_toint_fast.py @@ -1,4 +1,5 @@ """Implementation of the Steihaug-Toint Conjugate Gradient algorithm.""" + import numpy as np from numba import njit @@ -196,7 +197,13 @@ def _take_step_to_trustregion_boundary(x_candidate, p, dp, radius_sq, norm_d, no @njit def _check_convergence( - rnorm, rnorm0, abstol, ttol, divtol, converged, diverged # noqa: ARG001 + rnorm, + rnorm0, + abstol, + ttol, + divtol, + converged, + diverged, # noqa: ARG001 ): """Check for convergence.""" if rnorm <= ttol: diff --git a/src/tranquilo/subsolvers/_trsbox.py b/src/tranquilo/subsolvers/_trsbox.py index dc529de..33dc9db 100644 --- a/src/tranquilo/subsolvers/_trsbox.py +++ b/src/tranquilo/subsolvers/_trsbox.py @@ -1,4 +1,5 @@ """Implementation of the quadratic trustregion solver TRSBOX.""" + import numpy as np diff --git a/src/tranquilo/subsolvers/_trsbox_fast.py b/src/tranquilo/subsolvers/_trsbox_fast.py index c5cf225..d93ba1c 100644 --- a/src/tranquilo/subsolvers/_trsbox_fast.py +++ b/src/tranquilo/subsolvers/_trsbox_fast.py @@ -1,4 +1,5 @@ """Implementation of the quadratic trustregion solver TRSBOX.""" + import numpy as np from numba import njit diff --git a/src/tranquilo/subsolvers/bntr.py b/src/tranquilo/subsolvers/bntr.py index 1f8dae5..d487fdc 100644 --- a/src/tranquilo/subsolvers/bntr.py +++ b/src/tranquilo/subsolvers/bntr.py @@ -1,8 +1,10 @@ """Auxiliary functions for the quadratic BNTR trust-region subsolver.""" + from functools import reduce from typing import NamedTuple, Union import numpy as np + from tranquilo.subsolvers._conjugate_gradient import ( minimize_trust_cg, ) @@ -583,8 +585,7 @@ def _perform_gradient_descent_step( square_terms = x_inactive.T @ hessian_inactive @ x_inactive predicted_reduction = trustregion_radius * ( - gradient_norm - - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) + gradient_norm - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) ) actual_reduction = f_candidate_initial - f_candidate diff --git a/src/tranquilo/subsolvers/bntr_fast.py b/src/tranquilo/subsolvers/bntr_fast.py index b02cc19..393e7ab 100644 --- a/src/tranquilo/subsolvers/bntr_fast.py +++ b/src/tranquilo/subsolvers/bntr_fast.py @@ -1,5 +1,8 @@ """Auxiliary functions for the quadratic BNTR trust-region subsolver.""" + import numpy as np +from numba import njit + from tranquilo.subsolvers._conjugate_gradient_fast import ( minimize_trust_cg_fast, ) @@ -9,7 +12,6 @@ from tranquilo.subsolvers._trsbox_fast import ( minimize_trust_trsbox_fast, ) -from numba import njit EPSILON = np.finfo(float).eps ** (2 / 3) @@ -790,8 +792,7 @@ def _perform_gradient_descent_step( square_terms = x_inactive.T @ hessian_inactive @ x_inactive predicted_reduction = trustregion_radius * ( - gradient_norm - - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) + gradient_norm - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) ) actual_reduction = f_candidate_initial - f_candidate @@ -1111,7 +1112,9 @@ def _update_trustregion_radius_and_gradient_descent( def _get_fischer_burmeister_direction_vector(x, gradient, lower_bounds, upper_bounds): """Compute the constrained direction vector via the Fischer-Burmeister function.""" direction = np.zeros(len(x)) - for i, (x_, g_, l_, u_) in enumerate(zip(x, gradient, lower_bounds, upper_bounds)): + for i, (x_, g_, l_, u_) in enumerate( + zip(x, gradient, lower_bounds, upper_bounds, strict=False) + ): fischer_scalar = _get_fischer_burmeister_scalar(u_ - x_, -g_) fischer_scalar = _get_fischer_burmeister_scalar(fischer_scalar, x_ - l_) diff --git a/src/tranquilo/subsolvers/fallback_subsolvers.py b/src/tranquilo/subsolvers/fallback_subsolvers.py index 281225d..d52561e 100644 --- a/src/tranquilo/subsolvers/fallback_subsolvers.py +++ b/src/tranquilo/subsolvers/fallback_subsolvers.py @@ -1,6 +1,7 @@ -import numpy as np from functools import partial -from scipy.optimize import Bounds, minimize, NonlinearConstraint + +import numpy as np +from scipy.optimize import Bounds, NonlinearConstraint, minimize from tranquilo.exploration_sample import draw_exploration_sample diff --git a/src/tranquilo/subsolvers/gqtpar.py b/src/tranquilo/subsolvers/gqtpar.py index c48997f..ef18fd0 100644 --- a/src/tranquilo/subsolvers/gqtpar.py +++ b/src/tranquilo/subsolvers/gqtpar.py @@ -1,4 +1,5 @@ """Auxiliary functions for the quadratic GQTPAR trust-region subsolver.""" + from typing import NamedTuple, Union import numpy as np @@ -356,9 +357,7 @@ def _update_lambdas_when_factorization_unsuccessful( ) v_norm = np.linalg.norm(v) - lambda_lower_bound = max( - lambdas.lower_bound, lambdas.candidate + delta / v_norm**2 - ) + lambda_lower_bound = max(lambdas.lower_bound, lambdas.candidate + delta / v_norm**2) lambda_new_candidate = _get_new_lambda_candidate( lower_bound=lambda_lower_bound, upper_bound=lambdas.upper_bound ) diff --git a/src/tranquilo/subsolvers/gqtpar_fast.py b/src/tranquilo/subsolvers/gqtpar_fast.py index 5994fea..7685bf1 100644 --- a/src/tranquilo/subsolvers/gqtpar_fast.py +++ b/src/tranquilo/subsolvers/gqtpar_fast.py @@ -1,4 +1,5 @@ """Auxiliary functions for the quadratic GQTPAR trust-region subsolver.""" + import numpy as np from numba import njit from scipy.linalg import cho_solve, solve_triangular @@ -177,7 +178,6 @@ def _get_initial_guess_for_lambdas(model_gradient, model_hessian): """ gradient_norm = _norm(model_gradient, -1.0) - model_hessian = model_hessian hessian_infinity_norm = _norm(model_hessian, np.inf) hessian_frobenius_norm = _norm(model_hessian, -1.0) @@ -540,7 +540,6 @@ def _compute_terms_to_make_leading_submatrix_singular( hessian after ``delta`` is added to its element (k, k). """ - hessian_plus_lambda = hessian_plus_lambda upper_triangular = hessian_upper_triangular delta = ( diff --git a/src/tranquilo/subsolvers/linear_subsolvers.py b/src/tranquilo/subsolvers/linear_subsolvers.py index fd33dda..924c93a 100644 --- a/src/tranquilo/subsolvers/linear_subsolvers.py +++ b/src/tranquilo/subsolvers/linear_subsolvers.py @@ -1,4 +1,5 @@ """Collection of linear trust-region subsolvers.""" + from typing import NamedTuple, Union import numpy as np @@ -97,7 +98,7 @@ def improve_geomtery_trsbox_linear( upper_bounds, trustregion_radius, *, - zero_treshold=1e-14 + zero_treshold=1e-14, ): """Maximize a Lagrange polynomial of degree one to improve geometry of the model. @@ -326,9 +327,7 @@ def _get_distance_to_trustregion_boundary( else: distance_to_boundary = ( np.sqrt( - np.maximum( - 0, g_dot_x**2 + g_sumsq * (trustregion_radius**2 - x_sumsq) - ) + np.maximum(0, g_dot_x**2 + g_sumsq * (trustregion_radius**2 - x_sumsq)) ) - g_dot_x ) / g_sumsq diff --git a/src/tranquilo/tranquilo.py b/src/tranquilo/tranquilo.py index 845d2bb..479f11f 100644 --- a/src/tranquilo/tranquilo.py +++ b/src/tranquilo/tranquilo.py @@ -3,6 +3,7 @@ import numpy as np +from tranquilo.adjust_n_evals import adjust_n_evals from tranquilo.adjust_radius import adjust_radius from tranquilo.filter_points import ( drop_worst_points, @@ -11,10 +12,9 @@ ScalarModel, VectorModel, ) -from tranquilo.process_arguments import process_arguments, next_multiple +from tranquilo.process_arguments import next_multiple, process_arguments from tranquilo.region import Region from tranquilo.rho_noise import simulate_rho_noise -from tranquilo.adjust_n_evals import adjust_n_evals # wrapping gives us the signature and docstring of process arguments diff --git a/src/tranquilo/utilities.py b/src/tranquilo/utilities.py index 55783ee..02ab772 100644 --- a/src/tranquilo/utilities.py +++ b/src/tranquilo/utilities.py @@ -1,7 +1,8 @@ -import numpy as np import difflib import warnings +import numpy as np + def propose_alternatives(requested, possibilities, number=3): """Propose possible alternatives based on similarity to requested. diff --git a/src/tranquilo/visualize.py b/src/tranquilo/visualize.py index 99f42b4..6ddd37b 100644 --- a/src/tranquilo/visualize.py +++ b/src/tranquilo/visualize.py @@ -1,15 +1,14 @@ from copy import deepcopy +from typing import Any, Protocol, runtime_checkable import numpy as np import pandas as pd from numba import njit from tranquilo.clustering import cluster +from tranquilo.config import IS_PLOTLY_INSTALLED from tranquilo.geometry import log_d_quality_calculator from tranquilo.volume import get_radius_after_volume_scaling -from tranquilo.config import IS_PLOTLY_INSTALLED - -from typing import Any, Protocol, runtime_checkable if IS_PLOTLY_INSTALLED: import plotly.express as px @@ -540,7 +539,7 @@ def _process_results(result): elif result.algorithm in ["tranquilo", "tranquilo_ls"]: pass else: - NotImplementedError( + raise NotImplementedError( f"Diagnostic plots are not implemented for {result.algorithm}" ) return result diff --git a/src/tranquilo/volume.py b/src/tranquilo/volume.py index 1c092f8..35c5c40 100644 --- a/src/tranquilo/volume.py +++ b/src/tranquilo/volume.py @@ -6,6 +6,7 @@ This is why we caracterize hypercubes by their radius (half the side length). """ + import numpy as np from scipy.special import gamma, loggamma diff --git a/src/tranquilo/wrap_criterion.py b/src/tranquilo/wrap_criterion.py index 6011979..de76a28 100644 --- a/src/tranquilo/wrap_criterion.py +++ b/src/tranquilo/wrap_criterion.py @@ -1,4 +1,5 @@ import numpy as np + from tranquilo.history import History diff --git a/tests/subsolvers/test_bntr_fast.py b/tests/subsolvers/test_bntr_fast.py index d85e84e..3663f72 100644 --- a/tests/subsolvers/test_bntr_fast.py +++ b/tests/subsolvers/test_bntr_fast.py @@ -1,7 +1,11 @@ import numpy as np import pandas as pd import pytest +from numpy.testing import assert_array_almost_equal as aaae +from numpy.testing import assert_array_equal as aae + from tranquilo.config import TEST_FIXTURES_DIR +from tranquilo.models import ScalarModel from tranquilo.subsolvers.bntr import ( ActiveBounds, _update_trustregion_radius_and_gradient_descent, @@ -85,9 +89,6 @@ from tranquilo.subsolvers.bntr_fast import ( _update_trustregion_radius_conjugate_gradient as update_radius_cg_fast, ) -from tranquilo.models import ScalarModel -from numpy.testing import assert_array_almost_equal as aaae -from numpy.testing import assert_array_equal as aae def test_eval_criterion(): @@ -252,7 +253,6 @@ def test_apply_bounds_to_conjugate_gradient_step(): step_inactive, x_candidate, lower_bounds, upper_bounds, bounds_info ) aae(res_orig, res_fast) - pass @pytest.mark.slow() diff --git a/tests/subsolvers/test_fallback_solvers.py b/tests/subsolvers/test_fallback_solvers.py index 68175a3..d7029be 100644 --- a/tests/subsolvers/test_fallback_solvers.py +++ b/tests/subsolvers/test_fallback_solvers.py @@ -1,8 +1,8 @@ -import pytest import numpy as np +import pytest from numpy.testing import assert_array_almost_equal as aaae -from tranquilo.models import ScalarModel +from tranquilo.models import ScalarModel from tranquilo.subsolvers.fallback_subsolvers import ( robust_cube_solver, robust_cube_solver_multistart, diff --git a/tests/subsolvers/test_gqtpar_fast.py b/tests/subsolvers/test_gqtpar_fast.py index 5820801..d5d8119 100644 --- a/tests/subsolvers/test_gqtpar_fast.py +++ b/tests/subsolvers/test_gqtpar_fast.py @@ -1,4 +1,7 @@ import numpy as np +from numpy.testing import assert_array_almost_equal as aaae + +from tranquilo.models import ScalarModel from tranquilo.subsolvers.gqtpar import ( DampingFactors, HessianInfo, @@ -21,8 +24,6 @@ from tranquilo.subsolvers.gqtpar_fast import ( _get_initial_guess_for_lambdas as init_lambdas_fast, ) -from tranquilo.models import ScalarModel -from numpy.testing import assert_array_almost_equal as aaae def test_get_initial_guess_for_lambda(): diff --git a/tests/subsolvers/test_gqtpar_lambdas.py b/tests/subsolvers/test_gqtpar_lambdas.py index 5be7840..3a34130 100644 --- a/tests/subsolvers/test_gqtpar_lambdas.py +++ b/tests/subsolvers/test_gqtpar_lambdas.py @@ -1,9 +1,10 @@ import pytest + from tranquilo.config import IS_OPTIMAGIC_INSTALLED if IS_OPTIMAGIC_INSTALLED: - from optimagic.optimization.optimize import minimize from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems + from optimagic.optimization.optimize import minimize @pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") diff --git a/tests/subsolvers/test_minimize_trust_region.py b/tests/subsolvers/test_minimize_trust_region.py index 714926c..6aa0457 100644 --- a/tests/subsolvers/test_minimize_trust_region.py +++ b/tests/subsolvers/test_minimize_trust_region.py @@ -1,5 +1,8 @@ import numpy as np import pytest +from numpy.testing import assert_array_almost_equal as aaae +from numpy.testing import assert_array_equal as aae + from tranquilo.subsolvers._conjugate_gradient import ( _get_distance_to_trustregion_boundary as gdtb, ) @@ -91,8 +94,6 @@ from tranquilo.subsolvers._trsbox_fast import ( minimize_trust_trsbox_fast, ) -from numpy.testing import assert_array_almost_equal as aaae -from numpy.testing import assert_array_equal as aae def test_minimize_trust_cg(): diff --git a/tests/test_acceptance_decision.py b/tests/test_acceptance_decision.py index 2be2bab..39f3c8b 100644 --- a/tests/test_acceptance_decision.py +++ b/tests/test_acceptance_decision.py @@ -2,23 +2,24 @@ import numpy as np import pytest -from tranquilo.sample_points import get_sampler +from numpy.testing import assert_array_equal + from tranquilo.acceptance_decision import ( _accept_simple, - _get_acceptance_result, - calculate_rho, _generate_alpha_grid, + _generate_speculative_sample, + _get_acceptance_result, _is_on_border, _is_on_cube_border, _is_on_sphere_border, _sample_on_line, - _generate_speculative_sample, + calculate_rho, ) +from tranquilo.bounds import Bounds from tranquilo.history import History from tranquilo.region import Region -from tranquilo.bounds import Bounds +from tranquilo.sample_points import get_sampler from tranquilo.solve_subproblem import SubproblemResult -from numpy.testing import assert_array_equal # ====================================================================================== # Fixtures @@ -157,6 +158,7 @@ def test_calculate_rho(actual_improvement, expected_improvement, expected): CASES = zip( [1, 2, 4, 6], [np.array([]), np.array([2]), np.array([2, 4, 8]), np.array([2, 4, 8])], + strict=False, ) diff --git a/tests/test_acceptance_sample_size.py b/tests/test_acceptance_sample_size.py index a1f3536..a7a3638 100644 --- a/tests/test_acceptance_sample_size.py +++ b/tests/test_acceptance_sample_size.py @@ -1,9 +1,10 @@ import pytest +from scipy.stats import norm + from tranquilo.acceptance_sample_size import ( _compute_factor, _get_optimal_sample_sizes, ) -from scipy.stats import norm TEST_CASES = [ (0.5, 0.5, 0.5, 0), diff --git a/tests/test_adjust_radius.py b/tests/test_adjust_radius.py index ed29705..c4c6b7d 100644 --- a/tests/test_adjust_radius.py +++ b/tests/test_adjust_radius.py @@ -1,5 +1,6 @@ import numpy as np import pytest + from tranquilo.adjust_radius import adjust_radius from tranquilo.options import RadiusOptions diff --git a/tests/test_aggregate_models.py b/tests/test_aggregate_models.py index f3ee400..c693681 100644 --- a/tests/test_aggregate_models.py +++ b/tests/test_aggregate_models.py @@ -1,5 +1,7 @@ import numpy as np import pytest +from numpy.testing import assert_array_equal + from tranquilo.aggregate_models import ( aggregator_identity, aggregator_information_equality_linear, @@ -7,7 +9,6 @@ aggregator_sum, ) from tranquilo.models import ScalarModel, VectorModel -from numpy.testing import assert_array_equal @pytest.mark.parametrize("square_terms", [np.arange(9).reshape(1, 3, 3), None]) diff --git a/tests/test_bounds.py b/tests/test_bounds.py index 2582c41..4470441 100644 --- a/tests/test_bounds.py +++ b/tests/test_bounds.py @@ -1,5 +1,6 @@ import numpy as np import pytest + from tranquilo.bounds import Bounds, _any_finite CASES = [ diff --git a/tests/test_clustering.py b/tests/test_clustering.py index 4084706..2d49f20 100644 --- a/tests/test_clustering.py +++ b/tests/test_clustering.py @@ -1,7 +1,8 @@ import numpy as np -from tranquilo.clustering import cluster from numpy.testing import assert_array_equal as aae +from tranquilo.clustering import cluster + def test_cluster_lollipop(): rng = np.random.default_rng(123456) diff --git a/tests/test_estimate_variance.py b/tests/test_estimate_variance.py index f8eb23d..e1c570c 100644 --- a/tests/test_estimate_variance.py +++ b/tests/test_estimate_variance.py @@ -1,11 +1,12 @@ import numpy as np import pytest +from numpy.testing import assert_array_almost_equal as aaae + from tranquilo.estimate_variance import ( _estimate_variance_classic, ) from tranquilo.history import History from tranquilo.tranquilo import Region -from numpy.testing import assert_array_almost_equal as aaae @pytest.mark.parametrize("model_type", ["scalar", "vector"]) diff --git a/tests/test_exploration_sample.py b/tests/test_exploration_sample.py index 3b93dc5..e4ca901 100644 --- a/tests/test_exploration_sample.py +++ b/tests/test_exploration_sample.py @@ -2,9 +2,9 @@ import numpy as np import pytest -from tranquilo.exploration_sample import draw_exploration_sample from numpy.testing import assert_array_almost_equal as aaae +from tranquilo.exploration_sample import draw_exploration_sample dim = 2 distributions = ["uniform", "triangular"] diff --git a/tests/test_filter_points.py b/tests/test_filter_points.py index 59ac427..cabd627 100644 --- a/tests/test_filter_points.py +++ b/tests/test_filter_points.py @@ -1,10 +1,10 @@ -from tranquilo.filter_points import get_sample_filter -from tranquilo.filter_points import drop_worst_points -from tranquilo.tranquilo import State -from tranquilo.region import Region -from numpy.testing import assert_array_equal as aae -import pytest import numpy as np +import pytest +from numpy.testing import assert_array_equal as aae + +from tranquilo.filter_points import drop_worst_points, get_sample_filter +from tranquilo.region import Region +from tranquilo.tranquilo import State @pytest.fixture() diff --git a/tests/test_fit_models.py b/tests/test_fit_models.py index 2d14ed6..6f51ef9 100644 --- a/tests/test_fit_models.py +++ b/tests/test_fit_models.py @@ -2,9 +2,9 @@ import pytest from numpy.testing import assert_array_almost_equal, assert_array_equal +from tranquilo.config import IS_OPTIMAGIC_INSTALLED from tranquilo.fit_models import _quadratic_features, get_fitter from tranquilo.region import Region -from tranquilo.config import IS_OPTIMAGIC_INSTALLED if IS_OPTIMAGIC_INSTALLED: from optimagic.differentiation.derivatives import ( diff --git a/tests/test_get_component.py b/tests/test_get_component.py index 0d4ac91..7daf600 100644 --- a/tests/test_get_component.py +++ b/tests/test_get_component.py @@ -1,5 +1,7 @@ -import pytest from collections import namedtuple + +import pytest + from tranquilo.get_component import ( _add_redundant_argument_handling, _fail_if_mandatory_argument_is_missing, diff --git a/tests/test_handle_infinity.py b/tests/test_handle_infinity.py index 8cf0153..72da10a 100644 --- a/tests/test_handle_infinity.py +++ b/tests/test_handle_infinity.py @@ -1,7 +1,8 @@ import numpy as np -from tranquilo.handle_infinity import get_infinity_handler from numpy.testing import assert_array_almost_equal as aaae +from tranquilo.handle_infinity import get_infinity_handler + def test_clip_relative(): func = get_infinity_handler("relative") diff --git a/tests/test_history.py b/tests/test_history.py index efc0e29..3a8cc23 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -1,10 +1,11 @@ """Test the history class for least-squares optimizers.""" + import numpy as np import pytest -from tranquilo.history import History -from tranquilo.region import Region from numpy.testing import assert_array_almost_equal as aaae +from tranquilo.history import History +from tranquilo.region import Region XS = [ np.arange(3), diff --git a/tests/test_models.py b/tests/test_models.py index 90beda8..dffe713 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,8 @@ import numpy as np import pytest -from tranquilo.region import Region +from numpy.testing import assert_array_almost_equal as aaae +from numpy.testing import assert_array_equal + from tranquilo.models import ( ScalarModel, VectorModel, @@ -13,8 +15,7 @@ n_interactions, n_second_order_terms, ) -from numpy.testing import assert_array_almost_equal as aaae -from numpy.testing import assert_array_equal +from tranquilo.region import Region def test_predict_scalar(): diff --git a/tests/test_options.py b/tests/test_options.py index ac8b713..cc6afd6 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,5 +1,7 @@ -import pytest from collections import namedtuple + +import pytest + from tranquilo.options import ( get_default_aggregator, update_option_bundle, diff --git a/tests/test_poisedness.py b/tests/test_poisedness.py index f4c3c7a..555e988 100644 --- a/tests/test_poisedness.py +++ b/tests/test_poisedness.py @@ -1,6 +1,9 @@ +import sys + import numpy as np import pytest -import sys +from numpy.testing import assert_array_almost_equal as aaae + from tranquilo.poisedness import ( _get_minimize_options, _lagrange_poly_matrix, @@ -8,7 +11,6 @@ get_poisedness_constant, improve_poisedness, ) -from numpy.testing import assert_array_almost_equal as aaae def evaluate_scalar_model(x, intercept, linear_terms, square_terms): diff --git a/tests/test_process_arguments.py b/tests/test_process_arguments.py index 98b3129..8f44ad1 100644 --- a/tests/test_process_arguments.py +++ b/tests/test_process_arguments.py @@ -4,18 +4,20 @@ depend on the inputs, not the values with static defaults. """ -import pytest + import numpy as np +import pytest + from tranquilo.process_arguments import ( - process_arguments, - _process_batch_size, - _process_sample_size, - _process_model_type, - _process_search_radius_factor, _process_acceptance_decider, + _process_batch_size, _process_model_fitter, + _process_model_type, _process_residualize, + _process_sample_size, + _process_search_radius_factor, next_multiple, + process_arguments, ) diff --git a/tests/test_region.py b/tests/test_region.py index 7e021f6..197b6de 100644 --- a/tests/test_region.py +++ b/tests/test_region.py @@ -1,20 +1,21 @@ import numpy as np +import pytest +from numpy.testing import assert_array_equal + from tranquilo.bounds import Bounds from tranquilo.region import ( Region, _any_bounds_binding, - _get_shape, _get_cube_bounds, _get_cube_center, - _get_effective_radius, _get_effective_center, + _get_effective_radius, + _get_shape, _map_from_unit_cube, _map_from_unit_sphere, _map_to_unit_cube, _map_to_unit_sphere, ) -from numpy.testing import assert_array_equal -import pytest def test_map_to_unit_sphere(): diff --git a/tests/test_rho_noise.py b/tests/test_rho_noise.py index 70a58f2..3e72e46 100644 --- a/tests/test_rho_noise.py +++ b/tests/test_rho_noise.py @@ -1,13 +1,14 @@ import numpy as np import pytest +from numpy.testing import assert_array_almost_equal as aaae + from tranquilo.aggregate_models import get_aggregator +from tranquilo.bounds import Bounds from tranquilo.fit_models import get_fitter +from tranquilo.options import NoiseAdaptationOptions from tranquilo.region import Region -from tranquilo.bounds import Bounds from tranquilo.rho_noise import simulate_rho_noise from tranquilo.solve_subproblem import get_subsolver -from numpy.testing import assert_array_almost_equal as aaae -from tranquilo.options import NoiseAdaptationOptions @pytest.mark.parametrize("functype", ["scalar", "least_squares"]) diff --git a/tests/test_sample_points.py b/tests/test_sample_points.py index 973ceb5..ec47353 100644 --- a/tests/test_sample_points.py +++ b/tests/test_sample_points.py @@ -1,5 +1,8 @@ import numpy as np import pytest +from numpy.testing import assert_array_almost_equal as aaae +from scipy.spatial.distance import pdist + from tranquilo.bounds import Bounds from tranquilo.region import Region from tranquilo.sample_points import ( @@ -8,8 +11,6 @@ _project_onto_unit_hull, get_sampler, ) -from numpy.testing import assert_array_almost_equal as aaae -from scipy.spatial.distance import pdist SAMPLERS = ["random_interior", "random_hull", "optimal_hull"] diff --git a/tests/test_solve_subproblem.py b/tests/test_solve_subproblem.py index 28fb3da..a9ff67e 100644 --- a/tests/test_solve_subproblem.py +++ b/tests/test_solve_subproblem.py @@ -1,10 +1,11 @@ import numpy as np import pytest +from numpy.testing import assert_array_almost_equal as aaae + +from tranquilo.bounds import Bounds from tranquilo.models import ScalarModel -from tranquilo.solve_subproblem import get_subsolver from tranquilo.region import Region -from tranquilo.bounds import Bounds -from numpy.testing import assert_array_almost_equal as aaae +from tranquilo.solve_subproblem import get_subsolver solvers = ["gqtpar", "gqtpar_fast"] diff --git a/tests/test_tranquilo.py b/tests/test_tranquilo.py index 1df2f46..f17ddf4 100644 --- a/tests/test_tranquilo.py +++ b/tests/test_tranquilo.py @@ -1,16 +1,16 @@ import itertools - -import pytest from functools import partial + import numpy as np +import pytest from numpy.testing import assert_array_almost_equal as aaae -from tranquilo.tranquilo import _tranquilo from tranquilo.config import IS_OPTIMAGIC_INSTALLED +from tranquilo.tranquilo import _tranquilo if IS_OPTIMAGIC_INSTALLED: - from optimagic.optimization.optimize import minimize from optimagic import mark + from optimagic.optimization.optimize import minimize tranquilo = partial( diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 969fbd6..85afc52 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,13 +1,14 @@ -import pytest -from tranquilo.utilities import propose_alternatives, get_rng import numpy as np +import pytest + +from tranquilo.utilities import get_rng, propose_alternatives def test_propose_alternatives(): possibilities = ["scipy_lbfgsb", "scipy_slsqp", "nlopt_lbfgsb"] inputs = [["scipy_L-BFGS-B", 1], ["L-BFGS-B", 2]] expected = [["scipy_slsqp"], ["scipy_slsqp", "scipy_lbfgsb"]] - for inp, exp in zip(inputs, expected): + for inp, exp in zip(inputs, expected, strict=False): assert propose_alternatives(inp[0], possibilities, number=inp[1]) == exp diff --git a/tests/test_visualize.py b/tests/test_visualize.py index eb5aba3..b035222 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -1,10 +1,11 @@ import pytest -from tranquilo.visualize import visualize_tranquilo + from tranquilo.config import IS_OPTIMAGIC_INSTALLED +from tranquilo.visualize import visualize_tranquilo if IS_OPTIMAGIC_INSTALLED: - from optimagic.optimization.optimize import minimize from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems + from optimagic.optimization.optimize import minimize cases = [] algo_options = { diff --git a/tests/test_volume.py b/tests/test_volume.py index ff11aa8..718b052 100644 --- a/tests/test_volume.py +++ b/tests/test_volume.py @@ -1,5 +1,6 @@ import numpy as np import pytest + from tranquilo.volume import ( _cube_radius, _cube_volume, @@ -12,7 +13,7 @@ get_volume, ) -dims = dims = [1, 2, 3, 4, 12, 13, 15] +dims = [1, 2, 3, 4, 12, 13, 15] coeffs = [ 2, np.pi, @@ -80,7 +81,7 @@ def test_radius_after_volume_rescaling_scaling_factor_cube(dim): assert np.allclose(got, naive) -@pytest.mark.parametrize("dim, coeff", list(zip(dims, coeffs))) +@pytest.mark.parametrize("dim, coeff", list(zip(dims, coeffs, strict=False))) def test_shpere_volume_and_radius(dim, coeff): radius = 0.5 expected_volume = coeff * radius**dim diff --git a/tests/test_weighting.py b/tests/test_weighting.py index 5ea0dfa..78f95f4 100644 --- a/tests/test_weighting.py +++ b/tests/test_weighting.py @@ -1,4 +1,5 @@ import numpy as np + from tranquilo.weighting import get_sample_weighter diff --git a/tests/test_wrap_criterion.py b/tests/test_wrap_criterion.py index 545a7af..1deabd3 100644 --- a/tests/test_wrap_criterion.py +++ b/tests/test_wrap_criterion.py @@ -2,9 +2,10 @@ import numpy as np import pytest +from numpy.testing import assert_array_almost_equal as aaae + from tranquilo.history import History from tranquilo.wrap_criterion import get_wrapped_criterion -from numpy.testing import assert_array_almost_equal as aaae TEST_CASES = list(itertools.product(["scalar", "least_squares", "likelihood"], [1, 2])) From 48b18f4473e497fb62d1626dc5bd34ba9fee64b4 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 19:08:55 +0100 Subject: [PATCH 14/20] Monkey-patch version to test compatibility with optimagic --- src/tranquilo/models.py | 2 +- tests/conftest.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/conftest.py diff --git a/src/tranquilo/models.py b/src/tranquilo/models.py index 2a133cf..ec5e0b7 100644 --- a/src/tranquilo/models.py +++ b/src/tranquilo/models.py @@ -98,7 +98,7 @@ def add_models(model1, model2): Union[ScalarModel, VectorModel]: The sum of the two models. """ - if isinstance(model1, type(model2)): + if not isinstance(model1, type(model2)): raise TypeError("Models must be of the same type.") if not np.allclose(model1.shift, model2.shift): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0981526 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import optimagic.optimizers.tranquilo as t +from _pytest.monkeypatch import MonkeyPatch + +_mp = MonkeyPatch() + + +def pytest_configure(config): + _mp.setattr( + t, "IS_TRANQUILO_VERSION_NEWER_OR_EQUAL_TO_0_1_0", "patched-value", raising=True + ) + + +def pytest_unconfigure(config): + _mp.undo() From 0df6d5bf6c5a83583aaeeb55dbbdcffc4c0689f3 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 19:21:45 +0100 Subject: [PATCH 15/20] Auto-review --- .envs/testenv-linux.yml | 2 -- .envs/testenv-others.yml | 2 -- .envs/update_envs.py | 2 +- docs/rtd_environment.yml | 1 - docs/source/conf.py | 1 - environment.yml | 2 -- pyproject.toml | 10 ++-------- src/tranquilo/estimate_variance.py | 4 ++-- src/tranquilo/exploration_sample.py | 2 +- src/tranquilo/get_component.py | 2 +- src/tranquilo/history.py | 2 +- src/tranquilo/subsolvers/bntr_fast.py | 11 ++++------- tests/test_acceptance_decision.py | 2 +- tests/test_utilities.py | 2 +- tests/test_volume.py | 2 +- 15 files changed, 15 insertions(+), 32 deletions(-) diff --git a/.envs/testenv-linux.yml b/.envs/testenv-linux.yml index bc7c237..27ff3c2 100644 --- a/.envs/testenv-linux.yml +++ b/.envs/testenv-linux.yml @@ -10,8 +10,6 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - pytest-timeout # dev, tests - - joblib # run, tests - numba # run, tests - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests diff --git a/.envs/testenv-others.yml b/.envs/testenv-others.yml index 3009c85..d7f2f00 100644 --- a/.envs/testenv-others.yml +++ b/.envs/testenv-others.yml @@ -9,8 +9,6 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - pytest-timeout # dev, tests - - joblib # run, tests - numba # run, tests - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests diff --git a/.envs/update_envs.py b/.envs/update_envs.py index 8fb7bdd..5e13284 100644 --- a/.envs/update_envs.py +++ b/.envs/update_envs.py @@ -41,7 +41,7 @@ def main(): # write environments for name, env in zip( - ["linux", "others"], [test_env_linux, test_env_others], strict=False + ["linux", "others"], [test_env_linux, test_env_others], strict=True ): Path(f".envs/testenv-{name}.yml").write_text("\n".join(env) + "\n") diff --git a/docs/rtd_environment.yml b/docs/rtd_environment.yml index fcbd561..36ec2bb 100644 --- a/docs/rtd_environment.yml +++ b/docs/rtd_environment.yml @@ -11,7 +11,6 @@ dependencies: - optimagic - ipython - ipython_genutils - - joblib - matplotlib - myst-nb - numpy diff --git a/docs/source/conf.py b/docs/source/conf.py index 095ee0f..2ba04f6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -66,7 +66,6 @@ "cloudpickle", "cyipopt", "fides", - "joblib", "nlopt", "pandas", "pytest", diff --git a/environment.yml b/environment.yml index 202cb3a..42b9baf 100644 --- a/environment.yml +++ b/environment.yml @@ -10,10 +10,8 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - pytest-timeout # dev, tests - hatch # dev - toml # dev - - joblib # run, tests - numba # run, tests - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests diff --git a/pyproject.toml b/pyproject.toml index 88d7271..9a4fe33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,8 +155,8 @@ convention = "google" # ====================================================================================== [tool.pytest.ini_options] filterwarnings = [ - "ignore:delta_grad == 0.0", - "ignore:Jupyter is migrating", + "ignore:delta_grad == 0.0", # UserWarning in test_poisedness.py + "ignore:Jupyter is migrating", # DeprecationWarning from jupyter client "ignore:Noisy scalar functions are experimental", "ignore:Parallelization together with", ] @@ -170,12 +170,6 @@ norecursedirs = ["docs", ".envs"] # ====================================================================================== # Misc configuration # ====================================================================================== -[tool.nbqa.config] -black = "pyproject.toml" - -[tool.nbqa.mutate] -black = 1 - [tool.yamlfix] line_length = 88 sequence_style = "block_style" diff --git a/src/tranquilo/estimate_variance.py b/src/tranquilo/estimate_variance.py index d95e0be..98e6bd7 100644 --- a/src/tranquilo/estimate_variance.py +++ b/src/tranquilo/estimate_variance.py @@ -47,14 +47,14 @@ def _estimate_variance_classic( if model_type == "scalar": samples = list(history.get_fvals(valid_indices).values()) out = 0.0 - for weight, sample in zip(weights, samples, strict=False): + for weight, sample in zip(weights, samples, strict=True): out += weight * np.var(sample, ddof=1) else: samples = list(history.get_fvecs(valid_indices).values()) dim = samples[0].shape[1] out = np.zeros((dim, dim)) - for weight, sample in zip(weights, samples, strict=False): + for weight, sample in zip(weights, samples, strict=True): out += weight * np.cov(sample, rowvar=False, ddof=1) return out diff --git a/src/tranquilo/exploration_sample.py b/src/tranquilo/exploration_sample.py index f32c59d..67f7b18 100644 --- a/src/tranquilo/exploration_sample.py +++ b/src/tranquilo/exploration_sample.py @@ -47,7 +47,7 @@ def draw_exploration_sample( if sampling_distribution not in valid_distributions: raise ValueError(f"Unsupported distribution: {sampling_distribution}") - for name, bound in zip(["lower", "upper"], [lower, upper], strict=False): + for name, bound in zip(["lower", "upper"], [lower, upper], strict=True): if not np.isfinite(bound).all(): raise ValueError( f"multistart optimization requires finite {name}_bounds or " diff --git a/src/tranquilo/get_component.py b/src/tranquilo/get_component.py index 94f5737..716f9d1 100644 --- a/src/tranquilo/get_component.py +++ b/src/tranquilo/get_component.py @@ -213,7 +213,7 @@ def _add_redundant_argument_handling(func, signature, warn): @functools.wraps(func) def _wrapper_add_redundant_argument_handling(*args, **kwargs): - _kwargs = {**dict(zip(signature[: len(args)], args, strict=False)), **kwargs} + _kwargs = {**dict(zip(signature[: len(args)], args, strict=True)), **kwargs} _redundant = {k: v for k, v in _kwargs.items() if k not in signature} _valid = {k: v for k, v in _kwargs.items() if k in signature} diff --git a/src/tranquilo/history.py b/src/tranquilo/history.py index 9f74afc..4ad586f 100644 --- a/src/tranquilo/history.py +++ b/src/tranquilo/history.py @@ -104,7 +104,7 @@ def add_evals(self, x_indices, evals): f_indices = np.arange(self.n_fun, self.n_fun + n_new_points) - for x_index, f_index in zip(x_indices, f_indices, strict=False): + for x_index, f_index in zip(x_indices, f_indices, strict=True): self.index_mapper[x_index].append(f_index) self.n_fun += n_new_points diff --git a/src/tranquilo/subsolvers/bntr_fast.py b/src/tranquilo/subsolvers/bntr_fast.py index 393e7ab..b02cc19 100644 --- a/src/tranquilo/subsolvers/bntr_fast.py +++ b/src/tranquilo/subsolvers/bntr_fast.py @@ -1,8 +1,5 @@ """Auxiliary functions for the quadratic BNTR trust-region subsolver.""" - import numpy as np -from numba import njit - from tranquilo.subsolvers._conjugate_gradient_fast import ( minimize_trust_cg_fast, ) @@ -12,6 +9,7 @@ from tranquilo.subsolvers._trsbox_fast import ( minimize_trust_trsbox_fast, ) +from numba import njit EPSILON = np.finfo(float).eps ** (2 / 3) @@ -792,7 +790,8 @@ def _perform_gradient_descent_step( square_terms = x_inactive.T @ hessian_inactive @ x_inactive predicted_reduction = trustregion_radius * ( - gradient_norm - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) + gradient_norm + - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) ) actual_reduction = f_candidate_initial - f_candidate @@ -1112,9 +1111,7 @@ def _update_trustregion_radius_and_gradient_descent( def _get_fischer_burmeister_direction_vector(x, gradient, lower_bounds, upper_bounds): """Compute the constrained direction vector via the Fischer-Burmeister function.""" direction = np.zeros(len(x)) - for i, (x_, g_, l_, u_) in enumerate( - zip(x, gradient, lower_bounds, upper_bounds, strict=False) - ): + for i, (x_, g_, l_, u_) in enumerate(zip(x, gradient, lower_bounds, upper_bounds)): fischer_scalar = _get_fischer_burmeister_scalar(u_ - x_, -g_) fischer_scalar = _get_fischer_burmeister_scalar(fischer_scalar, x_ - l_) diff --git a/tests/test_acceptance_decision.py b/tests/test_acceptance_decision.py index 39f3c8b..4f04647 100644 --- a/tests/test_acceptance_decision.py +++ b/tests/test_acceptance_decision.py @@ -158,7 +158,7 @@ def test_calculate_rho(actual_improvement, expected_improvement, expected): CASES = zip( [1, 2, 4, 6], [np.array([]), np.array([2]), np.array([2, 4, 8]), np.array([2, 4, 8])], - strict=False, + strict=True, ) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 85afc52..cde09ae 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -8,7 +8,7 @@ def test_propose_alternatives(): possibilities = ["scipy_lbfgsb", "scipy_slsqp", "nlopt_lbfgsb"] inputs = [["scipy_L-BFGS-B", 1], ["L-BFGS-B", 2]] expected = [["scipy_slsqp"], ["scipy_slsqp", "scipy_lbfgsb"]] - for inp, exp in zip(inputs, expected, strict=False): + for inp, exp in zip(inputs, expected, strict=True): assert propose_alternatives(inp[0], possibilities, number=inp[1]) == exp diff --git a/tests/test_volume.py b/tests/test_volume.py index 718b052..deac64f 100644 --- a/tests/test_volume.py +++ b/tests/test_volume.py @@ -81,7 +81,7 @@ def test_radius_after_volume_rescaling_scaling_factor_cube(dim): assert np.allclose(got, naive) -@pytest.mark.parametrize("dim, coeff", list(zip(dims, coeffs, strict=False))) +@pytest.mark.parametrize("dim, coeff", list(zip(dims, coeffs, strict=True))) def test_shpere_volume_and_radius(dim, coeff): radius = 0.5 expected_volume = coeff * radius**dim From e612e0cc8ef3a07eb04ec8f298ad82f7e6b2801b Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 19:24:50 +0100 Subject: [PATCH 16/20] Ignore zip/numba issue --- src/tranquilo/subsolvers/bntr_fast.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tranquilo/subsolvers/bntr_fast.py b/src/tranquilo/subsolvers/bntr_fast.py index b02cc19..03f2c28 100644 --- a/src/tranquilo/subsolvers/bntr_fast.py +++ b/src/tranquilo/subsolvers/bntr_fast.py @@ -1,5 +1,8 @@ """Auxiliary functions for the quadratic BNTR trust-region subsolver.""" + import numpy as np +from numba import njit + from tranquilo.subsolvers._conjugate_gradient_fast import ( minimize_trust_cg_fast, ) @@ -9,7 +12,6 @@ from tranquilo.subsolvers._trsbox_fast import ( minimize_trust_trsbox_fast, ) -from numba import njit EPSILON = np.finfo(float).eps ** (2 / 3) @@ -790,8 +792,7 @@ def _perform_gradient_descent_step( square_terms = x_inactive.T @ hessian_inactive @ x_inactive predicted_reduction = trustregion_radius * ( - gradient_norm - - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) + gradient_norm - 0.5 * trustregion_radius * square_terms / (gradient_norm**2) ) actual_reduction = f_candidate_initial - f_candidate @@ -1111,7 +1112,9 @@ def _update_trustregion_radius_and_gradient_descent( def _get_fischer_burmeister_direction_vector(x, gradient, lower_bounds, upper_bounds): """Compute the constrained direction vector via the Fischer-Burmeister function.""" direction = np.zeros(len(x)) - for i, (x_, g_, l_, u_) in enumerate(zip(x, gradient, lower_bounds, upper_bounds)): + for i, (x_, g_, l_, u_) in enumerate( + zip(x, gradient, lower_bounds, upper_bounds) # noqa: B905 # strict=... not supported by numba yet, see https://github.com/numba/numba/issues/8943 + ): fischer_scalar = _get_fischer_burmeister_scalar(u_ - x_, -g_) fischer_scalar = _get_fischer_burmeister_scalar(fischer_scalar, x_ - l_) From 5967a3e08a32f9fa632e5c90b945e6443f6c0da6 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 19:26:33 +0100 Subject: [PATCH 17/20] Fix pandas warning --- src/tranquilo/visualize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tranquilo/visualize.py b/src/tranquilo/visualize.py index 6ddd37b..b26b955 100644 --- a/src/tranquilo/visualize.py +++ b/src/tranquilo/visualize.py @@ -456,7 +456,7 @@ def _get_sample_points(state, history): ] ), ) - df["case"] = np.nan + df["case"] = pd.NA df.loc[state.new_indices, "case"] = "new" df.loc[state.old_indices_used, "case"] = "existing" df.loc[ From 3a190acf05c56bf6320be3643eed5bfa9b852ea7 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 19:30:15 +0100 Subject: [PATCH 18/20] Remove optimagic from testing environment --- .envs/testenv-linux.yml | 1 - .envs/testenv-others.yml | 1 - environment.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/.envs/testenv-linux.yml b/.envs/testenv-linux.yml index 27ff3c2..ed828ae 100644 --- a/.envs/testenv-linux.yml +++ b/.envs/testenv-linux.yml @@ -11,7 +11,6 @@ dependencies: - pytest-cov # tests - pytest-xdist # dev, tests - numba # run, tests - - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests - pandas # run, tests - plotly # run, tests diff --git a/.envs/testenv-others.yml b/.envs/testenv-others.yml index d7f2f00..9bfc08b 100644 --- a/.envs/testenv-others.yml +++ b/.envs/testenv-others.yml @@ -10,7 +10,6 @@ dependencies: - pytest-cov # tests - pytest-xdist # dev, tests - numba # run, tests - - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests - pandas # run, tests - plotly # run, tests diff --git a/environment.yml b/environment.yml index 42b9baf..0ce66cb 100644 --- a/environment.yml +++ b/environment.yml @@ -13,7 +13,6 @@ dependencies: - hatch # dev - toml # dev - numba # run, tests - - optimagic>=0.5.0 # tests - numpy>=1.17.0 # run, tests - pandas # run, tests - plotly # run, tests From fcc6f3143955193ce5d838b6c8ad84bf9cf9aec9 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 19:34:15 +0100 Subject: [PATCH 19/20] Fix conftest.py --- tests/conftest.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0981526..ff77497 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,19 @@ -import optimagic.optimizers.tranquilo as t from _pytest.monkeypatch import MonkeyPatch -_mp = MonkeyPatch() +from tranquilo.config import IS_OPTIMAGIC_INSTALLED +if IS_OPTIMAGIC_INSTALLED: + import optimagic.optimizers.tranquilo as t -def pytest_configure(config): - _mp.setattr( - t, "IS_TRANQUILO_VERSION_NEWER_OR_EQUAL_TO_0_1_0", "patched-value", raising=True - ) + _mp = MonkeyPatch() + def pytest_configure(config): + _mp.setattr( + t, + "IS_TRANQUILO_VERSION_NEWER_OR_EQUAL_TO_0_1_0", + "patched-value", + raising=True, + ) -def pytest_unconfigure(config): - _mp.undo() + def pytest_unconfigure(config): + _mp.undo() From 65befecf24ea24c8f1ae04f18eb805d7279694c7 Mon Sep 17 00:00:00 2001 From: timmens Date: Mon, 29 Dec 2025 19:44:59 +0100 Subject: [PATCH 20/20] Remove optimagic dependency from test-suite --- tests/test_tranquilo.py | 25 +++++++--------- tests/test_visualize.py | 63 +++++++++++++++++++++-------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/tests/test_tranquilo.py b/tests/test_tranquilo.py index f17ddf4..33bb909 100644 --- a/tests/test_tranquilo.py +++ b/tests/test_tranquilo.py @@ -192,15 +192,18 @@ def test_external_tranquilo_ls_sphere_defaults(): # Noisy case # ====================================================================================== - -@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") -@pytest.mark.parametrize( - "algorithm, criterion", - [ +if IS_OPTIMAGIC_INSTALLED: + # Has to be defined here to avoid import errors when optimagic is not installed + ALGORITHM_AND_CRITERION = [ ("tranquilo", mark.scalar(lambda x: x @ x)), ("tranquilo_ls", mark.least_squares(lambda x: x)), - ], -) + ] +else: + ALGORITHM_AND_CRITERION = [] + + +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") +@pytest.mark.parametrize("algorithm, criterion", ALGORITHM_AND_CRITERION) def test_tranquilo_with_noise_handling_and_deterministic_function(algorithm, criterion): res = minimize( fun=criterion, @@ -238,13 +241,7 @@ def _f(x): @pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") -@pytest.mark.parametrize( - "algorithm, criterion", - [ - ("tranquilo", mark.scalar(lambda x: x @ x)), - ("tranquilo_ls", mark.least_squares(lambda x: x)), - ], -) +@pytest.mark.parametrize("algorithm, criterion", ALGORITHM_AND_CRITERION) def test_tranquilo_with_binding_bounds(algorithm, criterion): res = minimize( fun=criterion, diff --git a/tests/test_visualize.py b/tests/test_visualize.py index b035222..7e62b48 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -7,38 +7,41 @@ from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems from optimagic.optimization.optimize import minimize -cases = [] -algo_options = { - "random_hull": { - "sampler": "random_hull", - "sphere_subsolver": "gqtpar_fast", - "sample_filter": "keep_all", - "stopping_maxiter": 10, - }, - "optimal_hull": { - "sampler": "optimal_hull", - "sphere_subsolver": "gqtpar_fast", - "sample_filter": "keep_all", - "stopping_maxiter": 10, - }, -} -for problem in ["rosenbrock_good_start", "watson_6_good_start"]: - inputs = get_benchmark_problems("more_wild")[problem]["inputs"] - fun = inputs["fun"] - start_params = inputs["params"] - for algorithm in ["tranquilo", "tranquilo_ls"]: - results = {} - for s, options in algo_options.items(): - results[s] = minimize( - fun=fun, - params=start_params, - algo_options=options, - algorithm=algorithm, - ) - cases.append(results) +def benchmark_cases(): + cases = [] + algo_options = { + "random_hull": { + "sampler": "random_hull", + "sphere_subsolver": "gqtpar_fast", + "sample_filter": "keep_all", + "stopping_maxiter": 10, + }, + "optimal_hull": { + "sampler": "optimal_hull", + "sphere_subsolver": "gqtpar_fast", + "sample_filter": "keep_all", + "stopping_maxiter": 10, + }, + } + for problem in ["rosenbrock_good_start", "watson_6_good_start"]: + inputs = get_benchmark_problems("more_wild")[problem]["inputs"] + fun = inputs["fun"] + start_params = inputs["params"] + for algorithm in ["tranquilo", "tranquilo_ls"]: + results = {} + for s, options in algo_options.items(): + results[s] = minimize( + fun=fun, + params=start_params, + algo_options=options, + algorithm=algorithm, + ) + cases.append(results) + return cases -@pytest.mark.parametrize("results", cases) + +@pytest.mark.parametrize("results", benchmark_cases() if IS_OPTIMAGIC_INSTALLED else []) def test_visualize_tranquilo(results): visualize_tranquilo(results, 5) for res in results.values():