# Copyright 2017 University of Maryland.
#
# This file is part of Sesame. It is subject to the license terms in the file
# LICENSE.rst found in the top-level directory of this distribution.
import numpy as np
from scipy.io import savemat
from . import analyzer
from .utils import save_sim
from .analyzer import Analyzer
import scipy.sparse.linalg as lg
from scipy.sparse import coo_matrix, csr_matrix
from .getFandJ_eq import getFandJ_eq
from .getF import getF
from .jacobian import getJ
import logging
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
__all__ = ['solve', 'IVcurve']
# check if MUMPS is available
mumps_available = False
try:
from . import mumps
mumps_available = True
except:
pass
class NewtonError(Exception):
pass
class SparseSolverError(Exception):
pass
class BCsError(Exception):
def __init__(self, BCs):
msg = "\n*********************************************" +\
"\n* Unknown contacts boundary conditions *" +\
"\n*********************************************"
logging.error(msg)
logging.error("Contacts boundary conditions: '{0}' is different from 'Ohmic', 'Schottky', or 'Neumann'.\n".format(BCs))
[docs]class Solver():
"""
An object that creates an interface for the equilibrium and nonequilibrium
solvers of Sesame, and stores the equilibrium electrostatic potential once
computed.
Parameters
----------
use_mumps: boolean
Flag for the use of the MUMPS library if available. The flag is set to
True by default. If the MUMPS library is absent, the flag has no effect.
Attributes
----------
equilibrium: numpy array of floats
Electrostatic potential computed at thermal equilibrium.
"""
def __init__(self, use_mumps=True):
self.equilibrium = None
self.use_mumps = use_mumps
def make_guess(self, system):
# Make a linear assumption based on Dirichlet contacts
nx = system.nx
# determine what the potential on the left might be
if system.contacts_bcs[0] == 'Ohmic' or\
system.contacts_bcs[0] == 'Neutral':
if system.rho[0] < 0: # p-doped
v_left = -system.Eg[0]\
- np.log(abs(system.rho[0])/system.Nv[0]) - system.bl[0]
else: # n-doped
v_left = np.log(system.rho[0]/system.Nc[0]) - system.bl[0]
if system.contacts_bcs[0] == 'Schottky':
v_left = -system.contacts_WF[0] / system.scaling.energy
# determine what the potential on the right might be
if system.contacts_bcs[1] == 'Ohmic' or\
system.contacts_bcs[1] == 'Neutral':
if system.rho[nx-1] < 0:
v_right = -system.Eg[nx-1] - np.log(abs(system.rho[nx-1])/system.Nv[nx-1]) - system.bl[nx-1]
else:
v_right = np.log(system.rho[nx-1]/system.Nc[nx-1]) - system.bl[nx-1]
if system.contacts_bcs[1] == 'Schottky':
v_right = -system.contacts_WF[1] / system.scaling.energy
# Make a linear guess for the equilibrium potential
v = np.linspace(v_left, v_right, system.nx)
if system.dimension == 2:
# replicate the guess in the y-direction
v = np.tile(v, system.ny)
return v
def solve(self, system, compute='all', guess=None, tol=1e-6, periodic_bcs=True,\
maxiter=300, verbose=True, htp=1):
"""
Solve the drift diffusion Poisson equation on a given discretized
system out of equilibrium. If the equilibrium electrostatic potential is
not yet computed, the routine will compute it and save it for further
computations.
Parameters
----------
system: Builder
The discretized system.
compute: string
Set to 'all' to solve the full drift-diffusion-Poisson equations, or
to 'Poisson' to only solve the Poisson equation. Default is set to
'all'.
guess: dictionary of numpy arrays of floats
Contains the one-dimensional arrays of the initial guesses for the
electron quasi-Fermi level, the hole quasi-Fermi level and the
electrostatic potential. Keys should be 'efn', 'efp' and 'v'.
tol: float
Accepted error made by the Newton-Raphson scheme.
periodic_bcs: boolean
Defines the choice of boundary conditions in the y-direction. True
(False) corresponds to periodic (abrupt) boundary conditions.
maxiter: integer
Maximum number of steps taken by the Newton-Raphson scheme.
verbose: boolean
The solver returns the step number and the associated error at every
step if set to True (default).
htp: integer
Number of homotopic Newton loops to perform.
Returns
-------
solution: dictionary with numpy arrays of floats
Dictionary containing the one-dimensional arrays of the solution. The
keys are the same as the ones for the guess. An exception is raised
if no solution could be found.
"""
# Check if we only want the electrostatic potential
if compute == 'Poisson': # Only Poisson is solved
self.equilibrium = None # delete it to force its computation
if self.equilibrium is None:
if verbose == True:
logging.info("Solving for the equilibrium electrostatic potential")
if guess is None:
guess = self.make_guess(system)
else:
# testing of the data type of guess.
if type(guess) is dict:
guess = guess['v']
# Compute the potential (Newton returns an array)
self.equilibrium = self._newton(system, guess, tol=tol,\
periodic_bcs=periodic_bcs,\
maxiter=maxiter, verbose=verbose, htp=htp)
if self.equilibrium is None:
return None
# Return now if the electrostatic potential is all we wanted
if compute == 'Poisson':
efn = np.zeros_like(self.equilibrium)
efp = np.zeros_like(self.equilibrium)
return {'efn': efn, 'efp':efp, 'v':np.copy(self.equilibrium)}
# Otherwise, keep going with the full problem
if compute == 'all':
# array to pass to Newton routine
x = np.zeros((3*system.nx*system.ny,), dtype=np.float64)
if guess is None: # I will try with equilibrium
x[2::3] = np.copy(self.equilibrium)
else:
x[0::3] = guess['efn']
x[1::3] = guess['efp']
x[2::3] = guess['v']
# Compute solution (Newton returns an array)
x = self._newton(system, x, tol=tol, periodic_bcs=periodic_bcs,\
maxiter=maxiter, verbose=verbose, htp=htp)
if x is not None:
return {'efn': x[0::3], 'efp': x[1::3], 'v': x[2::3]}
else:
return None
def _damping(self, dx):
# This damping procedure is inspired from Solid-State Electronics, vol. 19,
# pp. 991-992 (1976).
b = np.abs(dx) > 1
dx[b] = np.log(1+np.abs(dx[b])*1.72)*np.sign(dx[b])
def _sparse_solver(self, J, f):
spsolve = lg.spsolve
if self.use_mumps and mumps_available:
spsolve = mumps.spsolve
else:
J = J.tocsr()
dx = spsolve(J, f)
return dx
def _get_system(self, x, system, periodic_bcs):
# Compute the right hand side of J * x = f
if self.equilibrium is None:
size = system.nx * system.ny
f, rows, columns, data = getFandJ_eq(system, x)
else:
f = getF(system, x[2::3], x[0::3], x[1::3], self.equilibrium)
rows, columns, data = getJ(system, x[2::3], x[0::3], x[1::3])
# form the Jacobian
if self.use_mumps and mumps_available:
J = coo_matrix((data, (rows, columns)), dtype=np.float64)
else:
J = csr_matrix((data, (rows, columns)), dtype=np.float64)
return f, J
def _newton(self, system, x, tol=1e-6, periodic_bcs=True, maxiter=300, verbose=True, htp=1):
htpy = np.linspace(1./htp, 1, htp)
for gdx, gamma in enumerate(htpy):
if verbose:
logging.info("Newton loop {0}/{1}".format(gdx+1, htp))
if gamma < 1:
htol = 1
else:
htol = tol
cc = 0
converged = False
if gamma != 1:
f0, _ = self._get_system(x, system, periodic_bcs)
while not converged:
cc = cc + 1
# break if no solution found after maxiterations
if cc > maxiter:
msg = "** Maximum number of iterations reached **"
logging.error(msg)
break
# solve linear system
f, J = self._get_system(x, system, periodic_bcs)
if gamma != 1:
f -= (1-gamma)*f0
try:
dx = self._sparse_solver(J, -f)
if dx is None:
raise SparseSolverError
break
else:
dx.transpose()
# compute error
error = max(np.abs(dx))
if np.isnan(error) or error > 1e30:
raise NewtonError
break
if error < htol:
converged = True
else:
# damping and new value of x
self._damping(dx)
x += dx
# print status of solution procedure
if verbose:
logging.info('step {0}, error = {1}'.format(cc, error))
except SparseSolverError:
msg = "** The linear system could not be solved **"
logging.error(msg)
break
except NewtonError:
msg = "** The Newton-Raphson algorithm diverged, try a better guess or finer grid **"
logging.error(msg)
break
if converged:
return x
else:
return None
def IVcurve(self, system, voltages, file_name, guess=None, tol=1e-6,
periodic_bcs=True, maxiter=300, verbose=True, htp=1, fmt='npz'):
"""
Solve the Drift Diffusion Poisson equations for the voltages provided. The
results are stored in files with ``.npz`` format by default (See below for
saving in Matlab format). The steady state current is computed at the
end of the voltage loop and returned. Note that the
potential is always applied on the right contact.
Parameters
----------
system: Builder
The discretized system.
voltages: array-like
List of voltages for which the current should be computed.
file_name: string
Name of the file to write the data to. The file name will be appended
the index of the voltage list, e.g. ``file_name_0.npz``.
guess: dictionary of numpy arrays of floats (optional)
Starting point of the solver. Keys of the dictionary must be 'efn',
'efp', 'v' for the electron and quasi-Fermi levels, and the
electrostatic potential respectively.
tol: float
Accepted error made by the Newton-Raphson scheme.
periodic_bcs: boolean
Defines the choice of boundary conditions in the y-direction. True
(False) corresponds to periodic (abrupt) boundary conditions.
maxiter: integer
Maximum number of steps taken by the Newton-Raphson scheme.
verbose: boolean
The solver returns the step number and the associated error at every
step, and this function prints the current applied voltage if set to True (default).
htp: integer
Number of homotopic Newton loops to perform.
fmt: string
Format string for the data files. Use ``mat`` to save the data in a
Matlab format (version 5 and above).
Returns
-------
J: numpy array of floats
Steady state current computed for each voltage value.
Notes
-----
The data files can be loaded and used as follows:
>>> results = np.load('file.npz')
>>> efn = results['efn']
>>> efp = results['efp']
>>> v = results['v']
"""
# create a dictionary 'result' with efn and efp
if guess is None:
result = self.solve(system, compute='Poisson', tol=tol,
periodic_bcs=periodic_bcs, maxiter=maxiter,
verbose=verbose, htp=htp)
else:
result = guess
# sites of the right contact
nx = system.nx
s = [nx-1 + j*nx for j in range(system.ny)]
# sign of the voltage to apply
if system.rho[nx-1] < 0:
q = 1
else:
q = -1
# Solving equilbrium potential first
if self.equilibrium is not None:
if verbose:
logging.info("Equilibrium potential already computed. Moving on.")
else:
self.solve(system, compute='Poisson', tol=tol,
periodic_bcs=periodic_bcs, maxiter=maxiter,
verbose=verbose, htp=htp)
# Applied potentials made dimensionless
Vapp = [i / system.scaling.energy for i in voltages]
# Array of the steady state current
J = np.zeros((len(Vapp),))
J[:] = np.nan
for idx, vapp in enumerate(Vapp):
if verbose:
logging.info("Applied voltage: {0} V".format(voltages[idx]))
# Apply the voltage on the right contact
result['v'][s] = self.equilibrium[s] + q*vapp
# Call the Drift Diffusion Poisson solver
result = self.solve(system, guess=result, tol=tol, periodic_bcs=periodic_bcs,\
maxiter=maxiter, verbose=verbose, htp=htp)
if result is not None:
# 1. Save efn, efp, v
name = file_name + "_{0}".format(idx)
# add some system settings to the saved results
if fmt == 'mat':
save_sim(system, result, name, fmt='mat')
else:
filename = "%s.gzip" % name
save_sim(system, result, filename)
# 2. Compute the steady state current
try:
az = Analyzer(system, result)
J[idx] = az.full_current()
except Exception:
logging.info("Could not compute the current for the applied voltage"\
+ " {0} V (index {1}).".format(voltages[idx], idx))
else:
logging.info("The solver failed to converge for the applied voltage"\
+ " {0} V (index {1}).".format(voltages[idx], idx))
return J
break
return J
default = Solver()
solve = default.solve
IVcurve = default.IVcurve