dense-Evolution / dashboard_core /simulation_runner.py
Tatopenn's picture
Sync v8.1.41: pytest-cov coverage tracking, 84.1% -> 94.4%
9131ae9 verified
Raw
History Blame Contribute Delete
15 kB
"""
Circuit execution: run_simulation() and its internals.
Split out of the former monolithic dashboard_core.py (Phase 1 of the
dashboard refactor) -- pure move, no behavior change.
"""
import time
import numpy as np
import jax
import jax.numpy as jnp
import dense_evolution as de
from .qasm_library import QASM_LIBRARY, _run_on_mps
# Above this many basis states, shots sampling draws from only the
# top-K most probable states instead of the full 2**n_qubits distribution
# (see run_simulation) -- same idea as build_panel_mosaico's dim_vis cap.
SHOTS_SAMPLING_CAP = 4096
def estrai_valore_puro(elemento):
if elemento is None:
return 0
# Checked before the .index/.value attribute-extraction branches below,
# not after: every Python str has an .index() *method* (str.index),
# so hasattr(elemento, 'index') is true for ANY string -- without this
# ordering, a numeric string like "3.5" would hit that branch first,
# fail calling .index() with no arguments, and silently fall through
# to 0 instead of being parsed as a number. Confirmed directly: before
# this fix, estrai_valore_puro("3.5") returned 0, not 3.5 -- the
# str-parsing block below was unreachable dead code for any real
# string input, not a deliberately unused case.
if isinstance(elemento, str):
try:
if '.' in elemento:
return float(elemento)
return int(elemento)
except ValueError:
return elemento
tipo_str = str(type(elemento)).lower()
if "builtin" in tipo_str or "method" in tipo_str or "function" in tipo_str:
try:
return estrai_valore_puro(elemento())
except Exception:
return 0
if callable(elemento):
try:
return estrai_valore_puro(elemento())
except Exception:
pass
if hasattr(elemento, 'index'):
val = getattr(elemento, 'index')
val_tipo = str(type(val)).lower()
if "builtin" in val_tipo or "method" in val_tipo or callable(val):
try: val = val()
except Exception: pass
if val is not elemento:
return estrai_valore_puro(val)
if hasattr(elemento, 'value'):
val = getattr(elemento, 'value')
val_tipo = str(type(val)).lower()
if "builtin" in val_tipo or "method" in val_tipo or callable(val):
try: val = val()
except Exception: pass
if val is not elemento:
return estrai_valore_puro(val)
if isinstance(elemento, (int, float, np.integer, np.floating)):
return elemento
return elemento
def run_simulation(source_mode, circuit_name, qasm_text, noise_model, noise_p, shots, seed, use_float32=True, engine='dense'):
"""Adapted from core_calcolo_quantistico (dash.py:3356, canonical/later definition).
engine: 'dense' (default, DenseSVSimulator/Chunk) or 'mps' (MPSSimulator,
dense_evolution/mps.py -- bond-truncated, exact for low-entanglement
circuits, capped at 24 qubits in this dashboard path; see that module's
docstring for why the Lloyd-Max quantization from the original
prototype was dropped)."""
previous_x64_state = jax.config.jax_enable_x64
jax.config.update('jax_enable_x64', not use_float32)
try:
return _run_simulation_body(source_mode, circuit_name, qasm_text, noise_model, noise_p, shots, seed, use_float32, engine)
finally:
# jax_enable_x64 is a process-wide flag: without restoring it here, a float32 run here
# silently downgrades precision for unrelated code (e.g. the Vector Healing page) that
# runs later in the same process and never sets its own precision.
jax.config.update('jax_enable_x64', previous_x64_state)
def _run_simulation_body(source_mode, circuit_name, qasm_text, noise_model, noise_p, shots, seed, use_float32, engine='dense'):
if source_mode == 'Libreria Built-in' and QASM_LIBRARY:
qasm_string = QASM_LIBRARY[circuit_name]
nome_circuito = circuit_name
else:
qasm_string = qasm_text
nome_circuito = 'Custom Workspace'
parser = de.QASMParser()
parsed_circuit = parser.parse(qasm_string)
comandi_originali = parsed_circuit.ops
try:
n_qubits = int(parsed_circuit.n_qubits)
except Exception:
n_qubits = 0
if n_qubits <= 2 or n_qubits > 34:
max_qubit_idx = -1
for cmd in comandi_originali:
if isinstance(cmd, dict) and 'qubits' in cmd:
q_list = cmd['qubits']
if isinstance(q_list, (list, tuple, np.ndarray)):
for q_item in q_list:
val_puro = estrai_valore_puro(q_item)
try:
idx_check = int(val_puro)
if idx_check > max_qubit_idx and idx_check < 40:
max_qubit_idx = idx_check
except Exception:
pass
else:
try:
idx_check = int(estrai_valore_puro(q_list))
if idx_check > max_qubit_idx and idx_check < 40:
max_qubit_idx = idx_check
except Exception:
pass
n_qubits = max_qubit_idx + 1 if max_qubit_idx != -1 else 4
for token in nome_circuito.replace('_', ' ').split():
if 'q' in token.lower() and token.lower().replace('q', '').isdigit():
n_qubits = int(token.lower().replace('q', ''))
elif 'd' in token.lower() and token.lower().replace('d', '').isdigit():
n_qubits = int(token.lower().replace('d', ''))
# Direct DenseSVSimulator allocates 2**n_qubits complex elements
# immediately (17 GB at 30 qubits) -- for circuits that don't fit
# safely in RAM, de.Chunk is used instead: it collapses to a single
# inner DenseSVSimulator when the circuit *does* fit (no behavior
# change, verified against the pre-existing test suite), and only
# actually splits into multiple chunks when the direct allocation
# would risk an OOM crash. Decided by de.Chunk's own dynamic,
# available-RAM-based sizing (dense_evolution.chunk.get_dynamic_chunk),
# not a hardcoded qubit-count constant, so it adapts to the machine
# it's running on rather than an arbitrary limit.
usa_mps = engine == 'mps'
if usa_mps and n_qubits > 24:
raise ValueError(
f"Motore MPS: {n_qubits} qubit superano il limite di 24 qubit per la "
"modalita' dashboard (oltre serve il campionamento sequenziale di "
"dense_evolution.mps.MPSSimulator.get_probabilities_sampled, non ancora "
"collegato ai pannelli esistenti -- questi si aspettano un array denso "
"di probabilita'). Usa il motore denso per circuiti piu' grandi, oppure "
"MPSSimulator direttamente via script per centinaia di qubit a bassa "
"entanglement."
)
usa_chunk = (not usa_mps) and de.chunk.MemoryChunker(n_qubits).num_chunks > 1
if usa_mps:
# Placeholder DenseSVSimulator: its .sv gets overwritten below with the
# MPS-computed statevector. Keeping everything downstream (noise
# injection, get_probabilities, memory_mb, VQE's res['sim'] usage) on
# the same DenseSVSimulator interface regardless of which engine
# produced the state -- MPS is a different way to COMPUTE the same
# statevector, not a different downstream data shape, at this <=24
# qubit scope.
sim = de.DenseSVSimulator(n_qubits=n_qubits, use_gpu=False, use_float32=use_float32)
elif usa_chunk:
sim = de.Chunk(n_qubits=n_qubits, use_gpu=False, use_float32=use_float32)
else:
sim = de.DenseSVSimulator(n_qubits=n_qubits, use_gpu=False, use_float32=use_float32)
comandi_beast_mode = []
for cmd in comandi_originali:
if not isinstance(cmd, dict) or 'name' not in cmd:
continue
nome_porta = str(cmd['name']).lower().strip()
qubits_grezzi = cmd.get('qubits', [])
params_grezzi = cmd.get('params', [])
if nome_porta in ['h', 'x', 'y', 'z', 's', 'sdg', 't', 'tdg']:
try:
target = int(estrai_valore_puro(qubits_grezzi[0]))
if target < n_qubits:
comandi_beast_mode.append([nome_porta, target, -1])
except Exception: pass
elif nome_porta in ['rx', 'ry', 'rz', 'u1', 'u2', 'u3', 'p']:
try:
param = float(estrai_valore_puro(params_grezzi[0]))
target = int(estrai_valore_puro(qubits_grezzi[0]))
if target < n_qubits:
comandi_beast_mode.append([nome_porta, target, param])
except Exception: pass
elif nome_porta in ['cx', 'cy', 'cz', 'swap']:
try:
control = int(estrai_valore_puro(qubits_grezzi[0]))
target = int(estrai_valore_puro(qubits_grezzi[1]))
if control < n_qubits and target < n_qubits:
# compiler.py's documented tuple contract is (gate, control, target)
# for 2-qubit gates — this used to be reversed (see audit finding #1).
comandi_beast_mode.append([nome_porta, control, target])
except Exception: pass
elif nome_porta in ['ccx', 'toffoli']:
try:
c1 = int(estrai_valore_puro(qubits_grezzi[0]))
c2 = int(estrai_valore_puro(qubits_grezzi[1]))
t = int(estrai_valore_puro(qubits_grezzi[2]))
if c1 < n_qubits and c2 < n_qubits and t < n_qubits:
comandi_beast_mode.append([nome_porta, c1, c2, t])
except Exception: pass
elif nome_porta in ['cp', 'crz']:
try:
param = float(estrai_valore_puro(params_grezzi[0]))
control = int(estrai_valore_puro(qubits_grezzi[0]))
target = int(estrai_valore_puro(qubits_grezzi[1]))
if control < n_qubits and target < n_qubits:
comandi_beast_mode.append([nome_porta, control, target, param])
except Exception: pass
start_time = time.perf_counter()
def _run(s, ops):
# de.Chunk exposes run_chunk() instead of run_circuit_jit_beast_mode()
# -- everything else about the two classes is forwarded identically
# (get_probabilities, sv, memory_mb), so this is the only branch point.
if isinstance(s, de.Chunk):
s.run_chunk(ops)
else:
s.run_circuit_jit_beast_mode(ops)
def _mps_statevector(ops):
"""Runs ops on a fresh MPSSimulator (dense_evolution/mps.py) and
returns the contracted statevector as a JAX array matching sim's
dtype -- a new MPSSimulator per call, so this is safe to call twice
(ideal + noisy comparison) without shared mutable state."""
mps_sim = _run_on_mps(ops, n_qubits)
sv = mps_sim.contract_to_statevector()
return jnp.asarray(sv, dtype=jnp.complex64 if use_float32 else jnp.complex128)
if noise_model == 'ideal':
if usa_mps:
sim.sv = _mps_statevector(comandi_beast_mode)
else:
_run(sim, comandi_beast_mode)
prob_ideal = sim.get_probabilities()
prob_noisy = prob_ideal
else:
np.random.seed(seed)
if usa_mps:
sim_ideal = de.DenseSVSimulator(n_qubits=n_qubits, use_float32=use_float32)
sim_ideal.sv = _mps_statevector(comandi_beast_mode)
elif usa_chunk:
sim_ideal = de.Chunk(n_qubits=n_qubits, use_float32=use_float32)
_run(sim_ideal, comandi_beast_mode)
else:
sim_ideal = de.DenseSVSimulator(n_qubits=n_qubits, use_float32=use_float32)
_run(sim_ideal, comandi_beast_mode)
prob_ideal = sim_ideal.get_probabilities()
if usa_mps:
sim.sv = _mps_statevector(comandi_beast_mode)
else:
_run(sim, comandi_beast_mode)
if noise_p > 0:
# `rng=` matters here even though sim.sv is a JAX array: without
# it, NoiseModel.apply_to_sv falls back to OS entropy for the
# noise channel's randomness (see dense_evolution/registry.py's
# rng/jax_key docs), so two runs with the same `seed` produced
# different noisy results despite the seed parameter implying
# otherwise -- found via a flaky test that called this with a
# fixed seed and expected (and got, only sometimes) the same
# fidelity degradation curve.
sim.sv = de.NoiseModel.apply_to_sv(
sv=sim.sv, n=n_qubits, model=noise_model, p=float(noise_p),
rng=np.random.default_rng(seed),
)
prob_noisy = sim.get_probabilities()
t_elapsed = time.perf_counter() - start_time
if use_float32:
prob = np.array(prob_noisy, dtype=np.float32)
prob_id = np.array(prob_ideal, dtype=np.float32)
else:
prob = np.array(prob_noisy, dtype=np.float64)
prob_id = np.array(prob_ideal, dtype=np.float64)
shannon_entropy = -np.sum(prob * np.log2(prob + 1e-10))
idx_max = np.argmax(prob)
stato_dominante = format(idx_max, '0' + str(n_qubits) + 'b')
fidelity_value = float(np.sum(np.sqrt(prob * prob_id)))
noise_factor_curve = np.array([fidelity_value * (1.0 - (i * float(noise_p) / 20.0)) for i in range(100)])
noise_factor_curve = np.clip(noise_factor_curve, 0.0, 1.0)
# np.random.choice(len(prob), p=prob, ...) builds an internal cumulative
# array over the full 2**n_qubits distribution -- another O(2**n) cost
# independent of the Chunk/DenseSVSimulator split above. For huge
# qubit counts, sample from only the top-K most probable states
# (same truncation idea already used by build_panel_mosaico's
# dim_vis = min(1024, ...)) instead of the full distribution.
if len(prob) > SHOTS_SAMPLING_CAP:
top_idx = np.argpartition(prob, -SHOTS_SAMPLING_CAP)[-SHOTS_SAMPLING_CAP:]
top_prob = prob[top_idx]
top_prob = top_prob / top_prob.sum()
shots_data = top_idx[np.random.choice(len(top_idx), p=top_prob, size=shots)]
else:
shots_data = np.random.choice(len(prob), p=prob, size=shots)
return {
'prob': prob,
'prob_ideal': prob_ideal,
'noise_factor': noise_factor_curve,
'fidelity': fidelity_value,
'n_qubits': n_qubits,
'entropy': shannon_entropy,
'idx_max': idx_max,
'stato_dominante': stato_dominante,
'tempo': t_elapsed,
'ram': sim.memory_mb(),
'nome': nome_circuito,
'porte_count': len(comandi_beast_mode),
'shots_data': shots_data,
'sim': sim,
'parser': parser,
}