File size: 11,946 Bytes
2a87ede
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
"""

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.

"""

from dataclasses import dataclass
from functools import lru_cache
from typing import Optional

import numpy as np

import dense_evolution as de

__all__ = ['SimulationResult', 'run_circuit_from_qasm', 'LargeScaleMPSResult', 'run_large_circuit_mps']


def _qasm_to_tuples(qasm_text: str):
    """Parses qasm_text with dense_evolution's own QASMParser (never

    Qiskit's) and returns (n_qubits, ops) where ops is the same

    (name, *qubits[, param]) tuple format every other dense_evolution

    entry point (run_circuit, QuantumTranspiler.transpile, ...) accepts."""
    parsed = de.QASMParser().parse(qasm_text)
    return parsed.n_qubits, parsed.to_tuples()

# MPSSimulator.contract_to_statevector's own hard ceiling (RAM-independent
# -- it refuses above this regardless of available memory, since a dense
# 2**n array stops being the point of using MPS at all). Circuits at or
# below this go through the normal dense-statevector path (same
# Probabilities/Q-sphere/Statevector panels as the Dense backend); above
# it, run_large_circuit_mps is the only real path.
MPS_DENSE_CONTRACTION_LIMIT = 24


@lru_cache(maxsize=None)
def _qiskit_bit_order_perm(n_qubits: int) -> np.ndarray:
    """The bit-reverse index permutation itself, cached per n_qubits --

    it's O(2**n_qubits) to build and depends only on n_qubits, so a

    circuit run repeatedly at the same qubit count (the common case: a

    fixed molecule/preset re-run with different parameters) was

    rebuilding an identical array every single call. lru_cache keys on

    the (hashable) n_qubits argument only -- the returned array itself

    doesn't need to be hashable."""
    perm = np.array([int(format(i, f'0{n_qubits}b')[::-1], 2) for i in range(2 ** n_qubits)])
    perm.setflags(write=False)  # cached and shared across calls -- never mutate in place
    return perm


def _to_qiskit_bit_order(values: np.ndarray, n_qubits: int) -> np.ndarray:
    """Bit-reverse index order: Dense-Evolution is MSB-first (qubit 0 =

    most significant bit of the index), Qiskit is little-endian (qubit 0

    = least significant bit) -- the same remap dense_evolution.interop

    applies to probabilities internally, used here for the raw complex

    statevector too since it is the identical index permutation."""
    return values[_qiskit_bit_order_perm(n_qubits)]


@dataclass
class SimulationResult:
    ops: list  # dense_evolution gate tuples -- feeds dashboard_core.circuit_diagram for the Circuit tab
    n_qubits: int
    statevector: np.ndarray    # complex128, Qiskit bit order
    probabilities: np.ndarray  # Qiskit bit order
    counts: dict                # Qiskit-style bitstring -> shot count
    noise_model: str = "ideal"
    noise_p: float = 0.0
    fidelity_vs_ideal: Optional[float] = None
    backend: str = "dense"
    mps_max_bond_used: Optional[int] = None
    mps_memory_mb: Optional[float] = None
    mps_avg_jsd: Optional[float] = None


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


@dataclass
class LargeScaleMPSResult:
    ops: list  # dense_evolution gate tuples -- feeds dashboard_core.circuit_diagram for the Circuit tab
    n_qubits: int
    top_k_states: list  # [(bitstring, probability), ...] Qiskit bit order, sorted descending
    k_requested: int
    mps_max_bond_used: int
    mps_memory_mb: float
    mps_avg_jsd: float


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(),
    )