Skip to content

Simulator

DenseSVSimulator is the engine every other page's sim.run_circuit_jit(...) call runs against — a dense statevector of length 2**n, held in memory and updated in place as gates apply to it. This is a JAX library: the whole point of DenseSVSimulator is running circuits as one compiled XLA call, not one Python step per gate — every example below runs on that compiled path.

Step 1. Create a simulator

import dense_evolution as de

sim = de.DenseSVSimulator(2)
sim.n, sim.dim, sim.dtype
(2, 4, <class 'numpy.complex128'>)

DenseSVSimulator only needs one thing to start: how many qubits. n is that count, dim is the statevector length 2**n, dtype is complex128 by default (Step 7 has the complex64 alternative). A fresh simulator starts in |00>sim.get_statevector() right now would show a 1 in the first entry and zero everywhere else.

Step 2. Run a real circuit

qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; h q[0]; cx q[0],q[1];'
circuit = de.QASMParser().parse(qasm)
sim.run_circuit_jit(circuit.to_tuples())
sim.get_statevector().round(4)
array([0.7071+0.j, 0.    +0.j, 0.    +0.j, 0.7071+0.j])

run_circuit_jit takes exactly the tuple list QASMCircuit.to_tuples() produces, compiles the whole circuit into one XLA call, and applies it to sim's statevector in place — sim is now the Bell state this same circuit builds on every other page. get_statevector() reads the result back out as a plain NumPy array.

Step 3. Measurement probabilities

sim.get_probabilities().round(4)
array([0.5, 0. , 0. , 0.5])

get_probabilities() is |amplitude|^2 for every basis state, always real and normalized to sum to 1 — the distribution a real device's repeated measurements would approximate, as opposed to get_statevector()'s complex amplitudes, which no device can read out directly.

Step 4. Collapse a qubit with a real measurement

import jax

sim2 = de.DenseSVSimulator(2)
sim2.run_circuit_jit(circuit.to_tuples())
sim2.measure(0, jax_key=jax.random.PRNGKey(0))
1

measure is a real projective measurement, not a peek at get_probabilities(): it samples one outcome from qubit 0's marginal, returns it, and collapses sim2's statevector to match — sim2.get_statevector() now has a single 1 at the |11> entry, since this Bell state's qubits are always measured equal. jax_key makes the random outcome reproducible; omit it for a genuinely random draw each call.

Step 5. Build a circuit one gate at a time

sim3 = de.DenseSVSimulator(2)
sim3.apply_gate_1q(de.GATES['h'], 0)
sim3.apply_cx(0, 1)
sim3.get_statevector().round(4)
array([0.7071+0.j, 0.    +0.j, 0.    +0.j, 0.7071+0.j])

Identical result to Step 2, built without a circuit or a parser at all. apply_gate_1q/ apply_gate_2q take any matrix from GATES/PARAMETRIC_GATES directly; apply_cx, apply_cz, apply_rx, apply_ry, apply_rz are named shortcuts for the most common ones — each one still runs through the active backend (JAX when installed, which is the normal case), just one gate at a time instead of a whole circuit compiled at once. Useful when a circuit is being constructed programmatically rather than written as QASM.

Step 6. Run many parameter values at once

import numpy as np

template = [('ry', 0, None)]
thetas = np.array([[0.0], [1.0], [2.0], [3.0]])
batch_sv = de.DenseSVSimulator(1).run_batch_jit(template, thetas)
(np.abs(np.asarray(batch_sv)) ** 2).round(4)
array([[1.    , 0.    ],
       [0.7702, 0.2298],
       [0.2919, 0.7081],
       [0.005 , 0.995 ]])

This is the actual reason a JAX-based simulator exists: run_batch_jit runs the same circuit shape once per row of thetas — one column per parametric gate, None standing in for "filled in from the batch" — all jax.vmap'd together as a single compiled call. Row i's output is exactly RY(thetas[i])|0>'s probabilities; a real VQE parameter sweep is this same call with many more rows and a real circuit, still one call, not one Python loop iteration per parameter set.

Step 7. Memory and precision

sim_small = de.DenseSVSimulator(20, use_float32=True)
sim_big = de.DenseSVSimulator(20, use_float32=False)
sim_small.memory_mb(), sim_big.memory_mb()
(8.388608, 16.777216)

use_float32=True stores the statevector as complex64 instead of complex128 — half the memory, shown here on a 20-qubit register, at the cost of numerical precision. The default (complex128) is what every other page on this site uses; switch to complex64 only once memory, not precision, is the bottleneck. For circuits too large for either dtype to fit in memory at all, see Chunk instead.


Details

run_circuit exists, but don't reach for it

run_circuit (plain, no _jit suffix) is an eager, one-gate-at-a-time Python loop — the opposite of what this library is built around. It still ends up calling run_circuit_jit internally on any real install (JAX is a core dependency, not optional, so its own HAS_JAX and all(gate in GATE_IDS ...) check almost always passes), but it pays for that with a redundant transpile-and-scan on every call that run_circuit_jit skips by going straight there. It's kept only for the one case where neither path can be taken — a gate name genuinely outside both GATE_IDS and GATES/PARAMETRIC_GATES — and for backward compatibility with code written before run_circuit_jit existed. Every example on this site calls run_circuit_jit (or run_batch_jit) directly, on purpose.

Qubit ordering is MSB-first

Qubit 0 is the most significant bit of the statevector index, not the least — the opposite of some other simulators' convention. physical_bit_position = n - 1 - qubit is the actual bit position apply_gate_1q/measure/etc. operate on internally; this only matters if you're indexing into get_statevector()'s raw array yourself; every gate/measurement method already takes qubit indices in the qubit-0-is-qubit-0 sense this whole page uses.

Starting from a custom state

set_initial_state(state) resets sim to any complex array of length 2**n instead of |0...0>, normalizing it automatically (set_state is the same method, aliased for the VQE engine). Passing nothing (or explicitly None) resets back to |0...0>.

Chunked execution for large, variable-length circuits

run_circuit_with_chunking(circuit, chunk_size=500) runs a circuit in fixed-size chunks, each a separate run_circuit_jit call — XLA recompiles per distinct circuit length, so a long circuit whose length varies run to run (e.g. a variational loop with early stopping) recompiles far less often when split into same-sized pieces than when run as one whole, differently-sized circuit each time.

run_batch_jit's deprecated alias

run_parametric_batch_jit and run_circuit_jit_beast_mode are deprecated aliases for run_batch_jit/run_circuit_jit, kept for pre-8.1.46 code; both emit a DeprecationWarning and will be removed in a future major version.

An out-of-range qubit index on the compiled path doesn't raise the way you'd expect

apply_gate_1q/apply_gate_2q/measure all validate their qubit index directly and raise ValueError immediately for one out of [0, n). The compiled path (run_circuit_jit, run_batch_jit) encodes qubit indices as bit-shift amounts inside jax.lax.scan/switch instead, which does not raise on an out-of-range index — it silently corrupts the whole statevector to all-zero. _check_qubit_range exists specifically to catch this before it happens, on every code path that reaches the compiled kernels.

measure's bug history

The original NumPy branch zeroed the wrong basis-state slot on collapse (result=0 zeroed slot 1's amplitudes and vice versa) and never normalized the JAX branch at all — both fixed. A second, separate bug affected only the JAX branch: it computed the moveaxis target from n - 1 - qubit_idx (correct for the NumPy branch's raw stride arithmetic, which is a genuinely different indexing scheme) instead of qubit_idx directly (what the JAX branch's reshape-based indexing actually needs, matching apply_gate_1q's own convention) — silently measuring the wrong qubit's marginal whenever qubit_idx != n - 1 - qubit_idx.

statevector

DenseSVSimulator

DenseSVSimulator(n_qubits: int, use_float32: bool = False)

Dense statevector quantum circuit simulator.

Qubit ordering: MSB-first (qubit 0 is the most significant bit). Backends: NumPy (CPU), JAX XLA JIT (CPU/GPU/TPU) -- GPU dispatch is automatic whenever a CUDA-enabled jaxlib is installed and a GPU is present (jax.devices() reports it); no flag on this class selects it, JAX's own default-device placement does.

Parameters:

Name Type Description Default
n_qubits int
required
use_float32 bool
False
Source code in dense_evolution/backends/statevector.py
def __init__(self, n_qubits: int,
             use_float32: bool = False):
    if n_qubits < 1 or n_qubits > 34:
        raise ValueError(f"n_qubits must be in [1, 34], got {n_qubits}")
    if HAS_JAX and not use_float32:
        ensure_x64()
    self.n         = n_qubits
    self.dim       = 1 << n_qubits            # 2 ** n_qubits
    self.use_float32 = use_float32
    self.dtype     = np.complex64 if use_float32 else np.complex128
    self.xp        = jnp if HAS_JAX else np
    self._reset_sv()

