diff --git a/qiskit/transpiler/synthesis/aqc/aqc.py b/qiskit/transpiler/synthesis/aqc/aqc.py index e90fd5612f97..905116cd2a6c 100644 --- a/qiskit/transpiler/synthesis/aqc/aqc.py +++ b/qiskit/transpiler/synthesis/aqc/aqc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # 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,30 +10,84 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """A generic implementation of Approximate Quantum Compiler.""" -from typing import Optional +from __future__ import annotations + +from functools import partial + +from collections.abc import Callable +from typing import Protocol import numpy as np +from scipy.optimize import OptimizeResult, minimize -from qiskit.algorithms.optimizers import L_BFGS_B, Optimizer +from qiskit.algorithms.optimizers import Optimizer from qiskit.quantum_info import Operator +from qiskit.utils.deprecation import deprecate_arg + from .approximate import ApproximateCircuit, ApproximatingObjective +class Minimizer(Protocol): + """Callable Protocol for minimizer. + + This interface is based on `SciPy's optimize module + `__. + + This protocol defines a callable taking the following parameters: + + fun + The objective function to minimize. + x0 + The initial point for the optimization. + jac + The gradient of the objective function. + bounds + Parameters bounds for the optimization. Note that these might not be supported + by all optimizers. + + and which returns a SciPy minimization result object. + """ + + def __call__( + self, + fun: Callable[[np.ndarray], float], + x0: np.ndarray, # pylint: disable=invalid-name + jac: Callable[[np.ndarray], np.ndarray] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizeResult: + """Minimize the objective function. + + This interface is based on `SciPy's optimize module `__. + + Args: + fun: The objective function to minimize. + x0: The initial point for the optimization. + jac: The gradient of the objective function. + bounds: Parameters bounds for the optimization. Note that these might not be supported + by all optimizers. + + Returns: + The SciPy minimization result object. + """ + ... # pylint: disable=unnecessary-ellipsis + + class AQC: """ - A generic implementation of Approximate Quantum Compiler. This implementation is agnostic of + A generic implementation of the Approximate Quantum Compiler. This implementation is agnostic of the underlying implementation of the approximate circuit, objective, and optimizer. Users may pass corresponding implementations of the abstract classes: - * Optimizer is an instance of :class:`~qiskit.algorithms.optimizers.Optimizer` and used to run - the optimization process. A choice of optimizer may affect overall convergence, required time + * The *optimizer* is an implementation of the :class:`~.Minimizer` protocol, a callable used to run + the optimization process. The choice of optimizer may affect overall convergence, required time for the optimization process and achieved objective value. - * Approximate circuit represents a template which parameters we want to optimize. Currently, + * The *approximate circuit* represents a template which parameters we want to optimize. Currently, there's only one implementation based on 4-rotations CNOT unit blocks: :class:`.CNOTUnitCircuit`. See the paper for more details. - * Approximate objective is tightly coupled with the approximate circuit implementation and + * The *approximate objective* is tightly coupled with the approximate circuit implementation and provides two methods for computing objective function and gradient with respect to approximate circuit parameters. This objective is passed to the optimizer. Currently, there are two implementations based on 4-rotations CNOT unit blocks: :class:`.DefaultCNOTUnitObjective` and @@ -48,20 +102,36 @@ class AQC: also allocates a number of temporary memory buffers comparable in size to the target matrix. """ + @deprecate_arg( + "optimizer", + deprecation_description=( + "Setting the `optimizer` argument to an instance " + "of `qiskit.algorithms.optimizers.Optimizer` " + ), + additional_msg=("Please, submit a callable that follows the `Minimizer` protocol instead."), + predicate=lambda optimizer: isinstance(optimizer, Optimizer), + since="0.45.0", + ) def __init__( self, - optimizer: Optional[Optimizer] = None, - seed: Optional[int] = None, + optimizer: Minimizer | Optimizer | None = None, + seed: int | None = None, ): """ Args: optimizer: an optimizer to be used in the optimization procedure of the search for - the best approximate circuit. By default, :obj:`.L_BFGS_B` is used with max - iterations set to 1000. - seed: a seed value to be user by a random number generator. + the best approximate circuit. By default, the scipy minimizer with the + ``L-BFGS-B`` method is used with max iterations set to 1000. + seed: a seed value to be used by a random number generator. """ super().__init__() - self._optimizer = optimizer + self._optimizer = optimizer or partial( + minimize, args=(), method="L-BFGS-B", options={"maxiter": 1000} + ) + # temporary fix -> remove after deprecation period of Optimizer + if isinstance(self._optimizer, Optimizer): + self._optimizer = self._optimizer.minimize + self._seed = seed def compile_unitary( @@ -69,7 +139,7 @@ def compile_unitary( target_matrix: np.ndarray, approximate_circuit: ApproximateCircuit, approximating_objective: ApproximatingObjective, - initial_point: Optional[np.ndarray] = None, + initial_point: np.ndarray | None = None, ) -> None: """ Approximately compiles a circuit represented as a unitary matrix by solving an optimization @@ -96,13 +166,11 @@ def compile_unitary( # set the matrix to approximate in the algorithm approximating_objective.target_matrix = su_matrix - optimizer = self._optimizer or L_BFGS_B(maxiter=1000) - if initial_point is None: np.random.seed(self._seed) initial_point = np.random.uniform(0, 2 * np.pi, approximating_objective.num_thetas) - opt_result = optimizer.minimize( + opt_result = self._optimizer( fun=approximating_objective.objective, x0=initial_point, jac=approximating_objective.gradient, diff --git a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py index 0138d8e7b97a..93403a64f81c 100644 --- a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py +++ b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -12,6 +12,7 @@ """ An AQC synthesis plugin to Qiskit's transpiler. """ +from functools import partial import numpy as np from qiskit.converters import circuit_to_dag @@ -104,7 +105,7 @@ def run(self, unitary, **options): # Runtime imports to avoid the overhead of these imports for # plugin discovery and only use them if the plugin is run/used - from qiskit.algorithms.optimizers import L_BFGS_B + from scipy.optimize import minimize from qiskit.transpiler.synthesis.aqc.aqc import AQC from qiskit.transpiler.synthesis.aqc.cnot_structures import make_cnot_network from qiskit.transpiler.synthesis.aqc.cnot_unit_circuit import CNOTUnitCircuit @@ -125,7 +126,8 @@ def run(self, unitary, **options): depth=depth, ) - optimizer = config.get("optimizer", L_BFGS_B(maxiter=1000)) + default_optimizer = partial(minimize, args=(), method="L-BFGS-B", options={"maxiter": 1000}) + optimizer = config.get("optimizer", default_optimizer) seed = config.get("seed") aqc = AQC(optimizer, seed) diff --git a/releasenotes/notes/fix-aqc-optimizer-typehint-34b54c6278d23f79.yaml b/releasenotes/notes/fix-aqc-optimizer-typehint-34b54c6278d23f79.yaml new file mode 100644 index 000000000000..bca717c18745 --- /dev/null +++ b/releasenotes/notes/fix-aqc-optimizer-typehint-34b54c6278d23f79.yaml @@ -0,0 +1,21 @@ +--- +fixes: + - | + The use of the (deprecated) ``Optimizer`` class on :class:`~.AQC` did not have a + non-deprecated alternative path, which should have been introduced in + the original ``qiskit-algorithms`` deprecation PR + [#10406](https://github.com/Qiskit/qiskit/pull/10406). + It now accepts a callable that implements the :class:`~.Minimizer` protocol, + as explicitly stated in the deprecation warning. The callable can look like the + following example: + + .. code-block:: python + + from scipy.optimize import minimize + from qiskit.transpiler.synthesis.aqc.aqc import AQC + + optimizer = partial(minimize, args=(), method="L-BFGS-B", options={"maxiter": 200}) + aqc = AQC(optimizer=optimizer) + + + diff --git a/test/python/transpiler/aqc/test_aqc.py b/test/python/transpiler/aqc/test_aqc.py index 45b1a5ea51ee..885fe4c1fc13 100644 --- a/test/python/transpiler/aqc/test_aqc.py +++ b/test/python/transpiler/aqc/test_aqc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # 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 @@ -12,9 +12,15 @@ """ Tests AQC framework using hardcoded and randomly generated circuits. """ +from functools import partial + import unittest from test.python.transpiler.aqc.sample_data import ORIGINAL_CIRCUIT, INITIAL_THETAS + +from ddt import ddt, data import numpy as np +from scipy.optimize import minimize + from qiskit.algorithms.optimizers import L_BFGS_B from qiskit.quantum_info import Operator from qiskit.test import QiskitTestCase @@ -25,10 +31,12 @@ from qiskit.transpiler.synthesis.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective +@ddt class TestAqc(QiskitTestCase): """Main tests of approximate quantum compiler.""" - def test_aqc(self): + @data(True, False) + def test_aqc(self, uses_default): """Tests AQC on a hardcoded circuit/matrix.""" seed = 12345 @@ -38,9 +46,11 @@ def test_aqc(self): num_qubits=num_qubits, network_layout="spin", connectivity_type="full", depth=0 ) - optimizer = L_BFGS_B(maxiter=200) - - aqc = AQC(optimizer=optimizer, seed=seed) + if uses_default: + aqc = AQC(seed=seed) + else: + optimizer = partial(minimize, args=(), method="L-BFGS-B", options={"maxiter": 200}) + aqc = AQC(optimizer=optimizer, seed=seed) target_matrix = ORIGINAL_CIRCUIT approximate_circuit = CNOTUnitCircuit(num_qubits, cnots) @@ -55,7 +65,16 @@ def test_aqc(self): approx_matrix = Operator(approximate_circuit).data error = 0.5 * (np.linalg.norm(approx_matrix - ORIGINAL_CIRCUIT, "fro") ** 2) - self.assertTrue(error < 1e-3) + self.assertLess(error, 1e-3) + + def test_aqc_deprecation(self): + """Tests that AQC raises deprecation warning.""" + + seed = 12345 + optimizer = L_BFGS_B(maxiter=200) + + with self.assertRaises(DeprecationWarning): + _ = AQC(optimizer=optimizer, seed=seed) def test_aqc_fastgrad(self): """ @@ -70,7 +89,7 @@ def test_aqc_fastgrad(self): num_qubits=num_qubits, network_layout="spin", connectivity_type="full", depth=0 ) - optimizer = L_BFGS_B(maxiter=200) + optimizer = partial(minimize, args=(), method="L-BFGS-B", options={"maxiter": 200}) aqc = AQC(optimizer=optimizer, seed=seed) # Make multi-control CNOT gate matrix. @@ -103,7 +122,7 @@ def test_aqc_determinant_minus_one(self): num_qubits=num_qubits, network_layout="spin", connectivity_type="full", depth=0 ) - optimizer = L_BFGS_B(maxiter=200) + optimizer = partial(minimize, args=(), method="L-BFGS-B", options={"maxiter": 200}) aqc = AQC(optimizer=optimizer, seed=seed) target_matrix = np.eye(2**num_qubits, dtype=int) diff --git a/test/python/transpiler/aqc/test_aqc_plugin.py b/test/python/transpiler/aqc/test_aqc_plugin.py index 0ad2742a1195..b5f3bf1858f4 100644 --- a/test/python/transpiler/aqc/test_aqc_plugin.py +++ b/test/python/transpiler/aqc/test_aqc_plugin.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2023. # # 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 @@ -12,11 +12,12 @@ """ Tests AQC plugin. """ +from functools import partial import numpy as np +from scipy.optimize import minimize from qiskit import QuantumCircuit -from qiskit.algorithms.optimizers import SLSQP from qiskit.converters import dag_to_circuit, circuit_to_dag from qiskit.quantum_info import Operator from qiskit.test import QiskitTestCase @@ -68,12 +69,13 @@ def test_plugin_setup(self): def test_plugin_configuration(self): """Tests plugin with a custom configuration.""" + optimizer = partial(minimize, args=(), method="SLSQP") config = { "network_layout": "sequ", "connectivity_type": "full", "depth": 0, "seed": 12345, - "optimizer": SLSQP(), + "optimizer": optimizer, } transpiler_pass = UnitarySynthesis( basis_gates=["rx", "ry", "rz", "cx"], method="aqc", plugin_config=config