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