set_initial_state

set_initial_state(state: Optional[ndarray] = None)

Reset the simulator.

Parameters:

Name Type Description Default
state optional complex array of length 2**n.
If None, resets to |0...0⟩.
The array is normalised automatically.
None
Source code in dense_evolution/backends/statevector.py
def set_initial_state(self, state: Optional[np.ndarray] = None):
    """
    Reset the simulator.

    Parameters
    ----------
    state : optional complex array of length 2**n.
            If None, resets to |0...0⟩.
            The array is normalised automatically.
    """
    if state is None:
        self._reset_sv()
        return
    state = np.asarray(state, dtype=self.dtype)
    if state.shape != (self.dim,):
        raise ValueError(
            f"State vector length {len(state)} != 2**{self.n} = {self.dim}")
    norm = np.linalg.norm(state)
    if norm < 1e-12:
        raise ValueError("Cannot set a zero-norm state vector")
    state = state / norm
    if HAS_JAX:
        self.sv = jnp.array(state)
    else:
        self.sv = state.copy()

apply_gate_1q

apply_gate_1q(gate: ndarray, qubit: int)

Apply a 2×2 unitary to qubit via tensor contraction.

Uses reshape + moveaxis + matmul — fully vectorised, no Python loops, compatible with both NumPy and JAX.

Source code in dense_evolution/backends/statevector.py
def apply_gate_1q(self, gate: np.ndarray, qubit: int):
    """
    Apply a 2×2 unitary to *qubit* via tensor contraction.

    Uses reshape + moveaxis + matmul — fully vectorised,
    no Python loops, compatible with both NumPy and JAX.
    """
    if not 0 <= qubit < self.n:
        raise ValueError(f"Qubit index {qubit} out of range [0, {self.n})")
    gate = self.xp.array(gate, dtype=self.dtype)
    sv_nd      = self.sv.reshape([2] * self.n)
    sv_moved   = self.xp.moveaxis(sv_nd, qubit, -1)          # qubit axis → last
    flat_shape = (self.dim >> 1, 2)
    # matmul: (dim/2, 2) @ (2, 2).T  → (dim/2, 2)
    result     = self.xp.dot(sv_moved.reshape(flat_shape),
                             gate.T)
    self.sv    = self.xp.moveaxis(
        result.reshape([2] * self.n), -1, qubit).ravel()

apply_gate_2q

apply_gate_2q(gate: ndarray, q1: int, q2: int)

Apply a 4×4 unitary to qubits (q1, q2) via tensor contraction.

Source code in dense_evolution/backends/statevector.py
def apply_gate_2q(self, gate: np.ndarray, q1: int, q2: int):
    """
    Apply a 4×4 unitary to qubits (q1, q2) via tensor contraction.
    """
    if q1 == q2:
        raise ValueError("Control and target qubits must differ")
    if not (0 <= q1 < self.n and 0 <= q2 < self.n):
        raise ValueError(f"Qubit indices ({q1},{q2}) out of range [0, {self.n})")
    gate = self.xp.array(gate, dtype=self.dtype)
    sv_nd      = self.sv.reshape([2] * self.n)
    sv_moved   = self.xp.moveaxis(sv_nd, (q1, q2), (-2, -1))
    flat_shape = (self.dim >> 2, 4)
    result     = self.xp.dot(sv_moved.reshape(flat_shape),
                             gate.reshape(4, 4).T)
    self.sv    = self.xp.moveaxis(
        result.reshape([2] * self.n), (-2, -1), (q1, q2)).ravel()

apply_cx

apply_cx(ctrl: int, tgt: int)

CX (CNOT) gate.

JAX path: matrix contraction via apply_gate_2q. NumPy path: fully vectorised index swap — no Python loops.

