Skip to content

Repository files navigation

TUSQ

A fast and efficient noisy quantum circuit simulator backed by NVIDIA cuQuantum.

Building

You will need CMake 3.20+, CUDA (nvcc), and NVIDIA cuQuantum.

Setup script

./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-smi and skips driver installation either way — installing/upgrading a driver is left to you, since it can require a reboot)
  • sudo access, only used if an apt step (build-essential, cmake, CUDA toolkit) is actually needed
  • Python 3 with pip available

2. Steps

  1. Run ./install.sh. It installs missing pieces without sudo where possible: cuQuantum and CUnit are fetched as tarballs into $HOME/cuquantum and $HOME/cunit, and missing Python packages (numpy, pandas, matplotlib) go in via pip install --user.

  2. Add the CUQUANTUM_DIR/CUNIT_DIR exports 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

Quick build

./install.sh   # one-time setup; see above

cmake -B build
cmake --build build

Checking the install

python3 benchmarks/benchmark.py

Running an IQP circuit (benchmarks/run_iqp.py)

benchmarks/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.py

Output:

  • The transpiled circuit is written to benchmarks/iqp_circuit.qasm.
  • build/tusq is 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.

Adding noise to a circuit

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 probability p a random Pauli (X, Y, or Z) is applied, otherwise identity.
  • GateName::DepolarizingNoise2Qubit — two-qubit depolarizing noise; target_qubit_indices takes both qubits, e.g. {i, (i+1)%nqubits} right after a Cx. Same prob = {p} semantics.
  • GateName::BitFlipNoise — applies a bit-flip (X) with a given probability. prob can be {p} (flip probability p) or the explicit two-element form {1 - p, p} used in create_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.).

Attribution

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}, 
}

About

C++ state vector and tensor network simulator for quantum circuits.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages