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,
    }