Source code in dense_evolution/backends/statevector.py
def apply_cx(self, ctrl: int, tgt: int):
    """
    CX (CNOT) gate.

    JAX path: matrix contraction via apply_gate_2q.
    NumPy path: fully vectorised index swap — no Python loops.
    """
    if ctrl == tgt:
        raise ValueError("Control and target qubits must differ")
    if not (0 <= ctrl < self.n and 0 <= tgt < self.n):
        raise ValueError(f"Qubit indices ({ctrl},{tgt}) out of range [0, {self.n})")
    if HAS_JAX:
        cx_mat = jnp.array([
            [1, 0, 0, 0],
            [0, 1, 0, 0],
            [0, 0, 0, 1],
            [0, 0, 1, 0],
        ], dtype=self.dtype)
        self.apply_gate_2q(cx_mat, ctrl, tgt)
    else:
        self.sv = _cx_numpy(np.array(self.sv), self.n, ctrl, tgt)

apply_cz

apply_cz(ctrl: int, tgt: int)

CZ gate.

JAX path: matrix contraction via apply_gate_2q. NumPy path: fully vectorised sign flip — no Python loops.

Source code in dense_evolution/backends/statevector.py
def apply_cz(self, ctrl: int, tgt: int):
    """
    CZ gate.

    JAX path: matrix contraction via apply_gate_2q.
    NumPy path: fully vectorised sign flip — no Python loops.
    """
    if ctrl == tgt:
        raise ValueError("Control and target qubits must differ")
    if not (0 <= ctrl < self.n and 0 <= tgt < self.n):
        raise ValueError(f"Qubit indices ({ctrl},{tgt}) out of range [0, {self.n})")
    if HAS_JAX:
        cz_mat = jnp.array([
            [1, 0, 0,  0],
            [0, 1, 0,  0],
            [0, 0, 1,  0],
            [0, 0, 0, -1],
        ], dtype=self.dtype)
        self.apply_gate_2q(cz_mat, ctrl, tgt)
    else:
        self.sv = _cz_numpy(np.array(self.sv), self.n, ctrl, tgt)

apply_rx

apply_rx(qubit: int, theta: float)

Apply a parameterized RX gate using the active backend (NumPy/JAX).

Source code in dense_evolution/backends/statevector.py
def apply_rx(self, qubit: int, theta: float):
    """Apply a parameterized RX gate using the active backend (NumPy/JAX)."""
    cos, sin = self.xp.cos(theta / 2), self.xp.sin(theta / 2)
    mat = self.xp.array([[cos, -1j * sin], [-1j * sin, cos]], dtype=self.dtype)
    self.apply_gate_1q(mat, qubit)

apply_ry

apply_ry(qubit: int, theta: float)

Apply a parameterized RY gate using the active backend (NumPy/JAX).

Source code in dense_evolution/backends/statevector.py
def apply_ry(self, qubit: int, theta: float):
    """Apply a parameterized RY gate using the active backend (NumPy/JAX)."""
    cos, sin = self.xp.cos(theta / 2), self.xp.sin(theta / 2)
    mat = self.xp.array([[cos, -sin], [sin, cos]], dtype=self.dtype)
    self.apply_gate_1q(mat, qubit)

apply_rz

apply_rz(qubit: int, theta: float)

Apply a parameterized RZ gate using the active backend (NumPy/JAX).

Source code in dense_evolution/backends/statevector.py
def apply_rz(self, qubit: int, theta: float):
    """Apply a parameterized RZ gate using the active backend (NumPy/JAX)."""
    exp_neg = self.xp.exp(-1j * theta / 2)
    exp_pos = self.xp.exp(1j * theta / 2)
    mat = self.xp.array([[exp_neg, 0.0], [0.0, exp_pos]], dtype=self.dtype)
    self.apply_gate_1q(mat, qubit)

measure

measure(
    qubit_idx: int, jax_key: Optional[Array] = None
) -> int

Projective measurement on qubit_idx.

Returns 0 or 1 and collapses the statevector. Uses MSB-first physical bit index: phys = n - 1 - qubit_idx.

BUG FIX (original): the original NumPy collapse wrote sv_reshaped[:, 1 if result == 0 else 0, :] = 0.0 which zeroed the wrong basis state (0 when result=1, 1 when result=0) and never normalised the JAX path.

jax_key : optional JAX PRNGKey. When given, the random outcome is drawn via jax.random.choice(jax_key, ...) instead of the global np.random.choice -- explicit, seedable, and independent of NumPy's global RNG state, matching registry.NoiseModel.apply_to_sv's own jax_key convention (see that function's docstring for why explicit keys, not hidden per-instance state, are this codebase's convention for JAX-side reproducibility). Default (None) keeps the original np.random.choice behavior unchanged, on both backends -- this measurement's own state-collapse still does concrete Python branching either way (the result drives which basis-state slot gets zeroed), so passing a key makes the outcome reproducible, not this method jax.jit-traceable.

