Skip to content

QDK Python Simulators

orpuente-MS edited this page Apr 22, 2026 · 2 revisions

QDK Python Simulators

The QDK exposes several quantum simulators from Python. Each section below describes a simulator, explains when to use it, and provides a minimal runnable snippet.

All samples assume pip install qdk (add [qiskit] where noted). Import paths use the qdk metapackage; the underlying qsharp package works as well.

Summary

Simulator Python API Noise Typical use
SparseSim (default) qsharp.run NoiseConfig Sparse-state simulation of Q# programs
SparseSim (default) openqasm.run NoiseConfig Sparse-state simulation of OpenQASM programs
NeutralAtomDevice NeutralAtomDevice.simulate(..., type=...) NoiseConfig Device-level simulation with Clifford, CPU, or GPU backend
run_qir qdk.simulation.run_qir(qir, type=...) NoiseConfig Simulate a QIR module on a Clifford / CPU / GPU backend
QSharpBackend qdk.qiskit.QSharpBackend None Qiskit circuits on the sparse state simulator
NeutralAtomBackend qdk.qiskit.NeutralAtomBackend NoiseConfig Qiskit circuits on the neutral-atom device simulator
DensityMatrixSimulator (experimental) qdk.simulation.DensityMatrixSimulator Kraus operators Low-level mixed-state simulation for custom channels and decoherence
StateVectorSimulator (experimental) qdk.simulation.StateVectorSimulator Kraus operators Low-level pure-state simulation built from custom Kraus operators

Sparse state simulator (default)

The default backend used by qsharp.eval, qsharp.run, and qdk.openqasm.run. It tracks only non-zero amplitudes, which keeps memory usage low for programs that do not generate a large amount of superposition (for example, programs dominated by Clifford gates or with limited entanglement).

Use it when:

  • You are writing Q# or OpenQASM and want a quick, faithful simulation.
  • You want to inspect the state vector via qsharp.dump_machine().
  • You want to run shots with optional Pauli noise or qubit loss.
from qdk import qsharp
from qsharp._qsharp import NoiseConfig

qsharp.eval("""
operation BellPair() : (Result, Result) {
    use (q1, q2) = (Qubit(), Qubit());
    H(q1);
    CNOT(q1, q2);
    (MResetZ(q1), MResetZ(q2))
}
""")

# Noiseless shots
results = qsharp.run("BellPair()", shots=1000)

# Same simulator with per-gate noise via NoiseConfig
noise = NoiseConfig()
noise.h.set_depolarizing(0.01)
noise.cx.set_depolarizing(0.005)
noise.mresetz.set_bitflip(0.01)
noise.cx.loss = 0.001

results = qsharp.run("BellPair()", shots=1000, noise=noise)

OpenQASM programs

The same simulator runs OpenQASM programs via openqasm.run, with the same noise options.

from qsharp import openqasm
from qsharp._qsharp import NoiseConfig

