File size: 14,991 Bytes
d2486ac 9131ae9 d2486ac | 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 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | """
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,
}
|