"""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