qasm = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c[0] = measure q[0];
c[1] = measure q[1];
"""

# Noiseless shots
results = openqasm.run(qasm, shots=1000)

# With per-gate noise via NoiseConfig
noise = NoiseConfig()
noise.h.set_depolarizing(0.01)
noise.cx.set_depolarizing(0.005)
noise.mresetz.set_bitflip(0.01)
noise.cx.loss = 0.001

results = openqasm.run(qasm, shots=1000, noise=noise)

Neutral-atom device

qdk.simulation.NeutralAtomDevice models a neutral-atom device, including gate decomposition, scheduling, and atom movement. The type= argument selects the simulation backend:

type= Gate support Scaling When to use
"clifford" Clifford gates only Scales to many qubits Large Clifford-only programs where full state is too costly
"cpu" All gates (incl. T, Rz) Memory grows as $2^n$ Non-Clifford programs that fit in CPU memory
"gpu" All gates (incl. T, Rz) Same, offloaded to GPU Same as CPU but faster on hardware with GPUs

If type is omitted, the device tries GPU first and falls back to CPU.

All three backends accept a NoiseConfig for per-gate Pauli channels, qubit loss, and movement noise.

Clifford backend

Use type="clifford" when your program uses only Clifford gates and measurements and you want to scale beyond what a full state-vector sim can handle.

from qdk import qsharp
from qdk.simulation import NeutralAtomDevice, NoiseConfig

qsharp.init(target_profile=qsharp.TargetProfile.Base)
qsharp.eval("""
operation BellPair() : (Result, Result) {
    use (q1, q2) = (Qubit(), Qubit());
    H(q1);
    CNOT(q1, q2);
    (MResetZ(q1), MResetZ(q2))
}
""")
qir = qsharp.compile("BellPair()")

device = NeutralAtomDevice()

noise = NoiseConfig()
noise.cz.set_depolarizing(0.005)
noise.mresetz.set_bitflip(0.01)
noise.mov.loss = 1e-4

results = device.simulate(qir, shots=1000, type="clifford", noise=noise)
device.show_trace(qir)  # visualize device-level scheduling

CPU full-state backend

Use type="cpu" when your program includes non-Clifford gates (T, arbitrary rotations) and fits comfortably in CPU memory.

from qdk import qsharp
from qdk.simulation import NeutralAtomDevice, NoiseConfig

qsharp.init(target_profile=qsharp.TargetProfile.Base)
qsharp.eval("""
operation Main() : Result {
    use q = Qubit();
    H(q);
    T(q);
    MResetZ(q)
}
""")
qir = qsharp.compile("Main()")

noise = NoiseConfig()
noise.sx.set_depolarizing(0.001)
noise.cz.set_depolarizing(0.01)

device = NeutralAtomDevice()
results = device.simulate(qir, shots=500, type="cpu", noise=noise)

GPU full-state backend

Use type="gpu" when you have a GPU and want faster shots. Omit type= to auto-detect GPU with CPU fallback.

from qdk import qsharp
from qdk.simulation import NeutralAtomDevice, NoiseConfig

qsharp.init(target_profile=qsharp.TargetProfile.Base)
qsharp.eval("""
operation Main() : Result {
    use q = Qubit();
    H(q);
    T(q);
    MResetZ(q)
}
""")
qir = qsharp.compile("Main()")

device = NeutralAtomDevice()

noise = NoiseConfig()
noise.cz.set_depolarizing(0.01)

# Explicit backend selection; omit `type=` to auto-detect GPU with CPU fallback.
results = device.simulate(qir, shots=2000, type="gpu", noise=noise)

run_qir

qdk.simulation.run_qir simulates a QIR module directly. It dispatches to a Clifford, CPU full-state, or GPU full-state backend via the type= argument. When type is omitted, it tries GPU first and falls back to CPU. Unlike NeutralAtomDevice.simulate, it does not apply neutral-atom gate decomposition or scheduling: the QIR is executed as given.

Use it when:

  • You already have a QIR module (from qsharp.compile, a .ll/.bc file, or another frontend) and want to simulate it directly.
  • You want to pick a backend ("clifford", "cpu", "gpu") independent of any device model.
  • You want to apply a NoiseConfig to a QIR module without going through NeutralAtomDevice.
from qdk import qsharp
from qdk.simulation import run_qir, NoiseConfig

qsharp.init(target_profile=qsharp.TargetProfile.Base)
qsharp.eval("""
operation BellPair() : (Result, Result) {
    use (q1, q2) = (Qubit(), Qubit());
    H(q1);
    CNOT(q1, q2);
    (MResetZ(q1), MResetZ(q2))
}
""")
qir = qsharp.compile("BellPair()")

# Noiseless, explicit CPU backend.
results = run_qir(qir, shots=1000, type="cpu")

# With per-gate noise; auto-pick GPU if available, else CPU.
noise = NoiseConfig()
noise.cz.set_depolarizing(0.01)
noisy = run_qir(qir, shots=1000, noise=noise, seed=42)

Qiskit backends

Requires pip install "qdk[qiskit]". Two Qiskit backends are available: QSharpBackend for noiseless simulation and NeutralAtomBackend for noisy simulation on the neutral-atom device pipeline.

QSharpBackend

Runs Qiskit QuantumCircuits on the sparse state simulator. This backend does not support noise parameters.

Use it when:

  • You already have a Qiskit workflow and want QDK's local simulator as a drop-in backend.
  • You do not need noise modeling.
from qiskit import QuantumCircuit
from qdk.qiskit import QSharpBackend

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

backend = QSharpBackend()
counts = backend.run(qc, shots=1024).result().get_counts()

NeutralAtomBackend

Runs Qiskit QuantumCircuits through the neutral-atom device compilation pipeline (gate decomposition, scheduling, and atom movement). Accepts a NoiseConfig for per-gate noise and qubit loss, making it the Qiskit entry point for noisy simulations.

Use it when:

  • You have a Qiskit circuit and want to simulate it with realistic neutral-atom noise.
  • You want to model qubit loss during simulation.
from qiskit import QuantumCircuit
from qdk.qiskit import NeutralAtomBackend
from qdk.simulation import NoiseConfig

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

noise = NoiseConfig()
noise.cz.set_depolarizing(1e-3)
noise.mresetz.set_bitflip(1e-3)
noise.mov.loss = 1e-4

backend = NeutralAtomBackend()
job = backend.run(qc, shots=1000, noise=noise, seed=42)
counts = job.result().get_counts()

DensityMatrixSimulator (experimental)

qdk.simulation.DensityMatrixSimulator is a low-level simulator that applies user-supplied quantum Operations (lists of Kraus operators) and Instruments to a density matrix. It does not run Q# or QIR programs directly.

The density matrix simulator is faster than the StateVectorSimulator because it evolves the full probability space of the system at once. You only need to run the simulation once and then draw all the samples you need from the final density matrix. However, it is more memory-intensive.

A density matrix has 2 ^ (2 * number_of_qubits) complex-number entries. If each complex number is stored as two 64-bit floating-point numbers, the matrix requires 2 ^ (2 * number_of_qubits) * 16 bytes. For example, a 20-qubit density matrix takes roughly 16 TB of memory.

Use it when:

  • You are prototyping or researching custom channels expressed as Kraus operators.
  • You want direct access to get_state() / set_state() between steps.
  • Your qubit count is 13 or fewer (due to memory requirements).
import numpy as np
from qdk.simulation import Operation, Instrument, DensityMatrixSimulator

# Bit-flip channel with probability p.
p = 0.1
I = np.eye(2, dtype=complex)
X = np.array([[0, 1], [1, 0]], dtype=complex)
bit_flip = Operation([np.sqrt(1 - p) * I, np.sqrt(p) * X])

P0 = np.array([[1, 0], [0, 0]], dtype=complex)
P1 = np.array([[0, 0], [0, 1]], dtype=complex)
measure_z = Instrument([Operation([P0]), Operation([P1])])

sim = DensityMatrixSimulator(number_of_qubits=1, seed=0)
sim.apply_operation(bit_flip, [0])
outcome = sim.sample_instrument(measure_z, [0])
rho = sim.get_state().data()

StateVectorSimulator (experimental)

qdk.simulation.StateVectorSimulator is a low-level simulator that applies user-supplied quantum Operations (lists of Kraus operators) and Instruments to a state vector. It does not run Q# or QIR programs directly.

Use it when:

  • You are prototyping or researching custom channels expressed as Kraus operators.
  • You want direct access to get_state() / set_state() between steps.
  • Your qubit count exceeds what the DensityMatrixSimulator can handle (more than ~13 qubits).
import numpy as np
from qdk.simulation import Operation, Instrument, StateVectorSimulator

H = (1 / np.sqrt(2)) * np.array([[1, 1], [1, -1]], dtype=complex)
hadamard = Operation([H])

# Z-basis measurement expressed as a 2-outcome instrument.
P0 = np.array([[1, 0], [0, 0]], dtype=complex)
P1 = np.array([[0, 0], [0, 1]], dtype=complex)
measure_z = Instrument([Operation([P0]), Operation([P1])])

sim = StateVectorSimulator(number_of_qubits=1, seed=42)
sim.apply_operation(hadamard, [0])
outcome = sim.sample_instrument(measure_z, [0])  # 0 or 1
print("Measured:", outcome)

Clone this wiki locally