Skip to content

Commit

Permalink
Improved MCXVChain with dirty auxiliary qubits (#9687)
Browse files Browse the repository at this point in the history
* improved mcxvchain with dirty aux qubits

Co-authored-by: thiagom123 <thiagomdazevedo@hotmail.com>
Co-authored-by: IsmaelCesar <leamscesar@gmail.com>
Co-authored-by: Israel F. Araujo <israelferrazaraujo@hotmail.com>
Co-authored-by: Adenilton Silva <7927558+adjs@users.noreply.github.com>

* tests for improved mcxvchain with dirty aux qubits

Co-authored-by: thiagom123 <thiagomdazevedo@hotmail.com>
Co-authored-by: IsmaelCesar <leamscesar@gmail.com>
Co-authored-by: Israel F. Araujo <israelferrazaraujo@hotmail.com>
Co-Authored-By: Adenilton Silva <7927558+adjs@users.noreply.github.com>

* black

* lint

* Update x.py

* Update test_controlled_gate.py

fix test_mcxvchain_dirty_ancilla_relative_phase
reduce number of controls in test_mcxvchain_dirty_ancilla_action_only and test_mcxvchain_dirty_ancilla_relative_phase

* Update test_controlled_gate.py

* release notes

* removing non existend module

* removing U1 and U2 gates MCXVChain

* black

* using the quantum_circuit.operation(qubit_indices) notation

* mcx

* comments

* lint

* rearranging transpile import

---------

Co-authored-by: thiagom123 <thiagomdazevedo@hotmail.com>
Co-authored-by: IsmaelCesar <leamscesar@gmail.com>
Co-authored-by: Israel F. Araujo <israelferrazaraujo@hotmail.com>
Co-authored-by: Adenilton Silva <7927558+adjs@users.noreply.github.com>
  • Loading branch information
5 people authored Jul 7, 2024
1 parent bb60891 commit ee41b18
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 55 deletions.
151 changes: 96 additions & 55 deletions qiskit/circuit/library/standard_gates/x.py
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,8 @@ def __new__(
duration=None,
unit="dt",
_base_label=None,
relative_phase: bool = False, # pylint: disable=unused-argument
action_only: bool = False, # pylint: disable=unused-argument
):
"""Create a new MCX instance.
Expand All @@ -1434,7 +1436,24 @@ def __init__(
duration=None,
unit="dt",
_base_label=None,
relative_phase: bool = False,
action_only: bool = False,
):
"""
Args:
dirty_ancillas: when set to ``True``, the method applies an optimized multicontrolled-X gate
up to a relative phase using dirty ancillary qubits with the properties of lemmas 7 and 8
from arXiv:1501.06911, with at most 8*k - 6 CNOT gates.
For k within the range {1, ..., ceil(n/2)}. And for n representing the total number of
qubits.
relative_phase: when set to ``True``, the method applies the optimized multicontrolled-X gate
up to a relative phase, in a way that, by lemma 7 of arXiv:1501.06911, the relative
phases of the ``action part`` cancel out with the phases of the ``reset part``.
action_only: when set to ``True``, the method applies only the action part of lemma 8
from arXiv:1501.06911.
"""
super().__init__(
num_ctrl_qubits,
label=label,
Expand All @@ -1445,6 +1464,9 @@ def __init__(
unit=unit,
)
self._dirty_ancillas = dirty_ancillas
self._relative_phase = relative_phase
self._action_only = action_only
super().__init__(num_ctrl_qubits, label=label, ctrl_state=ctrl_state, _name="mcx_vchain")

def inverse(self, annotated: bool = False):
"""Invert this gate. The MCX is its own inverse.
Expand All @@ -1462,6 +1484,8 @@ def inverse(self, annotated: bool = False):
num_ctrl_qubits=self.num_ctrl_qubits,
dirty_ancillas=self._dirty_ancillas,
ctrl_state=self.ctrl_state,
relative_phase=self._relative_phase,
action_only=self._action_only,
)

@staticmethod
Expand All @@ -1473,73 +1497,90 @@ def _define(self):
"""Define the MCX gate using a V-chain of CX gates."""
# pylint: disable=cyclic-import
from qiskit.circuit.quantumcircuit import QuantumCircuit
from .u1 import U1Gate
from .u2 import U2Gate

q = QuantumRegister(self.num_qubits, name="q")
qc = QuantumCircuit(q, name=self.name)
q_controls = q[: self.num_ctrl_qubits]
q_target = q[self.num_ctrl_qubits]
q_ancillas = q[self.num_ctrl_qubits + 1 :]

definition = []

