File size: 19,745 Bytes
d8eca12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
"""

Quantum Simulator page — circuit execution, VQE (real JAX-autodiff or

hash-seeded mock), MD telemetry, Hamiltonian library, and the

Overview/Fisica Stato/Mosaico/VQE Results/MD Results/Performance/3D

Helix/Hamiltonian panels (dashboard_core.py, ported from dash.py).



VQE and MD telemetry are routed through the same AI vector-healing shield

used standalone on the Vector Healing page (ui_pages/ai_middleware.py)

before any panel is built from them — this is real infrastructure, not a

cosmetic touch: if a run produces NaN/Inf/spike artifacts, the shield

cleans them up here exactly like it does on the Vector Healing page, and

its telemetry (fallback/radius/reconstruction-error) is shown alongside

the physics metrics it protects.

"""

import io
import json

import matplotlib.pyplot as plt
import pandas as pd
import streamlit as st

import dashboard_core as dc
from ui_pages.ai_middleware import heal_telemetry
from ui_pages.components import (
    render_ai_shield_card, render_metric_grid, render_page_banner, render_run_guard,
    AI_SHIELD_NEUTRAL_META,
)


def _render_sidebar() -> dict:
    with st.sidebar:
        st.header("⚙️ Circuito")
        source_mode = st.radio("Sorgente", ["Libreria Built-in", "Custom QASM Textarea"], key="qs_source")
        if source_mode == "Libreria Built-in":
            circuit_name = st.selectbox("Circuito", options=list(dc.QASM_LIBRARY.keys()), key="qs_circuit")
            qasm_text = ""
        else:
            circuit_name = "Custom Workspace"
            qasm_text = st.text_area(
                "OpenQASM 2.0", height=140,
                value='OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; creg c[2]; h q[0]; cx q[0],q[1]; measure q -> c;',
                key="qs_qasm_text",
            )

        st.header("🧮 Motore")
        engine = st.selectbox(
            "Motore di simulazione", options=["dense", "mps"],
            format_func=lambda e: "Denso (DenseSVSimulator/Chunk)" if e == "dense" else "MPS (bond-troncato, ≤24 qubit)",
            key="qs_engine",
            help=(
                "Denso: sempre esatto, adatta memoria automaticamente (Chunk) fino al "
                "limite RAM della macchina. MPS: esatto per circuiti a bassa entanglement "
                "(vedi dense_evolution/mps.py), limitato a 24 qubit in questa dashboard "
                "-- oltre serve il campionamento sequenziale, non ancora collegato ai pannelli."
            ),
        )

        st.header("🌪️ Rumore")
        noise_model = st.selectbox("Modello", options=['ideal', 'depolarizing', 'bitflip', 'phaseflip', 'amplitude_damping', 'combined'], key="qs_noise")
        noise_p = st.slider("Probabilità p", 0.0, 0.5, 0.0, 0.01, key="qs_noise_p")
        shots = st.slider("Shots", 50, 5000, 512, 50, key="qs_shots")
        seed = st.number_input("Seed", min_value=0, max_value=10_000, value=42, key="qs_seed")
        double_precision = st.checkbox("Doppia precisione (float64)", value=False, key="qs_precision")

        st.header("🩹 Error Mitigation (ZNE)")
        zne_enabled = st.checkbox(
            "Abilita Zero-Noise Extrapolation", value=False, key="qs_zne_enabled",
            disabled=(noise_model == 'ideal'),
            help="Richiede un modello di rumore attivo -- non applicabile a 'ideal'.",
        )
        if noise_model == 'ideal' and zne_enabled:
            st.caption("⚠️ ZNE disabilitato: nessun rumore da estrapolare con 'ideal'.")
            zne_enabled = False
        if zne_enabled:
            zne_healing = st.checkbox(
                "Healing predittivo (Δpre_emp-adapted)", value=False, key="qs_zne_healing",
                help=(
                    "Corregge i 3 coefficienti di Richardson in base alla deviazione di "
                    "coerenza osservata (σ da shot-noise binomiale alla scala base), invece "
                    "del ZNE standard a pesi fissi."
                ),
            )
            zne_target_sigma = (
                st.number_input("Target σ ideale", value=10.0, key="qs_zne_target_sigma")
                if zne_healing else 10.0
            )
        else:
            zne_healing, zne_target_sigma = False, 10.0

        st.header("🧪 VQE")
        vqe_enabled = st.checkbox("Abilita telemetria VQE", value=True, key="qs_vqe_enabled")
        if vqe_enabled:
            vqe_epochs = st.slider("Epochs", 5, 100, 20, key="qs_vqe_epochs")
            vqe_lr = st.slider("Learning rate", 0.001, 0.5, 0.05, key="qs_vqe_lr")
            vqe_beta1 = st.slider("Adam β1", 0.5, 0.999, 0.9, key="qs_vqe_beta1")
            vqe_beta2 = st.slider("Adam β2", 0.9, 0.9999, 0.999, key="qs_vqe_beta2")
            confirm_heavy_vqe = st.checkbox(
                f"Confermo VQE reale anche su circuiti pesanti (>{dc.QM_MM_HEAVY_QUBIT_THRESHOLD} qubit — costo O(4ⁿ), può essere lento)",
                value=False, key="qs_confirm_heavy",
            )
        else:
            vqe_epochs, vqe_lr, vqe_beta1, vqe_beta2, confirm_heavy_vqe = 20, 0.05, 0.9, 0.999, False

        hamiltonian_values, hamiltonian_name = None, None
        if vqe_enabled:
            st.header("🧬 Hamiltoniana personalizzata")
            hamiltonian_enabled = st.checkbox("Abilita Hamiltoniana personalizzata", value=False, key="qs_ham_enabled")
            if hamiltonian_enabled:
                ham_library = st.session_state.setdefault("hamiltonian_library", dict(dc.LIBRERIA_HAMILTONIANE))
                current_qasm = dc.QASM_LIBRARY[circuit_name] if source_mode == "Libreria Built-in" else qasm_text
                inferred_n_qubits = dc.infer_qubit_count_from_qasm(current_qasm)
                compatible = dc.get_compatible_hamiltonians(inferred_n_qubits, ham_library)

                ham_mode = st.radio("Modalità", ["Libreria Built-in", "Custom JSON Textarea"], key="qs_ham_mode")
                if ham_mode == "Libreria Built-in":
                    if compatible:
                        hamiltonian_name = st.selectbox(
                            f"Hamiltoniana ({inferred_n_qubits} qubit, dim={2**inferred_n_qubits if inferred_n_qubits else '?'})",
                            options=list(compatible.keys()), key="qs_ham_sel",
                        )
                        hamiltonian_values = compatible[hamiltonian_name]
                    else:
                        st.caption(
                            f"Nessuna Hamiltoniana in libreria è compatibile con {inferred_n_qubits or '?'} qubit "
                            f"(serve un array di lunghezza 2^n = {2**inferred_n_qubits if inferred_n_qubits else '?'}). "
                            "Usa la modalità Custom JSON qui sotto."
                        )
                else:
                    ham_json_text = st.text_area(
                        "Array JSON (lunghezza = 2^n_qubits)", value="[-1.13, -0.45, 0.12, 0.64]",
                        height=80, key="qs_ham_json",
                    )
                    ham_save_name = st.text_input("Nome per salvare in libreria (opzionale)", key="qs_ham_save_name")
                    if st.button("💾 Salva in libreria", key="qs_ham_save_btn"):
                        ok, msg = dc.save_custom_hamiltonian(ham_library, ham_save_name, ham_json_text)
                        (st.success if ok else st.error)(msg)
                    try:
                        hamiltonian_values = json.loads(ham_json_text)
                        hamiltonian_name = ham_save_name or "Custom JSON"
                    except json.JSONDecodeError:
                        st.error("JSON non valido — verrà usata un'Hamiltoniana casuale per questa run.")
                        hamiltonian_values = None

        st.header("🌡️ Molecular Dynamics")
        md_enabled = st.checkbox("Abilita telemetria MD", value=True, key="qs_md_enabled")
        if md_enabled:
            md_steps = st.slider("MD steps", 10, 500, 80, key="qs_md_steps")
            md_temp = st.slider("Temperatura (K)", 10, 800, 300, key="qs_md_temp")
        else:
            md_steps, md_temp = 80, 300

        run_clicked = st.button("▶️ Esegui Simulazione", type="primary")

    return dict(
        source_mode=source_mode, circuit_name=circuit_name, qasm_text=qasm_text,
        engine=engine,
        noise_model=noise_model, noise_p=noise_p, shots=shots, seed=seed,
        double_precision=double_precision,
        zne_enabled=zne_enabled, zne_healing=zne_healing, zne_target_sigma=zne_target_sigma,
        vqe_enabled=vqe_enabled, vqe_epochs=vqe_epochs, vqe_lr=vqe_lr,
        vqe_beta1=vqe_beta1, vqe_beta2=vqe_beta2, confirm_heavy_vqe=confirm_heavy_vqe,
        hamiltonian_values=hamiltonian_values, hamiltonian_name=hamiltonian_name,
        md_enabled=md_enabled, md_steps=md_steps, md_temp=md_temp,
        run_clicked=run_clicked,
    )


