Skip to content

Fix bounds parsing in Scipy optimizers and warn when unsupported #155

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
bc4c98c
Avoid multiply defined bounds options in optimizer
edoaltamura Feb 13, 2024
85e16b6
Update change date
edoaltamura Feb 13, 2024
6604837
Add warnings for parsing unsupported bounds
edoaltamura Feb 14, 2024
77abbcb
Add algorithms and optimizer specific warnings
edoaltamura Feb 14, 2024
0163c53
Fix missed import
edoaltamura Feb 14, 2024
8167209
Merge branch 'main' into main
edoaltamura Feb 14, 2024
343ae7c
Apply style patches
edoaltamura Feb 15, 2024
d1db2a3
Add unit tests and introduce _bounds attribute
edoaltamura Feb 19, 2024
83df2c1
Formatting and final patches to tests
edoaltamura Feb 19, 2024
f4159ad
Merge branch 'qiskit-community:main' into main
edoaltamura Feb 20, 2024
7215fcc
Merge branch 'main' into main
woodsp-ibm Feb 22, 2024
28c5f67
Bounds parsing forbidden in __init__ and allowed in minimize()
edoaltamura Feb 23, 2024
a217786
Merge remote-tracking branch 'origin/main'
edoaltamura Feb 23, 2024
6d057ec
Add bounds type hinting
edoaltamura Feb 24, 2024
cdfc487
Change copyright in tests
edoaltamura Feb 24, 2024
7c0bf6b
Remove scikit-quant dependency
edoaltamura Feb 24, 2024
a4f45ee
Update qiskit_algorithms/optimizers/scipy_optimizer.py
edoaltamura Feb 24, 2024
18b0f77
Update qiskit_algorithms/optimizers/scipy_optimizer.py
edoaltamura Feb 27, 2024
140fce6
Update qiskit_algorithms/optimizers/scipy_optimizer.py
edoaltamura Feb 27, 2024
2fbd23a
Remove internal implementation checks in test_optimizers.py
edoaltamura Feb 27, 2024
d3eb8f2
Merge branch 'qiskit-community:main' into main
edoaltamura Feb 29, 2024
30c4b72
Avoid multiply defined bounds options in optimizer
edoaltamura Feb 13, 2024
06240b8
Update change date
edoaltamura Feb 13, 2024
be6dd75
Add warnings for parsing unsupported bounds
edoaltamura Feb 14, 2024
28d1493
Add algorithms and optimizer specific warnings
edoaltamura Feb 14, 2024
8446806
Fix missed import
edoaltamura Feb 14, 2024
f47d31a
Apply style patches
edoaltamura Feb 15, 2024
d7a0986
Add unit tests and introduce _bounds attribute
edoaltamura Feb 19, 2024
adfb264
Formatting and final patches to tests
edoaltamura Feb 19, 2024
cd7a34e
Bounds parsing forbidden in __init__ and allowed in minimize()
edoaltamura Feb 23, 2024
960cd7c
Add bounds type hinting
edoaltamura Feb 24, 2024
91ffeff
Change copyright in tests
edoaltamura Feb 24, 2024
ca0d277
Remove scikit-quant dependency
edoaltamura Feb 24, 2024
a7bbd21
Update qiskit_algorithms/optimizers/scipy_optimizer.py
edoaltamura Feb 24, 2024
3611957
Update qiskit_algorithms/optimizers/scipy_optimizer.py
edoaltamura Feb 27, 2024
08284c9
Update qiskit_algorithms/optimizers/scipy_optimizer.py
edoaltamura Feb 27, 2024
b9a32dc
Remove internal implementation checks in test_optimizers.py
edoaltamura Feb 27, 2024
7a5fc0c
Remove loose incompatible-bounds logic
edoaltamura Feb 29, 2024
f20fe98
Merge remote-tracking branch 'origin/main'
edoaltamura Feb 29, 2024
a467036
Remove loose incompatible-bounds logic
edoaltamura Feb 29, 2024
a8d52e0
Update qiskit_algorithms/optimizers/scipy_optimizer.py
edoaltamura Mar 7, 2024
0f602e7
Remove redundant `_bounds` attributes
edoaltamura Mar 7, 2024
1472f4f
Add release note
edoaltamura Mar 7, 2024
f4d76fa
Remove _bounds attr from tests, add `list[tuple[None, None]]` as poss…
edoaltamura Mar 13, 2024
997f38b
Remove _bounds attr from tests
edoaltamura Mar 13, 2024
602ab8c
Merge branch 'main' into main
woodsp-ibm Apr 23, 2024
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
23 changes: 21 additions & 2 deletions qiskit_algorithms/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2017, 2023.
# (C) Copyright IBM 2017, 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
Expand All @@ -10,7 +10,7 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Exception for errors raised by Algorithms module."""
"""Exception and warnings for errors raised by Algorithms module."""

from qiskit.exceptions import QiskitError

Expand All @@ -19,3 +19,22 @@ class AlgorithmError(QiskitError):
"""For Algorithm specific errors."""

pass


class QiskitAlgorithmsWarning(UserWarning):
"""Base class for warnings raised by Qiskit Algorithms."""

def __init__(self, *message):
"""Set the error message."""
super().__init__(" ".join(message))
self.message = " ".join(message)

def __str__(self):
"""Return the message."""
return repr(self.message)


class QiskitAlgorithmsOptimizersWarning(QiskitAlgorithmsWarning):
"""For Algorithm specific warnings."""

pass
44 changes: 39 additions & 5 deletions qiskit_algorithms/optimizers/scipy_optimizer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2018, 2023.
# (C) Copyright IBM 2018, 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
Expand All @@ -15,10 +15,12 @@

from collections.abc import Callable
from typing import Any
import warnings

import numpy as np
from scipy.optimize import minimize

from qiskit_algorithms.exceptions import QiskitAlgorithmsOptimizersWarning
from qiskit_algorithms.utils.validation import validate_min
from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT

Expand Down Expand Up @@ -76,6 +78,20 @@ def __init__(
self._max_evals_grouped = max_evals_grouped
self._kwargs = kwargs

# Initialise bounds and re-allocate if definition in options or kwargs
self._bounds: list[tuple[float, float]] | None = None

if "bounds" in self._kwargs:
raise RuntimeError(
"Optimizer bounds should be parsed in SciPyOptimizer.minimize() and not in "
"SciPyOptimizer.__init__()."
)
if "bounds" in self._options:
raise RuntimeError(
"Optimizer bounds should be parsed in SciPyOptimizer.minimize() as a kwarg and not as "
"options."
)

def get_support_level(self):
"""Return support level dictionary"""
return {
Expand Down Expand Up @@ -116,9 +132,27 @@ def minimize(
jac: Callable[[POINT], POINT] | None = None,
bounds: list[tuple[float, float]] | None = None,
) -> OptimizerResult:
# Remove ignored parameters to suppress the warning of scipy.optimize.minimize
if self.is_bounds_ignored:
bounds = None

# Overwrite the previous bounds
if bounds is not None:
self._bounds = bounds

# Remove ignored bounds to suppress the warning of scipy.optimize.minimize
if self.is_bounds_ignored and self._bounds is not None:
warnings.warn(
f"Optimizer method {self._method} does not support bounds. Bounds ignored.",
QiskitAlgorithmsOptimizersWarning,
)
self._bounds = None
elif not self.is_bounds_ignored and self._bounds is None:
warnings.warn(
f"Optimizer method {self._method} may require defining bounds. "
f"Check the Scipy documentation for more info: "
f"https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html",
QiskitAlgorithmsOptimizersWarning,
)

# Remove ignored gradient to suppress the warning of scipy.optimize.minimize
if self.is_gradient_ignored:
jac = None

Expand Down Expand Up @@ -150,7 +184,7 @@ def minimize(
x0=x0,
method=self._method,
jac=jac,
bounds=bounds,
bounds=self._bounds,
options=self._options,
**self._kwargs,
)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
qiskit>=0.44
scipy>=1.4
numpy>=1.17
scikit-quant
84 changes: 82 additions & 2 deletions test/optimizers/test_optimizers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2018, 2023.
# (C) Copyright IBM 2018, 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
Expand Down Expand Up @@ -49,6 +49,7 @@
SciPyOptimizer,
)
from qiskit_algorithms.utils import algorithm_globals
from qiskit_algorithms.exceptions import QiskitAlgorithmsOptimizersWarning


@ddt
Expand Down Expand Up @@ -149,7 +150,7 @@ def test_gsls(self):
max_eval=10000,
min_step_size=1.0e-12,
)
x_0 = [1.3, 0.7, 0.8, 1.9, 1.2]
x_0 = np.asarray([1.3, 0.7, 0.8, 1.9, 1.2])

algorithm_globals.random_seed = 1
res = optimizer.minimize(rosen, x_0)
Expand Down Expand Up @@ -183,6 +184,85 @@ def callback(x):
self.run_optimizer(optimizer, max_nfev=10000)
self.assertTrue(values) # Check the list is nonempty.

def test_scipy_optimizer_parse_bounds(self):
"""
Test the parsing of bounds in SciPyOptimizer.minimize method. Verifies that the bounds are
correctly parsed and set within the optimizer object.

