Skip to content

Commit

Permalink
Fix lack of alternative for Optimizer in AQC (#11099) (#11148)
Browse files Browse the repository at this point in the history
* Add alternative and deprecation warning

* Add deprecation test

* Apply Julien's suggestion

Co-authored-by: Julien Gacon <gaconju@gmail.com>

* Apply suggestions from Julien's code review

* Move optimizer setting to init

* Fix ddt import

* Fix lint

* Add reno

---------

Co-authored-by: Julien Gacon <gaconju@gmail.com>
(cherry picked from commit f0d3937)

Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
  • Loading branch information
mergify[bot] and ElePT authored Oct 30, 2023
1 parent 2e861ba commit e1d5cfe
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 32 deletions.
104 changes: 86 additions & 18 deletions qiskit/transpiler/synthesis/aqc/aqc.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
<https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`__.
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 <https://docs.scipy.org/doc
/scipy/reference/generated/scipy.optimize.minimize.html>`__.
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
Expand All @@ -48,28 +102,44 @@ 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(
self,
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
Expand All @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions qiskit/transpiler/synthesis/aqc/aqc_plugin.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
35 changes: 27 additions & 8 deletions test/python/transpiler/aqc/test_aqc.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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):
"""
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions test/python/transpiler/aqc/test_aqc_plugin.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit e1d5cfe

Please sign in to comment.