Repository: Qiskit/qiskit

Issue:
`qpy` fails to serialize certain parametrized instructions 
### Environment

- **Qiskit Terra version**:  0.25.1
- **Python version**: 3.9
- **Operating system**: Mac OS


### What is happening?

This [issue about the IAE algorithm](https://github.com/qiskit-community/qiskit-algorithms/issues/71) showed different results when running on the runtime Sampler vs Aer Sampler. After some investigation, the issue seems to be caused by the serialization of the multi-controlled RY gate that is used to build the state preparation circuit. 

After discussing it with @Cryoris, we had the theory that the parameters were not properly stored, but I am not sure, because the printed before/after circuits look the same (see here circuit for the example presented below)

```

        ┌───┐         ┌────────────┐              ░ ┌─┐      
   q_0: ┤ H ├────■────┤ Ry(1.5635) ├──────■───────░─┤M├──────
        ├───┤    │    └─────┬──────┘┌─────┴─────┐ ░ └╥┘┌─┐   
   q_1: ┤ H ├────■──────────■───────┤ Ry(3.127) ├─░──╫─┤M├───
        ├───┤┌───┴───┐      │       └─────┬─────┘ ░  ║ └╥┘┌─┐
   q_2: ┤ H ├┤ Ry(0) ├──────■─────────────■───────░──╫──╫─┤M├
        └───┘└───────┘                            ░  ║  ║ └╥┘
meas: 3/═════════════════════════════════════════════╩══╩══╩═
                                                     0  1  2 

```

[Edit] After further inspection, the parameters are stored but the circuit instructions are not using them properly.
Before serializing the circuit shown in the example, `circ.data` contains:

```

[CircuitInstruction(operation=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 0),), clbits=()), CircuitInstruction(operation=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 1),), clbits=()), CircuitInstruction(operation=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 2),), clbits=()), CircuitInstruction(operation=Instruction(name='ccry', num_qubits=3, num_clbits=0, params=[0.0]), qubits=(Qubit(QuantumRegister(3, 'q'), 0), Qubit(QuantumRegister(3, 'q'), 1), Qubit(QuantumRegister(3, 'q'), 2)), clbits=()), CircuitInstruction(operation=Instruction(name='ccry', num_qubits=3, num_clbits=0, params=[1.563487]), qubits=(Qubit(QuantumRegister(3, 'q'), 1), Qubit(QuantumRegister(3, 'q'), 2), Qubit(QuantumRegister(3, 'q'), 0)), clbits=()), CircuitInstruction(operation=Instruction(name='ccry', num_qubits=3, num_clbits=0, params=[3.126974]), qubits=(Qubit(QuantumRegister(3, 'q'), 2), Qubit(QuantumRegister(3, 'q'), 0), Qubit(QuantumRegister(3, 'q'), 1)), clbits=()), CircuitInstruction(operation=Instruction(name='barrier', num_qubits=3, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 0), Qubit(QuantumRegister(3, 'q'), 1), Qubit(QuantumRegister(3, 'q'), 2)), clbits=()), CircuitInstruction(operation=Instruction(name='measure', num_qubits=1, num_clbits=1, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 0),), clbits=(Clbit(ClassicalRegister(3, 'meas'), 0),)), CircuitInstruction(operation=Instruction(name='measure', num_qubits=1, num_clbits=1, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 1),), clbits=(Clbit(ClassicalRegister(3, 'meas'), 1),)), CircuitInstruction(operation=Instruction(name='measure', num_qubits=1, num_clbits=1, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 2),), clbits=(Clbit(ClassicalRegister(3, 'meas'), 2),))]
```

Where, before serializing:

```
circ.data[4].operation.definition.draw()
Out[11]: 
                                                    
control_0: ─────────────────■────────────────────■──
                            │                    │  
control_1: ─────────────────■────────────────────■──
           ┌─────────────┐┌─┴─┐┌──────────────┐┌─┴─┐
   target: ┤ Ry(0.78174) ├┤ X ├┤ Ry(-0.78174) ├┤ X ├
           └─────────────┘└───┘└──────────────┘└───┘

circ.data[5].operation.definition.draw()
Out[12]: 
                                                  
control_0: ────────────────■───────────────────■──
                           │                   │  
control_1: ────────────────■───────────────────■──
           ┌────────────┐┌─┴─┐┌─────────────┐┌─┴─┐
   target: ┤ Ry(1.5635) ├┤ X ├┤ Ry(-1.5635) ├┤ X ├
           └────────────┘└───┘└─────────────┘└───┘
```

And after serializing:

```
circ2[0].data[4].operation.definition.draw()
Out[5]: 
                                       
control_0: ───────────■─────────────■──
                      │             │  
control_1: ───────────■─────────────■──
           ┌───────┐┌─┴─┐┌───────┐┌─┴─┐
   target: ┤ Ry(0) ├┤ X ├┤ Ry(0) ├┤ X ├
           └───────┘└───┘└───────┘└───┘
circ2[0].data[5].operation.definition.draw()
Out[6]: 
                                       
control_0: ───────────■─────────────■──
                      │             │  
control_1: ───────────■─────────────■──
           ┌───────┐┌─┴─┐┌───────┐┌─┴─┐
   target: ┤ Ry(0) ├┤ X ├┤ Ry(0) ├┤ X ├
           └───────┘└───┘└───────┘└───┘
```

So it looks like qpy is only reading the initial parameter (`0`) and applying it to all gates.

### How can we reproduce the issue?

Let's say we have a circuit with multi-controlled RY gates (the issue is more evident the larger the amount of gates, but I wanted to keep the example small):

```
from qiskit import qpy
from qiskit import QuantumCircuit
from qiskit.circuit.library import RYGate
from qiskit import transpile

circ = QuantumCircuit(3)

circ.h(circ.qubits)
for i in range(3):
    c2ry = RYGate(i*1.563487).control(2)
    circ.append(c2ry, [i % 3, (i+1) % 3, (i+2) % 3])

# circ = transpile(circ, basis_gates=['sx', 'x', 'rz', 'cx'])
circ.measure_all()
```

Let's now dump and load the circuit again:

```
with open('circuit.qpy', 'wb') as fd:
    qpy.dump(circ, fd)

with open('circuit.qpy', 'rb') as fd:
    circ2 = qpy.load(fd)
```

And run an exact simulation using the reference Sampler:

```
from qiskit.primitives import Sampler as RefSampler

print("Before serialization")
print("--------------------")
ref_sampler = RefSampler(options={"shots": 2000, "seed": 10})
result = ref_sampler.run(circ).result()
print("ref", result.quasi_dists)

print("\nAfter serialization")
print("--------------------")
# primitives cache circuits -> reusing primitive changes the result
ref_sampler = RefSampler(options={"shots": 2000, "seed": 10})
result = ref_sampler.run(circ2).result()
print("ref", result.quasi_dists)
```
(I am not re-using the primitive instance because it caches the circuits).
The results should be identical, and yet, they are not:

```
Before serialization
--------------------
ref [{0: 0.129, 1: 0.1335, 2: 0.1185, 3: 0.127, 4: 0.1245, 5: 0.249, 7: 0.1185}]

After serialization
--------------------
ref [{0: 0.129, 1: 0.1335, 2: 0.1185, 3: 0.127, 4: 0.1245, 5: 0.116, 6: 0.118, 7: 0.1335}]
```

Similarly, if we try the Aer Sampler:
```
from qiskit_aer.primitives import Sampler as AerSampler

print("Before serialization")
print("--------------------")
aer_sampler = AerSampler(run_options={"shots": 2000, "seed": 10})
result = aer_sampler.run(circ).result()
print("aer", result.quasi_dists)

print("\nAfter serialization")
print("--------------------")
# primitives cache circuits -> reusing primitive changes the result
aer_sampler = AerSampler(run_options={"shots": 2000, "seed": 10})
result = aer_sampler.run(circ2).result()
print("aer", result.quasi_dists)
```

```
Before serialization
--------------------
aer [{5: 0.245, 4: 0.124, 1: 0.123, 0: 0.124, 2: 0.1245, 7: 0.142, 3: 0.1175}]

After serialization
--------------------
aer [{5: 0.1335, 6: 0.114, 4: 0.124, 1: 0.123, 0: 0.124, 2: 0.1245, 7: 0.1395, 3: 0.1175}]
```

If we configure the runtime sampler to match the Aer Sampler settings, we get that in both cases the runtime results match the "after serialization" aer results:

```
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options

service = QiskitRuntimeService(channel='ibm_quantum')
backend = service.backend("ibmq_qasm_simulator")
options = Options()
options.execution.shots = 2000
options.optimization_level = 0
options.resilience_level = 0
options.simulator.seed_simulator = 10

print("Before serialization")
print("--------------------")
runtime_sampler = Sampler(backend=backend, options=options)
result = runtime_sampler.run(circ).result()
print("runtime", result.quasi_dists)

print("\nAfter serialization")
print("--------------------")
# primitives cache circuits -> reusing primitive changes the result
runtime_sampler = Sampler(backend=backend, options=options)
result = runtime_sampler.run(circ2).result()
print("runtime", result.quasi_dists)
```

```
Before serialization
--------------------
runtime [{5: 0.1335, 1: 0.123, 2: 0.1245, 6: 0.114, 4: 0.124, 0: 0.124, 3: 0.1175, 7: 0.1395}]

After serialization
--------------------
runtime [{5: 0.1335, 1: 0.123, 2: 0.1245, 6: 0.114, 4: 0.124, 0: 0.124, 3: 0.1175, 7: 0.1395}]
```

Finally, if we transpile the circuit locally (uncommenting the line in the first snippet), all results match the "before serialization" aer/reference ones (which is the expected outcome in any case).

### What should happen?

Serializing the circuit should not change the result of the sampler.

### Any suggestions?

See edit on top.


Discussion:

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
@@ -540,7 +540,6 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map):
         )
         or gate_class_name == "Gate"
         or gate_class_name == "Instruction"
