Repository: Qiskit/qiskit

Issue:
QPY can't serialize `UCRZGate`
### Environment

- **Qiskit Terra version**: 0.19.2
- **Python version**: 3.9.7
- **Operating system**: macOS Monterey 12.2.1


### What is happening?

While developing a [Grover's tutorial](https://github.com/Qiskit/qiskit-ibm-runtime/pull/236) for the new Qiskit Runtime primitives, I found a bug where the grover circuit submitted to runtime provider with sampler program return `TypeError: __init__() takes 2 positional arguments but 5 were given`. After some investigation, I have narrowed down the issue to `UCRZGate` which is part of the [Grover operator](https://qiskit.org/documentation/stubs/qiskit.circuit.library.GroverOperator.html#qiskit.circuit.library.GroverOperator) in the grover circuit.

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [1], in <cell line: 20>()
     18 circuit_json = json.loads(json_str)
     19 qpy_file = io.BytesIO(base64.b64decode(circuit_json["circuits"]))
---> 20 circuit = qpy_serialization.load(qpy_file)[0]

File /usr/local/anaconda3/envs/primitives/lib/python3.9/site-packages/qiskit/circuit/qpy_serialization.py:1776, in load(file_obj)
   1774 circuits = []
   1775 for _ in range(file_header[5]):
-> 1776     circuits.append(_read_circuit(file_obj, qpy_version))
   1777 return circuits

File /usr/local/anaconda3/envs/primitives/lib/python3.9/site-packages/qiskit/circuit/qpy_serialization.py:1912, in _read_circuit(file_obj, version)
   1910 custom_instructions = _read_custom_instructions(file_obj, version, vectors)
   1911 for _instruction in range(num_instructions):
-> 1912     _read_instruction(file_obj, circ, out_registers, custom_instructions, version, vectors)
   1913 for vec_name, (vector, initialized_params) in vectors.items():
   1914     if len(initialized_params) != len(vector):

File /usr/local/anaconda3/envs/primitives/lib/python3.9/site-packages/qiskit/circuit/qpy_serialization.py:1121, in _read_instruction(file_obj, circuit, registers, custom_instructions, version, vectors)
   1119         elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}:
   1120             params = [len(qargs), len(cargs)]
-> 1121         gate = gate_class(*params)
   1122     gate.condition = condition_tuple
   1123 if label_size > 0:

TypeError: __init__() takes 2 positional arguments but 5 were given
```

### How can we reproduce the issue?

```python
import io
import json
import base64
from numpy import pi

from qiskit.circuit import qpy_serialization
from qiskit import QuantumCircuit

qc = QuantumCircuit(3)
qc.ucrz([0,0,0,-pi], [1,2], [0])
qc.measure_all()

buf = io.BytesIO()
qpy_serialization.dump(qc, buf)
json_str = json.dumps({
    'circuits': base64.b64encode(buf.getvalue()).decode('utf8')
})
circuit_json = json.loads(json_str)
qpy_file = io.BytesIO(base64.b64decode(circuit_json["circuits"]))
circuit = qpy_serialization.load(qpy_file)[0]
```
qpy code based on [this answer](https://quantumcomputing.stackexchange.com/questions/21795/is-there-a-quantum-circuit-builder-package-that-allows-you-to-import-export-a-ci) by @mtreinish on StackExchange.
### What should happen?

Circuits containing `UCRZGate` should be able to be serialized by QPY.

### Any suggestions?

_No response_


Discussion:
The problem is the `UCRZGate` is non-standard in it's construction. It looks like it sets the `params` attribute to the angle list (as does anything based on `UCPauliRotGate`. By default qpy deserialization tries to do deserialize a gate as `cls(*params)` to create an instance of the gate in the circuit. However that doesn't work because the ucrz params is the angle list. To fix this we'll probably need to do two things, first special case the deserialization for the `UCPauliRotGate` family of gates here:

https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/qpy/binary_io/circuits.py#L240-L257

so that qpy knows that it needs to handle the creation of them separately as they're not built in the standard way. Secondly it might require updating this function if we need to include extra attributes in the serialization of the gate https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/qpy/binary_io/circuits.py#L382 function to enable creating a new equivalent instance in deserialization (I'm thinking for a generic `UCPauliRotGate` we might need to embed the rotation axis as an extra parameter as that's not stored in the params list)
I see. That sounds like my next project :D Can I assign myself on this issue? I will take a closer look at QPY later. For the purpose of creating the Grover tutorial, I am going to use decomposed circuits below UCRZ level which I have tested working.

Resolving patch:
diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py
--- a/qiskit/qpy/binary_io/circuits.py
+++ b/qiskit/qpy/binary_io/circuits.py
@@ -240,7 +240,7 @@ def _read_instruction(file_obj, circuit, registers, custom_instructions, version
     if gate_name in {"IfElseOp", "WhileLoopOp"}:
         gate = gate_class(condition_tuple, *params)
     else:
-        if gate_name == "Initialize":
+        if gate_name in {"Initialize", "UCRXGate", "UCRYGate", "UCRZGate"}:
             gate = gate_class(params)
         else:
             if gate_name == "Barrier":


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
@@ -941,3 +941,16 @@ def test_empty_tuple_param(self):
         qpy_file.seek(0)
         new_circuit = load(qpy_file)[0]
         self.assertEqual(qc, new_circuit)
+
+    def test_ucr_gates(self):
+        """Test qpy with UCRX, UCRY, and UCRZ gates."""
+        qc = QuantumCircuit(3)
+        qc.ucrz([0, 0, 0, -np.pi], [0, 1], 2)
+        qc.ucry([0, 0, 0, -np.pi], [0, 2], 1)
+        qc.ucrx([0, 0, 0, -np.pi], [2, 1], 0)
+        qc.measure_all()
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circuit = load(qpy_file)[0]
+        self.assertEqual(qc.decompose().decompose(), new_circuit.decompose().decompose())