Common Migration Paths

Not every migration is equally common. These are the five transitions practitioners encounter most often, with the main reasons behind each.


Side-by-Side Code Examples

Each operation below is shown in Qiskit, Cirq, and PennyLane. All examples are runnable Python using standard library versions as of 2026.

1. Create a Bell State

A Bell state requires a Hadamard on qubit 0 followed by a CNOT. The structural difference here is that Cirq uses qubit objects while Qiskit and PennyLane use integer indices or wire labels.

Qiskit

from qiskit import QuantumCircuit

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

Cirq

import cirq

q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
    cirq.H(q0),
    cirq.CNOT(q0, q1),
    cirq.measure(q0, q1, key='result')
)

PennyLane

import pennylane as qml

dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev)
def bell_state():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.probs(wires=[0, 1])

2. Parameterized Rotation Gate (Rx)

Parameterized circuits are handled differently in each framework. Qiskit uses a Parameter symbol bound at execution time. Cirq uses sympy symbols resolved via ParamResolver. PennyLane accepts a plain Python float directly, and autograd tracks it automatically.

Qiskit

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

theta = Parameter('theta')
qc = QuantumCircuit(1)
qc.rx(theta, 0)

# Bind the parameter before running
bound_qc = qc.assign_parameters({theta: 1.0472})
print(bound_qc)

Cirq

import cirq
import sympy

theta = sympy.Symbol('theta')
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.rx(theta)(q0))

# Resolve the symbol before simulation
resolver = cirq.ParamResolver({'theta': 1.0472})
resolved = cirq.resolve_parameters(circuit, resolver)
print(resolved)

PennyLane

import pennylane as qml
from pennylane import numpy as np

dev = qml.device('default.qubit', wires=1)

@qml.qnode(dev)
def rx_circuit(theta):
    qml.RX(theta, wires=0)
    return qml.expval(qml.PauliZ(0))

# Plain float works; autograd tracks it
theta_val = np.array(1.0472, requires_grad=True)
print(rx_circuit(theta_val))
print(qml.grad(rx_circuit)(theta_val))

3. Run on Simulator and Get Counts

Each framework has its own simulator API. Qiskit uses AerSimulator from qiskit-aer. Cirq's built-in Simulator uses repetitions. PennyLane returns exact probability distributions by default; use shots and qml.counts to get a sample-based histogram.

Qiskit

from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

sim = AerSimulator()
compiled = transpile(qc, sim)
result = sim.run(compiled, shots=1024).result()
print(result.get_counts())

Cirq

import cirq

q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
    cirq.H(q0),
    cirq.CNOT(q0, q1),
    cirq.measure(q0, q1, key='result')
)

sim = cirq.Simulator()
result = sim.run(circuit, repetitions=1024)
print(result.histogram(key='result'))

PennyLane

import pennylane as qml

dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev, shots=1024)
def bell_counts():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.counts(wires=[0, 1])

print(bell_counts())

4. Statevector Simulation

Getting the full statevector is useful for debugging and small-circuit analysis. Qiskit provides a Statevector class in qiskit.quantum_info. Cirq's simulate() returns a result object with final_state_vector. PennyLane returns the statevector via qml.state() as a NumPy array.

Qiskit

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)

sv = Statevector(qc)
print(sv.data)
# [0.70710678+0.j, 0.+0.j,
#  0.+0.j, 0.70710678+0.j]
print(sv.probabilities_dict())

Cirq

import cirq

q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
    cirq.H(q0),
    cirq.CNOT(q0, q1)
)

sim = cirq.Simulator()
result = sim.simulate(circuit)
print(result.final_state_vector)
# [0.707+0.j, 0.+0.j,
#  0.+0.j, 0.707+0.j]

PennyLane

import pennylane as qml

dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev)
def get_state():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.state()

sv = get_state()
print(sv)
# [0.707+0.j, 0.+0.j,
#  0.+0.j, 0.707+0.j]

5. Depolarizing Noise Model

Noise models simulate realistic hardware behaviour in software. Qiskit uses NoiseModel from qiskit_aer.noise injected into AerSimulator. Cirq's DensityMatrixSimulator applies noise as channels on the circuit. PennyLane uses the default.mixed device, which works with density matrices natively.

Qiskit

from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit_aer.noise import (
    NoiseModel, depolarizing_error
)

noise_model = NoiseModel()
dep_err = depolarizing_error(0.01, 1)
noise_model.add_all_qubit_quantum_error(
    dep_err, ['h', 'rx', 'ry', 'rz']
)

sim = AerSimulator(noise_model=noise_model)

qc = QuantumCircuit(1, 1)
qc.h(0)
qc.measure(0, 0)

result = sim.run(
    transpile(qc, sim), shots=1024
).result()
print(result.get_counts())

Cirq

import cirq

q0 = cirq.LineQubit(0)

# Insert depolarizing noise after each gate
noisy_circuit = cirq.Circuit(
    cirq.H(q0),
    cirq.depolarize(p=0.01)(q0),
    cirq.measure(q0, key='m')
)

# DensityMatrixSimulator handles mixed states
sim = cirq.DensityMatrixSimulator()
result = sim.run(noisy_circuit, repetitions=1024)
print(result.histogram(key='m'))

PennyLane

import pennylane as qml

# default.mixed supports noise channels
dev = qml.device('default.mixed', wires=1)

