From bc4c98cd53e824c09cb0c9a726c69a867d31e029 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:49:33 +0000 Subject: [PATCH 01/39] Avoid multiply defined bounds options in optimizer --- qiskit_algorithms/optimizers/scipy_optimizer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 230822cf..b016f135 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -145,6 +145,15 @@ def minimize( swapped_deprecated_args = True self._options["maxfun"] = self._options.pop("maxiter") + # Avoid clashing bounds defined in kwargs or _options + if 'bounds' in self._kwargs and not self.is_bounds_ignored: + bounds = self._kwargs['bounds'] + del self._kwargs['bounds'] + + if 'bounds' in self._options and not self.is_bounds_ignored: + bounds = self._options['bounds'] + del self._options['bounds'] + raw_result = minimize( fun=fun, x0=x0, From 85e16b6f15d1f7100b275399335edbe03589f800 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:30:47 +0000 Subject: [PATCH 02/39] Update change date --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index b016f135..199a9ba5 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 From 6604837648a8fac07bbb32d9d69fee017a23e1ca Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:27:48 +0100 Subject: [PATCH 03/39] Add warnings for parsing unsupported bounds --- .../optimizers/scipy_optimizer.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 199a9ba5..85446d0a 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -19,6 +19,7 @@ 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 @@ -116,9 +117,25 @@ 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: + # Loop up for bounds specified in options or kwargs + if 'bounds' in self._kwargs: + bounds = self._kwargs['bounds'] + del self._kwargs['bounds'] + + if 'bounds' in self._options: + bounds = self._options['bounds'] + del self._options['bounds'] + + # Remove ignored bounds to suppress the warning of scipy.optimize.minimize + if self.is_bounds_ignored and bounds is not None: + warnings.warn( + (f'Optimizer method {self._method:s} does not support bounds. ' + f'Got bounds={bounds}, setting bounds=None.'), + QiskitAlgorithmsOptimizersWarning + ) bounds = None + + # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: jac = None @@ -145,15 +162,6 @@ def minimize( swapped_deprecated_args = True self._options["maxfun"] = self._options.pop("maxiter") - # Avoid clashing bounds defined in kwargs or _options - if 'bounds' in self._kwargs and not self.is_bounds_ignored: - bounds = self._kwargs['bounds'] - del self._kwargs['bounds'] - - if 'bounds' in self._options and not self.is_bounds_ignored: - bounds = self._options['bounds'] - del self._options['bounds'] - raw_result = minimize( fun=fun, x0=x0, From 77abbcbbeedba111cfde75df0cfea2940c9790dc Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:28:46 +0100 Subject: [PATCH 04/39] Add algorithms and optimizer specific warnings --- qiskit_algorithms/exceptions.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/qiskit_algorithms/exceptions.py b/qiskit_algorithms/exceptions.py index 7eda309e..4b7d4b6b 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,12 +10,30 @@ # 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 +import warnings 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 From 0163c53d986dbcd801f67f8e69ac3be774504eb4 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:11:57 +0100 Subject: [PATCH 05/39] Fix missed import --- qiskit_algorithms/optimizers/scipy_optimizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 85446d0a..61f38d2d 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -15,6 +15,7 @@ from collections.abc import Callable from typing import Any +import warnings import numpy as np from scipy.optimize import minimize @@ -129,8 +130,7 @@ def minimize( # Remove ignored bounds to suppress the warning of scipy.optimize.minimize if self.is_bounds_ignored and bounds is not None: warnings.warn( - (f'Optimizer method {self._method:s} does not support bounds. ' - f'Got bounds={bounds}, setting bounds=None.'), + f'Optimizer method {self._method} does not support bounds.', QiskitAlgorithmsOptimizersWarning ) bounds = None From 343ae7c56f9ca12c74d75b8476d41fd541d417cb Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:23:09 +0100 Subject: [PATCH 06/39] Apply style patches --- qiskit_algorithms/exceptions.py | 7 ++++--- .../optimizers/scipy_optimizer.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/qiskit_algorithms/exceptions.py b/qiskit_algorithms/exceptions.py index 4b7d4b6b..9d274e1a 100644 --- a/qiskit_algorithms/exceptions.py +++ b/qiskit_algorithms/exceptions.py @@ -13,7 +13,6 @@ """Exception and warnings for errors raised by Algorithms module.""" from qiskit.exceptions import QiskitError -import warnings class AlgorithmError(QiskitError): @@ -21,6 +20,7 @@ class AlgorithmError(QiskitError): pass + class QiskitAlgorithmsWarning(UserWarning): """Base class for warnings raised by Qiskit Algorithms.""" @@ -32,8 +32,9 @@ def __init__(self, *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 61f38d2d..6b8dd9eb 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -119,22 +119,22 @@ def minimize( bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: # Loop up for bounds specified in options or kwargs - if 'bounds' in self._kwargs: - bounds = self._kwargs['bounds'] - del self._kwargs['bounds'] + if "bounds" in self._kwargs: + bounds = self._kwargs["bounds"] + del self._kwargs["bounds"] - if 'bounds' in self._options: - bounds = self._options['bounds'] - del self._options['bounds'] + if "bounds" in self._options: + bounds = self._options["bounds"] + del self._options["bounds"] # Remove ignored bounds to suppress the warning of scipy.optimize.minimize if self.is_bounds_ignored and bounds is not None: warnings.warn( - f'Optimizer method {self._method} does not support bounds.', - QiskitAlgorithmsOptimizersWarning + f"Optimizer method {self._method} does not support bounds.", + QiskitAlgorithmsOptimizersWarning, ) bounds = None - + # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: jac = None From d1db2a3c92d9438eb40319f15d5c01efdc861c10 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:24:16 +0000 Subject: [PATCH 07/39] Add unit tests and introduce _bounds attribute --- .../optimizers/scipy_optimizer.py | 27 ++-- test/optimizers/test_optimizers.py | 130 +++++++++++++++++- 2 files changed, 146 insertions(+), 11 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 6b8dd9eb..2df993e5 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -78,6 +78,17 @@ 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 = None + + if "bounds" in self._kwargs: + self._bounds = self._kwargs["bounds"] + del self._kwargs["bounds"] + + if "bounds" in self._options: + self._bounds = self._options["bounds"] + del self._options["bounds"] + def get_support_level(self): """Return support level dictionary""" return { @@ -118,22 +129,18 @@ def minimize( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - # Loop up for bounds specified in options or kwargs - if "bounds" in self._kwargs: - bounds = self._kwargs["bounds"] - del self._kwargs["bounds"] - if "bounds" in self._options: - bounds = self._options["bounds"] - del self._options["bounds"] + # Overwrite the previous bounds if bounds are parsed in .minimize() + 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 bounds is not None: + if self.is_bounds_ignored and self._bounds is not None: warnings.warn( f"Optimizer method {self._method} does not support bounds.", QiskitAlgorithmsOptimizersWarning, ) - bounds = None + self._bounds = None # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: @@ -167,7 +174,7 @@ def minimize( x0=x0, method=self._method, jac=jac, - bounds=bounds, + bounds=self._bounds, options=self._options, **self._kwargs, ) diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 731bb90a..fe572659 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -49,6 +49,7 @@ SciPyOptimizer, ) from qiskit_algorithms.utils import algorithm_globals +from qiskit_algorithms.exceptions import QiskitAlgorithmsOptimizersWarning @ddt @@ -183,6 +184,133 @@ 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. + + This function tests the behavior of parsing bounds in the SciPyOptimizer class. + It checks whether the bounds are correctly allocated and raises an assertion error + if unexpected TypeErrors are encountered during parsing. + + Cases: + 1. Test parsing bounds specified in the 'options' parameter of SciPyOptimizer. + 2. Test parsing bounds specified directly in the 'bounds' parameter (as kwarg) of SciPyOptimizer. + 3. Test parsing bounds specified directly in the 'minimize' method of SciPyOptimizer. + + This function aims to ensure that bounds are correctly allocated within the object attributes + and not in the 'options' or 'kwargs' dictionaries, to avoid known parsing issues. + + Raises: + AssertionError: If unexpected TypeErrors are raised during parsing of bounds. + """ + + cases = {} + + try: + optimizer = SciPyOptimizer('SLSQP', maxiter=10, options={'bounds': [(0., 1.)]}) + + # Make sure the minimize method takes place + optimizer.minimize(lambda x: -x, 1.) + + 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._options.") + + cases['SciPyOptimizer._options'] = optimizer + + try: + optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) + optimizer.minimize(lambda x: -x, 1.) + + 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._kwargs.") + + cases['SciPyOptimizer._kwargs'] = optimizer + + try: + optimizer = SciPyOptimizer('SLSQP', maxiter=10) + optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + + 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(...).") + + cases['SciPyOptimizer.minimize'] = optimizer + + # Check that the bounds are correctly allocated in the attr and not in dicts + # This avoids the Scipy parsing issue documented in + # https://github.com/qiskit-community/qiskit-machine-learning/issues/570 + for optimizer in cases.values(): + self.assertTrue(hasattr(optimizer, '_bounds')) + self.assertFalse('bounds' in optimizer._options) + self.assertFalse('bounds' in optimizer._kwargs) + self.assertFalse(optimizer._bounds is None) # Because we set bounds to a non-empty tuple + + def test_scipy_optimizer_bounds_overwrite(self): + """ + Test bounds overwrite behavior of SciPyOptimizer. + + This function tests the behavior of SciPyOptimizer when new bounds are supplied to it + after initial bounds have been set. It verifies that the new bounds overwrite the old + ones correctly. + + Raises: + AssertionError: If the bounds overwrite behavior is incorrect. + """ + # Initialize SciPyOptimizer with initial bounds + optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) + + # Record the old bounds + old_bounds = np.asarray(list(optimizer._bounds[0])) + + # Call the minimize method with new bounds + optimizer.minimize(lambda x: -x, 1., bounds=[(2., 3.)]) + + # Record the new bounds + new_bounds = np.asarray(list(optimizer._bounds[0])) + + # Verify that the old bounds and new bounds are not identical + self.assertFalse(np.any(old_bounds == new_bounds)) + + # Verify that the new bounds match the expected values + self.assertTrue(np.all(new_bounds == np.array([2., 3.]))) + + def test_scipy_optimizer_warning(self): + """ + Test warning behavior of SciPyOptimizer with unsupported bounds. + + This function tests the warning behavior of SciPyOptimizer when using the COBYLA optimizer + with bounds supplied, which are unsupported by COBYLA. The function verifies that a specific + warning, QiskitAlgorithmsOptimizersWarning, is raised when bounds are supplied to COBYLA, + and the optimizer ignores them as expected. + + Cases: + 1. Test warning when bounds are supplied via 'options' parameter. + 2. Test warning when bounds are supplied directly as kwargs. + 3. Test warning when bounds are supplied via 'minimize' method. + + Raises: + AssertionError: If the expected warning is not raised. + """ + + # Cobyla does not support bounds and is expected to warn if bounds (then ignored) are supplied + optimizer = SciPyOptimizer('cobyla', maxiter=10, options={'bounds': [(0., 1.)]}) + + # Using lambda function to pass a function that will raise the specific warning + with self.assertWarns(QiskitAlgorithmsOptimizersWarning): + optimizer.minimize(lambda x: -x, 1.) + + # The same should happen with bounds parsed as kwargs + optimizer = SciPyOptimizer('cobyla', maxiter=10, bounds=[(0., 1.)]) + with self.assertWarns(QiskitAlgorithmsOptimizersWarning): + optimizer.minimize(lambda x: -x, 1.) + + # The same should happen with bounds parsed via minimize(...) + optimizer = SciPyOptimizer('cobyla', maxiter=10) + with self.assertWarns(QiskitAlgorithmsOptimizersWarning): + optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + # ESCH and ISRES do not do well with rosen @data( (CRS, True), @@ -346,7 +474,7 @@ def test_spsa_custom_iterators(self): def powerlaw(): n = 0 while True: - yield rate**n + yield rate ** n n += 1 def steps(): From 83df2c1a7c7a9cd8caa0661b0659471a82e52315 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 19 Feb 2024 19:00:36 +0000 Subject: [PATCH 08/39] Formatting and final patches to tests --- requirements.txt | 1 + test/optimizers/test_optimizers.py | 80 ++++++++++++++++-------------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index a9a2724a..0143579c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ qiskit>=0.44 scipy>=1.4 numpy>=1.17 +scikit-quant diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index fe572659..15cbbac6 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -150,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) @@ -204,68 +204,76 @@ def test_scipy_optimizer_parse_bounds(self): AssertionError: If unexpected TypeErrors are raised during parsing of bounds. """ - cases = {} + optimizer_instances = [] try: - optimizer = SciPyOptimizer('SLSQP', maxiter=10, options={'bounds': [(0., 1.)]}) + optimizer = SciPyOptimizer("SLSQP", options={"bounds": [(0.0, 1.0)]}) # Make sure the minimize method takes place - optimizer.minimize(lambda x: -x, 1.) + optimizer.minimize(lambda x: -x, 1.0) 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._options.") + self.fail( + "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer._options." + ) - cases['SciPyOptimizer._options'] = optimizer + optimizer_instances.append(optimizer) try: - optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) - optimizer.minimize(lambda x: -x, 1.) + optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) + optimizer.minimize(lambda x: -x, 1.0) 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._kwargs.") + self.fail( + "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer._kwargs." + ) - cases['SciPyOptimizer._kwargs'] = optimizer + optimizer_instances.append(optimizer) try: - optimizer = SciPyOptimizer('SLSQP', maxiter=10) - optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + optimizer = SciPyOptimizer("SLSQP") + optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) 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(...).") + self.fail( + "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer.minimize(...)." + ) - cases['SciPyOptimizer.minimize'] = optimizer + optimizer_instances.append(optimizer) # Check that the bounds are correctly allocated in the attr and not in dicts # This avoids the Scipy parsing issue documented in # https://github.com/qiskit-community/qiskit-machine-learning/issues/570 - for optimizer in cases.values(): - self.assertTrue(hasattr(optimizer, '_bounds')) - self.assertFalse('bounds' in optimizer._options) - self.assertFalse('bounds' in optimizer._kwargs) - self.assertFalse(optimizer._bounds is None) # Because we set bounds to a non-empty tuple + for optimizer in optimizer_instances: + self.assertTrue(hasattr(optimizer, "_bounds")) + self.assertFalse("bounds" in optimizer._options) + self.assertFalse("bounds" in optimizer._kwargs) + self.assertFalse( + optimizer._bounds is None + ) # Because we set bounds to a non-empty tuple def test_scipy_optimizer_bounds_overwrite(self): """ - Test bounds overwrite behavior of SciPyOptimizer. + Test bounds overwrite behavior of SciPyOptimizer. - This function tests the behavior of SciPyOptimizer when new bounds are supplied to it - after initial bounds have been set. It verifies that the new bounds overwrite the old - ones correctly. + This function tests the behavior of SciPyOptimizer when new bounds are supplied to it + after initial bounds have been set. It verifies that the new bounds overwrite the old + ones correctly. - Raises: - AssertionError: If the bounds overwrite behavior is incorrect. - """ + Raises: + AssertionError: If the bounds overwrite behavior is incorrect. + """ # Initialize SciPyOptimizer with initial bounds - optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) + optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) # Record the old bounds old_bounds = np.asarray(list(optimizer._bounds[0])) # Call the minimize method with new bounds - optimizer.minimize(lambda x: -x, 1., bounds=[(2., 3.)]) + optimizer.minimize(lambda x: -x, 1.0, bounds=[(2.0, 3.0)]) # Record the new bounds new_bounds = np.asarray(list(optimizer._bounds[0])) @@ -274,7 +282,7 @@ def test_scipy_optimizer_bounds_overwrite(self): self.assertFalse(np.any(old_bounds == new_bounds)) # Verify that the new bounds match the expected values - self.assertTrue(np.all(new_bounds == np.array([2., 3.]))) + self.assertTrue(np.all(new_bounds == np.array([2.0, 3.0]))) def test_scipy_optimizer_warning(self): """ @@ -295,21 +303,21 @@ def test_scipy_optimizer_warning(self): """ # Cobyla does not support bounds and is expected to warn if bounds (then ignored) are supplied - optimizer = SciPyOptimizer('cobyla', maxiter=10, options={'bounds': [(0., 1.)]}) + optimizer = SciPyOptimizer("cobyla", options={"bounds": [(0.0, 1.0)]}) # Using lambda function to pass a function that will raise the specific warning with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.) + optimizer.minimize(lambda x: -x, 1.0) # The same should happen with bounds parsed as kwargs - optimizer = SciPyOptimizer('cobyla', maxiter=10, bounds=[(0., 1.)]) + optimizer = SciPyOptimizer("cobyla", bounds=[(0.0, 1.0)]) with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.) + optimizer.minimize(lambda x: -x, 1.0) # The same should happen with bounds parsed via minimize(...) - optimizer = SciPyOptimizer('cobyla', maxiter=10) + optimizer = SciPyOptimizer("cobyla") with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) # ESCH and ISRES do not do well with rosen @data( @@ -474,7 +482,7 @@ def test_spsa_custom_iterators(self): def powerlaw(): n = 0 while True: - yield rate ** n + yield rate**n n += 1 def steps(): From 28c5f6746cb334e2e6f290a88ba00c8337fafaf4 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:42:58 +0000 Subject: [PATCH 09/39] Bounds parsing forbidden in __init__ and allowed in minimize() --- .../optimizers/scipy_optimizer.py | 24 ++- test/optimizers/test_optimizers.py | 148 ++++++------------ 2 files changed, 63 insertions(+), 109 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 2df993e5..215534a9 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -82,12 +82,15 @@ def __init__( self._bounds = None if "bounds" in self._kwargs: - self._bounds = self._kwargs["bounds"] - del self._kwargs["bounds"] - + raise RuntimeError( + "Optimizer bounds should be parsed in SciPyOptimizer.minimize() and not in " + "SciPyOptimizer.__init__()." + ) if "bounds" in self._options: - self._bounds = self._options["bounds"] - del self._options["bounds"] + 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""" @@ -130,17 +133,24 @@ def minimize( bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - # Overwrite the previous bounds if bounds are parsed in .minimize() + # 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.", + 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: diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 15cbbac6..ea9cdca1 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -186,55 +186,34 @@ def callback(x): def test_scipy_optimizer_parse_bounds(self): """ - Test the parsing of bounds in SciPyOptimizer. - - This function tests the behavior of parsing bounds in the SciPyOptimizer class. - It checks whether the bounds are correctly allocated and raises an assertion error - if unexpected TypeErrors are encountered during parsing. - - Cases: - 1. Test parsing bounds specified in the 'options' parameter of SciPyOptimizer. - 2. Test parsing bounds specified directly in the 'bounds' parameter (as kwarg) of SciPyOptimizer. - 3. Test parsing bounds specified directly in the 'minimize' method of SciPyOptimizer. - - This function aims to ensure that bounds are correctly allocated within the object attributes - and not in the 'options' or 'kwargs' dictionaries, to avoid known parsing issues. + 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 unexpected TypeErrors are raised during parsing of bounds. - """ - - optimizer_instances = [] + AssertionError: If any of the assertions fail. + AssertionError: If a TypeError is raised unexpectedly while parsing bounds. + """ try: - optimizer = SciPyOptimizer("SLSQP", options={"bounds": [(0.0, 1.0)]}) - - # Make sure the minimize method takes place - optimizer.minimize(lambda x: -x, 1.0) - - 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._options." - ) + # Initialize SciPyOptimizer instance with SLSQP method + optimizer = SciPyOptimizer("SLSQP") - optimizer_instances.append(optimizer) + # Ensure that _bounds attribute is initially present and =None + self.assertTrue(hasattr(optimizer, "_bounds")) + self.assertTrue(optimizer._bounds is None) - try: - optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) - optimizer.minimize(lambda x: -x, 1.0) + # Call minimize method with a simple lambda function and bounds + optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) - 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._kwargs." - ) + # Assert that _bounds attribute is set after minimize method call + self.assertTrue(hasattr(optimizer, "_bounds")) - optimizer_instances.append(optimizer) + # Assert that "bounds" is not present in optimizer options and kwargs + self.assertFalse("bounds" in optimizer._options) + self.assertFalse("bounds" in optimizer._kwargs) - try: - optimizer = SciPyOptimizer("SLSQP") - optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) + # 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 @@ -242,83 +221,48 @@ def test_scipy_optimizer_parse_bounds(self): "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer.minimize(...)." ) - optimizer_instances.append(optimizer) + # 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)]}) - # Check that the bounds are correctly allocated in the attr and not in dicts - # This avoids the Scipy parsing issue documented in - # https://github.com/qiskit-community/qiskit-machine-learning/issues/570 - for optimizer in optimizer_instances: - self.assertTrue(hasattr(optimizer, "_bounds")) - self.assertFalse("bounds" in optimizer._options) - self.assertFalse("bounds" in optimizer._kwargs) - self.assertFalse( - optimizer._bounds is None - ) # Because we set bounds to a non-empty tuple - - def test_scipy_optimizer_bounds_overwrite(self): + def test_scipy_optimizer_warning(self): """ - Test bounds overwrite behavior of SciPyOptimizer. - - This function tests the behavior of SciPyOptimizer when new bounds are supplied to it - after initial bounds have been set. It verifies that the new bounds overwrite the old - ones correctly. + 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 bounds overwrite behavior is incorrect. - """ - # Initialize SciPyOptimizer with initial bounds - optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) - - # Record the old bounds - old_bounds = np.asarray(list(optimizer._bounds[0])) - - # Call the minimize method with new bounds - optimizer.minimize(lambda x: -x, 1.0, bounds=[(2.0, 3.0)]) - - # Record the new bounds - new_bounds = np.asarray(list(optimizer._bounds[0])) - - # Verify that the old bounds and new bounds are not identical - self.assertFalse(np.any(old_bounds == new_bounds)) - - # Verify that the new bounds match the expected values - self.assertTrue(np.all(new_bounds == np.array([2.0, 3.0]))) + AssertionError: If the expected warning is not raised. - def test_scipy_optimizer_warning(self): """ - Test warning behavior of SciPyOptimizer with unsupported bounds. - - This function tests the warning behavior of SciPyOptimizer when using the COBYLA optimizer - with bounds supplied, which are unsupported by COBYLA. The function verifies that a specific - warning, QiskitAlgorithmsOptimizersWarning, is raised when bounds are supplied to COBYLA, - and the optimizer ignores them as expected. + # Initialize SciPyOptimizer instance with "cobyla" method + optimizer = SciPyOptimizer("cobyla") - Cases: - 1. Test warning when bounds are supplied via 'options' parameter. - 2. Test warning when bounds are supplied directly as kwargs. - 3. Test warning when bounds are supplied via 'minimize' method. + # 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)]) - Raises: - AssertionError: If the expected warning is not raised. + 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. - # Cobyla does not support bounds and is expected to warn if bounds (then ignored) are supplied - optimizer = SciPyOptimizer("cobyla", options={"bounds": [(0.0, 1.0)]}) + Raises: + AssertionError: If the expected exception is not raised. - # Using lambda function to pass a function that will raise the specific warning - with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.0) + """ + # Initialize SciPyOptimizer instance with "SLSQP" method + optimizer = SciPyOptimizer("SLSQP") - # The same should happen with bounds parsed as kwargs - optimizer = SciPyOptimizer("cobyla", bounds=[(0.0, 1.0)]) + # 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) - # The same should happen with bounds parsed via minimize(...) - optimizer = SciPyOptimizer("cobyla") - with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) - # ESCH and ISRES do not do well with rosen @data( (CRS, True), From 6d057ec704c3270fa79fbfd5215c339df811182d Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:14:33 +0000 Subject: [PATCH 10/39] Add bounds type hinting --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 215534a9..69ff5384 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -79,7 +79,7 @@ def __init__( self._kwargs = kwargs # Initialise bounds and re-allocate if definition in options or kwargs - self._bounds = None + self._bounds: list[tuple[float, float]] | None = None if "bounds" in self._kwargs: raise RuntimeError( From cdfc487af4e1b9b17715466c7687296cab6f2bb2 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:16:21 +0000 Subject: [PATCH 11/39] Change copyright in tests --- test/optimizers/test_optimizers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index ea9cdca1..8fb9151e 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 From 7c0bf6bdaa238e06d96188d5579629ae933d35b5 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 18:59:01 +0000 Subject: [PATCH 12/39] Remove scikit-quant dependency --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0143579c..a9a2724a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ qiskit>=0.44 scipy>=1.4 numpy>=1.17 -scikit-quant From a4f45ee2638371e2ee58731ddc87d95dd70f7cd4 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 19:01:38 +0000 Subject: [PATCH 13/39] Update qiskit_algorithms/optimizers/scipy_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 69ff5384..d1941a6b 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -78,7 +78,7 @@ def __init__( self._max_evals_grouped = max_evals_grouped self._kwargs = kwargs - # Initialise bounds and re-allocate if definition in options or kwargs + # Initialize bounds and re-allocate if definition in options or kwargs self._bounds: list[tuple[float, float]] | None = None if "bounds" in self._kwargs: From 18b0f77756b23cafb4c0ea5bc3a616c7f9d0b951 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:33:04 +0000 Subject: [PATCH 14/39] Update qiskit_algorithms/optimizers/scipy_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_algorithms/optimizers/scipy_optimizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index d1941a6b..2af01040 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -83,8 +83,8 @@ def __init__( if "bounds" in self._kwargs: raise RuntimeError( - "Optimizer bounds should be parsed in SciPyOptimizer.minimize() and not in " - "SciPyOptimizer.__init__()." + "Optimizer bounds should be passed to SciPyOptimizer.minimize() and is not " + "supported in SciPyOptimizer constructor kwargs." ) if "bounds" in self._options: raise RuntimeError( From 140fce64032bc245819722ce67288de729e6eef5 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:34:32 +0000 Subject: [PATCH 15/39] Update qiskit_algorithms/optimizers/scipy_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 2af01040..8df1ed45 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -88,7 +88,7 @@ def __init__( ) if "bounds" in self._options: raise RuntimeError( - "Optimizer bounds should be parsed in SciPyOptimizer.minimize() as a kwarg and not as " + "Optimizer bounds should be passed to SciPyOptimizer.minimize() and is not as " "options." ) From 2fbd23a391181ce158ca3c86e18d5578af5d5e94 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:45:52 +0000 Subject: [PATCH 16/39] Remove internal implementation checks in test_optimizers.py --- test/optimizers/test_optimizers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 8fb9151e..989d3626 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -198,10 +198,6 @@ def test_scipy_optimizer_parse_bounds(self): # 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)]) From 30c4b7236500d02c6875edb29fda40af94c21a00 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:49:33 +0000 Subject: [PATCH 17/39] Avoid multiply defined bounds options in optimizer --- qiskit_algorithms/optimizers/scipy_optimizer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 230822cf..b016f135 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -145,6 +145,15 @@ def minimize( swapped_deprecated_args = True self._options["maxfun"] = self._options.pop("maxiter") + # Avoid clashing bounds defined in kwargs or _options + if 'bounds' in self._kwargs and not self.is_bounds_ignored: + bounds = self._kwargs['bounds'] + del self._kwargs['bounds'] + + if 'bounds' in self._options and not self.is_bounds_ignored: + bounds = self._options['bounds'] + del self._options['bounds'] + raw_result = minimize( fun=fun, x0=x0, From 06240b838c88d307f571ff1a0ba226f5eb180afb Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:30:47 +0000 Subject: [PATCH 18/39] Update change date --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index b016f135..199a9ba5 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 From be6dd75376cdb5be438e36723b4c7aaa58f338aa Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:27:48 +0100 Subject: [PATCH 19/39] Add warnings for parsing unsupported bounds --- .../optimizers/scipy_optimizer.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 199a9ba5..85446d0a 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -19,6 +19,7 @@ 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 @@ -116,9 +117,25 @@ 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: + # Loop up for bounds specified in options or kwargs + if 'bounds' in self._kwargs: + bounds = self._kwargs['bounds'] + del self._kwargs['bounds'] + + if 'bounds' in self._options: + bounds = self._options['bounds'] + del self._options['bounds'] + + # Remove ignored bounds to suppress the warning of scipy.optimize.minimize + if self.is_bounds_ignored and bounds is not None: + warnings.warn( + (f'Optimizer method {self._method:s} does not support bounds. ' + f'Got bounds={bounds}, setting bounds=None.'), + QiskitAlgorithmsOptimizersWarning + ) bounds = None + + # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: jac = None @@ -145,15 +162,6 @@ def minimize( swapped_deprecated_args = True self._options["maxfun"] = self._options.pop("maxiter") - # Avoid clashing bounds defined in kwargs or _options - if 'bounds' in self._kwargs and not self.is_bounds_ignored: - bounds = self._kwargs['bounds'] - del self._kwargs['bounds'] - - if 'bounds' in self._options and not self.is_bounds_ignored: - bounds = self._options['bounds'] - del self._options['bounds'] - raw_result = minimize( fun=fun, x0=x0, From 28d1493f63ddcb85a71d0d05d7b2b485498588cd Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:28:46 +0100 Subject: [PATCH 20/39] Add algorithms and optimizer specific warnings --- qiskit_algorithms/exceptions.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/qiskit_algorithms/exceptions.py b/qiskit_algorithms/exceptions.py index 7eda309e..4b7d4b6b 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,12 +10,30 @@ # 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 +import warnings 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 From 8446806b2fc7ea154e629713b2bc947b2c143108 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:11:57 +0100 Subject: [PATCH 21/39] Fix missed import --- qiskit_algorithms/optimizers/scipy_optimizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 85446d0a..61f38d2d 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -15,6 +15,7 @@ from collections.abc import Callable from typing import Any +import warnings import numpy as np from scipy.optimize import minimize @@ -129,8 +130,7 @@ def minimize( # Remove ignored bounds to suppress the warning of scipy.optimize.minimize if self.is_bounds_ignored and bounds is not None: warnings.warn( - (f'Optimizer method {self._method:s} does not support bounds. ' - f'Got bounds={bounds}, setting bounds=None.'), + f'Optimizer method {self._method} does not support bounds.', QiskitAlgorithmsOptimizersWarning ) bounds = None From f47d31a6d7de8d89b8041cc3c18b8d742346bc5a Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:23:09 +0100 Subject: [PATCH 22/39] Apply style patches --- qiskit_algorithms/exceptions.py | 7 ++++--- .../optimizers/scipy_optimizer.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/qiskit_algorithms/exceptions.py b/qiskit_algorithms/exceptions.py index 4b7d4b6b..9d274e1a 100644 --- a/qiskit_algorithms/exceptions.py +++ b/qiskit_algorithms/exceptions.py @@ -13,7 +13,6 @@ """Exception and warnings for errors raised by Algorithms module.""" from qiskit.exceptions import QiskitError -import warnings class AlgorithmError(QiskitError): @@ -21,6 +20,7 @@ class AlgorithmError(QiskitError): pass + class QiskitAlgorithmsWarning(UserWarning): """Base class for warnings raised by Qiskit Algorithms.""" @@ -32,8 +32,9 @@ def __init__(self, *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 61f38d2d..6b8dd9eb 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -119,22 +119,22 @@ def minimize( bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: # Loop up for bounds specified in options or kwargs - if 'bounds' in self._kwargs: - bounds = self._kwargs['bounds'] - del self._kwargs['bounds'] + if "bounds" in self._kwargs: + bounds = self._kwargs["bounds"] + del self._kwargs["bounds"] - if 'bounds' in self._options: - bounds = self._options['bounds'] - del self._options['bounds'] + if "bounds" in self._options: + bounds = self._options["bounds"] + del self._options["bounds"] # Remove ignored bounds to suppress the warning of scipy.optimize.minimize if self.is_bounds_ignored and bounds is not None: warnings.warn( - f'Optimizer method {self._method} does not support bounds.', - QiskitAlgorithmsOptimizersWarning + f"Optimizer method {self._method} does not support bounds.", + QiskitAlgorithmsOptimizersWarning, ) bounds = None - + # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: jac = None From d7a09863723edaf0bac89cd48a7eaa9f38fb06c9 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:24:16 +0000 Subject: [PATCH 23/39] Add unit tests and introduce _bounds attribute --- .../optimizers/scipy_optimizer.py | 27 ++-- test/optimizers/test_optimizers.py | 130 +++++++++++++++++- 2 files changed, 146 insertions(+), 11 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 6b8dd9eb..2df993e5 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -78,6 +78,17 @@ 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 = None + + if "bounds" in self._kwargs: + self._bounds = self._kwargs["bounds"] + del self._kwargs["bounds"] + + if "bounds" in self._options: + self._bounds = self._options["bounds"] + del self._options["bounds"] + def get_support_level(self): """Return support level dictionary""" return { @@ -118,22 +129,18 @@ def minimize( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - # Loop up for bounds specified in options or kwargs - if "bounds" in self._kwargs: - bounds = self._kwargs["bounds"] - del self._kwargs["bounds"] - if "bounds" in self._options: - bounds = self._options["bounds"] - del self._options["bounds"] + # Overwrite the previous bounds if bounds are parsed in .minimize() + 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 bounds is not None: + if self.is_bounds_ignored and self._bounds is not None: warnings.warn( f"Optimizer method {self._method} does not support bounds.", QiskitAlgorithmsOptimizersWarning, ) - bounds = None + self._bounds = None # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: @@ -167,7 +174,7 @@ def minimize( x0=x0, method=self._method, jac=jac, - bounds=bounds, + bounds=self._bounds, options=self._options, **self._kwargs, ) diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 731bb90a..fe572659 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -49,6 +49,7 @@ SciPyOptimizer, ) from qiskit_algorithms.utils import algorithm_globals +from qiskit_algorithms.exceptions import QiskitAlgorithmsOptimizersWarning @ddt @@ -183,6 +184,133 @@ 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. + + This function tests the behavior of parsing bounds in the SciPyOptimizer class. + It checks whether the bounds are correctly allocated and raises an assertion error + if unexpected TypeErrors are encountered during parsing. + + Cases: + 1. Test parsing bounds specified in the 'options' parameter of SciPyOptimizer. + 2. Test parsing bounds specified directly in the 'bounds' parameter (as kwarg) of SciPyOptimizer. + 3. Test parsing bounds specified directly in the 'minimize' method of SciPyOptimizer. + + This function aims to ensure that bounds are correctly allocated within the object attributes + and not in the 'options' or 'kwargs' dictionaries, to avoid known parsing issues. + + Raises: + AssertionError: If unexpected TypeErrors are raised during parsing of bounds. + """ + + cases = {} + + try: + optimizer = SciPyOptimizer('SLSQP', maxiter=10, options={'bounds': [(0., 1.)]}) + + # Make sure the minimize method takes place + optimizer.minimize(lambda x: -x, 1.) + + 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._options.") + + cases['SciPyOptimizer._options'] = optimizer + + try: + optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) + optimizer.minimize(lambda x: -x, 1.) + + 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._kwargs.") + + cases['SciPyOptimizer._kwargs'] = optimizer + + try: + optimizer = SciPyOptimizer('SLSQP', maxiter=10) + optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + + 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(...).") + + cases['SciPyOptimizer.minimize'] = optimizer + + # Check that the bounds are correctly allocated in the attr and not in dicts + # This avoids the Scipy parsing issue documented in + # https://github.com/qiskit-community/qiskit-machine-learning/issues/570 + for optimizer in cases.values(): + self.assertTrue(hasattr(optimizer, '_bounds')) + self.assertFalse('bounds' in optimizer._options) + self.assertFalse('bounds' in optimizer._kwargs) + self.assertFalse(optimizer._bounds is None) # Because we set bounds to a non-empty tuple + + def test_scipy_optimizer_bounds_overwrite(self): + """ + Test bounds overwrite behavior of SciPyOptimizer. + + This function tests the behavior of SciPyOptimizer when new bounds are supplied to it + after initial bounds have been set. It verifies that the new bounds overwrite the old + ones correctly. + + Raises: + AssertionError: If the bounds overwrite behavior is incorrect. + """ + # Initialize SciPyOptimizer with initial bounds + optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) + + # Record the old bounds + old_bounds = np.asarray(list(optimizer._bounds[0])) + + # Call the minimize method with new bounds + optimizer.minimize(lambda x: -x, 1., bounds=[(2., 3.)]) + + # Record the new bounds + new_bounds = np.asarray(list(optimizer._bounds[0])) + + # Verify that the old bounds and new bounds are not identical + self.assertFalse(np.any(old_bounds == new_bounds)) + + # Verify that the new bounds match the expected values + self.assertTrue(np.all(new_bounds == np.array([2., 3.]))) + + def test_scipy_optimizer_warning(self): + """ + Test warning behavior of SciPyOptimizer with unsupported bounds. + + This function tests the warning behavior of SciPyOptimizer when using the COBYLA optimizer + with bounds supplied, which are unsupported by COBYLA. The function verifies that a specific + warning, QiskitAlgorithmsOptimizersWarning, is raised when bounds are supplied to COBYLA, + and the optimizer ignores them as expected. + + Cases: + 1. Test warning when bounds are supplied via 'options' parameter. + 2. Test warning when bounds are supplied directly as kwargs. + 3. Test warning when bounds are supplied via 'minimize' method. + + Raises: + AssertionError: If the expected warning is not raised. + """ + + # Cobyla does not support bounds and is expected to warn if bounds (then ignored) are supplied + optimizer = SciPyOptimizer('cobyla', maxiter=10, options={'bounds': [(0., 1.)]}) + + # Using lambda function to pass a function that will raise the specific warning + with self.assertWarns(QiskitAlgorithmsOptimizersWarning): + optimizer.minimize(lambda x: -x, 1.) + + # The same should happen with bounds parsed as kwargs + optimizer = SciPyOptimizer('cobyla', maxiter=10, bounds=[(0., 1.)]) + with self.assertWarns(QiskitAlgorithmsOptimizersWarning): + optimizer.minimize(lambda x: -x, 1.) + + # The same should happen with bounds parsed via minimize(...) + optimizer = SciPyOptimizer('cobyla', maxiter=10) + with self.assertWarns(QiskitAlgorithmsOptimizersWarning): + optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + # ESCH and ISRES do not do well with rosen @data( (CRS, True), @@ -346,7 +474,7 @@ def test_spsa_custom_iterators(self): def powerlaw(): n = 0 while True: - yield rate**n + yield rate ** n n += 1 def steps(): From adfb264970f863c7216123cf4ada55ab19857321 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 19 Feb 2024 19:00:36 +0000 Subject: [PATCH 24/39] Formatting and final patches to tests --- requirements.txt | 1 + test/optimizers/test_optimizers.py | 80 ++++++++++++++++-------------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index a9a2724a..0143579c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ qiskit>=0.44 scipy>=1.4 numpy>=1.17 +scikit-quant diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index fe572659..15cbbac6 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -150,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) @@ -204,68 +204,76 @@ def test_scipy_optimizer_parse_bounds(self): AssertionError: If unexpected TypeErrors are raised during parsing of bounds. """ - cases = {} + optimizer_instances = [] try: - optimizer = SciPyOptimizer('SLSQP', maxiter=10, options={'bounds': [(0., 1.)]}) + optimizer = SciPyOptimizer("SLSQP", options={"bounds": [(0.0, 1.0)]}) # Make sure the minimize method takes place - optimizer.minimize(lambda x: -x, 1.) + optimizer.minimize(lambda x: -x, 1.0) 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._options.") + self.fail( + "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer._options." + ) - cases['SciPyOptimizer._options'] = optimizer + optimizer_instances.append(optimizer) try: - optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) - optimizer.minimize(lambda x: -x, 1.) + optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) + optimizer.minimize(lambda x: -x, 1.0) 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._kwargs.") + self.fail( + "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer._kwargs." + ) - cases['SciPyOptimizer._kwargs'] = optimizer + optimizer_instances.append(optimizer) try: - optimizer = SciPyOptimizer('SLSQP', maxiter=10) - optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + optimizer = SciPyOptimizer("SLSQP") + optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) 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(...).") + self.fail( + "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer.minimize(...)." + ) - cases['SciPyOptimizer.minimize'] = optimizer + optimizer_instances.append(optimizer) # Check that the bounds are correctly allocated in the attr and not in dicts # This avoids the Scipy parsing issue documented in # https://github.com/qiskit-community/qiskit-machine-learning/issues/570 - for optimizer in cases.values(): - self.assertTrue(hasattr(optimizer, '_bounds')) - self.assertFalse('bounds' in optimizer._options) - self.assertFalse('bounds' in optimizer._kwargs) - self.assertFalse(optimizer._bounds is None) # Because we set bounds to a non-empty tuple + for optimizer in optimizer_instances: + self.assertTrue(hasattr(optimizer, "_bounds")) + self.assertFalse("bounds" in optimizer._options) + self.assertFalse("bounds" in optimizer._kwargs) + self.assertFalse( + optimizer._bounds is None + ) # Because we set bounds to a non-empty tuple def test_scipy_optimizer_bounds_overwrite(self): """ - Test bounds overwrite behavior of SciPyOptimizer. + Test bounds overwrite behavior of SciPyOptimizer. - This function tests the behavior of SciPyOptimizer when new bounds are supplied to it - after initial bounds have been set. It verifies that the new bounds overwrite the old - ones correctly. + This function tests the behavior of SciPyOptimizer when new bounds are supplied to it + after initial bounds have been set. It verifies that the new bounds overwrite the old + ones correctly. - Raises: - AssertionError: If the bounds overwrite behavior is incorrect. - """ + Raises: + AssertionError: If the bounds overwrite behavior is incorrect. + """ # Initialize SciPyOptimizer with initial bounds - optimizer = SciPyOptimizer('SLSQP', maxiter=10, bounds=[(0., 1.)]) + optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) # Record the old bounds old_bounds = np.asarray(list(optimizer._bounds[0])) # Call the minimize method with new bounds - optimizer.minimize(lambda x: -x, 1., bounds=[(2., 3.)]) + optimizer.minimize(lambda x: -x, 1.0, bounds=[(2.0, 3.0)]) # Record the new bounds new_bounds = np.asarray(list(optimizer._bounds[0])) @@ -274,7 +282,7 @@ def test_scipy_optimizer_bounds_overwrite(self): self.assertFalse(np.any(old_bounds == new_bounds)) # Verify that the new bounds match the expected values - self.assertTrue(np.all(new_bounds == np.array([2., 3.]))) + self.assertTrue(np.all(new_bounds == np.array([2.0, 3.0]))) def test_scipy_optimizer_warning(self): """ @@ -295,21 +303,21 @@ def test_scipy_optimizer_warning(self): """ # Cobyla does not support bounds and is expected to warn if bounds (then ignored) are supplied - optimizer = SciPyOptimizer('cobyla', maxiter=10, options={'bounds': [(0., 1.)]}) + optimizer = SciPyOptimizer("cobyla", options={"bounds": [(0.0, 1.0)]}) # Using lambda function to pass a function that will raise the specific warning with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.) + optimizer.minimize(lambda x: -x, 1.0) # The same should happen with bounds parsed as kwargs - optimizer = SciPyOptimizer('cobyla', maxiter=10, bounds=[(0., 1.)]) + optimizer = SciPyOptimizer("cobyla", bounds=[(0.0, 1.0)]) with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.) + optimizer.minimize(lambda x: -x, 1.0) # The same should happen with bounds parsed via minimize(...) - optimizer = SciPyOptimizer('cobyla', maxiter=10) + optimizer = SciPyOptimizer("cobyla") with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1., bounds=[(0., 1.)]) + optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) # ESCH and ISRES do not do well with rosen @data( @@ -474,7 +482,7 @@ def test_spsa_custom_iterators(self): def powerlaw(): n = 0 while True: - yield rate ** n + yield rate**n n += 1 def steps(): From cd7a34e5aca84e36e102132c4b50f52fe2fa681e Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:42:58 +0000 Subject: [PATCH 25/39] Bounds parsing forbidden in __init__ and allowed in minimize() --- .../optimizers/scipy_optimizer.py | 24 ++- test/optimizers/test_optimizers.py | 148 ++++++------------ 2 files changed, 63 insertions(+), 109 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 2df993e5..215534a9 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -82,12 +82,15 @@ def __init__( self._bounds = None if "bounds" in self._kwargs: - self._bounds = self._kwargs["bounds"] - del self._kwargs["bounds"] - + raise RuntimeError( + "Optimizer bounds should be parsed in SciPyOptimizer.minimize() and not in " + "SciPyOptimizer.__init__()." + ) if "bounds" in self._options: - self._bounds = self._options["bounds"] - del self._options["bounds"] + 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""" @@ -130,17 +133,24 @@ def minimize( bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - # Overwrite the previous bounds if bounds are parsed in .minimize() + # 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.", + 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: diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 15cbbac6..ea9cdca1 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -186,55 +186,34 @@ def callback(x): def test_scipy_optimizer_parse_bounds(self): """ - Test the parsing of bounds in SciPyOptimizer. - - This function tests the behavior of parsing bounds in the SciPyOptimizer class. - It checks whether the bounds are correctly allocated and raises an assertion error - if unexpected TypeErrors are encountered during parsing. - - Cases: - 1. Test parsing bounds specified in the 'options' parameter of SciPyOptimizer. - 2. Test parsing bounds specified directly in the 'bounds' parameter (as kwarg) of SciPyOptimizer. - 3. Test parsing bounds specified directly in the 'minimize' method of SciPyOptimizer. - - This function aims to ensure that bounds are correctly allocated within the object attributes - and not in the 'options' or 'kwargs' dictionaries, to avoid known parsing issues. + 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 unexpected TypeErrors are raised during parsing of bounds. - """ - - optimizer_instances = [] + AssertionError: If any of the assertions fail. + AssertionError: If a TypeError is raised unexpectedly while parsing bounds. + """ try: - optimizer = SciPyOptimizer("SLSQP", options={"bounds": [(0.0, 1.0)]}) - - # Make sure the minimize method takes place - optimizer.minimize(lambda x: -x, 1.0) - - 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._options." - ) + # Initialize SciPyOptimizer instance with SLSQP method + optimizer = SciPyOptimizer("SLSQP") - optimizer_instances.append(optimizer) + # Ensure that _bounds attribute is initially present and =None + self.assertTrue(hasattr(optimizer, "_bounds")) + self.assertTrue(optimizer._bounds is None) - try: - optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) - optimizer.minimize(lambda x: -x, 1.0) + # Call minimize method with a simple lambda function and bounds + optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) - 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._kwargs." - ) + # Assert that _bounds attribute is set after minimize method call + self.assertTrue(hasattr(optimizer, "_bounds")) - optimizer_instances.append(optimizer) + # Assert that "bounds" is not present in optimizer options and kwargs + self.assertFalse("bounds" in optimizer._options) + self.assertFalse("bounds" in optimizer._kwargs) - try: - optimizer = SciPyOptimizer("SLSQP") - optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) + # 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 @@ -242,83 +221,48 @@ def test_scipy_optimizer_parse_bounds(self): "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer.minimize(...)." ) - optimizer_instances.append(optimizer) + # 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)]}) - # Check that the bounds are correctly allocated in the attr and not in dicts - # This avoids the Scipy parsing issue documented in - # https://github.com/qiskit-community/qiskit-machine-learning/issues/570 - for optimizer in optimizer_instances: - self.assertTrue(hasattr(optimizer, "_bounds")) - self.assertFalse("bounds" in optimizer._options) - self.assertFalse("bounds" in optimizer._kwargs) - self.assertFalse( - optimizer._bounds is None - ) # Because we set bounds to a non-empty tuple - - def test_scipy_optimizer_bounds_overwrite(self): + def test_scipy_optimizer_warning(self): """ - Test bounds overwrite behavior of SciPyOptimizer. - - This function tests the behavior of SciPyOptimizer when new bounds are supplied to it - after initial bounds have been set. It verifies that the new bounds overwrite the old - ones correctly. + 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 bounds overwrite behavior is incorrect. - """ - # Initialize SciPyOptimizer with initial bounds - optimizer = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) - - # Record the old bounds - old_bounds = np.asarray(list(optimizer._bounds[0])) - - # Call the minimize method with new bounds - optimizer.minimize(lambda x: -x, 1.0, bounds=[(2.0, 3.0)]) - - # Record the new bounds - new_bounds = np.asarray(list(optimizer._bounds[0])) - - # Verify that the old bounds and new bounds are not identical - self.assertFalse(np.any(old_bounds == new_bounds)) - - # Verify that the new bounds match the expected values - self.assertTrue(np.all(new_bounds == np.array([2.0, 3.0]))) + AssertionError: If the expected warning is not raised. - def test_scipy_optimizer_warning(self): """ - Test warning behavior of SciPyOptimizer with unsupported bounds. - - This function tests the warning behavior of SciPyOptimizer when using the COBYLA optimizer - with bounds supplied, which are unsupported by COBYLA. The function verifies that a specific - warning, QiskitAlgorithmsOptimizersWarning, is raised when bounds are supplied to COBYLA, - and the optimizer ignores them as expected. + # Initialize SciPyOptimizer instance with "cobyla" method + optimizer = SciPyOptimizer("cobyla") - Cases: - 1. Test warning when bounds are supplied via 'options' parameter. - 2. Test warning when bounds are supplied directly as kwargs. - 3. Test warning when bounds are supplied via 'minimize' method. + # 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)]) - Raises: - AssertionError: If the expected warning is not raised. + 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. - # Cobyla does not support bounds and is expected to warn if bounds (then ignored) are supplied - optimizer = SciPyOptimizer("cobyla", options={"bounds": [(0.0, 1.0)]}) + Raises: + AssertionError: If the expected exception is not raised. - # Using lambda function to pass a function that will raise the specific warning - with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.0) + """ + # Initialize SciPyOptimizer instance with "SLSQP" method + optimizer = SciPyOptimizer("SLSQP") - # The same should happen with bounds parsed as kwargs - optimizer = SciPyOptimizer("cobyla", bounds=[(0.0, 1.0)]) + # 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) - # The same should happen with bounds parsed via minimize(...) - optimizer = SciPyOptimizer("cobyla") - with self.assertWarns(QiskitAlgorithmsOptimizersWarning): - optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) - # ESCH and ISRES do not do well with rosen @data( (CRS, True), From 960cd7cdea781fd66e79300c974731ab0e6a74fa Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:14:33 +0000 Subject: [PATCH 26/39] Add bounds type hinting --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 215534a9..69ff5384 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -79,7 +79,7 @@ def __init__( self._kwargs = kwargs # Initialise bounds and re-allocate if definition in options or kwargs - self._bounds = None + self._bounds: list[tuple[float, float]] | None = None if "bounds" in self._kwargs: raise RuntimeError( From 91ffeff26962262a7fec71bef20aae18334e4c23 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:16:21 +0000 Subject: [PATCH 27/39] Change copyright in tests --- test/optimizers/test_optimizers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index ea9cdca1..8fb9151e 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 From ca0d277bc8ddef15f5b815c0bb9b49b53d0836e5 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 18:59:01 +0000 Subject: [PATCH 28/39] Remove scikit-quant dependency --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0143579c..a9a2724a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ qiskit>=0.44 scipy>=1.4 numpy>=1.17 -scikit-quant From a7bbd21f25321bc87162b6aa17f0031a4401faa6 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Sat, 24 Feb 2024 19:01:38 +0000 Subject: [PATCH 29/39] Update qiskit_algorithms/optimizers/scipy_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 69ff5384..d1941a6b 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -78,7 +78,7 @@ def __init__( self._max_evals_grouped = max_evals_grouped self._kwargs = kwargs - # Initialise bounds and re-allocate if definition in options or kwargs + # Initialize bounds and re-allocate if definition in options or kwargs self._bounds: list[tuple[float, float]] | None = None if "bounds" in self._kwargs: From 3611957ec64bc9b415665de46c52c2b870951054 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:33:04 +0000 Subject: [PATCH 30/39] Update qiskit_algorithms/optimizers/scipy_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_algorithms/optimizers/scipy_optimizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index d1941a6b..2af01040 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -83,8 +83,8 @@ def __init__( if "bounds" in self._kwargs: raise RuntimeError( - "Optimizer bounds should be parsed in SciPyOptimizer.minimize() and not in " - "SciPyOptimizer.__init__()." + "Optimizer bounds should be passed to SciPyOptimizer.minimize() and is not " + "supported in SciPyOptimizer constructor kwargs." ) if "bounds" in self._options: raise RuntimeError( From 08284c9bcf62746c3613ba5702b2b4de35bf9d40 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:34:32 +0000 Subject: [PATCH 31/39] Update qiskit_algorithms/optimizers/scipy_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 2af01040..8df1ed45 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -88,7 +88,7 @@ def __init__( ) if "bounds" in self._options: raise RuntimeError( - "Optimizer bounds should be parsed in SciPyOptimizer.minimize() as a kwarg and not as " + "Optimizer bounds should be passed to SciPyOptimizer.minimize() and is not as " "options." ) From b9a32dc24c9382b8ffd047aba0622f1b29cced63 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:45:52 +0000 Subject: [PATCH 32/39] Remove internal implementation checks in test_optimizers.py --- test/optimizers/test_optimizers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 8fb9151e..989d3626 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -198,10 +198,6 @@ def test_scipy_optimizer_parse_bounds(self): # 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)]) From 7a5fc0c64bd221476fc7154407a398ec350117d3 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 29 Feb 2024 19:48:41 +0000 Subject: [PATCH 33/39] Remove loose incompatible-bounds logic --- .../optimizers/scipy_optimizer.py | 17 +-------- test/optimizers/test_optimizers.py | 37 ------------------- 2 files changed, 2 insertions(+), 52 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 8df1ed45..629b33b6 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -15,12 +15,10 @@ 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 @@ -133,24 +131,13 @@ def minimize( bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - # Overwrite the previous bounds + # Allocate 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, - ) + if self.is_bounds_ignored: 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: diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 989d3626..7512f8cd 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -49,7 +49,6 @@ SciPyOptimizer, ) from qiskit_algorithms.utils import algorithm_globals -from qiskit_algorithms.exceptions import QiskitAlgorithmsOptimizersWarning @ddt @@ -223,42 +222,6 @@ def test_scipy_optimizer_parse_bounds(self): 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), From a467036cf9b8c3f6e658d3d2cd4c2a27c81c1c09 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 29 Feb 2024 19:52:55 +0000 Subject: [PATCH 34/39] Remove loose incompatible-bounds logic --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index 629b33b6..a2110fe3 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -131,7 +131,7 @@ def minimize( bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - # Allocate bounds + # Allocate bounds attribute if bounds is not None: self._bounds = bounds From a8d52e075d9e40220b6b9d7e8f23befd6cb9adc0 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:08:47 +0100 Subject: [PATCH 35/39] Update qiskit_algorithms/optimizers/scipy_optimizer.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_algorithms/optimizers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index a2110fe3..f1a17b40 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -86,7 +86,7 @@ def __init__( ) if "bounds" in self._options: raise RuntimeError( - "Optimizer bounds should be passed to SciPyOptimizer.minimize() and is not as " + "Optimizer bounds should be passed to SciPyOptimizer.minimize() and not as " "options." ) From 0f602e736c726cebaea7bd6bf34e4b00c32b2c32 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 7 Mar 2024 23:04:23 +0100 Subject: [PATCH 36/39] Remove redundant `_bounds` attributes --- qiskit_algorithms/optimizers/scipy_optimizer.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/qiskit_algorithms/optimizers/scipy_optimizer.py b/qiskit_algorithms/optimizers/scipy_optimizer.py index f1a17b40..f8cbaf84 100644 --- a/qiskit_algorithms/optimizers/scipy_optimizer.py +++ b/qiskit_algorithms/optimizers/scipy_optimizer.py @@ -76,9 +76,6 @@ def __init__( self._max_evals_grouped = max_evals_grouped self._kwargs = kwargs - # Initialize 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 passed to SciPyOptimizer.minimize() and is not " @@ -131,13 +128,9 @@ def minimize( bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - # Allocate bounds attribute - if bounds is not None: - self._bounds = bounds - # Remove ignored bounds to suppress the warning of scipy.optimize.minimize if self.is_bounds_ignored: - self._bounds = None + bounds = None # Remove ignored gradient to suppress the warning of scipy.optimize.minimize if self.is_gradient_ignored: @@ -171,7 +164,7 @@ def minimize( x0=x0, method=self._method, jac=jac, - bounds=self._bounds, + bounds=bounds, options=self._options, **self._kwargs, ) From 1472f4f2c032c22fb0b72b53e4537b5f1e13119d Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 7 Mar 2024 23:15:45 +0100 Subject: [PATCH 37/39] Add release note --- ...ise-error-incorrect-bounds-passing-c878f155221fa8f4.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 releasenotes/notes/0.3/raise-error-incorrect-bounds-passing-c878f155221fa8f4.yaml 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``. From f4d76fa9daf593191e319e6ac366037741334a3d Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:02:05 +0000 Subject: [PATCH 38/39] Remove _bounds attr from tests, add `list[tuple[None, None]]` as possible bounds typing --- qiskit_algorithms/optimizers/gsls.py | 2 +- test/optimizers/test_optimizers.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/qiskit_algorithms/optimizers/gsls.py b/qiskit_algorithms/optimizers/gsls.py index c6b423db..76ba3db6 100644 --- a/qiskit_algorithms/optimizers/gsls.py +++ b/qiskit_algorithms/optimizers/gsls.py @@ -110,7 +110,7 @@ def minimize( fun: Callable[[POINT], float], x0: POINT, jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, + bounds: list[tuple[float, float]] | list[tuple[None, None]] | None = None, ) -> OptimizerResult: if not isinstance(x0, np.ndarray): x0 = np.asarray(x0) diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 7512f8cd..c3b9e4cb 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -200,16 +200,10 @@ def test_scipy_optimizer_parse_bounds(self): # 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( From 997f38b91048c65f1413be31fe64516d833d1987 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:03:00 +0000 Subject: [PATCH 39/39] Remove _bounds attr from tests --- qiskit_algorithms/optimizers/gsls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_algorithms/optimizers/gsls.py b/qiskit_algorithms/optimizers/gsls.py index 76ba3db6..c6b423db 100644 --- a/qiskit_algorithms/optimizers/gsls.py +++ b/qiskit_algorithms/optimizers/gsls.py @@ -110,7 +110,7 @@ def minimize( fun: Callable[[POINT], float], x0: POINT, jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | list[tuple[None, None]] | None = None, + bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: if not isinstance(x0, np.ndarray): x0 = np.asarray(x0)