dense-evolution 8.3.0__py3-none-macosx_11_0_universal2.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. dashboard_core/__init__.py +115 -0
  2. dashboard_core/_gate_tables.py +30 -0
  3. dashboard_core/band_structure.py +71 -0
  4. dashboard_core/circuit_builder_component.py +232 -0
  5. dashboard_core/circuit_diagram.py +216 -0
  6. dashboard_core/crypto_protocols.py +77 -0
  7. dashboard_core/engine.py +326 -0
  8. dashboard_core/graphical_builder.py +114 -0
  9. dashboard_core/hamiltonians.py +593 -0
  10. dashboard_core/mass_decomposition_tool.py +47 -0
  11. dashboard_core/mitigation.py +343 -0
  12. dashboard_core/native_hf_diagnostics.py +62 -0
  13. dashboard_core/noise_tools.py +125 -0
  14. dashboard_core/qasm_library.py +233 -0
  15. dashboard_core/qmmm.py +16 -0
  16. dashboard_core/rag_tool.py +45 -0
  17. dashboard_core/state_visuals.py +288 -0
  18. dashboard_core/system_limits.py +60 -0
  19. dashboard_core/vector_healing.py +102 -0
  20. dashboard_core/visuals.py +158 -0
  21. dashboard_core/vqe.py +533 -0
  22. dashboard_core/wormhole.py +580 -0
  23. dense_evolution/__init__.py +114 -0
  24. dense_evolution/autodiff.py +10 -0
  25. dense_evolution/backends/__init__.py +5 -0
  26. dense_evolution/backends/chunk/__init__.py +37 -0
  27. dense_evolution/backends/chunk/_engine_imports.py +57 -0
  28. dense_evolution/backends/chunk/circuit_chunker.py +55 -0
  29. dense_evolution/backends/chunk/core.py +432 -0
  30. dense_evolution/backends/chunk/disk_overflow.py +232 -0
  31. dense_evolution/backends/chunk/geometry.py +95 -0
  32. dense_evolution/backends/chunk/guard.py +190 -0
  33. dense_evolution/backends/chunk/kernels.py +531 -0
  34. dense_evolution/backends/mps.py +1569 -0
  35. dense_evolution/backends/statevector.py +616 -0
  36. dense_evolution/chunk.py +25 -0
  37. dense_evolution/circuits/__init__.py +20 -0
  38. dense_evolution/circuits/compiler.py +488 -0
  39. dense_evolution/circuits/diagram.py +94 -0
  40. dense_evolution/circuits/gates.py +91 -0
  41. dense_evolution/circuits/parser.py +632 -0
  42. dense_evolution/circuits/qft.py +66 -0
  43. dense_evolution/circuits/random_circuit.py +85 -0
  44. dense_evolution/circuits/registry.py +74 -0
  45. dense_evolution/circuits/topology.py +79 -0
  46. dense_evolution/circuits/trotter.py +265 -0
  47. dense_evolution/circuits/uccsd.py +275 -0
  48. dense_evolution/cli.py +199 -0
  49. dense_evolution/compiler.py +9 -0
  50. dense_evolution/config.py +49 -0
  51. dense_evolution/drawing.py +10 -0
  52. dense_evolution/entropy.py +9 -0
  53. dense_evolution/fermions.py +9 -0
  54. dense_evolution/gates.py +9 -0
  55. dense_evolution/harrison_tb.py +16 -0
  56. dense_evolution/healing.py +18 -0
  57. dense_evolution/interop/__init__.py +18 -0
  58. dense_evolution/interop/qiskit_pennylane.py +406 -0
  59. dense_evolution/measurement.py +10 -0
  60. dense_evolution/mitigation/__init__.py +54 -0
  61. dense_evolution/mitigation/healing.py +215 -0
  62. dense_evolution/mitigation/kl_divergence.py +93 -0
  63. dense_evolution/mitigation/magic_entropy.py +163 -0
  64. dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
  65. dense_evolution/mitigation/renyi.py +168 -0
  66. dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
  67. dense_evolution/mitigation/zne.py +990 -0
  68. dense_evolution/mps.py +9 -0
  69. dense_evolution/native_hf/__init__.py +26 -0
  70. dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
  71. dense_evolution/native_hf/_libcint/libdecint.dylib +0 -0
  72. dense_evolution/native_hf/assembly.py +304 -0
  73. dense_evolution/native_hf/basis.py +117 -0
  74. dense_evolution/native_hf/boys.py +35 -0
  75. dense_evolution/native_hf/bridge.py +112 -0
  76. dense_evolution/native_hf/cartesian.py +64 -0
  77. dense_evolution/native_hf/coulomb.py +196 -0
  78. dense_evolution/native_hf/differentiable.py +53 -0
  79. dense_evolution/native_hf/gaussians.py +79 -0
  80. dense_evolution/native_hf/kinetic.py +52 -0
  81. dense_evolution/native_hf/libcint_bridge.py +167 -0
  82. dense_evolution/native_hf/overlap.py +91 -0
  83. dense_evolution/native_hf/scf.py +404 -0
  84. dense_evolution/noise/__init__.py +79 -0
  85. dense_evolution/noise/coherent_attack.py +264 -0
  86. dense_evolution/noise/cosmic_ray.py +61 -0
  87. dense_evolution/noise/density_matrix_channels.py +78 -0
  88. dense_evolution/noise/differentiable.py +66 -0
  89. dense_evolution/noise/kraus/__init__.py +6 -0
  90. dense_evolution/noise/kraus/amplitude_damping.py +47 -0
  91. dense_evolution/noise/kraus/bitflip.py +22 -0
  92. dense_evolution/noise/kraus/combined.py +16 -0
  93. dense_evolution/noise/kraus/depolarizing.py +47 -0
  94. dense_evolution/noise/kraus/ideal.py +10 -0
  95. dense_evolution/noise/kraus/phaseflip.py +21 -0
  96. dense_evolution/noise/kraus_channels.py +285 -0
  97. dense_evolution/noise/oscillating.py +32 -0
  98. dense_evolution/noise/pink.py +80 -0
  99. dense_evolution/observables.py +11 -0
  100. dense_evolution/parser.py +9 -0
  101. dense_evolution/physics/__init__.py +27 -0
  102. dense_evolution/physics/entropy.py +161 -0
  103. dense_evolution/physics/fermions.py +322 -0
  104. dense_evolution/physics/observables.py +523 -0
  105. dense_evolution/physics/qec.py +1113 -0
  106. dense_evolution/physics/spectral.py +143 -0
  107. dense_evolution/physics/states.py +43 -0
  108. dense_evolution/protocols/__init__.py +27 -0
  109. dense_evolution/protocols/bb84.py +133 -0
  110. dense_evolution/protocols/di_qkd_ghz.py +199 -0
  111. dense_evolution/protocols/dicka_protocol2.py +124 -0
  112. dense_evolution/qec.py +20 -0
  113. dense_evolution/qft.py +9 -0
  114. dense_evolution/qmmm/__init__.py +13 -0
  115. dense_evolution/qmmm/ase_bridge.py +97 -0
  116. dense_evolution/qmmm/forces.py +388 -0
  117. dense_evolution/qmmm/propagation.py +80 -0
  118. dense_evolution/qmmm/region.py +137 -0
  119. dense_evolution/random_circuit.py +15 -0
  120. dense_evolution/registry.py +9 -0
  121. dense_evolution/simulator.py +10 -0
  122. dense_evolution/solvers/__init__.py +19 -0
  123. dense_evolution/solvers/autodiff.py +169 -0
  124. dense_evolution/solvers/harrison_tb.py +189 -0
  125. dense_evolution/solvers/vhd_tb.py +187 -0
  126. dense_evolution/states.py +9 -0
  127. dense_evolution/topology.py +9 -0
  128. dense_evolution/trotter.py +9 -0
  129. dense_evolution/utils/__init__.py +13 -0
  130. dense_evolution/utils/drawing.py +101 -0
  131. dense_evolution/utils/mass_decomposition.py +246 -0
  132. dense_evolution/utils/measurement.py +94 -0
  133. dense_evolution/vhd_tb.py +16 -0
  134. dense_evolution-8.3.0.dist-info/METADATA +364 -0
  135. dense_evolution-8.3.0.dist-info/RECORD +164 -0
  136. dense_evolution-8.3.0.dist-info/WHEEL +5 -0
  137. dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
  138. dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
  139. ia_utils/__init__.py +0 -0
  140. ia_utils/adversarial_vector_attack.py +196 -0
  141. ia_utils/rag.py +288 -0
  142. ia_utils/vector_healing.py +399 -0
  143. local_site/__init__.py +0 -0
  144. local_site/app/__init__.py +0 -0
  145. local_site/app/server.py +1009 -0
  146. mcp_server/__init__.py +0 -0
  147. mcp_server/client.py +324 -0
  148. mcp_server/config.py +32 -0
  149. mcp_server/models.py +347 -0
  150. mcp_server/molecules.py +71 -0
  151. mcp_server/server.py +119 -0
  152. mcp_server/tools/__init__.py +0 -0
  153. mcp_server/tools/chemistry_tools.py +225 -0
  154. mcp_server/tools/circuit_tools.py +83 -0
  155. mcp_server/tools/crypto_tools.py +66 -0
  156. mcp_server/tools/mitigation_tools.py +81 -0
  157. mcp_server/tools/noise_tools.py +60 -0
  158. mcp_server/tools/retrieval_tools.py +44 -0
  159. mcp_server/tools/system_tools.py +149 -0
  160. mcp_server/tools/wormhole_tools.py +142 -0
  161. mcp_server/utils/__init__.py +0 -0
  162. mcp_server/utils/cache.py +55 -0
  163. mcp_server/utils/images.py +67 -0
  164. mcp_server/utils/truncation.py +38 -0
