Repository: Qiskit/qiskit-terra (python)

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_

Pull request description:
Fix QPY deserialization of UCRX, UCRY, and UCRZ gates
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

This commit fixes the deserialization of the UCRX, UCRY, and UCRZ gates.
These gates aren't constructed in the standard way as other gate objects
where the parameters are passed directly as args to the class
constructor. Instead they behave the same as Initialize and the params
are passed as a list to the constructor to make a new gate object. This
commit fixes the handling in deserialization to treat these gates the
same as Initialize so that it can be properly deserialized by
qpy.load().

### Details and comments

Fixes #7847

Patch:
diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py
index a54df2d44..e09515263 100644
--- 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":
diff --git a/releasenotes/notes/ucr-gates-qpy-b8f6fb1e34fae258.yaml b/releasenotes/notes/ucr-gates-qpy-b8f6fb1e34fae258.yaml
new file mode 100644
index 000000000..698c192d5
--- /dev/null
+++ b/releasenotes/notes/ucr-gates-qpy-b8f6fb1e34fae258.yaml
@@ -0,0 +1,18 @@
+---
+fixes:
+  - |
+    Fixed an issue with QPY deserialization via the :func:`.qpy.load` function
+    of the :class:`~.UCRXGate`, :class:`~.UCRYGate`, and :class:`~.UCRZGate`.
+    Previously, a QPY file that contained any of these gates would error
+    when trying to load the file.
+    Fixed `#7847 <https://github.com/Qiskit/qiskit-terra/issues/7847>`__
+issues:
+  - |
+    QPY deserialization with the :func:`.qpy.load` function of a directly
+    instantiated :class:`~.UCPauliRotGate` object in a circuit will fail
+    because the rotation axis argument to the class isn't stored in a standard
+    place. To workaround this you can instead use the subclasses:
+    :class:`~.UCRXGate`, :class:`~.UCRYGate`, or :class:`~.UCRZGate` (based on
+    whether you're using a rotation axis of ``"X"``, ``"Y"``, or ``"Z"``
+    respectively) which embeds the rotation axis in the class constructor and
+    will work correctly in QPY.


Test patch:
diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py
index 68966f8a3..241779447 100644
--- a/test/python/circuit/test_circuit_load_from_qpy.py
+++ b/test/python/circuit/test_circuit_load_from_qpy.py
@@ -941,3 +941,16 @@ class TestLoadFromQPY(QiskitTestCase):
         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())