-
Notifications
You must be signed in to change notification settings - Fork 171
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.
| 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 |
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)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)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 |
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.
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 schedulingUse 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)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)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/.bcfile, 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
NoiseConfigto a QIR module without going throughNeutralAtomDevice.
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)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.
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()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()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()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
DensityMatrixSimulatorcan 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)Q# Wiki
Overview
Q# language & features
- Q# Structs
- Q# External Dependencies (Libraries)
- Differences from the previous QDK
- V1.3 features
- Curated list of Q# libraries
- Advanced Topics and Configuration
- QDK Profile Selection
OpenQASM support
VS Code
Python
- Invoking Q# callables from Python
- Working with Jupyter Notebooks
- Qiskit Interop
- Windows on ARM64
- QDK Python Simulators
Circuit diagrams
Azure Quantum
For contributors