Skip to content

Commit

Permalink
Fix zero-operand gates and instructions (#8272) (#9034)
Browse files Browse the repository at this point in the history
* Fix zero-operand gates and instructions

A few checks in different places didn't account for zero-operand gates
being possible.  In most uses, there never would be anything useful
here, but it can occasionally be useful to create a "global-phase"
instruction, especially if this is later going to have controls applied
to it.  This is well-formed within the Qiskit data model; spectator
qubits implicitly undergo the identity, so this corresponds to the
global phase being multiplied by the identity on _all_ qubits.

* Fix controlled-gate qubit-number documentation

* Add ScalarOp test

* Be stricter about allowed num_ctrl_qubits

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit bb93592)

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
  • Loading branch information
mergify[bot] and jakelishman authored Oct 29, 2022
1 parent 8c15424 commit ad6f295
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 11 deletions.
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

0 comments on commit ad6f295

Please sign in to comment.