def _execute_run(p: dict) -> None:
    with st.spinner("Esecuzione circuito..."):
        try:
            res = dc.run_simulation(
                p["source_mode"], p["circuit_name"], p["qasm_text"],
                p["noise_model"], p["noise_p"], p["shots"], int(p["seed"]),
                use_float32=not p["double_precision"], engine=p["engine"],
            )
        except Exception as e:
            st.error(f"Errore durante l'esecuzione del circuito: {e}")
            return

    mitigation_res = None
    if p["zne_enabled"]:
        with st.spinner("Mitigazione ZNE (3 scale di rumore)..."):
            try:
                mitigation_res = dc.run_mitigation_sweep(
                    p["source_mode"], p["circuit_name"], p["qasm_text"], p["noise_model"],
                    p["noise_p"], p["shots"], int(p["seed"]),
                    use_float32=not p["double_precision"], engine=p["engine"],
                    healing_enabled=p["zne_healing"], target_sigma_ideal=p["zne_target_sigma"],
                )
            except Exception as e:
                st.error(f"Errore durante la mitigazione ZNE: {e}")

    df_vqe = pd.DataFrame()
    vqe_ai_meta = dict(AI_SHIELD_NEUTRAL_META)
    if p["vqe_enabled"]:
        if res['n_qubits'] > dc.QM_MM_HEAVY_QUBIT_THRESHOLD and not p["confirm_heavy_vqe"]:
            st.warning(
                f"Circuito a {res['n_qubits']} qubit: il VQE reale (se il circuito ha gate parametrici) "
                f"userebbe QMMMForceEngine con costo O(4ⁿ) ≈ dim²={2**res['n_qubits']:,}² — telemetria VQE "
                "saltata per questa run. Spunta la conferma nella sidebar per eseguirla comunque."
            )
        else:
            progress_bar = st.progress(0.0, text="VQE: avvio ottimizzazione...")
            log_lines = []
            log_container = st.empty()

            def _on_epoch(epoch, total, row):
                progress_bar.progress((epoch + 1) / total, text=f"VQE epoch {epoch + 1}/{total}")
                log_lines.append(
                    f"[epoch {epoch + 1:03d}/{total}] "
                    f"E={row['VQE_Energy']:.5f}  S={row['Entropy']:.4f}  "
                    f"purity={row['Purity']:.4f}  grad={row['Gradient']:.5f}"
                )
                log_container.code("\n".join(log_lines[-12:]), language="text")

            try:
                df_vqe = dc.run_vqe_telemetry(
                    res['sim'], res['parser'],
                    dc.QASM_LIBRARY[p["circuit_name"]] if p["source_mode"] == "Libreria Built-in" else p["qasm_text"],
                    p["circuit_name"], res['n_qubits'], not p["double_precision"],
                    p["vqe_epochs"], p["vqe_lr"], p["vqe_beta1"], p["vqe_beta2"], int(p["seed"]),
                    hamiltonian_values=p["hamiltonian_values"], on_epoch=_on_epoch,
                )
            except Exception as e:
                st.error(f"Errore durante la telemetria VQE: {e}")
            finally:
                progress_bar.empty()

            if not df_vqe.empty:
                df_vqe, vqe_ai_meta = heal_telemetry(df_vqe)
                if vqe_ai_meta["fallback_triggered"]:
                    log_lines.append(
                        f"⚠️ AI Shield: fallback mediano attivato (raggio adattivo="
                        f"{vqe_ai_meta['adaptive_radius_used']}, errore ricostruzione="
                        f"{vqe_ai_meta['reconstruction_error']:.4f})"
                    )
                log_container.code("\n".join(log_lines[-12:]), language="text")

    df_md, corr_matrix = pd.DataFrame(), pd.DataFrame()
    md_ai_meta = dict(AI_SHIELD_NEUTRAL_META)
    md_is_real, md_note = False, ''
    if p["md_enabled"]:
        with st.spinner("Telemetria MD..."):
            # Real dynamics under p["hamiltonian_values"] when it's compatible
            # with this circuit's statevector; otherwise run_md_telemetry
            # falls back to the labeled mock itself (see md_telemetry.py).
            df_md, _raw_corr = dc.run_md_telemetry(
                p["md_steps"], p["md_temp"],
                hamiltonian_values=p["hamiltonian_values"], sv=res['sim'].sv,
                n_qubits=res['n_qubits'], seed=int(p["seed"]),
            )
            md_is_real = df_md.attrs.get('is_real', False)
            md_note = df_md.attrs.get('note', '')
            df_md, md_ai_meta = heal_telemetry(df_md)
            corr_matrix = df_md.corr(method="pearson")  # recomputed from the healed data, not the raw one

    history = st.session_state.setdefault("qs_run_history", [])
    history.append({
        'nome': res['nome'],
        'n_qubits': res['n_qubits'],
        'tempo': res['tempo'],
        'ram': res['ram'],
        'porte_count': res['porte_count'],
        'entropy': float(res['entropy']),
        'fidelity': res['fidelity'],
        'stato_dominante': res['stato_dominante'],
        'noise_model': p["noise_model"],
        'noise_p': p["noise_p"],
    })

    # Build every panel once, here, right after the data that feeds it
    # changes — not on every sidebar rerun. Figures are closed immediately
    # (plt.close) and the objects kept in session_state; st.pyplot() can
    # render an already-closed Figure object just fine, this only stops
    # them accumulating in pyplot's registry.
    with st.spinner("Rendering pannelli..."):
        metrics = dc.compute_overview_metrics(res, p["noise_model"], p["noise_p"])
        overview_fig = dc.build_panel_overview(res, df_vqe, corr_matrix, p["noise_model"], p["noise_p"])
        fisica_fig = dc.build_panel_fisica(res, seed=int(p["seed"]))
        mosaico_fig = dc.build_panel_mosaico(res)
        vqe_fig = dc.build_panel_vqe_results(df_vqe)
        md_fig = dc.build_panel_md_results(df_md, corr_matrix)
        perf_fig = dc.build_panel_performance(res, history)
        helix_fig = dc.build_3d_helix_patch(res['n_qubits'], res['prob'])
        ham_fig = dc.build_panel_hamiltonian(p["hamiltonian_values"], p["hamiltonian_name"] or 'nessuna')
        mitigation_fig = dc.build_panel_mitigation(mitigation_res, res) if mitigation_res else None

        overview_png = io.BytesIO()
        overview_fig.savefig(overview_png, format="png", dpi=300, facecolor='#010409', bbox_inches='tight')

        figs_to_close = [overview_fig, fisica_fig, mosaico_fig, vqe_fig, md_fig, perf_fig, ham_fig]
        if mitigation_fig is not None:
            figs_to_close.append(mitigation_fig)
        for fig in figs_to_close:
            plt.close(fig)

    st.session_state["qs_res"] = res
    st.session_state["qs_panels"] = {
        "metrics": metrics,
        "vqe_ai_meta": vqe_ai_meta,
        "md_ai_meta": md_ai_meta,
        "md_is_real": md_is_real,
        "md_note": md_note,
        "overview": overview_fig,
        "overview_png": overview_png.getvalue(),
        "fisica": fisica_fig,
        "mosaico": mosaico_fig,
        "vqe": vqe_fig,
        "md": md_fig,
        "hamiltonian": ham_fig,
        "performance": perf_fig,
        "helix": helix_fig,
        "mitigation": mitigation_fig,
    }