@qml.qnode(dev)
def noisy_circuit():
    qml.Hadamard(wires=0)
    # Apply depolarizing channel (p total error)
    qml.DepolarizingChannel(0.01, wires=0)
    return qml.probs(wires=[0])

print(noisy_circuit())
# Slightly perturbed from ideal [0.5, 0.5]

6. VQE / Variational Circuit Optimisation

Variational algorithms minimise a cost function by optimising circuit parameters. Qiskit uses its Estimator primitive with SciPy's minimize. Cirq computes expectation values manually via simulate() and expectation_values_from_state_vector(). PennyLane's GradientDescentOptimizer uses parameter-shift gradients automatically.

Qiskit

import numpy as np
from scipy.optimize import minimize
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer.primitives import Estimator

theta = Parameter('theta')
qc = QuantumCircuit(1)
qc.ry(theta, 0)

# Minimise expectation of Z (ground state = -1)
observable = SparsePauliOp('Z')
estimator = Estimator()

def cost(params):
    job = estimator.run([qc], [observable],
                        parameter_values=[params])
    return job.result().values[0]

result = minimize(cost, x0=[0.1], method='COBYLA')
print('Optimal theta:', result.x)
print('Min energy:', result.fun)

Cirq

import numpy as np
import cirq
from scipy.optimize import minimize

q0 = cirq.LineQubit(0)
sim = cirq.Simulator()

def cirq_cost(params):
    theta = params[0]
    circuit = cirq.Circuit(cirq.ry(theta)(q0))
    result = sim.simulate(circuit)
    sv = result.final_state_vector
    # Expectation of Z: <Z> = |a0|^2 - |a1|^2
    probs = np.abs(sv) ** 2
    return float(probs[0] - probs[1])

result = minimize(cirq_cost, x0=[0.1],
                  method='COBYLA')
print('Optimal theta:', result.x)
print('Min energy:', result.fun)

PennyLane

import pennylane as qml
from pennylane import numpy as np

dev = qml.device('default.qubit', wires=1)

@qml.qnode(dev)
def cost_circuit(theta):
    qml.RY(theta, wires=0)
    return qml.expval(qml.PauliZ(0))

# Parameter-shift gradient is automatic
opt = qml.GradientDescentOptimizer(stepsize=0.4)
theta = np.array(0.1, requires_grad=True)

for step in range(50):
    theta, cost = opt.step_and_cost(
        cost_circuit, theta
    )

print('Optimal theta:', float(theta))
print('Min energy:', float(cost))

Key Conceptual Differences

These are the mental model shifts that trip up developers most often when switching frameworks. Getting these right prevents most migration bugs.

Concept Qiskit Cirq PennyLane
Qubit representation Integer index (0, 1, 2) within a QuantumCircuit Qubit objects: LineQubit(0), GridQubit(0,1) Integer wire labels passed to each gate call
Circuit execution backend.run(transpile(qc, backend)) or Sampler / Estimator primitives cirq.Simulator().simulate(circuit) or .run(circuit, repetitions=N) Calling the decorated @qml.qnode function like a regular Python function
Measurement Explicit qc.measure() writes to classical bits; classical register required cirq.measure(q, key='m'); result accessed by key name Declared in the return statement: qml.probs(), qml.expval(), qml.sample()
Parameterisation ParameterVector or Parameter; bound with assign_parameters() sympy.Symbol; resolved with cirq.ParamResolver Direct Python float or pennylane.numpy array; autograd tracks automatically
Transpilation transpile(qc, backend) maps to device gate set and topology Manual decomposition or cirq-google for Sycamore targets Device plugin handles mapping; user rarely calls transpilation directly
Gradients Via Qiskit Machine Learning; not built into the core runtime Not built-in; manual finite differences or parameter-shift via third-party tools First-class: parameter-shift, adjoint, and backpropagation differentiation built in

tket as a Universal Compiler Layer

tket (from Quantinuum) can sit between any high-level framework and any hardware backend. Instead of rewriting your circuits, you import them into tket, apply optimisation passes, and submit to whichever backend you need. This means you can keep writing Qiskit or Cirq code and still benefit from tket's compiler passes.

Write your circuit in Qiskit, Cirq, or any supported framework

Convert to tket using the appropriate extension (pytket-qiskit, pytket-cirq)

Optimise using tket's SequencePass and pass manager

Submit to any backend via tket's backend plugins

tket (via pytket-qiskit)

from qiskit import QuantumCircuit
from pytket.extensions.qiskit import qiskit_to_tk, AerBackend
from pytket.passes import (
    SequencePass, FullPeepholeOptimise, DecomposeBoxes
)

# 1. Write the circuit in Qiskit as normal
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

# 2. Convert to a tket Circuit
tk_circ = qiskit_to_tk(qc)

# 3. Apply optimisation passes
optimiser = SequencePass([
    DecomposeBoxes(),
    FullPeepholeOptimise(),
])
optimiser.apply(tk_circ)

# 4. Submit to a backend (here: Aer via tket)
backend = AerBackend()
compiled = backend.get_compiled_circuit(tk_circ)
result = backend.run_circuit(compiled, n_shots=1024)
print(result.get_counts())

Install with: pip install pytket pytket-qiskit. For Cirq circuits use pytket-cirq and cirq_to_tk(). Backends exist for IBM, IonQ, Quantinuum, Braket, Rigetti, and more.


Braket and PyQuil Migration Notes