Raises:
AssertionError: If any of the assertions fail.
AssertionError: If a TypeError is raised unexpectedly while parsing bounds.

"""
try:
# Initialize SciPyOptimizer instance with SLSQP method
optimizer = SciPyOptimizer("SLSQP")

# Ensure that _bounds attribute is initially present and =None
self.assertTrue(hasattr(optimizer, "_bounds"))
self.assertTrue(optimizer._bounds is None)

# Call minimize method with a simple lambda function and bounds
optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)])

# Assert that _bounds attribute is set after minimize method call
self.assertTrue(hasattr(optimizer, "_bounds"))

# Assert that "bounds" is not present in optimizer options and kwargs
self.assertFalse("bounds" in optimizer._options)
self.assertFalse("bounds" in optimizer._kwargs)

# Assert that _bounds attribute is not None after setting bounds
self.assertFalse(optimizer._bounds is None)

except TypeError:
# This would give: https://github.com/qiskit-community/qiskit-machine-learning/issues/570
self.fail(
"TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer.minimize(...)."
)

# Finally, expect exceptions if bounds are parsed incorrectly, i.e. differently than as in Scipy
with self.assertRaises(RuntimeError):
_ = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)])
with self.assertRaises(RuntimeError):
_ = SciPyOptimizer("SLSQP", options={"bounds": [(0.0, 1.0)]})

def test_scipy_optimizer_warning(self):
"""
Test warning handling in SciPyOptimizer.minimize method when using unsupported
optimizer. Verifies that a warning is raised when attempting to use an
optimizer that does not support bounds.

Raises:
AssertionError: If the expected warning is not raised.

"""
# Initialize SciPyOptimizer instance with "cobyla" method
optimizer = SciPyOptimizer("cobyla")

# Use assertWarns context manager to check if the expected warning is raised
with self.assertWarns(QiskitAlgorithmsOptimizersWarning):
# Call minimize method with a simple lambda function and bounds for unsupported optimizer
optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)])

def test_scipy_optimizer_bounds_required(self):
"""
Test bounds requirement handling in SciPyOptimizer.minimize method for optimizers
that require bounds. Verifies that an exception is raised when attempting to use
an optimizer that requires bounds without supplying them.

Raises:
AssertionError: If the expected exception is not raised.

"""
# Initialize SciPyOptimizer instance with "SLSQP" method
optimizer = SciPyOptimizer("SLSQP")

# Use assertWarns context manager to check if the expected exception is raised
with self.assertWarns(QiskitAlgorithmsOptimizersWarning):
# Call minimize method with a simple lambda function without supplying required bounds
optimizer.minimize(lambda x: -x, 1.0)

# ESCH and ISRES do not do well with rosen
@data(
(CRS, True),
Expand Down