def solve_poisson(grid: TransformedGrid, rho: np.ndarray) -> np.ndarray: """Solve the radial poisson equation for a spherically symmetric density.""" norm = grid.integrate(4 * np.pi * grid.points**2 * rho) int1 = grid.antiderivative(grid.points * rho) int1 -= int1[0] int2 = grid.antiderivative(int1) int2 -= int2[0] pot = -(4 * np.pi) * int2 alpha = (norm - pot[-1]) / grid.points[-1] pot += alpha * grid.points pot /= grid.points return pot
def test_tf_grid_exp(): # pylint: disable=redefined-outer-name def transform(x, np): return 10 * np.arctanh((1 + x) / 2)**2 grid = TransformedGrid(transform, 201) fnvals = np.exp(-grid.points) fnvalsa = grid.antiderivative(fnvals) fnvalsa += -1 - fnvalsa[0] fnvalsd = grid.derivative(fnvals) assert_allclose(grid.integrate(fnvals), 1.0, atol=1e-13, rtol=0) assert_allclose(fnvalsa, -fnvals, atol=1e-7, rtol=0) assert_allclose(fnvalsd, -fnvals, atol=1e-7, rtol=0)
def test_tf_grid_hydrogen_few(): # pylint: disable=redefined-outer-name def transform(x, np): left = 1e-2 right = 1e3 alpha = np.log(right / left) return left * (np.exp(alpha * (1 + x) / 2) - 1) grid = TransformedGrid(transform, 101) # Solutions of the radial equation (U=R/r) psi_1s = np.sqrt(4 * np.pi) * grid.points * np.exp(-grid.points) / np.sqrt( np.pi) psi_2s = np.sqrt(4 * np.pi) * grid.points * np.exp(-grid.points / 2) / \ np.sqrt(2 * np.pi) / 4 * (2 - grid.points) # Check norms with vectorization norms = grid.integrate(np.array([psi_1s, psi_2s])**2) assert_allclose(norms, 1.0, atol=1e-14, rtol=0) # Check norms and energies for eps, psi in [(-0.5, psi_1s), (-0.125, psi_2s)]: ekin = grid.integrate(-psi * grid.derivative(grid.derivative(psi)) / 2) assert_allclose(ekin, -eps, atol=1e-11, rtol=0) epot = grid.integrate(-psi**2 / grid.points) assert_allclose(epot, 2 * eps, atol=1e-14, rtol=0) dot = grid.integrate(psi_1s * psi_2s) assert_allclose(dot, 0.0, atol=1e-15, rtol=0)
def __init__(self, grid: TransformedGrid, alphamin: float = 1e-6, alphamax: float = 1e7, nbasis: int = 80): """Initialize a basis. Parameters ---------- grid The radial integration grid. alphamin The lowest Gaussian exponent. alphamax The highest Gaussian exponent. nbasis The number of basis functions. """ self.grid = grid self.alphas = 10**np.linspace(np.log10(alphamin), np.log10(alphamax), nbasis) self.fnvals = np.exp( -np.outer(self.alphas, grid.points**2)) * grid.points self.normalizations = np.sqrt(np.sqrt(self.alphas))**3 * np.sqrt( np.sqrt(2 / np.pi) * 8) self.fnvals *= self.normalizations[:, np.newaxis] assert_allclose(np.sqrt(grid.integrate(self.fnvals**2)), 1.0, atol=7e-14, rtol=0)
def test_tf_grid_exp_vectorized(): # pylint: disable=redefined-outer-name def transform(x, np): return 15 * np.arctanh((1 + x) / 2)**2 grid = TransformedGrid(transform, 201) exponents = np.array([1.0, 0.5, 2.0]) fnsvals = np.exp(-np.outer(exponents, grid.points)) fnsvalsa = grid.antiderivative(fnsvals) fnsvalsa += (-1 / exponents - fnsvalsa[:, 0])[:, np.newaxis] fnsvalsd = grid.derivative(fnsvals) assert_allclose(grid.integrate(fnsvals), 1 / exponents, atol=1e-13, rtol=0) assert_allclose(fnsvalsa, -fnsvals / exponents[:, np.newaxis], atol=1e-7, rtol=0) assert_allclose(fnsvalsd, -fnsvals * exponents[:, np.newaxis], atol=1e-7, rtol=0) fns_other = np.exp(-np.outer([1.1, 1.2, 0.8], grid.points)) integrals2 = np.array([[grid.integrate(fn1 * fn2) for fn2 in fns_other] for fn1 in fnsvals]) assert_allclose(grid.integrate(fnsvals, fns_other), integrals2, atol=1e-14, rtol=0)
def setup_grid(npoint: int = 256) -> TransformedGrid: """Create a suitable grid for integration and differentiation.""" # pylint: disable=redefined-outer-name def transform(x: np.ndarray, np) -> np.ndarray: """Transform from [-1, 1] to [0, big_radius].""" left = 1e-3 right = 1e4 alpha = np.log(right / left) return left * (np.exp(alpha * (1 + x) / 2) - 1) return TransformedGrid(transform, npoint)
def scf_atom(atnum: float, occups: List[np.ndarray], grid: TransformedGrid, basis: Basis, nscf: int = 25, mixing: float = 0.5) \ -> Tuple[np.ndarray, List[Tuple[np.ndarray, np.ndarray]]]: """Perform a self-consistent field atomic calculation. Parameters ---------- atnum The nuclear charge. occups Occupation numbers, see interpret_econf. grid A radial grid. basis The radial orbital basis. nscf The number of SCF cycles. mixing The SCF mixing parameter. 1 means the old Fock operator is not mixed in. Returns ------- energies The atomic energy and its contributions. eps_orbs_u A list of tuples of (orbital energy, orbital coefficients). One tuple for each angular momentum quantum number. The orbital coefficients represent the radial solutions U = R/r. """ # Fock operators from previous iteration, used for mixing. focks_old = [] # Volume element in spherical coordinates vol = 4 * np.pi * grid.points**2 nelec = np.concatenate(occups).sum() maxangqn = len(occups) - 1 print("Occupation numbers per ang. mom. {:}".format(occups)) print("Number of electrons {:8.1f}".format(nelec)) print("Maximum ang. mol. quantum number {:8d}".format(maxangqn)) print() print("Number of SCF iterations {:8d}".format(nscf)) print("Mixing parameter {:8.3f}".format(mixing)) print() # pylint: disable=redefined-outer-name def excfunction(rho, np): """Compute the exchange(-correlation) energy density.""" clda = (3 / 4) * (3.0 / np.pi)**(1 / 3) return -clda * rho**(4 / 3) print( " It Total Rad Kin Ang Kin Hartree " " XC Ext") print( "=== =============== =============== =============== =============== " "=============== ===============") # SCF cycle # For the first iteration, the density is set to zero to obtain the core guess. rho = np.zeros(len(grid.points)) vhartree = np.zeros(len(grid.points)) vxc = np.zeros(len(grid.points)) for iscf in range(nscf): # A) Solve for each angular momentum the radial Schrodinger equation. # orbitals energy and radial orbitals: U = R/r eps_orbs_u = [] energy_ext = 0.0 energy_kin_rad = 0.0 energy_kin_ang = 0.0 # Hartree and XC potential are the same for all angular momenta. jxc = basis.pot(vhartree + vxc) for angqn in range(maxangqn + 1): # The new fock matrix. fock = basis.kin_rad + atnum * basis.ext + jxc if angqn > 0: angmom_factor = (angqn * (angqn + 1)) / 2 fock += basis.kin_ang * angmom_factor # Mix with the old fock matrix, if available. if iscf == 0: fock_mix = fock focks_old.append(fock) else: fock_mix = mixing * fock + (1 - mixing) * focks_old[angqn] focks_old[angqn] = fock_mix # Solve for the occupied orbitals. evals, evecs = eigh(fock_mix, basis.olp, eigvals=(0, len(occups[angqn]) - 1)) eps_orbs_u.append((evals, evecs)) # Compute the kinetic energy contributions using the orbitals. energy_kin_rad += np.einsum('i,ji,jk,ki', occups[angqn], evecs, basis.kin_rad, evecs) energy_ext += atnum * np.einsum('i,ji,jk,ki', occups[angqn], evecs, basis.ext, evecs) if angqn > 0: energy_kin_ang += np.einsum('i,ji,jk,ki', occups[angqn], evecs, basis.kin_ang, evecs) * angmom_factor # B) Build the density and derived quantities. # Compute the total density. rho = build_rho(occups, eps_orbs_u, grid, basis) # Check the total number of electrons. assert_allclose(grid.integrate(rho * vol), nelec, atol=1e-10, rtol=0) # Solve the Poisson problem for the new density. vhartree = solve_poisson(grid, rho) energy_hartree = 0.5 * grid.integrate(vhartree * rho * vol) # Compute the exchange-correlation potential and energy density. exc, vxc = xcfunctional(rho, excfunction) energy_xc = grid.integrate(exc * vol) # Compute the total energy. energy = energy_kin_rad + energy_kin_ang + energy_hartree + energy_xc + energy_ext print("{:3d} {:15.6f} {:15.6f} {:15.6f} {:15.6f} {:15.6f} {:15.6f}". format(iscf, energy, energy_kin_rad, energy_kin_ang, energy_hartree, energy_xc, energy_ext)) # Assemble return values energies = np.array([ energy, energy_kin_rad, energy_kin_ang, energy_hartree, energy_xc, energy_ext ]) return energies, eps_orbs_u