File size: 4,809 Bytes
2a87ede
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""Loading contracted basis-set shells for a molecule.



Basis-set parameters (exponents, contraction coefficients) are fetched

from the Basis Set Exchange (the `basis_set_exchange` PyPI package,

data-only, BSD-licensed -- https://www.basissetexchange.org), so this

module works for any element the basis has data for, not just the

handful PennyLane's own bundled STO-3G table covers (which stops at

Ne and is why Silicon needed a hand-written patch earlier in this

project).



The coefficients BSE reports are for *unnormalized* primitives, so each

primitive's contraction coefficient must be rescaled by its own L2 norm

before it can be summed against another shell's primitives. We get that

norm for free by reusing our own overlap_3d on the primitive against

itself: N = 1/sqrt(<primitive|primitive>).

"""

import dataclasses

import basis_set_exchange as bse
import jax
import jax.numpy as jnp
import numpy as np

from dense_evolution.native_hf.gaussians import GaussianShell3D
from dense_evolution.native_hf.overlap import overlap_3d


@dataclasses.dataclass(frozen=True)
class ContractedShell:
    """One angular-momentum shell (s, p, ...) of a contracted GTO."""

    atom_index: int
    center: jax.Array  # shape (3,), atomic units (Bohr)
    degree: int  # 0 for s, 1 for p, ...
    exponents: jax.Array  # shape (K,)
    coefficients: jax.Array  # shape (K,), already primitive-normalized


def _primitive_norm(exponent: jax.Array, degree: int) -> jax.Array:
    """1/sqrt(self-overlap) for the (degree,0,0) Cartesian component --

    for a shell of pure angular momentum `degree` every Cartesian

    component (e.g. px, py, pz) has the same norm by symmetry, so one

    component is enough."""
    g = GaussianShell3D(degree=degree, exponent=exponent, center=jnp.zeros(3))
    self_overlap = overlap_3d(g, g)[degree, 0, 0, degree, 0, 0]
    return 1.0 / jnp.sqrt(self_overlap)


def _contracted_shell_from_bse(shell: dict, center: jax.Array, atom_index: int) -> list[ContractedShell]:
    """BSE groups shells like STO-3G's "SP" as one entry with two rows of

    coefficients (one for the S part, one for the P part) sharing the

    same exponents; we split those back into separate ContractedShells

    since our integral code handles one angular momentum at a time."""
    angular_momenta = shell["angular_momentum"]
    exponents = jnp.array([float(e) for e in shell["exponents"]])

    shells = []
    for row, degree in enumerate(angular_momenta):
        coeffs = jnp.array([float(c) for c in shell["coefficients"][row]])
        norms = jnp.array([_primitive_norm(a, degree) for a in exponents])
        shells.append(
            ContractedShell(
                atom_index=atom_index,
                center=center,
                degree=degree,
                exponents=exponents,
                coefficients=coeffs * norms,
            )
        )
    return shells


def load_element_shells(basis_name: str, atomic_number: int, center: jax.Array, atom_index: int) -> list[ContractedShell]:
    data = bse.get_basis(basis_name, elements=[atomic_number])
    electron_shells = data["elements"][str(atomic_number)]["electron_shells"]

    if any(s["function_type"] not in ("gto", "gto_cartesian") for s in electron_shells):
        raise NotImplementedError("Only Cartesian Gaussian basis sets are currently supported.")

    max_degree = max(
        degree for shell in electron_shells for degree in shell["angular_momentum"]
    )
    if max_degree > 1:
        from basis_set_exchange.lut import element_sym_from_Z

        sym = element_sym_from_Z(atomic_number).capitalize()
        raise NotImplementedError(
            f"native_hf's overlap/kinetic/Coulomb integrals only implement s and p "
            f"shells (degree <= 1); {sym} (Z={atomic_number}) needs a degree-{max_degree} "
            f"shell (d-orbitals or higher) in {basis_name}. Not a silent approximation -- "
            f"this element genuinely isn't supported by this engine yet."
        )

    out = []
    for shell in electron_shells:
        out.extend(_contracted_shell_from_bse(shell, center, atom_index))
    return out


def build_molecule_shells(atomic_numbers: list[int], geometry_bohr: np.ndarray, basis_name: str) -> list[ContractedShell]:
    """geometry_bohr: shape (n_atoms, 3), atomic units."""
    shells = []
    for i, (z, r) in enumerate(zip(atomic_numbers, geometry_bohr)):
        shells.extend(load_element_shells(basis_name, z, jnp.asarray(r), i))
    return shells


_DEGREE_TO_N_CARTESIAN = {0: 1, 1: 3}  # s: 1 component, p: 3 (x,y,z)


def n_cartesian_functions(shells: list[ContractedShell]) -> int:
    return sum(_DEGREE_TO_N_CARTESIAN[s.degree] for s in shells)