-        or gate_class_name == "ControlledGate"
         or isinstance(instruction.operation, library.BlueprintCircuit)
     ):
         if instruction.operation.name not in custom_operations:
@@ -548,6 +547,11 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map):
             custom_operations_list.append(instruction.operation.name)
         gate_class_name = instruction.operation.name
 
+    elif gate_class_name == "ControlledGate":
+        gate_class_name = instruction.operation.name + "_" + str(uuid.uuid4())
+        custom_operations[gate_class_name] = instruction.operation
+        custom_operations_list.append(gate_class_name)
+
     elif isinstance(instruction.operation, library.PauliEvolutionGate):
         gate_class_name = r"###PauliEvolutionGate_" + str(uuid.uuid4())
         custom_operations[gate_class_name] = instruction.operation


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
@@ -29,6 +29,7 @@
 from qiskit.circuit.gate import Gate
 from qiskit.circuit.library import (
     XGate,
+    RYGate,
     QFT,
     QAOAAnsatz,
     PauliEvolutionGate,
@@ -1310,6 +1311,25 @@ def _define(self) -> None:
         self.assertEqual(qc.decompose(), new_circ.decompose())
         self.assertDeprecatedBitProperties(qc, new_circ)
 
+    def test_multiple_controlled_gates(self):
+        """Test multiple controlled gates with same name but different
+        parameter values.
+
+        Reproduce from: https://github.com/Qiskit/qiskit-terra/issues/10735
+        """
+
+        qc = QuantumCircuit(3)
+        for i in range(3):
+            c2ry = RYGate(i + 1).control(2)
+            qc.append(c2ry, [i % 3, (i + 1) % 3, (i + 2) % 3])
+        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.assertDeprecatedBitProperties(qc, new_circ)
+
     def test_load_with_loose_bits(self):
         """Test that loading from a circuit with loose bits works."""
         qc = QuantumCircuit([Qubit(), Qubit(), Clbit()])