Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not unroll gates in basis in add_control (backport #13475) #13528

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 110 additions & 92 deletions qiskit/circuit/add_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""Add control to operation if supported."""
from __future__ import annotations

from math import pi
from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.library import UnitaryGate
from qiskit.transpiler import PassManager
Expand Down Expand Up @@ -92,7 +93,6 @@ def control(
Raises:
CircuitError: gate contains non-gate in definition
"""
from math import pi

# pylint: disable=cyclic-import
from qiskit.circuit import controlledgate
Expand All @@ -101,22 +101,23 @@ def control(

q_control = QuantumRegister(num_ctrl_qubits, name="control")
q_target = QuantumRegister(operation.num_qubits, name="target")
q_ancillae = None # TODO: add
controlled_circ = QuantumCircuit(q_control, q_target, name=f"c_{operation.name}")
if isinstance(operation, controlledgate.ControlledGate):
original_ctrl_state = operation.ctrl_state
operation = operation.to_mutable()
operation.ctrl_state = None

global_phase = 0
if operation.name == "x" or (
isinstance(operation, controlledgate.ControlledGate) and operation.base_gate.name == "x"
):
controlled_circ.mcx(q_control[:] + q_target[:-1], q_target[-1], q_ancillae)
if operation.definition is not None and operation.definition.global_phase:
global_phase += operation.definition.global_phase

basis = ["p", "u", "x", "z", "rx", "ry", "rz", "cx"]

if operation.name in basis:
apply_basic_controlled_gate(controlled_circ, operation, q_control, q_target[0])
else:
basis = ["p", "u", "x", "z", "rx", "ry", "rz", "cx"]
if isinstance(operation, controlledgate.ControlledGate):
operation = operation.to_mutable()
operation.ctrl_state = None

unrolled_gate = _unroll_gate(operation, basis_gates=basis)
if unrolled_gate.definition.global_phase:
global_phase += unrolled_gate.definition.global_phase
Expand All @@ -130,87 +131,17 @@ def control(

for instruction in definition.data:
gate, qargs = instruction.operation, instruction.qubits
if gate.name == "x":
controlled_circ.mcx(q_control, q_target[bit_indices[qargs[0]]], q_ancillae)
elif gate.name == "rx":
controlled_circ.mcrx(
gate.definition.data[0].operation.params[0],
q_control,
q_target[bit_indices[qargs[0]]],
use_basis_gates=False,
)
elif gate.name == "ry":
controlled_circ.mcry(
gate.definition.data[0].operation.params[0],
q_control,
q_target[bit_indices[qargs[0]]],
q_ancillae,
mode="noancilla",
use_basis_gates=False,
)
elif gate.name == "rz":
controlled_circ.mcrz(
gate.definition.data[0].operation.params[0],
q_control,
q_target[bit_indices[qargs[0]]],
use_basis_gates=False,
)
continue
elif gate.name == "p":
from qiskit.circuit.library import MCPhaseGate

controlled_circ.append(
MCPhaseGate(gate.params[0], num_ctrl_qubits),
q_control[:] + [q_target[bit_indices[qargs[0]]]],
)
elif gate.name == "cx":
controlled_circ.mcx(
q_control[:] + [q_target[bit_indices[qargs[0]]]],
q_target[bit_indices[qargs[1]]],
q_ancillae,
)
elif gate.name == "u":
theta, phi, lamb = gate.params
if num_ctrl_qubits == 1:
if theta == 0 and phi == 0:
controlled_circ.cp(lamb, q_control[0], q_target[bit_indices[qargs[0]]])
else:
controlled_circ.cu(
theta, phi, lamb, 0, q_control[0], q_target[bit_indices[qargs[0]]]
)
else:
if phi == -pi / 2 and lamb == pi / 2:
controlled_circ.mcrx(
theta, q_control, q_target[bit_indices[qargs[0]]], use_basis_gates=True
)
elif phi == 0 and lamb == 0:
controlled_circ.mcry(
theta,
q_control,
q_target[bit_indices[qargs[0]]],
q_ancillae,
use_basis_gates=True,
)
elif theta == 0 and phi == 0:
controlled_circ.mcp(lamb, q_control, q_target[bit_indices[qargs[0]]])
else:
controlled_circ.mcp(lamb, q_control, q_target[bit_indices[qargs[0]]])
controlled_circ.mcry(
theta,
q_control,
q_target[bit_indices[qargs[0]]],
q_ancillae,
use_basis_gates=True,
)
controlled_circ.mcp(phi, q_control, q_target[bit_indices[qargs[0]]])
elif gate.name == "z":
controlled_circ.h(q_target[bit_indices[qargs[0]]])
controlled_circ.mcx(q_control, q_target[bit_indices[qargs[0]]], q_ancillae)
controlled_circ.h(q_target[bit_indices[qargs[0]]])
if len(qargs) == 1:
target = q_target[bit_indices[qargs[0]]]
else:
raise CircuitError(f"gate contains non-controllable instructions: {gate.name}")
if gate.definition is not None and gate.definition.global_phase:
target = [q_target[bit_indices[qarg]] for qarg in qargs]

apply_basic_controlled_gate(controlled_circ, gate, q_control, target)

if gate.definition is not None and gate.definition.global_phase and gate.name != "rz":
global_phase += gate.definition.global_phase

# apply controlled global phase
if global_phase:
if len(q_control) < 2:
Expand All @@ -228,6 +159,7 @@ def control(
new_ctrl_state = ctrl_state
base_name = operation.name
base_gate = operation

# In order to maintain some backward compatibility with gate names this
# uses a naming convention where if the number of controls is <=2 the gate
# is named like "cc<base_gate.name>", else it is named like
Expand All @@ -250,15 +182,101 @@ def control(
return cgate


def apply_basic_controlled_gate(circuit, gate, controls, target):
"""Apply a controlled version of ``gate`` to the circuit.

This implements multi-control operations for the following basis gates:

["p", "u", "x", "z", "rx", "ry", "rz", "cx"]

"""
num_ctrl_qubits = len(controls)

if gate.name == "x":
circuit.mcx(controls, target)

elif gate.name == "rx":
circuit.mcrx(
gate.definition.data[0].operation.params[0],
controls,
target,
use_basis_gates=False,
)
elif gate.name == "ry":
circuit.mcry(
gate.definition.data[0].operation.params[0],
controls,
target,
mode="noancilla",
use_basis_gates=False,
)
elif gate.name == "rz":
circuit.mcrz(
gate.definition.data[0].operation.params[0],
controls,
target,
use_basis_gates=False,
)
# continue
elif gate.name == "p":
from qiskit.circuit.library import MCPhaseGate

circuit.append(
MCPhaseGate(gate.params[0], num_ctrl_qubits),
controls[:] + [target],
)
elif gate.name == "cx":
circuit.mcx(
controls[:] + [target[0]], # CX has two targets
target[1],
)
elif gate.name == "u":
theta, phi, lamb = gate.params
if num_ctrl_qubits == 1:
if theta == 0 and phi == 0:
circuit.cp(lamb, controls[0], target)
else:
circuit.cu(theta, phi, lamb, 0, controls[0], target)
else:
if phi == -pi / 2 and lamb == pi / 2:
circuit.mcrx(theta, controls, target, use_basis_gates=True)
elif phi == 0 and lamb == 0:
circuit.mcry(
theta,
controls,
target,
use_basis_gates=True,
)
elif theta == 0 and phi == 0:
circuit.mcp(lamb, controls, target)
else:
circuit.mcp(lamb, controls, target)
circuit.mcry(
theta,
controls,
target,
use_basis_gates=True,
)
circuit.mcp(phi, controls, target)

elif gate.name == "z":
circuit.h(target)
circuit.mcx(controls, target)
circuit.h(target)

else:
raise CircuitError(f"Gate {gate} not in supported basis.")


def _gate_to_circuit(operation):
"""Converts a gate instance to a QuantumCircuit"""
if hasattr(operation, "definition") and operation.definition is not None:
return operation.definition
else:
qr = QuantumRegister(operation.num_qubits)
qc = QuantumCircuit(qr, name=operation.name)
qc.append(operation, qr)
return qc

qr = QuantumRegister(operation.num_qubits)
qc = QuantumCircuit(qr, name=operation.name)
qc.append(operation, qr)
return qc


def _unroll_gate(operation, basis_gates):
Expand Down
11 changes: 11 additions & 0 deletions releasenotes/notes/fix-mcrz-19d14a8d973a82cb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
fixes:
- |
Fixed a bug in :meth:`.RZGate.control` for more than 1 control qubit, which used
an unnecessarily expensive decomposition.
Fixed `#13473 <https://github.com/Qiskit/qiskit/issues/13473>`__.
upgrade_circuits:
- |
The generic control method for gates now avoids attempting to translate gates
into a supported basis, if the gate is already supported. This can slightly change the
synthesis of the controlled gate, although it should not increase the two-qubit gate count.
34 changes: 28 additions & 6 deletions test/python/circuit/test_controlled_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from qiskit.quantum_info.states import Statevector
import qiskit.circuit.add_control as ac
from qiskit.transpiler.passes import UnrollCustomDefinitions, BasisTranslator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.converters.circuit_to_dag import circuit_to_dag
from qiskit.converters.dag_to_circuit import dag_to_circuit
from qiskit.quantum_info import Operator
Expand Down Expand Up @@ -1571,13 +1572,15 @@ def test_single_controlled_rotation_gates(self, gate, cgate):
uqc = dag_to_circuit(basis_translator.run(unroller.run(dag)))
self.log.info("%s gate count: %d", cgate.name, uqc.size())
self.log.info("\n%s", str(uqc))
# these limits could be changed
if gate.name == "ry":
self.assertLessEqual(uqc.size(), 32, f"\n{uqc}")

if gate.name in ["ry", "rx"]:
expected_cx = 8
elif gate.name == "rz":
self.assertLessEqual(uqc.size(), 43, f"\n{uqc}")
else:
self.assertLessEqual(uqc.size(), 20, f"\n{uqc}")
expected_cx = 4
else: # u1
expected_cx = 6

self.assertLessEqual(uqc.count_ops().get("cx", 0), expected_cx, f"\n{uqc}")

def test_composite(self):
"""Test composite gate count."""
Expand All @@ -1595,6 +1598,25 @@ def test_composite(self):
self.log.info("%s gate count: %d", uqc.name, uqc.size())
self.assertLessEqual(uqc.size(), 96, f"\n{uqc}") # this limit could be changed

def test_mcrz_complexity(self):
"""Test MCRZ is decomposed using the efficient MC-SU(2) algorithm.

Regression test of #13473.
"""
basis_gates = ["sx", "x", "rz", "ecr"]
pm = generate_preset_pass_manager(
optimization_level=3, basis_gates=basis_gates, seed_transpiler=12345
)

num_qubits = 6
angle = np.pi / 7
qc = QuantumCircuit(num_qubits)
qc.append(RZGate(angle).control(num_qubits - 1), qc.qubits)

isa_qc = pm.run(qc)

self.assertLessEqual(isa_qc.count_ops().get("ecr", 0), 40)


@ddt
class TestControlledStandardGates(QiskitTestCase):
Expand Down
Loading