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
dense_evolution/mps.py ADDED
@@ -0,0 +1,9 @@
1
+ """Backward-compatibility shim -- the real implementation moved to
2
+ dense_evolution.backends.mps as part of the Phase 2 subpackage split (see
3
+ prog.txt). Kept so `from dense_evolution.mps import MPSSimulator` (used
4
+ by external consumers, e.g. Dense-Evolution-Discovery) keeps working
5
+ unchanged. Import from dense_evolution.backends.mps directly in new code.
6
+ """
7
+ from dense_evolution.backends.mps import MPSSimulator
8
+
9
+ __all__ = ["MPSSimulator"]
@@ -0,0 +1,26 @@
1
+ """
2
+ Native, JAX-vectorized ab-initio Hartree-Fock engine for Dense-Evolution.
3
+
4
+ Computes molecular one- and two-electron integrals (overlap, kinetic,
5
+ nuclear attraction, electron repulsion) via the Obara-Saika recursion
6
+ [Obara, Saika, J. Chem. Phys. 84, 3963 (1986)], batched with jax.lax.scan
7
+ so the whole shell-quartet loop compiles to a single XLA program instead
8
+ of looping in the Python interpreter.
9
+
10
+ This module exists because PennyLane's own differentiable Hartree-Fock
11
+ solver (qml.qchem, method="dhf") builds these same integrals through a
12
+ Python-level loop wrapped in its autograd-tracing numpy layer -- correct,
13
+ but roughly 100x slower than a properly vmap/jit-compiled implementation
14
+ for anything beyond a couple of atoms (profiled on Si2/STO-3G: 482 of
15
+ 483 total seconds were spent in that loop). The second-quantization and
16
+ Jordan-Wigner mapping steps that come after the integrals are already
17
+ fast in PennyLane, so this module only replaces the integral/SCF stage
18
+ and hands its output to qml.qchem.observable_hf.fermionic_observable.
19
+
20
+ Design and algorithm choice were informed by studying lowdanie/hartree-fock-solver
21
+ ("slaterform", Apache-2.0) as a reference for how to structure the
22
+ Obara-Saika recursion with jax.lax.scan, and by PennyLane's own white
23
+ paper (Delgado et al., "Differentiable quantum computational chemistry
24
+ with PennyLane", arXiv:2111.09967). No source code from either project
25
+ is copied here.
26
+ """
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2012, Qiming Sun
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,304 @@
1
+ """Assembling full molecular AO integral matrices from contracted shells.
2
+
3
+ Each pair (or quartet, for repulsion) of shells contributes a block to
4
+ the overall S/T/V matrices (or ERI tensor). We loop over shell
5
+ pairs/quartets in plain Python -- for a minimal basis like STO-3G there
6
+ are only a handful of shells per atom (Si2/STO-3G: 6 shells total), so
7
+ this loop is cheap. The sum over primitives *within* a shell pair/quartet
8
+ and the slicing down to physical Cartesian components both happen
9
+ inside a single jax.jit-compiled call per shell pair/quartet: doing
10
+ that slicing eagerly (one jnp.ndarray.__getitem__ per primitive
11
+ combination) turned out to cost as much per-call dispatch overhead as
12
+ PennyLane's own scalar-at-a-time autograd loop, defeating the purpose,
13
+ so everything from "loop over primitives" to "slice out the physical
14
+ components" is fused into one compiled program per shell pair/quartet
15
+ and only that program's *output* touches eager Python.
16
+ """
17
+
18
+ import functools
19
+
20
+ import jax
21
+ import jax.numpy as jnp
22
+ import numpy as np
23
+
24
+ from dense_evolution.native_hf.basis import ContractedShell, n_cartesian_functions
25
+ from dense_evolution.native_hf.cartesian import cartesian_powers, cartesian_normalization_ratios
26
+ from dense_evolution.native_hf.gaussians import GaussianShell3D
27
+ from dense_evolution.native_hf.overlap import overlap_3d
28
+ from dense_evolution.native_hf.kinetic import kinetic_3d
29
+ from dense_evolution.native_hf.coulomb import nuclear_attraction, electron_repulsion
30
+
31
+
32
+ @functools.partial(jax.jit, static_argnames=("degree_a", "degree_b", "primitive_fn"))
33
+ def _pair_block(exponents_a, coeffs_a, center_a, degree_a, exponents_b, coeffs_b, center_b, degree_b, primitive_fn, extra=()):
34
+ """extra: additional *traced* arrays (e.g. nuclear charges/positions)
35
+ forwarded to primitive_fn unchanged -- traced (not static) so that
36
+ calling this with a different molecule geometry reuses the same
37
+ compiled program instead of retracing. primitive_fn must itself be a
38
+ stable, module-level (not freshly-created-per-call) callable, since
39
+ it IS a static jit key."""
40
+ powers_a = jnp.asarray(cartesian_powers(degree_a))
41
+ powers_b = jnp.asarray(cartesian_powers(degree_b))
42
+ ratio_a = jnp.asarray(cartesian_normalization_ratios(degree_a))
43
+ ratio_b = jnp.asarray(cartesian_normalization_ratios(degree_b))
44
+
45
+ def one_primitive_pair(ea, ca, eb, cb):
46
+ ga = GaussianShell3D(degree=degree_a, exponent=ea, center=center_a)
47
+ gb = GaussianShell3D(degree=degree_b, exponent=eb, center=center_b)
48
+ full = primitive_fn(ga, gb, *extra)
49
+ sliced = full[powers_a[:, 0], powers_a[:, 1], powers_a[:, 2]][:, powers_b[:, 0], powers_b[:, 1], powers_b[:, 2]]
50
+ return ca * cb * sliced
51
+
52
+ batched = jax.vmap(
53
+ jax.vmap(one_primitive_pair, in_axes=(None, None, 0, 0)),
54
+ in_axes=(0, 0, None, None),
55
+ )(exponents_a, coeffs_a, exponents_b, coeffs_b)
56
+ return ratio_a[:, None] * ratio_b[None, :] * jnp.sum(batched, axis=(0, 1))
57
+
58
+
59
+ @functools.partial(jax.jit, static_argnames=("degrees", "primitive_fn"))
60
+ def _quartet_block(exponents, coeffs, centers, degrees, primitive_fn):
61
+ """exponents/coeffs: tuples of 4 arrays (one per shell), centers: tuple
62
+ of 4 (3,) arrays, degrees: tuple of 4 static ints."""
63
+ powers = [jnp.asarray(cartesian_powers(d)) for d in degrees]
64
+ ratios = [jnp.asarray(cartesian_normalization_ratios(d)) for d in degrees]
65
+
66
+ def one_quartet(ea, ca, eb, cb, ec, cc, ed, cd):
67
+ ga = GaussianShell3D(degree=degrees[0], exponent=ea, center=centers[0])
68
+ gb = GaussianShell3D(degree=degrees[1], exponent=eb, center=centers[1])
69
+ gc = GaussianShell3D(degree=degrees[2], exponent=ec, center=centers[2])
70
+ gd = GaussianShell3D(degree=degrees[3], exponent=ed, center=centers[3])
71
+ full = primitive_fn(ga, gb, gc, gd)
72
+ sliced = full[powers[0][:, 0], powers[0][:, 1], powers[0][:, 2]]
73
+ sliced = sliced[:, powers[1][:, 0], powers[1][:, 1], powers[1][:, 2]]
74
+ sliced = sliced[:, :, powers[2][:, 0], powers[2][:, 1], powers[2][:, 2]]
75
+ sliced = sliced[:, :, :, powers[3][:, 0], powers[3][:, 1], powers[3][:, 2]]
76
+ return ca * cb * cc * cd * sliced
77
+
78
+ vmapped = one_quartet
79
+ # vmap outward-in: d, c, b, a -- each adds a leading batch axis.
80
+ vmapped = jax.vmap(vmapped, in_axes=(None, None, None, None, None, None, 0, 0))
81
+ vmapped = jax.vmap(vmapped, in_axes=(None, None, None, None, 0, 0, None, None))
82
+ vmapped = jax.vmap(vmapped, in_axes=(None, None, 0, 0, None, None, None, None))
83
+ vmapped = jax.vmap(vmapped, in_axes=(0, 0, None, None, None, None, None, None))
84
+
85
+ ea, ca, eb, cb, ec, cc, ed, cd = (
86
+ exponents[0], coeffs[0], exponents[1], coeffs[1],
87
+ exponents[2], coeffs[2], exponents[3], coeffs[3],
88
+ )
89
+ batched = vmapped(ea, ca, eb, cb, ec, cc, ed, cd)
90
+ summed = jnp.sum(batched, axis=(0, 1, 2, 3))
91
+ ratio_outer = ratios[0][:, None, None, None] * ratios[1][None, :, None, None] \
92
+ * ratios[2][None, None, :, None] * ratios[3][None, None, None, :]
93
+ return ratio_outer * summed
94
+
95
+
96
+ _QUARTET_SYMMETRY_PERMS = (
97
+ (0, 1, 2, 3), (1, 0, 2, 3), (0, 1, 3, 2), (1, 0, 3, 2),
98
+ (2, 3, 0, 1), (2, 3, 1, 0), (3, 2, 0, 1), (3, 2, 1, 0),
99
+ )
100
+
101
+
102
+ def _canonical_quartet_perm(degrees: tuple) -> tuple:
103
+ """The electron-repulsion integral (ab|cd) is invariant under swapping
104
+ a<->b, swapping c<->d, and swapping the pair (a,b)<->(c,d) -- the same
105
+ 8-element symmetry build_repulsion_tensor already uses to store one
106
+ computed block into 8 tensor locations via transpose. Applied here to
107
+ the OTHER end: _quartet_block's jax.jit cache keys on the exact
108
+ (a,b,c,d)-ordered `degrees` tuple, so two quartets that are physically
109
+ the same integral under one of these 8 relabelings still trigger two
110
+ separate compilations if their degrees arrive in different orders.
111
+ Picking the lexicographically largest of the 8 relabelings as a fixed
112
+ canonical form (independent of which one happens to come in) collapses
113
+ that duplication -- measured on Ne/6-31G*: 35 distinct compiled
114
+ signatures down to fewer, see native_hf_eri_profile_ne_631gstar.py in
115
+ Dense-Evolution-Discovery."""
116
+ best_perm = _QUARTET_SYMMETRY_PERMS[0]
117
+ best_key = tuple(degrees[i] for i in best_perm)
118
+ for perm in _QUARTET_SYMMETRY_PERMS[1:]:
119
+ key = tuple(degrees[i] for i in perm)
120
+ if key > best_key:
121
+ best_key = key
122
+ best_perm = perm
123
+ return best_perm
124
+
125
+
126
+ def _invert_perm(perm: tuple) -> tuple:
127
+ inv = [0, 0, 0, 0]
128
+ for i, p in enumerate(perm):
129
+ inv[p] = i
130
+ return tuple(inv)
131
+
132
+
133
+ def _canonical_quartet_block(exponents, coeffs, centers, degrees, primitive_fn):
134
+ perm = _canonical_quartet_perm(degrees)
135
+ if perm == (0, 1, 2, 3):
136
+ return _quartet_block(exponents, coeffs, centers, degrees, primitive_fn)
137
+ p_exponents = tuple(exponents[i] for i in perm)
138
+ p_coeffs = tuple(coeffs[i] for i in perm)
139
+ p_centers = tuple(centers[i] for i in perm)
140
+ p_degrees = tuple(degrees[i] for i in perm)
141
+ block = _quartet_block(p_exponents, p_coeffs, p_centers, p_degrees, primitive_fn)
142
+ return jnp.transpose(block, _invert_perm(perm))
143
+
144
+
145
+ def _shell_offsets(shells: list[ContractedShell]) -> list[int]:
146
+ offsets, running = [], 0
147
+ for s in shells:
148
+ offsets.append(running)
149
+ running += len(cartesian_powers(s.degree))
150
+ return offsets
151
+
152
+
153
+ def _overlap_primitive(ga, gb):
154
+ return overlap_3d(ga, gb)
155
+
156
+
157
+ def build_overlap_matrix(shells: list[ContractedShell]) -> jax.Array:
158
+ return _build_two_index(shells, _overlap_primitive)
159
+
160
+
161
+ def _core_hamiltonian_primitive(ga, gb, charges, positions):
162
+ T = kinetic_3d(ga, gb)
163
+ per_nucleus = jax.vmap(lambda z, r: -z * nuclear_attraction(ga, gb, r))(charges, positions)
164
+ V = jnp.sum(per_nucleus, axis=0)
165
+ return -0.5 * T + V
166
+
167
+
168
+ def build_core_hamiltonian(shells: list[ContractedShell], nuclear_charges: list[float], nuclear_positions: np.ndarray) -> jax.Array:
169
+ charges = jnp.asarray(nuclear_charges, dtype=jnp.float64)
170
+ positions = jnp.asarray(nuclear_positions, dtype=jnp.float64)
171
+ return _build_two_index(shells, _core_hamiltonian_primitive, extra=(charges, positions))
172
+
173
+
174
+ def _build_two_index(shells: list[ContractedShell], primitive_fn, extra=()) -> jax.Array:
175
+ n = n_cartesian_functions(shells)
176
+ offsets = _shell_offsets(shells)
177
+ M = jnp.zeros((n, n))
178
+ for i, sa in enumerate(shells):
179
+ for j, sb in enumerate(shells):
180
+ block = _pair_block(
181
+ sa.exponents, sa.coefficients, sa.center, sa.degree,
182
+ sb.exponents, sb.coefficients, sb.center, sb.degree,
183
+ primitive_fn, extra=extra,
184
+ )
185
+ oi, oj = offsets[i], offsets[j]
186
+ M = M.at[oi : oi + block.shape[0], oj : oj + block.shape[1]].set(block)
187
+ return M
188
+
189
+
190
+ def _pad_primitives(exponents: jax.Array, coeffs: jax.Array, target_n: int) -> tuple:
191
+ """Pad a shell's primitives up to target_n by repeating its last exponent
192
+ with coefficient 0 -- contributes exactly zero to the primitive sum (not
193
+ an approximation), so every shell in a molecule can share one primitive
194
+ count and _quartet_block's JIT cache stops keying on it. Unlike padding
195
+ the angular-momentum degree (measured as a regression: it forces cheap,
196
+ many-primitive low-degree shells through the expensive high-degree
197
+ recursion), padding primitive count doesn't touch the recursion at all --
198
+ it only lengthens an already-cheap vmap axis (measured warm-cache cost:
199
+ 0.0001-0.005s regardless of primitive count)."""
200
+ n = exponents.shape[0]
201
+ if n == target_n:
202
+ return exponents, coeffs
203
+ pad_n = target_n - n
204
+ exponents = jnp.concatenate([exponents, jnp.full((pad_n,), exponents[-1])])
205
+ coeffs = jnp.concatenate([coeffs, jnp.zeros(pad_n)])
206
+ return exponents, coeffs
207
+
208
+
209
+ def _schwarz_bound(sa: ContractedShell, sb: ContractedShell, max_primitives: int) -> float:
210
+ ea, ca = _pad_primitives(sa.exponents, sa.coefficients, max_primitives)
211
+ eb, cb = _pad_primitives(sb.exponents, sb.coefficients, max_primitives)
212
+ block = np.array(
213
+ _canonical_quartet_block(
214
+ (ea, eb, ea, eb),
215
+ (ca, cb, ca, cb),
216
+ (sa.center, sb.center, sa.center, sb.center),
217
+ (sa.degree, sb.degree, sa.degree, sb.degree),
218
+ electron_repulsion,
219
+ )
220
+ )
221
+ na, nb = block.shape[0], block.shape[1]
222
+ diag = np.array([[block[p, q, p, q] for q in range(nb)] for p in range(na)])
223
+ return float(np.sqrt(np.max(np.abs(diag))))
224
+
225
+
226
+ def _shell_pair_schwarz_bounds(shells: list[ContractedShell], max_primitives: int) -> dict:
227
+ bounds = {}
228
+ for i, sa in enumerate(shells):
229
+ for j in range(i + 1):
230
+ sb = shells[j]
231
+ q = _schwarz_bound(sa, sb, max_primitives)
232
+ bounds[(i, j)] = q
233
+ bounds[(j, i)] = q
234
+ return bounds
235
+
236
+
237
+ def quartet_screening_indices(shells: list[ContractedShell], screening_tol: float = 1e-12) -> list[tuple[int, int, int, int]]:
238
+ """The (i,j,k,l) shell-index quartets build_repulsion_tensor would
239
+ compute, decided from Schwarz bounds -- split out so a caller
240
+ differentiating build_repulsion_tensor w.r.t. nuclear positions can
241
+ compute this ONCE from a concrete (non-traced) geometry and reuse it.
242
+
243
+ Schwarz screening is a discrete, structural decision (which terms
244
+ exist in the sum at all), not a smooth function of geometry -- Python
245
+ control flow like `if bound < tol` cannot run on a traced value, the
246
+ same reason any code with data-dependent sparsity can't be
247
+ differentiated as a black box. Real differentiable quantum chemistry
248
+ codes handle this the same way: freeze the sparsity pattern from a
249
+ concrete evaluation, then differentiate a numerical re-evaluation
250
+ that reuses that fixed pattern. Passing this list's output back into
251
+ build_repulsion_tensor's `quartet_indices` argument is that reuse."""
252
+ max_primitives = max(s.exponents.shape[0] for s in shells)
253
+ schwarz = _shell_pair_schwarz_bounds(shells, max_primitives)
254
+ indices = []
255
+ for i in range(len(shells)):
256
+ for j in range(i + 1):
257
+ ij_index = i * (i + 1) // 2 + j
258
+ for k in range(len(shells)):
259
+ for l in range(k + 1):
260
+ kl_index = k * (k + 1) // 2 + l
261
+ if ij_index < kl_index:
262
+ continue
263
+ if schwarz[(i, j)] * schwarz[(k, l)] < screening_tol:
264
+ continue
265
+ indices.append((i, j, k, l))
266
+ return indices
267
+
268
+
269
+ def build_repulsion_tensor(
270
+ shells: list[ContractedShell], screening_tol: float = 1e-12,
271
+ quartet_indices: list[tuple[int, int, int, int]] = None,
272
+ ) -> jax.Array:
273
+ n = n_cartesian_functions(shells)
274
+ offsets = _shell_offsets(shells)
275
+ V = jnp.zeros((n, n, n, n))
276
+ max_primitives = max(s.exponents.shape[0] for s in shells)
277
+ if quartet_indices is None:
278
+ quartet_indices = quartet_screening_indices(shells, screening_tol)
279
+ for i, j, k, l in quartet_indices:
280
+ sa, sb, sc, sd = shells[i], shells[j], shells[k], shells[l]
281
+ ea, ca = _pad_primitives(sa.exponents, sa.coefficients, max_primitives)
282
+ eb, cb = _pad_primitives(sb.exponents, sb.coefficients, max_primitives)
283
+ ec, cc = _pad_primitives(sc.exponents, sc.coefficients, max_primitives)
284
+ ed, cd = _pad_primitives(sd.exponents, sd.coefficients, max_primitives)
285
+ block = _canonical_quartet_block(
286
+ (ea, eb, ec, ed),
287
+ (ca, cb, cc, cd),
288
+ (sa.center, sb.center, sc.center, sd.center),
289
+ (sa.degree, sb.degree, sc.degree, sd.degree),
290
+ electron_repulsion,
291
+ )
292
+ oi, oj, ok, ol = offsets[i], offsets[j], offsets[k], offsets[l]
293
+ for pi, pj, pk, pl, b in (
294
+ (oi, oj, ok, ol, block),
295
+ (oj, oi, ok, ol, block.transpose(1, 0, 2, 3)),
296
+ (oi, oj, ol, ok, block.transpose(0, 1, 3, 2)),
297
+ (oj, oi, ol, ok, block.transpose(1, 0, 3, 2)),
298
+ (ok, ol, oi, oj, block.transpose(2, 3, 0, 1)),
299
+ (ol, ok, oi, oj, block.transpose(3, 2, 0, 1)),
300
+ (ok, ol, oj, oi, block.transpose(2, 3, 1, 0)),
301
+ (ol, ok, oj, oi, block.transpose(3, 2, 1, 0)),
302
+ ):
303
+ V = V.at[pi : pi + b.shape[0], pj : pj + b.shape[1], pk : pk + b.shape[2], pl : pl + b.shape[3]].set(b)
304
+ return V
@@ -0,0 +1,117 @@
1
+ """Loading contracted basis-set shells for a molecule.
2
+
3
+ Basis-set parameters (exponents, contraction coefficients) are fetched
4
+ from the Basis Set Exchange (the `basis_set_exchange` PyPI package,
5
+ data-only, BSD-licensed -- https://www.basissetexchange.org), so this
6
+ module works for any element the basis has data for, not just the
7
+ handful PennyLane's own bundled STO-3G table covers (which stops at
8
+ Ne and is why Silicon needed a hand-written patch earlier in this
9
+ project).
10
+
11
+ The coefficients BSE reports are for *unnormalized* primitives, so each
12
+ primitive's contraction coefficient must be rescaled by its own L2 norm
13
+ before it can be summed against another shell's primitives. We get that
14
+ norm for free by reusing our own overlap_3d on the primitive against
15
+ itself: N = 1/sqrt(<primitive|primitive>).
16
+ """
17
+
18
+ import dataclasses
19
+
20
+ import basis_set_exchange as bse
21
+ import jax
22
+ import jax.numpy as jnp
23
+ import numpy as np
24
+
25
+ from dense_evolution.config import ensure_x64
26
+ from dense_evolution.native_hf.cartesian import cartesian_powers
27
+ from dense_evolution.native_hf.gaussians import GaussianShell3D
28
+ from dense_evolution.native_hf.overlap import overlap_3d
29
+
30
+
31
+ @dataclasses.dataclass(frozen=True)
32
+ class ContractedShell:
33
+ """One angular-momentum shell (s, p, ...) of a contracted GTO."""
34
+
35
+ atom_index: int
36
+ center: jax.Array # shape (3,), atomic units (Bohr)
37
+ degree: int # 0 for s, 1 for p, ...
38
+ exponents: jax.Array # shape (K,)
39
+ coefficients: jax.Array # shape (K,), already primitive-normalized
40
+
41
+
42
+ def _primitive_norm(exponent: jax.Array, degree: int) -> jax.Array:
43
+ """1/sqrt(self-overlap) for the (degree,0,0) Cartesian component,
44
+ used as the shared reference normalization for every Cartesian
45
+ component of this shell. For degree<=1 that's already each
46
+ component's own norm (px, py, pz are equivalent by symmetry). For
47
+ degree>=2 it is not (dxx and dxy have different norms) --
48
+ cartesian_normalization_ratios supplies the per-component correction
49
+ relative to this same (degree,0,0) reference, applied at assembly
50
+ time in assembly.py rather than here."""
51
+ g = GaussianShell3D(degree=degree, exponent=exponent, center=jnp.zeros(3))
52
+ self_overlap = overlap_3d(g, g)[degree, 0, 0, degree, 0, 0]
53
+ return 1.0 / jnp.sqrt(self_overlap)
54
+
55
+
56
+ def _contracted_shell_from_bse(shell: dict, center: jax.Array, atom_index: int) -> list[ContractedShell]:
57
+ """BSE groups shells like STO-3G's "SP" as one entry with two rows of
58
+ coefficients (one for the S part, one for the P part) sharing the
59
+ same exponents; we split those back into separate ContractedShells
60
+ since our integral code handles one angular momentum at a time."""
61
+ angular_momenta = shell["angular_momentum"]
62
+ exponents = jnp.array([float(e) for e in shell["exponents"]])
63
+
64
+ shells = []
65
+ for row, degree in enumerate(angular_momenta):
66
+ coeffs = jnp.array([float(c) for c in shell["coefficients"][row]])
67
+ norms = jax.vmap(_primitive_norm, in_axes=(0, None))(exponents, degree)
68
+ shells.append(
69
+ ContractedShell(
70
+ atom_index=atom_index,
71
+ center=center,
72
+ degree=degree,
73
+ exponents=exponents,
74
+ coefficients=coeffs * norms,
75
+ )
76
+ )
77
+ return shells
78
+
79
+
80
+ def load_element_shells(basis_name: str, atomic_number: int, center: jax.Array, atom_index: int) -> list[ContractedShell]:
81
+ data = bse.get_basis(basis_name, elements=[atomic_number])
82
+ electron_shells = data["elements"][str(atomic_number)]["electron_shells"]
83
+
84
+ if any(s["function_type"] not in ("gto", "gto_cartesian") for s in electron_shells):
85
+ raise NotImplementedError("Only Cartesian Gaussian basis sets are currently supported.")
86
+
87
+ max_degree = max(
88
+ degree for shell in electron_shells for degree in shell["angular_momentum"]
89
+ )
90
+ if max_degree > 2:
91
+ from basis_set_exchange.lut import element_sym_from_Z
92
+
93
+ sym = element_sym_from_Z(atomic_number).capitalize()
94
+ raise NotImplementedError(
95
+ f"native_hf's overlap/kinetic/Coulomb integrals only implement s, p and d "
96
+ f"shells (degree <= 2); {sym} (Z={atomic_number}) needs a degree-{max_degree} "
97
+ f"shell (f-orbitals or higher) in {basis_name}. Not a silent approximation -- "
98
+ f"this element genuinely isn't supported by this engine yet."
99
+ )
100
+
101
+ out = []
102
+ for shell in electron_shells:
103
+ out.extend(_contracted_shell_from_bse(shell, center, atom_index))
104
+ return out
105
+
106
+
107
+ def build_molecule_shells(atomic_numbers: list[int], geometry_bohr: np.ndarray, basis_name: str) -> list[ContractedShell]:
108
+ """geometry_bohr: shape (n_atoms, 3), atomic units."""
109
+ ensure_x64()
110
+ shells = []
111
+ for i, (z, r) in enumerate(zip(atomic_numbers, geometry_bohr)):
112
+ shells.extend(load_element_shells(basis_name, z, jnp.asarray(r), i))
113
+ return shells
114
+
115
+
116
+ def n_cartesian_functions(shells: list[ContractedShell]) -> int:
117
+ return sum(len(cartesian_powers(s.degree)) for s in shells)
@@ -0,0 +1,35 @@
1
+ """The Boys function F_n(x), needed to evaluate any integral involving
2
+ 1/r12 (nuclear attraction, electron repulsion) between Gaussians.
3
+
4
+ F_n(x) = integral_0^1 t^(2n) exp(-x t^2) dt
5
+
6
+ which can be written in closed form via the regularized lower incomplete
7
+ gamma function P:
8
+
9
+ F_n(x) = Gamma(n + 1/2) * P(n + 1/2, x) / (2 * x^(n + 1/2))
10
+
11
+ For x -> 0 this formula divides 0/0, so we fall back to the first-order
12
+ Taylor expansion F_n(x) ~= 1/(2n+1) - x/(2n+3), which is accurate to
13
+ better than machine epsilon once x is small enough that no other term
14
+ in the calculation could still be sensitive to it.
15
+ """
16
+
17
+ import jax
18
+ import jax.numpy as jnp
19
+ from jax.scipy.special import gammainc, gammaln
20
+
21
+ _TAYLOR_CUTOFF = 1e-12
22
+
23
+
24
+ def boys(n: jax.Array, x: jax.Array) -> jax.Array:
25
+ """Evaluate F_n(x) elementwise. n and x broadcast against each other."""
26
+ x = jnp.asarray(x)
27
+ n = jnp.asarray(n)
28
+
29
+ x_safe = jnp.where(x < _TAYLOR_CUTOFF, 1.0, x)
30
+ log_gamma = gammaln(n + 0.5)
31
+ closed_form = jnp.exp(log_gamma) * gammainc(n + 0.5, x_safe) / (2.0 * x_safe ** (n + 0.5))
32
+
33
+ taylor = 1.0 / (2.0 * n + 1.0) - x / (2.0 * n + 3.0)
34
+
35
+ return jnp.where(x < _TAYLOR_CUTOFF, taylor, closed_form)
@@ -0,0 +1,112 @@
1
+ """Bridge from our native Hartree-Fock result to a PennyLane qubit Hamiltonian.
2
+
3
+ The expensive part (Hartree-Fock: integrals + SCF, everything in this
4
+ package) is entirely ours. Second quantization and the Jordan-Wigner
5
+ mapping are cheap (profiled at under 2 seconds even for Si2 -- see
6
+ dense_evolution/native_hf/__init__.py's module docstring) and PennyLane
7
+ already does them well via public functions, so we call those directly
8
+ instead of reimplementing them.
9
+ """
10
+
11
+ import numpy as np
12
+
13
+ from dense_evolution.native_hf.basis import build_molecule_shells
14
+ from dense_evolution.native_hf.assembly import build_overlap_matrix, build_core_hamiltonian, build_repulsion_tensor
15
+ from dense_evolution.native_hf.scf import run_scf, HFResult
16
+
17
+ # pennylane is installed in this test suite's own environment, so the
18
+ # except branch below (and the `if qml is None` check further down) is
19
+ # only exercised by tests/unit/test_imports.py, which blocks pennylane
20
+ # in a separate subprocess -- invisible to same-process coverage, so
21
+ # excluded rather than chased with an artificial same-process test.
22
+ try:
23
+ import pennylane as qml
24
+ import pennylane.qchem.observable_hf as _pl_observable
25
+ except ModuleNotFoundError: # pragma: no cover
26
+ qml = None
27
+ _pl_observable = None
28
+
29
+ _BOHR_PER_ANGSTROM = 1.0 / 0.52917721067
30
+
31
+
32
+ def _ao_to_mo(H_core: np.ndarray, repulsion: np.ndarray, C: np.ndarray):
33
+ one = np.einsum("qr,rs,st->qt", C.T, H_core, C)
34
+ two = np.swapaxes(
35
+ np.einsum("ab,cd,bdeg,ef,gh->acfh", C.T, C.T, repulsion, C, C),
36
+ 1, 3,
37
+ )
38
+ return one, two
39
+
40
+
41
+ def _apply_active_space(core_constant: float, one: np.ndarray, two: np.ndarray, core_idx, active_idx):
42
+ for i in core_idx:
43
+ core_constant = core_constant + 2 * one[i][i]
44
+ for j in core_idx:
45
+ core_constant = core_constant + 2 * two[i][j][j][i] - two[i][j][i][j]
46
+
47
+ for p in active_idx:
48
+ for q in active_idx:
49
+ for i in core_idx:
50
+ delta = np.zeros(one.shape)
51
+ delta[p, q] = 1.0
52
+ one = one + (2 * two[i][p][q][i] - two[i][p][i][q]) * delta
53
+
54
+ one_active = one[np.ix_(active_idx, active_idx)]
55
+ two_active = two[np.ix_(active_idx, active_idx, active_idx, active_idx)]
56
+ return core_constant, one_active, two_active
57
+
58
+
59
+ def build_qubit_hamiltonian(
60
+ atomic_numbers: list[int],
61
+ geometry_angstrom: np.ndarray,
62
+ n_electrons: int,
63
+ active_electrons: int = None,
64
+ active_orbitals: int = None,
65
+ basis_name: str = "sto-3g",
66
+ cutoff: float = 1e-12,
67
+ ) -> tuple["qml.Hamiltonian", int, HFResult]:
68
+ """Runs native Hartree-Fock, then hands the result to PennyLane for
69
+ second quantization + Jordan-Wigner mapping.
70
+
71
+ Returns:
72
+ tuple[qml.Hamiltonian, int, HFResult]: the mapped qubit
73
+ Hamiltonian, the qubit count, and the native HFResult -- the
74
+ latter useful for e.g. reporting the SCF energy alongside the
75
+ post-mapping ground-state energy.
76
+ """
77
+ if qml is None: # pragma: no cover -- see the try/except above
78
+ raise ModuleNotFoundError(
79
+ "build_qubit_hamiltonian requires pennylane. "
80
+ "Install it with: pip install dense-evolution[pennylane]"
81
+ )
82
+
83
+ geometry_bohr = np.asarray(geometry_angstrom) * _BOHR_PER_ANGSTROM
84
+ shells = build_molecule_shells(atomic_numbers, geometry_bohr, basis_name)
85
+
86
+ S = build_overlap_matrix(shells)
87
+ H_core = build_core_hamiltonian(shells, [float(z) for z in atomic_numbers], geometry_bohr)
88
+ repulsion = build_repulsion_tensor(shells)
89
+
90
+ hf_result = run_scf(S, H_core, repulsion, n_electrons, [float(z) for z in atomic_numbers], geometry_bohr)
91
+
92
+ one, two = _ao_to_mo(H_core, repulsion, hf_result.orbital_coefficients)
93
+ n_orbitals = one.shape[0]
94
+
95
+ if active_electrons is None and active_orbitals is None:
96
+ core_idx, active_idx = [], list(range(n_orbitals))
97
+ else:
98
+ core_idx, active_idx = qml.qchem.active_space(
99
+ n_electrons, n_orbitals, active_electrons=active_electrons, active_orbitals=active_orbitals
100
+ )
101
+
102
+ core_constant, one_active, two_active = _apply_active_space(
103
+ hf_result.nuclear_repulsion_energy, one, two, core_idx, active_idx
104
+ )
105
+
106
+ fermi_op = _pl_observable.fermionic_observable(
107
+ np.array([core_constant]), one_active, two_active, cutoff
108
+ )
109
+ qubit_hamiltonian = qml.jordan_wigner(fermi_op)
110
+ n_qubits = 2 * len(active_idx)
111
+
112
+ return qubit_hamiltonian, n_qubits, hf_result