Skip to content

Commit 7a2553a

Browse files
edoaltamurawoodsp-ibm
authored andcommitted
Fix bounds parsing in Scipy optimizers and warn when unsupported (qiskit-community#155)
Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com>
1 parent 09ccec9 commit 7a2553a

File tree

4 files changed

+78
-6
lines changed

4 files changed

+78
-6
lines changed

qiskit_algorithms/exceptions.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of a Qiskit project.
22
#
3-
# (C) Copyright IBM 2017, 2023.
3+
# (C) Copyright IBM 2017, 2024.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -10,7 +10,7 @@
1010
# copyright notice, and modified files need to carry a notice indicating
1111
# that they have been altered from the originals.
1212

13-
"""Exception for errors raised by Algorithms module."""
13+
"""Exception and warnings for errors raised by Algorithms module."""
1414

1515
from qiskit.exceptions import QiskitError
1616

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

2121
pass
22+
23+
24+
class QiskitAlgorithmsWarning(UserWarning):
25+
"""Base class for warnings raised by Qiskit Algorithms."""
26+
27+
def __init__(self, *message):
28+
"""Set the error message."""
29+
super().__init__(" ".join(message))
30+
self.message = " ".join(message)
31+
32+
def __str__(self):
33+
"""Return the message."""
34+
return repr(self.message)
35+
36+
37+
class QiskitAlgorithmsOptimizersWarning(QiskitAlgorithmsWarning):
38+
"""For Algorithm specific warnings."""
39+
40+
pass

qiskit_algorithms/optimizers/scipy_optimizer.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of a Qiskit project.
22
#
3-
# (C) Copyright IBM 2018, 2023.
3+
# (C) Copyright IBM 2018, 2024.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -76,6 +76,17 @@ def __init__(
7676
self._max_evals_grouped = max_evals_grouped
7777
self._kwargs = kwargs
7878

79+
if "bounds" in self._kwargs:
80+
raise RuntimeError(
81+
"Optimizer bounds should be passed to SciPyOptimizer.minimize() and is not "
82+
"supported in SciPyOptimizer constructor kwargs."
83+
)
84+
if "bounds" in self._options:
85+
raise RuntimeError(
86+
"Optimizer bounds should be passed to SciPyOptimizer.minimize() and not as "
87+
"options."
88+
)
89+
7990
def get_support_level(self):
8091
"""Return support level dictionary"""
8192
return {
@@ -116,9 +127,12 @@ def minimize(
116127
jac: Callable[[POINT], POINT] | None = None,
117128
bounds: list[tuple[float, float]] | None = None,
118129
) -> OptimizerResult:
119-
# Remove ignored parameters to suppress the warning of scipy.optimize.minimize
130+
131+
# Remove ignored bounds to suppress the warning of scipy.optimize.minimize
120132
if self.is_bounds_ignored:
121133
bounds = None
134+
135+
# Remove ignored gradient to suppress the warning of scipy.optimize.minimize
122136
if self.is_gradient_ignored:
123137
jac = None
124138

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Resolved the issue with multiply-defined Scipy bounds in Optimizers. In line with Scipy, now only the ``minimize()``
5+
method supports the ``bounds`` keyword. An error is raised when trying to pass ``bounds`` in the Optimizer constructor
6+
via ``kwargs`` or ``options``.

test/optimizers/test_optimizers.py

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of a Qiskit project.
22
#
3-
# (C) Copyright IBM 2018, 2023.
3+
# (C) Copyright IBM 2018, 2024.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -149,7 +149,7 @@ def test_gsls(self):
149149
max_eval=10000,
150150
min_step_size=1.0e-12,
151151
)
152-
x_0 = [1.3, 0.7, 0.8, 1.9, 1.2]
152+
x_0 = np.asarray([1.3, 0.7, 0.8, 1.9, 1.2])
153153

154154
algorithm_globals.random_seed = 1
155155
res = optimizer.minimize(rosen, x_0)
@@ -183,6 +183,39 @@ def callback(x):
183183
self.run_optimizer(optimizer, max_nfev=10000)
184184
self.assertTrue(values) # Check the list is nonempty.
185185

186+
def test_scipy_optimizer_parse_bounds(self):
187+
"""
188+
Test the parsing of bounds in SciPyOptimizer.minimize method. Verifies that the bounds are
189+
correctly parsed and set within the optimizer object.
190+
191+
Raises:
192+
AssertionError: If any of the assertions fail.
193+
AssertionError: If a TypeError is raised unexpectedly while parsing bounds.
194+
195+
"""
196+
try:
197+
# Initialize SciPyOptimizer instance with SLSQP method
198+
optimizer = SciPyOptimizer("SLSQP")
199+
200+
# Call minimize method with a simple lambda function and bounds
201+
optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)])
202+
203+
# Assert that "bounds" is not present in optimizer options and kwargs
204+
self.assertFalse("bounds" in optimizer._options)
205+
self.assertFalse("bounds" in optimizer._kwargs)
206+
207+
except TypeError:
208+
# This would give: https://github.com/qiskit-community/qiskit-machine-learning/issues/570
209+
self.fail(
210+
"TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer.minimize(...)."
211+
)
212+
213+
# Finally, expect exceptions if bounds are parsed incorrectly, i.e. differently than as in Scipy
214+
with self.assertRaises(RuntimeError):
215+
_ = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)])
216+
with self.assertRaises(RuntimeError):
217+
_ = SciPyOptimizer("SLSQP", options={"bounds": [(0.0, 1.0)]})
218+
186219
# ESCH and ISRES do not do well with rosen
187220
@data(
188221
(CRS, True),

0 commit comments

Comments
 (0)