-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Description
Environment
- Qiskit Terra version: 0.25.1
- Python version: 3.9
- Operating system: Mac OS
What is happening?
This issue about the IAE algorithm 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.