Configuration

This section describes OpenQARP’s configuration system and type definitions, and states what OpenQARP does under an MPI launcher.

  • Global configuration: Centralized settings for seeds, parallelization, and circuit cutting

  • Type safety: Clear type definitions for improved code quality

  • MPI: detection only — see MPI below


Configuration System

OpenQARP provides a global configuration object that controls various settings throughout the package.

The Config Object

The configuration is accessed via the config object imported from the main package:

from qarp import config

This object is a singleton instance of the internal __ConfigObject__ class and provides the following configurable properties:

Random Seed

Control random number generation for reproducibility:

# Set a seed for reproducible results
config.seed = 42

# Clear the seed
config.seed = None

# Get current seed
current_seed = config.seed

Setting the seed affects:

  • Python’s built-in random module

  • NumPy’s random number generator

  • SciPy (which uses NumPy’s RNG internally)

  • NetworkX random functions (which also use NumPy)

Maximum Number of Cuts

For circuit cutting operations, limit the maximum number of cuts:

# Set maximum cuts (1-6, default is 6)
config.max_number_of_cuts = 4

# Get current value
max_cuts = config.max_number_of_cuts

Warning

Setting this value too high can generate an exponential number of circuits (6^n), significantly increasing computational cost.

Implementation Details

The configuration object is defined in qarp/_config.py and provides:

  • Property-based getters and setters with validation

  • Automatic propagation of settings to dependent libraries

  • Runtime validation of configuration values


Type Definitions

OpenQARP defines type aliases for improved code clarity and type checking. These are defined in qarp/_types.py.

SamplingDictionary

A type alias for dictionary structures representing sampling results:

from qarp import SamplingDictionary

# SamplingDictionary is defined as: dict[tuple[int, ...], float]

# Example usage
results: SamplingDictionary = {
    (0, 0, 0): 0.25,
    (0, 0, 1): 0.25,
    (1, 1, 0): 0.25,
    (1, 1, 1): 0.25,
}

The keys are tuples of integers representing measurement outcomes, and values are floats representing probabilities or counts.


Threads

Two parallel layers share one knob. The C++ simulator runs a circuit’s shots as one OpenMP team when the circuit forces per-shot trajectories (mid-circuit measurement, reset, feed-forward, or a noise model); a single-statevector evaluation instead lets the csim kernels parallelise each gate across the amplitudes. The two never nest: inside a shot worker the kernel regions serialise, so a worker never spawns a kernel team of its own (the contention that did cost 1.3 ms per shot on 0.02 ms of work).

QARP_NUM_THREADS sizes both layers and the compilation thread pool. It is read once, at import qarpx:

QARP_NUM_THREADS=4 python my_script.py

Unset, every layer uses the hardware thread count. The kernel layer keeps its own finer overrides (QULACS_NUM_THREADS, QULACS_PARALLEL_NQUBIT_THRESHOLD, see Installation); a QULACS_NUM_THREADS you set yourself wins over the forwarded value. Results are seeded per shot, independent of how the shots are partitioned, so a seeded run is bit-identical at every thread count.

The Pauli expectation kernel (expectation / transition / batch_expectation, what StateVector contracts with) is one OpenMP region per call, capped by both QARP_NUM_THREADS and OMP_NUM_THREADS. Its floating-point reduction order follows the team size, so its results agree across thread counts to rounding (about 1e-14 relative), not bit for bit.

The simulator releases the GIL while it computes (run, batch_run, statevector, unitary_matrix, the expectation kernel and the gradient kernels), so Python threads can drive it concurrently. Below the kernel layer’s parallel threshold (16 qubits, see Gate fusion) two threads overlap almost fully; above it each call already occupies every core, so threading buys little there.

Gate fusion

Every kernel call is a pass over all \(2^n\) amplitudes, so the cost of a statevector run is passes × state size, not gates × state size. Before dispatching, QarpSimulator fuses runs of gates into dense blocks of up to fusion_max_qubits qubits and applies each block in one pass — the optimisation behind qiskit-aer’s fusion_enable. It is exact (global phase included) and applies to statevector, run and batch_run; unitary_matrix, the adjoint gradient and noisy trajectories run gate by gate.

import qarpx as qx

sim = qx.QarpSimulator()
sim.fusion_max_qubits          # 3: the built-in default
sim.fusion_max_qubits = 1      # single-qubit fusion only (the 0.1.0 behaviour)
sim.fusion_max_qubits = 0      # raw per-gate dispatch, for diagnostics
sim.fusion_min_qubits          # 12: narrower registers keep the single-qubit pass

QARP_FUSION_MAX_QUBITS sets the process-wide default for every simulator an engine constructs — read once, at import qarpx; values that do not parse or exceed QarpSimulator.MAX_FUSION_QUBITS are ignored:

QARP_FUSION_MAX_QUBITS=1 python my_script.py   # compare against unfused timings

Wider is not better: a \(k\)-qubit block costs \(2^k\) multiplies per amplitude, so the default is the width measured fastest on this kernel — at 20 qubits, single thread, brickwork 207 → 40 ms, QFT 462 → 190 ms, a Trotter step 230 → 94 ms (macOS arm64, 2026-09-13; see the statevector benchmark table) — and the pass only widens a block past two qubits through a shared qubit. Below fusion_min_qubits the fold costs more than the passes it saves, so small registers keep the single-qubit pass.

The same import also exports QULACS_PARALLEL_NQUBIT_THRESHOLD=16 to the kernel layer unless you set it: csim’s own per-kernel default of 13 forks an OpenMP team where the fork costs more than the gate, which is where a 13-qubit circuit used to run 11× its 12-qubit twin.

MPI

OpenQARP does not implement MPI parallelism. Earlier versions ran every rank through the same serial optimisation in lockstep, which cost a full cluster allocation for a single-process result; that machinery is gone.

What remains is detection. qarp.MPIConfig reads the launcher’s environment variables (OpenMPI, MPICH/PMI/SLURM, Intel MPI, MVAPICH2) without importing mpi4py — so no MPI_Init_thread ever runs inside OpenQARP — and every composite algorithm refuses to construct under a multi-rank launcher:

from qarp import MPIConfig

MPIConfig.is_mpi_env()   # True under mpirun / srun
MPIConfig.world_size()   # ranks reported by the environment (1 when unknown)
MPIConfig.is_disabled()  # QARP_DISABLE_MPI=1 silences detection
$ mpirun -np 4 python my_vqe.py
CapabilityError: VQE: MPI parallelism is not implemented — 4 ranks detected.
Run OpenQARP in a single process, or set QARP_DISABLE_MPI=1 on the one rank that drives it.

Set QARP_DISABLE_MPI=1 when one rank of a larger job drives OpenQARP serially on purpose. Parallelism inside a single process (threads, the C++ batch runners) is unaffected; see Engines.