Repository: Qiskit/qiskit

Issue:
QPY serialization of empty gates yields ``None`` definitions 
### Environment

- **Qiskit Terra version**: 0.19.1
- **Python version**: 3.9
- **Operating system**: macOS


### What is happening?

Serializing + deserializing a circuit containing an empty gate (i.e. no operations) with QPY yields a gate where the definition is `None` instead of an empty circuit. If the definition is `None` the transpiler cannot decompose the gate, unless the gate is in the basis set (like e.g. `x` or `cx` are). 

This is a problem in particular for Qiskit runtime as users might unintentionally construct circuits that contain empty gates, as e.g. was the case in Qiskit/qiskit-nature#449.

### How can we reproduce the issue?

```python
import io
from qiskit import Aer, transpile, QuantumCircuit
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.qpy_serialization import dump, load

empty = QuantumCircuit(1, name="empty")
circuit = QuantumCircuit(1)
circuit.append(empty, [0])

qpy_file = io.BytesIO()
dump(circuit, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]

sim = Aer.get_backend("aer_simulator")
transpile(new_circuit, sim)
```
produces 
```
qiskit.exceptions.QiskitError: "Cannot unroll the circuit to the given basis, ['ccx', 'cp', 'cswap', 'csx', 'cu', 'cu1', 'cu2', 'cu3', 'cx', 'cy', 'cz', 'delay', 'diagonal', 'h', 'id', 'initialize', 'mcp', 'mcphase', 'mcr', 'mcrx', 'mcry', 'mcrz', 'mcswap', 'mcsx', 'mcu', 'mcu1', 'mcu2', 'mcu3', 'mcx', 'mcx_gray', 'mcy', 'mcz', 'multiplexer', 'p', 'pauli', 'r', 'roerror', 'rx', 'rxx', 'ry', 'ryy', 'rz', 'rzx', 'rzz', 's', 'sdg', 'swap', 'sx', 'sxdg', 't', 'tdg', 'u', 'u1', 'u2', 'u3', 'unitary', 'x', 'y', 'z', 'kraus', 'roerror', 'save_amplitudes', 'save_amplitudes_sq', 'save_density_matrix', 'save_expval', 'save_expval_var', 'save_matrix_product_state', 'save_probabilities', 'save_probabilities_dict', 'save_stabilizer', 'save_state', 'save_statevector', 'save_statevector_dict', 'save_superop', 'save_unitary', 'set_density_matrix', 'set_matrix_product_state', 'set_stabilizer', 'set_statevector', 'set_superop', 'set_unitary', 'snapshot', 'superop']. Instruction empty not found in equivalence library and no rule found to expand."
```

### What should happen?

The gate should have a definition which is an empty circuit instead of being just `None`.

### Any suggestions?

_No response_


Discussion:
This seems pretty fixable without a QPY format bump.  It's our old friend `bool` causing problems here: QPY decides if it needs to output a definition based on `bool(instruction.definition)`, but `bool(empty)` is `False` because `QuantumCircuit` defines `__len__` as a pass through to the number of operations, and `bool` essentially tries `len(x) != 0`.

Pretty sure I can fix this no problems, and it'll be stable for backport.

Resolving patch:
diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py
--- a/qiskit/circuit/qpy_serialization.py
+++ b/qiskit/circuit/qpy_serialization.py
@@ -1002,7 +1002,7 @@ def _parse_custom_instruction(custom_instructions, gate_name, params):
     (type_str, num_qubits, num_clbits, definition) = custom_instructions[gate_name]
     if type_str == "i":
         inst_obj = Instruction(gate_name, num_qubits, num_clbits, params)
-        if definition:
+        if definition is not None:
             inst_obj.definition = definition
     elif type_str == "g":
         inst_obj = Gate(gate_name, num_qubits, params)
@@ -1338,7 +1338,7 @@ def _write_custom_instruction(file_obj, name, instruction):
     data = None
     num_qubits = instruction.num_qubits
     num_clbits = instruction.num_clbits
-    if instruction.definition or type_str == b"p":
+    if instruction.definition is not None or type_str == b"p":
         has_definition = True
         definition_buffer = io.BytesIO()
         if type_str == b"p":


Test patch:
diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py
--- a/test/python/circuit/test_circuit_load_from_qpy.py
+++ b/test/python/circuit/test_circuit_load_from_qpy.py
@@ -451,6 +451,50 @@ def test_custom_instruction_with_label(self):
         self.assertEqual(qc.decompose(), new_circ.decompose())
         self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data])
 
+    def test_custom_gate_with_noop_definition(self):
+        """Test that a custom gate whose definition contains no elements is serialized with a
+        proper definition.
+
+        Regression test of gh-7429."""
+        empty = QuantumCircuit(1, name="empty").to_gate()
+        opaque = Gate("opaque", 1, [])
+        qc = QuantumCircuit(2)
+        qc.append(empty, [0], [])
+        qc.append(opaque, [1], [])
+
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circ = load(qpy_file)[0]
+
+        self.assertEqual(qc, new_circ)
+        self.assertEqual(qc.decompose(), new_circ.decompose())
+        self.assertEqual(len(new_circ), 2)
+        self.assertIsInstance(new_circ.data[0][0].definition, QuantumCircuit)
+        self.assertIs(new_circ.data[1][0].definition, None)
+
+    def test_custom_instruction_with_noop_definition(self):
+        """Test that a custom instruction whose definition contains no elements is serialized with a
+        proper definition.
+
+        Regression test of gh-7429."""
+        empty = QuantumCircuit(1, name="empty").to_instruction()
+        opaque = Instruction("opaque", 1, 0, [])
+        qc = QuantumCircuit(2)
+        qc.append(empty, [0], [])
+        qc.append(opaque, [1], [])
+
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circ = load(qpy_file)[0]
+
+        self.assertEqual(qc, new_circ)
+        self.assertEqual(qc.decompose(), new_circ.decompose())
+        self.assertEqual(len(new_circ), 2)
+        self.assertIsInstance(new_circ.data[0][0].definition, QuantumCircuit)
+        self.assertIs(new_circ.data[1][0].definition, None)
+
     def test_standard_gate_with_label(self):
         """Test a standard gate with a label."""
         qc = QuantumCircuit(1)