dense-evolution 8.3.0__py3-none-win_amd64.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 (165) 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.dll +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 +366 -0
  135. dense_evolution-8.3.0.dist-info/RECORD +165 -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/licenses/license.md +58 -0
  139. dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
  140. ia_utils/__init__.py +0 -0
  141. ia_utils/adversarial_vector_attack.py +196 -0
  142. ia_utils/rag.py +288 -0
  143. ia_utils/vector_healing.py +399 -0
  144. local_site/__init__.py +0 -0
  145. local_site/app/__init__.py +0 -0
  146. local_site/app/server.py +1009 -0
  147. mcp_server/__init__.py +0 -0
  148. mcp_server/client.py +324 -0
  149. mcp_server/config.py +32 -0
  150. mcp_server/models.py +347 -0
  151. mcp_server/molecules.py +71 -0
  152. mcp_server/server.py +119 -0
  153. mcp_server/tools/__init__.py +0 -0
  154. mcp_server/tools/chemistry_tools.py +225 -0
  155. mcp_server/tools/circuit_tools.py +83 -0
  156. mcp_server/tools/crypto_tools.py +66 -0
  157. mcp_server/tools/mitigation_tools.py +81 -0
  158. mcp_server/tools/noise_tools.py +60 -0
  159. mcp_server/tools/retrieval_tools.py +44 -0
  160. mcp_server/tools/system_tools.py +149 -0
  161. mcp_server/tools/wormhole_tools.py +142 -0
  162. mcp_server/utils/__init__.py +0 -0
  163. mcp_server/utils/cache.py +55 -0
  164. mcp_server/utils/images.py +67 -0
  165. mcp_server/utils/truncation.py +38 -0