if self._dirty_ancillas:
i = self.num_ctrl_qubits - 3
ancilla_pre_rule = [
(U2Gate(0, numpy.pi), [q_target], []),
(CXGate(), [q_target, q_ancillas[i]], []),
(U1Gate(-numpy.pi / 4), [q_ancillas[i]], []),
(CXGate(), [q_controls[-1], q_ancillas[i]], []),
(U1Gate(numpy.pi / 4), [q_ancillas[i]], []),
(CXGate(), [q_target, q_ancillas[i]], []),
(U1Gate(-numpy.pi / 4), [q_ancillas[i]], []),
(CXGate(), [q_controls[-1], q_ancillas[i]], []),
(U1Gate(numpy.pi / 4), [q_ancillas[i]], []),
]
for inst in ancilla_pre_rule:
definition.append(inst)
if self.num_ctrl_qubits < 3:
qc.mcx(q_controls, q_target)
elif not self._relative_phase and self.num_ctrl_qubits == 3:
qc._append(C3XGate(), [*q_controls, q_target], [])
else:
num_ancillas = self.num_ctrl_qubits - 2
targets = [q_target] + q_ancillas[:num_ancillas][::-1]

for j in range(2):
for i in range(self.num_ctrl_qubits): # action part
if i < self.num_ctrl_qubits - 2:
if targets[i] != q_target or self._relative_phase:
# gate cancelling

# cancel rightmost gates of action part
# with leftmost gates of reset part
if self._relative_phase and targets[i] == q_target and j == 1:
qc.cx(q_ancillas[num_ancillas - i - 1], targets[i])
qc.t(targets[i])
qc.cx(q_controls[self.num_ctrl_qubits - i - 1], targets[i])
qc.tdg(targets[i])
qc.h(targets[i])
else:
qc.h(targets[i])
qc.t(targets[i])
qc.cx(q_controls[self.num_ctrl_qubits - i - 1], targets[i])
qc.tdg(targets[i])
qc.cx(q_ancillas[num_ancillas - i - 1], targets[i])
else:
controls = [
q_controls[self.num_ctrl_qubits - i - 1],
q_ancillas[num_ancillas - i - 1],
]

qc.ccx(controls[0], controls[1], targets[i])
else:
# implements an optimized toffoli operation
# up to a diagonal gate, akin to lemma 6 of arXiv:1501.06911
qc.h(targets[i])
qc.t(targets[i])
qc.cx(q_controls[self.num_ctrl_qubits - i - 2], targets[i])
qc.tdg(targets[i])
qc.cx(q_controls[self.num_ctrl_qubits - i - 1], targets[i])
qc.t(targets[i])
qc.cx(q_controls[self.num_ctrl_qubits - i - 2], targets[i])
qc.tdg(targets[i])
qc.h(targets[i])

break

for i in range(num_ancillas - 1): # reset part
qc.cx(q_ancillas[i], q_ancillas[i + 1])
qc.t(q_ancillas[i + 1])
qc.cx(q_controls[2 + i], q_ancillas[i + 1])
qc.tdg(q_ancillas[i + 1])
qc.h(q_ancillas[i + 1])

if self._action_only:
qc.ccx(q_controls[-1], q_ancillas[-1], q_target)

break
else:
qc.rccx(q_controls[0], q_controls[1], q_ancillas[0])
i = 0
for j in range(2, self.num_ctrl_qubits - 1):
qc.rccx(q_controls[j], q_ancillas[i], q_ancillas[i + 1])

for j in reversed(range(2, self.num_ctrl_qubits - 1)):
definition.append(
(RCCXGate(), [q_controls[j], q_ancillas[i - 1], q_ancillas[i]], [])
)
i -= 1
i += 1

definition.append((RCCXGate(), [q_controls[0], q_controls[1], q_ancillas[0]], []))
i = 0
for j in range(2, self.num_ctrl_qubits - 1):
definition.append((RCCXGate(), [q_controls[j], q_ancillas[i], q_ancillas[i + 1]], []))
i += 1
qc.ccx(q_controls[-1], q_ancillas[i], q_target)

if self._dirty_ancillas:
ancilla_post_rule = [
(U1Gate(-numpy.pi / 4), [q_ancillas[i]], []),
(CXGate(), [q_controls[-1], q_ancillas[i]], []),
(U1Gate(numpy.pi / 4), [q_ancillas[i]], []),
(CXGate(), [q_target, q_ancillas[i]], []),
(U1Gate(-numpy.pi / 4), [q_ancillas[i]], []),
(CXGate(), [q_controls[-1], q_ancillas[i]], []),
(U1Gate(numpy.pi / 4), [q_ancillas[i]], []),
(CXGate(), [q_target, q_ancillas[i]], []),
(U2Gate(0, numpy.pi), [q_target], []),
]
for inst in ancilla_post_rule:
definition.append(inst)
else:
definition.append((CCXGate(), [q_controls[-1], q_ancillas[i], q_target], []))
for j in reversed(range(2, self.num_ctrl_qubits - 1)):
qc.rccx(q_controls[j], q_ancillas[i - 1], q_ancillas[i])

