Source code for iDEA.non_interacting

"""Contains all non-interacting functionality and solvers."""

import numpy as np
import scipy as sp
import numpy.linalg as npla
import scipy.linalg as spla


[docs]def kinetic_operator(s): """Compute single-particle kinetic energy operator as a matrix. This is built using a given number of finite differences to represent the second derivative. The number of differences taken is defined in s.stencil. s: System System object. returns: K: np.ndarray Kintetic energy operator. """ if s.stencil == 3: sd = 1.0 * np.array([1, -2, 1], dtype=np.float) / s.dx ** 2 sdi = (-1, 0, 1) elif s.stencil == 5: sd = 1.0 / 12.0 * np.array([-1, 16, -30, 16, -1], dtype=np.float) / s.dx ** 2 sdi = (-2, -1, 0, 1, 2) elif s.stencil == 7: sd = ( 1.0 / 180.0 * np.array([2, -27, 270, -490, 270, -27, 2], dtype=np.float) / s.dx ** 2 ) sdi = (-3, -2, -1, 0, 1, 2, 3) elif s.stencil == 9: sd = ( 1.0 / 5040.0 * np.array( [-9, 128, -1008, 8064, -14350, 8064, -1008, 128, -9], dtype=np.float ) / s.dx ** 2 ) sdi = (-4, -3, -2, -1, 0, 1, 2, 3, 4) elif s.stencil == 11: sd = ( 1.0 / 25200.0 * np.array( [8, -125, 1000, -6000, 42000, -73766, 42000, -6000, 1000, -125, 8], dtype=np.float, ) / s.dx ** 2 ) sdi = (-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5) elif s.stencil == 13: sd = ( 1.0 / 831600.0 * np.array( [ -50, 864, -7425, 44000, -222750, 1425600, -2480478, 1425600, -222750, 44000, -7425, 864, -50, ], dtype=np.float, ) / s.dx ** 2 ) sdi = (-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6) second_derivative = np.zeros((s.x.shape[0], s.x.shape[0])) for i in range(len(sdi)): second_derivative += np.diag( np.full( np.diag(np.zeros((s.x.shape[0], s.x.shape[0])), k=sdi[i]).shape[0], sd[i], ), k=sdi[i], ) K = -0.5 * second_derivative return K
[docs]def external_potential_operator(s): """Compute single-particle external potential energy operator as a matrix. s: System System object. returns: Vext: np.ndarray External potential energy operator. """ Vext = np.diag(s.v_ext) return Vext
[docs]def hamiltonian(s, K=None, V=None): """Compute the Hamiltonian from the kinetic and potential terms. s: System System object. K: np.ndarray Single-particle kinetic energy operator. V: np.ndarray Potential energy operator energy operator (local terms only). returns: H: np.ndarray Hamiltonian. """ if K is None: K = kinetic_operator(s) if V is None: V = external_potential_operator(s) H = K + V return H
[docs]def solve_states(s, H=None, empty=5): """Solves for the non-interacting eigenstates of the given system. s: System System object. H: np.ndarray Hamiltonian. If None this will be computed from s. (default = None) empty: int Number of empty states to inclue in solution. (default = 5) returns: orbitals: np.ndarray Array of normalised orbitals, indexed as orbitals[space,orbital_number]. energies: np.ndarray Array of single particle energies. """ if H is None: H = hamiltonian(s) energies, orbitals = spla.eigh(H) orbitals = orbitals / np.sqrt(s.dx) return orbitals[:, : s.NE + empty], energies[: s.NE + empty]
[docs]def charge_density(s, orbitals): r"""Compute charge density from given non-interacting orbitals. .. math:: n(x) = \sum_j^\mathrm{occ} = \phi_j^*(x)\phi_j(x) s: System System object. orbitals: np.ndarray Array of normalised orbitals, indexed as orbitals[space,orbital_number] or [time,space,orbital_number]. returns: n: np.ndarray Charge density. indexed as [space] or [time,space] """ if len(orbitals.shape) == 3: n = np.zeros(shape=(orbitals.shape[0], orbitals.shape[1])) for j in range(orbitals.shape[0]): n[j, :] = charge_density(s, orbitals[j, :, :]) return n elif len(orbitals.shape) == 2: occupied = orbitals[:, : s.NE] n = np.sum(occupied.conj() * occupied, axis=1).real return n else: pass # TODO
[docs]def probability_density(s, orbitals): r"""Compute probability density from given non-interacting orbitals. .. math:: n(x) =\frac{1}{N} \sum_j^\mathrm{occ} \phi_j^*(x)\phi_j(x) s: System System object. orbitals: np.ndarray Array of normalised orbitals, indexed as orbitals[space,orbital_number]. returns: n: np.ndarray Charge density. """ occupied = orbitals[:, : s.NE] n = np.sum(occupied.conj() * occupied, axis=1).real / s.NE return n
[docs]def orbital_density(s, orbital): r"""Compute charge density for a given non-interacting orbital. .. math:: n(x) = \phi^*(x)\phi(x) s: System System object. orbital: np.ndarray Normalised orbital. returns: n: np.ndarray Orbital density. """ n = (orbital.conj() * orbital).real return n
[docs]def one_body_reduced_density_matrix(s, orbitals): r"""Constructs the one-body reduced density matrix from single-particle orbitals. .. math:: \rho(x,x') = \sum^N_{n=1}\phi_n(x)\phi^*_n(x') s: System System object. orbitals: np.ndarray Array of normalised orbitals, indexed as orbitals[space,orbital_number]. returns: n: np.ndarray One body reduced density matrix. """ p = np.zeros(shape=(s.x.shape[0], s.x.shape[0]), dtype=np.complex) for i in range(s.NE): p += np.tensordot(orbitals[:, i].conj(), orbitals[:, i], axes=0) return p
[docs]def total_energy(s, energies): """Calculates the total energy from single particle energies. s: System System object. energies: np.ndarray Array of single particle energies. returns: E: float Total energy. """ E = np.sum(energies[: s.NE]) return E
[docs]def ionisation_potential(s, energies): """Calculates the ionisation potential from single particle energies. s: System System object. energies: np.ndarray Array of single particle energies. returns: ip: float Ionisation potential. """ ip = -energies[s.NE - 1] return ip
[docs]def electron_affinity(s, energies): """Calculates the electron affinity from single particle energies. s: System System object. energies: np.ndarray Array of single particle energies. returns: ea: float Electron affinity. """ ea = -energies[s.NE] return ea
[docs]def single_particle_gap(s, energies): """Calculates the single particle gap from single particle energies. s: System System object. energies: np.ndarray Array of single particle energies. returns: gap: float Single particle gap. """ gap = energies[s.NE] - energies[s.NE - 1] return gap
[docs]def propagate(s, orbitals, v_ptrb, times): """Propigate a set of orbitals forward in time due to a local pertubation s: System System object. orbitals: np.ndarray Array of normalised orbitals, indexed as orbitals[space,orbital_number]. v_ptrb: np.ndarray Local perturbing potential on the grid of x values. times: np.ndarray Grid of time values. returns: td_oribtals: nparray Array of time dependent orbitals, indexed as orbitals[time,space,orbital_number] Note: Returned orbitals is only the occupied orbitals, and so td_orbitals.shape[2] = s.NE. """ K = kinetic_operator(s) V = external_potential_operator(s) + np.diag(v_ptrb) H = hamiltonian(s, K, V) dt = times[1] - times[0] U = spla.expm(-1.0j * dt * H) td_oribtals = np.zeros( shape=(times.shape[0], orbitals.shape[0], orbitals.shape[1]), dtype=np.complex ) td_oribtals[0, :, :] = orbitals[:, :] for i in range(orbitals.shape[1]): for j, t in enumerate(times): if j != 0: print( "iDEA.non_interacting.propagate: propagating orbital {0}/{1}, time = {2:.3f}/{3:.3f}".format( i + 1, orbitals.shape[1], t, np.max(times) ), end="\r", ) td_oribtals[j, :, i] = np.dot(U, td_oribtals[j - 1, :, i]) norm = npla.norm(td_oribtals[j, :, i]) * np.sqrt(s.dx) td_oribtals[j, :, i] /= norm print() return td_oribtals