File size: 9,397 Bytes
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 | """
Molecular-dynamics-style telemetry.
Two modes, chosen automatically by run_md_telemetry() based on what the
caller passes in:
REAL mode -- a valid Hamiltonian (from LIBRERIA_HAMILTONIANE or a custom
one, diagonal, length 2**n_qubits -- see hamiltonians.py)
and the circuit's current statevector are both given, and
their sizes agree. The state is evolved in real time under
that Hamiltonian and a physically-grounded thermal-noise
channel; every column is a real, computed quantity.
MOCK mode -- no compatible Hamiltonian/statevector given. Falls back to
the original synthetic generator (adapted verbatim from
run_md_simulation_dummy, dash.py:1152 -- explicitly a
placeholder in the source since it was first written, never
real MD/quantum-chemistry data).
Both modes return the same (df_md, corr_matrix) shape the rest of the
dashboard already expects. Which mode actually ran is recorded in
df_md.attrs['is_real'] / df_md.attrs['note'] (pandas' own metadata slot --
survives on the DataFrame itself, does not require changing every call
site's return-value unpacking) so the UI can label the result honestly
instead of presenting a mock run as if it were real.
"""
import numpy as np
import pandas as pd
def run_md_telemetry(md_steps, md_temp, hamiltonian_values=None, sv=None, n_qubits=None, seed=None):
is_real, mismatch_note = _check_real_mode_inputs(hamiltonian_values, sv, n_qubits)
if is_real:
df_md, corr_matrix = _run_md_telemetry_real(md_steps, md_temp, hamiltonian_values, sv, int(n_qubits), seed)
df_md.attrs['is_real'] = True
df_md.attrs['note'] = 'Dinamica reale: evoluzione temporale sotto l\'Hamiltoniana selezionata.'
else:
df_md, corr_matrix = _run_md_telemetry_mock(md_steps, md_temp)
df_md.attrs['is_real'] = False
df_md.attrs['note'] = mismatch_note
return df_md, corr_matrix
def _check_real_mode_inputs(hamiltonian_values, sv, n_qubits):
if hamiltonian_values is None or sv is None or n_qubits is None:
return False, 'Dati dimostrativi -- nessuna Hamiltoniana reale attiva.'
expected_dim = 2 ** int(n_qubits)
if len(hamiltonian_values) != expected_dim or len(sv) != expected_dim:
return False, (
f'Dati dimostrativi -- l\'Hamiltoniana selezionata (dim={len(hamiltonian_values)}) '
f'non e\' compatibile con questo circuito (dim richiesta={expected_dim}).'
)
return True, None
def _run_md_telemetry_mock(md_steps, md_temp):
"""Synthetic MD telemetry generator — adapted verbatim from run_md_simulation_dummy
(dash.py:1144), explicitly a placeholder for real MD/quantum-chemistry calculations
in the source itself, not physically simulated data."""
data = {
"Step": [], "Energia_VQE_Ha": [], "Entropia_von_Neumann_Bit": [],
"Purita_Stato": [], "ID_Operatore_ADAPT": [], "Gradiente_Operatore": [],
"Fattore_Rumore_Termico": [], "Correzione_Variazionale_Theta": [], "Gradiente_Base_Classica": []
}
temp_factor = md_temp / 300.0 if md_temp > 0 else 0.1
temp_factor = np.clip(temp_factor, 0.1, 2.0)
for step in range(md_steps):
data["Step"].append(step)
energy = -25.0 * np.exp(-step / (md_steps / 5.0)) * temp_factor + np.random.uniform(-0.5, 0.5)
data["Energia_VQE_Ha"].append(energy)
entropy = 0.5 + 0.5 * (step / md_steps) * temp_factor + np.random.uniform(-0.01, 0.01)
data["Entropia_von_Neumann_Bit"].append(entropy)
purity = 0.8 * np.exp(-step / (md_steps / 10.0)) / temp_factor + np.random.uniform(-0.005, 0.005)
data["Purita_Stato"].append(purity)
data["ID_Operatore_ADAPT"].append(np.random.randint(0, 3))
grad = 1.5 * np.exp(-step / (md_steps / 2.0)) * temp_factor + np.random.uniform(-0.05, 0.05)
data["Gradiente_Operatore"].append(grad)
data["Fattore_Rumore_Termico"].append(1.0 - (step / md_steps * 0.1) * temp_factor + np.random.uniform(-0.001, 0.001))
data["Correzione_Variazionale_Theta"].append(0.1 * np.sin(step * 0.01 * temp_factor) + np.random.uniform(-0.005, 0.005))
data["Gradiente_Base_Classica"].append(grad * 0.8)
df_md = pd.DataFrame(data)
df_md.set_index("Step", inplace=True)
corr_matrix = df_md.corr(method="pearson")
return df_md, corr_matrix
def _run_md_telemetry_real(md_steps, md_temp, hamiltonian_values, sv, n_qubits, seed):
"""Real-time evolution of the actual circuit state under the actual
(diagonal) Hamiltonian selected by the user, plus a physically-grounded
thermal-noise channel reusing dense_evolution's own NoiseModel (not a
reinvented noise formula).
Every LIBRERIA_HAMILTONIANE entry is a diagonal Hamiltonian (a flat list
of 2**n_qubits eigenvalues, see hamiltonians.py) -- so time evolution
under it is *exact*, not a Trotter approximation: each basis-state
amplitude just picks up a phase exp(-i * eigenvalue * dt) per step.
DT is a fixed, unitless step size (these Hamiltonians are unitless
benchmark energies, not calibrated to a real physical time unit -- there
is no "correct" DT to derive, this one is chosen so md_steps=20-500
sweeps a visually reasonable range of phase evolution, nothing more).
"Temperature" drives the probability of dense_evolution's own
NoiseModel.apply_to_sv (depolarizing channel) applied once per step --
real decoherence from real, already-tested code, not a fabricated
thermal-noise curve.
Purity is computed exactly (not approximated) from an ensemble-averaged
density matrix: ENSEMBLE independent noisy realizations of the same
step are drawn, their outer products averaged into rho_avg, and
Purita_Stato = Tr(rho_avg^2). This is exact given rho_avg; the only
approximation is that ENSEMBLE=8 samples estimate the true
noise-channel-averaged state (more samples would converge tighter).
Every LIBRERIA_HAMILTONIANE entry is <=6 qubits (dim<=64), so
materializing dim x dim density matrices per step is cheap.
"""
import jax.numpy as jnp
import dense_evolution as de
DT = 0.15
ENSEMBLE = 8
dim = 2 ** n_qubits
h_diag = np.asarray(hamiltonian_values, dtype=np.float64)
psi = np.asarray(sv, dtype=np.complex128).copy()
norm = np.linalg.norm(psi)
if norm > 1e-12:
psi = psi / norm
temp_factor = float(np.clip(md_temp / 300.0 if md_temp > 0 else 0.1, 0.1, 2.0))
# 300K (temp_factor=1.0) -> p_thermal ~= 0.142; scales linearly with temp_factor
# in [0.1, 2.0] -> p_thermal in [0.0, 0.30]. An explicit design choice
# (no calibrated real-world mapping exists here), not a physical constant.
p_thermal = float(np.clip((temp_factor - 0.1) / 1.9 * 0.30, 0.0, 0.30))
rng = np.random.default_rng(seed if seed is not None else 0)
data = {
"Step": [], "Energia_VQE_Ha": [], "Entropia_von_Neumann_Bit": [],
"Purita_Stato": [], "Gradiente_Operatore": [], "Fattore_Rumore_Termico": [],
}
energy_prev = None
for step in range(md_steps):
phase = np.exp(-1j * h_diag * DT)
psi = psi * phase
psi = psi / np.linalg.norm(psi)
if p_thermal > 0:
realizations = []
for _ in range(ENSEMBLE):
sub_seed = int(rng.integers(0, 2**31 - 1))
noisy = de.NoiseModel.apply_to_sv(
sv=np.array(psi, copy=True), n=n_qubits, model='depolarizing',
p=p_thermal, rng=np.random.default_rng(sub_seed),
)
realizations.append(np.asarray(noisy, dtype=np.complex128))
else:
realizations = [psi]
rho_avg = sum(np.outer(v, v.conj()) for v in realizations) / len(realizations)
prob_t = np.real(np.diag(rho_avg))
purity_t = float(np.real(np.trace(rho_avg @ rho_avg)))
# True von Neumann entropy of rho_avg (matches the column's name,
# unlike the Shannon-entropy-of-the-diagonal used elsewhere in the
# dashboard for "entropy") -- cheap here since rho_avg is already
# materialized (dim<=64) and Hermitian, so eigvalsh is exact and fast.
eigvals = np.linalg.eigvalsh(rho_avg)
eig_safe = eigvals[eigvals > 1e-12]
entropy_t = float(-np.sum(eig_safe * np.log2(eig_safe))) if len(eig_safe) else 0.0
energy_t = float(np.sum(prob_t * h_diag))
drift_t = 0.0 if energy_prev is None else abs(energy_t - energy_prev)
energy_prev = energy_t
data["Step"].append(step)
data["Energia_VQE_Ha"].append(energy_t)
data["Entropia_von_Neumann_Bit"].append(entropy_t)
data["Purita_Stato"].append(purity_t)
data["Gradiente_Operatore"].append(drift_t)
data["Fattore_Rumore_Termico"].append(p_thermal)
# carry the *last* noisy realization forward as the next state (one
# MD trajectory, not a density-matrix simulation) -- the other
# ENSEMBLE-1 realizations exist only to estimate purity at this step
psi = realizations[-1] / np.linalg.norm(realizations[-1])
df_md = pd.DataFrame(data)
df_md.set_index("Step", inplace=True)
corr_matrix = df_md.corr(method="pearson")
return df_md, corr_matrix
|