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.
- dashboard_core/__init__.py +115 -0
- dashboard_core/_gate_tables.py +30 -0
- dashboard_core/band_structure.py +71 -0
- dashboard_core/circuit_builder_component.py +232 -0
- dashboard_core/circuit_diagram.py +216 -0
- dashboard_core/crypto_protocols.py +77 -0
- dashboard_core/engine.py +326 -0
- dashboard_core/graphical_builder.py +114 -0
- dashboard_core/hamiltonians.py +593 -0
- dashboard_core/mass_decomposition_tool.py +47 -0
- dashboard_core/mitigation.py +343 -0
- dashboard_core/native_hf_diagnostics.py +62 -0
- dashboard_core/noise_tools.py +125 -0
- dashboard_core/qasm_library.py +233 -0
- dashboard_core/qmmm.py +16 -0
- dashboard_core/rag_tool.py +45 -0
- dashboard_core/state_visuals.py +288 -0
- dashboard_core/system_limits.py +60 -0
- dashboard_core/vector_healing.py +102 -0
- dashboard_core/visuals.py +158 -0
- dashboard_core/vqe.py +533 -0
- dashboard_core/wormhole.py +580 -0
- dense_evolution/__init__.py +114 -0
- dense_evolution/autodiff.py +10 -0
- dense_evolution/backends/__init__.py +5 -0
- dense_evolution/backends/chunk/__init__.py +37 -0
- dense_evolution/backends/chunk/_engine_imports.py +57 -0
- dense_evolution/backends/chunk/circuit_chunker.py +55 -0
- dense_evolution/backends/chunk/core.py +432 -0
- dense_evolution/backends/chunk/disk_overflow.py +232 -0
- dense_evolution/backends/chunk/geometry.py +95 -0
- dense_evolution/backends/chunk/guard.py +190 -0
- dense_evolution/backends/chunk/kernels.py +531 -0
- dense_evolution/backends/mps.py +1569 -0
- dense_evolution/backends/statevector.py +616 -0
- dense_evolution/chunk.py +25 -0
- dense_evolution/circuits/__init__.py +20 -0
- dense_evolution/circuits/compiler.py +488 -0
- dense_evolution/circuits/diagram.py +94 -0
- dense_evolution/circuits/gates.py +91 -0
- dense_evolution/circuits/parser.py +632 -0
- dense_evolution/circuits/qft.py +66 -0
- dense_evolution/circuits/random_circuit.py +85 -0
- dense_evolution/circuits/registry.py +74 -0
- dense_evolution/circuits/topology.py +79 -0
- dense_evolution/circuits/trotter.py +265 -0
- dense_evolution/circuits/uccsd.py +275 -0
- dense_evolution/cli.py +199 -0
- dense_evolution/compiler.py +9 -0
- dense_evolution/config.py +49 -0
- dense_evolution/drawing.py +10 -0
- dense_evolution/entropy.py +9 -0
- dense_evolution/fermions.py +9 -0
- dense_evolution/gates.py +9 -0
- dense_evolution/harrison_tb.py +16 -0
- dense_evolution/healing.py +18 -0
- dense_evolution/interop/__init__.py +18 -0
- dense_evolution/interop/qiskit_pennylane.py +406 -0
- dense_evolution/measurement.py +10 -0
- dense_evolution/mitigation/__init__.py +54 -0
- dense_evolution/mitigation/healing.py +215 -0
- dense_evolution/mitigation/kl_divergence.py +93 -0
- dense_evolution/mitigation/magic_entropy.py +163 -0
- dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
- dense_evolution/mitigation/renyi.py +168 -0
- dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
- dense_evolution/mitigation/zne.py +990 -0
- dense_evolution/mps.py +9 -0
- dense_evolution/native_hf/__init__.py +26 -0
- dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
- dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
- dense_evolution/native_hf/assembly.py +304 -0
- dense_evolution/native_hf/basis.py +117 -0
- dense_evolution/native_hf/boys.py +35 -0
- dense_evolution/native_hf/bridge.py +112 -0
- dense_evolution/native_hf/cartesian.py +64 -0
- dense_evolution/native_hf/coulomb.py +196 -0
- dense_evolution/native_hf/differentiable.py +53 -0
- dense_evolution/native_hf/gaussians.py +79 -0
- dense_evolution/native_hf/kinetic.py +52 -0
- dense_evolution/native_hf/libcint_bridge.py +167 -0
- dense_evolution/native_hf/overlap.py +91 -0
- dense_evolution/native_hf/scf.py +404 -0
- dense_evolution/noise/__init__.py +79 -0
- dense_evolution/noise/coherent_attack.py +264 -0
- dense_evolution/noise/cosmic_ray.py +61 -0
- dense_evolution/noise/density_matrix_channels.py +78 -0
- dense_evolution/noise/differentiable.py +66 -0
- dense_evolution/noise/kraus/__init__.py +6 -0
- dense_evolution/noise/kraus/amplitude_damping.py +47 -0
- dense_evolution/noise/kraus/bitflip.py +22 -0
- dense_evolution/noise/kraus/combined.py +16 -0
- dense_evolution/noise/kraus/depolarizing.py +47 -0
- dense_evolution/noise/kraus/ideal.py +10 -0
- dense_evolution/noise/kraus/phaseflip.py +21 -0
- dense_evolution/noise/kraus_channels.py +285 -0
- dense_evolution/noise/oscillating.py +32 -0
- dense_evolution/noise/pink.py +80 -0
- dense_evolution/observables.py +11 -0
- dense_evolution/parser.py +9 -0
- dense_evolution/physics/__init__.py +27 -0
- dense_evolution/physics/entropy.py +161 -0
- dense_evolution/physics/fermions.py +322 -0
- dense_evolution/physics/observables.py +523 -0
- dense_evolution/physics/qec.py +1113 -0
- dense_evolution/physics/spectral.py +143 -0
- dense_evolution/physics/states.py +43 -0
- dense_evolution/protocols/__init__.py +27 -0
- dense_evolution/protocols/bb84.py +133 -0
- dense_evolution/protocols/di_qkd_ghz.py +199 -0
- dense_evolution/protocols/dicka_protocol2.py +124 -0
- dense_evolution/qec.py +20 -0
- dense_evolution/qft.py +9 -0
- dense_evolution/qmmm/__init__.py +13 -0
- dense_evolution/qmmm/ase_bridge.py +97 -0
- dense_evolution/qmmm/forces.py +388 -0
- dense_evolution/qmmm/propagation.py +80 -0
- dense_evolution/qmmm/region.py +137 -0
- dense_evolution/random_circuit.py +15 -0
- dense_evolution/registry.py +9 -0
- dense_evolution/simulator.py +10 -0
- dense_evolution/solvers/__init__.py +19 -0
- dense_evolution/solvers/autodiff.py +169 -0
- dense_evolution/solvers/harrison_tb.py +189 -0
- dense_evolution/solvers/vhd_tb.py +187 -0
- dense_evolution/states.py +9 -0
- dense_evolution/topology.py +9 -0
- dense_evolution/trotter.py +9 -0
- dense_evolution/utils/__init__.py +13 -0
- dense_evolution/utils/drawing.py +101 -0
- dense_evolution/utils/mass_decomposition.py +246 -0
- dense_evolution/utils/measurement.py +94 -0
- dense_evolution/vhd_tb.py +16 -0
- dense_evolution-8.3.0.dist-info/METADATA +366 -0
- dense_evolution-8.3.0.dist-info/RECORD +165 -0
- dense_evolution-8.3.0.dist-info/WHEEL +5 -0
- dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
- dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
- dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
- ia_utils/__init__.py +0 -0
- ia_utils/adversarial_vector_attack.py +196 -0
- ia_utils/rag.py +288 -0
- ia_utils/vector_healing.py +399 -0
- local_site/__init__.py +0 -0
- local_site/app/__init__.py +0 -0
- local_site/app/server.py +1009 -0
- mcp_server/__init__.py +0 -0
- mcp_server/client.py +324 -0
- mcp_server/config.py +32 -0
- mcp_server/models.py +347 -0
- mcp_server/molecules.py +71 -0
- mcp_server/server.py +119 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/chemistry_tools.py +225 -0
- mcp_server/tools/circuit_tools.py +83 -0
- mcp_server/tools/crypto_tools.py +66 -0
- mcp_server/tools/mitigation_tools.py +81 -0
- mcp_server/tools/noise_tools.py +60 -0
- mcp_server/tools/retrieval_tools.py +44 -0
- mcp_server/tools/system_tools.py +149 -0
- mcp_server/tools/wormhole_tools.py +142 -0
- mcp_server/utils/__init__.py +0 -0
- mcp_server/utils/cache.py +55 -0
- mcp_server/utils/images.py +67 -0
- 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
|
+
)
|