Skip to content

Commit

Permalink
Polish the algo selection tool (#550)
Browse files Browse the repository at this point in the history
  • Loading branch information
janosg authored Nov 13, 2024
1 parent 058c79e commit 84be118
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 72 deletions.
45 changes: 25 additions & 20 deletions .tools/create_algo_selection_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _get_algorithms_in_module(module: ModuleType) -> dict[str, Type[Algorithm]]:
}
algos = {}
for candidate in candidate_dict.values():
name = candidate.__algo_info__.name
name = candidate.algo_info.name
if issubclass(candidate, Algorithm) and candidate is not Algorithm:
algos[name] = candidate
return algos
Expand All @@ -119,47 +119,47 @@ def _get_algorithms_in_module(module: ModuleType) -> dict[str, Type[Algorithm]]:
# Functions to filter algorithms by selectors
# ======================================================================================
def _is_gradient_based(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.needs_jac # type: ignore
return algo.algo_info.needs_jac # type: ignore


def _is_gradient_free(algo: Type[Algorithm]) -> bool:
return not _is_gradient_based(algo)


def _is_global(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.is_global # type: ignore
return algo.algo_info.is_global # type: ignore


def _is_local(algo: Type[Algorithm]) -> bool:
return not _is_global(algo)


def _is_bounded(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.supports_bounds # type: ignore
return algo.algo_info.supports_bounds # type: ignore


def _is_linear_constrained(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.supports_linear_constraints # type: ignore
return algo.algo_info.supports_linear_constraints # type: ignore


def _is_nonlinear_constrained(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.supports_nonlinear_constraints # type: ignore
return algo.algo_info.supports_nonlinear_constraints # type: ignore


def _is_scalar(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.solver_type == AggregationLevel.SCALAR # type: ignore
return algo.algo_info.solver_type == AggregationLevel.SCALAR # type: ignore


def _is_least_squares(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.solver_type == AggregationLevel.LEAST_SQUARES # type: ignore
return algo.algo_info.solver_type == AggregationLevel.LEAST_SQUARES # type: ignore


def _is_likelihood(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.solver_type == AggregationLevel.LIKELIHOOD # type: ignore
return algo.algo_info.solver_type == AggregationLevel.LIKELIHOOD # type: ignore


def _is_parallel(algo: Type[Algorithm]) -> bool:
return algo.__algo_info__.supports_parallelism # type: ignore
return algo.algo_info.supports_parallelism # type: ignore


def _get_filters() -> dict[str, Callable[[Type[Algorithm]], bool]]:
Expand Down Expand Up @@ -385,27 +385,32 @@ def _all(self) -> list[Type[Algorithm]]:
def _available(self) -> list[Type[Algorithm]]:
_all = self._all()
return [
a for a in _all if a.__algo_info__.is_available # type: ignore
a for a in _all if a.algo_info.is_available # type: ignore
]
@property
def All(self) -> list[str]:
return [a.__algo_info__.name for a in self._all()] # type: ignore
def All(self) -> list[Type[Algorithm]]:
return self._all()
@property
def Available(self) -> list[str]:
return [a.__algo_info__.name for a in self._available()] # type: ignore
def Available(self) -> list[Type[Algorithm]]:
return self._available()
@property
def AllNames(self) -> list[str]:
return [str(a.name) for a in self._all()]
@property
def AvailableNames(self) -> list[str]:
return [str(a.name) for a in self._available()]
@property
def _all_algorithms_dict(self) -> dict[str, Type[Algorithm]]:
return {a.__algo_info__.name: a for a in self._all()} # type: ignore
return {str(a.name): a for a in self._all()}
@property
def _available_algorithms_dict(self) -> dict[str, Type[Algorithm]]:
return {
a.__algo_info__.name: a # type: ignore
for a in self._available()
}
return {str(a.name): a for a in self._available()}
""")
return out
Expand Down
22 changes: 20 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,27 @@ This is a record of all past optimagic releases and what went into them in rever
chronological order. We follow [semantic versioning](https://semver.org/) and all
releases are available on [Anaconda.org](https://anaconda.org/optimagic-dev/optimagic).

Following the [scientific python guidelines](https://scientific-python.org/specs/spec-0000/)
we drop the official support for Python 3.9.

## 0.5.1

This is a minor release that introduces the new algorithm selection tool and several
small improvements.

To learn more about the algorithm selection feature check out the following resources:

- [How to specify and configure algorithms](https://optimagic.readthedocs.io/en/latest/how_to/how_to_specify_algorithm_and_algo_options.html)
- [How to select local optimizers](https://optimagic.readthedocs.io/en/latest/how_to/how_to_algorithm_selection.html)

- {gh}`549` Add support for Python 3.13 ({ghuser}`timmens`)
- {gh}`550` and {gh}`534` implement the new algorithm selection tool ({ghuser}`janosg`)
- {gh}`548` and {gh}`531` improve the documentation ({ghuser}`ChristianZimpelmann`)
- {gh}`544` Adjusts the results processing of the nag optimizers to be compatible
with the latest releases ({ghuser}`timmens`)
- {gh}`543` Adds support for numpy 2.x ({ghuser}`timmens`)
- {gh}`536` Adds a how-to guide for choosing local optimizers ({ghuser}`mpetrosian`)
- {gh}`535` Allows algorithm classes and instances in estimation functions
({ghuser}`timmens`)
- {gh}`532` Makes several small improvements to the documentation.

## 0.5.0

Expand Down
6 changes: 0 additions & 6 deletions src/estimagic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import contextlib
import warnings
from dataclasses import dataclass

try:
import pdbp # noqa: F401
except ImportError:
contextlib.suppress(Exception)

from estimagic import utilities
from estimagic.bootstrap import BootstrapResult, bootstrap
from estimagic.estimate_ml import LikelihoodResult, estimate_ml
Expand Down
7 changes: 0 additions & 7 deletions src/optimagic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
from __future__ import annotations

import contextlib

try:
import pdbp # noqa: F401
except ImportError:
contextlib.suppress(Exception)

from optimagic import constraints, mark, utilities
from optimagic.algorithms import algos
from optimagic.benchmarking.benchmark_reports import (
Expand Down
25 changes: 15 additions & 10 deletions src/optimagic/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,32 @@ def _available(self) -> list[Type[Algorithm]]:
return [
a
for a in _all
if a.__algo_info__.is_available # type: ignore
if a.algo_info.is_available # type: ignore
]

@property
def All(self) -> list[str]:
return [a.__algo_info__.name for a in self._all()] # type: ignore
def All(self) -> list[Type[Algorithm]]:
return self._all()

@property
def Available(self) -> list[str]:
return [a.__algo_info__.name for a in self._available()] # type: ignore
def Available(self) -> list[Type[Algorithm]]:
return self._available()

@property
def AllNames(self) -> list[str]:
return [str(a.name) for a in self._all()]

@property
def AvailableNames(self) -> list[str]:
return [str(a.name) for a in self._available()]

@property
def _all_algorithms_dict(self) -> dict[str, Type[Algorithm]]:
return {a.__algo_info__.name: a for a in self._all()} # type: ignore
return {str(a.name): a for a in self._all()}

@property
def _available_algorithms_dict(self) -> dict[str, Type[Algorithm]]:
return {
a.__algo_info__.name: a # type: ignore
for a in self._available()
}
return {str(a.name): a for a in self._available()}


@dataclass(frozen=True)
Expand Down
34 changes: 32 additions & 2 deletions src/optimagic/optimization/algorithm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typing
import warnings
from abc import ABC, abstractmethod
from abc import ABC, ABCMeta, abstractmethod
from dataclasses import dataclass, replace
from typing import Any

Expand Down Expand Up @@ -143,8 +143,38 @@ def __post_init__(self) -> None:
raise TypeError(msg)


class AlgorithmMeta(ABCMeta):
"""Metaclass to get repr, algo_info and name for classes, not just instances."""

def __repr__(self) -> str:
if hasattr(self, "__algo_info__") and self.__algo_info__ is not None:
out = f"om.algos.{self.__algo_info__.name}"
else:
out = self.__class__.__name__
return out

@property
def name(self) -> str:
if hasattr(self, "__algo_info__") and self.__algo_info__ is not None:
out = self.__algo_info__.name
else:
out = self.__class__.__name__
return out

@property
def algo_info(self) -> AlgoInfo:
if not hasattr(self, "__algo_info__") or self.__algo_info__ is None:
msg = (
f"The algorithm {self.name} does not have have the __algo_info__ "
"attribute. Use the `mark.minimizer` decorator to add this attribute."
)
raise AttributeError(msg)

return self.__algo_info__


@dataclass(frozen=True)
class Algorithm(ABC):
class Algorithm(ABC, metaclass=AlgorithmMeta):
@abstractmethod
def _solve_internal_problem(
self, problem: InternalOptimizationProblem, x0: NDArray[np.float64]
Expand Down
5 changes: 3 additions & 2 deletions src/optimagic/optimizers/pygmo_optimizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

from __future__ import annotations

import contextlib
import warnings
from dataclasses import dataclass
from typing import Any, List, Literal
Expand Down Expand Up @@ -48,8 +47,10 @@

STOPPING_MAX_ITERATIONS_GENETIC = 250

with contextlib.suppress(ImportError):
try:
import pygmo as pg
except ImportError:
pass


@mark.minimizer(
Expand Down
5 changes: 3 additions & 2 deletions src/optimagic/optimizers/tao_optimizers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""This module implements the POUNDERs algorithm."""

import contextlib
import functools
from dataclasses import dataclass

Expand All @@ -23,8 +22,10 @@
from optimagic.typing import AggregationLevel, NonNegativeFloat, PositiveInt
from optimagic.utilities import calculate_trustregion_initial_radius

with contextlib.suppress(ImportError):
try:
from petsc4py import PETSc
except ImportError:
pass


@mark.minimizer(
Expand Down
61 changes: 44 additions & 17 deletions src/optimagic/visualization/history_plots.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import inspect
import itertools
from pathlib import Path
from typing import Any

import numpy as np
import plotly.graph_objects as go
from pybaum import leaf_names, tree_flatten, tree_just_flatten, tree_unflatten

from optimagic.config import PLOTLY_PALETTE, PLOTLY_TEMPLATE
from optimagic.logging.logger import LogReader, SQLiteLogOptions
from optimagic.optimization.algorithm import Algorithm
from optimagic.optimization.history_tools import get_history_arrays
from optimagic.optimization.optimize_result import OptimizeResult
from optimagic.parameters.tree_registry import get_registry
Expand Down Expand Up @@ -50,23 +53,7 @@ def criterion_plot(
# Process inputs
# ==================================================================================

if not isinstance(names, list) and names is not None:
names = [names]

if not isinstance(results, dict):
if isinstance(results, list):
names = range(len(results)) if names is None else names
if len(names) != len(results):
raise ValueError("len(results) needs to be equal to len(names).")
results = dict(zip(names, results, strict=False))
else:
name = 0 if names is None else names
if isinstance(name, list):
if len(name) > 1:
raise ValueError("len(results) needs to be equal to len(names).")
else:
name = name[0]
results = {name: results}
results = _harmonize_inputs_to_dict(results, names)

if not isinstance(palette, list):
palette = [palette]
Expand Down Expand Up @@ -180,6 +167,46 @@ def criterion_plot(
return fig


def _harmonize_inputs_to_dict(results, names):
"""Convert all valid inputs for results and names to dict[str, OptimizeResult]."""
# convert scalar case to list case
if not isinstance(names, list) and names is not None:
names = [names]

if isinstance(results, OptimizeResult):
results = [results]

if names is not None and len(names) != len(results):
raise ValueError("len(results) needs to be equal to len(names).")

# handle dict case
if isinstance(results, dict):
if names is not None:
results_dict = dict(zip(names, list(results.values()), strict=False))
else:
results_dict = results

# unlabeled iterable of results
else:
names = range(len(results)) if names is None else names
results_dict = dict(zip(names, results, strict=False))

# convert keys to strings
results_dict = {_convert_key_to_str(k): v for k, v in results_dict.items()}

return results_dict


def _convert_key_to_str(key: Any) -> str:
if inspect.isclass(key) and issubclass(key, Algorithm):
out = str(key.name)
elif isinstance(key, Algorithm):
out = str(key.name)
else:
out = str(key)
return out


def params_plot(
result,
selector=None,
Expand Down
2 changes: 1 addition & 1 deletion tests/optimagic/optimization/test_history_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
OPTIMIZERS = []
BOUNDED = []
for name, algo in AVAILABLE_ALGORITHMS.items():
info = algo.__algo_info__
info = algo.algo_info
if not info.disable_history:
if info.supports_parallelism:
OPTIMIZERS.append(name)
Expand Down
Loading

0 comments on commit 84be118

Please sign in to comment.