diff --git a/qiskit_algorithms/exceptions.py b/qiskit_algorithms/exceptions.py index 7eda309e..9d274e1a 100644 --- a/qiskit_algorithms/exceptions.py +++ b/qiskit_algorithms/exceptions.py @@ -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 @@ -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 @@ -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 diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 230822cf..f8cbaf84 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -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 @@ -76,6 +76,17 @@ def __init__( self._max_evals_grouped = max_evals_grouped self._kwargs = kwargs + if "bounds" in self._kwargs: + raise RuntimeError( + "Optimizer bounds should be passed to SciPyOptimizer.minimize() and is not " + "supported in SciPyOptimizer constructor kwargs." + ) + if "bounds" in self._options: + raise RuntimeError( + "Optimizer bounds should be passed to SciPyOptimizer.minimize() and not as " + "options." + ) + def get_support_level(self): """Return support level dictionary""" return { @@ -116,9 +127,12 @@ 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 + + # Remove ignored bounds to suppress the warning of scipy.optimize.minimize if self.is_bounds_ignored: bounds = None + + # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: jac = None diff --git a/releasenotes/notes/0.3/raise-error-incorrect-bounds-passing-c878f155221fa8f4.yaml b/releasenotes/notes/0.3/raise-error-incorrect-bounds-passing-c878f155221fa8f4.yaml new file mode 100644 index 00000000..c2a0908c --- /dev/null +++ b/releasenotes/notes/0.3/raise-error-incorrect-bounds-passing-c878f155221fa8f4.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Resolved the issue with multiply-defined Scipy bounds in Optimizers. In line with Scipy, now only the ``minimize()`` + method supports the ``bounds`` keyword. An error is raised when trying to pass ``bounds`` in the Optimizer constructor + via ``kwargs`` or ``options``. diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 731bb90a..c3b9e4cb 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -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 @@ -149,7 +149,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) @@ -183,6 +183,39 @@ 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") + + # 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" is not present in optimizer options and kwargs + self.assertFalse("bounds" in optimizer._options) + self.assertFalse("bounds" in optimizer._kwargs) + + 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)]}) + # ESCH and ISRES do not do well with rosen @data( (CRS, True),