Source code in dense_evolution/backends/statevector.py
def measure(self, qubit_idx: int, jax_key: Optional["jax.Array"] = None) -> int:
    """
    Projective measurement on *qubit_idx*.

    Returns 0 or 1 and collapses the statevector.
    Uses MSB-first physical bit index: phys = n - 1 - qubit_idx.

    BUG FIX (original): the original NumPy collapse wrote
        sv_reshaped[:, 1 if result == 0 else 0, :] = 0.0
    which zeroed the *wrong* basis state (0 when result=1, 1 when result=0)
    and never normalised the JAX path.

    jax_key : optional JAX PRNGKey. When given, the random outcome is
              drawn via jax.random.choice(jax_key, ...) instead of the
              global `np.random.choice` -- explicit, seedable, and
              independent of NumPy's global RNG state, matching
              registry.NoiseModel.apply_to_sv's own jax_key convention
              (see that function's docstring for why explicit keys, not
              hidden per-instance state, are this codebase's convention
              for JAX-side reproducibility). Default (None) keeps the
              original np.random.choice behavior unchanged, on both
              backends -- this measurement's own state-collapse still
              does concrete Python branching either way (the `result`
              drives which basis-state slot gets zeroed), so passing a
              key makes the *outcome* reproducible, not this method
              jax.jit-traceable.
    """
    if not 0 <= qubit_idx < self.n:
        raise ValueError(
            f"Qubit {qubit_idx} out of range [0, {self.n})")

    # BUG FIX: the JAX branch below reshapes to a [2]*n tensor and
    # moveaxis'd -- the exact same indexing scheme apply_gate_1q
    # uses (`moveaxis(sv_nd, qubit, -1)`, qubit axis == qubit index
    # directly, no conversion). This method's JAX branch was instead
    # using `phys = n-1-qubit_idx` for that moveaxis -- correct for
    # the *NumPy* branch below (genuinely different flat/stride
    # arithmetic on a raveled array), but wrong for the JAX branch's
    # reshape-based indexing, silently reading/collapsing the WRONG
    # qubit's marginal whenever qubit_idx != n-1-qubit_idx. Verified
    # directly: X on qubit 0 of a 2-qubit register, then measure(0),
    # returned 0 instead of 1 before this fix (see tests/unit/test_simulator.py's
    # TestMeasurement class).
    phys   = self.n - 1 - qubit_idx
    stride = 1 << phys

    # ── compute marginal probabilities ──────────────────────────
    if HAS_JAX:
        probs   = jnp.abs(self.sv) ** 2
        sv_nd   = probs.reshape([2] * self.n)
        mv      = jnp.moveaxis(sv_nd, qubit_idx, 0)
        prob_0  = float(jnp.sum(mv[0]))
        prob_1  = float(jnp.sum(mv[1]))
    else:
        sv_res  = self.sv.reshape(-1, 2, stride)
        prob_0  = float(np.sum(np.abs(sv_res[:, 0, :]) ** 2))
        prob_1  = float(np.sum(np.abs(sv_res[:, 1, :]) ** 2))

    total = prob_0 + prob_1
    if total < 1e-12:
        raise RuntimeError("Statevector norm is zero — cannot measure")
    prob_0 /= total
    prob_1 /= total

    if jax_key is not None:
        if not HAS_JAX:
            raise ValueError("measure(jax_key=...) requires JAX to be installed.")
        result = int(jax.random.choice(jax_key, jnp.array([0, 1]), p=jnp.array([prob_0, prob_1])))
    else:
        result = int(np.random.choice([0, 1], p=[prob_0, prob_1]))

    # ── collapse ────────────────────────────────────────────────
    # Zero out the amplitudes corresponding to the *opposite* outcome.
    zero_slot = 1 - result     # if result=0, zero slot 1; if result=1, zero slot 0

    if HAS_JAX:
        sv_nd  = self.sv.reshape([2] * self.n)
        mv     = jnp.moveaxis(sv_nd, qubit_idx, 0)
        mv     = mv.at[zero_slot].set(0.0 + 0j)
        self.sv = jnp.moveaxis(mv, 0, qubit_idx).ravel()
    else:
        sv_res = self.sv.reshape(-1, 2, stride)
        sv_res[:, zero_slot, :] = 0.0
        self.sv = sv_res.ravel()

    self.normalize()
    return result

