Skip to content

Commit

Permalink
Minor fixes for new adder and multiplier gates classes (#13530) (#13560)
Browse files Browse the repository at this point in the history
* 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

(cherry picked from commit b9ea266)

Co-authored-by: Alexander Ivrii <[email protected]>
mergify[bot] and alexanderivrii authored Dec 12, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 1e32313 commit 94e8cba
Showing 7 changed files with 279 additions and 20 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
25 changes: 25 additions & 0 deletions qiskit/circuit/library/arithmetic/adders/adder.py
Original file line number Diff line number Diff line change
@@ -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")
9 changes: 9 additions & 0 deletions qiskit/circuit/library/arithmetic/multipliers/multiplier.py
Original file line number Diff line number Diff line change
@@ -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)
103 changes: 85 additions & 18 deletions qiskit/transpiler/passes/synthesis/hls_plugins.py
Original file line number Diff line number Diff line change
@@ -300,13 +300,18 @@
- :class:`.ModularAdderSynthesisD00`
- 0
- a QFT-based adder
* - ``"default"``
- :class:`~.ModularAdderSynthesisDefault`
- any
- chooses the best algorithm based on the ancillas available
.. autosummary::
:toctree: ../stubs/
ModularAdderSynthesisC04
ModularAdderSynthesisD00
ModularAdderSynthesisV95
ModularAdderSynthesisDefault
Half Adder Synthesis
''''''''''''''''''''
@@ -330,13 +335,18 @@
- :class:`.HalfAdderSynthesisD00`
- 0
- a QFT-based adder
* - ``"default"``
- :class:`~.HalfAdderSynthesisDefault`
- any
- chooses the best algorithm based on the ancillas available
.. autosummary::
:toctree: ../stubs/
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")
18 changes: 18 additions & 0 deletions releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml
Original file line number Diff line number Diff line change
@@ -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.
102 changes: 101 additions & 1 deletion test/python/circuit/library/test_adders.py
Original file line number Diff line number Diff line change
@@ -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()
40 changes: 40 additions & 0 deletions test/python/circuit/test_gate_definitions.py
Original file line number Diff line number Diff line change
@@ -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):

0 comments on commit 94e8cba

Please sign in to comment.