def _render_tabs() -> None:
    guard = render_run_guard(
        "qs_res", "qs_panels",
        message="Configura il circuito nella sidebar e premi **Esegui Simulazione** per iniziare.",
    )
    if guard is None:
        return
    res, panels = guard

    run_history = st.session_state.get("qs_run_history", [])

    tabs = st.tabs(["Overview", "Fisica Stato", "Mosaico", "VQE Results", "MD Results", "Performance", "3D Helix", "Hamiltonian", "Mitigation (ZNE)"])

    with tabs[0]:
        render_ai_shield_card("AI Vector-Healing Shield — Telemetria VQE", panels["vqe_ai_meta"])
        with st.container(border=True):
            st.subheader("📊 Simulation Metrics")
            render_metric_grid(panels["metrics"])
        st.pyplot(panels["overview"])
        st.download_button(
            "🖼️ Esporta Overview come PNG (300 DPI)",
            data=panels["overview_png"],
            file_name="quantum_dashboard_overview.png",
            mime="image/png",
        )

    with tabs[1]:
        st.pyplot(panels["fisica"])

    with tabs[2]:
        st.pyplot(panels["mosaico"])

    with tabs[3]:
        st.pyplot(panels["vqe"])

    with tabs[4]:
        if panels.get("md_is_real"):
            st.success(f"● DATI REALI — {panels.get('md_note', '')}")
        else:
            st.warning(f"● MOCK — {panels.get('md_note', '')}")
        render_ai_shield_card("AI Vector-Healing Shield — Telemetria MD", panels["md_ai_meta"])
        st.pyplot(panels["md"])

    with tabs[5]:
        st.pyplot(panels["performance"])

        if run_history:
            st.dataframe(pd.DataFrame(run_history), use_container_width=True)
            st.download_button(
                "💾 Esporta provenance JSON",
                data=dc.build_provenance_json(run_history),
                file_name="quantum_provenance_archive.json",
                mime="application/json",
            )

        st.divider()
        st.caption(
            "Benchmark scan: alloca simulatori da 2 a 14 qubit e misura tempo/RAM. "
            "Genuinamente lento (allocazione densa fino a 2¹⁴) — esegui solo se necessario."
        )
        if st.button("🧪 Esegui Benchmark Scan (2→14 qubit)"):
            with st.spinner("Benchmark in corso..."):
                bench_df = dc.run_benchmark_scan()
            st.dataframe(bench_df, use_container_width=True)

    with tabs[6]:
        st.plotly_chart(panels["helix"], use_container_width=True)

    with tabs[7]:
        st.pyplot(panels["hamiltonian"])
        st.caption(
            "Se nessuna Hamiltoniana personalizzata è abilitata nella sidebar, il VQE usa uno spettro "
            "casuale ordinato (comportamento di default, invariato)."
        )

    with tabs[8]:
        if panels.get("mitigation") is None:
            st.info("Abilita ZNE nella sidebar e riesegui per vedere questo pannello.")
        else:
            st.pyplot(panels["mitigation"])


def render():
    render_page_banner(
        "Quantum Simulator Dashboard",
        """Esegui un circuito OpenQASM su <code>DenseSVSimulator</code>, opzionalmente con rumore,

        telemetria VQE (JAX autodiff + forze QM/MM Hellmann-Feynman) e dinamica molecolare —

        entrambe protette dallo stesso scudo AI Vector-Healing della pagina Vector Healing.""",
    )

    params = _render_sidebar()
    if params["run_clicked"]:
        _execute_run(params)
    _render_tabs()