@@ -0,0 +1,593 @@
1
+ """
2
+ Real molecular Hamiltonians, built on demand from actual atomic geometry
3
+ via PennyLane's qchem module (Hartree-Fock + Jordan-Wigner fermion-to-
4
+ qubit mapping, method='dhf' -- native to PennyLane, no PySCF/OpenFermion
5
+ dependency needed).
6
+
7
+ Ported from feature/streamlit-dashboard (git branch), where this was
8
+ built and verified against known values before the dashboard rebuild.
9
+ Only the real molecular catalog comes along here -- the old diagonal
10
+ "toy model" library and the VQE optimization loop stay out for now
11
+ (kept minimal on purpose, brought back separately if/when needed).
12
+
13
+ For elements PennyLane's own bundled STO-3G table doesn't cover (row 3+,
14
+ e.g. Silicon), this falls back to dense_evolution.native_hf -- a
15
+ from-scratch, jax-vmap-vectorized Hartree-Fock engine (Obara-Saika
16
+ integrals, Roothaan-Hall SCF) that sources basis-set data from
17
+ basis_set_exchange instead, so any element it has STO-3G parameters for
18
+ works. Only the Hartree-Fock/integral stage is native; the resulting
19
+ converged result is still handed to PennyLane's own fermionic_observable
20
+ + jordan_wigner for the qubit mapping (see native_hf/bridge.py), since
21
+ that stage is already fast and well-tested.
22
+ """
23
+
24
+ import numpy as np
25
+
26
+ import dense_evolution as de
27
+
28
+ __all__ = [
29
+ 'MOLECULE_CATALOG', 'build_molecular_hamiltonian',
30
+ 'get_compatible_molecules', 'get_all_molecules', 'get_molecule_n_qubits',
31
+ 'get_molecular_hamiltonian_matrix', 'ground_state_energy', 'ground_state_energy_sparse',
32
+ 'linear_chain_geometry', 'ring_geometry',
33
+ ]
34
+
35
+
36
+ def linear_chain_geometry(n_atoms: int, bond_length_angstrom: float):
37
+ """N atoms on a line, each bond_length_angstrom apart -- the real,
38
+ general shape behind every diatomic entry in the catalog (H2, HeH+,
39
+ LiH), extended to any atom count.
40
+
41
+ Examples
42
+ --------
43
+ >>> from dashboard_core.hamiltonians import linear_chain_geometry
44
+ >>> linear_chain_geometry(2, 0.7414)
45
+ array([[0. , 0. , 0. ],
46
+ [0. , 0. , 0.7414]])
47
+ """
48
+ if n_atoms < 1:
49
+ raise ValueError("linear_chain_geometry needs at least 1 atom")
50
+ return np.array([[0.0, 0.0, i * bond_length_angstrom] for i in range(n_atoms)])
51
+
52
+
53
+ def ring_geometry(n_atoms: int, bond_length_angstrom: float):
54
+ """N atoms on a regular polygon (equal bond_length_angstrom between
55
+ neighbors), circumradius R = bond_length / (2*sin(pi/n)) -- standard
56
+ regular-polygon geometry. At n_atoms=3 this is exactly an equilateral
57
+ triangle -- the same real D3h geometry H3+'s catalog entry uses, just
58
+ generalized to any ring size (still only meaningful up to whatever
59
+ qubit count this simulator's exact diagonalization / VQE range can
60
+ handle -- this function itself has no such limit, the caller does).
61
+
62
+ Examples
63
+ --------
64
+ >>> from dashboard_core.hamiltonians import ring_geometry
65
+ >>> ring_geometry(3, 0.8738).round(4)
66
+ array([[ 0.5045, 0. , 0. ],
67
+ [-0.2522, 0.4369, 0. ],
68
+ [-0.2522, -0.4369, 0. ]])
69
+ """
70
+ if n_atoms < 3:
71
+ raise ValueError("ring_geometry needs at least 3 atoms")
72
+ R = bond_length_angstrom / (2 * np.sin(np.pi / n_atoms))
73
+ angles = 2 * np.pi * np.arange(n_atoms) / n_atoms
74
+ return np.array([[R * np.cos(a), R * np.sin(a), 0.0] for a in angles])
75
+
76
+
77
+ def _triangular_h3_geometry(bond_length_angstrom: float):
78
+ """H3+'s real, published equilateral-triangle (D3h) ground-state
79
+ geometry -- the n_atoms=3 case of ring_geometry."""
80
+ return ring_geometry(3, bond_length_angstrom)
81
+
82
+
83
+ def _linear_two_atom_geometry(bond_length_angstrom: float):
84
+ return linear_chain_geometry(2, bond_length_angstrom)
85
+
86
+
87
+ # Bond lengths are real, published equilibrium geometries. LiH needs no
88
+ # active-space reduction (its full STO-3G Hamiltonian is already exactly
89
+ # 12 qubits). H2O's full STO-3G Hamiltonian is 14 qubits -- too large for
90
+ # dense diagonalization (2**14 x 2**14 complex128 is ~34 GB) and beyond
91
+ # the ~12-qubit range where this VQE ansatz still optimizes comfortably,
92
+ # so it's given a real frozen-core active space (freezing the O 1s core
93
+ # orbital: 10 electrons -> 8 active electrons, 7 orbitals -> 6 active
94
+ # orbitals = 12 qubits), a standard, physically honest quantum-chemistry
95
+ # approximation -- not a fabricated circuit.
96
+ MOLECULE_CATALOG = {
97
+ "H2 (Idrogeno) - R = 0.7414 A [equilibrio reale]": {
98
+ "symbols": ["H", "H"],
99
+ "geometry": lambda: _linear_two_atom_geometry(0.7414),
100
+ "charge": 0,
101
+ "active_electrons": None,
102
+ "active_orbitals": None,
103
+ },
104
+ "HeH+ (Idruro di Elio, catione) - R = 0.7743 A [equilibrio reale]": {
105
+ "symbols": ["He", "H"],
106
+ "geometry": lambda: _linear_two_atom_geometry(0.7743),
107
+ "charge": 1,
108
+ "active_electrons": None,
109
+ "active_orbitals": None,
110
+ },
111
+ "H3+ (Ione Triidrogeno) - triangolo equilatero D3h, R = 0.8738 A [equilibrio reale]": {
112
+ "symbols": ["H", "H", "H"],
113
+ "geometry": lambda: _triangular_h3_geometry(0.8738),
114
+ "charge": 1,
115
+ "active_electrons": None,
116
+ "active_orbitals": None,
117
+ },
118
+ "LiH (Idruro di Litio) - R = 1.5949 A [equilibrio reale]": {
119
+ "symbols": ["Li", "H"],
120
+ "geometry": lambda: _linear_two_atom_geometry(1.5949),
121
+ "charge": 0,
122
+ "active_electrons": None,
123
+ "active_orbitals": None,
124
+ },
125
+ "H2O (Acqua) - angolo 104.5 deg, R(O-H) = 0.9584 A [equilibrio reale, frozen-core O(1s)]": {
126
+ "symbols": ["O", "H", "H"],
127
+ "geometry": lambda: _water_geometry(0.9584, 104.5),
128
+ "charge": 0,
129
+ "active_electrons": 8,
130
+ "active_orbitals": 6,
131
+ },
132
+ # Real published equilibrium bond length (Balamurugan & Prasad,
133
+ # "Effect of hydrogen on ground state structures of small silicon
134
+ # clusters", arXiv:cond-mat/0108426). Si isn't in PennyLane's own
135
+ # bundled STO-3G table, so this routes through native_hf (see module
136
+ # docstring) -- verified against an independent reference
137
+ # (lowdanie/hartree-fock-solver) to 10 significant figures. Given only
138
+ # 4 active electrons/orbitals (freezing all 20 core electrons: Si's
139
+ # 1s,2s,2p x2 atoms), this active space is too small to reproduce
140
+ # 2.184 A as its own energy minimum (checked directly: a 10-point scan
141
+ # from 1.9-4.0 A found its minimum at the 1.9 A edge of the range, not
142
+ # an interior point) -- included anyway, with this caveat stated
143
+ # plainly, rather than silently picking a geometry that flatters the
144
+ # active-space choice.
145
+ "Si2 (Disilicio) - R = 2.184 A [equilibrio reale, active space minimo]": {
146
+ "symbols": ["Si", "Si"],
147
+ "geometry": lambda: _linear_two_atom_geometry(2.184),
148
+ "charge": 0,
149
+ "active_electrons": 4,
150
+ "active_orbitals": 4,
151
+ },
152
+ }
153
+
154
+ _pennylane_hamiltonian_cache = {}
155
+ _dense_hamiltonian_cache = {}
156
+
157
+
158
+ def _geometry_key(geometry):
159
+ """Hashable form of a geometry array for the three caches below
160
+ (prog.txt, dashboard_core audit point 5b): the one genuinely
161
+ fiddly, drift-prone part of each cache key -- rounding to 10
162
+ decimals so float noise doesn't spuriously miss a cache hit, then
163
+ converting to a tuple-of-tuples so it's hashable at all. Shared
164
+ here instead of each cache re-deriving its own copy of this exact
165
+ conversion, which had already drifted slightly (np.asarray(...)
166
+ wrapping present in one call site, absent in another, before this)."""
167
+ return tuple(map(tuple, np.asarray(geometry, dtype=float).round(10)))
168
+
169
+ # Single source of truth for this constant (prog.txt, dashboard_core audit
170
+ # point 1d) -- dashboard_core.qmmm imports it from here instead of
171
+ # redefining it (it already imports several other names from this module,
172
+ # so this isn't circular). Any real atomic radius is well above this; two
173
+ # nuclei closer than this in an *input* geometry (as opposed to qmmm's own
174
+ # post-MD-step divergence check) means malformed input, not physics.
175
+ MIN_NUCLEAR_DISTANCE_ANGSTROM = 0.3
176
+
177
+
178
+ def _validate_geometry(symbols, geometry):
179
+ """BUG FIX: build_molecular_hamiltonian had no input validation at
180
+ all -- a symbols/geometry length mismatch surfaced as a raw
181
+ IndexError deep inside PennyLane's own internals (verified directly:
182
+ 2 symbols + 1-row geometry -> 'IndexError: index 1 is out of bounds
183
+ for axis 0 with size 1', no indication the actual problem is the
184
+ caller's mismatched input), and non-finite coordinates (e.g. a NaN
185
+ from an upstream bug) were silently accepted and produced a NaN
186
+ Hamiltonian with no error at all (verified directly: NaN in a
187
+ geometry row -> np.any(np.isnan(H_dense)) is True, no exception).
188
+ Called once, at the real entry point every other function here
189
+ funnels through (_get_pennylane_hamiltonian), not duplicated at each
190
+ of its public callers.
191
+ """
192
+ geometry = np.asarray(geometry, dtype=np.float64)
193
+ if geometry.ndim != 2 or geometry.shape[1] != 3:
194
+ raise ValueError(
195
+ f"geometry must have shape (n_atoms, 3), got {geometry.shape}")
196
+ if len(symbols) != geometry.shape[0]:
197
+ raise ValueError(
198
+ f"{len(symbols)} symbols but {geometry.shape[0]} geometry rows -- "
199
+ f"these must match one-to-one")
200
+ if not np.all(np.isfinite(geometry)):
201
+ raise ValueError("geometry contains non-finite values (NaN/Inf)")
202
+ n_atoms = geometry.shape[0]
203
+ if n_atoms > 1:
204
+ diffs = geometry[:, None, :] - geometry[None, :, :]
205
+ dists = np.linalg.norm(diffs, axis=-1)
206
+ np.fill_diagonal(dists, np.inf)
207
+ i, j = np.unravel_index(np.argmin(dists), dists.shape)
208
+ if dists[i, j] < MIN_NUCLEAR_DISTANCE_ANGSTROM:
209
+ raise ValueError(
210
+ f"atoms {i} and {j} are {dists[i, j]:.4f} A apart, below the "
211
+ f"{MIN_NUCLEAR_DISTANCE_ANGSTROM} A physically-realistic floor "
212
+ f"-- check the geometry for a units mistake (e.g. Bohr instead "
213
+ f"of Angstrom) or a duplicated atom")
214
+ return geometry
215
+
216
+
217
+ _native_hamiltonian_cache = {}
218
+
219
+
220
+ def _get_native_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals):
221
+ """Same contract as _get_pennylane_hamiltonian (returns (H, n_qubits),
222
+ cached), but for elements outside PennyLane's bundled STO-3G table --
223
+ routes through dense_evolution.native_hf instead (see module
224
+ docstring). Jordan-Wigner only for now: native_hf/bridge.py calls
225
+ qml.jordan_wigner directly rather than taking a mapping parameter,
226
+ since every current caller (MOLECULE_CATALOG) already defaults to
227
+ jordan_wigner -- raising here instead of silently ignoring a
228
+ different requested mapping."""
229
+ if mapping != "jordan_wigner":
230
+ raise NotImplementedError(
231
+ f"native_hf fallback only supports mapping='jordan_wigner' (got {mapping!r}) "
232
+ f"-- native_hf/bridge.py calls qml.jordan_wigner directly, not a general mapper."
233
+ )
234
+
235
+ from basis_set_exchange.lut import element_Z_from_sym
236
+ from dense_evolution.native_hf.bridge import build_qubit_hamiltonian
237
+
238
+ geometry = _validate_geometry(symbols, geometry)
239
+
240
+ key = (tuple(symbols), _geometry_key(geometry), charge, active_electrons, active_orbitals)
241
+ if key in _native_hamiltonian_cache:
242
+ return _native_hamiltonian_cache[key]
243
+
244
+ atomic_numbers = [element_Z_from_sym(s) for s in symbols]
245
+ n_electrons = sum(atomic_numbers) - charge
246
+
247
+ H, n_qubits, _hf_result = build_qubit_hamiltonian(
248
+ atomic_numbers, geometry, n_electrons,
249
+ active_electrons=active_electrons, active_orbitals=active_orbitals,
250
+ )
251
+ _native_hamiltonian_cache[key] = (H, n_qubits)
252
+ return H, n_qubits
253
+
254
+
255
+ def _get_pennylane_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals):
256
+ """Real Hartree-Fock + fermion-to-qubit mapping (PennyLane qchem),
257
+ returning the PennyLane operator (not yet densified) and n_qubits.
258
+ Split out from build_molecular_hamiltonian so callers that only need
259
+ n_qubits (e.g. listing the catalog) don't pay for qml.matrix(H) --
260
+ building a dense matrix is the expensive/memory-heavy step, not the
261
+ HF+mapping step itself. Cached on its own so the catalog listing and
262
+ a later "compute ground state" / VQE call on the same molecule share
263
+ the one real HF computation instead of repeating it."""
264
+ import pennylane as qml
265
+ from pennylane.qchem.basis_data import STO3G
266
+
267
+ geometry = _validate_geometry(symbols, geometry)
268
+
269
+ key = (tuple(symbols), _geometry_key(geometry), charge, mapping, active_electrons, active_orbitals)
270
+ if key in _pennylane_hamiltonian_cache:
271
+ return _pennylane_hamiltonian_cache[key]
272
+
273
+ # PennyLane's own error for an unsupported element (raised deep inside
274
+ # molecular_hamiltonian, e.g. requesting Fe/Mo/Xe) is real and honest
275
+ # but written for someone already inside PennyLane's own codebase --
276
+ # "consider using load_data=True ... basis-set-exchange". Checked
277
+ # against STO3G's real key set (not a hardcoded/guessed list, so this
278
+ # never goes stale against a future PennyLane version) and re-raised
279
+ # with the concrete supported set and which of the caller's own
280
+ # symbols are the problem, before paying for anything else.
281
+ unsupported = sorted({s for s in symbols if s not in STO3G})
282
+ if unsupported:
283
+ raise ValueError(
284
+ f"No built-in STO-3G basis data for: {', '.join(unsupported)}. "
285
+ f"This pipeline (PennyLane qchem, method='dhf') only ships minimal-basis "
286
+ f"parameters for {', '.join(sorted(STO3G))} -- heavier elements (transition "
287
+ f"metals, anything past Ne) need an external basis-set source PennyLane "
288
+ f"doesn't bundle, not a dense_evolution limitation."
289
+ )
290
+
291
+ molecule = qml.qchem.Molecule(symbols, np.asarray(geometry), charge=charge, unit="angstrom")
292
+ H, n_qubits = qml.qchem.molecular_hamiltonian(
293
+ molecule, method="dhf", mapping=mapping,
294
+ active_electrons=active_electrons, active_orbitals=active_orbitals,
295
+ )
296
+ _pennylane_hamiltonian_cache[key] = (H, n_qubits)
297
+ return H, n_qubits
298
+
299
+
300
+ def _get_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals):
301
+ """Dispatches to PennyLane's own qchem pipeline when every symbol is
302
+ in its bundled STO-3G table, or to native_hf otherwise -- the single
303
+ real entry point every public function below funnels through, so
304
+ "does this molecule need the native fallback" is decided in exactly
305
+ one place."""
306
+ from pennylane.qchem.basis_data import STO3G
307
+
308
+ if all(s in STO3G for s in symbols):
309
+ return _get_pennylane_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals)
310
+ return _get_native_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals)
311
+
312
+
313
+ def _water_geometry(bond_length_angstrom: float, angle_degrees: float):
314
+ """Real equilibrium water geometry: O at the origin, both O-H bonds
315
+ at the given real bond length and real H-O-H bond angle (104.5 deg),
316
+ placed symmetrically about the z-axis in the xz-plane."""
317
+ half_angle = np.radians(angle_degrees) / 2.0
318
+ r = bond_length_angstrom
319
+ return np.array([
320
+ [0.0, 0.0, 0.0],
321
+ [r * np.sin(half_angle), 0.0, r * np.cos(half_angle)],
322
+ [-r * np.sin(half_angle), 0.0, r * np.cos(half_angle)],
323
+ ])
324
+
325
+
326
+ def _pennylane_hamiltonian_to_pauli_terms(H, n_qubits):
327
+ """Extracts (coeff, {qubit: 'X'|'Y'|'Z'}) terms from a PennyLane
328
+ Hamiltonian/Sum operator -- the same real Pauli decomposition
329
+ PennyLane itself uses internally, just handed over in the plain form
330
+ dense_evolution.pauli_hamiltonian_to_matrix accepts, so this
331
+ project's own engine builds the dense matrix instead of going through
332
+ qml.matrix(). Verified (see tests/unit/test_observables.py
333
+ and this session's own cross-checks) to reproduce qml.matrix(H)
334
+ exactly for H2/HeH+/H3+, not an approximation."""
335
+ coeffs, ops = H.terms()
336
+ terms = []
337
+ for coeff, op in zip(coeffs, ops):
338
+ pauli = {}
339
+ factors = op.operands if hasattr(op, 'operands') else [op]
340
+ for factor in factors:
341
+ wires = factor.wires
342
+ if not len(wires) or factor.name == 'Identity':
343
+ continue
344
+ pauli[int(wires[0])] = factor.name[-1] # 'PauliZ' -> 'Z', etc.
345
+ real_coeff = float(np.real(complex(coeff)))
346
+ terms.append((real_coeff, pauli))
347
+ return terms
348
+
349
+
350
+ def build_molecular_hamiltonian(symbols, geometry, charge: int = 0, mapping: str = "jordan_wigner",
351
+ active_electrons=None, active_orbitals=None):
352
+ """Runs real Hartree-Fock + fermion-to-qubit mapping (PennyLane
353
+ qchem) on the given geometry and returns (H_dense, n_qubits). The
354
+ eigenvalue spectrum (and therefore the ground-state energy) is
355
+ mapping-invariant -- Jordan-Wigner and Bravyi-Kitaev represent the
356
+ identical physical Hamiltonian in a different qubit basis -- so this
357
+ only changes which qubit operators appear, never the energies this
358
+ function's callers report. Cached: Hartree-Fock isn't free, and the
359
+ UI can re-request the same molecule repeatedly.
360
+
361
+ The dense matrix itself is built by this project's own
362
+ dense_evolution.pauli_hamiltonian_to_matrix from PennyLane's real
363
+ Pauli decomposition, not qml.matrix() -- verified to match qml.matrix
364
+ exactly (same ground-state energy, same matrix, atol=1e-8) for every
365
+ catalog molecule.
366
+
367
+ Examples
368
+ --------
369
+ >>> from dashboard_core.hamiltonians import build_molecular_hamiltonian, ground_state_energy
370
+ >>> H, n_qubits = build_molecular_hamiltonian(['H', 'H'], [[0, 0, 0], [0, 0, 0.7414]])
371
+ >>> n_qubits
372
+ 4
373
+ >>> round(ground_state_energy(H), 4)
374
+ -1.1373
375
+ """
376
+ H, n_qubits = _get_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals)
377
+
378
+ dense_key = (tuple(symbols), _geometry_key(geometry), charge, mapping, active_electrons, active_orbitals)
379
+ if dense_key in _dense_hamiltonian_cache:
380
+ return _dense_hamiltonian_cache[dense_key]
381
+
382
+ # H_dense is dim x dim (dim = 2**n_qubits), not just dim, and its only
383
+ # consumer (ground_state_energy) runs a full dense np.linalg.eigvalsh
384
+ # on it -- LAPACK's own eigh workspace needs comparable scratch memory
385
+ # on top of the matrix itself, and the geometry generators in the
386
+ # Composer's UI (linear_chain_geometry/ring_geometry) let a visitor
387
+ # build an arbitrarily long chain, with no smaller natural ceiling than
388
+ # whatever PennyLane's own Hartree-Fock step tolerates. Same real
389
+ # anti-OOM guard as dashboard_core.engine.run_circuit_from_qasm and
390
+ # mitigation.py's ZNE panels, sized for what this actually allocates
391
+ # (the x3 covers the matrix + its eigh scratch space + the cached copy
392
+ # this function stores in _dense_hamiltonian_cache).
393
+ dim = 2 ** n_qubits
394
+ required_mb = dim * dim * 16 / 1e6 * 3
395
+ de.chunk.SafeMemoryGuard().check_allocation(required_mb, context=f"{n_qubits}-qubit molecular Hamiltonian")
396
+
397
+ terms = _pennylane_hamiltonian_to_pauli_terms(H, n_qubits)
398
+ H_dense = de.pauli_hamiltonian_to_matrix(terms, n_qubits)
399
+ result = (H_dense, n_qubits)
400
+ _dense_hamiltonian_cache[dense_key] = result
401
+ return result
402
+
403
+
404
+ def get_molecule_n_qubits(symbols, geometry, charge=0, mapping="jordan_wigner",
405
+ active_electrons=None, active_orbitals=None):
406
+ """The qubit count a molecule's real Hamiltonian needs, without
407
+ paying for a dense matrix build -- cheap enough to call for every
408
+ catalog entry when just listing what's available."""
409
+ _, n_qubits = _get_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals)
410
+ return n_qubits
411
+
412
+
413
+ _STO3G_ORBITALS = {"H": 1, "He": 1}
414
+ _STO3G_ORBITALS.update({el: 5 for el in ("Li", "Be", "B", "C", "N", "O", "F", "Ne")})
415
+ _STO3G_ORBITALS.update({el: 9 for el in ("Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar")})
416
+
417
+
418
+ def _fast_n_qubits(spec, geometry, mapping):
419
+ """Qubit count from the STO-3G orbital count (two spin orbitals per
420
+ spatial orbital, or per active orbital when an active space is set),
421
+ without building the Hamiltonian. Falls back to the exact count for
422
+ elements outside the table."""
423
+ if spec.get("active_orbitals"):
424
+ return 2 * int(spec["active_orbitals"])
425
+ if all(s in _STO3G_ORBITALS for s in spec["symbols"]):
426
+ return 2 * sum(_STO3G_ORBITALS[s] for s in spec["symbols"])
427
+ return get_molecule_n_qubits(
428
+ spec["symbols"], geometry, spec["charge"], mapping=mapping,
429
+ active_electrons=spec.get("active_electrons"), active_orbitals=spec.get("active_orbitals"),
430
+ )
431
+
432
+
433
+ def get_all_molecules(catalog=None, mapping="jordan_wigner"):
434
+ """Every catalog molecule, each annotated with its real qubit count
435
+ under the given mapping -- unfiltered, so the UI can always show the
436
+ whole catalog and let the molecule choice drive the circuit's qubit
437
+ count (not the other way around).
438
+
439
+ Examples
440
+ --------
441
+ >>> from dashboard_core.hamiltonians import get_all_molecules
442
+ >>> molecules = get_all_molecules()
443
+ >>> molecules['H2 (Idrogeno) - R = 0.7414 A [equilibrio reale]']['n_qubits']
444
+ 4
445
+ """
446
+ catalog = catalog if catalog is not None else MOLECULE_CATALOG
447
+ out = {}
448
+ for name, spec in catalog.items():
449
+ geometry = spec["geometry"]() if callable(spec["geometry"]) else spec["geometry"]
450
+ n_qubits = _fast_n_qubits(spec, geometry, mapping)
451
+ out[name] = {
452
+ "symbols": spec["symbols"],
453
+ "geometry": np.asarray(geometry).tolist(),
454
+ "charge": spec["charge"],
455
+ "n_qubits": n_qubits,
456
+ }
457
+ return out
458
+
459
+
460
+ def get_compatible_molecules(n_qubits, catalog=None, mapping="jordan_wigner"):
461
+ """Filters MOLECULE_CATALOG down to molecules whose real Hamiltonian
462
+ needs exactly n_qubits. Kept for callers that want a qubit-filtered
463
+ view; the main catalog UI uses get_all_molecules instead so every
464
+ molecule is always visible."""
465
+ catalog = catalog if catalog is not None else MOLECULE_CATALOG
466
+ if n_qubits is None or n_qubits <= 0:
467
+ return {}
468
+ all_molecules = get_all_molecules(catalog, mapping=mapping)
469
+ return {name: catalog[name] for name, info in all_molecules.items() if info["n_qubits"] == n_qubits}
470
+
471
+
472
+ def get_molecular_hamiltonian_matrix(name, catalog=None, mapping="jordan_wigner"):
473
+ """Resolves a MOLECULE_CATALOG entry by name to its (cached) dense
474
+ Hermitian Hamiltonian matrix, under the given fermion-to-qubit
475
+ mapping (spectrum is identical either way, see build_molecular_hamiltonian)."""
476
+ catalog = catalog if catalog is not None else MOLECULE_CATALOG
477
+ spec = catalog[name]
478
+ geometry = spec["geometry"]() if callable(spec["geometry"]) else spec["geometry"]
479
+ H_dense, _ = build_molecular_hamiltonian(
480
+ spec["symbols"], geometry, spec["charge"], mapping=mapping,
481
+ active_electrons=spec.get("active_electrons"), active_orbitals=spec.get("active_orbitals"),
482
+ )
483
+ return H_dense
484
+
485
+
486
+ def ground_state_energy(H_dense) -> float:
487
+ """Exact ground-state energy via dense diagonalization -- a real,
488
+ checkable number (Hartree) for a Hamiltonian this small (H2/HeH+/H3+
489
+ all fit well within exact diagonalization), not an estimate."""
490
+ eigvals = np.linalg.eigvalsh(H_dense)
491
+ return float(eigvals.min())
492
+
493
+
494
+ def ground_state_energy_sparse(symbols, geometry, charge: int = 0, mapping: str = "jordan_wigner",
495
+ active_electrons=None, active_orbitals=None) -> float:
496
+ """Ground-state energy without ever materializing the dense
497
+ (2**n_qubits, 2**n_qubits) Hamiltonian matrix build_molecular_hamiltonian
498
+ + ground_state_energy require -- the fix for the PRIORITARIO gap
499
+ recorded in prog.txt (Sezione 4.1): that pair was blocked concretely
500
+ on Si2 (12 qubits, 805MB needed, SafeMemoryGuard refused it with only
501
+ 1.37GB free). This path uses scipy.sparse.linalg.eigsh (Lanczos, ARPACK)
502
+ against a LinearOperator wrapping dense_evolution.pauli_sum_matvec --
503
+ same real Pauli terms _get_hamiltonian/_pennylane_hamiltonian_to_pauli_terms
504
+ already produce, just never densified. Memory need drops from O(dim**2)
505
+ to O(dim * ncv) (ncv = ARPACK's Lanczos basis size, a small constant),
506
+ the same qualitative jump the wormhole/VQE code already relies on
507
+ elsewhere in this file for anything past ~12-14 qubits on modest
508
+ hardware.
509
+
510
+ Same caveat as build_molecular_hamiltonian: mapping choice (Jordan-
511
+ Wigner vs Bravyi-Kitaev) only changes the qubit basis, never the
512
+ energy spectrum this returns.
513
+
514
+ Not a drop-in replacement for ground_state_energy: that function
515
+ still exists unchanged for callers that already have a small dense
516
+ H_dense (e.g. an already-built/cached catalog matrix) and want the
517
+ exact, non-iterative answer -- this is the new, separate opt-in path
518
+ for systems too large to densify at all, not a behavior change to
519
+ the existing one.
520
+
521
+ Examples
522
+ --------
523
+ >>> from dashboard_core.hamiltonians import ground_state_energy_sparse, build_molecular_hamiltonian, ground_state_energy
524
+ >>> e_sparse = ground_state_energy_sparse(['H', 'H'], [[0, 0, 0], [0, 0, 0.7414]])
525
+ >>> H, _ = build_molecular_hamiltonian(['H', 'H'], [[0, 0, 0], [0, 0, 0.7414]])
526
+ >>> e_dense = ground_state_energy(H)
527
+ >>> abs(e_sparse - e_dense) < 1e-8 # matches the dense path without ever building H_dense
528
+ True
529
+ """
530
+ import scipy.sparse.linalg as sla
531
+
532
+ H, n_qubits = _get_hamiltonian(symbols, geometry, charge, mapping, active_electrons, active_orbitals)
533
+ terms = _pennylane_hamiltonian_to_pauli_terms(H, n_qubits)
534
+ dim = 2 ** n_qubits
535
+
536
+ if dim < 4:
537
+ # ARPACK's eigsh needs k < N-1; below 4 basis states there isn't
538
+ # room for even k=1 -- these sizes are exact-diagonalization
539
+ # territory anyway (ground_state_energy on a dense matrix this
540
+ # small costs nothing), not what this function exists for.
541
+ raise ValueError(
542
+ f"ground_state_energy_sparse needs at least 2 qubits (dim >= 4), got "
543
+ f"n_qubits={n_qubits} (dim={dim}) -- use build_molecular_hamiltonian + "
544
+ f"ground_state_energy for systems this small."
545
+ )
546
+
547
+ k = 1
548
+ ncv = min(dim - 1, max(2 * k + 1, 20))
549
+ # Lanczos needs ncv vectors of length dim in memory at once (complex128
550
+ # = 16 bytes), on top of a few fixed-size scratch vectors ARPACK keeps --
551
+ # the x3 mirrors build_molecular_hamiltonian's own safety margin, sized
552
+ # here for what eigsh actually allocates instead of a dense matrix.
553
+ required_mb = dim * ncv * 16 / 1e6 * 3
554
+ de.chunk.SafeMemoryGuard().check_allocation(
555
+ required_mb, context=f"{n_qubits}-qubit molecular Hamiltonian (sparse ground state)"
556
+ )
557
+
558
+ def matvec(v):
559
+ return de.pauli_sum_matvec(v, terms, n_qubits=n_qubits)
560
+
561
+ op = sla.LinearOperator(shape=(dim, dim), matvec=matvec, dtype=np.complex128)
562
+ eigvals = sla.eigsh(op, k=k, which='SA', ncv=ncv, return_eigenvectors=False)
563
+ return float(eigvals.min())
564
+
565
+
566
+ def mix_hamiltonians(H_a, H_b, weight_a: float = 0.5, weight_b: float = 0.5):
567
+ """Real weighted combination H_mix = weight_a*H_a + weight_b*H_b of
568
+ two molecular Hamiltonians acting on the same qubit space (same
569
+ electron/qubit count -- the only condition that makes the sum mean
570
+ anything, mirroring the old dashboard's own "mix molecules that
571
+ share an electron space" behavior). A real-weighted sum of two
572
+ Hermitian matrices is itself Hermitian, so H_mix is a real, valid
573
+ Hamiltonian with a real spectrum -- not a fabricated hybrid, just
574
+ linear algebra applied to two already-real operators.
575
+
576
+ Examples
577
+ --------
578
+ >>> import numpy as np
579
+ >>> from dashboard_core.hamiltonians import build_molecular_hamiltonian, mix_hamiltonians
580
+ >>> H, _ = build_molecular_hamiltonian(['H', 'H'], [[0, 0, 0], [0, 0, 0.7414]])
581
+ >>> H_mix = mix_hamiltonians(H, H, weight_a=0.5, weight_b=0.5)
582
+ >>> bool(np.allclose(H_mix, H)) # mixing H with itself in equal parts reproduces H
583
+ True
584
+ """
585
+ if H_a.shape != H_b.shape:
586
+ dim_a, dim_b = H_a.shape[0], H_b.shape[0]
587
+ raise ValueError(
588
+ f"cannot mix: different qubit spaces ({int(np.log2(dim_a))} vs {int(np.log2(dim_b))} qubits)"
589
+ )
590
+ H_mix = weight_a * H_a + weight_b * H_b
591
+ if not np.allclose(H_mix, H_mix.conj().T, atol=1e-9):
592
+ raise ValueError("mixed Hamiltonian is not Hermitian -- this should be unreachable")
593
+ return H_mix
@@ -0,0 +1,47 @@
1
+ """
2
+ Thin Composer-kernel wrapper around dense_evolution.utils.mass_decomposition
3
+ -- see docs/api/mass_decomposition.md for the real, validated CASMI26
4
+ provenance behind this. No new chemistry logic lives here.
5
+ """
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ from dense_evolution.utils.mass_decomposition import (
10
+ build_reachable_density_fft, build_reachable_masses, density_at_mass,
11
+ nearest_reachable_mass, parse_formula, rdbe,
12
+ )
13
+
14
+ __all__ = ['MassDecompositionResult', 'run_mass_decomposition']
15
+
16
+
17
+ @dataclass
18
+ class MassDecompositionResult:
19
+ formula_counts: dict
20
+ rdbe: float
21
+ nearest_reachable_mass: Optional[float]
22
+ density_at_target: float
23
+
24
+
25
+ def run_mass_decomposition(formula: str, target_mass: float, max_mass: Optional[float] = None) -> MassDecompositionResult:
26
+ """Check whether `target_mass` is a chemically valid, reachable
27
+ sub-formula mass of `formula` -- e.g. is a mass-spectrometry peak-pair
28
+ difference a real neutral loss this precursor's own atoms can produce.
29
+
30
+ `nearest_reachable_mass` is the exact answer (Minkowski-sum subset
31
+ sum, RDBE-filtered for chemical validity); `density_at_target` is the
32
+ same reachable-mass landscape via the FFT/convolution-theorem route,
33
+ a plausibility density rather than a 0/1 answer (higher means more,
34
+ better-supported combinations land there, not a probability).
35
+ `max_mass` defaults to `target_mass` itself if not given, since the
36
+ caller's own target is a natural bound on what needs to be searched.
37
+ """
38
+ counts = parse_formula(formula)
39
+ bound = max_mass if max_mass is not None else target_mass
40
+ reach = build_reachable_masses(counts, max_mass=bound)
41
+ mass_grid, density = build_reachable_density_fft(counts, max_mass=bound)
42
+ return MassDecompositionResult(
43
+ formula_counts=counts,
44
+ rdbe=rdbe(counts),
45
+ nearest_reachable_mass=nearest_reachable_mass(target_mass, reach),
46
+ density_at_target=density_at_mass(mass_grid, density, target_mass),
47
+ )