run_circuit_jit_beast_mode

run_circuit_jit_beast_mode(circuit: List)

Deprecated alias for run_circuit_jit -- kept so code written against any pre-8.1.46 release keeps working. Will be removed in a future major version; switch to run_circuit_jit.

Source code in dense_evolution/backends/statevector.py
def run_circuit_jit_beast_mode(self, circuit: List):
    """Deprecated alias for run_circuit_jit -- kept so code written
    against any pre-8.1.46 release keeps working. Will be removed in
    a future major version; switch to run_circuit_jit."""
    warnings.warn(
        "run_circuit_jit_beast_mode is deprecated, use run_circuit_jit instead "
        "(same behavior, shorter name). This alias will be removed in a future release.",
        DeprecationWarning, stacklevel=2,
    )
    return self.run_circuit_jit(circuit)

run_circuit_with_chunking

run_circuit_with_chunking(
    circuit: List, chunk_size: int = 500
)

Execute a circuit in chunks to avoid JIT recompilation on large variable-length circuits.

Each chunk is a separate _compile_and_run_circuit_jit call with a fixed-size ops array, allowing XLA to cache each size.

Source code in dense_evolution/backends/statevector.py
def run_circuit_with_chunking(self, circuit: List, chunk_size: int = 500):
    """
    Execute a circuit in chunks to avoid JIT recompilation on
    large variable-length circuits.

    Each chunk is a separate _compile_and_run_circuit_jit call
    with a fixed-size ops array, allowing XLA to cache each size.
    """
    target = QuantumTranspiler.transpile(circuit)
    for i in range(0, len(target), chunk_size):
        self.run_circuit_jit(target[i: i + chunk_size])

run_parametric_batch_jit

run_parametric_batch_jit(
    base_circuit: List, parameter_batch: ndarray
) -> jnp.ndarray

Deprecated alias for run_batch_jit -- kept so code written against any pre-8.1.46 release keeps working. Will be removed in a future major version; switch to run_batch_jit.

Source code in dense_evolution/backends/statevector.py
def run_parametric_batch_jit(self, base_circuit: List, parameter_batch: np.ndarray) -> "jnp.ndarray":
    """Deprecated alias for run_batch_jit -- kept so code written
    against any pre-8.1.46 release keeps working. Will be removed in
    a future major version; switch to run_batch_jit."""
    warnings.warn(
        "run_parametric_batch_jit is deprecated, use run_batch_jit instead "
        "(same behavior, shorter name). This alias will be removed in a future release.",
        DeprecationWarning, stacklevel=2,
    )
    return self.run_batch_jit(base_circuit, parameter_batch)

get_probabilities

get_probabilities() -> np.ndarray

Return measurement probability distribution as a NumPy float64 array.

Source code in dense_evolution/backends/statevector.py
def get_probabilities(self) -> np.ndarray:
    """Return measurement probability distribution as a NumPy float64 array."""
    probs = np.array(self.xp.abs(self.sv) ** 2, dtype=np.float64)
    # guard against floating-point leakage outside [0, 1]
    probs = np.clip(probs, 0.0, 1.0)
    total = probs.sum()
    if total > 1e-12:
        probs /= total
    return probs

get_statevector

get_statevector() -> np.ndarray

Return the current statevector as a NumPy complex array.

Source code in dense_evolution/backends/statevector.py
def get_statevector(self) -> np.ndarray:
    """Return the current statevector as a NumPy complex array."""
    return np.array(self.sv, dtype=self.dtype)

memory_mb

memory_mb() -> float

Statevector memory footprint in megabytes.

Source code in dense_evolution/backends/statevector.py
def memory_mb(self) -> float:
    """Statevector memory footprint in megabytes."""
    bytes_per_element = 8 if self.use_float32 else 16   # complex64=8, complex128=16
    return self.dim * bytes_per_element / 1_000_000

See Also

  • Gates — the GATES/PARAMETRIC_GATES/GATE_IDS tables Step 5 and run_circuit_jit's dispatch both read from.
  • QASMParser — turns a QASM string into the tuples run_circuit_jit expects.
  • Chunk — anti-OOM slicing for circuits too large for one dense allocation at any dtype.
  • MPSSimulator — an alternative backend for low-entanglement circuits, at a much larger qubit count than a dense statevector can reach at all.