physikmdb 1.0.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.
@@ -0,0 +1,66 @@
1
+ """Photoemission momentum maps and molecular orbitals from PhysikMDB.
2
+
3
+ The same C physics kernels the PhysikMDB website runs, plus a client for the
4
+ database behind it.
5
+
6
+ import physikmdb
7
+
8
+ db = physikmdb.Database()
9
+ calc = db.calculation(21)
10
+ image = calc.momentum_map("HOMO", photon_energy=21.2, k_max=3.0)
11
+
12
+ `Calculation` downloads the basis and the coefficient rows it needs on first
13
+ use and keeps them, so a loop over orbitals costs one small request each:
14
+
15
+ for name in ("HOMO", "HOMO-1", "HOMO-2"):
16
+ image = calc.momentum_map(name, photon_energy=21.2, k_max=3.0)
17
+
18
+ Work offline by saving a calculation once and reopening it:
19
+
20
+ calc.save("naphthalene/")
21
+ calc = physikmdb.load("naphthalene/")
22
+
23
+ Units are eV and Angstrom, matching the website's own controls. Everything
24
+ below is Hartree and Bohr, as the kernels take them; `physikmdb.units` is the
25
+ only place that converts. To skip the conversion entirely, call
26
+ `physikmdb.kernels` with your own basis and coefficients.
27
+
28
+ No compiler needed: a prebuilt kernel library ships for Linux, macOS and
29
+ Windows, and is built from source only on a platform without one.
30
+
31
+ Submodules:
32
+
33
+ - `physikmdb.kernels` the kernels as numpy functions, in atomic units
34
+ - `physikmdb.binary` read/write basis.bin and coefficients.bin
35
+ - `physikmdb.units` eV/Angstrom <-> Hartree/Bohr
36
+ - `physikmdb.plot` quick matplotlib views (needs `physikmdb[plot]`)
37
+ """
38
+
39
+ from physikmdb import binary, kernels, units
40
+ from physikmdb.binary import Basis
41
+ from physikmdb.database import (
42
+ Calculation,
43
+ Database,
44
+ Orbital,
45
+ PhysikMDBError,
46
+ System,
47
+ load,
48
+ )
49
+ from physikmdb.units import ANGSTROM_TO_BOHR, HARTREE_TO_EV
50
+
51
+ __version__ = "1.0.0"
52
+
53
+ __all__ = [
54
+ "ANGSTROM_TO_BOHR",
55
+ "HARTREE_TO_EV",
56
+ "Basis",
57
+ "Calculation",
58
+ "Database",
59
+ "Orbital",
60
+ "PhysikMDBError",
61
+ "System",
62
+ "binary",
63
+ "kernels",
64
+ "load",
65
+ "units",
66
+ ]
@@ -0,0 +1,257 @@
1
+ """Loading the compiled physics kernels. Internal - use `physikmdb.kernels`.
2
+
3
+ `load()` returns the shared library, trying in order:
4
+
5
+ 1. The prebuilt binary bundled in this platform's wheel
6
+ (`_vendor/prebuilt/libkernels.*`). Nearly every install takes this path and
7
+ never needs a compiler.
8
+ 2. Compiling the C sources in `_vendor/kernels/` with whatever C compiler is
9
+ on PATH - only when no prebuilt binary shipped for this platform, or the
10
+ bundled one will not load.
11
+
12
+ The result is cached, so a script calling several kernels pays the cost once.
13
+ """
14
+
15
+ import ctypes
16
+ import platform
17
+ import shutil
18
+ import subprocess
19
+ import tempfile
20
+ from pathlib import Path
21
+
22
+ _VENDOR_DIR = Path(__file__).resolve().parent / "_vendor"
23
+ KERNEL_SOURCE_DIR = _VENDOR_DIR / "kernels"
24
+
25
+ # map_image.c is separate from momentum_map.c but part of the same library
26
+ KERNEL_SOURCES = ("momentum_map.c", "map_image.c", "realspace.c", "momentumspace.c")
27
+
28
+ _PREBUILT_EXTENSION = {"Linux": "so", "Darwin": "dylib", "Windows": "dll"}.get(
29
+ platform.system(), "so"
30
+ )
31
+ PREBUILT_PATH = _VENDOR_DIR / "prebuilt" / f"libkernels.{_PREBUILT_EXTENSION}"
32
+
33
+ FLOAT_PTR = ctypes.POINTER(ctypes.c_float)
34
+ INT_PTR = ctypes.POINTER(ctypes.c_int)
35
+
36
+
37
+ class KernelBuildError(RuntimeError):
38
+ """Neither a bundled prebuilt library nor a from-source build was usable."""
39
+
40
+
41
+ def _declare(library: ctypes.CDLL) -> ctypes.CDLL:
42
+ """Declare every entry point's argument types on a freshly loaded library.
43
+
44
+ ctypes does not check arity against the C signature, so a wrong list here
45
+ shifts every later argument and produces plausible nonsense rather than an
46
+ error. Keep these next to kernels.h.
47
+
48
+ Args:
49
+ library: the loaded shared library.
50
+
51
+ Returns:
52
+ The same library, with restype/argtypes set.
53
+ """
54
+ # The shell-major basis every kernel takes, in `physikmdb.binary`'s layout:
55
+ # Natoms, shells per atom, positions, then per shell l and primitive count,
56
+ # then the primitives themselves
57
+ basis_args = [
58
+ ctypes.c_int, # Natoms
59
+ INT_PTR, # Nshells per atom
60
+ FLOAT_PTR,
61
+ FLOAT_PTR,
62
+ FLOAT_PTR, # Rx, Ry, Rz
63
+ INT_PTR,
64
+ INT_PTR, # shell_l, shell_Nprim
65
+ FLOAT_PTR,
66
+ FLOAT_PTR, # exponents, contractions
67
+ ]
68
+
69
+ signatures = {
70
+ "normalisation": [ctypes.c_int, FLOAT_PTR],
71
+ "symmetrisation": [ctypes.c_int, ctypes.c_int, FLOAT_PTR],
72
+ "polarisation_factor": [
73
+ ctypes.c_float, # E_kin
74
+ ctypes.c_float, # k0
75
+ ctypes.c_float, # dk
76
+ ctypes.c_int, # Nk
77
+ ctypes.c_float, # polar
78
+ ctypes.c_float, # azimuth
79
+ ctypes.c_int, # polarisation
80
+ ctypes.c_float, # s_share
81
+ ctypes.c_int, # handedness
82
+ ctypes.c_float, # gamma
83
+ FLOAT_PTR,
84
+ ],
85
+ "momentum_map": [
86
+ ctypes.c_float,
87
+ ctypes.c_float,
88
+ ctypes.c_float,
89
+ ctypes.c_int, # E_kin, k0, dk, Nk
90
+ ctypes.c_float,
91
+ ctypes.c_float,
92
+ ctypes.c_float, # phi, theta, psi
93
+ *basis_args,
94
+ FLOAT_PTR,
95
+ FLOAT_PTR, # coefficients, out
96
+ ],
97
+ "momentum_map_sum": [
98
+ ctypes.c_int,
99
+ FLOAT_PTR,
100
+ FLOAT_PTR,
101
+ FLOAT_PTR, # Norbitals, E_kin, weights, rows
102
+ ctypes.c_float,
103
+ ctypes.c_float,
104
+ ctypes.c_int, # k0, dk, Nk
105
+ ctypes.c_float,
106
+ ctypes.c_float,
107
+ ctypes.c_float, # phi, theta, psi
108
+ ctypes.c_int,
109
+ ctypes.c_int, # substrate, apply_polarisation
110
+ ctypes.c_float,
111
+ ctypes.c_float, # polar, azimuth
112
+ ctypes.c_int,
113
+ ctypes.c_float,
114
+ ctypes.c_int, # polarisation, s_share, handedness
115
+ FLOAT_PTR, # gamma (per orbital)
116
+ *basis_args,
117
+ FLOAT_PTR, # out
118
+ ],
119
+ "scalar_field": [
120
+ ctypes.c_int, # N
121
+ ctypes.c_float,
122
+ ctypes.c_float,
123
+ ctypes.c_float, # x0, y0, z0
124
+ ctypes.c_float, # spacing
125
+ *basis_args,
126
+ FLOAT_PTR,
127
+ FLOAT_PTR, # coefficients, field
128
+ ],
129
+ "charge_density_sum": [
130
+ ctypes.c_int, # N
131
+ ctypes.c_float,
132
+ ctypes.c_float,
133
+ ctypes.c_float, # x0, y0, z0
134
+ ctypes.c_float, # spacing
135
+ ctypes.c_int,
136
+ FLOAT_PTR,
137
+ FLOAT_PTR, # Norbitals, weights, rows
138
+ *basis_args,
139
+ FLOAT_PTR, # out
140
+ ],
141
+ "momentum_density": [
142
+ ctypes.c_int,
143
+ ctypes.c_float,
144
+ ctypes.c_float, # N, k0, dk
145
+ *basis_args,
146
+ FLOAT_PTR,
147
+ FLOAT_PTR, # coefficients, field
148
+ ],
149
+ "momentum_density_sum": [
150
+ ctypes.c_int,
151
+ ctypes.c_float,
152
+ ctypes.c_float, # N, k0, dk
153
+ ctypes.c_int,
154
+ FLOAT_PTR,
155
+ FLOAT_PTR, # Norbitals, weights, rows
156
+ *basis_args,
157
+ FLOAT_PTR, # out
158
+ ],
159
+ }
160
+
161
+ for name, argtypes in signatures.items():
162
+ entry = getattr(library, name)
163
+ entry.restype = None
164
+ entry.argtypes = argtypes
165
+
166
+ return library
167
+
168
+
169
+ def build(
170
+ source_dir: Path | None = None,
171
+ library_path: Path | None = None,
172
+ extra_flags=(),
173
+ ) -> ctypes.CDLL:
174
+ """Compile the kernels into a shared library and load it.
175
+
176
+ This is the fallback path - see the module docstring. Most installs never
177
+ call this; it only runs when no prebuilt binary shipped for the current
178
+ platform, or the bundled one failed to load.
179
+
180
+ Args:
181
+ source_dir: directory holding the .c and .h files; defaults to the
182
+ vendored copy in this package.
183
+ library_path: where to write the library; defaults to a temporary
184
+ file that lives as long as the process.
185
+ extra_flags: extra compiler flags, e.g. `["-fopenmp"]`.
186
+
187
+ Returns:
188
+ The loaded library, with every entry point declared.
189
+
190
+ Raises:
191
+ KernelBuildError: if no C compiler is on PATH, or the build fails.
192
+ """
193
+ source_dir = Path(source_dir) if source_dir is not None else KERNEL_SOURCE_DIR
194
+ if library_path is None:
195
+ library_path = Path(tempfile.mkdtemp(prefix="physikmdb-kernels-")) / "libkernels.so"
196
+
197
+ compiler = shutil.which("cc") or shutil.which("clang") or shutil.which("gcc")
198
+ if compiler is None:
199
+ raise KernelBuildError(
200
+ "physikmdb has no prebuilt kernel library for this platform, and no C "
201
+ "compiler (cc, clang, gcc) is on PATH to build one from source. Install a C "
202
+ "compiler (e.g. Xcode Command Line Tools on macOS, build-essential on Linux, "
203
+ "or MSVC/MinGW on Windows) and try again."
204
+ )
205
+
206
+ result = subprocess.run(
207
+ [
208
+ compiler,
209
+ "-shared",
210
+ "-fPIC",
211
+ "-O2",
212
+ "-std=c99",
213
+ *extra_flags,
214
+ *[str(source_dir / name) for name in KERNEL_SOURCES],
215
+ "-lm",
216
+ "-o",
217
+ str(library_path),
218
+ ],
219
+ capture_output=True,
220
+ text=True,
221
+ )
222
+ if result.returncode != 0:
223
+ flags = " ".join(extra_flags)
224
+ raise KernelBuildError(
225
+ f"Failed to build the kernels{f' with {flags}' if flags else ''}:\n{result.stderr}"
226
+ )
227
+
228
+ return _declare(ctypes.CDLL(str(library_path)))
229
+
230
+
231
+ _cached: ctypes.CDLL | None = None
232
+
233
+
234
+ def load() -> ctypes.CDLL:
235
+ """The kernels, built once per process: prebuilt if this wheel bundled
236
+ one for the current platform, compiled from the vendored source otherwise.
237
+
238
+ Returns:
239
+ The loaded library.
240
+
241
+ Raises:
242
+ KernelBuildError: if there is no usable prebuilt binary and the
243
+ fallback build also fails.
244
+ """
245
+ global _cached
246
+ if _cached is not None:
247
+ return _cached
248
+
249
+ if PREBUILT_PATH.exists():
250
+ try:
251
+ _cached = _declare(ctypes.CDLL(str(PREBUILT_PATH)))
252
+ return _cached
253
+ except OSError:
254
+ pass # bundled binary present but unusable (e.g. glibc too old) - fall through
255
+
256
+ _cached = build()
257
+ return _cached
@@ -0,0 +1,199 @@
1
+ /* The LCAO-GTO basis set, and how one shell is evaluated.
2
+ *
3
+ * All three kernels take the same nine flat arrays straight out of basis.bin
4
+ * (see docs/binary-format.md) and walked them with their own ishell/iprim/
5
+ * icoeff running counters - three copies of the same bookkeeping, and the
6
+ * largest index-bug surface in the kernels. `Basis` bundles the arrays;
7
+ * `radial_*` are the two per-shell radial amplitudes, which were written out
8
+ * twice each.
9
+ *
10
+ * The layout is shell-major: atoms in order, each atom's shells in order, and
11
+ * per shell one angular momentum l, one list of shell_Nprim primitives, and
12
+ * 2l+1 consecutive MO coefficients (m in ORCA's order). So walking it means
13
+ * carrying three offsets at once - the shell, its first primitive, and its
14
+ * first coefficient.
15
+ *
16
+ * Everything here is Hartree atomic units: Bohr, Bohr^-1, Bohr^-2.
17
+ */
18
+ #ifndef PHYSIKMDB_BASIS_H
19
+ #define PHYSIKMDB_BASIS_H
20
+
21
+ #include <math.h>
22
+
23
+ /**
24
+ * One orbital's basis set. Borrows the caller's arrays - it copies nothing and
25
+ * owns nothing, so they must outlive it.
26
+ */
27
+ typedef struct
28
+ {
29
+ int Natoms;
30
+ int Nshells; // total over all atoms, i.e. sum(Nshells_per_atom)
31
+ int Nbasis; // sum of 2l+1 over all shells; the length of one coefficient row
32
+
33
+ const int *Nshells_per_atom; // length Natoms
34
+ const float *Rx; // atom x-positions [Bohr]; length Natoms
35
+ const float *Ry; // atom y-positions [Bohr]; length Natoms
36
+ const float *Rz; // atom z-positions [Bohr]; length Natoms
37
+ const int *shell_l; // angular momentum per shell; length Nshells
38
+ const int *shell_Nprim; // primitives per shell; length Nshells
39
+ const float *exponents; // alpha_p [Bohr^-2]; length sum(shell_Nprim)
40
+ const float *contractions; // c_p per primitive; length sum(shell_Nprim)
41
+ } Basis;
42
+
43
+ /**
44
+ * Bundle the flat arrays, counting the shell and basis-function totals the
45
+ * kernels each used to count for themselves.
46
+ *
47
+ * @param Natoms number of atoms
48
+ * @param Nshells_per_atom shells per atom; length Natoms
49
+ * @param Rx x-positions [Bohr]; length Natoms
50
+ * @param Ry y-positions [Bohr]; length Natoms
51
+ * @param Rz z-positions [Bohr]; length Natoms
52
+ * @param shell_l angular momentum per shell
53
+ * @param shell_Nprim primitives per shell
54
+ * @param exponents alpha_p [Bohr^-2]
55
+ * @param contractions c_p per primitive
56
+ * @return the bundle, with Nshells and Nbasis filled in
57
+ */
58
+ static inline Basis basis_from_arrays(
59
+ int Natoms,
60
+ const int *Nshells_per_atom,
61
+ const float *Rx, const float *Ry, const float *Rz,
62
+ const int *shell_l, const int *shell_Nprim,
63
+ const float *exponents, const float *contractions)
64
+ {
65
+ Basis basis;
66
+ basis.Natoms = Natoms;
67
+ basis.Nshells_per_atom = Nshells_per_atom;
68
+ basis.Rx = Rx;
69
+ basis.Ry = Ry;
70
+ basis.Rz = Rz;
71
+ basis.shell_l = shell_l;
72
+ basis.shell_Nprim = shell_Nprim;
73
+ basis.exponents = exponents;
74
+ basis.contractions = contractions;
75
+
76
+ basis.Nshells = 0;
77
+ for (int a = 0; a < Natoms; a++)
78
+ {
79
+ basis.Nshells += Nshells_per_atom[a];
80
+ }
81
+
82
+ basis.Nbasis = 0;
83
+ for (int s = 0; s < basis.Nshells; s++)
84
+ {
85
+ basis.Nbasis += 2 * shell_l[s] + 1;
86
+ }
87
+
88
+ return basis;
89
+ }
90
+
91
+ /* The two approximations, shared by all three kernels.
92
+ *
93
+ * Both are RELATIVE AMPLITUDES: a contribution below this fraction of what it
94
+ * would otherwise be is dropped. So smaller is always closer to exact, and
95
+ * always slower - which is the only reason they exist, since most of a large
96
+ * basis set contributes nothing to any single orbital.
97
+ *
98
+ * They used to differ per kernel, and the difference was not a considered
99
+ * accuracy budget so much as drift: the momentum map applied no cutoff at all,
100
+ * momentumspace.c cut primitives at exp(-30) = 9e-14, and realspace.c at
101
+ * exp(-7) = 9e-4 - which, because it turns that into a cutoff radius
102
+ * sqrt(eps/alpha), discarded up to 5% of a g shell's peak amplitude. Being
103
+ * consistent and being right both point the same way here.
104
+ *
105
+ * The primitive cutoff was written as the exponent argument rather than the
106
+ * amplitude, which made it read backwards: larger meant more exact. Stating
107
+ * the amplitude and deriving the exponent is the same arithmetic under a name
108
+ * that means what it says. */
109
+ #define PRIMITIVE_EPSILON 1e-13f // exp(-x) below this: primitive dropped
110
+ #define COEFFICIENT_EPSILON 1e-6f // |c_mu| below this: whole shell dropped
111
+
112
+ /**
113
+ * The exponent argument at which a primitive reaches PRIMITIVE_EPSILON.
114
+ *
115
+ * `alpha_p * r^2` in real space, `k^2 / (4 alpha_p)` in momentum space; both
116
+ * enter as exp(-x). Derived rather than written down twice, so the two cannot
117
+ * drift apart.
118
+ *
119
+ * @return the cutoff in x, i.e. -log(PRIMITIVE_EPSILON)
120
+ */
121
+ static inline float primitive_exponent_cutoff(void)
122
+ {
123
+ return -logf(PRIMITIVE_EPSILON);
124
+ }
125
+
126
+ /**
127
+ * The radial amplitude of one shell in real space: sum_p c_p exp(-alpha_p r^2).
128
+ *
129
+ * The 2l+1 members of a shell share it, so it is evaluated once per shell and
130
+ * per point, not once per basis function.
131
+ *
132
+ * @param basis the basis set
133
+ * @param shell shell index, 0 .. Nshells-1
134
+ * @param first_primitive index of the shell's first primitive in `exponents`
135
+ * @param r2 squared distance from the shell's atom [Bohr^2]
136
+ * @param cutoff skip primitives whose alpha_p * r^2 reaches this; normally
137
+ * primitive_exponent_cutoff()
138
+ * @return the radial amplitude
139
+ */
140
+ static inline float radial_realspace(const Basis *basis, int shell,
141
+ int first_primitive, float r2, float cutoff)
142
+ {
143
+ float radial = 0.0f;
144
+
145
+ for (int p = 0; p < basis->shell_Nprim[shell]; p++)
146
+ {
147
+ float exponent = basis->exponents[first_primitive + p] * r2;
148
+
149
+ // Skip primitives that have underflowed to zero anyway
150
+ if (exponent < cutoff)
151
+ {
152
+ radial += basis->contractions[first_primitive + p] * expf(-exponent);
153
+ }
154
+ }
155
+
156
+ return radial;
157
+ }
158
+
159
+ /**
160
+ * The radial amplitude of one shell in momentum space:
161
+ * (k/2)^l * sum_p c_p alpha_p^(-l-3/2) exp(-k^2 / 4 alpha_p).
162
+ *
163
+ * This is the closed-form radial Fourier integral of a contracted Gaussian
164
+ * (see theory/main.tex); the momentum map evaluates it once per shell at the
165
+ * hemisphere's fixed |k|, the momentum density tabulates it over k.
166
+ *
167
+ * @param basis the basis set
168
+ * @param shell shell index, 0 .. Nshells-1
169
+ * @param first_primitive index of the shell's first primitive in `exponents`
170
+ * @param k momentum magnitude [Bohr^-1]
171
+ * @param cutoff skip primitives whose k^2 / (4 alpha_p) reaches this;
172
+ * normally primitive_exponent_cutoff()
173
+ * @return the radial amplitude
174
+ */
175
+ static inline float radial_momentum(const Basis *basis, int shell,
176
+ int first_primitive, float k, float cutoff)
177
+ {
178
+ int l = basis->shell_l[shell];
179
+ float k2 = k * k;
180
+ float radial = 0.0f;
181
+
182
+ for (int p = 0; p < basis->shell_Nprim[shell]; p++)
183
+ {
184
+ float exponent = k2 / (4.0f * basis->exponents[first_primitive + p]);
185
+
186
+ if (exponent < cutoff)
187
+ {
188
+ radial += basis->contractions[first_primitive + p] *
189
+ powf(basis->exponents[first_primitive + p], -l - 1.5f) *
190
+ expf(-exponent);
191
+ }
192
+ }
193
+
194
+ // Factor independent of alpha_p and c_p. Dividing by 2^l is exact, so it
195
+ // does not matter that the two callers used to bracket this differently.
196
+ return radial * (powf(k, l) / powf(2.0f, l));
197
+ }
198
+
199
+ #endif
@@ -0,0 +1,103 @@
1
+ /* Real cubic harmonics, shared by all three kernels.
2
+ *
3
+ * The table was copy-pasted three times before this header. Two copies (the
4
+ * momentum map and the momentum density) wrote the unit-vector form, valid
5
+ * only on |k| = 1; the real-space copy wrote the general solid-harmonic form
6
+ * with an explicit r^2. They are the same polynomials - the general form
7
+ * evaluated at r^2 = 1 is the unit-vector form - so one helper covers all
8
+ * three, with the k-space callers passing the literal 1.0f.
9
+ *
10
+ * That literal matters. Passing kx^2 + ky^2 + kz^2 instead would be "the same"
11
+ * only in exact arithmetic; in float it differs from 1 in the last bits and
12
+ * shifts every d, f and g harmonic. tests/test_wasm_physics.py has a
13
+ * regression test for exactly that mistake.
14
+ *
15
+ * Normalisations follow ORCA's real solid harmonic convention:
16
+ * https://www.faccts.de/docs/orca/6.1/manual/contents/utilitiesvisualization/orca_2json.html#definition-of-the-real-solid-harmonic-orbitals
17
+ */
18
+ #ifndef PHYSIKMDB_HARMONICS_H
19
+ #define PHYSIKMDB_HARMONICS_H
20
+
21
+ #define C_S 0.28209479177387814 // 1/2 * sqrt(1 / pi)
22
+ #define C_P 0.4886025119029199 // 1/2 * sqrt(3 / pi)
23
+ #define C_D 1.0925484305920792 // 1/2 * sqrt(15 / pi)
24
+ #define C_F 2.890611442640554 // 1/2 * sqrt(105 / pi)
25
+ #define C_G 5.006685883593409 // 3/2 * sqrt(35 / pi)
26
+
27
+ // First cubic-harmonic index of each shell (s, p, d, f, g); a shell of angular
28
+ // momentum l occupies SHELL_START[l] .. SHELL_START[l] + 2l
29
+ static const int SHELL_START[5] = {0, 1, 4, 9, 16};
30
+
31
+ // The highest angular momentum the table covers (g)
32
+ #define MAX_ANGULAR_MOMENTUM 4
33
+
34
+ /**
35
+ * Evaluate the real cubic harmonics at one point, up to a given shell.
36
+ *
37
+ * Filled cumulatively: only entries belonging to shells the caller actually
38
+ * needs are written, since for one orbital most of a basis set contributes
39
+ * nothing. `max_l` is a parameter rather than something derived here because
40
+ * the three kernels each decide it differently - over every shell, over the
41
+ * significant shells only, or per atom.
42
+ *
43
+ * @param x [Bohr or Bohr^-1] first Cartesian component
44
+ * @param y [Bohr or Bohr^-1] second Cartesian component
45
+ * @param z [Bohr or Bohr^-1] third Cartesian component
46
+ * @param r2 [x^2 + y^2 + z^2] in the same units squared; pass the literal
47
+ * 1.0f when x/y/z are already a unit vector
48
+ * @param max_l highest angular momentum to fill (0 = s only, 4 = through g)
49
+ * @param H out; entries SHELL_START[0] .. SHELL_START[max_l] + 2*max_l written
50
+ */
51
+ static inline void cubic_harmonics(float x, float y, float z, float r2,
52
+ int max_l, float H[25])
53
+ {
54
+ H[0] = C_S; // s
55
+
56
+ if (max_l >= 1)
57
+ {
58
+ H[1] = C_P * z; // pz
59
+ H[2] = C_P * x; // px
60
+ H[3] = C_P * y; // py
61
+ }
62
+
63
+ if (max_l >= 2)
64
+ {
65
+ float x2 = x * x;
66
+ float y2 = y * y;
67
+ float z2 = z * z;
68
+
69
+ H[4] = C_D * 0.28867513459481287f * (3.0f * z2 - r2); // dz2
70
+ H[5] = C_D * x * z; // dxz
71
+ H[6] = C_D * y * z; // dyz
72
+ H[7] = C_D * 0.5f * (x2 - y2); // dx2-y2
73
+ H[8] = C_D * x * y; // dxy
74
+
75
+ if (max_l >= 3)
76
+ {
77
+ H[9] = C_F * 0.12909944487358058f * z * (5.0f * z2 - 3.0f * r2); // fz3
78
+ H[10] = C_F * 0.15811388300841897f * x * (5.0f * z2 - r2); // fxz2
79
+ H[11] = C_F * 0.15811388300841897f * y * (5.0f * z2 - r2); // fyz2
80
+ H[12] = C_F * 0.5f * z * (x2 - y2); // fz(x2-y2)
81
+ H[13] = C_F * x * y * z; // fxyz
82
+ H[14] = -C_F * 0.20412414523193148f * x * (x2 - 3.0f * y2); // fx(x2-3y2)
83
+ H[15] = -C_F * 0.20412414523193148f * y * (3.0f * x2 - y2); // fy(3x2-y2)
84
+ }
85
+
86
+ if (max_l >= 4)
87
+ {
88
+ float r4 = r2 * r2;
89
+
90
+ H[16] = C_G * 0.021128856368212916f * (35.0f * z2 * z2 - 30.0f * z2 * r2 + 3.0f * r4); // gz4
91
+ H[17] = C_G * 0.1336306209562122f * x * z * (7.0f * z2 - 3.0f * r2); // gxz3
92
+ H[18] = C_G * 0.1336306209562122f * y * z * (7.0f * z2 - 3.0f * r2); // gyz3
93
+ H[19] = C_G * 0.09449111825230681f * (x2 - y2) * (7.0f * z2 - r2); // gx2-y2
94
+ H[20] = C_G * 0.18898223650461363f * x * y * (7.0f * z2 - r2); // gxy
95
+ H[21] = -C_G * 0.3535533905932738f * x * z * (x2 - 3.0f * y2); // gx(x2-3y2)
96
+ H[22] = -C_G * 0.3535533905932738f * y * z * (3.0f * x2 - y2); // gy(3x2-y2)
97
+ H[23] = -C_G * 0.125f * (x2 * x2 - 6.0f * x2 * y2 + y2 * y2); // gx4-6x2y2+y4
98
+ H[24] = -C_G * 0.5f * x * y * (x2 - y2); // gxy(x2-y2)
99
+ }
100
+ }
101
+ }
102
+
103
+ #endif