Download tools/dashboard_core/engine.py from Tatopenn/dense-Evolution: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/Tatopenn/dense-Evolution/resolve/main/tools/dashboard_core/engine.py
- Command line
-
hf download hf://Tatopenn/dense-Evolution/tools/dashboard_core/engine.py
-
curl -L -o engine.py https://huggingface.co/Tatopenn/dense-Evolution/resolve/main/tools/dashboard_core/engine.py
11.9 kB
| """ | |
| 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 | |
| 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)] | |
| 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, | |
| ) | |
| 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(), | |
| ) | |