Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistent statevector extraction after applying SWAP gate during simulation #13583

Open
vili-1 opened this issue Dec 19, 2024 · 2 comments
Open
Labels
bug Something isn't working

Comments

@vili-1
Copy link

vili-1 commented Dec 19, 2024

Environment

  • Qiskit version: 1.3.1
  • Python version: 3.12.6
  • Operating system: macOS, 15.1 (24B83)

What is happening?

Imagine a simple 2-qubit circuit: first, an X gate is applied to qubit 1, followed by a SWAP gate between qubits 1 and 0. Intuitively, after the X gate, the state should become $|10\rangle$ (in little endian). Applying the SWAP gate should then flip the excited state to qubit 0, resulting in $|01\rangle$. However, when extracting the statevector during simulation, the state remains $|10\rangle$, as if the SWAP gate had no effect.

Interestingly, extracting the statevector directly using Statevector(qiskit_qc).data (without running the simulation but the operations are run in a symbolic manner as they are recorded in the circuit) behaves as expected, correctly reflecting the SWAP operation.

How can we reproduce the issue?

from qiskit import QuantumCircuit, transpile
from qiskit_aer import Aer
from qiskit.quantum_info import Statevector
import numpy as np

def state_to_str(statevector):
    """Convert statevector to string in |0> |1> notation."""
    n = len(statevector)
    result = []
    for i in range(n):
        binary_str = format(i, f'0{int(np.log2(n))}b')
        if np.abs(statevector[i]) > 1e-10:  
            result.append(f"{statevector[i]:.2f}|{binary_str}>")

    return " + ".join(result) if result else "0"


def test_swap_gate_step_by_step():

    n_qubits = 2
    qiskit_qc = QuantumCircuit(n_qubits)


    # Create a simulator
    simulator = Aer.get_backend('statevector_simulator')


    # Apply X gate on qubit 1 (sets it to 1)..
    qiskit_qc.x(1)
    print("\n\n\nState after X gate:")
    print("\nDirect Statevector Extraction (Without Simulation):")
    print(Statevector(qiskit_qc).data)  # Displays the raw statevector data
    # Transpile the circuit for simulator
    transpiled_circuit = transpile(qiskit_qc, simulator)
    result = simulator.run(transpiled_circuit).result()  # Simulate
    state = result.get_statevector()  # Get statevector
    print("\nFull complex statevector (Extraction During Simulation):")
    print(state)
    print("\nComputational basis state (Extraction During Simulation):")
    print(state_to_str(np.asarray(state)))  # Print the state in |0> |1> notation

    # Add the SWAP gate..
    qiskit_qc.swap(1, 0)
    print("\n\n\nState after SWAP gate:")
    print("\nDirect Statevector Extraction (Without Simulation):")
    print(Statevector(qiskit_qc).data)  # Displays the raw statevector data
    # Transpile the updated circuit
    transpiled_circuit = transpile(qiskit_qc, simulator)
    result = simulator.run(transpiled_circuit).result()  # Simulate
    state = result.get_statevector()  # Get statevector
    print("\nFull complex statevector (Extraction During Simulation):")
    print(state)
    print("\nComputational basis state (Extraction During Simulation):")
    print(state_to_str(np.asarray(state)))  # Print the state in |0> |1> notation


    # Apply measurements
    qiskit_qc.measure_all()
    print("\n\n\nState after measurement:")
    transpiled_circuit = transpile(qiskit_qc, simulator)
    result = simulator.run(transpiled_circuit).result()  # Simulate
    state = result.get_statevector()  # Get statevector
    print("\nFull complex statevector:")
    print(state)  # Full complex statevector
    print("\nComputational basis state:")
    print(state_to_str(np.asarray(state)))  # Final state after measurement (should collapse based on the probabilities)

    print("\n\n" + str(qiskit_qc))


if __name__ == "__main__":
    test_swap_gate_step_by_step()

..and here's the output:

State after X gate:

Direct Statevector Extraction (Without Simulation):
[0.+0.j 0.+0.j 1.+0.j 0.+0.j]

Full complex statevector (Extraction During Simulation):
Statevector([0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
            dims=(2, 2))

Computational basis state (Extraction During Simulation):
1.00+0.00j|10>



State after SWAP gate:

Direct Statevector Extraction (Without Simulation):
[0.+0.j 1.+0.j 0.+0.j 0.+0.j]

Full complex statevector (Extraction During Simulation):
Statevector([0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
            dims=(2, 2))

Computational basis state (Extraction During Simulation):
1.00+0.00j|10>



State after measurement:

Full complex statevector:
Statevector([0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
            dims=(2, 2))

Computational basis state:
1.00+0.00j|10>


                 ░ ┌─┐   
   q_0: ──────X──░─┤M├───
        ┌───┐ │  ░ └╥┘┌─┐
   q_1: ┤ X ├─X──░──╫─┤M├
        └───┘    ░  ║ └╥┘
meas: 2/════════════╩══╩═
                    0  1 

What should happen?

For straightforward circuits like this, after applying the SWAP gate, the state should correctly update to $|01\rangle$ regardless of how the statevector is extracted (directly or upon simulation).

Any suggestions?

No response

@vili-1 vili-1 added the bug Something isn't working label Dec 19, 2024
@jakelishman
Copy link
Member

jakelishman commented Dec 19, 2024

The trouble here is that a "swap" on virtual qubits need not be realised by a swap operation on physical qubits. When you transpile, we map virtual qubits to physical ones, but that mapping need not (and often cannot) stay the same throughout the entire execution owing to backend constraints - we call that routing. Even though the circuit will start off (in this case) as mapping virtual qubit 0 to physical 0 (etc), it won't end like that.

When you ask for the statevector of a compiled circuit, you'll get the statevector in terms of the physical qubits at the instant the snapshot is taken. If we had to insert swaps for routing to satisfy limited hardware connectivity, you'd see those as having transposed the indices as well, for example. What you're seeing here is the same thing: it's more efficient for both simulation and execution if we don't insert a literal swap gate, but instead just relabel the physical-virtual mapping of qubits from that point on. That's almost certainly what's happened here.

After transpilation, any induced permutation by elided (in this case) or added (routing to satisfy hardware constraints) swaps or other non-entangling multi-q gates is stored in QuantumCircuit.layout.routing_permutation(). That field (and the other helper methods in the TranspileLayout class) are what you use to interpret the resulting Statevector: the answer given here isn't wrong, it's just that you've assumed that the virtual/physical qubit labelling is different to what it is. It's valid to want either the physical qubit form (what we give by default, and how Aer actually calculates, so the cheaper one) and the virtual one (what you were perhaps expecting).

@jakelishman
Copy link
Member

btw, if you do result.get_counts() after the measurements, even after transpilation, you should get the bit string you expect. If you look at the transpiled circuit, it should be that the measurements will have been relabelled and the swap gate will be missing, so qubit 1, which has the X applied, will be measured into clbit 0, even though the non-transpiled circuit has an explicit swap gate and measures qubit 0 into clbit 0, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants