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
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.
|
|
Binary file
|
|
@@ -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
|