From 081c5400f7f480c156234f53970cc884ff253831 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 18 Dec 2023 08:53:45 +0000 Subject: [PATCH 01/35] deprecate transpiler/synthesis/graysynth.py --- qiskit/synthesis/__init__.py | 1 + qiskit/transpiler/synthesis/graysynth.py | 14 +++++++++-- .../synthesis/test_cnot_phase_synthesis.py | 24 +++++++++++++++---- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index f4d6a73f2a70..a17a7fb3c047 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -43,6 +43,7 @@ .. autofunction:: synth_cz_depth_line_mr .. autofunction:: synth_cx_cz_depth_line_my + .. autofunction:: synth_cnot_phase_aam Permutation Synthesis ===================== diff --git a/qiskit/transpiler/synthesis/graysynth.py b/qiskit/transpiler/synthesis/graysynth.py index 7f0ab1c08ba0..b32cacc2d758 100644 --- a/qiskit/transpiler/synthesis/graysynth.py +++ b/qiskit/transpiler/synthesis/graysynth.py @@ -19,12 +19,16 @@ """ -# Redirect getattrs to modules new location -# TODO: Deprecate in 0.24.0 and remove in 0.26.0 from qiskit.synthesis.linear.cnot_synth import * from qiskit.synthesis.linear_phase.cnot_phase_synth import * +from qiskit.utils.deprecation import deprecate_func +@deprecate_func( + since="0.46.0", + additional_msg="Instead, use the function ``synth_cnot_count_full_pmh`` from the module" + + "``qiskit.synthesis.linear``.", +) def cnot_synth(state, section_size=2): """ Synthesize linear reversible circuits for all-to-all architecture @@ -55,6 +59,12 @@ def cnot_synth(state, section_size=2): return synth_cnot_count_full_pmh(state, section_size=section_size) + +@deprecate_func( + since="0.46.0", + additional_msg="Instead, use the function ``synth_cnot_phase_aam`` from the module" + + "``qiskit.synthesis.linear_phase``.", +) def graysynth(cnots, angles, section_size=2): """This function is an implementation of the GraySynth algorithm of Amy, Azimadeh and Mosca. diff --git a/test/python/synthesis/test_cnot_phase_synthesis.py b/test/python/synthesis/test_cnot_phase_synthesis.py index d96d7cbc8448..8bf6c28a8cc3 100644 --- a/test/python/synthesis/test_cnot_phase_synthesis.py +++ b/test/python/synthesis/test_cnot_phase_synthesis.py @@ -75,7 +75,11 @@ def test_gray_synth(self, synth_func): [0, 1, 0, 0, 1, 0], ] angles = ["s", "t", "z", "s", "t", "t"] - c_gray = synth_func(cnots, angles) + if synth_func.__name__ == "graysynth": + with self.assertWarns(DeprecationWarning): + c_gray = synth_func(cnots, angles) + else: + c_gray = synth_func(cnots, angles) unitary_gray = UnitaryGate(Operator(c_gray)) # Create the circuit displayed above: @@ -133,7 +137,11 @@ def test_paper_example(self, synth_func): """ cnots = [[0, 1, 1, 1, 1, 1], [1, 0, 0, 1, 1, 1], [1, 0, 0, 1, 0, 0], [0, 0, 1, 0, 1, 0]] angles = ["t"] * 6 - c_gray = synth_func(cnots, angles) + if synth_func.__name__ == "graysynth": + with self.assertWarns(DeprecationWarning): + c_gray = synth_func(cnots, angles) + else: + c_gray = synth_func(cnots, angles) unitary_gray = UnitaryGate(Operator(c_gray)) # Create the circuit displayed above: @@ -186,7 +194,11 @@ def test_ccz(self, synth_func): """ cnots = [[1, 0, 0, 1, 1, 0, 1], [0, 1, 0, 1, 0, 1, 1], [0, 0, 1, 0, 1, 1, 1]] angles = ["t", "t", "t", "tdg", "tdg", "tdg", "t"] - c_gray = synth_func(cnots, angles) + if synth_func.__name__ == "graysynth": + with self.assertWarns(DeprecationWarning): + c_gray = synth_func(cnots, angles) + else: + c_gray = synth_func(cnots, angles) unitary_gray = UnitaryGate(Operator(c_gray)) # Create the circuit displayed above: @@ -252,7 +264,11 @@ def test_patel_markov_hayes(self, synth_func): [1, 1, 0, 1, 1, 1], [0, 0, 1, 1, 1, 0], ] - c_patel = synth_func(state) + if synth_func.__name__ == "cnot_synth": + with self.assertWarns(DeprecationWarning): + c_patel = synth_func(state) + else: + c_patel = synth_func(state) unitary_patel = UnitaryGate(Operator(c_patel)) # Create the circuit displayed above: From de19803aa60ef94acc69f1dda50d35bf170dfb57 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 18 Dec 2023 09:08:30 +0000 Subject: [PATCH 02/35] style --- qiskit/transpiler/synthesis/graysynth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/transpiler/synthesis/graysynth.py b/qiskit/transpiler/synthesis/graysynth.py index b32cacc2d758..5439243d84cd 100644 --- a/qiskit/transpiler/synthesis/graysynth.py +++ b/qiskit/transpiler/synthesis/graysynth.py @@ -59,7 +59,6 @@ def cnot_synth(state, section_size=2): return synth_cnot_count_full_pmh(state, section_size=section_size) - @deprecate_func( since="0.46.0", additional_msg="Instead, use the function ``synth_cnot_phase_aam`` from the module" From 2bf99a3c4cb35aa52a25af91f6ba4d16b7ad75d9 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 18 Dec 2023 09:45:11 +0000 Subject: [PATCH 03/35] style --- qiskit/synthesis/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index a17a7fb3c047..3577d7310364 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -43,7 +43,7 @@ .. autofunction:: synth_cz_depth_line_mr .. autofunction:: synth_cx_cz_depth_line_my - .. autofunction:: synth_cnot_phase_aam +.. autofunction:: synth_cnot_phase_aam Permutation Synthesis ===================== From f884e6d2f2f275a1dddb3353573ac3f07f385431 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Tue, 19 Dec 2023 10:41:24 +0000 Subject: [PATCH 04/35] move aqc_plugin to qiskit/transpiler/passes/synthesis --- pyproject.toml | 2 +- qiskit/transpiler/passes/__init__.py | 2 + .../transpiler/passes/synthesis/__init__.py | 1 + .../transpiler/passes/synthesis/aqc_plugin.py | 146 ++++++++++++++++++ qiskit/transpiler/synthesis/aqc/__init__.py | 2 - qiskit/transpiler/synthesis/aqc/aqc_plugin.py | 7 + test/python/transpiler/aqc/test_aqc_plugin.py | 2 +- 7 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 qiskit/transpiler/passes/synthesis/aqc_plugin.py diff --git a/pyproject.toml b/pyproject.toml index 04bafbf3f995..55d10f19398c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ Changelog = "https://qiskit.org/documentation/release_notes.html" [project.entry-points."qiskit.unitary_synthesis"] default = "qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis" -aqc = "qiskit.transpiler.synthesis.aqc.aqc_plugin:AQCSynthesisPlugin" +aqc = "qiskit.transpiler.passes.synthesis.aqc_plugin:AQCSynthesisPlugin" sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevSynthesis" [project.entry-points."qiskit.synthesis"] diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 9616b11740a3..19925f3fe147 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -147,6 +147,7 @@ HLSConfig SolovayKitaev SolovayKitaevSynthesis + AQCSynthesisPlugin Post Layout (Post transpile qubit selection) ============================================ @@ -253,6 +254,7 @@ from .synthesis import HLSConfig from .synthesis import SolovayKitaev from .synthesis import SolovayKitaevSynthesis +from .synthesis import AQCSynthesisPlugin # calibration from .calibration import PulseGates diff --git a/qiskit/transpiler/passes/synthesis/__init__.py b/qiskit/transpiler/passes/synthesis/__init__.py index 11540d89387a..dba751b90be8 100644 --- a/qiskit/transpiler/passes/synthesis/__init__.py +++ b/qiskit/transpiler/passes/synthesis/__init__.py @@ -17,3 +17,4 @@ from .linear_functions_synthesis import LinearFunctionsSynthesis, LinearFunctionsToPermutations from .high_level_synthesis import HighLevelSynthesis, HLSConfig from .solovay_kitaev_synthesis import SolovayKitaev, SolovayKitaevSynthesis +from .aqc_plugin import AQCSynthesisPlugin diff --git a/qiskit/transpiler/passes/synthesis/aqc_plugin.py b/qiskit/transpiler/passes/synthesis/aqc_plugin.py new file mode 100644 index 000000000000..0fa153566557 --- /dev/null +++ b/qiskit/transpiler/passes/synthesis/aqc_plugin.py @@ -0,0 +1,146 @@ +# This code is part of Qiskit. +# +# (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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +An AQC synthesis plugin to Qiskit's transpiler. +""" +from functools import partial +import numpy as np + +from qiskit.converters import circuit_to_dag +from qiskit.transpiler.passes.synthesis.plugin import UnitarySynthesisPlugin + + +class AQCSynthesisPlugin(UnitarySynthesisPlugin): + """ + An AQC-based Qiskit unitary synthesis plugin. + + This plugin is invoked by :func:`~.compiler.transpile` when the ``unitary_synthesis_method`` + parameter is set to ``"aqc"``. + + This plugin supports customization and additional parameters can be passed to the plugin + by passing a dictionary as the ``unitary_synthesis_plugin_config`` parameter of + the :func:`~qiskit.compiler.transpile` function. + + Supported parameters in the dictionary: + + network_layout (str) + Type of network geometry, one of {``"sequ"``, ``"spin"``, ``"cart"``, ``"cyclic_spin"``, + ``"cyclic_line"``}. Default value is ``"spin"``. + + connectivity_type (str) + type of inter-qubit connectivity, {``"full"``, ``"line"``, ``"star"``}. Default value + is ``"full"``. + + depth (int) + depth of the CNOT-network, i.e. the number of layers, where each layer consists of a + single CNOT-block. + + optimizer (:class:`~.Minimizer`) + An implementation of the ``Minimizer`` protocol to be used in the optimization process. + + seed (int) + A random seed. + + initial_point (:class:`~numpy.ndarray`) + Initial values of angles/parameters to start the optimization process from. + """ + + @property + def max_qubits(self): + """Maximum number of supported qubits is ``14``.""" + return 14 + + @property + def min_qubits(self): + """Minimum number of supported qubits is ``3``.""" + return 3 + + @property + def supports_natural_direction(self): + """The plugin does not support natural direction, + it assumes bidirectional two qubit gates.""" + return False + + @property + def supports_pulse_optimize(self): + """The plugin does not support optimization of pulses.""" + return False + + @property + def supports_gate_lengths(self): + """The plugin does not support gate lengths.""" + return False + + @property + def supports_gate_errors(self): + """The plugin does not support gate errors.""" + return False + + @property + def supported_bases(self): + """The plugin does not support bases for synthesis.""" + return None + + @property + def supports_basis_gates(self): + """The plugin does not support basis gates and by default it synthesizes a circuit using + ``["rx", "ry", "rz", "cx"]`` gate basis.""" + return False + + @property + def supports_coupling_map(self): + """The plugin does not support coupling maps.""" + return False + + 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 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 + from qiskit.transpiler.synthesis.aqc.cnot_unit_objective import DefaultCNOTUnitObjective + + num_qubits = int(round(np.log2(unitary.shape[0]))) + + config = options.get("config") or {} + + network_layout = config.get("network_layout", "spin") + connectivity_type = config.get("connectivity_type", "full") + depth = config.get("depth", 0) + + cnots = make_cnot_network( + num_qubits=num_qubits, + network_layout=network_layout, + connectivity_type=connectivity_type, + depth=depth, + ) + + 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) + + approximate_circuit = CNOTUnitCircuit(num_qubits=num_qubits, cnots=cnots) + approximating_objective = DefaultCNOTUnitObjective(num_qubits=num_qubits, cnots=cnots) + + initial_point = config.get("initial_point") + aqc.compile_unitary( + target_matrix=unitary, + approximate_circuit=approximate_circuit, + approximating_objective=approximating_objective, + initial_point=initial_point, + ) + + dag_circuit = circuit_to_dag(approximate_circuit) + return dag_circuit diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index 2227a78a0984..c851b76d3a59 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -31,7 +31,6 @@ :template: autosummary/class_no_inherited_members.rst AQC - AQCSynthesisPlugin ApproximateCircuit ApproximatingObjective CNOTUnitCircuit @@ -171,7 +170,6 @@ from .approximate import ApproximateCircuit, ApproximatingObjective from .aqc import AQC -from .aqc_plugin import AQCSynthesisPlugin from .cnot_structures import make_cnot_network from .cnot_unit_circuit import CNOTUnitCircuit from .cnot_unit_objective import CNOTUnitObjective, DefaultCNOTUnitObjective diff --git a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py index 0fa153566557..2ea58f2ea37c 100644 --- a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py +++ b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py @@ -17,6 +17,7 @@ from qiskit.converters import circuit_to_dag from qiskit.transpiler.passes.synthesis.plugin import UnitarySynthesisPlugin +from qiskit.utils.deprecation import deprecate_func class AQCSynthesisPlugin(UnitarySynthesisPlugin): @@ -101,6 +102,12 @@ def supports_coupling_map(self): """The plugin does not support coupling maps.""" return False + @deprecate_func( + since="0.46.0", + pending=True, + additional_msg="AQCSynthesisPlugin has been moved to qiskit.transpiler.passes.synthesis" + "instead use AQCSynthesisPlugin from qiskit.transpiler.passes.synthesis", + ) def run(self, unitary, **options): # Runtime imports to avoid the overhead of these imports for diff --git a/test/python/transpiler/aqc/test_aqc_plugin.py b/test/python/transpiler/aqc/test_aqc_plugin.py index b5f3bf1858f4..b55e6ffb379b 100644 --- a/test/python/transpiler/aqc/test_aqc_plugin.py +++ b/test/python/transpiler/aqc/test_aqc_plugin.py @@ -23,7 +23,7 @@ from qiskit.test import QiskitTestCase from qiskit.transpiler import PassManager from qiskit.transpiler.passes import UnitarySynthesis -from qiskit.transpiler.synthesis.aqc.aqc_plugin import AQCSynthesisPlugin +from qiskit.transpiler.passes.synthesis import AQCSynthesisPlugin class TestAQCSynthesisPlugin(QiskitTestCase): From 54f0bab3419463ec6d8e37f78f01be4c44539667 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 08:15:12 +0000 Subject: [PATCH 05/35] remove code from qiskit/transpiler/synthesis/aqc/aqc_plugin.py --- qiskit/transpiler/synthesis/aqc/__init__.py | 1 + qiskit/transpiler/synthesis/aqc/aqc_plugin.py | 104 ++---------------- test/python/transpiler/aqc/test_aqc_plugin.py | 3 + 3 files changed, 15 insertions(+), 93 deletions(-) diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index c851b76d3a59..bf3e4c80ba5a 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -174,3 +174,4 @@ from .cnot_unit_circuit import CNOTUnitCircuit from .cnot_unit_objective import CNOTUnitObjective, DefaultCNOTUnitObjective from .fast_gradient.fast_gradient import FastCNOTUnitObjective +from .aqc_plugin import AQCSynthesisPlugin diff --git a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py index 2ea58f2ea37c..9c21c6b5d188 100644 --- a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py +++ b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py @@ -12,15 +12,12 @@ """ An AQC synthesis plugin to Qiskit's transpiler. """ -from functools import partial -import numpy as np -from qiskit.converters import circuit_to_dag -from qiskit.transpiler.passes.synthesis.plugin import UnitarySynthesisPlugin +from qiskit.transpiler.passes.synthesis import AQCSynthesisPlugin as NewAQCSynthesisPlugin from qiskit.utils.deprecation import deprecate_func -class AQCSynthesisPlugin(UnitarySynthesisPlugin): +class AQCSynthesisPlugin(NewAQCSynthesisPlugin): """ An AQC-based Qiskit unitary synthesis plugin. @@ -55,52 +52,14 @@ class AQCSynthesisPlugin(UnitarySynthesisPlugin): Initial values of angles/parameters to start the optimization process from. """ - @property - def max_qubits(self): - """Maximum number of supported qubits is ``14``.""" - return 14 - - @property - def min_qubits(self): - """Minimum number of supported qubits is ``3``.""" - return 3 - - @property - def supports_natural_direction(self): - """The plugin does not support natural direction, - it assumes bidirectional two qubit gates.""" - return False - - @property - def supports_pulse_optimize(self): - """The plugin does not support optimization of pulses.""" - return False - - @property - def supports_gate_lengths(self): - """The plugin does not support gate lengths.""" - return False - - @property - def supports_gate_errors(self): - """The plugin does not support gate errors.""" - return False - - @property - def supported_bases(self): - """The plugin does not support bases for synthesis.""" - return None - - @property - def supports_basis_gates(self): - """The plugin does not support basis gates and by default it synthesizes a circuit using - ``["rx", "ry", "rz", "cx"]`` gate basis.""" - return False - - @property - def supports_coupling_map(self): - """The plugin does not support coupling maps.""" - return False + @deprecate_func( + since="0.46.0", + pending=True, + additional_msg="AQCSynthesisPlugin has been moved to qiskit.transpiler.passes.synthesis" + "instead use AQCSynthesisPlugin from qiskit.transpiler.passes.synthesis", + ) + def __init__(self): + super().__init__() @deprecate_func( since="0.46.0", @@ -109,45 +68,4 @@ def supports_coupling_map(self): "instead use AQCSynthesisPlugin from qiskit.transpiler.passes.synthesis", ) 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 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 - from qiskit.transpiler.synthesis.aqc.cnot_unit_objective import DefaultCNOTUnitObjective - - num_qubits = int(round(np.log2(unitary.shape[0]))) - - config = options.get("config") or {} - - network_layout = config.get("network_layout", "spin") - connectivity_type = config.get("connectivity_type", "full") - depth = config.get("depth", 0) - - cnots = make_cnot_network( - num_qubits=num_qubits, - network_layout=network_layout, - connectivity_type=connectivity_type, - depth=depth, - ) - - 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) - - approximate_circuit = CNOTUnitCircuit(num_qubits=num_qubits, cnots=cnots) - approximating_objective = DefaultCNOTUnitObjective(num_qubits=num_qubits, cnots=cnots) - - initial_point = config.get("initial_point") - aqc.compile_unitary( - target_matrix=unitary, - approximate_circuit=approximate_circuit, - approximating_objective=approximating_objective, - initial_point=initial_point, - ) - - dag_circuit = circuit_to_dag(approximate_circuit) - return dag_circuit + return super().run(unitary, **options) diff --git a/test/python/transpiler/aqc/test_aqc_plugin.py b/test/python/transpiler/aqc/test_aqc_plugin.py index b55e6ffb379b..4868ffce4297 100644 --- a/test/python/transpiler/aqc/test_aqc_plugin.py +++ b/test/python/transpiler/aqc/test_aqc_plugin.py @@ -24,6 +24,7 @@ from qiskit.transpiler import PassManager from qiskit.transpiler.passes import UnitarySynthesis from qiskit.transpiler.passes.synthesis import AQCSynthesisPlugin +from qiskit.transpiler.synthesis.aqc import AQCSynthesisPlugin as OldAQCSynthesisPlugin class TestAQCSynthesisPlugin(QiskitTestCase): @@ -47,6 +48,8 @@ def test_aqc_plugin(self): """Basic test of the plugin.""" plugin = AQCSynthesisPlugin() dag = plugin.run(self._target_unitary, config=self._seed_config) + with self.assertWarns(PendingDeprecationWarning): + _ = OldAQCSynthesisPlugin() approx_circuit = dag_to_circuit(dag) approx_unitary = Operator(approx_circuit).data From 2ea0016f5762ef6414b0897f4f261897a3d505e5 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 09:22:37 +0000 Subject: [PATCH 06/35] copy qiskit/transpiler/synthesis/aqc to qiskit/synthesis/unitary --- qiskit/synthesis/unitary/__init__.py | 13 + qiskit/synthesis/unitary/aqc/__init__.py | 176 +++++++++ qiskit/synthesis/unitary/aqc/approximate.py | 116 ++++++ qiskit/synthesis/unitary/aqc/aqc.py | 170 ++++++++ .../synthesis/unitary/aqc/cnot_structures.py | 299 ++++++++++++++ .../unitary/aqc/cnot_unit_circuit.py | 103 +++++ .../unitary/aqc/cnot_unit_objective.py | 299 ++++++++++++++ .../unitary/aqc/elementary_operations.py | 108 +++++ .../unitary/aqc/fast_gradient/__init__.py | 164 ++++++++ .../aqc/fast_gradient/fast_grad_utils.py | 237 +++++++++++ .../aqc/fast_gradient/fast_gradient.py | 225 +++++++++++ .../unitary/aqc/fast_gradient/layer.py | 370 ++++++++++++++++++ .../unitary/aqc/fast_gradient/pmatrix.py | 312 +++++++++++++++ .../aqc/fast_gradient/test_cmp_gradients.py | 4 +- .../aqc/fast_gradient/test_layer1q.py | 4 +- .../aqc/fast_gradient/test_layer2q.py | 4 +- .../aqc/fast_gradient/test_utils.py | 6 +- .../aqc/fast_gradient/utils_for_testing.py | 2 +- test/python/transpiler/aqc/test_aqc.py | 10 +- .../transpiler/aqc/test_cnot_networks.py | 2 +- test/python/transpiler/aqc/test_gradient.py | 4 +- 21 files changed, 2610 insertions(+), 18 deletions(-) create mode 100644 qiskit/synthesis/unitary/__init__.py create mode 100644 qiskit/synthesis/unitary/aqc/__init__.py create mode 100644 qiskit/synthesis/unitary/aqc/approximate.py create mode 100644 qiskit/synthesis/unitary/aqc/aqc.py create mode 100644 qiskit/synthesis/unitary/aqc/cnot_structures.py create mode 100644 qiskit/synthesis/unitary/aqc/cnot_unit_circuit.py create mode 100644 qiskit/synthesis/unitary/aqc/cnot_unit_objective.py create mode 100644 qiskit/synthesis/unitary/aqc/elementary_operations.py create mode 100644 qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py create mode 100644 qiskit/synthesis/unitary/aqc/fast_gradient/fast_grad_utils.py create mode 100644 qiskit/synthesis/unitary/aqc/fast_gradient/fast_gradient.py create mode 100644 qiskit/synthesis/unitary/aqc/fast_gradient/layer.py create mode 100644 qiskit/synthesis/unitary/aqc/fast_gradient/pmatrix.py diff --git a/qiskit/synthesis/unitary/__init__.py b/qiskit/synthesis/unitary/__init__.py new file mode 100644 index 000000000000..f19592ee8bdb --- /dev/null +++ b/qiskit/synthesis/unitary/__init__.py @@ -0,0 +1,13 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017 - 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module containing unitary synthesis methods.""" diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py new file mode 100644 index 000000000000..c851b76d3a59 --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -0,0 +1,176 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +r""" +===================================================================== +Approximate Quantum Compiler (:mod:`qiskit.transpiler.synthesis.aqc`) +===================================================================== + +.. currentmodule:: qiskit.transpiler.synthesis.aqc + +Implementation of Approximate Quantum Compiler as described in the paper [1]. + +Interface +========= + +The main public interface of this module is reached by passing ``unitary_synthesis_method='aqc'`` to +:obj:`~.compiler.transpile`. This will swap the synthesis method to use :obj:`AQCSynthesisPlugin`. +The individual classes are: + +.. autosummary:: + :toctree: ../stubs + :template: autosummary/class_no_inherited_members.rst + + AQC + ApproximateCircuit + ApproximatingObjective + CNOTUnitCircuit + CNOTUnitObjective + DefaultCNOTUnitObjective + FastCNOTUnitObjective + + +Mathematical Detail +=================== + +We are interested in compiling a quantum circuit, which we formalize as finding the best +circuit representation in terms of an ordered gate sequence of a target unitary matrix +:math:`U\in U(d)`, with some additional hardware constraints. In particular, we look at +representations that could be constrained in terms of hardware connectivity, as well +as gate depth, and we choose a gate basis in terms of CNOT and rotation gates. +We recall that the combination of CNOT and rotation gates is universal in :math:`SU(d)` and +therefore it does not limit compilation. + +To properly define what we mean by best circuit representation, we define the metric +as the Frobenius norm between the unitary matrix of the compiled circuit :math:`V` and +the target unitary matrix :math:`U`, i.e., :math:`\|V - U\|_{\mathrm{F}}`. This choice +is motivated by mathematical programming considerations, and it is related to other +formulations that appear in the literature. Let's take a look at the problem in more details. + +Let :math:`n` be the number of qubits and :math:`d=2^n`. Given a CNOT structure :math:`ct` +and a vector of rotation angles :math:`\theta`, the parametric circuit forms a matrix +:math:`Vct(\theta)\in SU(d)`. If we are given a target circuit forming a matrix +:math:`U\in SU(d)`, then we would like to compute + +.. math:: + + argmax_{\theta}\frac{1}{d}|\langle Vct(\theta),U\rangle| + +where the inner product is the Frobenius inner product. Note that +:math:`|\langle V,U\rangle|\leq d` for all unitaries :math:`U` and :math:`V`, so the objective +has range in :math:`[0,1]`. + +Our strategy is to maximize + +.. math:: + + \frac{1}{d}\Re \langle Vct(\theta),U\rangle + +using its gradient. We will now discuss the specifics by going through an example. + +While the range of :math:`Vct` is a subset of :math:`SU(d)` by construction, the target +circuit may form a general unitary matrix. However, for any :math:`U\in U(d)`, + +.. math:: + + \frac{\exp(2\pi i k/d)}{\det(U)^{1/d}}U\in SU(d)\text{ for all }k\in\{0,\ldots,d-1\}. + +Thus, we should normalize the target circuit by its global phase and then approximately +compile the normalized circuit. We can add the global phase back in afterwards. + +In the algorithm let :math:`U'` denote the un-normalized target matrix and :math:`U` +the normalized target matrix. Now that we have :math:`U`, we give the gradient function +to the Nesterov's method optimizer and compute :math:`\theta`. + +To add the global phase back in, we can form the control circuit as + +.. math:: + + \frac{\langle Vct(\theta),U'\rangle}{|\langle Vct(\theta),U'\rangle|}Vct(\theta). + +Note that while we optimized using Nesterov's method in the paper, this was for its convergence +guarantees, not its speed in practice. It is much faster to use L-BFGS which is used as a +default optimizer in this implementation. + +A basic usage of the AQC algorithm should consist of the following steps:: + + # Define a target circuit as a unitary matrix + unitary = ... + + # Define a number of qubits for the algorithm, at least 3 qubits + num_qubits = int(round(np.log2(unitary.shape[0]))) + + # Choose a layout of the CNOT structure for the approximate circuit, e.g. ``spin`` for + # a linear layout. + layout = options.get("layout") or "spin" + + # Choose a connectivity type, e.g. ``full`` for full connectivity between qubits. + connectivity = options.get("connectivity") or "full" + + # Define a targeted depth of the approximate circuit in the number of CNOT units. + depth = int(options.get("depth") or 0) + + # Generate a network made of CNOT units + cnots = make_cnot_network( + num_qubits=num_qubits, + network_layout=layout, + connectivity_type=connectivity, + depth=depth + ) + + # Create an optimizer to be used by AQC + optimizer = L_BFGS_B() + + # Create an instance + aqc = AQC(optimizer) + + # Create a template circuit that will approximate our target circuit + approximate_circuit = CNOTUnitCircuit(num_qubits=num_qubits, cnots=cnots) + + # Create an objective that defines our optimization problem + approximating_objective = DefaultCNOTUnitObjective(num_qubits=num_qubits, cnots=cnots) + + # Run optimization process to compile the unitary + aqc.compile_unitary( + target_matrix=unitary, + approximate_circuit=approximate_circuit, + approximating_objective=approximating_objective + ) + +Now ``approximate_circuit`` is a circuit that approximates the target unitary to a certain +degree and can be used instead of the original matrix. + +This uses a helper function, :obj:`make_cnot_network`. + +.. autofunction:: make_cnot_network + +One can take advantage of accelerated version of objective function. It implements the same +mathematical algorithm as the default one ``DefaultCNOTUnitObjective`` but runs several times +faster. Instantiation of accelerated objective function class is similar to the default case: + + # Create an objective that defines our optimization problem + approximating_objective = FastCNOTUnitObjective(num_qubits=num_qubits, cnots=cnots) + +The rest of the code in the above example does not change. + +References: + + [1]: Liam Madden, Andrea Simonetto, Best Approximate Quantum Compiling Problems. + `arXiv:2106.05649 `_ +""" + +from .approximate import ApproximateCircuit, ApproximatingObjective +from .aqc import AQC +from .cnot_structures import make_cnot_network +from .cnot_unit_circuit import CNOTUnitCircuit +from .cnot_unit_objective import CNOTUnitObjective, DefaultCNOTUnitObjective +from .fast_gradient.fast_gradient import FastCNOTUnitObjective diff --git a/qiskit/synthesis/unitary/aqc/approximate.py b/qiskit/synthesis/unitary/aqc/approximate.py new file mode 100644 index 000000000000..6c3f11fb71fc --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/approximate.py @@ -0,0 +1,116 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Base classes for an approximate circuit definition.""" +from __future__ import annotations +from abc import ABC, abstractmethod +from typing import Optional, SupportsFloat +import numpy as np + +from qiskit import QuantumCircuit + + +class ApproximateCircuit(QuantumCircuit, ABC): + """A base class that represents an approximate circuit.""" + + def __init__(self, num_qubits: int, name: Optional[str] = None) -> None: + """ + Args: + num_qubits: number of qubit this circuit will span. + name: a name of the circuit. + """ + super().__init__(num_qubits, name=name) + + @property + @abstractmethod + def thetas(self) -> np.ndarray: + """ + The property is not implemented and raises a ``NotImplementedException`` exception. + + Returns: + a vector of parameters of this circuit. + """ + raise NotImplementedError + + @abstractmethod + def build(self, thetas: np.ndarray) -> None: + """ + Constructs this circuit out of the parameters(thetas). Parameter values must be set before + constructing the circuit. + + Args: + thetas: a vector of parameters to be set in this circuit. + """ + raise NotImplementedError + + +class ApproximatingObjective(ABC): + """ + A base class for an optimization problem definition. An implementing class must provide at least + an implementation of the ``objective`` method. In such case only gradient free optimizers can + be used. Both method, ``objective`` and ``gradient``, preferable to have in an implementation. + """ + + def __init__(self) -> None: + # must be set before optimization + self._target_matrix: np.ndarray | None = None + + @abstractmethod + def objective(self, param_values: np.ndarray) -> SupportsFloat: + """ + Computes a value of the objective function given a vector of parameter values. + + Args: + param_values: a vector of parameter values for the optimization problem. + + Returns: + a float value of the objective function. + """ + raise NotImplementedError + + @abstractmethod + def gradient(self, param_values: np.ndarray) -> np.ndarray: + """ + Computes a gradient with respect to parameters given a vector of parameter values. + + Args: + param_values: a vector of parameter values for the optimization problem. + + Returns: + an array of gradient values. + """ + raise NotImplementedError + + @property + def target_matrix(self) -> np.ndarray: + """ + Returns: + a matrix being approximated + """ + return self._target_matrix + + @target_matrix.setter + def target_matrix(self, target_matrix: np.ndarray) -> None: + """ + Args: + target_matrix: a matrix to approximate in the optimization procedure. + """ + self._target_matrix = target_matrix + + @property + @abstractmethod + def num_thetas(self) -> int: + """ + + Returns: + the number of parameters in this optimization problem. + """ + raise NotImplementedError diff --git a/qiskit/synthesis/unitary/aqc/aqc.py b/qiskit/synthesis/unitary/aqc/aqc.py new file mode 100644 index 000000000000..4ced39a7e4ac --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/aqc.py @@ -0,0 +1,170 @@ +# This code is part of Qiskit. +# +# (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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# 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 __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.quantum_info import Operator + +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 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: + + * 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. + + * 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. + + * 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 + its accelerated version :class:`.FastCNOTUnitObjective`. Both implementations share the same + idea of maximization the Hilbert-Schmidt product between the target matrix and its + approximation. The former implementation approach should be considered as a baseline one. It + may suffer from performance issues, and is mostly suitable for a small number of qubits + (up to 5 or 6), whereas the latter, accelerated one, can be applied to larger problems. + + * One should take into consideration the exponential growth of matrix size with the number of + qubits because the implementation not only creates a potentially large target matrix, but + also allocates a number of temporary memory buffers comparable in size to the target matrix. + """ + + def __init__( + self, + optimizer: Minimizer | 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, 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 or partial( + minimize, args=(), method="L-BFGS-B", options={"maxiter": 1000} + ) + + self._seed = seed + + def compile_unitary( + self, + target_matrix: np.ndarray, + approximate_circuit: ApproximateCircuit, + approximating_objective: ApproximatingObjective, + initial_point: np.ndarray | None = None, + ) -> None: + """ + Approximately compiles a circuit represented as a unitary matrix by solving an optimization + problem defined by ``approximating_objective`` and using ``approximate_circuit`` as a + template for the approximate circuit. + + Args: + target_matrix: a unitary matrix to approximate. + approximate_circuit: a template circuit that will be filled with the parameter values + obtained in the optimization procedure. + approximating_objective: a definition of the optimization problem. + initial_point: initial values of angles/parameters to start optimization from. + """ + matrix_dim = target_matrix.shape[0] + # check if it is actually a special unitary matrix + target_det = np.linalg.det(target_matrix) + if not np.isclose(target_det, 1): + su_matrix = target_matrix / np.power(target_det, (1 / matrix_dim), dtype=complex) + global_phase_required = True + else: + su_matrix = target_matrix + global_phase_required = False + + # set the matrix to approximate in the algorithm + approximating_objective.target_matrix = su_matrix + + 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 = self._optimizer( + fun=approximating_objective.objective, + x0=initial_point, + jac=approximating_objective.gradient, + ) + + approximate_circuit.build(opt_result.x) + + approx_matrix = Operator(approximate_circuit).data + + if global_phase_required: + alpha = np.angle(np.trace(np.dot(approx_matrix.conj().T, target_matrix))) + approximate_circuit.global_phase = alpha diff --git a/qiskit/synthesis/unitary/aqc/cnot_structures.py b/qiskit/synthesis/unitary/aqc/cnot_structures.py new file mode 100644 index 000000000000..49ce1c3c7cbb --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/cnot_structures.py @@ -0,0 +1,299 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +These are the CNOT structure methods: anything that you need for creating CNOT structures. +""" +import logging + +import numpy as np + +_NETWORK_LAYOUTS = ["sequ", "spin", "cart", "cyclic_spin", "cyclic_line"] +_CONNECTIVITY_TYPES = ["full", "line", "star"] + + +logger = logging.getLogger(__name__) + + +def _lower_limit(num_qubits: int) -> int: + """ + Returns lower limit on the number of CNOT units that guarantees exact representation of + a unitary operator by quantum gates. + + Args: + num_qubits: number of qubits. + + Returns: + lower limit on the number of CNOT units. + """ + num_cnots = round(np.ceil((4**num_qubits - 3 * num_qubits - 1) / 4.0)) + return num_cnots + + +def make_cnot_network( + num_qubits: int, + network_layout: str = "spin", + connectivity_type: str = "full", + depth: int = 0, +) -> np.ndarray: + """ + Generates a network consisting of building blocks each containing a CNOT gate and possibly some + single-qubit ones. This network models a quantum operator in question. Note, each building + block has 2 input and outputs corresponding to a pair of qubits. What we actually return here + is a chain of indices of qubit pairs shared by every building block in a row. + + Args: + num_qubits: number of qubits. + network_layout: type of network geometry, ``{"sequ", "spin", "cart", "cyclic_spin", + "cyclic_line"}``. + connectivity_type: type of inter-qubit connectivity, ``{"full", "line", "star"}``. + depth: depth of the CNOT-network, i.e. the number of layers, where each layer consists of + a single CNOT-block; default value will be selected, if ``L <= 0``. + + Returns: + A matrix of size ``(2, N)`` matrix that defines layers in cnot-network, where ``N`` + is either equal ``L``, or defined by a concrete type of the network. + + Raises: + ValueError: if unsupported type of CNOT-network layout or number of qubits or combination + of parameters are passed. + """ + if num_qubits < 2: + raise ValueError("Number of qubits must be greater or equal to 2") + + if depth <= 0: + new_depth = _lower_limit(num_qubits) + logger.debug( + "Number of CNOT units chosen as the lower limit: %d, got a non-positive value: %d", + new_depth, + depth, + ) + depth = new_depth + + if network_layout == "sequ": + links = _get_connectivity(num_qubits=num_qubits, connectivity=connectivity_type) + return _sequential_network(num_qubits=num_qubits, links=links, depth=depth) + + elif network_layout == "spin": + return _spin_network(num_qubits=num_qubits, depth=depth) + + elif network_layout == "cart": + cnots = _cartan_network(num_qubits=num_qubits) + logger.debug( + "Optimal lower bound: %d; Cartan CNOTs: %d", _lower_limit(num_qubits), cnots.shape[1] + ) + return cnots + + elif network_layout == "cyclic_spin": + if connectivity_type != "full": + raise ValueError(f"'{network_layout}' layout expects 'full' connectivity") + + return _cyclic_spin_network(num_qubits, depth) + + elif network_layout == "cyclic_line": + if connectivity_type != "line": + raise ValueError(f"'{network_layout}' layout expects 'line' connectivity") + + return _cyclic_line_network(num_qubits, depth) + else: + raise ValueError( + f"Unknown type of CNOT-network layout, expects one of {_NETWORK_LAYOUTS}, " + f"got {network_layout}" + ) + + +def _get_connectivity(num_qubits: int, connectivity: str) -> dict: + """ + Generates connectivity structure between qubits. + + Args: + num_qubits: number of qubits. + connectivity: type of connectivity structure, ``{"full", "line", "star"}``. + + Returns: + dictionary of allowed links between qubits. + + Raises: + ValueError: if unsupported type of CNOT-network layout is passed. + """ + if num_qubits == 1: + links = {0: [0]} + + elif connectivity == "full": + # Full connectivity between qubits. + links = {i: list(range(num_qubits)) for i in range(num_qubits)} + + elif connectivity == "line": + # Every qubit is connected to its immediate neighbours only. + links = {i: [i - 1, i, i + 1] for i in range(1, num_qubits - 1)} + + # first qubit + links[0] = [0, 1] + + # last qubit + links[num_qubits - 1] = [num_qubits - 2, num_qubits - 1] + + elif connectivity == "star": + # Every qubit is connected to the first one only. + links = {i: [0, i] for i in range(1, num_qubits)} + + # first qubit + links[0] = list(range(num_qubits)) + + else: + raise ValueError( + f"Unknown connectivity type, expects one of {_CONNECTIVITY_TYPES}, got {connectivity}" + ) + return links + + +def _sequential_network(num_qubits: int, links: dict, depth: int) -> np.ndarray: + """ + Generates a sequential network. + + Args: + num_qubits: number of qubits. + links: dictionary of connectivity links. + depth: depth of the network (number of layers of building blocks). + + Returns: + A matrix of ``(2, N)`` that defines layers in qubit network. + """ + layer = 0 + cnots = np.zeros((2, depth), dtype=int) + while True: + for i in range(0, num_qubits - 1): + for j in range(i + 1, num_qubits): + if j in links[i]: + cnots[0, layer] = i + cnots[1, layer] = j + layer += 1 + if layer >= depth: + return cnots + + +def _spin_network(num_qubits: int, depth: int) -> np.ndarray: + """ + Generates a spin-like network. + + Args: + num_qubits: number of qubits. + depth: depth of the network (number of layers of building blocks). + + Returns: + A matrix of size ``2 x L`` that defines layers in qubit network. + """ + layer = 0 + cnots = np.zeros((2, depth), dtype=int) + while True: + for i in range(0, num_qubits - 1, 2): + cnots[0, layer] = i + cnots[1, layer] = i + 1 + layer += 1 + if layer >= depth: + return cnots + + for i in range(1, num_qubits - 1, 2): + cnots[0, layer] = i + cnots[1, layer] = i + 1 + layer += 1 + if layer >= depth: + return cnots + + +def _cartan_network(num_qubits: int) -> np.ndarray: + """ + Cartan decomposition in a recursive way, starting from n = 3. + + Args: + num_qubits: number of qubits. + + Returns: + 2xN matrix that defines layers in qubit network, where N is the + depth of Cartan decomposition. + + Raises: + ValueError: if number of qubits is less than 3. + """ + n = num_qubits + if n > 3: + cnots = np.asarray([[0, 0, 0], [1, 1, 1]]) + mult = np.asarray([[n - 2, n - 3, n - 2, n - 3], [n - 1, n - 1, n - 1, n - 1]]) + for _ in range(n - 2): + cnots = np.hstack((np.tile(np.hstack((cnots, mult)), 3), cnots)) + mult[0, -1] -= 1 + mult = np.tile(mult, 2) + elif n == 3: + cnots = np.asarray( + [ + [0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], + [1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1], + ] + ) + else: + raise ValueError(f"The number of qubits must be >= 3, got {n}.") + + return cnots + + +def _cyclic_spin_network(num_qubits: int, depth: int) -> np.ndarray: + """ + Same as in the spin-like network, but the first and the last qubits are also connected. + + Args: + num_qubits: number of qubits. + depth: depth of the network (number of layers of building blocks). + + Returns: + A matrix of size ``2 x L`` that defines layers in qubit network. + """ + + cnots = np.zeros((2, depth), dtype=int) + z = 0 + while True: + for i in range(0, num_qubits, 2): + if i + 1 <= num_qubits - 1: + cnots[0, z] = i + cnots[1, z] = i + 1 + z += 1 + if z >= depth: + return cnots + + for i in range(1, num_qubits, 2): + if i + 1 <= num_qubits - 1: + cnots[0, z] = i + cnots[1, z] = i + 1 + z += 1 + elif i == num_qubits - 1: + cnots[0, z] = i + cnots[1, z] = 0 + z += 1 + if z >= depth: + return cnots + + +def _cyclic_line_network(num_qubits: int, depth: int) -> np.ndarray: + """ + Generates a line based CNOT structure. + + Args: + num_qubits: number of qubits. + depth: depth of the network (number of layers of building blocks). + + Returns: + A matrix of size ``2 x L`` that defines layers in qubit network. + """ + + cnots = np.zeros((2, depth), dtype=int) + for i in range(depth): + cnots[0, i] = (i + 0) % num_qubits + cnots[1, i] = (i + 1) % num_qubits + return cnots diff --git a/qiskit/synthesis/unitary/aqc/cnot_unit_circuit.py b/qiskit/synthesis/unitary/aqc/cnot_unit_circuit.py new file mode 100644 index 000000000000..6973ae55d72f --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/cnot_unit_circuit.py @@ -0,0 +1,103 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +This is the Parametric Circuit class: anything that you need for a circuit +to be parametrized and used for approximate compiling optimization. +""" +from __future__ import annotations +from typing import Optional + +import numpy as np + +from .approximate import ApproximateCircuit + + +class CNOTUnitCircuit(ApproximateCircuit): + """A class that represents an approximate circuit based on CNOT unit blocks.""" + + def __init__( + self, + num_qubits: int, + cnots: np.ndarray, + tol: Optional[float] = 0.0, + name: Optional[str] = None, + ) -> None: + """ + Args: + num_qubits: the number of qubits in this circuit. + cnots: an array of dimensions ``(2, L)`` indicating where the CNOT units will be placed. + tol: angle parameter less or equal this (small) value is considered equal zero and + corresponding gate is not inserted into the output circuit (because it becomes + identity one in this case). + name: name of this circuit + + Raises: + ValueError: if an unsupported parameter is passed. + """ + super().__init__(num_qubits=num_qubits, name=name) + + if cnots.ndim != 2 or cnots.shape[0] != 2: + raise ValueError("CNOT structure must be defined as an array of the size (2, N)") + + self._cnots = cnots + self._num_cnots = cnots.shape[1] + self._tol = tol + + # Thetas to be optimized by the AQC algorithm + self._thetas: np.ndarray | None = None + + @property + def thetas(self) -> np.ndarray: + """ + Returns a vector of rotation angles used by CNOT units in this circuit. + + Returns: + Parameters of the rotation gates in this circuit. + """ + return self._thetas + + def build(self, thetas: np.ndarray) -> None: + """ + Constructs a Qiskit quantum circuit out of the parameters (angles) of this circuit. If a + parameter value is less in absolute value than the specified tolerance then the + corresponding rotation gate will be skipped in the circuit. + """ + n = self.num_qubits + self._thetas = thetas + cnots = self._cnots + + for k in range(n): + # add initial three rotation gates for each qubit + p = 4 * self._num_cnots + 3 * k + k = n - k - 1 + if np.abs(thetas[2 + p]) > self._tol: + self.rz(thetas[2 + p], k) + if np.abs(thetas[1 + p]) > self._tol: + self.ry(thetas[1 + p], k) + if np.abs(thetas[0 + p]) > self._tol: + self.rz(thetas[0 + p], k) + + for c in range(self._num_cnots): + p = 4 * c + # Extract where the CNOT goes + q1 = n - 1 - int(cnots[0, c]) + q2 = n - 1 - int(cnots[1, c]) + # Construct a CNOT unit + self.cx(q1, q2) + if np.abs(thetas[0 + p]) > self._tol: + self.ry(thetas[0 + p], q1) + if np.abs(thetas[1 + p]) > self._tol: + self.rz(thetas[1 + p], q1) + if np.abs(thetas[2 + p]) > self._tol: + self.ry(thetas[2 + p], q2) + if np.abs(thetas[3 + p]) > self._tol: + self.rx(thetas[3 + p], q2) diff --git a/qiskit/synthesis/unitary/aqc/cnot_unit_objective.py b/qiskit/synthesis/unitary/aqc/cnot_unit_objective.py new file mode 100644 index 000000000000..b8bcd6ea9abd --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/cnot_unit_objective.py @@ -0,0 +1,299 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +A definition of the approximate circuit compilation optimization problem based on CNOT unit +definition. +""" +from __future__ import annotations +import typing +from abc import ABC + +import numpy as np +from numpy import linalg as la + +from .approximate import ApproximatingObjective +from .elementary_operations import ry_matrix, rz_matrix, place_unitary, place_cnot, rx_matrix + + +class CNOTUnitObjective(ApproximatingObjective, ABC): + """ + A base class for a problem definition based on CNOT unit. This class may have different + subclasses for objective and gradient computations. + """ + + def __init__(self, num_qubits: int, cnots: np.ndarray) -> None: + """ + Args: + num_qubits: number of qubits. + cnots: a CNOT structure to be used in the optimization procedure. + """ + super().__init__() + self._num_qubits = num_qubits + self._cnots = cnots + self._num_cnots = cnots.shape[1] + + @property + def num_cnots(self): + """ + Returns: + A number of CNOT units to be used by the approximate circuit. + """ + return self._num_cnots + + @property + def num_thetas(self): + """ + Returns: + Number of parameters (angles) of rotation gates in this circuit. + """ + return 3 * self._num_qubits + 4 * self._num_cnots + + +class DefaultCNOTUnitObjective(CNOTUnitObjective): + """A naive implementation of the objective function based on CNOT units.""" + + def __init__(self, num_qubits: int, cnots: np.ndarray) -> None: + """ + Args: + num_qubits: number of qubits. + cnots: a CNOT structure to be used in the optimization procedure. + """ + super().__init__(num_qubits, cnots) + + # last objective computations to be re-used by gradient + self._last_thetas: np.ndarray | None = None + self._cnot_right_collection: np.ndarray | None = None + self._cnot_left_collection: np.ndarray | None = None + self._rotation_matrix: int | np.ndarray | None = None + self._cnot_matrix: np.ndarray | None = None + + def objective(self, param_values: np.ndarray) -> typing.SupportsFloat: + # rename parameters just to make shorter and make use of our dictionary + thetas = param_values + n = self._num_qubits + d = int(2**n) + cnots = self._cnots + num_cnots = self.num_cnots + + # to save intermediate computations we define the following matrices + # this is the collection of cnot unit matrices ordered from left to + # right as in the circuit, not matrix product + cnot_unit_collection = np.zeros((d, d * num_cnots), dtype=complex) + # this is the collection of matrix products of the cnot units up + # to the given position from the right of the circuit + cnot_right_collection = np.zeros((d, d * num_cnots), dtype=complex) + # this is the collection of matrix products of the cnot units up + # to the given position from the left of the circuit + cnot_left_collection = np.zeros((d, d * num_cnots), dtype=complex) + # first, we construct each cnot unit matrix + for cnot_index in range(num_cnots): + theta_index = 4 * cnot_index + + # cnot qubit indices for the cnot unit identified by cnot_index + q1 = int(cnots[0, cnot_index]) + q2 = int(cnots[1, cnot_index]) + + # rotations that are applied on the q1 qubit + ry1 = ry_matrix(thetas[0 + theta_index]) + rz1 = rz_matrix(thetas[1 + theta_index]) + + # rotations that are applied on the q2 qubit + ry2 = ry_matrix(thetas[2 + theta_index]) + rx2 = rx_matrix(thetas[3 + theta_index]) + + # combine the rotations on qubits q1 and q2 + single_q1 = np.dot(rz1, ry1) + single_q2 = np.dot(rx2, ry2) + + # we place single qubit matrices at the corresponding locations in the (2^n, 2^n) matrix + full_q1 = place_unitary(single_q1, n, q1) + full_q2 = place_unitary(single_q2, n, q2) + + # we place a cnot matrix at the qubits q1 and q2 in the full matrix + cnot_q1q2 = place_cnot(n, q1, q2) + + # compute the cnot unit matrix and store in cnot_unit_collection + cnot_unit_collection[:, d * cnot_index : d * (cnot_index + 1)] = la.multi_dot( + [full_q2, full_q1, cnot_q1q2] + ) + + # this is the matrix corresponding to the intermediate matrix products + # it will end up being the matrix product of all the cnot unit matrices + # first we multiply from the right-hand side of the circuit + cnot_matrix = np.eye(d) + for cnot_index in range(num_cnots - 1, -1, -1): + cnot_matrix = np.dot( + cnot_matrix, cnot_unit_collection[:, d * cnot_index : d * (cnot_index + 1)] + ) + cnot_right_collection[:, d * cnot_index : d * (cnot_index + 1)] = cnot_matrix + # now we multiply from the left-hand side of the circuit + cnot_matrix = np.eye(d) + for cnot_index in range(num_cnots): + cnot_matrix = np.dot( + cnot_unit_collection[:, d * cnot_index : d * (cnot_index + 1)], cnot_matrix + ) + cnot_left_collection[:, d * cnot_index : d * (cnot_index + 1)] = cnot_matrix + + # this is the matrix corresponding to the initial rotations + # we start with 1 and kronecker product each qubit's rotations + rotation_matrix: int | np.ndarray = 1 + for q in range(n): + theta_index = 4 * num_cnots + 3 * q + rz0 = rz_matrix(thetas[0 + theta_index]) + ry1 = ry_matrix(thetas[1 + theta_index]) + rz2 = rz_matrix(thetas[2 + theta_index]) + rotation_matrix = np.kron(rotation_matrix, la.multi_dot([rz0, ry1, rz2])) + + # the matrix corresponding to the full circuit is the cnot part and + # rotation part multiplied together + circuit_matrix = np.dot(cnot_matrix, rotation_matrix) + + # compute error + error = 0.5 * (la.norm(circuit_matrix - self._target_matrix, "fro") ** 2) + + # cache computations for gradient + self._last_thetas = thetas + self._cnot_left_collection = cnot_left_collection + self._cnot_right_collection = cnot_right_collection + self._rotation_matrix = rotation_matrix + self._cnot_matrix = cnot_matrix + + return error + + def gradient(self, param_values: np.ndarray) -> np.ndarray: + # just to make shorter + thetas = param_values + # if given thetas are the same as used at the previous objective computations, then + # we re-use computations, otherwise we have to re-compute objective + if not np.all(np.isclose(thetas, self._last_thetas)): + self.objective(thetas) + + # the partial derivative of the circuit with respect to an angle + # is the same circuit with the corresponding pauli gate, multiplied + # by a global phase of -1j / 2, next to the rotation gate (it commutes) + pauli_x = np.multiply(-1j / 2, np.asarray([[0, 1], [1, 0]])) + pauli_y = np.multiply(-1j / 2, np.asarray([[0, -1j], [1j, 0]])) + pauli_z = np.multiply(-1j / 2, np.asarray([[1, 0], [0, -1]])) + + n = self._num_qubits + d = int(2**n) + cnots = self._cnots + num_cnots = self.num_cnots + + # the partial derivative of the cost function is -Re + # where V' is the partial derivative of the circuit + # first we compute the partial derivatives in the cnot part + der = np.zeros(4 * num_cnots + 3 * n) + for cnot_index in range(num_cnots): + theta_index = 4 * cnot_index + + # cnot qubit indices for the cnot unit identified by cnot_index + q1 = int(cnots[0, cnot_index]) + q2 = int(cnots[1, cnot_index]) + + # rotations that are applied on the q1 qubit + ry1 = ry_matrix(thetas[0 + theta_index]) + rz1 = rz_matrix(thetas[1 + theta_index]) + + # rotations that are applied on the q2 qubit + ry2 = ry_matrix(thetas[2 + theta_index]) + rx2 = rx_matrix(thetas[3 + theta_index]) + + # combine the rotations on qubits q1 and q2 + # note we have to insert an extra pauli gate to take the derivative + # of the appropriate rotation gate + for i in range(4): + if i == 0: + single_q1 = la.multi_dot([rz1, pauli_y, ry1]) + single_q2 = np.dot(rx2, ry2) + elif i == 1: + single_q1 = la.multi_dot([pauli_z, rz1, ry1]) + single_q2 = np.dot(rx2, ry2) + elif i == 2: + single_q1 = np.dot(rz1, ry1) + single_q2 = la.multi_dot([rx2, pauli_y, ry2]) + else: + single_q1 = np.dot(rz1, ry1) + single_q2 = la.multi_dot([pauli_x, rx2, ry2]) + + # we place single qubit matrices at the corresponding locations in + # the (2^n, 2^n) matrix + full_q1 = place_unitary(single_q1, n, q1) + full_q2 = place_unitary(single_q2, n, q2) + + # we place a cnot matrix at the qubits q1 and q2 in the full matrix + cnot_q1q2 = place_cnot(n, q1, q2) + + # partial derivative of that particular cnot unit, size of (2^n, 2^n) + der_cnot_unit = la.multi_dot([full_q2, full_q1, cnot_q1q2]) + # der_cnot_unit is multiplied by the matrix product of cnot units to the left + # of it (if there are any) and to the right of it (if there are any) + if cnot_index == 0: + der_cnot_matrix = np.dot( + self._cnot_right_collection[:, d : 2 * d], + der_cnot_unit, + ) + elif num_cnots - 1 == cnot_index: + der_cnot_matrix = np.dot( + der_cnot_unit, + self._cnot_left_collection[:, d * (num_cnots - 2) : d * (num_cnots - 1)], + ) + else: + der_cnot_matrix = la.multi_dot( + [ + self._cnot_right_collection[ + :, d * (cnot_index + 1) : d * (cnot_index + 2) + ], + der_cnot_unit, + self._cnot_left_collection[:, d * (cnot_index - 1) : d * cnot_index], + ] + ) + + # the matrix corresponding to the full circuit partial derivative + # is the partial derivative of the cnot part multiplied by the usual + # rotation part + der_circuit_matrix = np.dot(der_cnot_matrix, self._rotation_matrix) + # we compute the partial derivative of the cost function + der[i + theta_index] = -np.real( + np.trace(np.dot(der_circuit_matrix.conj().T, self._target_matrix)) + ) + + # now we compute the partial derivatives in the rotation part + # we start with 1 and kronecker product each qubit's rotations + for i in range(3 * n): + der_rotation_matrix: int | np.ndarray = 1 + for q in range(n): + theta_index = 4 * num_cnots + 3 * q + rz0 = rz_matrix(thetas[0 + theta_index]) + ry1 = ry_matrix(thetas[1 + theta_index]) + rz2 = rz_matrix(thetas[2 + theta_index]) + # for the appropriate rotation gate that we are taking + # the partial derivative of, we have to insert the + # corresponding pauli matrix + if i - 3 * q == 0: + rz0 = np.dot(pauli_z, rz0) + elif i - 3 * q == 1: + ry1 = np.dot(pauli_y, ry1) + elif i - 3 * q == 2: + rz2 = np.dot(pauli_z, rz2) + der_rotation_matrix = np.kron(der_rotation_matrix, la.multi_dot([rz0, ry1, rz2])) + + # the matrix corresponding to the full circuit partial derivative + # is the usual cnot part multiplied by the partial derivative of + # the rotation part + der_circuit_matrix = np.dot(self._cnot_matrix, der_rotation_matrix) + # we compute the partial derivative of the cost function + der[4 * num_cnots + i] = -np.real( + np.trace(np.dot(der_circuit_matrix.conj().T, self._target_matrix)) + ) + + return der diff --git a/qiskit/synthesis/unitary/aqc/elementary_operations.py b/qiskit/synthesis/unitary/aqc/elementary_operations.py new file mode 100644 index 000000000000..b5739267e793 --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/elementary_operations.py @@ -0,0 +1,108 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +These are a number of elementary functions that are required for the AQC routines to work. +""" + +import numpy as np + +from qiskit.circuit.library import RXGate, RZGate, RYGate + + +def place_unitary(unitary: np.ndarray, n: int, j: int) -> np.ndarray: + """ + Computes I(j - 1) tensor product U tensor product I(n - j), where U is a unitary matrix + of size ``(2, 2)``. + + Args: + unitary: a unitary matrix of size ``(2, 2)``. + n: num qubits. + j: position where to place a unitary. + + Returns: + a unitary of n qubits with u in position j. + """ + return np.kron(np.kron(np.eye(2**j), unitary), np.eye(2 ** (n - 1 - j))) + + +def place_cnot(n: int, j: int, k: int) -> np.ndarray: + """ + Places a CNOT from j to k. + + Args: + n: number of qubits. + j: control qubit. + k: target qubit. + + Returns: + a unitary of n qubits with CNOT placed at ``j`` and ``k``. + """ + if j < k: + unitary = np.kron( + np.kron(np.eye(2**j), [[1, 0], [0, 0]]), np.eye(2 ** (n - 1 - j)) + ) + np.kron( + np.kron( + np.kron(np.kron(np.eye(2**j), [[0, 0], [0, 1]]), np.eye(2 ** (k - j - 1))), + [[0, 1], [1, 0]], + ), + np.eye(2 ** (n - 1 - k)), + ) + else: + unitary = np.kron( + np.kron(np.eye(2**j), [[1, 0], [0, 0]]), np.eye(2 ** (n - 1 - j)) + ) + np.kron( + np.kron( + np.kron(np.kron(np.eye(2**k), [[0, 1], [1, 0]]), np.eye(2 ** (j - k - 1))), + [[0, 0], [0, 1]], + ), + np.eye(2 ** (n - 1 - j)), + ) + return unitary + + +def rx_matrix(phi: float) -> np.ndarray: + """ + Computes an RX rotation by the angle of ``phi``. + + Args: + phi: rotation angle. + + Returns: + an RX rotation matrix. + """ + return RXGate(phi).to_matrix() + + +def ry_matrix(phi: float) -> np.ndarray: + """ + Computes an RY rotation by the angle of ``phi``. + + Args: + phi: rotation angle. + + Returns: + an RY rotation matrix. + """ + return RYGate(phi).to_matrix() + + +def rz_matrix(phi: float) -> np.ndarray: + """ + Computes an RZ rotation by the angle of ``phi``. + + Args: + phi: rotation angle. + + Returns: + an RZ rotation matrix. + """ + return RZGate(phi).to_matrix() diff --git a/qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py b/qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py new file mode 100644 index 000000000000..df13193f12bb --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py @@ -0,0 +1,164 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +r""" +================================================================================ +Fast implementation of objective function class +(:mod:`qiskit.transpiler.synthesis.aqc.fast_gradient`) +================================================================================ + +.. currentmodule:: qiskit.transpiler.synthesis.aqc.fast_gradient + +Extension to the implementation of Approximate Quantum Compiler as described in the paper [1]. + +Interface +========= + +The main public class of this module is FastCNOTUnitObjective. It replaces the default objective +function implementation :class:`.DefaultCNOTUnitObjective` for faster computation. +The individual classes include the public one (FastCNOTUnitObjective) and few +internal ones: + +.. autosummary:: + :toctree: ../stubs + :template: autosummary/class_no_inherited_members.rst + + FastCNOTUnitObjective + LayerBase + Layer1Q + Layer2Q + PMatrix + + +Mathematical Details +==================== + +In what follows we briefly outline the main ideas underlying the accelerated implementation +of objective function class. + +* The key ingredient of approximate compiling is the efficient optimization procedure + that minimizes :math:`\|V - U\|_{\mathrm{F}}` on a classical computer, where :math:`U` + is a given (target) unitary matrix and :math:`V` is a matrix of approximating quantum + circuit. Alternatively, we maximize the Hilbert-Schmidt product between :math:`U` and + :math:`V` as outlined in the main part of the documentation. + +* The circuit :math:`V` can be represented as a sequence of 2-qubit gates (layers) + applied one after another. The corresponding matrix takes the form: + :math:`V = C_0 C_1 \ldots C_{L-1} F`, where :math:`L` is the length of the sequence + (number of layers). If the total number of qubits :math:`n > 2`, every + :math:`C_i = C_i(\Theta_i)` is a sparse, :math:`2^n \times 2^n` matrix of 2-qubit gate + (CNOT unit block) parameterized by a sub-set of parameters :math:`\Theta_i` + (4 parameters per unit block), and :math:`F` is a matrix that comprises the action + of all 1-qubit gates in front of approximating circuit. See the paper [1] for details. + +* Over the course of optimization we compute the value of objective function and its + gradient, which implies computation of :math:`V` and its derivatives + :math:`{\partial V}/{\partial \Theta_i}` for all :math:`i`, given the current estimation + of all the parameters :math:`\Theta`. + +* A naive implementation of the product :math:`V = C_0 C_1 \ldots C_{L-1} F` and its + derivatives would include computation and memorization of forward and backward partial + products as required by the backtracking algorithm. This is wasteful in terms of + performance and resource allocation. + +* Minimization of :math:`\|V - U\|_{\mathrm{F}}^2` is equivalent to maximization of + :math:`\text{Re}\left(\text{Tr}\left(U^{\dagger} V\right)\right)`. By cyclic permutation + of the sequence of matrices under trace operation, we can avoid memorization of intermediate + partial products of gate matrices :math:`C_i`. Note, matrix size grows exponentially with + the number of qubits, quickly becoming prohibitively large. + +* Sparse structure of :math:`C_i` can be exploited to speed up matrix-matrix multiplication. + However, using sparse matrices as such does not give performance gain because sparse patterns + tend to violate data proximity inside the cache memory of modern CPUs. Instead, we make use + of special structure of gate matrices :math:`C_i` coupled with permutation ones. Although + permutation is not cache friendly either, its impact is seemingly less severe than that + of sparse matrix multiplication (at least in Python implementation). + +* On every optimization iteration we, first, compute :math:`V = C_0 C_1 \ldots C_{L-1} F` + given the current estimation of all the parameters :math:`\Theta`. + +* As for the gradient of objective function, it can be shown (by moving cyclically around + an individual matrices under trace operation) that: + +.. math:: + \text{Tr}\left( U^{\dagger} \frac{\partial V}{\partial \Theta_{l,k}} \right) = + \langle \text{vec}\left(E_l\right), \text{vec}\left( + \frac{\partial C_l}{\partial \Theta_{l,k}}\right) \rangle, + +where :math:`\Theta_{l,k}` is a :math:`k`-th parameter of :math:`l`-th CNOT unit block, +and :math:`E_l=C_{l-1}\left(C_{l-2}\left(\cdots\left(C_0\left(U^{\dagger}V +C_0^{\dagger}\right)C_1^{\dagger}\right) \cdots\right)C_{l-1}^{\dagger}\right)C_l^{\dagger}` +is an intermediate matrix. + +* For every :math:`l`-th gradient component, we compute the trace using the matrix + :math:`E_l`, then this matrix is updated by multiplication on left and on the right + by corresponding gate matrices :math:`C_l` and :math:`C_{l+1}^{\dagger}` respectively + and proceed to the next gradient component. + +* We save computations and resources by not storing intermediate partial products of + :math:`C_i`. Instead, incrementally updated matrix :math:`E_l` keeps all related + information. Also, vectorization of involved matrices (see the above formula) allows + us to replace matrix-matrix multiplication by "cheaper" vector-vector one under the + trace operation. + +* The matrices :math:`C_i` are sparse. However, even for relatively small matrices + (< 1M elements) sparse-dense multiplication can be very slow. Construction of sparse + matrices takes a time as well. We should update every gate matrix on each iteration + of optimization loop. + +* In fact, any gate matrix :math:`C_i` can be transformed to what we call a standard + form: :math:`C_i = P^T \widetilde{C}_i P`, where :math:`P` is an easily computable + permutation matrix and :math:`\widetilde{C}_i` has a block-diagonal layout: + +.. math:: + \widetilde{C}_i = \left( + \begin{array}{ccc} + G_{4 \times 4} & \ddots & 0 \\ + \ddots & \ddots & \ddots \\ + 0 & \ddots & G_{4 \times 4} + \end{array} + \right) + +* The 2-qubit gate matrix :math:`G_{4 \times 4}` is repeated along diagonal of the full + :math:`2^n \times 2^n` :math:`\widetilde{C}_i`. + +* We do not actually create neither matrix :math:`\widetilde{C}_i` nor :math:`P`. + In fact, only :math:`G_{4 \times 4}` and a permutation array (of size :math:`2^n`) + are kept in memory. + +* Consider left-hand side multiplication by some dense, :math:`2^n \times 2^n` matrix :math:`M`: + +.. math:: + C_i M = P^T \widetilde{C}_i P M = P^T \left( \widetilde{C}_i \left( P M \right) \right) + +* First, we permute rows of :math:`M`, which is equivalent to the product :math:`P M`, but + without expensive multiplication of two :math:`2^n \times 2^n` matrices. + +* Second, we compute :math:`\widetilde{C}_i P M` multiplying every block-diagonal sub-matrix + :math:`G_{4 \times 4}` by the corresponding rows of :math:`P M`. This is the dense-dense + matrix multiplication, which is very well optimized on modern CPUs. Important: the total + number of operations is :math:`O(2^{2 n})` in contrast to :math:`O(2^{3 n})` as in general + case. + +* Third, we permute rows of :math:`\widetilde{C}_i P M` by applying :math:`P^T`. + +* Right-hand side multiplication is done in a similar way. + +* In summary, we save computational resources by exploiting some properties of 2-qubit gate + matrices :math:`C_i` and using hardware optimized multiplication of dense matrices. There + is still a room for further improvement, of course. + +References: + + [1]: Liam Madden, Andrea Simonetto, Best Approximate Quantum Compiling Problems. + `arXiv:2106.05649 `_ +""" diff --git a/qiskit/synthesis/unitary/aqc/fast_gradient/fast_grad_utils.py b/qiskit/synthesis/unitary/aqc/fast_gradient/fast_grad_utils.py new file mode 100644 index 000000000000..b13c96ea3fe5 --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/fast_gradient/fast_grad_utils.py @@ -0,0 +1,237 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Utility functions in the fast gradient implementation. +""" +from __future__ import annotations +from typing import Union +import numpy as np + + +def is_permutation(x: np.ndarray) -> bool: + """ + Checks if array is really an index permutation. + + Args: + 1D-array of integers that supposedly represents a permutation. + + Returns: + True, if array is really a permutation of indices. + """ + return ( + isinstance(x, np.ndarray) + and x.ndim == 1 + and x.dtype == np.int64 + and np.all(np.sort(x) == np.arange(x.size, dtype=np.int64)) + ) + + +def reverse_bits(x: Union[int, np.ndarray], nbits: int, enable: bool) -> Union[int, np.ndarray]: + """ + Reverses the bit order in a number of ``nbits`` length. + If ``x`` is an array, then operation is applied to every entry. + + Args: + x: either a single integer or an array of integers. + nbits: number of meaningful bits in the number x. + enable: apply reverse operation, if enabled, otherwise leave unchanged. + + Returns: + a number or array of numbers with reversed bits. + """ + + if not enable: + if isinstance(x, int): + pass + else: + x = x.copy() + return x + + if isinstance(x, int): + res: int | np.ndarray = int(0) + else: + x = x.copy() + res = np.full_like(x, fill_value=0) + + for _ in range(nbits): + res <<= 1 + res |= x & 1 + x >>= 1 + return res + + +def swap_bits(num: int, a: int, b: int) -> int: + """ + Swaps the bits at positions 'a' and 'b' in the number 'num'. + + Args: + num: an integer number where bits should be swapped. + a: index of the first bit to be swapped. + b: index of the second bit to be swapped. + + Returns: + the number with swapped bits. + """ + x = ((num >> a) ^ (num >> b)) & 1 + return num ^ ((x << a) | (x << b)) + + +def bit_permutation_1q(n: int, k: int) -> np.ndarray: + """ + Constructs index permutation that brings a circuit consisting of a single + 1-qubit gate to "standard form": ``kron(I(2^n/2), G)``, as we call it. Here n + is the number of qubits, ``G`` is a 2x2 gate matrix, ``I(2^n/2)`` is the identity + matrix of size ``(2^n/2)x(2^n/2)``, and the full size of the circuit matrix is + ``(2^n)x(2^n)``. Circuit matrix in standard form becomes block-diagonal (with + sub-matrices ``G`` on the main diagonal). Multiplication of such a matrix and + a dense one is much faster than generic dense-dense product. Moreover, + we do not need to keep the entire circuit matrix in memory but just 2x2 ``G`` + one. This saves a lot of memory when the number of qubits is large. + + Args: + n: number of qubits. + k: index of qubit where single 1-qubit gate is applied. + + Returns: + permutation that brings the whole layer to the standard form. + """ + perm = np.arange(2**n, dtype=np.int64) + if k != n - 1: + for v in range(2**n): + perm[v] = swap_bits(v, k, n - 1) + return perm + + +def bit_permutation_2q(n: int, j: int, k: int) -> np.ndarray: + """ + Constructs index permutation that brings a circuit consisting of a single + 2-qubit gate to "standard form": ``kron(I(2^n/4), G)``, as we call it. Here ``n`` + is the number of qubits, ``G`` is a 4x4 gate matrix, ``I(2^n/4)`` is the identity + matrix of size ``(2^n/4)x(2^n/4)``, and the full size of the circuit matrix is + ``(2^n)x(2^n)``. Circuit matrix in standard form becomes block-diagonal (with + sub-matrices ``G`` on the main diagonal). Multiplication of such a matrix and + a dense one is much faster than generic dense-dense product. Moreover, + we do not need to keep the entire circuit matrix in memory but just 4x4 ``G`` + one. This saves a lot of memory when the number of qubits is large. + + Args: + n: number of qubits. + j: index of control qubit where single 2-qubit gate is applied. + k: index of target qubit where single 2-qubit gate is applied. + + Returns: + permutation that brings the whole layer to the standard form. + """ + dim = 2**n + perm = np.arange(dim, dtype=np.int64) + if j < n - 2: + if k < n - 2: + for v in range(dim): + perm[v] = swap_bits(swap_bits(v, j, n - 2), k, n - 1) + elif k == n - 2: + for v in range(dim): + perm[v] = swap_bits(swap_bits(v, n - 2, n - 1), j, n - 2) + else: + for v in range(dim): + perm[v] = swap_bits(v, j, n - 2) + elif j == n - 2: + if k < n - 2: + for v in range(dim): + perm[v] = swap_bits(v, k, n - 1) + else: + pass + else: + if k < n - 2: + for v in range(dim): + perm[v] = swap_bits(swap_bits(v, n - 2, n - 1), k, n - 1) + else: + for v in range(dim): + perm[v] = swap_bits(v, n - 2, n - 1) + return perm + + +def inverse_permutation(perm: np.ndarray) -> np.ndarray: + """ + Returns inverse permutation. + + Args: + perm: permutation to be reversed. + + Returns: + inverse permutation. + """ + inv = np.zeros_like(perm) + inv[perm] = np.arange(perm.size, dtype=np.int64) + return inv + + +def make_rx(phi: float, out: np.ndarray) -> np.ndarray: + """ + Makes a 2x2 matrix that corresponds to X-rotation gate. + This is a fast implementation that does not allocate the output matrix. + + Args: + phi: rotation angle. + out: placeholder for the result (2x2, complex-valued matrix). + + Returns: + rotation gate, same object as referenced by "out". + """ + a = 0.5 * phi + cs, sn = np.cos(a).item(), -1j * np.sin(a).item() + out[0, 0] = cs + out[0, 1] = sn + out[1, 0] = sn + out[1, 1] = cs + return out + + +def make_ry(phi: float, out: np.ndarray) -> np.ndarray: + """ + Makes a 2x2 matrix that corresponds to Y-rotation gate. + This is a fast implementation that does not allocate the output matrix. + + Args: + phi: rotation angle. + out: placeholder for the result (2x2, complex-valued matrix). + + Returns: + rotation gate, same object as referenced by "out". + """ + a = 0.5 * phi + cs, sn = np.cos(a).item(), np.sin(a).item() + out[0, 0] = cs + out[0, 1] = -sn + out[1, 0] = sn + out[1, 1] = cs + return out + + +def make_rz(phi: float, out: np.ndarray) -> np.ndarray: + """ + Makes a 2x2 matrix that corresponds to Z-rotation gate. + This is a fast implementation that does not allocate the output matrix. + + Args: + phi: rotation angle. + out: placeholder for the result (2x2, complex-valued matrix). + + Returns: + rotation gate, same object as referenced by "out". + """ + exp = np.exp(0.5j * phi).item() + out[0, 0] = 1.0 / exp + out[0, 1] = 0 + out[1, 0] = 0 + out[1, 1] = exp + return out diff --git a/qiskit/synthesis/unitary/aqc/fast_gradient/fast_gradient.py b/qiskit/synthesis/unitary/aqc/fast_gradient/fast_gradient.py new file mode 100644 index 000000000000..dc5078acf1a9 --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/fast_gradient/fast_gradient.py @@ -0,0 +1,225 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Implementation of the fast objective function class. +""" + +import warnings +import numpy as np + +from .layer import ( + LayerBase, + Layer2Q, + Layer1Q, + init_layer2q_matrices, + init_layer2q_deriv_matrices, + init_layer1q_matrices, + init_layer1q_deriv_matrices, +) +from .pmatrix import PMatrix +from ..cnot_unit_objective import CNOTUnitObjective + + +class FastCNOTUnitObjective(CNOTUnitObjective): + """ + Implementation of objective function and gradient calculator, which is + similar to + :class:`~qiskit.transpiler.aqc.DefaultCNOTUnitObjective` + but several times faster. + """ + + def __init__(self, num_qubits: int, cnots: np.ndarray): + super().__init__(num_qubits, cnots) + + if not 2 <= num_qubits <= 16: + raise ValueError("expects number of qubits in the range [2..16]") + + dim = 2**num_qubits + self._ucf_mat = PMatrix(num_qubits) # U^dagger @ C @ F + self._fuc_mat = PMatrix(num_qubits) # F @ U^dagger @ C + self._circ_thetas = np.zeros((self.num_thetas,)) # last thetas used + + # array of C-layers: + self._c_layers = np.asarray([object()] * self.num_cnots, dtype=LayerBase) + # array of F-layers: + self._f_layers = np.asarray([object()] * num_qubits, dtype=LayerBase) + # 4x4 C-gate matrices: + self._c_gates = np.full((self.num_cnots, 4, 4), fill_value=0, dtype=np.complex128) + # derivatives of 4x4 C-gate matrices: + self._c_dervs = np.full((self.num_cnots, 4, 4, 4), fill_value=0, dtype=np.complex128) + # 4x4 F-gate matrices: + self._f_gates = np.full((num_qubits, 2, 2), fill_value=0, dtype=np.complex128) + # derivatives of 4x4 F-gate matrices: + self._f_dervs = np.full((num_qubits, 3, 2, 2), fill_value=0, dtype=np.complex128) + # temporary NxN matrices: + self._tmp1 = np.full((dim, dim), fill_value=0, dtype=np.complex128) + self._tmp2 = np.full((dim, dim), fill_value=0, dtype=np.complex128) + + # Create layers of 2-qubit gates. + for q in range(self.num_cnots): + j = int(cnots[0, q]) + k = int(cnots[1, q]) + self._c_layers[q] = Layer2Q(num_qubits=num_qubits, j=j, k=k) + + # Create layers of 1-qubit gates. + for k in range(num_qubits): + self._f_layers[k] = Layer1Q(num_qubits=num_qubits, k=k) + + def objective(self, param_values: np.ndarray) -> float: + """ + Computes the objective function and some intermediate data for + the subsequent gradient computation. + See description of the base class method. + """ + depth, n = self.num_cnots, self._num_qubits + + # Memorize the last angular parameters used to compute the objective. + if self._circ_thetas.size == 0: + self._circ_thetas = np.zeros((self.num_thetas,)) + np.copyto(self._circ_thetas, param_values) + + thetas4d = param_values[: 4 * depth].reshape(depth, 4) + thetas3n = param_values[4 * depth :].reshape(n, 3) + + init_layer2q_matrices(thetas=thetas4d, dst=self._c_gates) + init_layer2q_deriv_matrices(thetas=thetas4d, dst=self._c_dervs) + init_layer1q_matrices(thetas=thetas3n, dst=self._f_gates) + init_layer1q_deriv_matrices(thetas=thetas3n, dst=self._f_dervs) + + self._init_layers() + self._calc_ucf_fuc() + objective_value = self._calc_objective_function() + return objective_value + + def gradient(self, param_values: np.ndarray) -> np.ndarray: + """ + Computes the gradient of objective function. + See description of the base class method. + """ + + # If thetas are the same as used for objective value calculation + # before calling this function, then we re-use the computations, + # otherwise we have to re-compute the objective. + tol = float(np.sqrt(np.finfo(float).eps)) + if not np.allclose(param_values, self._circ_thetas, atol=tol, rtol=tol): + self.objective(param_values) + warnings.warn("gradient is computed before the objective") + + grad = np.full((param_values.size,), fill_value=0, dtype=np.float64) + grad4d = grad[: 4 * self.num_cnots].reshape(self.num_cnots, 4) + grad3n = grad[4 * self.num_cnots :].reshape(self._num_qubits, 3) + self._calc_gradient4d(grad4d) + self._calc_gradient3n(grad3n) + return grad + + def _init_layers(self): + """ + Initializes C-layers and F-layers by corresponding gate matrices. + """ + c_gates = self._c_gates + c_layers = self._c_layers + for q in range(self.num_cnots): + c_layers[q].set_from_matrix(mat=c_gates[q]) + + f_gates = self._f_gates + f_layers = self._f_layers + for q in range(self._num_qubits): + f_layers[q].set_from_matrix(mat=f_gates[q]) + + def _calc_ucf_fuc(self): + """ + Computes matrices ``ucf_mat`` and ``fuc_mat``. Both remain non-finalized. + """ + ucf_mat = self._ucf_mat + fuc_mat = self._fuc_mat + tmp1 = self._tmp1 + c_layers = self._c_layers + f_layers = self._f_layers + depth, n = self.num_cnots, self._num_qubits + + # tmp1 = U^dagger. + np.conj(self.target_matrix.T, out=tmp1) + + # ucf_mat = fuc_mat = U^dagger @ C = U^dagger @ C_{depth-1} @ ... @ C_{0}. + self._ucf_mat.set_matrix(tmp1) + for q in range(depth - 1, -1, -1): + ucf_mat.mul_right_q2(c_layers[q], temp_mat=tmp1, dagger=False) + fuc_mat.set_matrix(ucf_mat.finalize(temp_mat=tmp1)) + + # fuc_mat = F @ U^dagger @ C = F_{n-1} @ ... @ F_{0} @ U^dagger @ C. + for q in range(n): + fuc_mat.mul_left_q1(f_layers[q], temp_mat=tmp1) + + # ucf_mat = U^dagger @ C @ F = U^dagger @ C @ F_{n-1} @ ... @ F_{0}. + for q in range(n - 1, -1, -1): + ucf_mat.mul_right_q1(f_layers[q], temp_mat=tmp1, dagger=False) + + def _calc_objective_function(self) -> float: + """ + Computes the value of objective function. + """ + ucf = self._ucf_mat.finalize(temp_mat=self._tmp1) + trace_ucf = np.trace(ucf) + fobj = abs((2**self._num_qubits) - float(np.real(trace_ucf))) + + return fobj + + def _calc_gradient4d(self, grad4d: np.ndarray): + """ + Calculates a part gradient contributed by 2-qubit gates. + """ + fuc = self._fuc_mat + tmp1, tmp2 = self._tmp1, self._tmp2 + c_gates = self._c_gates + c_dervs = self._c_dervs + c_layers = self._c_layers + for q in range(self.num_cnots): + # fuc[q] <-- C[q-1] @ fuc[q-1] @ C[q].conj.T. Note, c_layers[q] has + # been initialized in _init_layers(), however, c_layers[q-1] was + # reused on the previous step, see below, so we need to restore it. + if q > 0: + c_layers[q - 1].set_from_matrix(mat=c_gates[q - 1]) + fuc.mul_left_q2(c_layers[q - 1], temp_mat=tmp1) + fuc.mul_right_q2(c_layers[q], temp_mat=tmp1, dagger=True) + fuc.finalize(temp_mat=tmp1) + # Compute gradient components. We reuse c_layers[q] several times. + for i in range(4): + c_layers[q].set_from_matrix(mat=c_dervs[q, i]) + grad4d[q, i] = (-1) * np.real( + fuc.product_q2(layer=c_layers[q], tmp1=tmp1, tmp2=tmp2) + ) + + def _calc_gradient3n(self, grad3n: np.ndarray): + """ + Calculates a part gradient contributed by 1-qubit gates. + """ + ucf = self._ucf_mat + tmp1, tmp2 = self._tmp1, self._tmp2 + f_gates = self._f_gates + f_dervs = self._f_dervs + f_layers = self._f_layers + for q in range(self._num_qubits): + # ucf[q] <-- F[q-1] @ ucf[q-1] @ F[q].conj.T. Note, f_layers[q] has + # been initialized in _init_layers(), however, f_layers[q-1] was + # reused on the previous step, see below, so we need to restore it. + if q > 0: + f_layers[q - 1].set_from_matrix(mat=f_gates[q - 1]) + ucf.mul_left_q1(f_layers[q - 1], temp_mat=tmp1) + ucf.mul_right_q1(f_layers[q], temp_mat=tmp1, dagger=True) + ucf.finalize(temp_mat=tmp1) + # Compute gradient components. We reuse f_layers[q] several times. + for i in range(3): + f_layers[q].set_from_matrix(mat=f_dervs[q, i]) + grad3n[q, i] = (-1) * np.real( + ucf.product_q1(layer=f_layers[q], tmp1=tmp1, tmp2=tmp2) + ) diff --git a/qiskit/synthesis/unitary/aqc/fast_gradient/layer.py b/qiskit/synthesis/unitary/aqc/fast_gradient/layer.py new file mode 100644 index 000000000000..6cb763afbc4f --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/fast_gradient/layer.py @@ -0,0 +1,370 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Layer classes for the fast gradient implementation. +""" +from __future__ import annotations +from abc import abstractmethod, ABC +from typing import Optional +import numpy as np +from .fast_grad_utils import ( + bit_permutation_1q, + reverse_bits, + inverse_permutation, + bit_permutation_2q, + make_rz, + make_ry, +) + + +class LayerBase(ABC): + """ + Base class for any layer implementation. Each layer here is represented + by a 2x2 or 4x4 gate matrix ``G`` (applied to 1 or 2 qubits respectively) + interleaved with the identity ones: + ``Layer = I kron I kron ... kron G kron ... kron I kron I`` + """ + + @abstractmethod + def set_from_matrix(self, mat: np.ndarray): + """ + Updates this layer from an external gate matrix. + + Args: + mat: external gate matrix that initializes this layer's one. + """ + raise NotImplementedError() + + @abstractmethod + def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Returns gate matrix, direct and inverse permutations. + + Returns: + (1) gate matrix; (2) direct permutation; (3) inverse permutations. + """ + raise NotImplementedError() + + +class Layer1Q(LayerBase): + """ + Layer represents a simple circuit where 1-qubit gate matrix (of size 2x2) + interleaves with the identity ones. + """ + + def __init__(self, num_qubits: int, k: int, g2x2: Optional[np.ndarray] = None): + """ + Args: + num_qubits: number of qubits. + k: index of the bit where gate is applied. + g2x2: 2x2 matrix that makes up this layer along with identity ones, + or None (should be set up later). + """ + super().__init__() + + # 2x2 gate matrix (1-qubit gate). + self._gmat = np.full((2, 2), fill_value=0, dtype=np.complex128) + if isinstance(g2x2, np.ndarray): + np.copyto(self._gmat, g2x2) + + bit_flip = True + dim = 2**num_qubits + row_perm = reverse_bits( + bit_permutation_1q(n=num_qubits, k=k), nbits=num_qubits, enable=bit_flip + ) + col_perm = reverse_bits(np.arange(dim, dtype=np.int64), nbits=num_qubits, enable=bit_flip) + self._perm = np.full((dim,), fill_value=0, dtype=np.int64) + self._perm[row_perm] = col_perm + self._inv_perm = inverse_permutation(self._perm) + + def set_from_matrix(self, mat: np.ndarray): + """See base class description.""" + np.copyto(self._gmat, mat) + + def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """See base class description.""" + return self._gmat, self._perm, self._inv_perm + + +class Layer2Q(LayerBase): + """ + Layer represents a simple circuit where 2-qubit gate matrix (of size 4x4) + interleaves with the identity ones. + """ + + def __init__(self, num_qubits: int, j: int, k: int, g4x4: Optional[np.ndarray] = None): + """ + Args: + num_qubits: number of qubits. + j: index of the first (control) bit. + k: index of the second (target) bit. + g4x4: 4x4 matrix that makes up this layer along with identity ones, + or None (should be set up later). + """ + super().__init__() + + # 4x4 gate matrix (2-qubit gate). + self._gmat = np.full((4, 4), fill_value=0, dtype=np.complex128) + if isinstance(g4x4, np.ndarray): + np.copyto(self._gmat, g4x4) + + bit_flip = True + dim = 2**num_qubits + row_perm = reverse_bits( + bit_permutation_2q(n=num_qubits, j=j, k=k), nbits=num_qubits, enable=bit_flip + ) + col_perm = reverse_bits(np.arange(dim, dtype=np.int64), nbits=num_qubits, enable=bit_flip) + self._perm = np.full((dim,), fill_value=0, dtype=np.int64) + self._perm[row_perm] = col_perm + self._inv_perm = inverse_permutation(self._perm) + + def set_from_matrix(self, mat: np.ndarray): + """See base class description.""" + np.copyto(self._gmat, mat) + + def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """See base class description.""" + return self._gmat, self._perm, self._inv_perm + + +def init_layer1q_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: + """ + Initializes 4x4 matrices of 2-qubit gates defined in the paper. + + Args: + thetas: depth x 4 matrix of gate parameters for every layer, where + "depth" is the number of layers. + dst: destination array of size depth x 4 x 4 that will receive gate + matrices of each layer. + + Returns: + Returns the "dst" array. + """ + n = thetas.shape[0] + tmp = np.full((4, 2, 2), fill_value=0, dtype=np.complex128) + for k in range(n): + th = thetas[k] + a = make_rz(th[0], out=tmp[0]) + b = make_ry(th[1], out=tmp[1]) + c = make_rz(th[2], out=tmp[2]) + np.dot(np.dot(a, b, out=tmp[3]), c, out=dst[k]) + return dst + + +def init_layer1q_deriv_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: + """ + Initializes 4x4 derivative matrices of 2-qubit gates defined in the paper. + + Args: + thetas: depth x 4 matrix of gate parameters for every layer, where + "depth" is the number of layers. + dst: destination array of size depth x 4 x 4 x 4 that will receive gate + derivative matrices of each layer; there are 4 parameters per gate, + hence, 4 derivative matrices per layer. + + Returns: + Returns the "dst" array. + """ + n = thetas.shape[0] + y = np.asarray([[0, -0.5], [0.5, 0]], dtype=np.complex128) + z = np.asarray([[-0.5j, 0], [0, 0.5j]], dtype=np.complex128) + tmp = np.full((5, 2, 2), fill_value=0, dtype=np.complex128) + for k in range(n): + th = thetas[k] + a = make_rz(th[0], out=tmp[0]) + b = make_ry(th[1], out=tmp[1]) + c = make_rz(th[2], out=tmp[2]) + + za = np.dot(z, a, out=tmp[3]) + np.dot(np.dot(za, b, out=tmp[4]), c, out=dst[k, 0]) + yb = np.dot(y, b, out=tmp[3]) + np.dot(a, np.dot(yb, c, out=tmp[4]), out=dst[k, 1]) + zc = np.dot(z, c, out=tmp[3]) + np.dot(a, np.dot(b, zc, out=tmp[4]), out=dst[k, 2]) + return dst + + +def init_layer2q_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: + """ + Initializes 4x4 matrices of 2-qubit gates defined in the paper. + + Args: + thetas: depth x 4 matrix of gate parameters for every layer, where + "depth" is the number of layers. + dst: destination array of size depth x 4 x 4 that will receive gate + matrices of each layer. + + Returns: + Returns the "dst" array. + """ + depth = thetas.shape[0] + for k in range(depth): + th = thetas[k] + cs0 = np.cos(0.5 * th[0]).item() + sn0 = np.sin(0.5 * th[0]).item() + ep1 = np.exp(0.5j * th[1]).item() + en1 = np.exp(-0.5j * th[1]).item() + cs2 = np.cos(0.5 * th[2]).item() + sn2 = np.sin(0.5 * th[2]).item() + cs3 = np.cos(0.5 * th[3]).item() + sn3 = np.sin(0.5 * th[3]).item() + ep1cs0 = ep1 * cs0 + ep1sn0 = ep1 * sn0 + en1cs0 = en1 * cs0 + en1sn0 = en1 * sn0 + sn2cs3 = sn2 * cs3 + sn2sn3 = sn2 * sn3 + cs2cs3 = cs2 * cs3 + sn3cs2j = 1j * sn3 * cs2 + sn2sn3j = 1j * sn2sn3 + + flat_dst = dst[k].ravel() + flat_dst[:] = [ + -(sn2sn3j - cs2cs3) * en1cs0, + -(sn2cs3 + sn3cs2j) * en1cs0, + (sn2cs3 + sn3cs2j) * en1sn0, + (sn2sn3j - cs2cs3) * en1sn0, + (sn2cs3 - sn3cs2j) * en1cs0, + (sn2sn3j + cs2cs3) * en1cs0, + -(sn2sn3j + cs2cs3) * en1sn0, + (-sn2cs3 + sn3cs2j) * en1sn0, + (-sn2sn3j + cs2cs3) * ep1sn0, + -(sn2cs3 + sn3cs2j) * ep1sn0, + -(sn2cs3 + sn3cs2j) * ep1cs0, + (-sn2sn3j + cs2cs3) * ep1cs0, + (sn2cs3 - sn3cs2j) * ep1sn0, + (sn2sn3j + cs2cs3) * ep1sn0, + (sn2sn3j + cs2cs3) * ep1cs0, + (sn2cs3 - sn3cs2j) * ep1cs0, + ] + return dst + + +def init_layer2q_deriv_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: + """ + Initializes 4 x 4 derivative matrices of 2-qubit gates defined in the paper. + + Args: + thetas: depth x 4 matrix of gate parameters for every layer, where + "depth" is the number of layers. + dst: destination array of size depth x 4 x 4 x 4 that will receive gate + derivative matrices of each layer; there are 4 parameters per gate, + hence, 4 derivative matrices per layer. + + Returns: + Returns the "dst" array. + """ + depth = thetas.shape[0] + for k in range(depth): + th = thetas[k] + cs0 = np.cos(0.5 * th[0]).item() + sn0 = np.sin(0.5 * th[0]).item() + ep1 = np.exp(0.5j * th[1]).item() * 0.5 + en1 = np.exp(-0.5j * th[1]).item() * 0.5 + cs2 = np.cos(0.5 * th[2]).item() + sn2 = np.sin(0.5 * th[2]).item() + cs3 = np.cos(0.5 * th[3]).item() + sn3 = np.sin(0.5 * th[3]).item() + ep1cs0 = ep1 * cs0 + ep1sn0 = ep1 * sn0 + en1cs0 = en1 * cs0 + en1sn0 = en1 * sn0 + sn2cs3 = sn2 * cs3 + sn2sn3 = sn2 * sn3 + sn3cs2 = sn3 * cs2 + cs2cs3 = cs2 * cs3 + sn2cs3j = 1j * sn2cs3 + sn2sn3j = 1j * sn2sn3 + sn3cs2j = 1j * sn3cs2 + cs2cs3j = 1j * cs2cs3 + + flat_dst = dst[k, 0].ravel() + flat_dst[:] = [ + (sn2sn3j - cs2cs3) * en1sn0, + (sn2cs3 + sn3cs2j) * en1sn0, + (sn2cs3 + sn3cs2j) * en1cs0, + (sn2sn3j - cs2cs3) * en1cs0, + (-sn2cs3 + sn3cs2j) * en1sn0, + -(sn2sn3j + cs2cs3) * en1sn0, + -(sn2sn3j + cs2cs3) * en1cs0, + (-sn2cs3 + sn3cs2j) * en1cs0, + (-sn2sn3j + cs2cs3) * ep1cs0, + -(sn2cs3 + sn3cs2j) * ep1cs0, + (sn2cs3 + sn3cs2j) * ep1sn0, + (sn2sn3j - cs2cs3) * ep1sn0, + (sn2cs3 - sn3cs2j) * ep1cs0, + (sn2sn3j + cs2cs3) * ep1cs0, + -(sn2sn3j + cs2cs3) * ep1sn0, + (-sn2cs3 + sn3cs2j) * ep1sn0, + ] + + flat_dst = dst[k, 1].ravel() + flat_dst[:] = [ + -(sn2sn3 + cs2cs3j) * en1cs0, + (sn2cs3j - sn3cs2) * en1cs0, + -(sn2cs3j - sn3cs2) * en1sn0, + (sn2sn3 + cs2cs3j) * en1sn0, + -(sn2cs3j + sn3cs2) * en1cs0, + (sn2sn3 - cs2cs3j) * en1cs0, + (-sn2sn3 + cs2cs3j) * en1sn0, + (sn2cs3j + sn3cs2) * en1sn0, + (sn2sn3 + cs2cs3j) * ep1sn0, + (-sn2cs3j + sn3cs2) * ep1sn0, + (-sn2cs3j + sn3cs2) * ep1cs0, + (sn2sn3 + cs2cs3j) * ep1cs0, + (sn2cs3j + sn3cs2) * ep1sn0, + (-sn2sn3 + cs2cs3j) * ep1sn0, + (-sn2sn3 + cs2cs3j) * ep1cs0, + (sn2cs3j + sn3cs2) * ep1cs0, + ] + + flat_dst = dst[k, 2].ravel() + flat_dst[:] = [ + -(sn2cs3 + sn3cs2j) * en1cs0, + (sn2sn3j - cs2cs3) * en1cs0, + -(sn2sn3j - cs2cs3) * en1sn0, + (sn2cs3 + sn3cs2j) * en1sn0, + (sn2sn3j + cs2cs3) * en1cs0, + (-sn2cs3 + sn3cs2j) * en1cs0, + (sn2cs3 - sn3cs2j) * en1sn0, + -(sn2sn3j + cs2cs3) * en1sn0, + -(sn2cs3 + sn3cs2j) * ep1sn0, + (sn2sn3j - cs2cs3) * ep1sn0, + (sn2sn3j - cs2cs3) * ep1cs0, + -(sn2cs3 + sn3cs2j) * ep1cs0, + (sn2sn3j + cs2cs3) * ep1sn0, + (-sn2cs3 + sn3cs2j) * ep1sn0, + (-sn2cs3 + sn3cs2j) * ep1cs0, + (sn2sn3j + cs2cs3) * ep1cs0, + ] + + flat_dst = dst[k, 3].ravel() + flat_dst[:] = [ + -(sn2cs3j + sn3cs2) * en1cs0, + (sn2sn3 - cs2cs3j) * en1cs0, + (-sn2sn3 + cs2cs3j) * en1sn0, + (sn2cs3j + sn3cs2) * en1sn0, + -(sn2sn3 + cs2cs3j) * en1cs0, + (sn2cs3j - sn3cs2) * en1cs0, + -(sn2cs3j - sn3cs2) * en1sn0, + (sn2sn3 + cs2cs3j) * en1sn0, + -(sn2cs3j + sn3cs2) * ep1sn0, + (sn2sn3 - cs2cs3j) * ep1sn0, + (sn2sn3 - cs2cs3j) * ep1cs0, + -(sn2cs3j + sn3cs2) * ep1cs0, + -(sn2sn3 + cs2cs3j) * ep1sn0, + (sn2cs3j - sn3cs2) * ep1sn0, + (sn2cs3j - sn3cs2) * ep1cs0, + -(sn2sn3 + cs2cs3j) * ep1cs0, + ] + return dst diff --git a/qiskit/synthesis/unitary/aqc/fast_gradient/pmatrix.py b/qiskit/synthesis/unitary/aqc/fast_gradient/pmatrix.py new file mode 100644 index 000000000000..8b36a0b8877e --- /dev/null +++ b/qiskit/synthesis/unitary/aqc/fast_gradient/pmatrix.py @@ -0,0 +1,312 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Matrix designed for fast multiplication by permutation and block-diagonal ones. +""" + +import numpy as np +from .layer import Layer1Q, Layer2Q + + +class PMatrix: + """ + Wrapper around a matrix that enables fast multiplication by permutation + matrices and block-diagonal ones. + """ + + def __init__(self, num_qubits: int): + """ + Initializes the internal structures of this object but does not set + the matrix yet. + + Args: + num_qubits: number of qubits. + """ + dim = 2**num_qubits + self._mat = np.empty(0) + self._dim = dim + self._temp_g2x2 = np.zeros((2, 2), dtype=np.complex128) + self._temp_g4x4 = np.zeros((4, 4), dtype=np.complex128) + self._temp_2x2 = self._temp_g2x2.copy() + self._temp_4x4 = self._temp_g4x4.copy() + self._identity_perm = np.arange(dim, dtype=np.int64) + self._left_perm = self._identity_perm.copy() + self._right_perm = self._identity_perm.copy() + self._temp_perm = self._identity_perm.copy() + self._temp_slice_dim_x_2 = np.zeros((dim, 2), dtype=np.complex128) + self._temp_slice_dim_x_4 = np.zeros((dim, 4), dtype=np.complex128) + self._idx_mat = self._init_index_matrix(dim) + self._temp_block_diag = np.zeros(self._idx_mat.shape, dtype=np.complex128) + + def set_matrix(self, mat: np.ndarray): + """ + Copies specified matrix to internal storage. Once the matrix + is set, the object is ready for use. + + **Note**, the matrix will be copied, mind the size issues. + + Args: + mat: matrix we want to multiply on the left and on the right by + layer matrices. + """ + if self._mat.size == 0: + self._mat = mat.copy() + else: + np.copyto(self._mat, mat) + + @staticmethod + def _init_index_matrix(dim: int) -> np.ndarray: + """ + Fast multiplication can be implemented by picking up a subset of + entries in a sparse matrix. + + Args: + dim: problem dimensionality. + + Returns: + 2d-array of indices for the fast multiplication. + """ + all_idx = np.arange(dim * dim, dtype=np.int64).reshape(dim, dim) + idx = np.full((dim // 4, 4 * 4), fill_value=0, dtype=np.int64) + b = np.full((4, 4), fill_value=0, dtype=np.int64) + for i in range(0, dim, 4): + b[:, :] = all_idx[i : i + 4, i : i + 4] + idx[i // 4, :] = b.T.ravel() + return idx + + def mul_right_q1(self, layer: Layer1Q, temp_mat: np.ndarray, dagger: bool): + """ + Multiplies ``NxN`` matrix, wrapped by this object, by a 1-qubit layer + matrix of the right, where ``N`` is the actual size of matrices involved, + ``N = 2^{num. of qubits}``. + + Args: + layer: 1-qubit layer, i.e. the layer with just one non-trivial + 1-qubit gate and other gates are just identity operators. + temp_mat: a temporary NxN matrix used as a workspace. + dagger: if true, the right-hand side matrix will be taken as + conjugate transposed. + """ + + gmat, perm, inv_perm = layer.get_attr() + mat = self._mat + dim = perm.size + + # temp_mat <-- mat[:, right_perm[perm]] = mat[:, right_perm][:, perm]: + np.take(mat, np.take(self._right_perm, perm, out=self._temp_perm), axis=1, out=temp_mat) + + # mat <-- mat[:, right_perm][:, perm] @ kron(I(N/4), gmat)^dagger, where + # conjugate-transposition might be or might be not applied: + gmat_right = np.conj(gmat, out=self._temp_g2x2).T if dagger else gmat + for i in range(0, dim, 2): + mat[:, i : i + 2] = np.dot( + temp_mat[:, i : i + 2], gmat_right, out=self._temp_slice_dim_x_2 + ) + + # Update right permutation: + self._right_perm[:] = inv_perm + + def mul_right_q2(self, layer: Layer2Q, temp_mat: np.ndarray, dagger: bool = True): + """ + Multiplies ``NxN`` matrix, wrapped by this object, by a 2-qubit layer + matrix on the right, where ``N`` is the actual size of matrices involved, + ``N = 2^{num. of qubits}``. + + Args: + layer: 2-qubit layer, i.e. the layer with just one non-trivial + 2-qubit gate and other gates are just identity operators. + temp_mat: a temporary NxN matrix used as a workspace. + dagger: if true, the right-hand side matrix will be taken as + conjugate transposed. + """ + + gmat, perm, inv_perm = layer.get_attr() + mat = self._mat + dim = perm.size + + # temp_mat <-- mat[:, right_perm[perm]] = mat[:, right_perm][:, perm]: + np.take(mat, np.take(self._right_perm, perm, out=self._temp_perm), axis=1, out=temp_mat) + + # mat <-- mat[:, right_perm][:, perm] @ kron(I(N/4), gmat)^dagger, where + # conjugate-transposition might be or might be not applied: + gmat_right = np.conj(gmat, out=self._temp_g4x4).T if dagger else gmat + for i in range(0, dim, 4): + mat[:, i : i + 4] = np.dot( + temp_mat[:, i : i + 4], gmat_right, out=self._temp_slice_dim_x_4 + ) + + # Update right permutation: + self._right_perm[:] = inv_perm + + def mul_left_q1(self, layer: Layer1Q, temp_mat: np.ndarray): + """ + Multiplies ``NxN`` matrix, wrapped by this object, by a 1-qubit layer + matrix of the left, where ``dim`` is the actual size of matrices involved, + ``dim = 2^{num. of qubits}``. + + Args: + layer: 1-qubit layer, i.e. the layer with just one non-trivial + 1-qubit gate and other gates are just identity operators. + temp_mat: a temporary NxN matrix used as a workspace. + """ + mat = self._mat + gmat, perm, inv_perm = layer.get_attr() + dim = perm.size + + # temp_mat <-- mat[left_perm[perm]] = mat[left_perm][perm]: + np.take(mat, np.take(self._left_perm, perm, out=self._temp_perm), axis=0, out=temp_mat) + + # mat <-- kron(I(dim/4), gmat) @ mat[left_perm][perm]: + if dim > 512: + # Faster for large matrices. + for i in range(0, dim, 2): + np.dot(gmat, temp_mat[i : i + 2, :], out=mat[i : i + 2, :]) + else: + # Faster for small matrices. + half = dim // 2 + np.copyto( + mat.reshape((2, half, dim)), np.swapaxes(temp_mat.reshape((half, 2, dim)), 0, 1) + ) + np.dot(gmat, mat.reshape(2, -1), out=temp_mat.reshape(2, -1)) + np.copyto( + mat.reshape((half, 2, dim)), np.swapaxes(temp_mat.reshape((2, half, dim)), 0, 1) + ) + + # Update left permutation: + self._left_perm[:] = inv_perm + + def mul_left_q2(self, layer: Layer2Q, temp_mat: np.ndarray): + """ + Multiplies ``NxN`` matrix, wrapped by this object, by a 2-qubit layer + matrix on the left, where ``dim`` is the actual size of matrices involved, + ``dim = 2^{num. of qubits}``. + + Args: + layer: 2-qubit layer, i.e. the layer with just one non-trivial + 2-qubit gate and other gates are just identity operators. + temp_mat: a temporary NxN matrix used as a workspace. + """ + mat = self._mat + gmat, perm, inv_perm = layer.get_attr() + dim = perm.size + + # temp_mat <-- mat[left_perm[perm]] = mat[left_perm][perm]: + np.take(mat, np.take(self._left_perm, perm, out=self._temp_perm), axis=0, out=temp_mat) + + # mat <-- kron(I(dim/4), gmat) @ mat[left_perm][perm]: + if dim > 512: + # Faster for large matrices. + for i in range(0, dim, 4): + np.dot(gmat, temp_mat[i : i + 4, :], out=mat[i : i + 4, :]) + else: + # Faster for small matrices. + half = dim // 4 + np.copyto( + mat.reshape((4, half, dim)), np.swapaxes(temp_mat.reshape((half, 4, dim)), 0, 1) + ) + np.dot(gmat, mat.reshape(4, -1), out=temp_mat.reshape(4, -1)) + np.copyto( + mat.reshape((half, 4, dim)), np.swapaxes(temp_mat.reshape((4, half, dim)), 0, 1) + ) + + # Update left permutation: + self._left_perm[:] = inv_perm + + def product_q1(self, layer: Layer1Q, tmp1: np.ndarray, tmp2: np.ndarray) -> np.complex128: + """ + Computes and returns: ``Trace(mat @ C) = Trace(mat @ P^T @ gmat @ P) = + Trace((P @ mat @ P^T) @ gmat) = Trace(C @ (P @ mat @ P^T)) = + vec(gmat^T)^T @ vec(P @ mat @ P^T)``, where mat is ``NxN`` matrix wrapped + by this object, ``C`` is matrix representation of the layer ``L``, and gmat + is 2x2 matrix of underlying 1-qubit gate. + + **Note**: matrix of this class must be finalized beforehand. + + Args: + layer: 1-qubit layer. + tmp1: temporary, external matrix used as a workspace. + tmp2: temporary, external matrix used as a workspace. + + Returns: + trace of the matrix product. + """ + mat = self._mat + gmat, perm, _ = layer.get_attr() + + # tmp2 = P @ mat @ P^T: + np.take(np.take(mat, perm, axis=0, out=tmp1), perm, axis=1, out=tmp2) + + # matrix dot product = Tr(transposed(kron(I(dim/4), gmat)), (P @ mat @ P^T)): + gmat_t, tmp3 = self._temp_g2x2, self._temp_2x2 + np.copyto(gmat_t, gmat.T) + _sum = 0.0 + for i in range(0, mat.shape[0], 2): + tmp3[:, :] = tmp2[i : i + 2, i : i + 2] + _sum += np.dot(gmat_t.ravel(), tmp3.ravel()) + + return np.complex128(_sum) + + def product_q2(self, layer: Layer2Q, tmp1: np.ndarray, tmp2: np.ndarray) -> np.complex128: + """ + Computes and returns: ``Trace(mat @ C) = Trace(mat @ P^T @ gmat @ P) = + Trace((P @ mat @ P^T) @ gmat) = Trace(C @ (P @ mat @ P^T)) = + vec(gmat^T)^T @ vec(P @ mat @ P^T)``, where mat is ``NxN`` matrix wrapped + by this object, ``C`` is matrix representation of the layer ``L``, and gmat + is 4x4 matrix of underlying 2-qubit gate. + + **Note**: matrix of this class must be finalized beforehand. + + Args: + layer: 2-qubit layer. + tmp1: temporary, external matrix used as a workspace. + tmp2: temporary, external matrix used as a workspace. + + Returns: + trace of the matrix product. + """ + mat = self._mat + gmat, perm, _ = layer.get_attr() + + # Compute the matrix dot product: + # Tr(transposed(kron(I(dim/4), gmat)), (P @ mat @ P^T)): + + # The fastest version so far, but requires two NxN temp. matrices. + # tmp2 = P @ mat @ P^T: + np.take(np.take(mat, perm, axis=0, out=tmp1), perm, axis=1, out=tmp2) + + bldia = self._temp_block_diag + np.take(tmp2.ravel(), self._idx_mat.ravel(), axis=0, out=bldia.ravel()) + bldia *= gmat.reshape(-1, gmat.size) + return np.complex128(np.sum(bldia)) + + def finalize(self, temp_mat: np.ndarray) -> np.ndarray: + """ + Applies the left (row) and right (column) permutations to the matrix. + at the end of computation process. + + Args: + temp_mat: temporary, external matrix. + + Returns: + finalized matrix with all transformations applied. + """ + mat = self._mat + + # mat <-- mat[left_perm][:, right_perm] = P_left @ mat @ transposed(P_right) + np.take(mat, self._left_perm, axis=0, out=temp_mat) + np.take(temp_mat, self._right_perm, axis=1, out=mat) + + # Set both permutations to identity once they have been applied. + self._left_perm[:] = self._identity_perm + self._right_perm[:] = self._identity_perm + return self._mat diff --git a/test/python/transpiler/aqc/fast_gradient/test_cmp_gradients.py b/test/python/transpiler/aqc/fast_gradient/test_cmp_gradients.py index 27e7d96aba90..e895aaff3664 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_cmp_gradients.py +++ b/test/python/transpiler/aqc/fast_gradient/test_cmp_gradients.py @@ -19,8 +19,8 @@ from time import perf_counter from test.python.transpiler.aqc.fast_gradient.utils_for_testing import rand_circuit, rand_su_mat import numpy as np -from qiskit.transpiler.synthesis.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective -from qiskit.transpiler.synthesis.aqc.cnot_unit_objective import DefaultCNOTUnitObjective +from qiskit.synthesis.unitary.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective +from qiskit.synthesis.unitary.aqc.cnot_unit_objective import DefaultCNOTUnitObjective from qiskit.test import QiskitTestCase diff --git a/test/python/transpiler/aqc/fast_gradient/test_layer1q.py b/test/python/transpiler/aqc/fast_gradient/test_layer1q.py index 2c74e98e2324..7fc0533c190b 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_layer1q.py +++ b/test/python/transpiler/aqc/fast_gradient/test_layer1q.py @@ -18,8 +18,8 @@ from random import randint import test.python.transpiler.aqc.fast_gradient.utils_for_testing as tut import numpy as np -import qiskit.transpiler.synthesis.aqc.fast_gradient.layer as lr -from qiskit.transpiler.synthesis.aqc.fast_gradient.pmatrix import PMatrix +import qiskit.synthesis.unitary.aqc.fast_gradient.layer as lr +from qiskit.synthesis.unitary.aqc.fast_gradient.pmatrix import PMatrix from qiskit.test import QiskitTestCase diff --git a/test/python/transpiler/aqc/fast_gradient/test_layer2q.py b/test/python/transpiler/aqc/fast_gradient/test_layer2q.py index b8a19ccf19ed..e84a04d49282 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_layer2q.py +++ b/test/python/transpiler/aqc/fast_gradient/test_layer2q.py @@ -18,8 +18,8 @@ from random import randint import test.python.transpiler.aqc.fast_gradient.utils_for_testing as tut import numpy as np -import qiskit.transpiler.synthesis.aqc.fast_gradient.layer as lr -from qiskit.transpiler.synthesis.aqc.fast_gradient.pmatrix import PMatrix +import qiskit.synthesis.unitary.aqc.fast_gradient.layer as lr +from qiskit.synthesis.unitary.aqc.fast_gradient.pmatrix import PMatrix from qiskit.test import QiskitTestCase diff --git a/test/python/transpiler/aqc/fast_gradient/test_utils.py b/test/python/transpiler/aqc/fast_gradient/test_utils.py index 9d31ebd8cd9a..2e6fcd22d589 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_utils.py +++ b/test/python/transpiler/aqc/fast_gradient/test_utils.py @@ -20,9 +20,9 @@ import numpy as np import qiskit.transpiler.synthesis.aqc.fast_gradient.fast_grad_utils as myu from qiskit.test import QiskitTestCase -from qiskit.transpiler.synthesis.aqc.elementary_operations import rx_matrix as _rx -from qiskit.transpiler.synthesis.aqc.elementary_operations import ry_matrix as _ry -from qiskit.transpiler.synthesis.aqc.elementary_operations import rz_matrix as _rz +from qiskit.synthesis.unitary.aqc.elementary_operations import rx_matrix as _rx +from qiskit.synthesis.unitary.aqc.elementary_operations import ry_matrix as _ry +from qiskit.synthesis.unitary.aqc.elementary_operations import rz_matrix as _rz class TestUtils(QiskitTestCase): diff --git a/test/python/transpiler/aqc/fast_gradient/utils_for_testing.py b/test/python/transpiler/aqc/fast_gradient/utils_for_testing.py index bffc03e066bd..c6a61930141d 100644 --- a/test/python/transpiler/aqc/fast_gradient/utils_for_testing.py +++ b/test/python/transpiler/aqc/fast_gradient/utils_for_testing.py @@ -17,7 +17,7 @@ from typing import Tuple import numpy as np from scipy.stats import unitary_group -import qiskit.transpiler.synthesis.aqc.fast_gradient.fast_grad_utils as fgu +import qiskit.synthesis.unitary.aqc.fast_gradient.fast_grad_utils as fgu def relative_error(a_mat: np.ndarray, b_mat: np.ndarray) -> float: diff --git a/test/python/transpiler/aqc/test_aqc.py b/test/python/transpiler/aqc/test_aqc.py index c54de8c21dc7..d67c1d613416 100644 --- a/test/python/transpiler/aqc/test_aqc.py +++ b/test/python/transpiler/aqc/test_aqc.py @@ -23,11 +23,11 @@ from qiskit.quantum_info import Operator from qiskit.test import QiskitTestCase -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 -from qiskit.transpiler.synthesis.aqc.cnot_unit_objective import DefaultCNOTUnitObjective -from qiskit.transpiler.synthesis.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective +from qiskit.synthesis.unitary.aqc.aqc import AQC +from qiskit.synthesis.unitary.aqc.cnot_structures import make_cnot_network +from qiskit.synthesis.unitary.aqc.cnot_unit_circuit import CNOTUnitCircuit +from qiskit.synthesis.unitary.aqc.cnot_unit_objective import DefaultCNOTUnitObjective +from qiskit.synthesis.unitary.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective @ddt diff --git a/test/python/transpiler/aqc/test_cnot_networks.py b/test/python/transpiler/aqc/test_cnot_networks.py index 3f4b17943e7c..eee90b87be75 100644 --- a/test/python/transpiler/aqc/test_cnot_networks.py +++ b/test/python/transpiler/aqc/test_cnot_networks.py @@ -18,7 +18,7 @@ from ddt import ddt, data, unpack from qiskit.test import QiskitTestCase -from qiskit.transpiler.synthesis.aqc import make_cnot_network +from qiskit.synthesis.unitary.aqc import make_cnot_network @ddt diff --git a/test/python/transpiler/aqc/test_gradient.py b/test/python/transpiler/aqc/test_gradient.py index 1c2235441b55..db596ac1ff21 100644 --- a/test/python/transpiler/aqc/test_gradient.py +++ b/test/python/transpiler/aqc/test_gradient.py @@ -17,8 +17,8 @@ from test.python.transpiler.aqc.sample_data import ORIGINAL_CIRCUIT, INITIAL_THETAS import numpy as np from qiskit.test import QiskitTestCase -from qiskit.transpiler.synthesis.aqc.cnot_structures import make_cnot_network -from qiskit.transpiler.synthesis.aqc.cnot_unit_objective import DefaultCNOTUnitObjective +from qiskit.synthesis.unitary.aqc.cnot_structures import make_cnot_network +from qiskit.synthesis.unitary.aqc.cnot_unit_objective import DefaultCNOTUnitObjective class TestGradientAgainstFiniteDiff(QiskitTestCase): From 05b50e568e2e5ceedaee43a0b2305eda4f218539 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 10:14:05 +0000 Subject: [PATCH 07/35] move tests from test/python/transpiler/aqc to test/python/synthesis/aqc --- test/python/{transpiler => synthesis}/aqc/__init__.py | 0 .../{transpiler => synthesis}/aqc/fast_gradient/__init__.py | 0 .../aqc/fast_gradient/test_cmp_gradients.py | 2 +- .../{transpiler => synthesis}/aqc/fast_gradient/test_layer1q.py | 2 +- .../{transpiler => synthesis}/aqc/fast_gradient/test_layer2q.py | 2 +- .../{transpiler => synthesis}/aqc/fast_gradient/test_utils.py | 2 +- .../aqc/fast_gradient/utils_for_testing.py | 0 test/python/{transpiler => synthesis}/aqc/sample_data.py | 0 test/python/{transpiler => synthesis}/aqc/test_aqc.py | 2 +- test/python/{transpiler => synthesis}/aqc/test_aqc_plugin.py | 0 test/python/{transpiler => synthesis}/aqc/test_cnot_networks.py | 2 +- test/python/{transpiler => synthesis}/aqc/test_gradient.py | 2 +- 12 files changed, 7 insertions(+), 7 deletions(-) rename test/python/{transpiler => synthesis}/aqc/__init__.py (100%) rename test/python/{transpiler => synthesis}/aqc/fast_gradient/__init__.py (100%) rename test/python/{transpiler => synthesis}/aqc/fast_gradient/test_cmp_gradients.py (97%) rename test/python/{transpiler => synthesis}/aqc/fast_gradient/test_layer1q.py (98%) rename test/python/{transpiler => synthesis}/aqc/fast_gradient/test_layer2q.py (98%) rename test/python/{transpiler => synthesis}/aqc/fast_gradient/test_utils.py (99%) rename test/python/{transpiler => synthesis}/aqc/fast_gradient/utils_for_testing.py (100%) rename test/python/{transpiler => synthesis}/aqc/sample_data.py (100%) rename test/python/{transpiler => synthesis}/aqc/test_aqc.py (98%) rename test/python/{transpiler => synthesis}/aqc/test_aqc_plugin.py (100%) rename test/python/{transpiler => synthesis}/aqc/test_cnot_networks.py (96%) rename test/python/{transpiler => synthesis}/aqc/test_gradient.py (97%) diff --git a/test/python/transpiler/aqc/__init__.py b/test/python/synthesis/aqc/__init__.py similarity index 100% rename from test/python/transpiler/aqc/__init__.py rename to test/python/synthesis/aqc/__init__.py diff --git a/test/python/transpiler/aqc/fast_gradient/__init__.py b/test/python/synthesis/aqc/fast_gradient/__init__.py similarity index 100% rename from test/python/transpiler/aqc/fast_gradient/__init__.py rename to test/python/synthesis/aqc/fast_gradient/__init__.py diff --git a/test/python/transpiler/aqc/fast_gradient/test_cmp_gradients.py b/test/python/synthesis/aqc/fast_gradient/test_cmp_gradients.py similarity index 97% rename from test/python/transpiler/aqc/fast_gradient/test_cmp_gradients.py rename to test/python/synthesis/aqc/fast_gradient/test_cmp_gradients.py index e895aaff3664..3f648d946c7b 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_cmp_gradients.py +++ b/test/python/synthesis/aqc/fast_gradient/test_cmp_gradients.py @@ -17,7 +17,7 @@ import unittest from typing import Tuple from time import perf_counter -from test.python.transpiler.aqc.fast_gradient.utils_for_testing import rand_circuit, rand_su_mat +from test.python.synthesis.aqc.fast_gradient.utils_for_testing import rand_circuit, rand_su_mat import numpy as np from qiskit.synthesis.unitary.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective from qiskit.synthesis.unitary.aqc.cnot_unit_objective import DefaultCNOTUnitObjective diff --git a/test/python/transpiler/aqc/fast_gradient/test_layer1q.py b/test/python/synthesis/aqc/fast_gradient/test_layer1q.py similarity index 98% rename from test/python/transpiler/aqc/fast_gradient/test_layer1q.py rename to test/python/synthesis/aqc/fast_gradient/test_layer1q.py index 7fc0533c190b..2173e6d5c792 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_layer1q.py +++ b/test/python/synthesis/aqc/fast_gradient/test_layer1q.py @@ -16,7 +16,7 @@ import unittest from random import randint -import test.python.transpiler.aqc.fast_gradient.utils_for_testing as tut +import test.python.synthesis.aqc.fast_gradient.utils_for_testing as tut import numpy as np import qiskit.synthesis.unitary.aqc.fast_gradient.layer as lr from qiskit.synthesis.unitary.aqc.fast_gradient.pmatrix import PMatrix diff --git a/test/python/transpiler/aqc/fast_gradient/test_layer2q.py b/test/python/synthesis/aqc/fast_gradient/test_layer2q.py similarity index 98% rename from test/python/transpiler/aqc/fast_gradient/test_layer2q.py rename to test/python/synthesis/aqc/fast_gradient/test_layer2q.py index e84a04d49282..f97f53ad7d7c 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_layer2q.py +++ b/test/python/synthesis/aqc/fast_gradient/test_layer2q.py @@ -16,7 +16,7 @@ import unittest from random import randint -import test.python.transpiler.aqc.fast_gradient.utils_for_testing as tut +import test.python.synthesis.aqc.fast_gradient.utils_for_testing as tut import numpy as np import qiskit.synthesis.unitary.aqc.fast_gradient.layer as lr from qiskit.synthesis.unitary.aqc.fast_gradient.pmatrix import PMatrix diff --git a/test/python/transpiler/aqc/fast_gradient/test_utils.py b/test/python/synthesis/aqc/fast_gradient/test_utils.py similarity index 99% rename from test/python/transpiler/aqc/fast_gradient/test_utils.py rename to test/python/synthesis/aqc/fast_gradient/test_utils.py index 2e6fcd22d589..4a6f44591da0 100644 --- a/test/python/transpiler/aqc/fast_gradient/test_utils.py +++ b/test/python/synthesis/aqc/fast_gradient/test_utils.py @@ -16,7 +16,7 @@ import unittest import random -import test.python.transpiler.aqc.fast_gradient.utils_for_testing as tut +import test.python.synthesis.aqc.fast_gradient.utils_for_testing as tut import numpy as np import qiskit.transpiler.synthesis.aqc.fast_gradient.fast_grad_utils as myu from qiskit.test import QiskitTestCase diff --git a/test/python/transpiler/aqc/fast_gradient/utils_for_testing.py b/test/python/synthesis/aqc/fast_gradient/utils_for_testing.py similarity index 100% rename from test/python/transpiler/aqc/fast_gradient/utils_for_testing.py rename to test/python/synthesis/aqc/fast_gradient/utils_for_testing.py diff --git a/test/python/transpiler/aqc/sample_data.py b/test/python/synthesis/aqc/sample_data.py similarity index 100% rename from test/python/transpiler/aqc/sample_data.py rename to test/python/synthesis/aqc/sample_data.py diff --git a/test/python/transpiler/aqc/test_aqc.py b/test/python/synthesis/aqc/test_aqc.py similarity index 98% rename from test/python/transpiler/aqc/test_aqc.py rename to test/python/synthesis/aqc/test_aqc.py index d67c1d613416..3d153c492181 100644 --- a/test/python/transpiler/aqc/test_aqc.py +++ b/test/python/synthesis/aqc/test_aqc.py @@ -15,7 +15,7 @@ from functools import partial import unittest -from test.python.transpiler.aqc.sample_data import ORIGINAL_CIRCUIT, INITIAL_THETAS +from test.python.synthesis.aqc.sample_data import ORIGINAL_CIRCUIT, INITIAL_THETAS from ddt import ddt, data import numpy as np diff --git a/test/python/transpiler/aqc/test_aqc_plugin.py b/test/python/synthesis/aqc/test_aqc_plugin.py similarity index 100% rename from test/python/transpiler/aqc/test_aqc_plugin.py rename to test/python/synthesis/aqc/test_aqc_plugin.py diff --git a/test/python/transpiler/aqc/test_cnot_networks.py b/test/python/synthesis/aqc/test_cnot_networks.py similarity index 96% rename from test/python/transpiler/aqc/test_cnot_networks.py rename to test/python/synthesis/aqc/test_cnot_networks.py index eee90b87be75..f844d1a4b6e5 100644 --- a/test/python/transpiler/aqc/test_cnot_networks.py +++ b/test/python/synthesis/aqc/test_cnot_networks.py @@ -12,7 +12,7 @@ """ Tests building up CNOT unit structures. """ -from test.python.transpiler.aqc.sample_data import CARTAN_4, CARTAN_3 +from test.python.synthesis.aqc.sample_data import CARTAN_4, CARTAN_3 import numpy as np from ddt import ddt, data, unpack diff --git a/test/python/transpiler/aqc/test_gradient.py b/test/python/synthesis/aqc/test_gradient.py similarity index 97% rename from test/python/transpiler/aqc/test_gradient.py rename to test/python/synthesis/aqc/test_gradient.py index db596ac1ff21..584b0f3c7313 100644 --- a/test/python/transpiler/aqc/test_gradient.py +++ b/test/python/synthesis/aqc/test_gradient.py @@ -14,7 +14,7 @@ """ import unittest -from test.python.transpiler.aqc.sample_data import ORIGINAL_CIRCUIT, INITIAL_THETAS +from test.python.synthesis.aqc.sample_data import ORIGINAL_CIRCUIT, INITIAL_THETAS import numpy as np from qiskit.test import QiskitTestCase from qiskit.synthesis.unitary.aqc.cnot_structures import make_cnot_network From 3262d00a425e8f5ce6a99012249ca45ec641d520 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 12:57:03 +0000 Subject: [PATCH 08/35] update imports in aqc_plugin --- qiskit/transpiler/passes/synthesis/aqc_plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/aqc_plugin.py b/qiskit/transpiler/passes/synthesis/aqc_plugin.py index 0fa153566557..2214cba6fae3 100644 --- a/qiskit/transpiler/passes/synthesis/aqc_plugin.py +++ b/qiskit/transpiler/passes/synthesis/aqc_plugin.py @@ -106,10 +106,10 @@ 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 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 - from qiskit.transpiler.synthesis.aqc.cnot_unit_objective import DefaultCNOTUnitObjective + from qiskit.synthesis.unitary.aqc import AQC + from qiskit.synthesis.unitary.aqc.cnot_structures import make_cnot_network + from qiskit.synthesis.unitary.aqc.cnot_unit_circuit import CNOTUnitCircuit + from qiskit.synthesis.unitary.aqc.cnot_unit_objective import DefaultCNOTUnitObjective num_qubits = int(round(np.log2(unitary.shape[0]))) From dc316df0902477399e8057bdf0b18ddaa2ef419b Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 12:57:38 +0000 Subject: [PATCH 09/35] add deprecation warning to AQC module --- qiskit/transpiler/synthesis/aqc/__init__.py | 165 ++------------------ 1 file changed, 9 insertions(+), 156 deletions(-) diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index bf3e4c80ba5a..683d9efceb4b 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -10,163 +10,9 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -r""" -===================================================================== -Approximate Quantum Compiler (:mod:`qiskit.transpiler.synthesis.aqc`) -===================================================================== +"""Approximate Quantum Compiler - to be deprecated""" -.. currentmodule:: qiskit.transpiler.synthesis.aqc - -Implementation of Approximate Quantum Compiler as described in the paper [1]. - -Interface -========= - -The main public interface of this module is reached by passing ``unitary_synthesis_method='aqc'`` to -:obj:`~.compiler.transpile`. This will swap the synthesis method to use :obj:`AQCSynthesisPlugin`. -The individual classes are: - -.. autosummary:: - :toctree: ../stubs - :template: autosummary/class_no_inherited_members.rst - - AQC - ApproximateCircuit - ApproximatingObjective - CNOTUnitCircuit - CNOTUnitObjective - DefaultCNOTUnitObjective - FastCNOTUnitObjective - - -Mathematical Detail -=================== - -We are interested in compiling a quantum circuit, which we formalize as finding the best -circuit representation in terms of an ordered gate sequence of a target unitary matrix -:math:`U\in U(d)`, with some additional hardware constraints. In particular, we look at -representations that could be constrained in terms of hardware connectivity, as well -as gate depth, and we choose a gate basis in terms of CNOT and rotation gates. -We recall that the combination of CNOT and rotation gates is universal in :math:`SU(d)` and -therefore it does not limit compilation. - -To properly define what we mean by best circuit representation, we define the metric -as the Frobenius norm between the unitary matrix of the compiled circuit :math:`V` and -the target unitary matrix :math:`U`, i.e., :math:`\|V - U\|_{\mathrm{F}}`. This choice -is motivated by mathematical programming considerations, and it is related to other -formulations that appear in the literature. Let's take a look at the problem in more details. - -Let :math:`n` be the number of qubits and :math:`d=2^n`. Given a CNOT structure :math:`ct` -and a vector of rotation angles :math:`\theta`, the parametric circuit forms a matrix -:math:`Vct(\theta)\in SU(d)`. If we are given a target circuit forming a matrix -:math:`U\in SU(d)`, then we would like to compute - -.. math:: - - argmax_{\theta}\frac{1}{d}|\langle Vct(\theta),U\rangle| - -where the inner product is the Frobenius inner product. Note that -:math:`|\langle V,U\rangle|\leq d` for all unitaries :math:`U` and :math:`V`, so the objective -has range in :math:`[0,1]`. - -Our strategy is to maximize - -.. math:: - - \frac{1}{d}\Re \langle Vct(\theta),U\rangle - -using its gradient. We will now discuss the specifics by going through an example. - -While the range of :math:`Vct` is a subset of :math:`SU(d)` by construction, the target -circuit may form a general unitary matrix. However, for any :math:`U\in U(d)`, - -.. math:: - - \frac{\exp(2\pi i k/d)}{\det(U)^{1/d}}U\in SU(d)\text{ for all }k\in\{0,\ldots,d-1\}. - -Thus, we should normalize the target circuit by its global phase and then approximately -compile the normalized circuit. We can add the global phase back in afterwards. - -In the algorithm let :math:`U'` denote the un-normalized target matrix and :math:`U` -the normalized target matrix. Now that we have :math:`U`, we give the gradient function -to the Nesterov's method optimizer and compute :math:`\theta`. - -To add the global phase back in, we can form the control circuit as - -.. math:: - - \frac{\langle Vct(\theta),U'\rangle}{|\langle Vct(\theta),U'\rangle|}Vct(\theta). - -Note that while we optimized using Nesterov's method in the paper, this was for its convergence -guarantees, not its speed in practice. It is much faster to use L-BFGS which is used as a -default optimizer in this implementation. - -A basic usage of the AQC algorithm should consist of the following steps:: - - # Define a target circuit as a unitary matrix - unitary = ... - - # Define a number of qubits for the algorithm, at least 3 qubits - num_qubits = int(round(np.log2(unitary.shape[0]))) - - # Choose a layout of the CNOT structure for the approximate circuit, e.g. ``spin`` for - # a linear layout. - layout = options.get("layout") or "spin" - - # Choose a connectivity type, e.g. ``full`` for full connectivity between qubits. - connectivity = options.get("connectivity") or "full" - - # Define a targeted depth of the approximate circuit in the number of CNOT units. - depth = int(options.get("depth") or 0) - - # Generate a network made of CNOT units - cnots = make_cnot_network( - num_qubits=num_qubits, - network_layout=layout, - connectivity_type=connectivity, - depth=depth - ) - - # Create an optimizer to be used by AQC - optimizer = L_BFGS_B() - - # Create an instance - aqc = AQC(optimizer) - - # Create a template circuit that will approximate our target circuit - approximate_circuit = CNOTUnitCircuit(num_qubits=num_qubits, cnots=cnots) - - # Create an objective that defines our optimization problem - approximating_objective = DefaultCNOTUnitObjective(num_qubits=num_qubits, cnots=cnots) - - # Run optimization process to compile the unitary - aqc.compile_unitary( - target_matrix=unitary, - approximate_circuit=approximate_circuit, - approximating_objective=approximating_objective - ) - -Now ``approximate_circuit`` is a circuit that approximates the target unitary to a certain -degree and can be used instead of the original matrix. - -This uses a helper function, :obj:`make_cnot_network`. - -.. autofunction:: make_cnot_network - -One can take advantage of accelerated version of objective function. It implements the same -mathematical algorithm as the default one ``DefaultCNOTUnitObjective`` but runs several times -faster. Instantiation of accelerated objective function class is similar to the default case: - - # Create an objective that defines our optimization problem - approximating_objective = FastCNOTUnitObjective(num_qubits=num_qubits, cnots=cnots) - -The rest of the code in the above example does not change. - -References: - - [1]: Liam Madden, Andrea Simonetto, Best Approximate Quantum Compiling Problems. - `arXiv:2106.05649 `_ -""" +import warnings from .approximate import ApproximateCircuit, ApproximatingObjective from .aqc import AQC @@ -175,3 +21,10 @@ from .cnot_unit_objective import CNOTUnitObjective, DefaultCNOTUnitObjective from .fast_gradient.fast_gradient import FastCNOTUnitObjective from .aqc_plugin import AQCSynthesisPlugin + +warnings.warn( + "The qiskit.transpiler.synthesis.aqc module is pending deprecation since Qiskit 0.46.0. " + "It will be deprecated in a following release, no sooner than 3 months after the 0.46.0 release.", + stacklevel=2, + category=PendingDeprecationWarning, +) From 43811988a06ade9c3386e65d9822c76a35370712 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 13:15:39 +0000 Subject: [PATCH 10/35] handle cyclic imports --- qiskit/transpiler/synthesis/aqc/approximate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/synthesis/aqc/approximate.py b/qiskit/transpiler/synthesis/aqc/approximate.py index 6c3f11fb71fc..6294196b13c1 100644 --- a/qiskit/transpiler/synthesis/aqc/approximate.py +++ b/qiskit/transpiler/synthesis/aqc/approximate.py @@ -15,7 +15,7 @@ from typing import Optional, SupportsFloat import numpy as np -from qiskit import QuantumCircuit +from qiskit.circuit.quantumcircuit import QuantumCircuit class ApproximateCircuit(QuantumCircuit, ABC): From 6404bf9e2fca1b9977889d1abfe7fffdfe91d982 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 13:43:17 +0000 Subject: [PATCH 11/35] handle cyclic imports --- qiskit/synthesis/unitary/aqc/approximate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/unitary/aqc/approximate.py b/qiskit/synthesis/unitary/aqc/approximate.py index 6c3f11fb71fc..6294196b13c1 100644 --- a/qiskit/synthesis/unitary/aqc/approximate.py +++ b/qiskit/synthesis/unitary/aqc/approximate.py @@ -15,7 +15,7 @@ from typing import Optional, SupportsFloat import numpy as np -from qiskit import QuantumCircuit +from qiskit.circuit.quantumcircuit import QuantumCircuit class ApproximateCircuit(QuantumCircuit, ABC): From f88c2ff910ebafd451a536a3bc34a8c8cfb9f9dc Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 14:10:42 +0000 Subject: [PATCH 12/35] update link in docs --- qiskit/synthesis/unitary/aqc/__init__.py | 4 ++-- qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py index c851b76d3a59..cffad8e15504 100644 --- a/qiskit/synthesis/unitary/aqc/__init__.py +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -12,10 +12,10 @@ r""" ===================================================================== -Approximate Quantum Compiler (:mod:`qiskit.transpiler.synthesis.aqc`) +Approximate Quantum Compiler (:mod:`qiskit.synthesis.unitary.aqc`) ===================================================================== -.. currentmodule:: qiskit.transpiler.synthesis.aqc +.. currentmodule:: qiskit.synthesis.unitary.aqc Implementation of Approximate Quantum Compiler as described in the paper [1]. diff --git a/qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py b/qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py index df13193f12bb..7695325baba7 100644 --- a/qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py +++ b/qiskit/synthesis/unitary/aqc/fast_gradient/__init__.py @@ -13,10 +13,10 @@ r""" ================================================================================ Fast implementation of objective function class -(:mod:`qiskit.transpiler.synthesis.aqc.fast_gradient`) +(:mod:`qiskit.synthesis.unitary.aqc.fast_gradient`) ================================================================================ -.. currentmodule:: qiskit.transpiler.synthesis.aqc.fast_gradient +.. currentmodule:: qiskit.synthesis.unitary.aqc.fast_gradient Extension to the implementation of Approximate Quantum Compiler as described in the paper [1]. From 8078f2ee962438d975a02bc4846e05ef926ea58f Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 14:44:01 +0000 Subject: [PATCH 13/35] update init in qiskit/transpiler/synthesis/aqc --- qiskit/transpiler/synthesis/aqc/__init__.py | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index 683d9efceb4b..7c97a9068c4a 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -10,17 +10,23 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Approximate Quantum Compiler - to be deprecated""" +""" +===================================================================== +Approximate Quantum Compiler (:mod:`qiskit.transpiler.synthesis.aqc`) +===================================================================== + +.. currentmodule:: qiskit.transpiler.synthesis.aqc +""" import warnings -from .approximate import ApproximateCircuit, ApproximatingObjective -from .aqc import AQC -from .cnot_structures import make_cnot_network -from .cnot_unit_circuit import CNOTUnitCircuit -from .cnot_unit_objective import CNOTUnitObjective, DefaultCNOTUnitObjective -from .fast_gradient.fast_gradient import FastCNOTUnitObjective -from .aqc_plugin import AQCSynthesisPlugin +from qiskit.synthesis.unitary.aqc.approximate import ApproximateCircuit, ApproximatingObjective +from qiskit.synthesis.unitary.aqc import AQC +from qiskit.synthesis.unitary.aqc.cnot_structures import make_cnot_network +from qiskit.synthesis.unitary.aqc.cnot_unit_circuit import CNOTUnitCircuit +from qiskit.synthesis.unitary.aqc.cnot_unit_objective import CNOTUnitObjective, DefaultCNOTUnitObjective +from qiskit.synthesis.unitary.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective +from qiskit.transpiler.passes.synthesis.aqc_plugin import AQCSynthesisPlugin warnings.warn( "The qiskit.transpiler.synthesis.aqc module is pending deprecation since Qiskit 0.46.0. " From bfaa8058832f3b4897b9c03cbb09517cf219b161 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 25 Dec 2023 14:54:06 +0000 Subject: [PATCH 14/35] style --- qiskit/transpiler/synthesis/aqc/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index 7c97a9068c4a..f49b048f9aa6 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -24,7 +24,10 @@ from qiskit.synthesis.unitary.aqc import AQC from qiskit.synthesis.unitary.aqc.cnot_structures import make_cnot_network from qiskit.synthesis.unitary.aqc.cnot_unit_circuit import CNOTUnitCircuit -from qiskit.synthesis.unitary.aqc.cnot_unit_objective import CNOTUnitObjective, DefaultCNOTUnitObjective +from qiskit.synthesis.unitary.aqc.cnot_unit_objective import ( + CNOTUnitObjective, + DefaultCNOTUnitObjective, +) from qiskit.synthesis.unitary.aqc.fast_gradient.fast_gradient import FastCNOTUnitObjective from qiskit.transpiler.passes.synthesis.aqc_plugin import AQCSynthesisPlugin From cb042f325a2bd44388221460a7ad97a2755c6d3c Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Tue, 26 Dec 2023 09:28:10 +0000 Subject: [PATCH 15/35] temporary remove deprecation warning test --- test/python/synthesis/aqc/test_aqc_plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/python/synthesis/aqc/test_aqc_plugin.py b/test/python/synthesis/aqc/test_aqc_plugin.py index 4868ffce4297..a69f5e372bb6 100644 --- a/test/python/synthesis/aqc/test_aqc_plugin.py +++ b/test/python/synthesis/aqc/test_aqc_plugin.py @@ -24,7 +24,8 @@ from qiskit.transpiler import PassManager from qiskit.transpiler.passes import UnitarySynthesis from qiskit.transpiler.passes.synthesis import AQCSynthesisPlugin -from qiskit.transpiler.synthesis.aqc import AQCSynthesisPlugin as OldAQCSynthesisPlugin + +# from qiskit.transpiler.synthesis.aqc import AQCSynthesisPlugin as OldAQCSynthesisPlugin class TestAQCSynthesisPlugin(QiskitTestCase): @@ -48,8 +49,8 @@ def test_aqc_plugin(self): """Basic test of the plugin.""" plugin = AQCSynthesisPlugin() dag = plugin.run(self._target_unitary, config=self._seed_config) - with self.assertWarns(PendingDeprecationWarning): - _ = OldAQCSynthesisPlugin() + # with self.assertWarns(PendingDeprecationWarning): + # _ = OldAQCSynthesisPlugin() approx_circuit = dag_to_circuit(dag) approx_unitary = Operator(approx_circuit).data From 3ebbc8c0e0bd00d1f3a97e8fec1dd557324db434 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Tue, 26 Dec 2023 12:45:30 +0000 Subject: [PATCH 16/35] remove files from qiskit/transpiler/synthesis/aqc --- .../transpiler/synthesis/aqc/approximate.py | 116 ------ qiskit/transpiler/synthesis/aqc/aqc.py | 170 -------- qiskit/transpiler/synthesis/aqc/aqc_plugin.py | 71 ---- .../synthesis/aqc/cnot_structures.py | 299 -------------- .../synthesis/aqc/cnot_unit_circuit.py | 103 ----- .../synthesis/aqc/cnot_unit_objective.py | 299 -------------- .../synthesis/aqc/elementary_operations.py | 108 ----- .../synthesis/aqc/fast_gradient/__init__.py | 164 -------- .../aqc/fast_gradient/fast_grad_utils.py | 237 ----------- .../aqc/fast_gradient/fast_gradient.py | 225 ----------- .../synthesis/aqc/fast_gradient/layer.py | 370 ------------------ .../synthesis/aqc/fast_gradient/pmatrix.py | 312 --------------- test/python/synthesis/aqc/test_aqc_plugin.py | 4 - 13 files changed, 2478 deletions(-) delete mode 100644 qiskit/transpiler/synthesis/aqc/approximate.py delete mode 100644 qiskit/transpiler/synthesis/aqc/aqc.py delete mode 100644 qiskit/transpiler/synthesis/aqc/aqc_plugin.py delete mode 100644 qiskit/transpiler/synthesis/aqc/cnot_structures.py delete mode 100644 qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py delete mode 100644 qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py delete mode 100644 qiskit/transpiler/synthesis/aqc/elementary_operations.py delete mode 100644 qiskit/transpiler/synthesis/aqc/fast_gradient/__init__.py delete mode 100644 qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py delete mode 100644 qiskit/transpiler/synthesis/aqc/fast_gradient/fast_gradient.py delete mode 100644 qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py delete mode 100644 qiskit/transpiler/synthesis/aqc/fast_gradient/pmatrix.py diff --git a/qiskit/transpiler/synthesis/aqc/approximate.py b/qiskit/transpiler/synthesis/aqc/approximate.py deleted file mode 100644 index 6294196b13c1..000000000000 --- a/qiskit/transpiler/synthesis/aqc/approximate.py +++ /dev/null @@ -1,116 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Base classes for an approximate circuit definition.""" -from __future__ import annotations -from abc import ABC, abstractmethod -from typing import Optional, SupportsFloat -import numpy as np - -from qiskit.circuit.quantumcircuit import QuantumCircuit - - -class ApproximateCircuit(QuantumCircuit, ABC): - """A base class that represents an approximate circuit.""" - - def __init__(self, num_qubits: int, name: Optional[str] = None) -> None: - """ - Args: - num_qubits: number of qubit this circuit will span. - name: a name of the circuit. - """ - super().__init__(num_qubits, name=name) - - @property - @abstractmethod - def thetas(self) -> np.ndarray: - """ - The property is not implemented and raises a ``NotImplementedException`` exception. - - Returns: - a vector of parameters of this circuit. - """ - raise NotImplementedError - - @abstractmethod - def build(self, thetas: np.ndarray) -> None: - """ - Constructs this circuit out of the parameters(thetas). Parameter values must be set before - constructing the circuit. - - Args: - thetas: a vector of parameters to be set in this circuit. - """ - raise NotImplementedError - - -class ApproximatingObjective(ABC): - """ - A base class for an optimization problem definition. An implementing class must provide at least - an implementation of the ``objective`` method. In such case only gradient free optimizers can - be used. Both method, ``objective`` and ``gradient``, preferable to have in an implementation. - """ - - def __init__(self) -> None: - # must be set before optimization - self._target_matrix: np.ndarray | None = None - - @abstractmethod - def objective(self, param_values: np.ndarray) -> SupportsFloat: - """ - Computes a value of the objective function given a vector of parameter values. - - Args: - param_values: a vector of parameter values for the optimization problem. - - Returns: - a float value of the objective function. - """ - raise NotImplementedError - - @abstractmethod - def gradient(self, param_values: np.ndarray) -> np.ndarray: - """ - Computes a gradient with respect to parameters given a vector of parameter values. - - Args: - param_values: a vector of parameter values for the optimization problem. - - Returns: - an array of gradient values. - """ - raise NotImplementedError - - @property - def target_matrix(self) -> np.ndarray: - """ - Returns: - a matrix being approximated - """ - return self._target_matrix - - @target_matrix.setter - def target_matrix(self, target_matrix: np.ndarray) -> None: - """ - Args: - target_matrix: a matrix to approximate in the optimization procedure. - """ - self._target_matrix = target_matrix - - @property - @abstractmethod - def num_thetas(self) -> int: - """ - - Returns: - the number of parameters in this optimization problem. - """ - raise NotImplementedError diff --git a/qiskit/transpiler/synthesis/aqc/aqc.py b/qiskit/transpiler/synthesis/aqc/aqc.py deleted file mode 100644 index 4ced39a7e4ac..000000000000 --- a/qiskit/transpiler/synthesis/aqc/aqc.py +++ /dev/null @@ -1,170 +0,0 @@ -# This code is part of Qiskit. -# -# (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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# 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 __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.quantum_info import Operator - -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 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: - - * 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. - - * 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. - - * 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 - its accelerated version :class:`.FastCNOTUnitObjective`. Both implementations share the same - idea of maximization the Hilbert-Schmidt product between the target matrix and its - approximation. The former implementation approach should be considered as a baseline one. It - may suffer from performance issues, and is mostly suitable for a small number of qubits - (up to 5 or 6), whereas the latter, accelerated one, can be applied to larger problems. - - * One should take into consideration the exponential growth of matrix size with the number of - qubits because the implementation not only creates a potentially large target matrix, but - also allocates a number of temporary memory buffers comparable in size to the target matrix. - """ - - def __init__( - self, - optimizer: Minimizer | 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, 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 or partial( - minimize, args=(), method="L-BFGS-B", options={"maxiter": 1000} - ) - - self._seed = seed - - def compile_unitary( - self, - target_matrix: np.ndarray, - approximate_circuit: ApproximateCircuit, - approximating_objective: ApproximatingObjective, - initial_point: np.ndarray | None = None, - ) -> None: - """ - Approximately compiles a circuit represented as a unitary matrix by solving an optimization - problem defined by ``approximating_objective`` and using ``approximate_circuit`` as a - template for the approximate circuit. - - Args: - target_matrix: a unitary matrix to approximate. - approximate_circuit: a template circuit that will be filled with the parameter values - obtained in the optimization procedure. - approximating_objective: a definition of the optimization problem. - initial_point: initial values of angles/parameters to start optimization from. - """ - matrix_dim = target_matrix.shape[0] - # check if it is actually a special unitary matrix - target_det = np.linalg.det(target_matrix) - if not np.isclose(target_det, 1): - su_matrix = target_matrix / np.power(target_det, (1 / matrix_dim), dtype=complex) - global_phase_required = True - else: - su_matrix = target_matrix - global_phase_required = False - - # set the matrix to approximate in the algorithm - approximating_objective.target_matrix = su_matrix - - 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 = self._optimizer( - fun=approximating_objective.objective, - x0=initial_point, - jac=approximating_objective.gradient, - ) - - approximate_circuit.build(opt_result.x) - - approx_matrix = Operator(approximate_circuit).data - - if global_phase_required: - alpha = np.angle(np.trace(np.dot(approx_matrix.conj().T, target_matrix))) - approximate_circuit.global_phase = alpha diff --git a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py deleted file mode 100644 index 9c21c6b5d188..000000000000 --- a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py +++ /dev/null @@ -1,71 +0,0 @@ -# This code is part of Qiskit. -# -# (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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -An AQC synthesis plugin to Qiskit's transpiler. -""" - -from qiskit.transpiler.passes.synthesis import AQCSynthesisPlugin as NewAQCSynthesisPlugin -from qiskit.utils.deprecation import deprecate_func - - -class AQCSynthesisPlugin(NewAQCSynthesisPlugin): - """ - An AQC-based Qiskit unitary synthesis plugin. - - This plugin is invoked by :func:`~.compiler.transpile` when the ``unitary_synthesis_method`` - parameter is set to ``"aqc"``. - - This plugin supports customization and additional parameters can be passed to the plugin - by passing a dictionary as the ``unitary_synthesis_plugin_config`` parameter of - the :func:`~qiskit.compiler.transpile` function. - - Supported parameters in the dictionary: - - network_layout (str) - Type of network geometry, one of {``"sequ"``, ``"spin"``, ``"cart"``, ``"cyclic_spin"``, - ``"cyclic_line"``}. Default value is ``"spin"``. - - connectivity_type (str) - type of inter-qubit connectivity, {``"full"``, ``"line"``, ``"star"``}. Default value - is ``"full"``. - - depth (int) - depth of the CNOT-network, i.e. the number of layers, where each layer consists of a - single CNOT-block. - - optimizer (:class:`~.Minimizer`) - An implementation of the ``Minimizer`` protocol to be used in the optimization process. - - seed (int) - A random seed. - - initial_point (:class:`~numpy.ndarray`) - Initial values of angles/parameters to start the optimization process from. - """ - - @deprecate_func( - since="0.46.0", - pending=True, - additional_msg="AQCSynthesisPlugin has been moved to qiskit.transpiler.passes.synthesis" - "instead use AQCSynthesisPlugin from qiskit.transpiler.passes.synthesis", - ) - def __init__(self): - super().__init__() - - @deprecate_func( - since="0.46.0", - pending=True, - additional_msg="AQCSynthesisPlugin has been moved to qiskit.transpiler.passes.synthesis" - "instead use AQCSynthesisPlugin from qiskit.transpiler.passes.synthesis", - ) - def run(self, unitary, **options): - return super().run(unitary, **options) diff --git a/qiskit/transpiler/synthesis/aqc/cnot_structures.py b/qiskit/transpiler/synthesis/aqc/cnot_structures.py deleted file mode 100644 index 49ce1c3c7cbb..000000000000 --- a/qiskit/transpiler/synthesis/aqc/cnot_structures.py +++ /dev/null @@ -1,299 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -These are the CNOT structure methods: anything that you need for creating CNOT structures. -""" -import logging - -import numpy as np - -_NETWORK_LAYOUTS = ["sequ", "spin", "cart", "cyclic_spin", "cyclic_line"] -_CONNECTIVITY_TYPES = ["full", "line", "star"] - - -logger = logging.getLogger(__name__) - - -def _lower_limit(num_qubits: int) -> int: - """ - Returns lower limit on the number of CNOT units that guarantees exact representation of - a unitary operator by quantum gates. - - Args: - num_qubits: number of qubits. - - Returns: - lower limit on the number of CNOT units. - """ - num_cnots = round(np.ceil((4**num_qubits - 3 * num_qubits - 1) / 4.0)) - return num_cnots - - -def make_cnot_network( - num_qubits: int, - network_layout: str = "spin", - connectivity_type: str = "full", - depth: int = 0, -) -> np.ndarray: - """ - Generates a network consisting of building blocks each containing a CNOT gate and possibly some - single-qubit ones. This network models a quantum operator in question. Note, each building - block has 2 input and outputs corresponding to a pair of qubits. What we actually return here - is a chain of indices of qubit pairs shared by every building block in a row. - - Args: - num_qubits: number of qubits. - network_layout: type of network geometry, ``{"sequ", "spin", "cart", "cyclic_spin", - "cyclic_line"}``. - connectivity_type: type of inter-qubit connectivity, ``{"full", "line", "star"}``. - depth: depth of the CNOT-network, i.e. the number of layers, where each layer consists of - a single CNOT-block; default value will be selected, if ``L <= 0``. - - Returns: - A matrix of size ``(2, N)`` matrix that defines layers in cnot-network, where ``N`` - is either equal ``L``, or defined by a concrete type of the network. - - Raises: - ValueError: if unsupported type of CNOT-network layout or number of qubits or combination - of parameters are passed. - """ - if num_qubits < 2: - raise ValueError("Number of qubits must be greater or equal to 2") - - if depth <= 0: - new_depth = _lower_limit(num_qubits) - logger.debug( - "Number of CNOT units chosen as the lower limit: %d, got a non-positive value: %d", - new_depth, - depth, - ) - depth = new_depth - - if network_layout == "sequ": - links = _get_connectivity(num_qubits=num_qubits, connectivity=connectivity_type) - return _sequential_network(num_qubits=num_qubits, links=links, depth=depth) - - elif network_layout == "spin": - return _spin_network(num_qubits=num_qubits, depth=depth) - - elif network_layout == "cart": - cnots = _cartan_network(num_qubits=num_qubits) - logger.debug( - "Optimal lower bound: %d; Cartan CNOTs: %d", _lower_limit(num_qubits), cnots.shape[1] - ) - return cnots - - elif network_layout == "cyclic_spin": - if connectivity_type != "full": - raise ValueError(f"'{network_layout}' layout expects 'full' connectivity") - - return _cyclic_spin_network(num_qubits, depth) - - elif network_layout == "cyclic_line": - if connectivity_type != "line": - raise ValueError(f"'{network_layout}' layout expects 'line' connectivity") - - return _cyclic_line_network(num_qubits, depth) - else: - raise ValueError( - f"Unknown type of CNOT-network layout, expects one of {_NETWORK_LAYOUTS}, " - f"got {network_layout}" - ) - - -def _get_connectivity(num_qubits: int, connectivity: str) -> dict: - """ - Generates connectivity structure between qubits. - - Args: - num_qubits: number of qubits. - connectivity: type of connectivity structure, ``{"full", "line", "star"}``. - - Returns: - dictionary of allowed links between qubits. - - Raises: - ValueError: if unsupported type of CNOT-network layout is passed. - """ - if num_qubits == 1: - links = {0: [0]} - - elif connectivity == "full": - # Full connectivity between qubits. - links = {i: list(range(num_qubits)) for i in range(num_qubits)} - - elif connectivity == "line": - # Every qubit is connected to its immediate neighbours only. - links = {i: [i - 1, i, i + 1] for i in range(1, num_qubits - 1)} - - # first qubit - links[0] = [0, 1] - - # last qubit - links[num_qubits - 1] = [num_qubits - 2, num_qubits - 1] - - elif connectivity == "star": - # Every qubit is connected to the first one only. - links = {i: [0, i] for i in range(1, num_qubits)} - - # first qubit - links[0] = list(range(num_qubits)) - - else: - raise ValueError( - f"Unknown connectivity type, expects one of {_CONNECTIVITY_TYPES}, got {connectivity}" - ) - return links - - -def _sequential_network(num_qubits: int, links: dict, depth: int) -> np.ndarray: - """ - Generates a sequential network. - - Args: - num_qubits: number of qubits. - links: dictionary of connectivity links. - depth: depth of the network (number of layers of building blocks). - - Returns: - A matrix of ``(2, N)`` that defines layers in qubit network. - """ - layer = 0 - cnots = np.zeros((2, depth), dtype=int) - while True: - for i in range(0, num_qubits - 1): - for j in range(i + 1, num_qubits): - if j in links[i]: - cnots[0, layer] = i - cnots[1, layer] = j - layer += 1 - if layer >= depth: - return cnots - - -def _spin_network(num_qubits: int, depth: int) -> np.ndarray: - """ - Generates a spin-like network. - - Args: - num_qubits: number of qubits. - depth: depth of the network (number of layers of building blocks). - - Returns: - A matrix of size ``2 x L`` that defines layers in qubit network. - """ - layer = 0 - cnots = np.zeros((2, depth), dtype=int) - while True: - for i in range(0, num_qubits - 1, 2): - cnots[0, layer] = i - cnots[1, layer] = i + 1 - layer += 1 - if layer >= depth: - return cnots - - for i in range(1, num_qubits - 1, 2): - cnots[0, layer] = i - cnots[1, layer] = i + 1 - layer += 1 - if layer >= depth: - return cnots - - -def _cartan_network(num_qubits: int) -> np.ndarray: - """ - Cartan decomposition in a recursive way, starting from n = 3. - - Args: - num_qubits: number of qubits. - - Returns: - 2xN matrix that defines layers in qubit network, where N is the - depth of Cartan decomposition. - - Raises: - ValueError: if number of qubits is less than 3. - """ - n = num_qubits - if n > 3: - cnots = np.asarray([[0, 0, 0], [1, 1, 1]]) - mult = np.asarray([[n - 2, n - 3, n - 2, n - 3], [n - 1, n - 1, n - 1, n - 1]]) - for _ in range(n - 2): - cnots = np.hstack((np.tile(np.hstack((cnots, mult)), 3), cnots)) - mult[0, -1] -= 1 - mult = np.tile(mult, 2) - elif n == 3: - cnots = np.asarray( - [ - [0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], - [1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1], - ] - ) - else: - raise ValueError(f"The number of qubits must be >= 3, got {n}.") - - return cnots - - -def _cyclic_spin_network(num_qubits: int, depth: int) -> np.ndarray: - """ - Same as in the spin-like network, but the first and the last qubits are also connected. - - Args: - num_qubits: number of qubits. - depth: depth of the network (number of layers of building blocks). - - Returns: - A matrix of size ``2 x L`` that defines layers in qubit network. - """ - - cnots = np.zeros((2, depth), dtype=int) - z = 0 - while True: - for i in range(0, num_qubits, 2): - if i + 1 <= num_qubits - 1: - cnots[0, z] = i - cnots[1, z] = i + 1 - z += 1 - if z >= depth: - return cnots - - for i in range(1, num_qubits, 2): - if i + 1 <= num_qubits - 1: - cnots[0, z] = i - cnots[1, z] = i + 1 - z += 1 - elif i == num_qubits - 1: - cnots[0, z] = i - cnots[1, z] = 0 - z += 1 - if z >= depth: - return cnots - - -def _cyclic_line_network(num_qubits: int, depth: int) -> np.ndarray: - """ - Generates a line based CNOT structure. - - Args: - num_qubits: number of qubits. - depth: depth of the network (number of layers of building blocks). - - Returns: - A matrix of size ``2 x L`` that defines layers in qubit network. - """ - - cnots = np.zeros((2, depth), dtype=int) - for i in range(depth): - cnots[0, i] = (i + 0) % num_qubits - cnots[1, i] = (i + 1) % num_qubits - return cnots diff --git a/qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py b/qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py deleted file mode 100644 index 6973ae55d72f..000000000000 --- a/qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py +++ /dev/null @@ -1,103 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -This is the Parametric Circuit class: anything that you need for a circuit -to be parametrized and used for approximate compiling optimization. -""" -from __future__ import annotations -from typing import Optional - -import numpy as np - -from .approximate import ApproximateCircuit - - -class CNOTUnitCircuit(ApproximateCircuit): - """A class that represents an approximate circuit based on CNOT unit blocks.""" - - def __init__( - self, - num_qubits: int, - cnots: np.ndarray, - tol: Optional[float] = 0.0, - name: Optional[str] = None, - ) -> None: - """ - Args: - num_qubits: the number of qubits in this circuit. - cnots: an array of dimensions ``(2, L)`` indicating where the CNOT units will be placed. - tol: angle parameter less or equal this (small) value is considered equal zero and - corresponding gate is not inserted into the output circuit (because it becomes - identity one in this case). - name: name of this circuit - - Raises: - ValueError: if an unsupported parameter is passed. - """ - super().__init__(num_qubits=num_qubits, name=name) - - if cnots.ndim != 2 or cnots.shape[0] != 2: - raise ValueError("CNOT structure must be defined as an array of the size (2, N)") - - self._cnots = cnots - self._num_cnots = cnots.shape[1] - self._tol = tol - - # Thetas to be optimized by the AQC algorithm - self._thetas: np.ndarray | None = None - - @property - def thetas(self) -> np.ndarray: - """ - Returns a vector of rotation angles used by CNOT units in this circuit. - - Returns: - Parameters of the rotation gates in this circuit. - """ - return self._thetas - - def build(self, thetas: np.ndarray) -> None: - """ - Constructs a Qiskit quantum circuit out of the parameters (angles) of this circuit. If a - parameter value is less in absolute value than the specified tolerance then the - corresponding rotation gate will be skipped in the circuit. - """ - n = self.num_qubits - self._thetas = thetas - cnots = self._cnots - - for k in range(n): - # add initial three rotation gates for each qubit - p = 4 * self._num_cnots + 3 * k - k = n - k - 1 - if np.abs(thetas[2 + p]) > self._tol: - self.rz(thetas[2 + p], k) - if np.abs(thetas[1 + p]) > self._tol: - self.ry(thetas[1 + p], k) - if np.abs(thetas[0 + p]) > self._tol: - self.rz(thetas[0 + p], k) - - for c in range(self._num_cnots): - p = 4 * c - # Extract where the CNOT goes - q1 = n - 1 - int(cnots[0, c]) - q2 = n - 1 - int(cnots[1, c]) - # Construct a CNOT unit - self.cx(q1, q2) - if np.abs(thetas[0 + p]) > self._tol: - self.ry(thetas[0 + p], q1) - if np.abs(thetas[1 + p]) > self._tol: - self.rz(thetas[1 + p], q1) - if np.abs(thetas[2 + p]) > self._tol: - self.ry(thetas[2 + p], q2) - if np.abs(thetas[3 + p]) > self._tol: - self.rx(thetas[3 + p], q2) diff --git a/qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py b/qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py deleted file mode 100644 index b8bcd6ea9abd..000000000000 --- a/qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py +++ /dev/null @@ -1,299 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -A definition of the approximate circuit compilation optimization problem based on CNOT unit -definition. -""" -from __future__ import annotations -import typing -from abc import ABC - -import numpy as np -from numpy import linalg as la - -from .approximate import ApproximatingObjective -from .elementary_operations import ry_matrix, rz_matrix, place_unitary, place_cnot, rx_matrix - - -class CNOTUnitObjective(ApproximatingObjective, ABC): - """ - A base class for a problem definition based on CNOT unit. This class may have different - subclasses for objective and gradient computations. - """ - - def __init__(self, num_qubits: int, cnots: np.ndarray) -> None: - """ - Args: - num_qubits: number of qubits. - cnots: a CNOT structure to be used in the optimization procedure. - """ - super().__init__() - self._num_qubits = num_qubits - self._cnots = cnots - self._num_cnots = cnots.shape[1] - - @property - def num_cnots(self): - """ - Returns: - A number of CNOT units to be used by the approximate circuit. - """ - return self._num_cnots - - @property - def num_thetas(self): - """ - Returns: - Number of parameters (angles) of rotation gates in this circuit. - """ - return 3 * self._num_qubits + 4 * self._num_cnots - - -class DefaultCNOTUnitObjective(CNOTUnitObjective): - """A naive implementation of the objective function based on CNOT units.""" - - def __init__(self, num_qubits: int, cnots: np.ndarray) -> None: - """ - Args: - num_qubits: number of qubits. - cnots: a CNOT structure to be used in the optimization procedure. - """ - super().__init__(num_qubits, cnots) - - # last objective computations to be re-used by gradient - self._last_thetas: np.ndarray | None = None - self._cnot_right_collection: np.ndarray | None = None - self._cnot_left_collection: np.ndarray | None = None - self._rotation_matrix: int | np.ndarray | None = None - self._cnot_matrix: np.ndarray | None = None - - def objective(self, param_values: np.ndarray) -> typing.SupportsFloat: - # rename parameters just to make shorter and make use of our dictionary - thetas = param_values - n = self._num_qubits - d = int(2**n) - cnots = self._cnots - num_cnots = self.num_cnots - - # to save intermediate computations we define the following matrices - # this is the collection of cnot unit matrices ordered from left to - # right as in the circuit, not matrix product - cnot_unit_collection = np.zeros((d, d * num_cnots), dtype=complex) - # this is the collection of matrix products of the cnot units up - # to the given position from the right of the circuit - cnot_right_collection = np.zeros((d, d * num_cnots), dtype=complex) - # this is the collection of matrix products of the cnot units up - # to the given position from the left of the circuit - cnot_left_collection = np.zeros((d, d * num_cnots), dtype=complex) - # first, we construct each cnot unit matrix - for cnot_index in range(num_cnots): - theta_index = 4 * cnot_index - - # cnot qubit indices for the cnot unit identified by cnot_index - q1 = int(cnots[0, cnot_index]) - q2 = int(cnots[1, cnot_index]) - - # rotations that are applied on the q1 qubit - ry1 = ry_matrix(thetas[0 + theta_index]) - rz1 = rz_matrix(thetas[1 + theta_index]) - - # rotations that are applied on the q2 qubit - ry2 = ry_matrix(thetas[2 + theta_index]) - rx2 = rx_matrix(thetas[3 + theta_index]) - - # combine the rotations on qubits q1 and q2 - single_q1 = np.dot(rz1, ry1) - single_q2 = np.dot(rx2, ry2) - - # we place single qubit matrices at the corresponding locations in the (2^n, 2^n) matrix - full_q1 = place_unitary(single_q1, n, q1) - full_q2 = place_unitary(single_q2, n, q2) - - # we place a cnot matrix at the qubits q1 and q2 in the full matrix - cnot_q1q2 = place_cnot(n, q1, q2) - - # compute the cnot unit matrix and store in cnot_unit_collection - cnot_unit_collection[:, d * cnot_index : d * (cnot_index + 1)] = la.multi_dot( - [full_q2, full_q1, cnot_q1q2] - ) - - # this is the matrix corresponding to the intermediate matrix products - # it will end up being the matrix product of all the cnot unit matrices - # first we multiply from the right-hand side of the circuit - cnot_matrix = np.eye(d) - for cnot_index in range(num_cnots - 1, -1, -1): - cnot_matrix = np.dot( - cnot_matrix, cnot_unit_collection[:, d * cnot_index : d * (cnot_index + 1)] - ) - cnot_right_collection[:, d * cnot_index : d * (cnot_index + 1)] = cnot_matrix - # now we multiply from the left-hand side of the circuit - cnot_matrix = np.eye(d) - for cnot_index in range(num_cnots): - cnot_matrix = np.dot( - cnot_unit_collection[:, d * cnot_index : d * (cnot_index + 1)], cnot_matrix - ) - cnot_left_collection[:, d * cnot_index : d * (cnot_index + 1)] = cnot_matrix - - # this is the matrix corresponding to the initial rotations - # we start with 1 and kronecker product each qubit's rotations - rotation_matrix: int | np.ndarray = 1 - for q in range(n): - theta_index = 4 * num_cnots + 3 * q - rz0 = rz_matrix(thetas[0 + theta_index]) - ry1 = ry_matrix(thetas[1 + theta_index]) - rz2 = rz_matrix(thetas[2 + theta_index]) - rotation_matrix = np.kron(rotation_matrix, la.multi_dot([rz0, ry1, rz2])) - - # the matrix corresponding to the full circuit is the cnot part and - # rotation part multiplied together - circuit_matrix = np.dot(cnot_matrix, rotation_matrix) - - # compute error - error = 0.5 * (la.norm(circuit_matrix - self._target_matrix, "fro") ** 2) - - # cache computations for gradient - self._last_thetas = thetas - self._cnot_left_collection = cnot_left_collection - self._cnot_right_collection = cnot_right_collection - self._rotation_matrix = rotation_matrix - self._cnot_matrix = cnot_matrix - - return error - - def gradient(self, param_values: np.ndarray) -> np.ndarray: - # just to make shorter - thetas = param_values - # if given thetas are the same as used at the previous objective computations, then - # we re-use computations, otherwise we have to re-compute objective - if not np.all(np.isclose(thetas, self._last_thetas)): - self.objective(thetas) - - # the partial derivative of the circuit with respect to an angle - # is the same circuit with the corresponding pauli gate, multiplied - # by a global phase of -1j / 2, next to the rotation gate (it commutes) - pauli_x = np.multiply(-1j / 2, np.asarray([[0, 1], [1, 0]])) - pauli_y = np.multiply(-1j / 2, np.asarray([[0, -1j], [1j, 0]])) - pauli_z = np.multiply(-1j / 2, np.asarray([[1, 0], [0, -1]])) - - n = self._num_qubits - d = int(2**n) - cnots = self._cnots - num_cnots = self.num_cnots - - # the partial derivative of the cost function is -Re - # where V' is the partial derivative of the circuit - # first we compute the partial derivatives in the cnot part - der = np.zeros(4 * num_cnots + 3 * n) - for cnot_index in range(num_cnots): - theta_index = 4 * cnot_index - - # cnot qubit indices for the cnot unit identified by cnot_index - q1 = int(cnots[0, cnot_index]) - q2 = int(cnots[1, cnot_index]) - - # rotations that are applied on the q1 qubit - ry1 = ry_matrix(thetas[0 + theta_index]) - rz1 = rz_matrix(thetas[1 + theta_index]) - - # rotations that are applied on the q2 qubit - ry2 = ry_matrix(thetas[2 + theta_index]) - rx2 = rx_matrix(thetas[3 + theta_index]) - - # combine the rotations on qubits q1 and q2 - # note we have to insert an extra pauli gate to take the derivative - # of the appropriate rotation gate - for i in range(4): - if i == 0: - single_q1 = la.multi_dot([rz1, pauli_y, ry1]) - single_q2 = np.dot(rx2, ry2) - elif i == 1: - single_q1 = la.multi_dot([pauli_z, rz1, ry1]) - single_q2 = np.dot(rx2, ry2) - elif i == 2: - single_q1 = np.dot(rz1, ry1) - single_q2 = la.multi_dot([rx2, pauli_y, ry2]) - else: - single_q1 = np.dot(rz1, ry1) - single_q2 = la.multi_dot([pauli_x, rx2, ry2]) - - # we place single qubit matrices at the corresponding locations in - # the (2^n, 2^n) matrix - full_q1 = place_unitary(single_q1, n, q1) - full_q2 = place_unitary(single_q2, n, q2) - - # we place a cnot matrix at the qubits q1 and q2 in the full matrix - cnot_q1q2 = place_cnot(n, q1, q2) - - # partial derivative of that particular cnot unit, size of (2^n, 2^n) - der_cnot_unit = la.multi_dot([full_q2, full_q1, cnot_q1q2]) - # der_cnot_unit is multiplied by the matrix product of cnot units to the left - # of it (if there are any) and to the right of it (if there are any) - if cnot_index == 0: - der_cnot_matrix = np.dot( - self._cnot_right_collection[:, d : 2 * d], - der_cnot_unit, - ) - elif num_cnots - 1 == cnot_index: - der_cnot_matrix = np.dot( - der_cnot_unit, - self._cnot_left_collection[:, d * (num_cnots - 2) : d * (num_cnots - 1)], - ) - else: - der_cnot_matrix = la.multi_dot( - [ - self._cnot_right_collection[ - :, d * (cnot_index + 1) : d * (cnot_index + 2) - ], - der_cnot_unit, - self._cnot_left_collection[:, d * (cnot_index - 1) : d * cnot_index], - ] - ) - - # the matrix corresponding to the full circuit partial derivative - # is the partial derivative of the cnot part multiplied by the usual - # rotation part - der_circuit_matrix = np.dot(der_cnot_matrix, self._rotation_matrix) - # we compute the partial derivative of the cost function - der[i + theta_index] = -np.real( - np.trace(np.dot(der_circuit_matrix.conj().T, self._target_matrix)) - ) - - # now we compute the partial derivatives in the rotation part - # we start with 1 and kronecker product each qubit's rotations - for i in range(3 * n): - der_rotation_matrix: int | np.ndarray = 1 - for q in range(n): - theta_index = 4 * num_cnots + 3 * q - rz0 = rz_matrix(thetas[0 + theta_index]) - ry1 = ry_matrix(thetas[1 + theta_index]) - rz2 = rz_matrix(thetas[2 + theta_index]) - # for the appropriate rotation gate that we are taking - # the partial derivative of, we have to insert the - # corresponding pauli matrix - if i - 3 * q == 0: - rz0 = np.dot(pauli_z, rz0) - elif i - 3 * q == 1: - ry1 = np.dot(pauli_y, ry1) - elif i - 3 * q == 2: - rz2 = np.dot(pauli_z, rz2) - der_rotation_matrix = np.kron(der_rotation_matrix, la.multi_dot([rz0, ry1, rz2])) - - # the matrix corresponding to the full circuit partial derivative - # is the usual cnot part multiplied by the partial derivative of - # the rotation part - der_circuit_matrix = np.dot(self._cnot_matrix, der_rotation_matrix) - # we compute the partial derivative of the cost function - der[4 * num_cnots + i] = -np.real( - np.trace(np.dot(der_circuit_matrix.conj().T, self._target_matrix)) - ) - - return der diff --git a/qiskit/transpiler/synthesis/aqc/elementary_operations.py b/qiskit/transpiler/synthesis/aqc/elementary_operations.py deleted file mode 100644 index b5739267e793..000000000000 --- a/qiskit/transpiler/synthesis/aqc/elementary_operations.py +++ /dev/null @@ -1,108 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -These are a number of elementary functions that are required for the AQC routines to work. -""" - -import numpy as np - -from qiskit.circuit.library import RXGate, RZGate, RYGate - - -def place_unitary(unitary: np.ndarray, n: int, j: int) -> np.ndarray: - """ - Computes I(j - 1) tensor product U tensor product I(n - j), where U is a unitary matrix - of size ``(2, 2)``. - - Args: - unitary: a unitary matrix of size ``(2, 2)``. - n: num qubits. - j: position where to place a unitary. - - Returns: - a unitary of n qubits with u in position j. - """ - return np.kron(np.kron(np.eye(2**j), unitary), np.eye(2 ** (n - 1 - j))) - - -def place_cnot(n: int, j: int, k: int) -> np.ndarray: - """ - Places a CNOT from j to k. - - Args: - n: number of qubits. - j: control qubit. - k: target qubit. - - Returns: - a unitary of n qubits with CNOT placed at ``j`` and ``k``. - """ - if j < k: - unitary = np.kron( - np.kron(np.eye(2**j), [[1, 0], [0, 0]]), np.eye(2 ** (n - 1 - j)) - ) + np.kron( - np.kron( - np.kron(np.kron(np.eye(2**j), [[0, 0], [0, 1]]), np.eye(2 ** (k - j - 1))), - [[0, 1], [1, 0]], - ), - np.eye(2 ** (n - 1 - k)), - ) - else: - unitary = np.kron( - np.kron(np.eye(2**j), [[1, 0], [0, 0]]), np.eye(2 ** (n - 1 - j)) - ) + np.kron( - np.kron( - np.kron(np.kron(np.eye(2**k), [[0, 1], [1, 0]]), np.eye(2 ** (j - k - 1))), - [[0, 0], [0, 1]], - ), - np.eye(2 ** (n - 1 - j)), - ) - return unitary - - -def rx_matrix(phi: float) -> np.ndarray: - """ - Computes an RX rotation by the angle of ``phi``. - - Args: - phi: rotation angle. - - Returns: - an RX rotation matrix. - """ - return RXGate(phi).to_matrix() - - -def ry_matrix(phi: float) -> np.ndarray: - """ - Computes an RY rotation by the angle of ``phi``. - - Args: - phi: rotation angle. - - Returns: - an RY rotation matrix. - """ - return RYGate(phi).to_matrix() - - -def rz_matrix(phi: float) -> np.ndarray: - """ - Computes an RZ rotation by the angle of ``phi``. - - Args: - phi: rotation angle. - - Returns: - an RZ rotation matrix. - """ - return RZGate(phi).to_matrix() diff --git a/qiskit/transpiler/synthesis/aqc/fast_gradient/__init__.py b/qiskit/transpiler/synthesis/aqc/fast_gradient/__init__.py deleted file mode 100644 index df13193f12bb..000000000000 --- a/qiskit/transpiler/synthesis/aqc/fast_gradient/__init__.py +++ /dev/null @@ -1,164 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -r""" -================================================================================ -Fast implementation of objective function class -(:mod:`qiskit.transpiler.synthesis.aqc.fast_gradient`) -================================================================================ - -.. currentmodule:: qiskit.transpiler.synthesis.aqc.fast_gradient - -Extension to the implementation of Approximate Quantum Compiler as described in the paper [1]. - -Interface -========= - -The main public class of this module is FastCNOTUnitObjective. It replaces the default objective -function implementation :class:`.DefaultCNOTUnitObjective` for faster computation. -The individual classes include the public one (FastCNOTUnitObjective) and few -internal ones: - -.. autosummary:: - :toctree: ../stubs - :template: autosummary/class_no_inherited_members.rst - - FastCNOTUnitObjective - LayerBase - Layer1Q - Layer2Q - PMatrix - - -Mathematical Details -==================== - -In what follows we briefly outline the main ideas underlying the accelerated implementation -of objective function class. - -* The key ingredient of approximate compiling is the efficient optimization procedure - that minimizes :math:`\|V - U\|_{\mathrm{F}}` on a classical computer, where :math:`U` - is a given (target) unitary matrix and :math:`V` is a matrix of approximating quantum - circuit. Alternatively, we maximize the Hilbert-Schmidt product between :math:`U` and - :math:`V` as outlined in the main part of the documentation. - -* The circuit :math:`V` can be represented as a sequence of 2-qubit gates (layers) - applied one after another. The corresponding matrix takes the form: - :math:`V = C_0 C_1 \ldots C_{L-1} F`, where :math:`L` is the length of the sequence - (number of layers). If the total number of qubits :math:`n > 2`, every - :math:`C_i = C_i(\Theta_i)` is a sparse, :math:`2^n \times 2^n` matrix of 2-qubit gate - (CNOT unit block) parameterized by a sub-set of parameters :math:`\Theta_i` - (4 parameters per unit block), and :math:`F` is a matrix that comprises the action - of all 1-qubit gates in front of approximating circuit. See the paper [1] for details. - -* Over the course of optimization we compute the value of objective function and its - gradient, which implies computation of :math:`V` and its derivatives - :math:`{\partial V}/{\partial \Theta_i}` for all :math:`i`, given the current estimation - of all the parameters :math:`\Theta`. - -* A naive implementation of the product :math:`V = C_0 C_1 \ldots C_{L-1} F` and its - derivatives would include computation and memorization of forward and backward partial - products as required by the backtracking algorithm. This is wasteful in terms of - performance and resource allocation. - -* Minimization of :math:`\|V - U\|_{\mathrm{F}}^2` is equivalent to maximization of - :math:`\text{Re}\left(\text{Tr}\left(U^{\dagger} V\right)\right)`. By cyclic permutation - of the sequence of matrices under trace operation, we can avoid memorization of intermediate - partial products of gate matrices :math:`C_i`. Note, matrix size grows exponentially with - the number of qubits, quickly becoming prohibitively large. - -* Sparse structure of :math:`C_i` can be exploited to speed up matrix-matrix multiplication. - However, using sparse matrices as such does not give performance gain because sparse patterns - tend to violate data proximity inside the cache memory of modern CPUs. Instead, we make use - of special structure of gate matrices :math:`C_i` coupled with permutation ones. Although - permutation is not cache friendly either, its impact is seemingly less severe than that - of sparse matrix multiplication (at least in Python implementation). - -* On every optimization iteration we, first, compute :math:`V = C_0 C_1 \ldots C_{L-1} F` - given the current estimation of all the parameters :math:`\Theta`. - -* As for the gradient of objective function, it can be shown (by moving cyclically around - an individual matrices under trace operation) that: - -.. math:: - \text{Tr}\left( U^{\dagger} \frac{\partial V}{\partial \Theta_{l,k}} \right) = - \langle \text{vec}\left(E_l\right), \text{vec}\left( - \frac{\partial C_l}{\partial \Theta_{l,k}}\right) \rangle, - -where :math:`\Theta_{l,k}` is a :math:`k`-th parameter of :math:`l`-th CNOT unit block, -and :math:`E_l=C_{l-1}\left(C_{l-2}\left(\cdots\left(C_0\left(U^{\dagger}V -C_0^{\dagger}\right)C_1^{\dagger}\right) \cdots\right)C_{l-1}^{\dagger}\right)C_l^{\dagger}` -is an intermediate matrix. - -* For every :math:`l`-th gradient component, we compute the trace using the matrix - :math:`E_l`, then this matrix is updated by multiplication on left and on the right - by corresponding gate matrices :math:`C_l` and :math:`C_{l+1}^{\dagger}` respectively - and proceed to the next gradient component. - -* We save computations and resources by not storing intermediate partial products of - :math:`C_i`. Instead, incrementally updated matrix :math:`E_l` keeps all related - information. Also, vectorization of involved matrices (see the above formula) allows - us to replace matrix-matrix multiplication by "cheaper" vector-vector one under the - trace operation. - -* The matrices :math:`C_i` are sparse. However, even for relatively small matrices - (< 1M elements) sparse-dense multiplication can be very slow. Construction of sparse - matrices takes a time as well. We should update every gate matrix on each iteration - of optimization loop. - -* In fact, any gate matrix :math:`C_i` can be transformed to what we call a standard - form: :math:`C_i = P^T \widetilde{C}_i P`, where :math:`P` is an easily computable - permutation matrix and :math:`\widetilde{C}_i` has a block-diagonal layout: - -.. math:: - \widetilde{C}_i = \left( - \begin{array}{ccc} - G_{4 \times 4} & \ddots & 0 \\ - \ddots & \ddots & \ddots \\ - 0 & \ddots & G_{4 \times 4} - \end{array} - \right) - -* The 2-qubit gate matrix :math:`G_{4 \times 4}` is repeated along diagonal of the full - :math:`2^n \times 2^n` :math:`\widetilde{C}_i`. - -* We do not actually create neither matrix :math:`\widetilde{C}_i` nor :math:`P`. - In fact, only :math:`G_{4 \times 4}` and a permutation array (of size :math:`2^n`) - are kept in memory. - -* Consider left-hand side multiplication by some dense, :math:`2^n \times 2^n` matrix :math:`M`: - -.. math:: - C_i M = P^T \widetilde{C}_i P M = P^T \left( \widetilde{C}_i \left( P M \right) \right) - -* First, we permute rows of :math:`M`, which is equivalent to the product :math:`P M`, but - without expensive multiplication of two :math:`2^n \times 2^n` matrices. - -* Second, we compute :math:`\widetilde{C}_i P M` multiplying every block-diagonal sub-matrix - :math:`G_{4 \times 4}` by the corresponding rows of :math:`P M`. This is the dense-dense - matrix multiplication, which is very well optimized on modern CPUs. Important: the total - number of operations is :math:`O(2^{2 n})` in contrast to :math:`O(2^{3 n})` as in general - case. - -* Third, we permute rows of :math:`\widetilde{C}_i P M` by applying :math:`P^T`. - -* Right-hand side multiplication is done in a similar way. - -* In summary, we save computational resources by exploiting some properties of 2-qubit gate - matrices :math:`C_i` and using hardware optimized multiplication of dense matrices. There - is still a room for further improvement, of course. - -References: - - [1]: Liam Madden, Andrea Simonetto, Best Approximate Quantum Compiling Problems. - `arXiv:2106.05649 `_ -""" diff --git a/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py b/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py deleted file mode 100644 index b13c96ea3fe5..000000000000 --- a/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py +++ /dev/null @@ -1,237 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Utility functions in the fast gradient implementation. -""" -from __future__ import annotations -from typing import Union -import numpy as np - - -def is_permutation(x: np.ndarray) -> bool: - """ - Checks if array is really an index permutation. - - Args: - 1D-array of integers that supposedly represents a permutation. - - Returns: - True, if array is really a permutation of indices. - """ - return ( - isinstance(x, np.ndarray) - and x.ndim == 1 - and x.dtype == np.int64 - and np.all(np.sort(x) == np.arange(x.size, dtype=np.int64)) - ) - - -def reverse_bits(x: Union[int, np.ndarray], nbits: int, enable: bool) -> Union[int, np.ndarray]: - """ - Reverses the bit order in a number of ``nbits`` length. - If ``x`` is an array, then operation is applied to every entry. - - Args: - x: either a single integer or an array of integers. - nbits: number of meaningful bits in the number x. - enable: apply reverse operation, if enabled, otherwise leave unchanged. - - Returns: - a number or array of numbers with reversed bits. - """ - - if not enable: - if isinstance(x, int): - pass - else: - x = x.copy() - return x - - if isinstance(x, int): - res: int | np.ndarray = int(0) - else: - x = x.copy() - res = np.full_like(x, fill_value=0) - - for _ in range(nbits): - res <<= 1 - res |= x & 1 - x >>= 1 - return res - - -def swap_bits(num: int, a: int, b: int) -> int: - """ - Swaps the bits at positions 'a' and 'b' in the number 'num'. - - Args: - num: an integer number where bits should be swapped. - a: index of the first bit to be swapped. - b: index of the second bit to be swapped. - - Returns: - the number with swapped bits. - """ - x = ((num >> a) ^ (num >> b)) & 1 - return num ^ ((x << a) | (x << b)) - - -def bit_permutation_1q(n: int, k: int) -> np.ndarray: - """ - Constructs index permutation that brings a circuit consisting of a single - 1-qubit gate to "standard form": ``kron(I(2^n/2), G)``, as we call it. Here n - is the number of qubits, ``G`` is a 2x2 gate matrix, ``I(2^n/2)`` is the identity - matrix of size ``(2^n/2)x(2^n/2)``, and the full size of the circuit matrix is - ``(2^n)x(2^n)``. Circuit matrix in standard form becomes block-diagonal (with - sub-matrices ``G`` on the main diagonal). Multiplication of such a matrix and - a dense one is much faster than generic dense-dense product. Moreover, - we do not need to keep the entire circuit matrix in memory but just 2x2 ``G`` - one. This saves a lot of memory when the number of qubits is large. - - Args: - n: number of qubits. - k: index of qubit where single 1-qubit gate is applied. - - Returns: - permutation that brings the whole layer to the standard form. - """ - perm = np.arange(2**n, dtype=np.int64) - if k != n - 1: - for v in range(2**n): - perm[v] = swap_bits(v, k, n - 1) - return perm - - -def bit_permutation_2q(n: int, j: int, k: int) -> np.ndarray: - """ - Constructs index permutation that brings a circuit consisting of a single - 2-qubit gate to "standard form": ``kron(I(2^n/4), G)``, as we call it. Here ``n`` - is the number of qubits, ``G`` is a 4x4 gate matrix, ``I(2^n/4)`` is the identity - matrix of size ``(2^n/4)x(2^n/4)``, and the full size of the circuit matrix is - ``(2^n)x(2^n)``. Circuit matrix in standard form becomes block-diagonal (with - sub-matrices ``G`` on the main diagonal). Multiplication of such a matrix and - a dense one is much faster than generic dense-dense product. Moreover, - we do not need to keep the entire circuit matrix in memory but just 4x4 ``G`` - one. This saves a lot of memory when the number of qubits is large. - - Args: - n: number of qubits. - j: index of control qubit where single 2-qubit gate is applied. - k: index of target qubit where single 2-qubit gate is applied. - - Returns: - permutation that brings the whole layer to the standard form. - """ - dim = 2**n - perm = np.arange(dim, dtype=np.int64) - if j < n - 2: - if k < n - 2: - for v in range(dim): - perm[v] = swap_bits(swap_bits(v, j, n - 2), k, n - 1) - elif k == n - 2: - for v in range(dim): - perm[v] = swap_bits(swap_bits(v, n - 2, n - 1), j, n - 2) - else: - for v in range(dim): - perm[v] = swap_bits(v, j, n - 2) - elif j == n - 2: - if k < n - 2: - for v in range(dim): - perm[v] = swap_bits(v, k, n - 1) - else: - pass - else: - if k < n - 2: - for v in range(dim): - perm[v] = swap_bits(swap_bits(v, n - 2, n - 1), k, n - 1) - else: - for v in range(dim): - perm[v] = swap_bits(v, n - 2, n - 1) - return perm - - -def inverse_permutation(perm: np.ndarray) -> np.ndarray: - """ - Returns inverse permutation. - - Args: - perm: permutation to be reversed. - - Returns: - inverse permutation. - """ - inv = np.zeros_like(perm) - inv[perm] = np.arange(perm.size, dtype=np.int64) - return inv - - -def make_rx(phi: float, out: np.ndarray) -> np.ndarray: - """ - Makes a 2x2 matrix that corresponds to X-rotation gate. - This is a fast implementation that does not allocate the output matrix. - - Args: - phi: rotation angle. - out: placeholder for the result (2x2, complex-valued matrix). - - Returns: - rotation gate, same object as referenced by "out". - """ - a = 0.5 * phi - cs, sn = np.cos(a).item(), -1j * np.sin(a).item() - out[0, 0] = cs - out[0, 1] = sn - out[1, 0] = sn - out[1, 1] = cs - return out - - -def make_ry(phi: float, out: np.ndarray) -> np.ndarray: - """ - Makes a 2x2 matrix that corresponds to Y-rotation gate. - This is a fast implementation that does not allocate the output matrix. - - Args: - phi: rotation angle. - out: placeholder for the result (2x2, complex-valued matrix). - - Returns: - rotation gate, same object as referenced by "out". - """ - a = 0.5 * phi - cs, sn = np.cos(a).item(), np.sin(a).item() - out[0, 0] = cs - out[0, 1] = -sn - out[1, 0] = sn - out[1, 1] = cs - return out - - -def make_rz(phi: float, out: np.ndarray) -> np.ndarray: - """ - Makes a 2x2 matrix that corresponds to Z-rotation gate. - This is a fast implementation that does not allocate the output matrix. - - Args: - phi: rotation angle. - out: placeholder for the result (2x2, complex-valued matrix). - - Returns: - rotation gate, same object as referenced by "out". - """ - exp = np.exp(0.5j * phi).item() - out[0, 0] = 1.0 / exp - out[0, 1] = 0 - out[1, 0] = 0 - out[1, 1] = exp - return out diff --git a/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_gradient.py b/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_gradient.py deleted file mode 100644 index dc5078acf1a9..000000000000 --- a/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_gradient.py +++ /dev/null @@ -1,225 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Implementation of the fast objective function class. -""" - -import warnings -import numpy as np - -from .layer import ( - LayerBase, - Layer2Q, - Layer1Q, - init_layer2q_matrices, - init_layer2q_deriv_matrices, - init_layer1q_matrices, - init_layer1q_deriv_matrices, -) -from .pmatrix import PMatrix -from ..cnot_unit_objective import CNOTUnitObjective - - -class FastCNOTUnitObjective(CNOTUnitObjective): - """ - Implementation of objective function and gradient calculator, which is - similar to - :class:`~qiskit.transpiler.aqc.DefaultCNOTUnitObjective` - but several times faster. - """ - - def __init__(self, num_qubits: int, cnots: np.ndarray): - super().__init__(num_qubits, cnots) - - if not 2 <= num_qubits <= 16: - raise ValueError("expects number of qubits in the range [2..16]") - - dim = 2**num_qubits - self._ucf_mat = PMatrix(num_qubits) # U^dagger @ C @ F - self._fuc_mat = PMatrix(num_qubits) # F @ U^dagger @ C - self._circ_thetas = np.zeros((self.num_thetas,)) # last thetas used - - # array of C-layers: - self._c_layers = np.asarray([object()] * self.num_cnots, dtype=LayerBase) - # array of F-layers: - self._f_layers = np.asarray([object()] * num_qubits, dtype=LayerBase) - # 4x4 C-gate matrices: - self._c_gates = np.full((self.num_cnots, 4, 4), fill_value=0, dtype=np.complex128) - # derivatives of 4x4 C-gate matrices: - self._c_dervs = np.full((self.num_cnots, 4, 4, 4), fill_value=0, dtype=np.complex128) - # 4x4 F-gate matrices: - self._f_gates = np.full((num_qubits, 2, 2), fill_value=0, dtype=np.complex128) - # derivatives of 4x4 F-gate matrices: - self._f_dervs = np.full((num_qubits, 3, 2, 2), fill_value=0, dtype=np.complex128) - # temporary NxN matrices: - self._tmp1 = np.full((dim, dim), fill_value=0, dtype=np.complex128) - self._tmp2 = np.full((dim, dim), fill_value=0, dtype=np.complex128) - - # Create layers of 2-qubit gates. - for q in range(self.num_cnots): - j = int(cnots[0, q]) - k = int(cnots[1, q]) - self._c_layers[q] = Layer2Q(num_qubits=num_qubits, j=j, k=k) - - # Create layers of 1-qubit gates. - for k in range(num_qubits): - self._f_layers[k] = Layer1Q(num_qubits=num_qubits, k=k) - - def objective(self, param_values: np.ndarray) -> float: - """ - Computes the objective function and some intermediate data for - the subsequent gradient computation. - See description of the base class method. - """ - depth, n = self.num_cnots, self._num_qubits - - # Memorize the last angular parameters used to compute the objective. - if self._circ_thetas.size == 0: - self._circ_thetas = np.zeros((self.num_thetas,)) - np.copyto(self._circ_thetas, param_values) - - thetas4d = param_values[: 4 * depth].reshape(depth, 4) - thetas3n = param_values[4 * depth :].reshape(n, 3) - - init_layer2q_matrices(thetas=thetas4d, dst=self._c_gates) - init_layer2q_deriv_matrices(thetas=thetas4d, dst=self._c_dervs) - init_layer1q_matrices(thetas=thetas3n, dst=self._f_gates) - init_layer1q_deriv_matrices(thetas=thetas3n, dst=self._f_dervs) - - self._init_layers() - self._calc_ucf_fuc() - objective_value = self._calc_objective_function() - return objective_value - - def gradient(self, param_values: np.ndarray) -> np.ndarray: - """ - Computes the gradient of objective function. - See description of the base class method. - """ - - # If thetas are the same as used for objective value calculation - # before calling this function, then we re-use the computations, - # otherwise we have to re-compute the objective. - tol = float(np.sqrt(np.finfo(float).eps)) - if not np.allclose(param_values, self._circ_thetas, atol=tol, rtol=tol): - self.objective(param_values) - warnings.warn("gradient is computed before the objective") - - grad = np.full((param_values.size,), fill_value=0, dtype=np.float64) - grad4d = grad[: 4 * self.num_cnots].reshape(self.num_cnots, 4) - grad3n = grad[4 * self.num_cnots :].reshape(self._num_qubits, 3) - self._calc_gradient4d(grad4d) - self._calc_gradient3n(grad3n) - return grad - - def _init_layers(self): - """ - Initializes C-layers and F-layers by corresponding gate matrices. - """ - c_gates = self._c_gates - c_layers = self._c_layers - for q in range(self.num_cnots): - c_layers[q].set_from_matrix(mat=c_gates[q]) - - f_gates = self._f_gates - f_layers = self._f_layers - for q in range(self._num_qubits): - f_layers[q].set_from_matrix(mat=f_gates[q]) - - def _calc_ucf_fuc(self): - """ - Computes matrices ``ucf_mat`` and ``fuc_mat``. Both remain non-finalized. - """ - ucf_mat = self._ucf_mat - fuc_mat = self._fuc_mat - tmp1 = self._tmp1 - c_layers = self._c_layers - f_layers = self._f_layers - depth, n = self.num_cnots, self._num_qubits - - # tmp1 = U^dagger. - np.conj(self.target_matrix.T, out=tmp1) - - # ucf_mat = fuc_mat = U^dagger @ C = U^dagger @ C_{depth-1} @ ... @ C_{0}. - self._ucf_mat.set_matrix(tmp1) - for q in range(depth - 1, -1, -1): - ucf_mat.mul_right_q2(c_layers[q], temp_mat=tmp1, dagger=False) - fuc_mat.set_matrix(ucf_mat.finalize(temp_mat=tmp1)) - - # fuc_mat = F @ U^dagger @ C = F_{n-1} @ ... @ F_{0} @ U^dagger @ C. - for q in range(n): - fuc_mat.mul_left_q1(f_layers[q], temp_mat=tmp1) - - # ucf_mat = U^dagger @ C @ F = U^dagger @ C @ F_{n-1} @ ... @ F_{0}. - for q in range(n - 1, -1, -1): - ucf_mat.mul_right_q1(f_layers[q], temp_mat=tmp1, dagger=False) - - def _calc_objective_function(self) -> float: - """ - Computes the value of objective function. - """ - ucf = self._ucf_mat.finalize(temp_mat=self._tmp1) - trace_ucf = np.trace(ucf) - fobj = abs((2**self._num_qubits) - float(np.real(trace_ucf))) - - return fobj - - def _calc_gradient4d(self, grad4d: np.ndarray): - """ - Calculates a part gradient contributed by 2-qubit gates. - """ - fuc = self._fuc_mat - tmp1, tmp2 = self._tmp1, self._tmp2 - c_gates = self._c_gates - c_dervs = self._c_dervs - c_layers = self._c_layers - for q in range(self.num_cnots): - # fuc[q] <-- C[q-1] @ fuc[q-1] @ C[q].conj.T. Note, c_layers[q] has - # been initialized in _init_layers(), however, c_layers[q-1] was - # reused on the previous step, see below, so we need to restore it. - if q > 0: - c_layers[q - 1].set_from_matrix(mat=c_gates[q - 1]) - fuc.mul_left_q2(c_layers[q - 1], temp_mat=tmp1) - fuc.mul_right_q2(c_layers[q], temp_mat=tmp1, dagger=True) - fuc.finalize(temp_mat=tmp1) - # Compute gradient components. We reuse c_layers[q] several times. - for i in range(4): - c_layers[q].set_from_matrix(mat=c_dervs[q, i]) - grad4d[q, i] = (-1) * np.real( - fuc.product_q2(layer=c_layers[q], tmp1=tmp1, tmp2=tmp2) - ) - - def _calc_gradient3n(self, grad3n: np.ndarray): - """ - Calculates a part gradient contributed by 1-qubit gates. - """ - ucf = self._ucf_mat - tmp1, tmp2 = self._tmp1, self._tmp2 - f_gates = self._f_gates - f_dervs = self._f_dervs - f_layers = self._f_layers - for q in range(self._num_qubits): - # ucf[q] <-- F[q-1] @ ucf[q-1] @ F[q].conj.T. Note, f_layers[q] has - # been initialized in _init_layers(), however, f_layers[q-1] was - # reused on the previous step, see below, so we need to restore it. - if q > 0: - f_layers[q - 1].set_from_matrix(mat=f_gates[q - 1]) - ucf.mul_left_q1(f_layers[q - 1], temp_mat=tmp1) - ucf.mul_right_q1(f_layers[q], temp_mat=tmp1, dagger=True) - ucf.finalize(temp_mat=tmp1) - # Compute gradient components. We reuse f_layers[q] several times. - for i in range(3): - f_layers[q].set_from_matrix(mat=f_dervs[q, i]) - grad3n[q, i] = (-1) * np.real( - ucf.product_q1(layer=f_layers[q], tmp1=tmp1, tmp2=tmp2) - ) diff --git a/qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py b/qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py deleted file mode 100644 index 6cb763afbc4f..000000000000 --- a/qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py +++ /dev/null @@ -1,370 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Layer classes for the fast gradient implementation. -""" -from __future__ import annotations -from abc import abstractmethod, ABC -from typing import Optional -import numpy as np -from .fast_grad_utils import ( - bit_permutation_1q, - reverse_bits, - inverse_permutation, - bit_permutation_2q, - make_rz, - make_ry, -) - - -class LayerBase(ABC): - """ - Base class for any layer implementation. Each layer here is represented - by a 2x2 or 4x4 gate matrix ``G`` (applied to 1 or 2 qubits respectively) - interleaved with the identity ones: - ``Layer = I kron I kron ... kron G kron ... kron I kron I`` - """ - - @abstractmethod - def set_from_matrix(self, mat: np.ndarray): - """ - Updates this layer from an external gate matrix. - - Args: - mat: external gate matrix that initializes this layer's one. - """ - raise NotImplementedError() - - @abstractmethod - def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """ - Returns gate matrix, direct and inverse permutations. - - Returns: - (1) gate matrix; (2) direct permutation; (3) inverse permutations. - """ - raise NotImplementedError() - - -class Layer1Q(LayerBase): - """ - Layer represents a simple circuit where 1-qubit gate matrix (of size 2x2) - interleaves with the identity ones. - """ - - def __init__(self, num_qubits: int, k: int, g2x2: Optional[np.ndarray] = None): - """ - Args: - num_qubits: number of qubits. - k: index of the bit where gate is applied. - g2x2: 2x2 matrix that makes up this layer along with identity ones, - or None (should be set up later). - """ - super().__init__() - - # 2x2 gate matrix (1-qubit gate). - self._gmat = np.full((2, 2), fill_value=0, dtype=np.complex128) - if isinstance(g2x2, np.ndarray): - np.copyto(self._gmat, g2x2) - - bit_flip = True - dim = 2**num_qubits - row_perm = reverse_bits( - bit_permutation_1q(n=num_qubits, k=k), nbits=num_qubits, enable=bit_flip - ) - col_perm = reverse_bits(np.arange(dim, dtype=np.int64), nbits=num_qubits, enable=bit_flip) - self._perm = np.full((dim,), fill_value=0, dtype=np.int64) - self._perm[row_perm] = col_perm - self._inv_perm = inverse_permutation(self._perm) - - def set_from_matrix(self, mat: np.ndarray): - """See base class description.""" - np.copyto(self._gmat, mat) - - def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """See base class description.""" - return self._gmat, self._perm, self._inv_perm - - -class Layer2Q(LayerBase): - """ - Layer represents a simple circuit where 2-qubit gate matrix (of size 4x4) - interleaves with the identity ones. - """ - - def __init__(self, num_qubits: int, j: int, k: int, g4x4: Optional[np.ndarray] = None): - """ - Args: - num_qubits: number of qubits. - j: index of the first (control) bit. - k: index of the second (target) bit. - g4x4: 4x4 matrix that makes up this layer along with identity ones, - or None (should be set up later). - """ - super().__init__() - - # 4x4 gate matrix (2-qubit gate). - self._gmat = np.full((4, 4), fill_value=0, dtype=np.complex128) - if isinstance(g4x4, np.ndarray): - np.copyto(self._gmat, g4x4) - - bit_flip = True - dim = 2**num_qubits - row_perm = reverse_bits( - bit_permutation_2q(n=num_qubits, j=j, k=k), nbits=num_qubits, enable=bit_flip - ) - col_perm = reverse_bits(np.arange(dim, dtype=np.int64), nbits=num_qubits, enable=bit_flip) - self._perm = np.full((dim,), fill_value=0, dtype=np.int64) - self._perm[row_perm] = col_perm - self._inv_perm = inverse_permutation(self._perm) - - def set_from_matrix(self, mat: np.ndarray): - """See base class description.""" - np.copyto(self._gmat, mat) - - def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """See base class description.""" - return self._gmat, self._perm, self._inv_perm - - -def init_layer1q_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: - """ - Initializes 4x4 matrices of 2-qubit gates defined in the paper. - - Args: - thetas: depth x 4 matrix of gate parameters for every layer, where - "depth" is the number of layers. - dst: destination array of size depth x 4 x 4 that will receive gate - matrices of each layer. - - Returns: - Returns the "dst" array. - """ - n = thetas.shape[0] - tmp = np.full((4, 2, 2), fill_value=0, dtype=np.complex128) - for k in range(n): - th = thetas[k] - a = make_rz(th[0], out=tmp[0]) - b = make_ry(th[1], out=tmp[1]) - c = make_rz(th[2], out=tmp[2]) - np.dot(np.dot(a, b, out=tmp[3]), c, out=dst[k]) - return dst - - -def init_layer1q_deriv_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: - """ - Initializes 4x4 derivative matrices of 2-qubit gates defined in the paper. - - Args: - thetas: depth x 4 matrix of gate parameters for every layer, where - "depth" is the number of layers. - dst: destination array of size depth x 4 x 4 x 4 that will receive gate - derivative matrices of each layer; there are 4 parameters per gate, - hence, 4 derivative matrices per layer. - - Returns: - Returns the "dst" array. - """ - n = thetas.shape[0] - y = np.asarray([[0, -0.5], [0.5, 0]], dtype=np.complex128) - z = np.asarray([[-0.5j, 0], [0, 0.5j]], dtype=np.complex128) - tmp = np.full((5, 2, 2), fill_value=0, dtype=np.complex128) - for k in range(n): - th = thetas[k] - a = make_rz(th[0], out=tmp[0]) - b = make_ry(th[1], out=tmp[1]) - c = make_rz(th[2], out=tmp[2]) - - za = np.dot(z, a, out=tmp[3]) - np.dot(np.dot(za, b, out=tmp[4]), c, out=dst[k, 0]) - yb = np.dot(y, b, out=tmp[3]) - np.dot(a, np.dot(yb, c, out=tmp[4]), out=dst[k, 1]) - zc = np.dot(z, c, out=tmp[3]) - np.dot(a, np.dot(b, zc, out=tmp[4]), out=dst[k, 2]) - return dst - - -def init_layer2q_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: - """ - Initializes 4x4 matrices of 2-qubit gates defined in the paper. - - Args: - thetas: depth x 4 matrix of gate parameters for every layer, where - "depth" is the number of layers. - dst: destination array of size depth x 4 x 4 that will receive gate - matrices of each layer. - - Returns: - Returns the "dst" array. - """ - depth = thetas.shape[0] - for k in range(depth): - th = thetas[k] - cs0 = np.cos(0.5 * th[0]).item() - sn0 = np.sin(0.5 * th[0]).item() - ep1 = np.exp(0.5j * th[1]).item() - en1 = np.exp(-0.5j * th[1]).item() - cs2 = np.cos(0.5 * th[2]).item() - sn2 = np.sin(0.5 * th[2]).item() - cs3 = np.cos(0.5 * th[3]).item() - sn3 = np.sin(0.5 * th[3]).item() - ep1cs0 = ep1 * cs0 - ep1sn0 = ep1 * sn0 - en1cs0 = en1 * cs0 - en1sn0 = en1 * sn0 - sn2cs3 = sn2 * cs3 - sn2sn3 = sn2 * sn3 - cs2cs3 = cs2 * cs3 - sn3cs2j = 1j * sn3 * cs2 - sn2sn3j = 1j * sn2sn3 - - flat_dst = dst[k].ravel() - flat_dst[:] = [ - -(sn2sn3j - cs2cs3) * en1cs0, - -(sn2cs3 + sn3cs2j) * en1cs0, - (sn2cs3 + sn3cs2j) * en1sn0, - (sn2sn3j - cs2cs3) * en1sn0, - (sn2cs3 - sn3cs2j) * en1cs0, - (sn2sn3j + cs2cs3) * en1cs0, - -(sn2sn3j + cs2cs3) * en1sn0, - (-sn2cs3 + sn3cs2j) * en1sn0, - (-sn2sn3j + cs2cs3) * ep1sn0, - -(sn2cs3 + sn3cs2j) * ep1sn0, - -(sn2cs3 + sn3cs2j) * ep1cs0, - (-sn2sn3j + cs2cs3) * ep1cs0, - (sn2cs3 - sn3cs2j) * ep1sn0, - (sn2sn3j + cs2cs3) * ep1sn0, - (sn2sn3j + cs2cs3) * ep1cs0, - (sn2cs3 - sn3cs2j) * ep1cs0, - ] - return dst - - -def init_layer2q_deriv_matrices(thetas: np.ndarray, dst: np.ndarray) -> np.ndarray: - """ - Initializes 4 x 4 derivative matrices of 2-qubit gates defined in the paper. - - Args: - thetas: depth x 4 matrix of gate parameters for every layer, where - "depth" is the number of layers. - dst: destination array of size depth x 4 x 4 x 4 that will receive gate - derivative matrices of each layer; there are 4 parameters per gate, - hence, 4 derivative matrices per layer. - - Returns: - Returns the "dst" array. - """ - depth = thetas.shape[0] - for k in range(depth): - th = thetas[k] - cs0 = np.cos(0.5 * th[0]).item() - sn0 = np.sin(0.5 * th[0]).item() - ep1 = np.exp(0.5j * th[1]).item() * 0.5 - en1 = np.exp(-0.5j * th[1]).item() * 0.5 - cs2 = np.cos(0.5 * th[2]).item() - sn2 = np.sin(0.5 * th[2]).item() - cs3 = np.cos(0.5 * th[3]).item() - sn3 = np.sin(0.5 * th[3]).item() - ep1cs0 = ep1 * cs0 - ep1sn0 = ep1 * sn0 - en1cs0 = en1 * cs0 - en1sn0 = en1 * sn0 - sn2cs3 = sn2 * cs3 - sn2sn3 = sn2 * sn3 - sn3cs2 = sn3 * cs2 - cs2cs3 = cs2 * cs3 - sn2cs3j = 1j * sn2cs3 - sn2sn3j = 1j * sn2sn3 - sn3cs2j = 1j * sn3cs2 - cs2cs3j = 1j * cs2cs3 - - flat_dst = dst[k, 0].ravel() - flat_dst[:] = [ - (sn2sn3j - cs2cs3) * en1sn0, - (sn2cs3 + sn3cs2j) * en1sn0, - (sn2cs3 + sn3cs2j) * en1cs0, - (sn2sn3j - cs2cs3) * en1cs0, - (-sn2cs3 + sn3cs2j) * en1sn0, - -(sn2sn3j + cs2cs3) * en1sn0, - -(sn2sn3j + cs2cs3) * en1cs0, - (-sn2cs3 + sn3cs2j) * en1cs0, - (-sn2sn3j + cs2cs3) * ep1cs0, - -(sn2cs3 + sn3cs2j) * ep1cs0, - (sn2cs3 + sn3cs2j) * ep1sn0, - (sn2sn3j - cs2cs3) * ep1sn0, - (sn2cs3 - sn3cs2j) * ep1cs0, - (sn2sn3j + cs2cs3) * ep1cs0, - -(sn2sn3j + cs2cs3) * ep1sn0, - (-sn2cs3 + sn3cs2j) * ep1sn0, - ] - - flat_dst = dst[k, 1].ravel() - flat_dst[:] = [ - -(sn2sn3 + cs2cs3j) * en1cs0, - (sn2cs3j - sn3cs2) * en1cs0, - -(sn2cs3j - sn3cs2) * en1sn0, - (sn2sn3 + cs2cs3j) * en1sn0, - -(sn2cs3j + sn3cs2) * en1cs0, - (sn2sn3 - cs2cs3j) * en1cs0, - (-sn2sn3 + cs2cs3j) * en1sn0, - (sn2cs3j + sn3cs2) * en1sn0, - (sn2sn3 + cs2cs3j) * ep1sn0, - (-sn2cs3j + sn3cs2) * ep1sn0, - (-sn2cs3j + sn3cs2) * ep1cs0, - (sn2sn3 + cs2cs3j) * ep1cs0, - (sn2cs3j + sn3cs2) * ep1sn0, - (-sn2sn3 + cs2cs3j) * ep1sn0, - (-sn2sn3 + cs2cs3j) * ep1cs0, - (sn2cs3j + sn3cs2) * ep1cs0, - ] - - flat_dst = dst[k, 2].ravel() - flat_dst[:] = [ - -(sn2cs3 + sn3cs2j) * en1cs0, - (sn2sn3j - cs2cs3) * en1cs0, - -(sn2sn3j - cs2cs3) * en1sn0, - (sn2cs3 + sn3cs2j) * en1sn0, - (sn2sn3j + cs2cs3) * en1cs0, - (-sn2cs3 + sn3cs2j) * en1cs0, - (sn2cs3 - sn3cs2j) * en1sn0, - -(sn2sn3j + cs2cs3) * en1sn0, - -(sn2cs3 + sn3cs2j) * ep1sn0, - (sn2sn3j - cs2cs3) * ep1sn0, - (sn2sn3j - cs2cs3) * ep1cs0, - -(sn2cs3 + sn3cs2j) * ep1cs0, - (sn2sn3j + cs2cs3) * ep1sn0, - (-sn2cs3 + sn3cs2j) * ep1sn0, - (-sn2cs3 + sn3cs2j) * ep1cs0, - (sn2sn3j + cs2cs3) * ep1cs0, - ] - - flat_dst = dst[k, 3].ravel() - flat_dst[:] = [ - -(sn2cs3j + sn3cs2) * en1cs0, - (sn2sn3 - cs2cs3j) * en1cs0, - (-sn2sn3 + cs2cs3j) * en1sn0, - (sn2cs3j + sn3cs2) * en1sn0, - -(sn2sn3 + cs2cs3j) * en1cs0, - (sn2cs3j - sn3cs2) * en1cs0, - -(sn2cs3j - sn3cs2) * en1sn0, - (sn2sn3 + cs2cs3j) * en1sn0, - -(sn2cs3j + sn3cs2) * ep1sn0, - (sn2sn3 - cs2cs3j) * ep1sn0, - (sn2sn3 - cs2cs3j) * ep1cs0, - -(sn2cs3j + sn3cs2) * ep1cs0, - -(sn2sn3 + cs2cs3j) * ep1sn0, - (sn2cs3j - sn3cs2) * ep1sn0, - (sn2cs3j - sn3cs2) * ep1cs0, - -(sn2sn3 + cs2cs3j) * ep1cs0, - ] - return dst diff --git a/qiskit/transpiler/synthesis/aqc/fast_gradient/pmatrix.py b/qiskit/transpiler/synthesis/aqc/fast_gradient/pmatrix.py deleted file mode 100644 index 8b36a0b8877e..000000000000 --- a/qiskit/transpiler/synthesis/aqc/fast_gradient/pmatrix.py +++ /dev/null @@ -1,312 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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 -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Matrix designed for fast multiplication by permutation and block-diagonal ones. -""" - -import numpy as np -from .layer import Layer1Q, Layer2Q - - -class PMatrix: - """ - Wrapper around a matrix that enables fast multiplication by permutation - matrices and block-diagonal ones. - """ - - def __init__(self, num_qubits: int): - """ - Initializes the internal structures of this object but does not set - the matrix yet. - - Args: - num_qubits: number of qubits. - """ - dim = 2**num_qubits - self._mat = np.empty(0) - self._dim = dim - self._temp_g2x2 = np.zeros((2, 2), dtype=np.complex128) - self._temp_g4x4 = np.zeros((4, 4), dtype=np.complex128) - self._temp_2x2 = self._temp_g2x2.copy() - self._temp_4x4 = self._temp_g4x4.copy() - self._identity_perm = np.arange(dim, dtype=np.int64) - self._left_perm = self._identity_perm.copy() - self._right_perm = self._identity_perm.copy() - self._temp_perm = self._identity_perm.copy() - self._temp_slice_dim_x_2 = np.zeros((dim, 2), dtype=np.complex128) - self._temp_slice_dim_x_4 = np.zeros((dim, 4), dtype=np.complex128) - self._idx_mat = self._init_index_matrix(dim) - self._temp_block_diag = np.zeros(self._idx_mat.shape, dtype=np.complex128) - - def set_matrix(self, mat: np.ndarray): - """ - Copies specified matrix to internal storage. Once the matrix - is set, the object is ready for use. - - **Note**, the matrix will be copied, mind the size issues. - - Args: - mat: matrix we want to multiply on the left and on the right by - layer matrices. - """ - if self._mat.size == 0: - self._mat = mat.copy() - else: - np.copyto(self._mat, mat) - - @staticmethod - def _init_index_matrix(dim: int) -> np.ndarray: - """ - Fast multiplication can be implemented by picking up a subset of - entries in a sparse matrix. - - Args: - dim: problem dimensionality. - - Returns: - 2d-array of indices for the fast multiplication. - """ - all_idx = np.arange(dim * dim, dtype=np.int64).reshape(dim, dim) - idx = np.full((dim // 4, 4 * 4), fill_value=0, dtype=np.int64) - b = np.full((4, 4), fill_value=0, dtype=np.int64) - for i in range(0, dim, 4): - b[:, :] = all_idx[i : i + 4, i : i + 4] - idx[i // 4, :] = b.T.ravel() - return idx - - def mul_right_q1(self, layer: Layer1Q, temp_mat: np.ndarray, dagger: bool): - """ - Multiplies ``NxN`` matrix, wrapped by this object, by a 1-qubit layer - matrix of the right, where ``N`` is the actual size of matrices involved, - ``N = 2^{num. of qubits}``. - - Args: - layer: 1-qubit layer, i.e. the layer with just one non-trivial - 1-qubit gate and other gates are just identity operators. - temp_mat: a temporary NxN matrix used as a workspace. - dagger: if true, the right-hand side matrix will be taken as - conjugate transposed. - """ - - gmat, perm, inv_perm = layer.get_attr() - mat = self._mat - dim = perm.size - - # temp_mat <-- mat[:, right_perm[perm]] = mat[:, right_perm][:, perm]: - np.take(mat, np.take(self._right_perm, perm, out=self._temp_perm), axis=1, out=temp_mat) - - # mat <-- mat[:, right_perm][:, perm] @ kron(I(N/4), gmat)^dagger, where - # conjugate-transposition might be or might be not applied: - gmat_right = np.conj(gmat, out=self._temp_g2x2).T if dagger else gmat - for i in range(0, dim, 2): - mat[:, i : i + 2] = np.dot( - temp_mat[:, i : i + 2], gmat_right, out=self._temp_slice_dim_x_2 - ) - - # Update right permutation: - self._right_perm[:] = inv_perm - - def mul_right_q2(self, layer: Layer2Q, temp_mat: np.ndarray, dagger: bool = True): - """ - Multiplies ``NxN`` matrix, wrapped by this object, by a 2-qubit layer - matrix on the right, where ``N`` is the actual size of matrices involved, - ``N = 2^{num. of qubits}``. - - Args: - layer: 2-qubit layer, i.e. the layer with just one non-trivial - 2-qubit gate and other gates are just identity operators. - temp_mat: a temporary NxN matrix used as a workspace. - dagger: if true, the right-hand side matrix will be taken as - conjugate transposed. - """ - - gmat, perm, inv_perm = layer.get_attr() - mat = self._mat - dim = perm.size - - # temp_mat <-- mat[:, right_perm[perm]] = mat[:, right_perm][:, perm]: - np.take(mat, np.take(self._right_perm, perm, out=self._temp_perm), axis=1, out=temp_mat) - - # mat <-- mat[:, right_perm][:, perm] @ kron(I(N/4), gmat)^dagger, where - # conjugate-transposition might be or might be not applied: - gmat_right = np.conj(gmat, out=self._temp_g4x4).T if dagger else gmat - for i in range(0, dim, 4): - mat[:, i : i + 4] = np.dot( - temp_mat[:, i : i + 4], gmat_right, out=self._temp_slice_dim_x_4 - ) - - # Update right permutation: - self._right_perm[:] = inv_perm - - def mul_left_q1(self, layer: Layer1Q, temp_mat: np.ndarray): - """ - Multiplies ``NxN`` matrix, wrapped by this object, by a 1-qubit layer - matrix of the left, where ``dim`` is the actual size of matrices involved, - ``dim = 2^{num. of qubits}``. - - Args: - layer: 1-qubit layer, i.e. the layer with just one non-trivial - 1-qubit gate and other gates are just identity operators. - temp_mat: a temporary NxN matrix used as a workspace. - """ - mat = self._mat - gmat, perm, inv_perm = layer.get_attr() - dim = perm.size - - # temp_mat <-- mat[left_perm[perm]] = mat[left_perm][perm]: - np.take(mat, np.take(self._left_perm, perm, out=self._temp_perm), axis=0, out=temp_mat) - - # mat <-- kron(I(dim/4), gmat) @ mat[left_perm][perm]: - if dim > 512: - # Faster for large matrices. - for i in range(0, dim, 2): - np.dot(gmat, temp_mat[i : i + 2, :], out=mat[i : i + 2, :]) - else: - # Faster for small matrices. - half = dim // 2 - np.copyto( - mat.reshape((2, half, dim)), np.swapaxes(temp_mat.reshape((half, 2, dim)), 0, 1) - ) - np.dot(gmat, mat.reshape(2, -1), out=temp_mat.reshape(2, -1)) - np.copyto( - mat.reshape((half, 2, dim)), np.swapaxes(temp_mat.reshape((2, half, dim)), 0, 1) - ) - - # Update left permutation: - self._left_perm[:] = inv_perm - - def mul_left_q2(self, layer: Layer2Q, temp_mat: np.ndarray): - """ - Multiplies ``NxN`` matrix, wrapped by this object, by a 2-qubit layer - matrix on the left, where ``dim`` is the actual size of matrices involved, - ``dim = 2^{num. of qubits}``. - - Args: - layer: 2-qubit layer, i.e. the layer with just one non-trivial - 2-qubit gate and other gates are just identity operators. - temp_mat: a temporary NxN matrix used as a workspace. - """ - mat = self._mat - gmat, perm, inv_perm = layer.get_attr() - dim = perm.size - - # temp_mat <-- mat[left_perm[perm]] = mat[left_perm][perm]: - np.take(mat, np.take(self._left_perm, perm, out=self._temp_perm), axis=0, out=temp_mat) - - # mat <-- kron(I(dim/4), gmat) @ mat[left_perm][perm]: - if dim > 512: - # Faster for large matrices. - for i in range(0, dim, 4): - np.dot(gmat, temp_mat[i : i + 4, :], out=mat[i : i + 4, :]) - else: - # Faster for small matrices. - half = dim // 4 - np.copyto( - mat.reshape((4, half, dim)), np.swapaxes(temp_mat.reshape((half, 4, dim)), 0, 1) - ) - np.dot(gmat, mat.reshape(4, -1), out=temp_mat.reshape(4, -1)) - np.copyto( - mat.reshape((half, 4, dim)), np.swapaxes(temp_mat.reshape((4, half, dim)), 0, 1) - ) - - # Update left permutation: - self._left_perm[:] = inv_perm - - def product_q1(self, layer: Layer1Q, tmp1: np.ndarray, tmp2: np.ndarray) -> np.complex128: - """ - Computes and returns: ``Trace(mat @ C) = Trace(mat @ P^T @ gmat @ P) = - Trace((P @ mat @ P^T) @ gmat) = Trace(C @ (P @ mat @ P^T)) = - vec(gmat^T)^T @ vec(P @ mat @ P^T)``, where mat is ``NxN`` matrix wrapped - by this object, ``C`` is matrix representation of the layer ``L``, and gmat - is 2x2 matrix of underlying 1-qubit gate. - - **Note**: matrix of this class must be finalized beforehand. - - Args: - layer: 1-qubit layer. - tmp1: temporary, external matrix used as a workspace. - tmp2: temporary, external matrix used as a workspace. - - Returns: - trace of the matrix product. - """ - mat = self._mat - gmat, perm, _ = layer.get_attr() - - # tmp2 = P @ mat @ P^T: - np.take(np.take(mat, perm, axis=0, out=tmp1), perm, axis=1, out=tmp2) - - # matrix dot product = Tr(transposed(kron(I(dim/4), gmat)), (P @ mat @ P^T)): - gmat_t, tmp3 = self._temp_g2x2, self._temp_2x2 - np.copyto(gmat_t, gmat.T) - _sum = 0.0 - for i in range(0, mat.shape[0], 2): - tmp3[:, :] = tmp2[i : i + 2, i : i + 2] - _sum += np.dot(gmat_t.ravel(), tmp3.ravel()) - - return np.complex128(_sum) - - def product_q2(self, layer: Layer2Q, tmp1: np.ndarray, tmp2: np.ndarray) -> np.complex128: - """ - Computes and returns: ``Trace(mat @ C) = Trace(mat @ P^T @ gmat @ P) = - Trace((P @ mat @ P^T) @ gmat) = Trace(C @ (P @ mat @ P^T)) = - vec(gmat^T)^T @ vec(P @ mat @ P^T)``, where mat is ``NxN`` matrix wrapped - by this object, ``C`` is matrix representation of the layer ``L``, and gmat - is 4x4 matrix of underlying 2-qubit gate. - - **Note**: matrix of this class must be finalized beforehand. - - Args: - layer: 2-qubit layer. - tmp1: temporary, external matrix used as a workspace. - tmp2: temporary, external matrix used as a workspace. - - Returns: - trace of the matrix product. - """ - mat = self._mat - gmat, perm, _ = layer.get_attr() - - # Compute the matrix dot product: - # Tr(transposed(kron(I(dim/4), gmat)), (P @ mat @ P^T)): - - # The fastest version so far, but requires two NxN temp. matrices. - # tmp2 = P @ mat @ P^T: - np.take(np.take(mat, perm, axis=0, out=tmp1), perm, axis=1, out=tmp2) - - bldia = self._temp_block_diag - np.take(tmp2.ravel(), self._idx_mat.ravel(), axis=0, out=bldia.ravel()) - bldia *= gmat.reshape(-1, gmat.size) - return np.complex128(np.sum(bldia)) - - def finalize(self, temp_mat: np.ndarray) -> np.ndarray: - """ - Applies the left (row) and right (column) permutations to the matrix. - at the end of computation process. - - Args: - temp_mat: temporary, external matrix. - - Returns: - finalized matrix with all transformations applied. - """ - mat = self._mat - - # mat <-- mat[left_perm][:, right_perm] = P_left @ mat @ transposed(P_right) - np.take(mat, self._left_perm, axis=0, out=temp_mat) - np.take(temp_mat, self._right_perm, axis=1, out=mat) - - # Set both permutations to identity once they have been applied. - self._left_perm[:] = self._identity_perm - self._right_perm[:] = self._identity_perm - return self._mat diff --git a/test/python/synthesis/aqc/test_aqc_plugin.py b/test/python/synthesis/aqc/test_aqc_plugin.py index a69f5e372bb6..b55e6ffb379b 100644 --- a/test/python/synthesis/aqc/test_aqc_plugin.py +++ b/test/python/synthesis/aqc/test_aqc_plugin.py @@ -25,8 +25,6 @@ from qiskit.transpiler.passes import UnitarySynthesis from qiskit.transpiler.passes.synthesis import AQCSynthesisPlugin -# from qiskit.transpiler.synthesis.aqc import AQCSynthesisPlugin as OldAQCSynthesisPlugin - class TestAQCSynthesisPlugin(QiskitTestCase): """Basic tests of the AQC synthesis plugin.""" @@ -49,8 +47,6 @@ def test_aqc_plugin(self): """Basic test of the plugin.""" plugin = AQCSynthesisPlugin() dag = plugin.run(self._target_unitary, config=self._seed_config) - # with self.assertWarns(PendingDeprecationWarning): - # _ = OldAQCSynthesisPlugin() approx_circuit = dag_to_circuit(dag) approx_unitary = Operator(approx_circuit).data From aeb1a2d6877c55538ddb6f9fcb8f6415a91baa8e Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Tue, 26 Dec 2023 12:54:55 +0000 Subject: [PATCH 17/35] update link in test --- test/python/synthesis/aqc/fast_gradient/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/synthesis/aqc/fast_gradient/test_utils.py b/test/python/synthesis/aqc/fast_gradient/test_utils.py index 4a6f44591da0..ea84650791c6 100644 --- a/test/python/synthesis/aqc/fast_gradient/test_utils.py +++ b/test/python/synthesis/aqc/fast_gradient/test_utils.py @@ -18,7 +18,7 @@ import random import test.python.synthesis.aqc.fast_gradient.utils_for_testing as tut import numpy as np -import qiskit.transpiler.synthesis.aqc.fast_gradient.fast_grad_utils as myu +import qiskit.synthesis.unitary.aqc.fast_gradient.fast_grad_utils as myu from qiskit.test import QiskitTestCase from qiskit.synthesis.unitary.aqc.elementary_operations import rx_matrix as _rx from qiskit.synthesis.unitary.aqc.elementary_operations import ry_matrix as _ry From e9fce2ec4775adf1a18a429701814b4673d18990 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Tue, 26 Dec 2023 14:28:55 +0000 Subject: [PATCH 18/35] add release notes --- ...cate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml diff --git a/releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml b/releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml new file mode 100644 index 000000000000..39ede73d9a61 --- /dev/null +++ b/releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The :mod:`qiskit.transpiler.synthesis` module is pending deprecation and + will be deprecated in a future release. The following objects have been moved: + + * :mod:`qiskit.transpiler.synthesis.aqc` has been moved to :mod:`qiskit.synthesis.unitary.aqc` + (except of :class:`qiskit.synthesis.unitary.aqc.AQCSynthesisPlugin`). + * :class:`qiskit.synthesis.unitary.aqc.AQCSynthesisPlugin` has been moved to + :class:`qiskit.transpiler.passes.synthesis.AQCSynthesisPlugin`. + * :func:`qiskit.transpiler.synthesis.graysynth` has been moved to + :func:`qiskit.synthesis.synth_cnot_phase_aam`. + * :func:`qiskit.transpiler.synthesis.cnot_synth` has been moved to + :func:`qiskit.synthesis.synth_cnot_count_full_pmh`. From cc358029e188ee65b1c64affd2532e559a16320b Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Wed, 27 Dec 2023 08:12:06 +0000 Subject: [PATCH 19/35] update docs --- qiskit/synthesis/__init__.py | 3 +++ qiskit/transpiler/synthesis/aqc/__init__.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 3577d7310364..3150e6d0755d 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -90,6 +90,9 @@ .. autofunction:: synth_qft_line +Unitary Synthesis +================= + """ from .evolution import ( diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index f49b048f9aa6..ded9ad2a7183 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -14,8 +14,6 @@ ===================================================================== Approximate Quantum Compiler (:mod:`qiskit.transpiler.synthesis.aqc`) ===================================================================== - -.. currentmodule:: qiskit.transpiler.synthesis.aqc """ import warnings From 6b4cfec14cb80859e49905a4e3ec89d5f5ed6579 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Wed, 27 Dec 2023 10:02:41 +0000 Subject: [PATCH 20/35] update docs/apidocs/synthesis_aqc.rst --- docs/apidoc/synthesis_aqc.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/apidoc/synthesis_aqc.rst b/docs/apidoc/synthesis_aqc.rst index 0ce100071a52..1be57fbc8182 100644 --- a/docs/apidoc/synthesis_aqc.rst +++ b/docs/apidoc/synthesis_aqc.rst @@ -1,6 +1,6 @@ -.. _qiskit-transpiler-synthesis-aqc: +.. _qiskit-synthesis-unitary-aqc: -.. automodule:: qiskit.transpiler.synthesis.aqc +.. automodule:: qiskit.synthesis.unitary.aqc :no-members: :no-inherited-members: :no-special-members: From d1a559167d886381c5c35e62910c436c4cd2fab0 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Tue, 9 Jan 2024 12:07:55 +0000 Subject: [PATCH 21/35] add deprecations to qiskit/transpiler/synthesis/__init__.py --- qiskit/transpiler/synthesis/aqc/__init__.py | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index ded9ad2a7183..21fa107b60d2 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -35,3 +35,29 @@ stacklevel=2, category=PendingDeprecationWarning, ) + +_DEPRECATED_NAMES = { + "AQC": "qiskit.synthesis.unitary.aqc", + "ApproximateCircuit": "qiskit.synthesis.unitary.aqc", + "ApproximatingObjective": "qiskit.synthesis.unitary.aqc", + "CNOTUnitCircuit": "qiskit.synthesis.unitary.aqc", + "CNOTUnitObjective": "qiskit.synthesis.unitary.aqc", + "DefaultCNOTUnitObjective": "qiskit.synthesis.unitary.aqc", + "FastCNOTUnitObjective": "qiskit.synthesis.unitary.aqc", + "AQCSynthesisPlugin": "qiskit.transpiler.passes.synthesis", +} + + +def __getattr__(name): + if name in _DEPRECATED_NAMES: + import importlib + + module_name = _DEPRECATED_NAMES[name] + warnings.warn( + f"Accessing '{name}' from '{__name__}' is deprecated since Qiskit 0.46" + f" and will be removed in 1.0. Import from '{module_name}' instead.", + DeprecationWarning, + stacklevel=2, + ) + return getattr(importlib.import_module(module_name), name) + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") From 52e851fa747156233775b67dd81df9e13058cfa3 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Tue, 16 Jan 2024 09:46:13 +0000 Subject: [PATCH 22/35] fix link --- qiskit/circuit/library/generalized_gates/linear_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/generalized_gates/linear_function.py b/qiskit/circuit/library/generalized_gates/linear_function.py index b90652255312..3097a4952d77 100644 --- a/qiskit/circuit/library/generalized_gates/linear_function.py +++ b/qiskit/circuit/library/generalized_gates/linear_function.py @@ -30,7 +30,7 @@ class LinearFunction(Gate): as a n x n matrix of 0s and 1s in numpy array format. A linear function can be synthesized into CX and SWAP gates using the Patel–Markov–Hayes - algorithm, as implemented in :func:`~qiskit.transpiler.synthesis.cnot_synth` + algorithm, as implemented in :func:`~qiskit.synthesis.synth_cnot_count_full_pmh` based on reference [1]. For efficiency, the internal n x n matrix is stored in the format expected From c81bc7b343a15fc4a50ae99975144f27758aeb86 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Thu, 18 Jan 2024 10:43:18 +0000 Subject: [PATCH 23/35] improve docs following review --- qiskit/synthesis/__init__.py | 4 +++- qiskit/synthesis/unitary/__init__.py | 2 +- qiskit/synthesis/unitary/aqc/__init__.py | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 3150e6d0755d..db5bb5b65366 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017 - 2023. +# (C) Copyright IBM 2017, 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 @@ -93,6 +93,8 @@ Unitary Synthesis ================= +The Approximate Quantum Compiler is available here: :mod:`qiskit.synthesis.unitary.aqc` + """ from .evolution import ( diff --git a/qiskit/synthesis/unitary/__init__.py b/qiskit/synthesis/unitary/__init__.py index f19592ee8bdb..ebc27df47226 100644 --- a/qiskit/synthesis/unitary/__init__.py +++ b/qiskit/synthesis/unitary/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017 - 2023. +# (C) Copyright IBM 2017, 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 diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py index cffad8e15504..e7fddfbc8ff1 100644 --- a/qiskit/synthesis/unitary/aqc/__init__.py +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -23,7 +23,7 @@ ========= The main public interface of this module is reached by passing ``unitary_synthesis_method='aqc'`` to -:obj:`~.compiler.transpile`. This will swap the synthesis method to use :obj:`AQCSynthesisPlugin`. +:func:`~.compiler.transpile`. This will swap the synthesis method to use :class:`AQCSynthesisPlugin`. The individual classes are: .. autosummary:: @@ -63,7 +63,7 @@ .. math:: - argmax_{\theta}\frac{1}{d}|\langle Vct(\theta),U\rangle| + \mathrm{argmax}_{\theta}\frac{1}{d}|\langle Vct(\theta),U\rangle| where the inner product is the Frobenius inner product. Note that :math:`|\langle V,U\rangle|\leq d` for all unitaries :math:`U` and :math:`V`, so the objective @@ -128,7 +128,7 @@ ) # Create an optimizer to be used by AQC - optimizer = L_BFGS_B() + optimizer = partial(scipy.optimize.minimize, method="L-BFGS-B") # Create an instance aqc = AQC(optimizer) From d67d49a01ebbad0c7734b06f646fc60f5691c310 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Thu, 18 Jan 2024 13:35:54 +0000 Subject: [PATCH 24/35] update docs --- qiskit/synthesis/unitary/aqc/__init__.py | 3 ++- qiskit/transpiler/synthesis/aqc/__init__.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py index e7fddfbc8ff1..abd4253d75d2 100644 --- a/qiskit/synthesis/unitary/aqc/__init__.py +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -23,7 +23,8 @@ ========= The main public interface of this module is reached by passing ``unitary_synthesis_method='aqc'`` to -:func:`~.compiler.transpile`. This will swap the synthesis method to use :class:`AQCSynthesisPlugin`. +:func:`~.compiler.transpile`. This will swap the synthesis method to use +:class:`~.transpiler.passes.synthesis.AQCSynthesisPlugin`. The individual classes are: .. autosummary:: diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index 21fa107b60d2..87b394895654 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -===================================================================== -Approximate Quantum Compiler (:mod:`qiskit.transpiler.synthesis.aqc`) -===================================================================== +============================ +Approximate Quantum Compiler +============================ """ import warnings From 9c85c2cf71e3e9c90add716699e5948c9c01991e Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Sun, 21 Jan 2024 07:20:29 +0000 Subject: [PATCH 25/35] add aqc to synthesis docs after review --- qiskit/synthesis/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index db5bb5b65366..2ab6ef84a617 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -93,7 +93,10 @@ Unitary Synthesis ================= -The Approximate Quantum Compiler is available here: :mod:`qiskit.synthesis.unitary.aqc` +.. autosummary:: + :toctree: ../stubs/ + + AQC """ @@ -132,3 +135,4 @@ from .stabilizer import synth_stabilizer_layers, synth_stabilizer_depth_lnn from .discrete_basis import SolovayKitaevDecomposition, generate_basic_approximations from .qft import synth_qft_line +from .unitary import aqc From 7ff67201636352c664b076a30a8f642a94b8c09b Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Sun, 21 Jan 2024 08:12:11 +0000 Subject: [PATCH 26/35] update qiskit/transpiler/synthesis/aqc/__init__.py after review --- qiskit/transpiler/synthesis/aqc/__init__.py | 32 ++------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/qiskit/transpiler/synthesis/aqc/__init__.py b/qiskit/transpiler/synthesis/aqc/__init__.py index 87b394895654..f6c51b2c1dd4 100644 --- a/qiskit/transpiler/synthesis/aqc/__init__.py +++ b/qiskit/transpiler/synthesis/aqc/__init__.py @@ -30,34 +30,8 @@ from qiskit.transpiler.passes.synthesis.aqc_plugin import AQCSynthesisPlugin warnings.warn( - "The qiskit.transpiler.synthesis.aqc module is pending deprecation since Qiskit 0.46.0. " - "It will be deprecated in a following release, no sooner than 3 months after the 0.46.0 release.", + "The qiskit.transpiler.synthesis.aqc module is deprecated since Qiskit 0.46.0 " + "and will be removed in Qiskit 1.0.", stacklevel=2, - category=PendingDeprecationWarning, + category=DeprecationWarning, ) - -_DEPRECATED_NAMES = { - "AQC": "qiskit.synthesis.unitary.aqc", - "ApproximateCircuit": "qiskit.synthesis.unitary.aqc", - "ApproximatingObjective": "qiskit.synthesis.unitary.aqc", - "CNOTUnitCircuit": "qiskit.synthesis.unitary.aqc", - "CNOTUnitObjective": "qiskit.synthesis.unitary.aqc", - "DefaultCNOTUnitObjective": "qiskit.synthesis.unitary.aqc", - "FastCNOTUnitObjective": "qiskit.synthesis.unitary.aqc", - "AQCSynthesisPlugin": "qiskit.transpiler.passes.synthesis", -} - - -def __getattr__(name): - if name in _DEPRECATED_NAMES: - import importlib - - module_name = _DEPRECATED_NAMES[name] - warnings.warn( - f"Accessing '{name}' from '{__name__}' is deprecated since Qiskit 0.46" - f" and will be removed in 1.0. Import from '{module_name}' instead.", - DeprecationWarning, - stacklevel=2, - ) - return getattr(importlib.import_module(module_name), name) - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") From cd93f32edf38d1317a583effb11d458499f49b19 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Sun, 21 Jan 2024 08:12:49 +0000 Subject: [PATCH 27/35] update pending deprecation to deprecation in release notes --- .../deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml b/releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml index 39ede73d9a61..bb89cfbdfbe2 100644 --- a/releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml +++ b/releasenotes/notes/deprecate-transpiler-synthesis-cf4e6e6dcdb42eca.yaml @@ -1,8 +1,8 @@ --- deprecations: - | - The :mod:`qiskit.transpiler.synthesis` module is pending deprecation and - will be deprecated in a future release. The following objects have been moved: + The :mod:`qiskit.transpiler.synthesis` module is deprecated and + will be removed in a future release. The following objects have been moved: * :mod:`qiskit.transpiler.synthesis.aqc` has been moved to :mod:`qiskit.synthesis.unitary.aqc` (except of :class:`qiskit.synthesis.unitary.aqc.AQCSynthesisPlugin`). From 572094b68b5a2edb1e61d32b8edd4818241b274c Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Sun, 21 Jan 2024 08:18:13 +0000 Subject: [PATCH 28/35] handle cyclic imports --- qiskit/synthesis/unitary/aqc/elementary_operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/unitary/aqc/elementary_operations.py b/qiskit/synthesis/unitary/aqc/elementary_operations.py index b5739267e793..3aeba6a749f2 100644 --- a/qiskit/synthesis/unitary/aqc/elementary_operations.py +++ b/qiskit/synthesis/unitary/aqc/elementary_operations.py @@ -15,7 +15,7 @@ import numpy as np -from qiskit.circuit.library import RXGate, RZGate, RYGate +from qiskit.circuit.library.standard_gates import RXGate, RZGate, RYGate def place_unitary(unitary: np.ndarray, n: int, j: int) -> np.ndarray: From a21c77135af3d6e028e9d83a50712506585aabf0 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Sun, 21 Jan 2024 09:11:18 +0000 Subject: [PATCH 29/35] update qiskit/synthesis docs following docs error --- qiskit/synthesis/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 2ab6ef84a617..9ee14803d258 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -93,10 +93,7 @@ Unitary Synthesis ================= -.. autosummary:: - :toctree: ../stubs/ - - AQC +The Approximate Quantum Compiler is available here: :mod:qiskit.synthesis.unitary.aqc """ From 9e87164e1f859acc00ac6de9618c629578e5811c Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 22 Jan 2024 06:36:09 +0000 Subject: [PATCH 30/35] another attempt to add AQC to synthesis docs --- docs/apidoc/synthesis_aqc.rst | 2 +- qiskit/synthesis/__init__.py | 5 ++++- qiskit/synthesis/unitary/aqc/__init__.py | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/apidoc/synthesis_aqc.rst b/docs/apidoc/synthesis_aqc.rst index 1be57fbc8182..96da7666a4d6 100644 --- a/docs/apidoc/synthesis_aqc.rst +++ b/docs/apidoc/synthesis_aqc.rst @@ -1,6 +1,6 @@ .. _qiskit-synthesis-unitary-aqc: -.. automodule:: qiskit.synthesis.unitary.aqc +.. automodule:: qiskit.synthesis.aqc :no-members: :no-inherited-members: :no-special-members: diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 9ee14803d258..2ab6ef84a617 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -93,7 +93,10 @@ Unitary Synthesis ================= -The Approximate Quantum Compiler is available here: :mod:qiskit.synthesis.unitary.aqc +.. autosummary:: + :toctree: ../stubs/ + + AQC """ diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py index abd4253d75d2..d2440cc8c2ac 100644 --- a/qiskit/synthesis/unitary/aqc/__init__.py +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. r""" -===================================================================== -Approximate Quantum Compiler (:mod:`qiskit.synthesis.unitary.aqc`) -===================================================================== +========================================================== +Approximate Quantum Compiler (:mod:`qiskit.synthesis.aqc`) +========================================================== .. currentmodule:: qiskit.synthesis.unitary.aqc From 25f93cae9103275bfe5057a5a07739cb75becc01 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 22 Jan 2024 08:20:09 +0000 Subject: [PATCH 31/35] another attempt to add AQC to the docs --- docs/apidoc/synthesis_aqc.rst | 2 +- qiskit/synthesis/unitary/aqc/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/apidoc/synthesis_aqc.rst b/docs/apidoc/synthesis_aqc.rst index 96da7666a4d6..dbf92ada547c 100644 --- a/docs/apidoc/synthesis_aqc.rst +++ b/docs/apidoc/synthesis_aqc.rst @@ -1,6 +1,6 @@ .. _qiskit-synthesis-unitary-aqc: -.. automodule:: qiskit.synthesis.aqc +.. automodule:: qiskit.synthesis.AQC :no-members: :no-inherited-members: :no-special-members: diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py index d2440cc8c2ac..ab207bebdffe 100644 --- a/qiskit/synthesis/unitary/aqc/__init__.py +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -12,7 +12,7 @@ r""" ========================================================== -Approximate Quantum Compiler (:mod:`qiskit.synthesis.aqc`) +Approximate Quantum Compiler (:mod:`qiskit.synthesis.AQC`) ========================================================== .. currentmodule:: qiskit.synthesis.unitary.aqc From e7ffca0aee6cf82a80d74bc7d1c06cd2a66f2a3a Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 22 Jan 2024 09:36:37 +0000 Subject: [PATCH 32/35] Revert "another attempt to add AQC to the docs" This reverts commit 25f93cae9103275bfe5057a5a07739cb75becc01. --- docs/apidoc/synthesis_aqc.rst | 2 +- qiskit/synthesis/unitary/aqc/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/apidoc/synthesis_aqc.rst b/docs/apidoc/synthesis_aqc.rst index dbf92ada547c..96da7666a4d6 100644 --- a/docs/apidoc/synthesis_aqc.rst +++ b/docs/apidoc/synthesis_aqc.rst @@ -1,6 +1,6 @@ .. _qiskit-synthesis-unitary-aqc: -.. automodule:: qiskit.synthesis.AQC +.. automodule:: qiskit.synthesis.aqc :no-members: :no-inherited-members: :no-special-members: diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py index ab207bebdffe..d2440cc8c2ac 100644 --- a/qiskit/synthesis/unitary/aqc/__init__.py +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -12,7 +12,7 @@ r""" ========================================================== -Approximate Quantum Compiler (:mod:`qiskit.synthesis.AQC`) +Approximate Quantum Compiler (:mod:`qiskit.synthesis.aqc`) ========================================================== .. currentmodule:: qiskit.synthesis.unitary.aqc From 4cd11c8245491cad099c2bb10ed30b40ce17c962 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 22 Jan 2024 09:37:17 +0000 Subject: [PATCH 33/35] Revert "another attempt to add AQC to synthesis docs" This reverts commit 9e87164e1f859acc00ac6de9618c629578e5811c. --- docs/apidoc/synthesis_aqc.rst | 2 +- qiskit/synthesis/__init__.py | 5 +---- qiskit/synthesis/unitary/aqc/__init__.py | 6 +++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/apidoc/synthesis_aqc.rst b/docs/apidoc/synthesis_aqc.rst index 96da7666a4d6..1be57fbc8182 100644 --- a/docs/apidoc/synthesis_aqc.rst +++ b/docs/apidoc/synthesis_aqc.rst @@ -1,6 +1,6 @@ .. _qiskit-synthesis-unitary-aqc: -.. automodule:: qiskit.synthesis.aqc +.. automodule:: qiskit.synthesis.unitary.aqc :no-members: :no-inherited-members: :no-special-members: diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 2ab6ef84a617..9ee14803d258 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -93,10 +93,7 @@ Unitary Synthesis ================= -.. autosummary:: - :toctree: ../stubs/ - - AQC +The Approximate Quantum Compiler is available here: :mod:qiskit.synthesis.unitary.aqc """ diff --git a/qiskit/synthesis/unitary/aqc/__init__.py b/qiskit/synthesis/unitary/aqc/__init__.py index d2440cc8c2ac..abd4253d75d2 100644 --- a/qiskit/synthesis/unitary/aqc/__init__.py +++ b/qiskit/synthesis/unitary/aqc/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. r""" -========================================================== -Approximate Quantum Compiler (:mod:`qiskit.synthesis.aqc`) -========================================================== +===================================================================== +Approximate Quantum Compiler (:mod:`qiskit.synthesis.unitary.aqc`) +===================================================================== .. currentmodule:: qiskit.synthesis.unitary.aqc From c57b11ff7899531715fb150d2f93ecfadebf9639 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 22 Jan 2024 13:09:08 +0000 Subject: [PATCH 34/35] add a deprecation test for AQC --- test/python/synthesis/aqc/test_aqc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/python/synthesis/aqc/test_aqc.py b/test/python/synthesis/aqc/test_aqc.py index 3d153c492181..97c70eb3b587 100644 --- a/test/python/synthesis/aqc/test_aqc.py +++ b/test/python/synthesis/aqc/test_aqc.py @@ -134,6 +134,12 @@ def test_aqc_determinant_minus_one(self): error = 0.5 * (np.linalg.norm(approx_matrix - target_matrix, "fro") ** 2) self.assertTrue(error < 1e-3) + def test_deprecation(self): + """Test that importing this module is deprecated.""" + # pylint: disable = unused-import + with self.assertWarns(DeprecationWarning): + import qiskit.transpiler.synthesis.aqc + if __name__ == "__main__": unittest.main() From efd8b26ebdab795759dfd9d347358dd9e249a683 Mon Sep 17 00:00:00 2001 From: Shelly Garion Date: Mon, 22 Jan 2024 16:18:00 +0000 Subject: [PATCH 35/35] minor --- qiskit/synthesis/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 9ee14803d258..3c6df12c92d1 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -93,7 +93,7 @@ Unitary Synthesis ================= -The Approximate Quantum Compiler is available here: :mod:qiskit.synthesis.unitary.aqc +The Approximate Quantum Compiler is available here: :mod:`qiskit.synthesis.unitary.aqc` """