for j in reversed(range(2, self.num_ctrl_qubits - 1)):
definition.append((RCCXGate(), [q_controls[j], q_ancillas[i - 1], q_ancillas[i]], []))
i -= 1
definition.append((RCCXGate(), [q_controls[0], q_controls[1], q_ancillas[i]], []))
i -= 1

if self._dirty_ancillas:
for i, j in enumerate(list(range(2, self.num_ctrl_qubits - 1))):
definition.append(
(RCCXGate(), [q_controls[j], q_ancillas[i], q_ancillas[i + 1]], [])
)
qc.rccx(q_controls[0], q_controls[1], q_ancillas[i])

for instr, qargs, cargs in definition:
qc._append(instr, qargs, cargs)
self.definition = qc
10 changes: 10 additions & 0 deletions releasenotes/notes/mcxvchain_dirty_auxiliary-5ea4037557209f6e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
features:
- |
:class:`.MCXVChain` has two new Boolean parameters `relative_phase` and `action_only`.
If `action_only` the circuit does not clean the dirty qubits. If `relative_phase`
the gate is implemented up to a global phase. Both parameters are used to optimize the
decomposition of MCXVChain.
fixes:
- |
:class:`.MCXVChain` with k controls and k-2 dirty auxiliary qubits now requires 8k-6 cx gates.
61 changes: 61 additions & 0 deletions test/python/circuit/test_controlled_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,67 @@ def test_mcx_gates(self, num_ctrl_qubits):
statevector = corrected
np.testing.assert_array_almost_equal(statevector.real, reference)

@data(5, 10, 15)
def test_mcxvchain_dirty_ancilla_cx_count(self, num_ctrl_qubits):
"""Test if cx count of the v-chain mcx with dirty ancilla
is less than upper bound."""
from qiskit import transpile

mcx_vchain = MCXVChain(num_ctrl_qubits, dirty_ancillas=True)
qc = QuantumCircuit(mcx_vchain.num_qubits)

qc.append(mcx_vchain, list(range(mcx_vchain.num_qubits)))

tr_mcx_vchain = transpile(qc, basis_gates=["u", "cx"])
cx_count = tr_mcx_vchain.count_ops()["cx"]

self.assertLessEqual(cx_count, 8 * num_ctrl_qubits - 6)

def test_mcxvchain_dirty_ancilla_action_only(self):
"""Test the v-chain mcx with dirty auxiliary qubits
with gate cancelling with mirrored circuit."""

num_ctrl_qubits = 5

gate = MCXVChain(num_ctrl_qubits, dirty_ancillas=True)
gate_with_cancelling = MCXVChain(num_ctrl_qubits, dirty_ancillas=True, action_only=True)

num_qubits = gate.num_qubits
ref_circuit = QuantumCircuit(num_qubits)
circuit = QuantumCircuit(num_qubits)

ref_circuit.append(gate, list(range(num_qubits)), [])
ref_circuit.h(num_ctrl_qubits)
ref_circuit.append(gate, list(range(num_qubits)), [])

circuit.append(gate_with_cancelling, list(range(num_qubits)), [])
circuit.h(num_ctrl_qubits)
circuit.append(gate_with_cancelling.inverse(), list(range(num_qubits)), [])

self.assertTrue(matrix_equal(Operator(circuit).data, Operator(ref_circuit).data))

def test_mcxvchain_dirty_ancilla_relative_phase(self):
"""Test the v-chain mcx with dirty auxiliary qubits
with only relative phase Toffoli gates."""
num_ctrl_qubits = 5

gate = MCXVChain(num_ctrl_qubits, dirty_ancillas=True)
gate_relative_phase = MCXVChain(num_ctrl_qubits, dirty_ancillas=True, relative_phase=True)

num_qubits = gate.num_qubits + 1
ref_circuit = QuantumCircuit(num_qubits)
circuit = QuantumCircuit(num_qubits)

ref_circuit.append(gate, list(range(num_qubits - 1)), [])
ref_circuit.h(num_qubits - 1)
ref_circuit.append(gate, list(range(num_qubits - 1)), [])

circuit.append(gate_relative_phase, list(range(num_qubits - 1)), [])
circuit.h(num_qubits - 1)
circuit.append(gate_relative_phase.inverse(), list(range(num_qubits - 1)), [])

self.assertTrue(matrix_equal(Operator(circuit).data, Operator(ref_circuit).data))

@data(1, 2, 3, 4)
def test_inverse_x(self, num_ctrl_qubits):
"""Test inverting the controlled X gate."""
Expand Down

0 comments on commit ee41b18

Please sign in to comment.