01 Common Migration Paths
Not every migration is equally common. These are the five transitions practitioners encounter most often, with the main reasons behind each.
Qiskit Cirq
Moving to Google hardware (Sycamore, Willow) or needing lower-level control over qubit placement and gate timing. Cirq exposes individual qubit objects and moment structure that Qiskit abstracts away. The main cost is rewriting qubit indexing from integers to LineQubit or GridQubit objects.
Qiskit PennyLane
Adding quantum machine learning, hybrid classical-quantum models, or differentiable programming. PennyLane circuits are Python functions decorated with @qml.qnode, which makes gradient computation via parameter-shift rules or autograd feel natural. Gate names are mostly one-to-one: qc.h(0) becomes qml.Hadamard(wires=0).
Qiskit Braket
Deploying on AWS infrastructure or accessing hardware from IonQ, Rigetti, QuEra, and IQM through a single SDK. Braket uses a Circuit class with method chaining. Gate names differ slightly: cx becomes cnot, and parameterized circuits use FreeParameter instead of Qiskit's Parameter.
PyQuil Qiskit
Rigetti's public cloud access has been curtailed and the IBM ecosystem is significantly larger. The main concept change is from Quil Program (list of Quil instructions) to Qiskit QuantumCircuit (a structured circuit object). Classical registers and measurement are handled differently: Qiskit requires explicit ClassicalRegister allocation.
Any framework tket
Adding hardware-agnostic circuit optimisation without abandoning your current framework. tket sits as a compilation layer: import your Qiskit or Cirq circuit, apply optimisation passes, and submit to any supported backend. You do not need to rewrite your circuit construction logic, only add the compilation step.
02 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)) 03 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 |
04 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
SequencePassand 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.
05 Braket and PyQuil Migration Notes
Migrating to Amazon Braket
Braket has its own Circuit class that is structurally similar to Qiskit but with different naming conventions and a fluent chaining API.
QuantumCircuit(n, n)becomesCircuit()(no classical bits declared upfront)qc.cx(0, 1)becomescircuit.cnot(0, 1)qc.rx(theta, 0)becomescircuit.rx(0, theta)(qubit index first, angle second)- Parameterized gates use
FreeParameter('theta')instead of Qiskit'sParameter('theta') - Measurement is implicit; no
.measure()call required for the local simulator - Circuits are run via
device.run(circuit, shots=N).result()
Braket Bell state
from braket.circuits import Circuit
from braket.devices import LocalSimulator
circuit = Circuit().h(0).cnot(0, 1)
device = LocalSimulator()
result = device.run(circuit, shots=1024).result()
print(result.measurement_counts) Migrating from PyQuil
PyQuil uses Quil, an assembly-like instruction set. Migrating to Qiskit or Cirq means moving from a list of instruction objects to a structured circuit abstraction.
- PyQuil's
Program()is a list of gate and measurement instructions; Qiskit'sQuantumCircuitis a graph-based circuit object - Gates are imported individually from
pyquil.gates(e.g.H(0),CNOT(0, 1)); in Qiskit they are methods on the circuit object - Classical registers must be declared explicitly in Qiskit:
QuantumCircuit(2, 2) - PyQuil's
quilccompiler is called viaget_qc(); Qiskit usestranspile(qc, backend) - PyQuil's
WavefunctionSimulatorcorresponds to Qiskit'sStatevectorclass - Parametric gates use
p.declare('angle', 'REAL'); Qiskit usesParameter('theta')
PyQuil Bell state (for comparison)
from pyquil import Program
from pyquil.gates import H, CNOT, MEASURE
p = Program()
ro = p.declare('ro', 'BIT', 2)
p += H(0)
p += CNOT(0, 1)
p += MEASURE(0, ro[0])
p += MEASURE(1, ro[1])
p.wrap_in_numshots_loop(1024)