Skip to content

Dashboard Core — Engine

The real simulation engine for the dashboard: runs an actual DenseSVSimulator circuit, not a mocked/placeholder result, and is what Composer's UI and the MCP server's simulation tools both call underneath.

engine

Real simulation engine for the dashboard.

OpenQASM text -> dense_evolution's own QASMParser -> executed on dense_evolution's actual DenseSVSimulator/MPSSimulator (not Qiskit's own simulator, and not Qiskit's QASM parser either) -> statevector, probabilities and shot counts, all reordered into Qiskit's little-endian qubit convention so they line up with the Circuit tab's qubit labels and with qiskit.visualization's functions (which assume that convention).

No Qiskit QuantumCircuit is ever constructed here, not even for the Circuit-diagram panel: qiskit.circuit.QuantumCircuit.init itself segfaults (SIGSEGV, inside qiskit's own compiled extension) on macOS, independent of how the circuit is built or what's done with it afterwards -- see tests/integration/test_interop.py::TestQiskitInterop for the full reproduction story (QuantumCircuit(3) alone, no QASM, no method calls, still crashes there). SimulationResult/LargeScaleMPSResult below carry the plain gate tuples instead, and dashboard_core.circuit_diagram draws the diagram directly from those with plain matplotlib.

No synthetic/placeholder data anywhere here: every quantity returned is computed from a real run of the real engine.

run_circuit_from_qasm

run_circuit_from_qasm(
    qasm_text: str,
    n_shots: int = 1000,
    seed: Optional[int] = None,
    noise_model: str = "ideal",
    noise_p: float = 0.0,
    backend: str = "dense",
) -> SimulationResult

Parse qasm_text and run it on a real dense_evolution engine, returning every quantity the dashboard's tabs need.

noise_model/noise_p: one of dense_evolution.NoiseModel.MODELS ('ideal', 'depolarizing', 'bitflip', 'phaseflip', 'amplitude_damping', 'combined') applied as a real stochastic Kraus channel to the statevector via NoiseModel.apply_to_sv -- not a fabricated decay curve, the actual channel math. 'ideal'/p<=0 skips it entirely (and leaves SimulationResult.fidelity_vs_ideal as None -- comparing a run against itself is not a real quantity). Otherwise fidelity_vs_ideal is the real dense_evolution.statevector_fidelity(||^2) between this run's one noisy trajectory and the same circuit's ideal statevector, both computed in this same call (not re-simulated by the caller).

backend: 'dense' (DenseSVSimulator, the default) or 'mps' (MPSSimulator -- adaptive SVD-truncated matrix product state, scales to far more qubits for low-entanglement circuits; contracted back to a dense statevector afterwards so the same Probabilities/Q-sphere/ Statevector panels work unchanged regardless of which engine ran the circuit). Verified to match 'dense' exactly (atol=1e-6) on every preset in QASM_LIBRARY, and to run a 12-qubit GHZ state in ~2.5s (max_bond=2) where 'dense' would need 2**12 complex amplitudes.

Source code in tools/dashboard_core/engine.py
def run_circuit_from_qasm(
    qasm_text: str,
    n_shots: int = 1000,
    seed: Optional[int] = None,
    noise_model: str = "ideal",
    noise_p: float = 0.0,
    backend: str = "dense",
) -> SimulationResult:
    """Parse `qasm_text` and run it on a real dense_evolution engine,
    returning every quantity the dashboard's tabs need.

    noise_model/noise_p: one of dense_evolution.NoiseModel.MODELS
    ('ideal', 'depolarizing', 'bitflip', 'phaseflip', 'amplitude_damping',
    'combined') applied as a real stochastic Kraus channel to the
    statevector via NoiseModel.apply_to_sv -- not a fabricated decay
    curve, the actual channel math. 'ideal'/p<=0 skips it entirely (and
    leaves SimulationResult.fidelity_vs_ideal as None -- comparing a run
    against itself is not a real quantity). Otherwise fidelity_vs_ideal
    is the real dense_evolution.statevector_fidelity(|<ideal|noisy>|^2)
    between this run's one noisy trajectory and the same circuit's ideal
    statevector, both computed in this same call (not re-simulated by the
    caller).

    backend: 'dense' (DenseSVSimulator, the default) or 'mps'
    (MPSSimulator -- adaptive SVD-truncated matrix product state, scales
    to far more qubits for low-entanglement circuits; contracted back to
    a dense statevector afterwards so the same Probabilities/Q-sphere/
    Statevector panels work unchanged regardless of which engine ran the
    circuit). Verified to match 'dense' exactly (atol=1e-6) on every
    preset in QASM_LIBRARY, and to run a 12-qubit GHZ state in ~2.5s
    (max_bond=2) where 'dense' would need 2**12 complex amplitudes.
    """
    n_qubits, ops = _qasm_to_tuples(qasm_text)
    if n_qubits < 1:
        raise ValueError("circuit must declare at least 1 qubit")
    if backend not in ("dense", "mps"):
        raise ValueError(f"unknown backend {backend!r}, must be 'dense' or 'mps'")

    # Both backends end up holding a real 2**n_qubits complex128 dense
    # statevector (Dense always; MPS after contract_to_statevector() for
    # display) -- check against this machine's *actual* free RAM before
    # allocating it, via the same real anti-OOM guard dense_evolution.chunk
    # already uses (15% safety threshold, psutil-backed), rather than
    # letting a manually-typed large QASM circuit (which bypasses the
    # UI's qubit cap entirely) crash the process.
    required_mb = (2 ** n_qubits) * 16 / 1e6
    de.chunk.SafeMemoryGuard().check_allocation(required_mb, context=f"{n_qubits}-qubit statevector")

    mps_max_bond_used = mps_memory_mb = mps_avg_jsd = None

    if backend == "mps":
        # MPSSimulator.run_circuit_jit does not auto-transpile (unlike
        # DenseSVSimulator.run_circuit) -- swap/ccx must be decomposed
        # first or it raises on 'swap' not being in its native GATE_IDS
        # (verified directly: the QFT preset's trailing swap fails here
        # without this step).
        transpiled = de.QuantumTranspiler.transpile(ops)
        mps = de.MPSSimulator(n_qubits)
        mps.run_circuit_jit(transpiled)
        sv_native = np.asarray(mps.contract_to_statevector())
        mps_max_bond_used = mps.max_bond_used()
        mps_memory_mb = mps.memory_mb()
        mps_avg_jsd = mps.avg_jsd()
    else:
        sim = de.DenseSVSimulator(n_qubits, use_float32=False)
        sim.run_circuit(ops)
        sv_native = np.asarray(sim.sv)

    rng = np.random.default_rng(seed)
    fidelity_vs_ideal = None
    if noise_model != "ideal" and noise_p > 0:
        # apply_to_sv mutates its numpy input in-place, so the pre-noise
        # amplitudes have to be copied out first -- this is one stochastic
        # Kraus-channel trajectory (a real single noisy realization, not a
        # density matrix), so the result is still a valid pure state and
        # dense_evolution.statevector_fidelity (the pure-state counterpart
        # to the density-matrix uhlmann_fidelity already used by the ZNE
        # panels below) is the right comparison against the ideal state.
        sv_ideal = sv_native.copy()
        sv_native = de.NoiseModel.apply_to_sv(sv_native, n_qubits, noise_model, noise_p, rng=rng)
        fidelity_vs_ideal = float(de.statevector_fidelity(sv_ideal, sv_native))

    statevector = _to_qiskit_bit_order(sv_native, n_qubits)
    probabilities = np.abs(statevector) ** 2

    # sample_counts expects/returns dense_evolution's native MSB-first
    # bitstring convention -- reverse each key to relabel into Qiskit's
    # little-endian convention (same physical samples, just relabeled to
    # match the Circuit/Probabilities tabs' qubit numbering).
    counts_native = de.sample_counts(sv_native, n_shots, rng=rng)
    counts = {key[::-1]: n for key, n in counts_native.items()}

    return SimulationResult(
        ops=ops,
        n_qubits=n_qubits,
        statevector=statevector,
        probabilities=probabilities,
        counts=counts,
        noise_model=noise_model,
        noise_p=noise_p,
        fidelity_vs_ideal=fidelity_vs_ideal,
        backend=backend,
        mps_max_bond_used=mps_max_bond_used,
        mps_memory_mb=mps_memory_mb,
        mps_avg_jsd=mps_avg_jsd,
    )

run_large_circuit_mps

run_large_circuit_mps(
    qasm_text: str, k: int = 32, seed: Optional[int] = None
) -> LargeScaleMPSResult

For circuits beyond MPS_DENSE_CONTRACTION_LIMIT qubits, where no dense (2**n,) statevector/probabilities array can exist at all: runs the real MPS circuit and finds the top-k most probable basis states via MPSSimulator.get_top_k_probable_states -- a real greedy beam search, EXACT probabilities for the states it finds (not sampled or approximated). Verified directly against exact dense diagonalization at 10 qubits: every state with non-negligible probability was found, matching to 6 decimal places (recall isn't guaranteed complete for highly-entangled circuits at fixed k -- see MPSSimulator's own docstring -- but the probabilities reported are always exact, never estimated).

Chosen over MPSSimulator.get_probabilities_sampled for this UI: measured directly, sampling 2000 shots takes 130s/257s/582s at 30/50/100 qubits (grows with n -- roughly O(n_samples * n_qubits)), while get_top_k_probable_states stays under 2s even at 100 qubits.

Source code in tools/dashboard_core/engine.py
def run_large_circuit_mps(qasm_text: str, k: int = 32, seed: Optional[int] = None) -> LargeScaleMPSResult:
    """For circuits beyond MPS_DENSE_CONTRACTION_LIMIT qubits, where no
    dense (2**n,) statevector/probabilities array can exist at all: runs
    the real MPS circuit and finds the top-k most probable basis states
    via MPSSimulator.get_top_k_probable_states -- a real greedy beam
    search, EXACT probabilities for the states it finds (not sampled or
    approximated). Verified directly against exact dense diagonalization
    at 10 qubits: every state with non-negligible probability was found,
    matching to 6 decimal places (recall isn't guaranteed complete for
    highly-entangled circuits at fixed k -- see MPSSimulator's own
    docstring -- but the probabilities reported are always exact, never
    estimated).

    Chosen over MPSSimulator.get_probabilities_sampled for this UI:
    measured directly, sampling 2000 shots takes 130s/257s/582s at
    30/50/100 qubits (grows with n -- roughly O(n_samples * n_qubits)),
    while get_top_k_probable_states stays under 2s even at 100 qubits.
    """
    n_qubits, raw_ops = _qasm_to_tuples(qasm_text)
    if n_qubits < 1:
        raise ValueError("circuit must declare at least 1 qubit")

    ops = de.QuantumTranspiler.transpile(raw_ops)
    mps = de.MPSSimulator(n_qubits)
    mps.run_circuit_jit(ops)

    idx, probs = mps.get_top_k_probable_states(k=k)

    top_k_states = []
    for i, p in zip(idx.tolist(), probs.tolist()):
        native_bits = format(int(i), f'0{n_qubits}b')
        qiskit_bits = native_bits[::-1]  # same MSB-first -> little-endian flip as _to_qiskit_bit_order
        top_k_states.append((qiskit_bits, float(p)))
    top_k_states.sort(key=lambda t: t[1], reverse=True)

    return LargeScaleMPSResult(
        ops=raw_ops,
        n_qubits=n_qubits,
        top_k_states=top_k_states,
        k_requested=k,
        mps_max_bond_used=mps.max_bond_used(),
        mps_memory_mb=mps.memory_mb(),
        mps_avg_jsd=mps.avg_jsd(),
    )