A fast and efficient noisy quantum circuit simulator backed by NVIDIA cuQuantum.
You will need CMake 3.20+, CUDA (nvcc), and NVIDIA cuQuantum.
./install.sh detects what's already on your machine and only installs
what's missing — it will not reinstall a working GPU driver, and it will not
install CUDA/cuQuantum/CUnit if it finds them already in place.
1. Prerequisites
- a Debian/Ubuntu-like system with
apt - an NVIDIA GPU with a driver already installed (the script checks
nvidia-smiand skips driver installation either way — installing/upgrading a driver is left to you, since it can require a reboot) sudoaccess, only used if an apt step (build-essential, cmake, CUDA toolkit) is actually needed- Python 3 with
pipavailable
2. Steps
-
Run
./install.sh. It installs missing pieces without sudo where possible: cuQuantum and CUnit are fetched as tarballs into$HOME/cuquantumand$HOME/cunit, and missing Python packages (numpy,pandas,matplotlib) go in viapip install --user. -
Add the
CUQUANTUM_DIR/CUNIT_DIRexports it prints at the end to your shell rc file (~/.bashrc,~/.zshrc, ...) so they're set for every future shell:export CUQUANTUM_DIR=/path/to/cuquantum # printed by install.sh export CUNIT_DIR=/path/to/cunit # printed by install.sh export LD_LIBRARY_PATH=$CUQUANTUM_DIR/lib:$LD_LIBRARY_PATH
./install.sh # one-time setup; see above
cmake -B build
cmake --build buildpython3 benchmarks/benchmark.pybenchmarks/run_iqp.py:
- loads a circuit from
benchmarks/test.json(Cirq's JSON format) - exports it to OpenQASM 2.0
- transpiles it in Qiskit to the gate set TUSQ's parser supports
(
h, x, sx, y, z, cx, rx, ry, rzz, u2, u3) - runs it on the real TUSQ GPU simulator via
build/tusq - computes
<Z0>(the expectation value of Z on qubit 0) from the returned samples
Requires cirq and qiskit (pip install --user cirq qiskit) in addition to
the base setup above. Run from the repo root:
python3 benchmarks/run_iqp.pyOutput:
- The transpiled circuit is written to
benchmarks/iqp_circuit.qasm. build/tusqis built automatically if it doesn't exist yet.- Everything else — the Cirq circuit, the QASM, the transpiled circuit
diagram, and the final
<Z0>value — is printed to stdout; nothing else is written to disk.
Bitstring format: samples come from tusq --output samples, one
bitstring per line, ordered MSB-first by qubit index — i.e. for an
n-qubit circuit, character 0 of the string is qubit n-1 and the
last character is qubit 0 (see to_bitstring() in src/tusq.cpp). This
is why <Z0> is computed by reading the last character of each sampled
bitstring: +1 if it's '0', -1 if it's '1', averaged over all shots.
Circuits are built gate-by-gate in C++ via QuantumCircuit::add_gate()
(include/circuit.hpp):
void add_gate(GateName g,
std::vector<int32_t> const& target_qubit_indices = {},
std::vector<int32_t> const& control_qubit_indices = {},
std::vector<double> const& params = {},
std::vector<double> const& prob = {1.0f});There's no separate "noise model" applied on top of a circuit — noise is
added as its own gate, inserted individually after whichever gate it should
follow. See create_qaoa_circuit() (include/circuit.hpp:540) for the
pattern: every real gate is immediately followed by a conditional noise gate
on the same qubit(s):
c.add_gate(GateName::H, {i});
if (add_depolarizing) {
c.add_gate(GateName::DepolarizingNoise, {i}, {}, {}, {prob});
}
c.add_gate(GateName::Cx, {(i+1)%nqubits}, {i});
if (add_depolarizing) {
c.add_gate(GateName::DepolarizingNoise2Qubit, {i, (i+1)%nqubits}, {}, {}, {prob});
}Available noise gates (include/noise_model.hpp), all applied via
add_gate(GateName::..., target_qubit_indices, {}, {}, prob):
GateName::DepolarizingNoise— single-qubit depolarizing noise.prob = {p}: with probabilitypa random Pauli (X, Y, or Z) is applied, otherwise identity.GateName::DepolarizingNoise2Qubit— two-qubit depolarizing noise;target_qubit_indicestakes both qubits, e.g.{i, (i+1)%nqubits}right after aCx. Sameprob = {p}semantics.GateName::BitFlipNoise— applies a bit-flip (X) with a given probability.probcan be{p}(flip probabilityp) or the explicit two-element form{1 - p, p}used increate_qaoa_circuit()— both mean the same thing.
To wire this into your own circuit-building function, follow
create_qaoa_circuit()'s pattern: take add_bitflip/add_depolarizing
bool flags and a prob float as parameters, and after each gate you add,
conditionally add the matching noise gate on the same qubit indices.
benchmarks/benchmark_circuits.cpp's --add_bitflip_noise,
--add_depolarizing_noise, and --prob CLI flags drive these same flags
for the built-in circuit types (qft, ghz, qaoa, etc.).
When using TUSQ please cite our paper:
@misc{dangwal2026noisyquantumsimulationusing,
title={Noisy Quantum Simulation Using Tracking, Uncomputation and Sampling},
author={Siddharth Dangwal and Tina Oberoi and Ajay Sailopal and Dhirpal Shah and Frederic T. Chong},
year={2026},
eprint={2508.04880},
archivePrefix={arXiv},
primaryClass={quant-ph},
url={https://arxiv.org/abs/2508.04880},
}