Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions pypesto/optimize/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,58 @@ def set_maxeval(self, evaluations: int) -> None:
f"Check supports_maxeval() before calling set_maxeval()."
)

def supports_tol(self) -> bool:
"""
Check whether optimizer supports absolute tolerance.

Returns
-------
True if optimizer supports setting an absolute tolerance,
False otherwise.
"""
return True

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.

Raises
------
NotImplementedError
If the optimizer does not support absolute tolerance.
"""
raise NotImplementedError(
f"{self.__class__.__name__} does not support absolute tolerance. "
f"Check supports_tol() before calling set_tol()."
)

def _set_option_tol(self, tol: float, option_key: str) -> None:
"""
Set tolerance in options dict with validation.

Parameters
----------
tol
Absolute tolerance value (must be positive).
option_key
The key to use in the options dictionary.

Raises
------
ValueError
If tolerance is not positive.
"""
if tol < 0:
raise ValueError(f"Tolerance must be positive, got {tol}")
if self.options is None:
self.options = {}
self.options[option_key] = tol


class ScipyOptimizer(Optimizer):
"""
Expand Down Expand Up @@ -670,6 +722,24 @@ def set_maxiter(self, iterations: int) -> None:
else:
self.options["maxiter"] = iterations

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.

Raises
------
ValueError
If tolerance is not positive.
"""
if tol <= 0:
raise ValueError(f"Tolerance must be positive, got {tol}")
self.tol = tol


class IpoptOptimizer(Optimizer):
"""Use Ipopt (https://pypi.org/project/cyipopt/) for optimization."""
Expand Down Expand Up @@ -789,6 +859,17 @@ def set_maxiter(self, iterations: int) -> None:
self.options = {}
self.options["max_iter"] = iterations

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.
"""
self._set_option_tol(tol, "tol")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I remember, ipopt has quite complex termination criteria. While various tolerances are supported, I think just hitting this single value is insufficient for termination, so it might be a bit confusing. Not completely sure whether it should be added here or not.



class DlibOptimizer(Optimizer):
"""Use the Dlib toolbox for optimization."""
Expand Down Expand Up @@ -912,6 +993,10 @@ def set_maxiter(self, iterations: int) -> None:
self.options = {}
self.options["maxiter"] = iterations

def supports_tol(self) -> bool:
"""Check whether optimizer supports absolute tolerance."""
return False


class PyswarmOptimizer(Optimizer):
"""Global optimization using pyswarm."""
Expand Down Expand Up @@ -987,6 +1072,17 @@ def set_maxiter(self, iterations: int) -> None:
self.options = {}
self.options["maxiter"] = iterations

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.
"""
self._set_option_tol(tol, "minfunc")


class CmaOptimizer(Optimizer):
"""
Expand Down Expand Up @@ -1100,6 +1196,17 @@ def set_maxeval(self, evaluations: int) -> None:
self.options = {}
self.options["maxfevals"] = evaluations

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.
"""
self._set_option_tol(tol, "tolfun")


class CmaesOptimizer(CmaOptimizer):
"""Deprecated, use CmaOptimizer instead."""
Expand Down Expand Up @@ -1201,6 +1308,17 @@ def set_maxiter(self, iterations: int) -> None:
self.options = {}
self.options["maxiter"] = iterations

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.
"""
self._set_option_tol(tol, "atol")


class PyswarmsOptimizer(Optimizer):
"""
Expand Down Expand Up @@ -1342,6 +1460,10 @@ def set_maxiter(self, iterations: int) -> None:
self.options = {}
self.options["maxiter"] = iterations

def supports_tol(self) -> bool:
"""Check whether optimizer supports absolute tolerance."""
return False


class NLoptOptimizer(Optimizer):
"""
Expand Down Expand Up @@ -1614,6 +1736,17 @@ def set_maxeval(self, evaluations: int) -> None:
"""
self.options["maxeval"] = evaluations

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.
"""
self._set_option_tol(tol, "ftol_abs")


class FidesOptimizer(Optimizer):
"""
Expand Down Expand Up @@ -1838,3 +1971,19 @@ def set_maxiter(self, iterations: int) -> None:
self.options[FidesOptions.MAXITER] = iterations
except ImportError:
raise OptimizerImportError("fides") from None

def set_tol(self, tol: float) -> None:
"""
Set the absolute tolerance for optimization.

Parameters
----------
tol
Absolute tolerance for termination.
"""
try:
from fides.constants import Options as FidesOptions

self._set_option_tol(tol, FidesOptions.FATOL)
except ImportError:
raise OptimizerImportError("fides") from None
104 changes: 104 additions & 0 deletions test/optimize/test_optimizer_common_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,107 @@ def test_ipopt_optimizer_no_support(self):

with pytest.raises(NotImplementedError):
optimizer.set_maxeval(100)


class TestOptimizerTolInterface:
"""Test the unified tolerance interface for optimizers."""

def test_scipy_optimizer_support(self):
"""Test ScipyOptimizer tolerance support."""
optimizer = optimize.ScipyOptimizer()
assert optimizer.supports_tol() is True

optimizer.set_tol(1e-6)
assert optimizer.tol == 1e-6

# Test updating existing value
optimizer.set_tol(1e-8)
assert optimizer.tol == 1e-8

def test_ipopt_optimizer_support(self):
"""Test IpoptOptimizer tolerance support."""
optimizer = optimize.IpoptOptimizer()
assert optimizer.supports_tol() is True

optimizer.set_tol(1e-7)
assert optimizer.options["tol"] == 1e-7

def test_nlopt_optimizer_support(self):
"""Test NLoptOptimizer tolerance support."""
optimizer = optimize.NLoptOptimizer()
assert optimizer.supports_tol() is True

optimizer.set_tol(1e-5)
assert optimizer.options["ftol_abs"] == 1e-5

def test_fides_optimizer_support(self):
"""Test FidesOptimizer tolerance support."""
optimizer = optimize.FidesOptimizer()
assert optimizer.supports_tol() is True

optimizer.set_tol(1e-6)

from fides.constants import Options as FidesOptions

assert FidesOptions.FATOL in optimizer.options
assert optimizer.options[FidesOptions.FATOL] == 1e-6

# Test updating existing value
optimizer.set_tol(1e-9)
assert optimizer.options[FidesOptions.FATOL] == 1e-9

def test_cma_optimizer_support(self):
"""Test CmaOptimizer tolerance support."""
optimizer = optimize.CmaOptimizer()
assert optimizer.supports_tol() is True

optimizer.set_tol(1e-4)
assert optimizer.options["tolfun"] == 1e-4

def test_scipy_de_optimizer_support(self):
"""Test ScipyDifferentialEvolutionOptimizer tolerance support."""
optimizer = optimize.ScipyDifferentialEvolutionOptimizer()
assert optimizer.supports_tol() is True

optimizer.set_tol(1e-5)
assert optimizer.options["atol"] == 1e-5

def test_pyswarm_optimizer_support(self):
"""Test PyswarmOptimizer tolerance support."""
optimizer = optimize.PyswarmOptimizer()
assert optimizer.supports_tol() is True

optimizer.set_tol(1e-7)
assert optimizer.options["minfunc"] == 1e-7

def test_dlib_optimizer_no_support(self):
"""Test that DlibOptimizer does not support tolerance."""
optimizer = optimize.DlibOptimizer()
assert optimizer.supports_tol() is False

with pytest.raises(NotImplementedError):
optimizer.set_tol(1e-6)

def test_pyswarms_optimizer_no_support(self):
"""Test that PyswarmsOptimizer does not support tolerance."""
optimizer = optimize.PyswarmsOptimizer()
assert optimizer.supports_tol() is False

with pytest.raises(NotImplementedError):
optimizer.set_tol(1e-6)

def test_tolerance_validation(self):
"""Test that invalid tolerance values are rejected."""
optimizer = optimize.ScipyOptimizer()

# Test that positive values work
optimizer.set_tol(1e-6)
assert optimizer.tol == 1e-6

def test_tolerance_validation_options_based(self):
"""Test tolerance validation for options-based optimizers."""
optimizer = optimize.IpoptOptimizer()

# Test that positive values work
optimizer.set_tol(1e-7)
assert optimizer.options["tol"] == 1e-7