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

Fix zero-operand gates and instructions (backport #8272) #9034

Merged
merged 1 commit into from
Oct 29, 2022
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
18 changes: 12 additions & 6 deletions qiskit/circuit/controlledgate.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,21 @@ def num_ctrl_qubits(self, num_ctrl_qubits):
"""Set the number of control qubits.

Args:
num_ctrl_qubits (int): The number of control qubits in [1, num_qubits-1].
num_ctrl_qubits (int): The number of control qubits.

Raises:
CircuitError: num_ctrl_qubits is not an integer in [1, num_qubits - 1].
CircuitError: ``num_ctrl_qubits`` is not an integer in ``[1, num_qubits]``.
"""
if num_ctrl_qubits == int(num_ctrl_qubits) and 1 <= num_ctrl_qubits < self.num_qubits:
self._num_ctrl_qubits = num_ctrl_qubits
else:
raise CircuitError("The number of control qubits must be in [1, num_qubits-1]")
if num_ctrl_qubits != int(num_ctrl_qubits):
raise CircuitError("The number of control qubits must be an integer.")
num_ctrl_qubits = int(num_ctrl_qubits)
# This is a range rather than an equality limit because some controlled gates represent a
# controlled version of the base gate whose definition also uses auxiliary qubits.
upper_limit = self.num_qubits - getattr(self.base_gate, "num_qubits", 0)
if num_ctrl_qubits < 1 or num_ctrl_qubits > upper_limit:
limit = "num_qubits" if self.base_gate is None else "num_qubits - base_gate.num_qubits"
raise CircuitError(f"The number of control qubits must be in `[1, {limit}]`.")
self._num_ctrl_qubits = num_ctrl_qubits

@property
def ctrl_state(self) -> int:
Expand Down
4 changes: 4 additions & 0 deletions qiskit/circuit/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ def broadcast_arguments(self, qargs: List, cargs: List) -> Tuple[List, List]:
if any(not qarg for qarg in qargs):
raise CircuitError("One or more of the arguments are empty")

if len(qargs) == 0:
return [
([], []),
]
if len(qargs) == 1:
return Gate._broadcast_single_argument(qargs[0])
elif len(qargs) == 2:
Expand Down
4 changes: 2 additions & 2 deletions qiskit/converters/circuit_to_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label
if equivalence_library is not None:
equivalence_library.add_equivalence(gate, target)

qc = QuantumCircuit(name=gate.name, global_phase=target.global_phase)
if gate.num_qubits > 0:
q = QuantumRegister(gate.num_qubits, "q")

qc.add_register(q)
qubit_map = {bit: q[idx] for idx, bit in enumerate(circuit.qubits)}

# The 3rd parameter in the output tuple) is hard coded to [] because
# Gate objects do not have cregs set and we've verified that all
# instructions are gates
qc = QuantumCircuit(q, name=gate.name, global_phase=target.global_phase)
for instruction in target.data:
qc._append(instruction.replace(qubits=tuple(qubit_map[y] for y in instruction.qubits)))
gate.definition = qc
Expand Down
2 changes: 1 addition & 1 deletion qiskit/quantum_info/operators/op_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ def _tensor(cls, a, b):
def compose(self, other, qargs=None, front=False):
"""Return composed OpShape."""
ret = OpShape()
if not qargs:
if qargs is None:
if front:
if self._num_qargs_r != other._num_qargs_l or self._dims_r != other._dims_l:
raise QiskitError(
Expand Down
10 changes: 10 additions & 0 deletions releasenotes/notes/fix-zero-operand-gates-323510ec8f392f27.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
fixes:
- |
Zero-operand gates and instructions will now work with
:func:`.circuit_to_gate`, :meth:`.QuantumCircuit.to_gate`,
:meth:`.Gate.control`, and the construction of an
:class:`~.quantum_info.Operator` from a :class:`.QuantumCircuit` containing
zero-operand instructions. This edge case is occasionally useful in creating
global-phase gates as part of larger compound instructions, though for many
uses, :attr:`.QuantumCircuit.global_phase` may be more appropriate.
38 changes: 37 additions & 1 deletion test/python/circuit/test_controlled_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from qiskit import QuantumRegister, QuantumCircuit, execute, BasicAer, QiskitError
from qiskit.test import QiskitTestCase
from qiskit.circuit import ControlledGate, Parameter
from qiskit.circuit import ControlledGate, Parameter, Gate
from qiskit.circuit.exceptions import CircuitError
from qiskit.quantum_info.operators.predicates import matrix_equal, is_unitary_matrix
from qiskit.quantum_info.random import random_unitary
Expand Down Expand Up @@ -1135,6 +1135,29 @@ def test_improper_num_ctrl_qubits(self, num_ctrl_qubits):
name="cgate", num_qubits=num_qubits, params=[], num_ctrl_qubits=num_ctrl_qubits
)

def test_improper_num_ctrl_qubits_base_gate(self):
"""Test that the allowed number of control qubits takes the base gate into account."""
with self.assertRaises(CircuitError):
ControlledGate(
name="cx?", num_qubits=2, params=[], num_ctrl_qubits=2, base_gate=XGate()
)
self.assertIsInstance(
ControlledGate(
name="cx?", num_qubits=2, params=[], num_ctrl_qubits=1, base_gate=XGate()
),
ControlledGate,
)
self.assertIsInstance(
ControlledGate(
name="p",
num_qubits=1,
params=[np.pi],
num_ctrl_qubits=1,
base_gate=Gate("gphase", 0, [np.pi]),
),
ControlledGate,
)

def test_open_controlled_equality(self):
"""
Test open controlled gates are equal if their base gates and control states are equal.
Expand Down Expand Up @@ -1220,6 +1243,19 @@ def test_nested_global_phase(self, num_ctrl_qubits):
target = _compute_control_matrix(base_mat, num_ctrl_qubits)
self.assertEqual(Operator(ctrl_qc), Operator(target))

@data(1, 2)
def test_control_zero_operand_gate(self, num_ctrl_qubits):
"""Test that a zero-operand gate (such as a make-shift global-phase gate) can be
controlled."""
gate = QuantumCircuit(global_phase=np.pi).to_gate()
controlled = gate.control(num_ctrl_qubits)
self.assertIsInstance(controlled, ControlledGate)
self.assertEqual(controlled.num_ctrl_qubits, num_ctrl_qubits)
self.assertEqual(controlled.num_qubits, num_ctrl_qubits)
target = np.eye(2**num_ctrl_qubits, dtype=np.complex128)
target.flat[-1] = -1
self.assertEqual(Operator(controlled), Operator(target))


@ddt
class TestOpenControlledToMatrix(QiskitTestCase):
Expand Down
16 changes: 16 additions & 0 deletions test/python/converters/test_circuit_to_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@

"""Tests for the converters."""

import math

import numpy as np

from qiskit import QuantumRegister, QuantumCircuit
from qiskit.circuit import Gate, Qubit
from qiskit.quantum_info import Operator
from qiskit.test import QiskitTestCase
from qiskit.exceptions import QiskitError

Expand Down Expand Up @@ -106,3 +111,14 @@ def test_to_gate_label(self):
gate = circ.to_gate(label="a label")

self.assertEqual(gate.label, "a label")

def test_zero_operands(self):
"""Test that a gate can be created, even if it has zero operands."""
base = QuantumCircuit(global_phase=math.pi)
gate = base.to_gate()
self.assertEqual(gate.num_qubits, 0)
self.assertEqual(gate.num_clbits, 0)
self.assertEqual(gate.definition, base)
compound = QuantumCircuit(1)
compound.append(gate, [], [])
np.testing.assert_allclose(-np.eye(2), Operator(compound), atol=1e-16)
15 changes: 15 additions & 0 deletions test/python/converters/test_circuit_to_instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@

"""Tests for the converters."""

import math
import unittest

import numpy as np

from qiskit.converters import circuit_to_instruction
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.circuit import Qubit, Clbit, Instruction
from qiskit.circuit import Parameter
from qiskit.quantum_info import Operator
from qiskit.test import QiskitTestCase
from qiskit.exceptions import QiskitError

Expand Down Expand Up @@ -203,6 +207,17 @@ def test_registerless_classical_bits(self):
self.assertIs(type(test_instruction.operation), type(expected_instruction.operation))
self.assertEqual(test_instruction.operation.condition, (test.definition.clbits[0], 0))

def test_zero_operands(self):
"""Test that an instruction can be created, even if it has zero operands."""
base = QuantumCircuit(global_phase=math.pi)
instruction = base.to_instruction()
self.assertEqual(instruction.num_qubits, 0)
self.assertEqual(instruction.num_clbits, 0)
self.assertEqual(instruction.definition, base)
compound = QuantumCircuit(1)
compound.append(instruction, [], [])
np.testing.assert_allclose(-np.eye(2), Operator(compound), atol=1e-16)


if __name__ == "__main__":
unittest.main(verbosity=2)
16 changes: 15 additions & 1 deletion test/python/quantum_info/operators/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from qiskit.circuit.library import HGate, CHGate, CXGate, QFT
from qiskit.test import QiskitTestCase
from qiskit.transpiler.layout import Layout, TranspileLayout
from qiskit.quantum_info.operators.operator import Operator
from qiskit.quantum_info.operators import Operator, ScalarOp
from qiskit.quantum_info.operators.predicates import matrix_equal
from qiskit.compiler.transpiler import transpile
from qiskit.circuit import Qubit
Expand Down Expand Up @@ -864,6 +864,20 @@ def test_from_circuit_constructor_empty_layout(self):
with self.assertRaises(IndexError):
Operator.from_circuit(circuit, layout=layout)

def test_compose_scalar(self):
"""Test that composition works with a scalar-valued operator over no qubits."""
base = Operator(np.eye(2, dtype=np.complex128))
scalar = Operator(np.array([[-1.0 + 0.0j]]))
composed = base.compose(scalar, qargs=[])
self.assertEqual(composed, Operator(-np.eye(2, dtype=np.complex128)))

def test_compose_scalar_op(self):
"""Test that composition works with an explicit scalar operator over no qubits."""
base = Operator(np.eye(2, dtype=np.complex128))
scalar = ScalarOp(coeff=-1.0 + 0.0j)
composed = base.compose(scalar, qargs=[])
self.assertEqual(composed, Operator(-np.eye(2, dtype=np.complex128)))

def test_from_circuit_single_flat_default_register_transpiled(self):
"""Test a transpiled circuit with layout set from default register."""
circuit = QuantumCircuit(5)
Expand Down