@@ -0,0 +1,115 @@
1
+ """
2
+ dashboard_core -- real compute + visualization layer for app_dashboard.py.
3
+
4
+ Rebuilt from scratch on the structure of IBM Quantum Composer (circuit
5
+ editor, statevector, probabilities, Q-sphere), wired to dense_evolution's
6
+ actual DenseSVSimulator and to Qiskit's own real visualization functions
7
+ -- no hand-rolled plotting, no synthetic data. The previous dashboard_core
8
+ (VQE, molecular Hamiltonians, mitigation, AI vector-healing) lives intact
9
+ on the feature/streamlit-dashboard and feature/ipywidgets-dash-panel
10
+ branches and will be reintegrated selectively once this base is solid.
11
+ """
12
+
13
+ import dense_evolution as _de
14
+
15
+
16
+ def enable_dashboard_precision():
17
+ """Enables float64 precision for the dashboard (prog.txt,
18
+ dashboard_core audit point 2). dense_evolution no longer forces
19
+ jax_enable_x64 as an import-time side effect (see
20
+ dense_evolution/config.py) -- it's enabled lazily, only by the
21
+ specific call that needs complex128 (e.g. DenseSVSimulator). Some
22
+ paths here (MPSSimulator-only branches in engine.py, Chunk usage in
23
+ system_limits.py/wormhole.py) never construct a DenseSVSimulator, so
24
+ this dashboard -- which always wants float64 -- must set it
25
+ explicitly rather than relying on that now-removed side effect.
26
+
27
+ This used to run as a bare module-level `_de.set_precision(True)`
28
+ call here, so merely `from dashboard_core.circuit_diagram import
29
+ draw_native_circuit_diagram` paid the x64-enable cost even though
30
+ that submodule never touches precision -- exactly the import-time
31
+ side effect dense_evolution's own config.py was written to eliminate
32
+ (see its module docstring), just not yet applied one level up here.
33
+ Call this explicitly from the dashboard's own entry point
34
+ (tools/dashboard/app.py) instead."""
35
+ _de.set_precision(True)
36
+
37
+ from .qasm_library import QASM_LIBRARY, gate_tuples_to_qasm
38
+ from .engine import (
39
+ SimulationResult, run_circuit_from_qasm,
40
+ LargeScaleMPSResult, run_large_circuit_mps, MPS_DENSE_CONTRACTION_LIMIT,
41
+ run_bond_convergence_check,
42
+ )
43
+ from .visuals import (
44
+ draw_circuit_figure, histogram_figure, qsphere_figure, bloch_multivector_figure,
45
+ energy_landscape_figure, zne_bar_figure, qec_syndrome_map_figure,
46
+ )
47
+ from .graphical_builder import GATE_PALETTE, ops_to_native_tuples
48
+ from .circuit_builder_component import mount_circuit_builder
49
+ from .hamiltonians import (
50
+ MOLECULE_CATALOG, build_molecular_hamiltonian, get_compatible_molecules,
51
+ get_all_molecules, get_molecule_n_qubits,
52
+ get_molecular_hamiltonian_matrix, ground_state_energy, ground_state_energy_sparse,
53
+ linear_chain_geometry, ring_geometry, mix_hamiltonians,
54
+ )
55
+ from .mitigation import (
56
+ MitigationResult, run_zne_mitigation, DensityMatrixZNEResult, run_density_matrix_zne,
57
+ CoherenceZNEResult, run_coherence_zne_mitigation,
58
+ )
59
+ from .crypto_protocols import (
60
+ Bb84Result, run_bb84, DiQkdGhzResult, run_di_qkd_ghz, run_dicka_protocol,
61
+ )
62
+ from .native_hf_diagnostics import NativeHfDiagnosticsResult, run_native_hf_diagnostics
63
+ from .mass_decomposition_tool import MassDecompositionResult, run_mass_decomposition
64
+ from .rag_tool import RagSearchResult, run_rag_search
65
+ from .system_limits import max_safe_dense_qubits
66
+ from .vqe import run_vqe, scan_hardware_efficient_energy_landscape
67
+ from .qmmm import (
68
+ ATOMIC_MASSES_AMU,
69
+ compute_hellmann_feynman_forces, md_step, run_md_trajectory,
70
+ )
71
+ from .wormhole import (
72
+ build_sparse_syk_terms, commuting_pair_count, select_good_instance,
73
+ run_wormhole_protocol, run_wormhole_protocol_trotter,
74
+ )
75
+ from .vector_healing import VectorHealingResult, run_vector_healing
76
+ from .noise_tools import (
77
+ CosmicRayBurstResult, run_cosmic_ray_burst,
78
+ OscillatingNoiseResult, run_oscillating_noise,
79
+ DensityMatrixChannelResult, run_density_matrix_channel,
80
+ )
81
+ from .band_structure import scan_bands_along_path, band_structure_figure
82
+
83
+ __all__ = [
84
+ 'enable_dashboard_precision',
85
+ 'QASM_LIBRARY', 'gate_tuples_to_qasm',
86
+ 'SimulationResult', 'run_circuit_from_qasm',
87
+ 'LargeScaleMPSResult', 'run_large_circuit_mps', 'MPS_DENSE_CONTRACTION_LIMIT',
88
+ 'run_bond_convergence_check',
89
+ 'draw_circuit_figure', 'histogram_figure', 'qsphere_figure', 'bloch_multivector_figure',
90
+ 'energy_landscape_figure', 'zne_bar_figure', 'qec_syndrome_map_figure',
91
+ 'GATE_PALETTE', 'ops_to_native_tuples',
92
+ 'mount_circuit_builder',
93
+ 'MOLECULE_CATALOG', 'build_molecular_hamiltonian', 'get_compatible_molecules',
94
+ 'get_all_molecules', 'get_molecule_n_qubits',
95
+ 'get_molecular_hamiltonian_matrix', 'ground_state_energy', 'ground_state_energy_sparse',
96
+ 'linear_chain_geometry', 'ring_geometry', 'mix_hamiltonians',
97
+ 'MitigationResult', 'run_zne_mitigation',
98
+ 'DensityMatrixZNEResult', 'run_density_matrix_zne',
99
+ 'CoherenceZNEResult', 'run_coherence_zne_mitigation',
100
+ 'Bb84Result', 'run_bb84', 'DiQkdGhzResult', 'run_di_qkd_ghz', 'run_dicka_protocol',
101
+ 'NativeHfDiagnosticsResult', 'run_native_hf_diagnostics',
102
+ 'MassDecompositionResult', 'run_mass_decomposition',
103
+ 'RagSearchResult', 'run_rag_search',
104
+ 'max_safe_dense_qubits',
105
+ 'run_vqe', 'scan_hardware_efficient_energy_landscape',
106
+ 'ATOMIC_MASSES_AMU',
107
+ 'compute_hellmann_feynman_forces', 'md_step', 'run_md_trajectory',
108
+ 'build_sparse_syk_terms', 'commuting_pair_count', 'select_good_instance',
109
+ 'run_wormhole_protocol', 'run_wormhole_protocol_trotter',
110
+ 'VectorHealingResult', 'run_vector_healing',
111
+ 'CosmicRayBurstResult', 'run_cosmic_ray_burst',
112
+ 'OscillatingNoiseResult', 'run_oscillating_noise',
113
+ 'DensityMatrixChannelResult', 'run_density_matrix_channel',
114
+ 'scan_bands_along_path', 'band_structure_figure',
115
+ ]
@@ -0,0 +1,30 @@
1
+ """Gate-name classification, shared by qasm_library.py and
2
+ circuit_diagram.py (prog.txt, dashboard_core audit point 2). Was: two
3
+ independently hand-copied sets of the same 5 constants, each with a
4
+ comment claiming to mirror a different, non-existent "canonical" source
5
+ (dense_evolution/compiler.py's gate categories, dashboard_core/engine.py's
6
+ dispatch tables) -- neither actually defines these as a literal data
7
+ structure. The two copies had already drifted apart in practice:
8
+ circuit_diagram.py's _ONE_QUBIT_PARAM was missing the 'u1'/'phase'
9
+ aliases qasm_library.py's copy had, so a circuit using either of those
10
+ two (both real gate names dense_evolution.circuits.gates.GATE_IDS
11
+ recognizes, both mapped to the same gate as 'p') crashed
12
+ draw_native_circuit_diagram with "unsupported gate for native circuit
13
+ diagram" -- a real bug, not just a duplication risk, fixed by making
14
+ this the one place these sets are defined.
15
+
16
+ Matches dense_evolution.circuits.gates.GATE_IDS's own gate-name
17
+ vocabulary (the actual source of truth for which names QASMParser
18
+ accepts) -- verified directly, not assumed, before writing this down.
19
+ """
20
+
21
+ __all__ = [
22
+ '_ONE_QUBIT_STATIC', '_ONE_QUBIT_PARAM', '_TWO_QUBIT_STATIC',
23
+ '_TWO_QUBIT_PARAM', '_THREE_QUBIT_STATIC',
24
+ ]
25
+
26
+ _ONE_QUBIT_STATIC = {"h", "x", "y", "z", "s", "sdg", "t", "tdg", "sx", "id"}
27
+ _ONE_QUBIT_PARAM = {"rx", "ry", "rz", "p", "u1", "phase"}
28
+ _TWO_QUBIT_STATIC = {"cx", "cz", "cy", "swap"}
29
+ _TWO_QUBIT_PARAM = {"cp", "crz"}
30
+ _THREE_QUBIT_STATIC = {"ccx"}
@@ -0,0 +1,71 @@
1
+ """
2
+ Real sp3s* tight-binding band structure along a k-path
3
+ (dense_evolution.sp3s_star_hamiltonian, the same real Hamiltonian
4
+ direct_gap_at_gamma/band_extrema_along_path already use -- all 10 bands
5
+ here, not just the VBM/CBM pair those two track), plus an interactive
6
+ Plotly figure with real hover tooltips (k-point and energy per band).
7
+
8
+ This is the one interactive (hover/zoom) visualization in the dashboard;
9
+ everywhere else in dashboard_core.visuals uses static matplotlib. The
10
+ choice here is unrelated to that module's own reason for staying
11
+ matplotlib-only (Qiskit's macOS instability, see visuals.py) -- this is
12
+ purely about wanting real per-point hover data that a static image
13
+ cannot provide, and needed adding plotly as a declared dependency
14
+ (pyproject.toml's `dashboard` extra) since it was only ever an
15
+ incidental transitive package before.
16
+ """
17
+
18
+ import numpy as np
19
+ import plotly.graph_objects as go
20
+
21
+ import dense_evolution as de
22
+
23
+ __all__ = ['scan_bands_along_path', 'band_structure_figure']
24
+
25
+
26
+ def scan_bands_along_path(material, k_start, k_end, n_points=201):
27
+ """All 10 sp3s* tight-binding bands (real eigenvalues of
28
+ dense_evolution.sp3s_star_hamiltonian at each sampled k-point, not
29
+ just the single VBM/CBM pair band_extrema_along_path tracks) along
30
+ the straight line from k_start to k_end, both in the same 2*pi/a
31
+ cubic-axis units as sp3s_star_hamiltonian itself.
32
+
33
+ Returns (t, bands): t is the fractional position along the path
34
+ (0..1, n_points), bands is a (n_points, 10) array with band j's
35
+ energy at t[i] in bands[i, j] (sorted ascending at every k-point).
36
+ """
37
+ k_start = np.asarray(k_start, dtype=float)
38
+ k_end = np.asarray(k_end, dtype=float)
39
+ t = np.linspace(0.0, 1.0, n_points)
40
+ ks = k_start[None, :] + t[:, None] * (k_end - k_start)[None, :]
41
+
42
+ bands = np.empty((n_points, 10))
43
+ for i, k in enumerate(ks):
44
+ bands[i] = np.sort(np.linalg.eigvalsh(de.sp3s_star_hamiltonian(k, material)).real)
45
+ return t, bands
46
+
47
+
48
+ def band_structure_figure(t, bands, material_name, k_start_label='Γ', k_end_label='X'):
49
+ """Interactive Plotly band-structure plot: one line per band, real
50
+ hover tooltip per point showing the exact k-path position and
51
+ energy (not read off a static image). Bands 3/4 (0-indexed, the
52
+ valence-band maximum / conduction-band minimum pair
53
+ band_extrema_along_path itself tracks) are highlighted."""
54
+ fig = go.Figure()
55
+ n_bands = bands.shape[1]
56
+ for j in range(n_bands):
57
+ is_frontier = j in (3, 4)
58
+ fig.add_trace(go.Scatter(
59
+ x=t, y=bands[:, j], mode='lines',
60
+ name=f'banda {j}' + (' (VBM)' if j == 3 else ' (CBM)' if j == 4 else ''),
61
+ line=dict(width=3 if is_frontier else 1, color='#d62728' if j == 3 else '#1f77b4' if j == 4 else '#999999'),
62
+ hovertemplate=f'banda {j}<br>t=%{{x:.3f}}<br>E=%{{y:.4f}} eV<extra></extra>',
63
+ ))
64
+ fig.update_layout(
65
+ title=f'Struttura a bande sp3s* -- {material_name} ({k_start_label}→{k_end_label})',
66
+ xaxis_title=f'{k_start_label} → {k_end_label}',
67
+ yaxis_title='Energia (eV)',
68
+ hovermode='closest',
69
+ template='plotly_white',
70
+ )
71
+ return fig
@@ -0,0 +1,232 @@
1
+ """
2
+ Graphical (drag-and-drop) circuit builder -- the fourth pillar of IBM
3
+ Quantum Composer's layout (graphical editor + code editor + Statevector +
4
+ Probabilities), the one missing piece the rest of dashboard_core didn't
5
+ have yet.
6
+
7
+ Built on Streamlit's own native bidirectional component API
8
+ (st.components.v2.component -- real HTML/CSS/JS running in the browser,
9
+ not a hand-waved "coming soon" panel). Gates are dragged from a palette
10
+ onto a qubit x time-step grid using plain HTML5 drag-and-drop; every drop
11
+ re-serializes the grid into an ops list and pushes it back to Python via
12
+ setStateValue, so Streamlit always has the real current state of what's
13
+ on the canvas.
14
+
15
+ The ops list this emits is consumed by dashboard_core.graphical_builder.
16
+ ops_to_native_tuples, which builds dense_evolution's own gate tuples
17
+ (no Qiskit) executed by the exact same dense_evolution engine as typed
18
+ OpenQASM -- no separate/fake execution path for graphically-built
19
+ circuits.
20
+ """
21
+
22
+ from .graphical_builder import GATE_PALETTE
23
+
24
+ __all__ = ['mount_circuit_builder']
25
+
26
+ _CSS = """
27
+ .cb-root { display: flex; flex-direction: column; gap: 10px; font-family: monospace; }
28
+ .cb-palette { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px; border: 1px solid var(--st-border-color, #444); border-radius: 6px; }
29
+ .cb-chip {
30
+ padding: 4px 10px; border-radius: 4px; cursor: grab; user-select: none;
31
+ background: var(--st-secondary-background-color, #262730);
32
+ color: var(--st-text-color, #fafafa);
33
+ border: 1px solid var(--st-border-color, #444);
34
+ font-size: 14px;
35
+ }
36
+ .cb-chip:active { cursor: grabbing; }
37
+ .cb-chip[data-kind="control"] { background: #3a5; color: #fff; }
38
+ .cb-chip[data-kind="target"] { background: #35a; color: #fff; }
39
+ .cb-chip[data-kind="swap"] { background: #a53; color: #fff; }
40
+ .cb-toolbar { display: flex; gap: 8px; align-items: center; }
41
+ .cb-toolbar button {
42
+ padding: 4px 10px; border-radius: 4px; cursor: pointer;
43
+ background: var(--st-secondary-background-color, #262730);
44
+ color: var(--st-text-color, #fafafa);
45
+ border: 1px solid var(--st-border-color, #444);
46
+ }
47
+ .cb-grid { display: grid; gap: 2px; align-items: center; position: relative; }
48
+ .cb-qlabel { font-size: 12px; opacity: 0.75; text-align: right; padding-right: 6px; }
49
+ .cb-cell {
50
+ width: 40px; height: 32px; border: 1px solid var(--st-border-color, #444);
51
+ border-radius: 4px; display: flex; align-items: center; justify-content: center;
52
+ font-size: 13px; cursor: pointer;
53
+ background: var(--st-background-color, #0e1117);
54
+ color: var(--st-text-color, #fafafa);
55
+ }
56
+ .cb-cell.cb-filled { background: var(--st-secondary-background-color, #262730); font-weight: bold; }
57
+ .cb-cell.cb-over { outline: 2px dashed #4af; }
58
+ .cb-count { font-size: 12px; opacity: 0.75; }
59
+ """
60
+
61
+ _HTML = """
62
+ <div class="cb-toolbar">
63
+ <span class="cb-count" data-role="count">0 gates placed</span>
64
+ <button data-role="clear">Clear grid</button>
65
+ </div>
66
+ """
67
+
68
+ _JS = r"""
69
+ export default function(component) {
70
+ const { data, setStateValue, parentElement } = component;
71
+ const nQubits = Math.max(1, data.n_qubits || 1);
72
+ const nCols = Math.max(1, data.n_columns || 12);
73
+ const palette = data.palette || [];
74
+
75
+ const grid = Array.from({ length: nQubits }, () => Array(nCols).fill(null));
76
+ const cellEls = Array.from({ length: nQubits }, () => Array(nCols).fill(null));
77
+
78
+ const root = document.createElement('div');
79
+ root.className = 'cb-root';
80
+
81
+ const paletteEl = document.createElement('div');
82
+ paletteEl.className = 'cb-palette';
83
+ palette.forEach((g) => {
84
+ const chip = document.createElement('div');
85
+ chip.className = 'cb-chip';
86
+ chip.draggable = true;
87
+ chip.textContent = g.label;
88
+ chip.dataset.kind = g.kind;
89
+ chip.title = g.kind === 'single' ? `Porta a 1 qubit: ${g.label}`
90
+ : g.kind === 'control' ? 'Controllo -- trascina un target nella stessa colonna'
91
+ : g.kind === 'target' ? `Target -- forma C${g.gate.toUpperCase()} con un controllo nella stessa colonna`
92
+ : 'SWAP -- trascina due marcatori nella stessa colonna';
93
+ chip.addEventListener('dragstart', (e) => {
94
+ e.dataTransfer.setData('text/plain', g.id);
95
+ e.dataTransfer.effectAllowed = 'copy';
96
+ });
97
+ paletteEl.appendChild(chip);
98
+ });
99
+ root.appendChild(paletteEl);
100
+
101
+ const gridEl = document.createElement('div');
102
+ gridEl.className = 'cb-grid';
103
+ gridEl.style.gridTemplateColumns = `36px repeat(${nCols}, 40px)`;
104
+
105
+ for (let r = 0; r < nQubits; r++) {
106
+ const label = document.createElement('div');
107
+ label.className = 'cb-qlabel';
108
+ label.textContent = `q${r}`;
109
+ gridEl.appendChild(label);
110
+ for (let c = 0; c < nCols; c++) {
111
+ const cell = document.createElement('div');
112
+ cell.className = 'cb-cell';
113
+ cell.addEventListener('dragover', (e) => { e.preventDefault(); cell.classList.add('cb-over'); });
114
+ cell.addEventListener('dragleave', () => cell.classList.remove('cb-over'));
115
+ cell.addEventListener('drop', (e) => {
116
+ e.preventDefault();
117
+ cell.classList.remove('cb-over');
118
+ const gateId = e.dataTransfer.getData('text/plain');
119
+ const g = palette.find((x) => x.id === gateId);
120
+ if (!g) return;
121
+ grid[r][c] = { kind: g.kind, gate: g.gate };
122
+ renderCell(r, c);
123
+ emit();
124
+ });
125
+ cell.addEventListener('click', () => {
126
+ if (grid[r][c]) {
127
+ grid[r][c] = null;
128
+ renderCell(r, c);
129
+ emit();
130
+ }
131
+ });
132
+ gridEl.appendChild(cell);
133
+ cellEls[r][c] = cell;
134
+ }
135
+ }
136
+ root.appendChild(gridEl);
137
+ parentElement.appendChild(root);
138
+
139
+ const countEl = parentElement.querySelector('[data-role="count"]');
140
+ parentElement.querySelector('[data-role="clear"]').addEventListener('click', () => {
141
+ for (let r = 0; r < nQubits; r++) {
142
+ for (let c = 0; c < nCols; c++) {
143
+ grid[r][c] = null;
144
+ renderCell(r, c);
145
+ }
146
+ }
147
+ emit();
148
+ });
149
+
150
+ const SYMS = { control: '●', swap: '×' };
151
+
152
+ function renderCell(r, c) {
153
+ const cell = cellEls[r][c];
154
+ const v = grid[r][c];
155
+ if (!v) {
156
+ cell.textContent = '';
157
+ cell.classList.remove('cb-filled');
158
+ return;
159
+ }
160
+ cell.classList.add('cb-filled');
161
+ if (v.kind === 'single') cell.textContent = v.gate.toUpperCase();
162
+ else if (v.kind === 'target') cell.textContent = v.gate === 'x' ? '⊕' : v.gate.toUpperCase();
163
+ else cell.textContent = SYMS[v.kind] || '?';
164
+ }
165
+
166
+ function buildOps() {
167
+ const ops = [];
168
+ for (let c = 0; c < nCols; c++) {
169
+ let single = null;
170
+ const controls = [];
171
+ const targets = [];
172
+ const swaps = [];
173
+ for (let r = 0; r < nQubits; r++) {
174
+ const v = grid[r][c];
175
+ if (!v) continue;
176
+ if (v.kind === 'single') single = { row: r, gate: v.gate };
177
+ else if (v.kind === 'control') controls.push(r);
178
+ else if (v.kind === 'target') targets.push({ row: r, gate: v.gate });
179
+ else if (v.kind === 'swap') swaps.push(r);
180
+ }
181
+ const isEmpty = !single && controls.length === 0 && targets.length === 0 && swaps.length === 0;
182
+ if (isEmpty) continue;
183
+ if (single && controls.length === 0 && targets.length === 0 && swaps.length === 0) {
184
+ ops.push({ gate: single.gate, qubits: [single.row] });
185
+ } else if (!single && controls.length === 1 && targets.length === 1 && swaps.length === 0) {
186
+ ops.push({ gate: 'c' + targets[0].gate, qubits: [controls[0], targets[0].row] });
187
+ } else if (!single && controls.length === 0 && targets.length === 0 && swaps.length === 2) {
188
+ ops.push({ gate: 'swap', qubits: swaps });
189
+ }
190
+ // Any other combination in a column (e.g. a lone control with no
191
+ // target yet) is an incomplete gate -- shown on the grid but not
192
+ // emitted as an op until it's completed.
193
+ }
194
+ return ops;
195
+ }
196
+
197
+ function emit() {
198
+ const ops = buildOps();
199
+ countEl.textContent = `${ops.length} gates placed`;
200
+ setStateValue('circuit', ops);
201
+ }
202
+
203
+ emit();
204
+ }
205
+ """
206
+
207
+ _COMPONENT_NAME = "dense_evolution_circuit_builder"
208
+
209
+
210
+ def mount_circuit_builder(n_qubits: int, n_columns: int = 12, key: str = "circuit_builder"):
211
+ """Mount the drag-and-drop circuit builder and return its current ops
212
+ list (list[dict] with 'gate'/'qubits'), or [] if nothing is placed yet.
213
+
214
+ `key` should change whenever `n_qubits` changes (e.g. include it in the
215
+ caller's key) so Streamlit mounts a fresh grid instead of reusing a
216
+ stale one sized for a different qubit count.
217
+
218
+ Streamlit is imported here, not at module level: this component only
219
+ means anything inside a Streamlit app (app_dashboard.py) -- the
220
+ Composer kernel (local_site/app/server.py) imports dashboard_core too
221
+ but never calls this function, and shouldn't need streamlit installed
222
+ just to start.
223
+ """
224
+ import streamlit as st
225
+ builder = st.components.v2.component(_COMPONENT_NAME, html=_HTML, css=_CSS, js=_JS)
226
+ result = builder(
227
+ key=key,
228
+ data={"n_qubits": n_qubits, "n_columns": n_columns, "palette": GATE_PALETTE},
229
+ default={"circuit": []},
230
+ on_circuit_change=lambda: None,
231
+ )
232
+ return result.circuit if result.circuit is not None else []
@@ -0,0 +1,216 @@
1
+ """
2
+ Native circuit-diagram renderer -- pure matplotlib, never a Qiskit
3
+ QuantumCircuit. Replaces qiskit's circuit.draw(output='mpl') for exactly
4
+ the reason documented in dashboard_core/engine.py's module docstring:
5
+ qiskit.circuit.QuantumCircuit.__init__ itself segfaults (SIGSEGV) on
6
+ macOS CI runners, on the simplest possible call (QuantumCircuit(3) alone,
7
+ no QASM, no methods called on it) -- see tests/integration/test_interop.py::
8
+ TestQiskitInterop for the full reproduction story. There is no way to
9
+ keep using Qiskit's own drawer without constructing that object, so this
10
+ module draws directly from the same (name, *qubits[, param]) gate-tuple
11
+ format every other dense_evolution entry point already uses.
12
+
13
+ Gate vocabulary comes from dashboard_core._gate_tables (_ONE_QUBIT_STATIC /
14
+ _ONE_QUBIT_PARAM / _TWO_QUBIT_STATIC / _TWO_QUBIT_PARAM /
15
+ _THREE_QUBIT_STATIC), shared with qasm_library.py -- the same gate set
16
+ QASMParser can ever hand back, so nothing here can see a name it doesn't
17
+ recognize. Was a second, independently hand-copied set of these 5
18
+ constants until this shared import (prog.txt, dashboard_core audit point
19
+ 2): this copy was missing the 'u1'/'phase' aliases the other one had,
20
+ so a circuit using either of those two real gate names crashed this
21
+ function with "unsupported gate for native circuit diagram" -- a real
22
+ bug the duplication caused, not just a divergence risk.
23
+ """
24
+
25
+ import matplotlib.pyplot as plt
26
+ from matplotlib.patches import Circle, Rectangle
27
+
28
+ from ._gate_tables import (
29
+ _ONE_QUBIT_STATIC, _ONE_QUBIT_PARAM, _TWO_QUBIT_STATIC,
30
+ _TWO_QUBIT_PARAM, _THREE_QUBIT_STATIC,
31
+ )
32
+
33
+ __all__ = ['draw_native_circuit_diagram']
34
+
35
+ _BOX_LABEL = {
36
+ "h": "H", "x": "X", "y": "Y", "z": "Z", "s": "S", "sdg": "S†",
37
+ "t": "T", "tdg": "T†", "sx": "√X", "id": "I",
38
+ "rx": "RX", "ry": "RY", "rz": "RZ", "p": "P",
39
+ # u1/phase are the same gate as 'p' (see _gate_tables.py) -- without
40
+ # an entry here they'd still draw (that bug is fixed), just as the
41
+ # raw uppercased name ("U1"/"PHASE") instead of the same "P" label
42
+ # 'p' gets, inconsistent with dense_evolution/utils/drawing.py's own
43
+ # _LABELS, which already maps all three to "P" (prog.txt, dashboard_core
44
+ # audit point 5e).
45
+ "u1": "P", "phase": "P",
46
+ "cy": "Y", "cp": "P", "crz": "RZ",
47
+ }
48
+
49
+ _WIRE_COLOR = "black"
50
+ _BOX_FACE = "#6FA8DC"
51
+ _BOX_EDGE = "black"
52
+ _DOT_COLOR = "black"
53
+ _MEASURE_FACE = "#DDDDDD"
54
+
55
+
56
+ def _op_qubits(op, name):
57
+ """Every op tuple is (name, *qubits, *params) -- qubit count is fixed
58
+ per gate name (never variadic), so this just slices the right span."""
59
+ if name in _ONE_QUBIT_STATIC:
60
+ return [op[1]]
61
+ if name in _ONE_QUBIT_PARAM:
62
+ return [op[1]]
63
+ if name in _TWO_QUBIT_STATIC:
64
+ return [op[1], op[2]]
65
+ if name in _TWO_QUBIT_PARAM:
66
+ return [op[1], op[2]]
67
+ if name in _THREE_QUBIT_STATIC:
68
+ return [op[1], op[2], op[3]]
69
+ raise ValueError(f"unsupported gate for native circuit diagram: {name!r}")
70
+
71
+
72
+ def _schedule_columns(ops):
73
+ """Greedy left-packing, same rule every real circuit drawer uses: an
74
+ op goes in the first column strictly after the last column any of its
75
+ qubits was already used in. Returns a list of (op, column) pairs and
76
+ the total column count.
77
+
78
+ A multi-qubit op's vertical connector is drawn spanning every row
79
+ between its lowest and highest qubit index (see _draw_vertical_link
80
+ call sites below), not just the qubits it actually touches -- e.g.
81
+ cy(2, 0) draws a line through q1's row even though q1 isn't part of
82
+ the gate. So every qubit in that span, not just the op's own qubits,
83
+ must be marked used at this column too, or an unrelated single-qubit
84
+ gate on the skipped-over qubit can land in the same column and get
85
+ visually cut through by the connector line."""
86
+ next_free_col = {}
87
+ scheduled = []
88
+ for op in ops:
89
+ name = op[0]
90
+ qubits = _op_qubits(op, name)
91
+ span = range(min(qubits), max(qubits) + 1)
92
+ col = max((next_free_col.get(q, 0) for q in span), default=0)
93
+ scheduled.append((op, col))
94
+ for q in span:
95
+ next_free_col[q] = col + 1
96
+ n_columns = max((col for _, col in scheduled), default=-1) + 1
97
+ return scheduled, n_columns
98
+
99
+
100
+ def _draw_one_qubit_box(ax, x, y, label):
101
+ box = Rectangle((x - 0.3, y - 0.3), 0.6, 0.6, facecolor=_BOX_FACE, edgecolor=_BOX_EDGE, zorder=3)
102
+ ax.add_patch(box)
103
+ ax.text(x, y, label, ha="center", va="center", fontsize=10, fontweight="bold", zorder=4)
104
+
105
+
106
+ def _draw_control_dot(ax, x, y):
107
+ ax.add_patch(Circle((x, y), 0.08, facecolor=_DOT_COLOR, edgecolor=_DOT_COLOR, zorder=4))
108
+
109
+
110
+ def _draw_target_plus(ax, x, y):
111
+ ax.add_patch(Circle((x, y), 0.28, facecolor="white", edgecolor=_DOT_COLOR, zorder=3))
112
+ ax.plot([x - 0.28, x + 0.28], [y, y], color=_DOT_COLOR, linewidth=1.4, zorder=4)
113
+ ax.plot([x, x], [y - 0.28, y + 0.28], color=_DOT_COLOR, linewidth=1.4, zorder=4)
114
+
115
+
116
+ def _draw_swap_x(ax, x, y):
117
+ ax.plot([x - 0.15, x + 0.15], [y - 0.15, y + 0.15], color=_DOT_COLOR, linewidth=2, zorder=4)
118
+ ax.plot([x - 0.15, x + 0.15], [y + 0.15, y - 0.15], color=_DOT_COLOR, linewidth=2, zorder=4)
119
+
120
+
121
+ def _draw_vertical_link(ax, x, y0, y1):
122
+ ax.plot([x, x], [y0, y1], color=_DOT_COLOR, linewidth=1.4, zorder=2)
123
+
124
+
125
+ def _param_label(name, params):
126
+ base = _BOX_LABEL.get(name, name.upper())
127
+ if params:
128
+ return f"{base}({params[0]:.2f})"
129
+ return base
130
+
131
+
132
+ def draw_native_circuit_diagram(ops, n_qubits: int, add_measure: bool = True):
133
+ """Draws a circuit diagram figure directly from dense_evolution gate
134
+ tuples -- no Qiskit QuantumCircuit ever constructed. Qubit 0 is drawn
135
+ at the top, increasing downward, matching Qiskit's own drawer
136
+ convention so this is a drop-in replacement for
137
+ dashboard_core.visuals.draw_circuit_figure's panel.
138
+
139
+ Returns
140
+ -------
141
+ matplotlib.figure.Figure
142
+
143
+ Examples
144
+ --------
145
+ >>> from dashboard_core.circuit_diagram import draw_native_circuit_diagram
146
+ >>> fig = draw_native_circuit_diagram([('h', 0), ('cx', 0, 1)], n_qubits=2)
147
+ >>> type(fig).__name__
148
+ 'Figure'
149
+ >>> fig.savefig('bell_pair.png') # doctest: +SKIP
150
+ """
151
+ scheduled, n_columns = _schedule_columns(ops)
152
+ n_display_columns = n_columns + (1 if add_measure else 0)
153
+
154
+ fig_width = max(3.0, 1.1 * (n_display_columns + 1))
155
+ fig_height = max(2.0, 0.9 * n_qubits + 0.5)
156
+ fig, ax = plt.subplots(figsize=(fig_width, fig_height))
157
+
158
+ def row_y(qubit):
159
+ return n_qubits - 1 - qubit
160
+
161
+ wire_x_end = n_display_columns + 0.7
162
+ for q in range(n_qubits):
163
+ y = row_y(q)
164
+ ax.plot([0, wire_x_end], [y, y], color=_WIRE_COLOR, linewidth=1.2, zorder=1)
165
+ ax.text(-0.35, y, f"q{q}", ha="right", va="center", fontsize=10)
166
+
167
+ for op, col in scheduled:
168
+ name = op[0]
169
+ x = col + 1.0
170
+ qubits = _op_qubits(op, name)
171
+ rows = [row_y(q) for q in qubits]
172
+ params = list(op[1 + len(qubits):])
173
+
174
+ if name in _ONE_QUBIT_STATIC:
175
+ _draw_one_qubit_box(ax, x, rows[0], _BOX_LABEL.get(name, name.upper()))
176
+ elif name in _ONE_QUBIT_PARAM:
177
+ _draw_one_qubit_box(ax, x, rows[0], _param_label(name, params))
178
+ elif name == "swap":
179
+ _draw_vertical_link(ax, x, rows[0], rows[1])
180
+ _draw_swap_x(ax, x, rows[0])
181
+ _draw_swap_x(ax, x, rows[1])
182
+ elif name in _TWO_QUBIT_STATIC:
183
+ # cx/cy/cz: qubits[0] is control, qubits[1] is target.
184
+ _draw_vertical_link(ax, x, rows[0], rows[1])
185
+ _draw_control_dot(ax, x, rows[0])
186
+ if name == "cx":
187
+ _draw_target_plus(ax, x, rows[1])
188
+ else:
189
+ _draw_one_qubit_box(ax, x, rows[1], _BOX_LABEL.get(name, name.upper()))
190
+ elif name in _TWO_QUBIT_PARAM:
191
+ _draw_vertical_link(ax, x, rows[0], rows[1])
192
+ _draw_control_dot(ax, x, rows[0])
193
+ _draw_one_qubit_box(ax, x, rows[1], _param_label(name, params))
194
+ elif name in _THREE_QUBIT_STATIC:
195
+ # ccx (Toffoli): qubits[0], qubits[1] control, qubits[2] target.
196
+ _draw_vertical_link(ax, x, min(rows), max(rows))
197
+ _draw_control_dot(ax, x, rows[0])
198
+ _draw_control_dot(ax, x, rows[1])
199
+ _draw_target_plus(ax, x, rows[2])
200
+ else:
201
+ raise ValueError(f"unsupported gate for native circuit diagram: {name!r}")
202
+
203
+ if add_measure:
204
+ x = n_columns + 1.0
205
+ for q in range(n_qubits):
206
+ y = row_y(q)
207
+ box = Rectangle((x - 0.3, y - 0.3), 0.6, 0.6, facecolor=_MEASURE_FACE, edgecolor=_BOX_EDGE, zorder=3)
208
+ ax.add_patch(box)
209
+ ax.text(x, y, "M", ha="center", va="center", fontsize=9, zorder=4)
210
+
211
+ ax.set_xlim(-1.2, wire_x_end + 0.3)
212
+ ax.set_ylim(-0.7, n_qubits - 0.3)
213
+ ax.set_aspect("equal")
214
+ ax.axis("off")
215
+ fig.tight_layout()
216
+ return fig