summaryrefslogtreecommitdiff
path: root/atom.py
diff options
context:
space:
mode:
Diffstat (limited to 'atom.py')
-rw-r--r--atom.py158
1 files changed, 158 insertions, 0 deletions
diff --git a/atom.py b/atom.py
new file mode 100644
index 0000000..c962a05
--- /dev/null
+++ b/atom.py
@@ -0,0 +1,158 @@
+from math import *
+from scipy.special import erf, sph_harm, eval_genlaguerre
+
+import numpy as np
+
+import element
+
+
+# Atomic Coulomb potential, modelled using HGH pseudopotentials.
+# See https://arxiv.org/abs/cond-mat/9803286 for details.
+class Atom:
+ def __init__(self, s, symbol, pos):
+ self.symbol = symbol
+ self.params = element.Element(symbol) # get element data
+ self.position = pos
+
+ # Set up local grid in spherical coordinates
+ self.r = np.zeros((s.Nres, s.Nres, s.Nres))
+ self.theta = np.zeros((s.Nres, s.Nres, s.Nres))
+ self.phi = np.zeros((s.Nres, s.Nres, s.Nres))
+ for i in range(s.Nres):
+ for j in range(s.Nres):
+ for k in range(s.Nres):
+ x = s.rx[i, j, k]
+ y = s.ry[i, j, k]
+ z = s.rz[i, j, k]
+
+ r = sqrt((x - pos[0])**2 + (y - pos[1])**2 + (z - pos[2])**2)
+ if r == 0:
+ theta = 0
+ phi = 0
+ else:
+ theta = acos((z - pos[2]) / r)
+ phi = atan2((y - pos[1]), (x - pos[0]))
+
+ self.r[i, j, k] = r
+ self.theta[i, j, k] = theta
+ self.phi[i, j, k] = phi
+
+ # Initialize and cache orbitals for nonlocal potential contribution
+ self.orbitals = {}
+ for l in range(0, 3):
+ self.orbitals[l] = {}
+ for i in range(1, 4):
+ p = self.get_projector(l, i)
+ self.orbitals[l][i] = {}
+ for m in range(-l, l + 1):
+ Y = self.cubic_harmonic(m, l)
+ self.orbitals[l][i][m] = p * Y
+
+
+ # Local potential representing short-range interactions.
+ # This is Eq.(1), but without the long-range erf-term:
+ def get_local_potential(self):
+ V = np.zeros_like(self.r)
+
+ rr = self.r / self.params.rloc # relative r
+ for i in range(4):
+ V += self.params.Cloc[i] * rr**(2 * i) * np.exp(-0.5 * rr**2)
+
+ return V
+
+
+ # The local erf-term decays as 1/r, which is problematic for periodicity.
+ # Instead, we replace it with a Hartree potential from a Gaussian charge.
+ # This is exact: multiply first term of Eq.(5) by g^2 (from Poisson).
+ def get_local_density(self):
+ alpha = 0.5 / self.params.rloc**2
+ rho = 4 * alpha**(3/2) * np.exp(-alpha * self.r**2) / sqrt(pi)
+
+ # Normalize in spherical coordinates: 4pi int rho(r) r^2 dr = Zion
+ rho *= self.params.Zion / (4 * pi)
+ return rho
+
+
+ # Given a psi in real space, apply nonlocal potential by integration
+ # Vpsi = sum_{lijm} |p_i> h_{ij} <p_j | psi>
+ def apply_nonlocal_potential(self, psi):
+ Vpsi = np.zeros_like(psi)
+
+ # Note that these if-statements provide a significant speedup
+ for l in range(0, 3):
+ if self.params.rl[l] != 0:
+ for i in range(1, 4):
+ for j in range(1, 4):
+ if self.params.h[l][i][j] != 0:
+ for m in range(-l, l + 1):
+ # Inner product normalization is handled by caller
+ ip = np.vdot(self.orbitals[l][j][m], psi)
+ Vpsi += self.orbitals[l][i][m] * self.params.h[l][i][j] * ip
+
+ return Vpsi
+
+
+ # Calculate projectors p_i^l(r) for nonlocal potential, see Eq.(3)
+ def get_projector(self, l, i):
+ p = np.zeros_like(self.r)
+
+ if self.params.rl[l] != 0:
+ p = sqrt(2) * self.r**(l + 2 * (i - 1))
+ p *= np.exp(-0.5 * (self.r / self.params.rl[l])**2)
+ p /= self.params.rl[l]**(l + 0.5 * (4 * i - 1))
+ p /= sqrt(gamma(l + 0.5 * (4 * i - 1)))
+
+ return p
+
+
+ # Guess this atom's electron density (to initialize main DFT loop)
+ def guess_density(self):
+ # We choose a spherical gaussian with standard deviation sigma
+ sigma = 1
+ rho = (sqrt(2 / pi) / sigma**3) * np.exp(- 0.5 * self.r**2 / sigma**2)
+
+ # Normalize in spherical coordinates: 4pi int rho(r) r^2 dr = Zion
+ rho /= self.params.Zion / (4 * pi)
+ return rho
+
+
+ # Guess this atom's eigenstates (to initialize eigensolver).
+ # We simply choose hydrogen orbitals, without any modifications:
+ def guess_orbitals(self, Norb):
+ psis = []
+ for n in range(1, 4):
+ rho = 2 * self.r / n
+ for l in range(0, n):
+ for m in range(-l, l + 1):
+ psi = self.cubic_harmonic(m, l)
+ psi *= np.exp(-0.5 * rho) * rho**l
+ psi *= eval_genlaguerre(n - l - 1, 2 * l + 1, rho)
+ psis.append(psi)
+
+ return psis[0 : Norb]
+
+
+ # Use cubic harmonics instead of spherical harmonics. This makes no
+ # physical difference, but it avoids confusion when visualizing.
+ def cubic_harmonic(self, m, l):
+ # Note that phi and theta are swapped according to SciPy
+ Yp = sph_harm( abs(m), l, self.phi, self.theta)
+ Yn = sph_harm(-abs(m), l, self.phi, self.theta)
+
+ f = np.zeros_like(self.r)
+ if m == -3:
+ f = (1 / sqrt(2)) * (Yn - Yp)
+ if m == -2:
+ f = (1j / sqrt(2)) * (Yn - Yp)
+ if m == -1:
+ f = (1 / sqrt(2)) * (Yn - Yp)
+ if m == 0:
+ f = Yp
+ if m == 1:
+ f = (1j / sqrt(2)) * (Yn + Yp)
+ if m == 2:
+ f = (1 / sqrt(2)) * (Yn + Yp)
+ if m == 3:
+ f = (1j / sqrt(2)) * (Yn + Yp)
+
+ return np.real(f)