From b9ea266a247c88dddb0f47735abf46efcf11b2a6 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Thu, 12 Dec 2024 14:09:48 +0200 Subject: [PATCH] Minor fixes for new adder and multiplier gates classes (#13530) * Fixes to AdderGate classes Added define method to HalfAdder, FullAdder, and ModularAdder classes to allow constructing Operators from quantum circuits containing such adder gates. Fixing plugins related to adder_ripple_v95, the plugins can be used only if n-1 clean ancillas are available. Improved the default adder plugins to choose the best decomposition based on the number of state qubits and the number of ancilla qubits. * Adding tests for the case that adder plugins do not apply * Constructing Operators from circuits with MultiplierGates * release notes * docstring fix * apply suggestions from code review * futher improving tests based on additional review comments --- pyproject.toml | 2 +- .../library/arithmetic/adders/adder.py | 25 +++++ .../arithmetic/multipliers/multiplier.py | 9 ++ .../passes/synthesis/hls_plugins.py | 103 +++++++++++++++--- .../fix-adder-gates-39cf3d5f683e8880.yaml | 18 +++ test/python/circuit/library/test_adders.py | 102 ++++++++++++++++- test/python/circuit/test_gate_definitions.py | 40 +++++++ 7 files changed, 279 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml diff --git a/pyproject.toml b/pyproject.toml index 98762f78a439..a89f191fe848 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS "HalfAdder.ripple_c04" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisC04" "HalfAdder.ripple_v95" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisV95" "HalfAdder.qft_d00" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisD00" -"FullAdder.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisC04" +"FullAdder.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisDefault" "FullAdder.ripple_c04" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisC04" "FullAdder.ripple_v95" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisV95" "Multiplier.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:MultiplierSynthesisR17" diff --git a/qiskit/circuit/library/arithmetic/adders/adder.py b/qiskit/circuit/library/arithmetic/adders/adder.py index 7fa3411d0436..435fab109476 100644 --- a/qiskit/circuit/library/arithmetic/adders/adder.py +++ b/qiskit/circuit/library/arithmetic/adders/adder.py @@ -116,6 +116,15 @@ def num_state_qubits(self) -> int: """ return self._num_state_qubits + def _define(self): + """Populates self.definition with some decomposition of this gate.""" + from qiskit.synthesis.arithmetic import adder_qft_d00 + + # This particular decomposition does not use any ancilla qubits. + # Note that the transpiler may choose a different decomposition + # based on the number of ancilla qubits available. + self.definition = adder_qft_d00(self.num_state_qubits, kind="half") + class ModularAdderGate(Gate): r"""Compute the sum modulo :math:`2^n` of two :math:`n`-sized qubit registers. @@ -162,6 +171,15 @@ def num_state_qubits(self) -> int: """ return self._num_state_qubits + def _define(self): + """Populates self.definition with some decomposition of this gate.""" + from qiskit.synthesis.arithmetic import adder_qft_d00 + + # This particular decomposition does not use any ancilla qubits. + # Note that the transpiler may choose a different decomposition + # based on the number of ancilla qubits available. + self.definition = adder_qft_d00(self.num_state_qubits, kind="fixed") + class FullAdderGate(Gate): r"""Compute the sum of two :math:`n`-sized qubit registers, including carry-in and -out bits. @@ -208,3 +226,10 @@ def num_state_qubits(self) -> int: The number of state qubits. """ return self._num_state_qubits + + def _define(self): + """Populates self.definition with a decomposition of this gate.""" + from qiskit.synthesis.arithmetic import adder_ripple_c04 + + # In the case of a full adder, this method does not use any ancilla qubits + self.definition = adder_ripple_c04(self.num_state_qubits, kind="full") diff --git a/qiskit/circuit/library/arithmetic/multipliers/multiplier.py b/qiskit/circuit/library/arithmetic/multipliers/multiplier.py index 4089cc35452a..38f362cf34d3 100644 --- a/qiskit/circuit/library/arithmetic/multipliers/multiplier.py +++ b/qiskit/circuit/library/arithmetic/multipliers/multiplier.py @@ -190,3 +190,12 @@ def num_result_qubits(self) -> int: The number of result qubits. """ return self._num_result_qubits + + def _define(self): + """Populates self.definition with some decomposition of this gate.""" + from qiskit.synthesis.arithmetic import multiplier_qft_r17 + + # This particular decomposition does not use any ancilla qubits. + # Note that the transpiler may choose a different decomposition + # based on the number of ancilla qubits available. + self.definition = multiplier_qft_r17(self.num_state_qubits) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index a609b11f0fef..eef996e636eb 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -300,6 +300,10 @@ - :class:`.ModularAdderSynthesisD00` - 0 - a QFT-based adder + * - ``"default"`` + - :class:`~.ModularAdderSynthesisDefault` + - any + - chooses the best algorithm based on the ancillas available .. autosummary:: :toctree: ../stubs/ @@ -307,6 +311,7 @@ ModularAdderSynthesisC04 ModularAdderSynthesisD00 ModularAdderSynthesisV95 + ModularAdderSynthesisDefault Half Adder Synthesis '''''''''''''''''''' @@ -330,6 +335,10 @@ - :class:`.HalfAdderSynthesisD00` - 0 - a QFT-based adder + * - ``"default"`` + - :class:`~.HalfAdderSynthesisDefault` + - any + - chooses the best algorithm based on the ancillas available .. autosummary:: :toctree: ../stubs/ @@ -337,6 +346,7 @@ HalfAdderSynthesisC04 HalfAdderSynthesisD00 HalfAdderSynthesisV95 + HalfAdderSynthesisDefault Full Adder Synthesis '''''''''''''''''''' @@ -356,12 +366,17 @@ - :class:`.FullAdderSynthesisV95` - :math:`n-1`, for :math:`n`-bit numbers - a ripple-carry adder + * - ``"default"`` + - :class:`~.FullAdderSynthesisDefault` + - any + - chooses the best algorithm based on the ancillas available .. autosummary:: :toctree: ../stubs/ FullAdderSynthesisC04 FullAdderSynthesisV95 + FullAdderSynthesisDefault Multiplier Synthesis @@ -1212,10 +1227,26 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** if not isinstance(high_level_object, ModularAdderGate): return None - if options.get("num_clean_ancillas", 0) >= 1: - return adder_ripple_c04(high_level_object.num_state_qubits, kind="fixed") + # For up to 5 qubits, the QFT-based adder is best + if high_level_object.num_state_qubits <= 5: + decomposition = ModularAdderSynthesisD00().run( + high_level_object, coupling_map, target, qubits, **options + ) + if decomposition is not None: + return decomposition - return adder_qft_d00(high_level_object.num_state_qubits, kind="fixed") + # Otherwise, the following decomposition is best (if there are enough ancillas) + if ( + decomposition := ModularAdderSynthesisC04().run( + high_level_object, coupling_map, target, qubits, **options + ) + ) is not None: + return decomposition + + # Otherwise, use the QFT-adder again + return ModularAdderSynthesisD00().run( + high_level_object, coupling_map, target, qubits, **options + ) class ModularAdderSynthesisC04(HighLevelSynthesisPlugin): @@ -1264,8 +1295,8 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** num_state_qubits = high_level_object.num_state_qubits - # for more than 1 state qubit, we need an ancilla - if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + # The synthesis method needs n-1 clean ancilla qubits + if num_state_qubits - 1 > options.get("num_clean_ancillas", 0): return None return adder_ripple_v95(num_state_qubits, kind="fixed") @@ -1309,10 +1340,26 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** if not isinstance(high_level_object, HalfAdderGate): return None - if options.get("num_clean_ancillas", 0) >= 1: - return adder_ripple_c04(high_level_object.num_state_qubits, kind="half") + # For up to 3 qubits, ripple_v95 is better (if there are enough ancilla qubits) + if high_level_object.num_state_qubits <= 3: + decomposition = HalfAdderSynthesisV95().run( + high_level_object, coupling_map, target, qubits, **options + ) + if decomposition is not None: + return decomposition - return adder_qft_d00(high_level_object.num_state_qubits, kind="half") + # The next best option is to use ripple_c04 (if there are enough ancilla qubits) + if ( + decomposition := HalfAdderSynthesisC04().run( + high_level_object, coupling_map, target, qubits, **options + ) + ) is not None: + return decomposition + + # The QFT-based adder does not require ancilla qubits and should always succeed + return HalfAdderSynthesisD00().run( + high_level_object, coupling_map, target, qubits, **options + ) class HalfAdderSynthesisC04(HighLevelSynthesisPlugin): @@ -1360,8 +1407,8 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** num_state_qubits = high_level_object.num_state_qubits - # for more than 1 state qubit, we need an ancilla - if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + # The synthesis method needs n-1 clean ancilla qubits + if num_state_qubits - 1 > options.get("num_clean_ancillas", 0): return None return adder_ripple_v95(num_state_qubits, kind="half") @@ -1381,18 +1428,38 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** return adder_qft_d00(high_level_object.num_state_qubits, kind="half") -class FullAdderSynthesisC04(HighLevelSynthesisPlugin): +class FullAdderSynthesisDefault(HighLevelSynthesisPlugin): """A ripple-carry adder with a carry-in and a carry-out bit. - This plugin name is:``FullAdder.ripple_c04`` which can be used as the key on + This plugin name is:``FullAdder.default`` which can be used as the key on an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ - This plugin requires at least one clean auxiliary qubit. + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, FullAdderGate): + return None - The plugin supports the following plugin-specific options: + # FullAdderSynthesisC04 requires no ancilla qubits and returns better results + # than FullAdderSynthesisV95 in all cases except for n=1. + if high_level_object.num_state_qubits == 1: + decomposition = FullAdderSynthesisV95().run( + high_level_object, coupling_map, target, qubits, **options + ) + if decomposition is not None: + return decomposition + + return FullAdderSynthesisC04().run( + high_level_object, coupling_map, target, qubits, **options + ) - * ``num_clean_ancillas``: The number of clean auxiliary qubits available. +class FullAdderSynthesisC04(HighLevelSynthesisPlugin): + """A ripple-carry adder with a carry-in and a carry-out bit. + + This plugin name is:``FullAdder.ripple_c04`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + This plugin requires no auxiliary qubits. """ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): @@ -1409,7 +1476,7 @@ class FullAdderSynthesisV95(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. For an adder on 2 registers with :math:`n` qubits each, this plugin requires at - least :math:`n-1` clean auxiliary qubit. + least :math:`n-1` clean auxiliary qubits. The plugin supports the following plugin-specific options: @@ -1422,8 +1489,8 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** num_state_qubits = high_level_object.num_state_qubits - # for more than 1 state qubit, we need an ancilla - if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + # The synthesis method needs n-1 clean ancilla qubits + if num_state_qubits - 1 > options.get("num_clean_ancillas", 0): return None return adder_ripple_v95(num_state_qubits, kind="full") diff --git a/releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml b/releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml new file mode 100644 index 000000000000..d4b792062352 --- /dev/null +++ b/releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml @@ -0,0 +1,18 @@ +--- +fixes: + - | + Added default definitions for :class:`.FullAdderGate`, :class:`.HalfAdderGate`, + :class:`.ModularAdderGate` and :class:`.MultiplierGate` gates, allowing to + contruct :class:`.Operator`\s from quantum circuits containing these gates. + - | + Fixed the number of clean ancilla qubits required by + :class:`.FullAdderSynthesisV95`, :class:`.HalfAdderSynthesisV95`, and + :class:`.ModularAdderSynthesisV95` plugins. + - | + Added missing :class:`.FullAdderSynthesisDefault` plugin that chooses the best + decomposition for :class:`.FullAdderGate` based on the number of clean ancilla qubits + available. + - | + Fixed :class:`.HalfAdderSynthesisDefault` and :class:`.ModularAdderSynthesisDefault` + plugins, for :class:`.HalfAdderGate` and :class:`.ModularAdderGate` respectively, + to choose the best decomposition based on the number of clean ancilla qubits available. diff --git a/test/python/circuit/library/test_adders.py b/test/python/circuit/library/test_adders.py index eacf8057775a..5c960cedb171 100644 --- a/test/python/circuit/library/test_adders.py +++ b/test/python/circuit/library/test_adders.py @@ -168,7 +168,7 @@ def test_raises_on_wrong_num_bits(self, adder): _ = adder(-1) def test_plugins(self): - """Test setting the HLS plugins for the modular adder.""" + """Test calling HLS plugins for various adder types.""" # all gates with the plugins we check modes = { @@ -204,6 +204,106 @@ def test_plugins(self): self.assertTrue(expected_ops[plugin] in ops) + def test_plugins_when_do_not_apply(self): + """Test that plugins do not do anything when not enough + clean ancilla qubits are available. + """ + with self.subTest(name="FullAdder"): + adder = FullAdderGate(3) + circuit = QuantumCircuit(9) + circuit.append(adder, range(adder.num_qubits)) + hls_config = HLSConfig(FullAdder=["ripple_v95"]) + hls = HighLevelSynthesis(hls_config=hls_config) + synth = hls(circuit) + self.assertEqual(synth.count_ops(), {"FullAdder": 1}) + with self.subTest(name="HalfAdder"): + adder = HalfAdderGate(3) + circuit = QuantumCircuit(8) + circuit.append(adder, range(adder.num_qubits)) + hls_config = HLSConfig(HalfAdder=["ripple_v95"]) + hls = HighLevelSynthesis(hls_config=hls_config) + synth = hls(circuit) + self.assertEqual(synth.count_ops(), {"HalfAdder": 1}) + with self.subTest(name="ModularAdder"): + adder = ModularAdderGate(3) + circuit = QuantumCircuit(7) + circuit.append(adder, range(adder.num_qubits)) + hls_config = HLSConfig(ModularAdder=["ripple_v95"]) + hls = HighLevelSynthesis(hls_config=hls_config) + synth = hls(circuit) + self.assertEqual(synth.count_ops(), {"ModularAdder": 1}) + + def test_default_plugins(self): + """Tests covering different branches in the default synthesis plugins.""" + + # Test's name indicates which synthesis method should get used. + with self.subTest(name="HalfAdder_use_ripple_v95"): + adder = HalfAdderGate(3) + circuit = QuantumCircuit(9) + circuit.append(adder, range(7)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("Carry" in ops) + with self.subTest(name="HalfAdder_use_ripple_c04"): + adder = HalfAdderGate(4) + circuit = QuantumCircuit(12) + circuit.append(adder, range(9)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("MAJ" in ops) + with self.subTest(name="HalfAdder_use_qft_d00"): + adder = HalfAdderGate(4) + circuit = QuantumCircuit(9) + circuit.append(adder, range(9)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("cp" in ops) + + with self.subTest(name="FullAdder_use_ripple_c04"): + adder = FullAdderGate(4) + circuit = QuantumCircuit(10) + circuit.append(adder, range(10)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("MAJ" in ops) + with self.subTest(name="FullAdder_use_ripple_v95"): + adder = FullAdderGate(1) + circuit = QuantumCircuit(10) + circuit.append(adder, range(4)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("Carry" in ops) + + with self.subTest(name="ModularAdder_use_qft_d00"): + adder = ModularAdderGate(4) + circuit = QuantumCircuit(8) + circuit.append(adder, range(8)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("cp" in ops) + with self.subTest(name="ModularAdder_also_use_qft_d00"): + adder = ModularAdderGate(6) + circuit = QuantumCircuit(12) + circuit.append(adder, range(12)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("cp" in ops) + with self.subTest(name="ModularAdder_use_ripple_c04"): + adder = ModularAdderGate(6) + circuit = QuantumCircuit(16) + circuit.append(adder, range(12)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("MAJ" in ops) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 3497ef9fe46a..1b78861c6c00 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -65,6 +65,11 @@ CSXGate, RVGate, XXMinusYYGate, + FullAdderGate, + HalfAdderGate, + ModularAdderGate, + LinearFunction, + MultiplierGate, ) from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, @@ -199,6 +204,41 @@ def test_ucpaulirotgate_repeat(self): operator = Operator(gate) self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator)) + def test_linear_function_definition(self): + """Test LinearFunction gate matrix and definition.""" + circ = QuantumCircuit(3) + circ.append(LinearFunction([[1, 1], [0, 1]]), [0, 2]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_full_adder_definition(self): + """Test FullAdder gate matrix and definition.""" + circ = QuantumCircuit(4) + circ.append(FullAdderGate(1), [0, 1, 2, 3]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_half_adder_definition(self): + """Test HalfAdder gate matrix and definition.""" + circ = QuantumCircuit(3) + circ.append(HalfAdderGate(1), [0, 1, 2]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_modular_adder_definition(self): + """Test ModularAdder gate matrix and definition.""" + circ = QuantumCircuit(2) + circ.append(ModularAdderGate(1), [0, 1]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_multiplier_gate_definition(self): + """Test Multiplier gate matrix and definition.""" + circ = QuantumCircuit(4) + circ.append(MultiplierGate(1), [0, 1, 2, 3]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + @ddt class TestStandardGates(QiskitTestCase):