diff --git a/.envs/testenv-linux.yml b/.envs/testenv-linux.yml index 61e82b9c..ed828aed 100644 --- a/.envs/testenv-linux.yml +++ b/.envs/testenv-linux.yml @@ -10,7 +10,6 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - optimagic # 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 84f89f11..9bfc08b1 100644 --- a/.envs/testenv-others.yml +++ b/.envs/testenv-others.yml @@ -9,7 +9,6 @@ dependencies: - pytest # dev, tests - pytest-cov # tests - pytest-xdist # dev, tests - - optimagic # run, tests - numba # run, tests - numpy>=1.17.0 # run, tests - pandas # run, tests diff --git a/.envs/update_envs.py b/.envs/update_envs.py index 8422b58e..5e132844 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=True + ): 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 dc342003..03e12708 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.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,84 +46,61 @@ 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 + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.7 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.5.1 -# 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 + - id: docformatter + args: + - --in-place + - --wrap-summaries + - '88' + - --wrap-descriptions + - '88' + - --blank + - 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.16 + 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/docs/rtd_environment.yml b/docs/rtd_environment.yml index fcbd561f..36ec2bbc 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 095ee0f2..2ba04f68 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 4d7bf32f..0ce66cbb 100644 --- a/environment.yml +++ b/environment.yml @@ -6,15 +6,12 @@ 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 + - hatch # dev - toml # dev - - optimagic # run, tests - numba # run, tests - numpy>=1.17.0 # run, tests - pandas # run, tests @@ -27,8 +24,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 ed4bf3be..9a4fe33f 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.setuptools_scm] -write_to = "src/tranquilo/_version.py" +[tool.hatch.build.targets.wheel] +only-include = ["src"] +sources = ["src"] +[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:Noisy scalar functions are experimental", + "ignore:Parallelization together with", ] markers = [ "wip: Tests that are work-in-progress.", @@ -81,6 +167,9 @@ markers = [ norecursedirs = ["docs", ".envs"] +# ====================================================================================== +# Misc configuration +# ====================================================================================== [tool.yamlfix] line_length = 88 sequence_style = "block_style" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 7c8eb2c7..00000000 --- a/setup.cfg +++ /dev/null @@ -1,49 +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 - optimagic>=0.5.1 - pandas - plotly - scipy>=1.2.1 -python_requires = >=3.8 -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 7f1a1763..00000000 --- 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 e69de29b..80f05db7 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__"] diff --git a/src/tranquilo/acceptance_decision.py b/src/tranquilo/acceptance_decision.py index 534b3397..331be1ef 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/aggregate_models.py b/src/tranquilo/aggregate_models.py index d5ea4d6b..adff6dbe 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/config.py b/src/tranquilo/config.py index 121780fd..13df4519 100644 --- a/src/tranquilo/config.py +++ b/src/tranquilo/config.py @@ -1,7 +1,6 @@ +import importlib.util from pathlib import Path -import plotly.express as px - DOCS_DIR = Path(__file__).parent.parent / "docs" EXAMPLE_DIR = Path(__file__).parent / "examples" @@ -9,15 +8,26 @@ 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 CRITERION_PENALTY_CONSTANT = 100 +# ====================================================================================== +# Check Available Packages +# ====================================================================================== + + +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") +IS_PLOTLY_INSTALLED = _is_installed("plotly") + + # ================================================================================= # Dashboard Defaults # ================================================================================= diff --git a/src/tranquilo/estimate_variance.py b/src/tranquilo/estimate_variance.py index 4a74e332..98e6bd70 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=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): + 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 27b00c70..67f7b189 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=True): 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 318d3b15..08f681ae 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 151ae5ca..65e432d3 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 8707ff73..716f9d19 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=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 7f81fefc..4ad586f9 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=True): 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 50b63da8..ec5e0b74 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 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/src/tranquilo/options.py b/src/tranquilo/options.py index c83eced2..491bad5d 100644 --- a/src/tranquilo/options.py +++ b/src/tranquilo/options.py @@ -1,8 +1,10 @@ +from enum import Enum from typing import NamedTuple -from tranquilo.models import n_free_params import numpy as np +from tranquilo.models import n_free_params + def get_default_stagnation_options(noisy, batch_size): if noisy: @@ -256,3 +258,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/process_arguments.py b/src/tranquilo/process_arguments.py index 579fd65c..266b3e72 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,39 +11,39 @@ 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( # 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 +60,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 +90,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 +190,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/sample_points.py b/src/tranquilo/sample_points.py index 5ff0745a..fa905e73 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): @@ -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/solve_subproblem.py b/src/tranquilo/solve_subproblem.py index f08ed813..d0d18379 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 7f07cd57..78868d13 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 6e46215b..499487a5 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 ad64c816..1033e672 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 1e87fcfa..aa341240 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 dc529de7..33dc9db7 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 c5cf2253..d93ba1c9 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 1f8dae54..d487fdc4 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 b02cc196..03f2c280 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_) diff --git a/src/tranquilo/subsolvers/fallback_subsolvers.py b/src/tranquilo/subsolvers/fallback_subsolvers.py index 5192aad8..d52561ee 100644 --- a/src/tranquilo/subsolvers/fallback_subsolvers.py +++ b/src/tranquilo/subsolvers/fallback_subsolvers.py @@ -1,5 +1,6 @@ -import numpy as np from functools import partial + +import numpy as np from scipy.optimize import Bounds, NonlinearConstraint, minimize from tranquilo.exploration_sample import draw_exploration_sample @@ -80,8 +81,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. @@ -184,6 +186,8 @@ def _grad(x, g, h): def _get_constraint(): + """Raises scipy warning.""" + def _constr_fun(x): return x @ x diff --git a/src/tranquilo/subsolvers/gqtpar.py b/src/tranquilo/subsolvers/gqtpar.py index c48997f5..ef18fd0b 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 5994feab..7685bf1d 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 fd33dda3..924c93aa 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 845d2bb9..479f11fd 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 55783eea..02ab772b 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 ca2092b6..b26b9557 100644 --- a/src/tranquilo/visualize.py +++ b/src/tranquilo/visualize.py @@ -1,18 +1,21 @@ from copy import deepcopy +from typing import Any, Protocol, runtime_checkable 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 optimagic.optimization.optimize_result import OptimizeResult 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 +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). @@ -52,11 +55,16 @@ 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} 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} @@ -83,8 +91,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 ) @@ -124,7 +132,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, @@ -319,7 +327,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)", @@ -422,7 +430,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( @@ -448,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[ @@ -516,7 +524,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] @@ -531,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 @@ -588,3 +596,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 diff --git a/src/tranquilo/volume.py b/src/tranquilo/volume.py index 1c092f84..35c5c40d 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 e432eae9..de76a28b 100644 --- a/src/tranquilo/wrap_criterion.py +++ b/src/tranquilo/wrap_criterion.py @@ -1,33 +1,34 @@ -import functools - import numpy as np +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.") @@ -41,28 +42,22 @@ def wrapper_criterion(eval_info): xs = history.get_xs(x_indices) xs = np.repeat(xs, repetitions, axis=0) - arguments = list(xs) + x_list = list(xs) - effective_n_cores = min(n_cores, len(arguments)) + effective_n_cores = min(n_cores, len(x_list)) - raw_evals = batch_evaluator( - criterion, - arguments=arguments, - n_cores=effective_n_cores, - error_handling="continue", - ) - - # 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( @@ -71,24 +66,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 optimagic.batch_evaluators import joblib_batch_evaluator as out - elif batch_evaluator == "pathos": - from optimagic.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 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..ff77497f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +from _pytest.monkeypatch import MonkeyPatch + +from tranquilo.config import IS_OPTIMAGIC_INSTALLED + +if IS_OPTIMAGIC_INSTALLED: + import optimagic.optimizers.tranquilo as t + + _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() diff --git a/tests/subsolvers/test_bntr_fast.py b/tests/subsolvers/test_bntr_fast.py index d85e84e1..3663f728 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 68175a35..d7029be4 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 58208018..d5d8119d 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 0c1a770e..3a341300 100644 --- a/tests/subsolvers/test_gqtpar_lambdas.py +++ b/tests/subsolvers/test_gqtpar_lambdas.py @@ -1,11 +1,17 @@ -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.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.") 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}, @@ -13,7 +19,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/subsolvers/test_minimize_trust_region.py b/tests/subsolvers/test_minimize_trust_region.py index 714926ce..6aa0457e 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 2be2bab9..4f04647a 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=True, ) diff --git a/tests/test_acceptance_sample_size.py b/tests/test_acceptance_sample_size.py index a1f3536c..a7a36387 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 ed29705c..c4c6b7dc 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 f3ee4007..c6936816 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 2582c419..44704414 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 40847068..2d49f20f 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 f8eb23de..e1c570c4 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 3b93dc56..e4ca9018 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 59ac427b..cabd627a 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 178963cc..6f51ef97 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.config import IS_OPTIMAGIC_INSTALLED 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 + +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} @@ -113,9 +121,10 @@ 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.") @pytest.mark.parametrize("model", ("ols", "ridge", "tranquilo", "powell")) def test_fit_ols_against_hessian(model, quadratic_case): options = {"l2_penalty_square": 0} @@ -134,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_get_component.py b/tests/test_get_component.py index 0d4ac913..7daf6004 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 8cf01533..72da10ae 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 efc0e298..3a8cc230 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 90beda8c..dffe713c 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 ac8b713e..cc6afd6e 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 f4c3c7a1..555e9884 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 78415934..8f44ad15 100644 --- a/tests/test_process_arguments.py +++ b/tests/test_process_arguments.py @@ -4,25 +4,27 @@ 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, ) 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_region.py b/tests/test_region.py index 7e021f63..197b6de4 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 70a58f28..3e72e465 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 973ceb5c..ec473539 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 28fb3da1..a9ff67e2 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 b966132f..33bb9096 100644 --- a/tests/test_tranquilo.py +++ b/tests/test_tranquilo.py @@ -1,12 +1,16 @@ import itertools +from functools import partial import numpy as np import pytest -from optimagic.optimization.optimize import minimize -from tranquilo.tranquilo import _tranquilo -from functools import partial from numpy.testing import assert_array_almost_equal as aaae -from optimagic import mark + +from tranquilo.config import IS_OPTIMAGIC_INSTALLED +from tranquilo.tranquilo import _tranquilo + +if IS_OPTIMAGIC_INSTALLED: + from optimagic import mark + from optimagic.optimization.optimize import minimize tranquilo = partial( @@ -68,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, @@ -105,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, @@ -119,9 +123,10 @@ 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", ) @@ -158,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, @@ -172,9 +177,10 @@ 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", ) @@ -186,32 +192,41 @@ def test_external_tranquilo_ls_sphere_defaults(): # Noisy case # ====================================================================================== +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.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.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( - criterion=_f, + fun=criterion, params=np.arange(5), - algorithm=algo, + algorithm=algorithm, algo_options={"noisy": True}, ) 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) + @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}, @@ -225,18 +240,13 @@ def _f(x): # ====================================================================================== -def sum_of_squares(x): - contribs = x**2 - return {"value": contribs.sum(), "contributions": contribs, "root_contributions": x} - - -@pytest.mark.parametrize("algorithm", ["tranquilo", "tranquilo_ls"]) -def test_tranquilo_with_binding_bounds(algorithm): +@pytest.mark.skipif(not IS_OPTIMAGIC_INSTALLED, reason="optimagic is not installed.") +@pytest.mark.parametrize("algorithm, criterion", ALGORITHM_AND_CRITERION) +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_utilities.py b/tests/test_utilities.py index 969fbd65..cde09aec 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=True): assert propose_alternatives(inp[0], possibilities, number=inp[1]) == exp diff --git a/tests/test_visualize.py b/tests/test_visualize.py index 6882164c..7e62b48b 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -1,48 +1,47 @@ 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 +from tranquilo.visualize import visualize_tranquilo -cases = [] -algo_options = { - "random_hull": { - "sampler": "random_hull", - "sphere_subsolver": "gqtpar_fast", - "sample_filter": "keep_all", - "stopping_max_iterations": 10, - }, - "optimal_hull": { - "sampler": "optimal_hull", - "sphere_subsolver": "gqtpar_fast", - "sample_filter": "keep_all", - "stopping_max_iterations": 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( - criterion=fun, - params=start_params, - algo_options=options, - algorithm=algorithm, - ) - cases.append(results) +if IS_OPTIMAGIC_INSTALLED: + from optimagic.benchmarking.get_benchmark_problems import get_benchmark_problems + from optimagic.optimization.optimize import minimize -skip_reason = ( - "History collection of tranquilo and tranquilo_ls is disabled in optimagic, but" - "visualize_tranquilo requires a history." -) +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.skip(reason=skip_reason) -@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(): diff --git a/tests/test_volume.py b/tests/test_volume.py index ff11aa8e..deac64f6 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=True))) 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 5ea0dfa2..78f95f42 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 c5af1497..1deabd37 100644 --- a/tests/test_wrap_criterion.py +++ b/tests/test_wrap_criterion.py @@ -2,13 +2,23 @@ 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])) +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 +29,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 +40,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