import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.colors import LogNorm

import matplotlib as mpl

from scipy import interpolate
import scipy.optimize

from copy import deepcopy
import time
from tqdm import tqdm

from ngsolve import *
from ngsolve.webgui import Draw
from netgen.csg import *

from netgen.occ import *
from netgen.meshing import IdentificationType

1. Solve the non-linear Poisson equation for a graphene nano ribbon#

Siyar Duman, TU Wien

\(\epsilon\Delta\phi = \rho(\phi)\)

\(\epsilon\) is the permittivity

\(\phi\) is the electric potential

\(\rho(\phi)\) is the charge density for a graphene nano ribbon of a given width \(w\). As a model, we take equation 13 in Appl. Phys. Lett. 91, 092109 (2007)

Below, the function charge_density_GNR(energy, w) is \(\rho(\phi)\). It is continuous, but not differentiable at every point and shows a large constant plateau at the center (Bandgap).

1.1. Constants and Functions#

### CONSTANTS. In the following we use ATOMIC UNITS.
## In atomic units, bohr_radius = 1, h_bar = 1, electron_charge = 1, electron_mass = 1

nm = 18.8972613
Angstrom = 0.1*nm
Volt = 0.036749322 
epsilon_0 = 0.07958
grapheneheight = 3.35*Angstrom


c_light = 137.037 ### speed of light in atomic units

v_F =  0.003335 * c_light
    
### epsilon, permittivity
    
inplane_hBn_bulk = 4.98 ### see https://www.nature.com/articles/s41699-018-0050-x 
outofplane_hBn_bulk = 3.03

inplane_graphene = 1.8 ### https://pubs.acs.org/doi/10.1021/nl303611v
outofplane_graphene = 3
    
    
eps_dict = {"hBn" : CoefficientFunction((inplane_hBn_bulk, 0, 0,
                             0, inplane_hBn_bulk, 0,
                             0, 0, outofplane_hBn_bulk),dims=(3,3)),
                        "GNR" : CoefficientFunction((inplane_graphene, 0, 0,
                             0, inplane_graphene, 0,
                             0, 0, outofplane_graphene),dims=(3,3))}
def generate_charge_density(gfu, alpha = 1):

    potential  = np.zeros((npts,npts))
    
    potential = (gfu(meshptsGNR).reshape(npts,npts))
    
    charge = np.zeros((npts,npts))
    
    for j in range(npts):
        
        for i in range(npts):
            
            charge[j,i] = evaluate_interpolated_charge_density(potential[j,i])
            
    if alpha != 1:
        
        charge = alpha * charge
            

    return charge, potential
def put_charge_on_grid(values, gfQ):
    
    nz = 3
    
    new_values = np.zeros(npts*npts*nz)
    
    new_values = new_values.reshape(npts,npts,nz)
    
    new_values = new_values.reshape(nz,npts,npts)
    
    for nn in range(nz):
        
        for ny in range(npts):
            
            for nx in range(npts):
                
                new_values[nn][ny][nx] = values[nx][ny]
    
    
    func = VoxelCoefficient((0, 0, height), 
            (box_width, box_width, height + grapheneheight), new_values)
    
    gfQ.Set(func)
def E_n(n, w): # E_n defined below eq. 8
    
    return (n*np.pi*v_F)/w

def get_number_of_states(energy, w):
    
    energy = np.abs(energy)
    
    n_energy = energy * (w / (np.pi*v_F))
    
    n_energy = int(np.floor(n_energy))
    
    if n_energy == 0:
        
        inside_bandgap = True
    
    else:
    
        inside_bandgap = False
    
    return n_energy, inside_bandgap

def carrier_concentration(energy, w):
    
    
    n_energy, inside_bandgap = get_number_of_states(energy, w)
    
    if inside_bandgap == True:
        
        return 0
    
    sum_carrier = 0
    
    for j in range(1, n_energy + 1):
        
        dummy_E_n = E_n(j, w)
        
        sum_carrier += np.sqrt(energy*energy - dummy_E_n * dummy_E_n)
        
    sum_carrier = sum_carrier * 4.0 / (np.pi*v_F)
    
    return sum_carrier


def charge_density_GNR(energy, w):
    
    if energy == 0:
        
        return 0
    
    if energy > 0: # electrons
        
        density =  - carrier_concentration(energy, w) / (grapheneheight * w) ## this is in units of 1/Bohr radius^3
        
        return density
        
    if energy < 0: # holes
        
        density = carrier_concentration(energy, w) / (grapheneheight * w) ## this is in units of 1/Bohr radius^3
        
        return density
chosen_width = 5

en_steps = 20000

pot_max = 30

pot_min = -30

en_intervall = np.linspace(pot_min, pot_max, en_steps)

values_charge_density_GNR = np.zeros(en_steps)

for i, en in enumerate(en_intervall):
    
    values_charge_density_GNR[i] = charge_density_GNR(en*Volt, chosen_width * nm)
    

interpolated_charge_density_GNR = scipy.interpolate.interp1d(en_intervall*Volt, values_charge_density_GNR)
plot_en_steps = 1000

plot_pot_max = 2

plot_pot_min = -2


plot_en_intervall = np.linspace(plot_pot_min, plot_pot_max, plot_en_steps)

plot_charge_density_GNR = np.zeros(plot_en_steps)

for i, en in enumerate(plot_en_intervall):

    plot_charge_density_GNR[i] = charge_density_GNR(en*Volt, chosen_width * nm)



values_interpolated_charge_density_GNR = np.zeros(plot_en_steps)

for i, en in enumerate(plot_en_intervall*Volt):
    
    values_interpolated_charge_density_GNR[i] = interpolated_charge_density_GNR(en)
    

plt.figure(figsize=(11,9))
plt.plot(plot_en_intervall, plot_charge_density_GNR, label="analytical")
plt.plot(plot_en_intervall, values_interpolated_charge_density_GNR, linestyle=(0, (5, 5)), label="interpolated", c="red")
plt.xlabel(r"$\phi$ [eV]", fontsize=20)
plt.ylabel(r"$\rho(\phi)$ [$\frac{1}{a_0^3}$]", fontsize=20)
plt.title("non-linear term for a graphene nano ribbon of "+str(chosen_width) + " nm width", fontsize=20)
plt.legend()
plt.show()
../_images/cacf27b3d16d51d558d691184cc3c325778a62a5eb9ba748f4122826236dab7e.png
small_delta = pow(10,-5)

pot_min_in_Volt = pot_min*Volt

pot_max_in_Volt = pot_max*Volt


### we use the interpolated function since it is much cheaper to evaluate

def evaluate_interpolated_charge_density(pot):
    
    # if potential is beyond intervall, map it to the boundary
    
    # avoid boundary evaluation; shift by small_delta
    

    if pot <= pot_min_in_Volt: 
        
        pot = pot_min_in_Volt + small_delta
        
    if pot >= pot_max_in_Volt:
        
        pot = pot_max_in_Volt - small_delta
        
    return interpolated_charge_density_GNR(pot)

1.2. Geometry consists of a Bottom Gate, Insulating Material (hBn), GNR, and top cylinder gate#

box_width = chosen_width * nm

height = grapheneheight*4

cylinder_radius = chosen_width * nm * 0.07


maxh = 5 * Angstrom

hBn_bottom = Box(Pnt(0 ,0 ,0 ), Pnt(box_width, box_width, height))

GNR = Box(Pnt(0 ,0 , height), Pnt(box_width, box_width, height + grapheneheight))

hBn_top = Box(Pnt(0 ,0 , height + grapheneheight), Pnt(box_width, box_width, 2*height + grapheneheight))

cylinder_gate = Cylinder(Pnt(box_width/2, box_width/2, 2*height + grapheneheight), Z, r=cylinder_radius, h=height/2)


hBn_bottom.mat("hBn");

GNR.mat("GNR");

hBn_top.mat("hBn");


cylinder_gate.bc("top");

hBn_bottom.faces.Min(Z).name = "bottom"



geo = OCCGeometry([hBn_bottom, GNR, hBn_top, cylinder_gate])


mesh = Mesh(geo.GenerateMesh(maxh=maxh)).Curve(3)


npts = 200

xs   = np.linspace(0, box_width, npts)
ys   = np.linspace(0, box_width, npts)

ptsGNR = np.zeros( (len(xs)*len(ys),3))  

for i,x in enumerate(xs):
    for j,y in enumerate(ys):
        nr = j+i*len(ys)
        ptsGNR[nr,0] = x
        ptsGNR[nr,1] = y
        ptsGNR[nr,2] = height + grapheneheight/2

            
meshptsGNR = mesh(ptsGNR[:,0], ptsGNR[:,1], ptsGNR[:,2])
Draw(mesh)
BaseWebGuiScene

1.3. solve once for right-hand side = 0#

bottom = 2  # chosing bottom = 1 leaves the potential inside the bandgap, 
            # no charge and iteration stops immediately. try it out!

top = -7

bound_dict = {'bottom' : bottom*Volt, 'top' :top*Volt}


start = time.time()

##### solve once

fes = H1(mesh, order=2, dirichlet="bottom|top")   


print ("ndof=", fes.ndof)
epsr = mesh.MaterialCF( eps_dict )
u, v = fes.TnT()
a = BilinearForm(epsilon_0 * epsr * grad(u) * grad(v) * dx)

a.Assemble()


gfu  = GridFunction(fes)
gfud = mesh.BoundaryCF( bound_dict, default=0)
gfu.Set(gfud, BND)

inv = a.mat.Inverse(freedofs=fes.FreeDofs(), inverse="sparsecholesky")


res = (a.mat * gfu.vec).Evaluate()

gfu.vec.data -= inv * res


Draw(gfu);

end = time.time()

print(end-start)
ndof= 6239
0.1526191234588623

1.4. Solve non-linear Poisson’s equation iteratively#

def residual_of_charge(charge):
    
    charge = charge.reshape((npts,npts))
    
    u, v = fes.TnT()

    put_charge_on_grid( charge, gfQ)
    
    #update right side of poisson equation
    with TaskManager():
        
        f   = LinearForm( gfQ * v * dx("GNR")).Assemble()
        
        gfu.Set(gfud, BND)

        res = (a.mat * gfu.vec - f.vec).Evaluate()
    
        gfu.vec.data -= inv * res
        

    new_charge, new_potential = generate_charge_density(gfu)
        
    ### plot charges, their differences and the new potential
    
    box_extent = [0, chosen_width, 0, chosen_width]
    
    plt.figure(figsize=(11,8))
    
    gs=gridspec.GridSpec(1,4, wspace = 0.6)
    
    ax = plt.subplot(gs[0,0])
    
    img = ax.imshow(charge.T*pow(nm,3), extent = box_extent)
    
    plt.xlabel("x [nm]")
    plt.ylabel("y [nm]")
    plt.title(r"$\rho_{s}$ in $[\frac{1}{nm^3}]$")
    
    plt.colorbar(img, ax=ax,fraction=0.046, pad=0.04)
    
    ax2 = plt.subplot(gs[0,1])
    
    img2 = ax2.imshow(new_charge.T*pow(nm,3), extent = box_extent)
    
    plt.xlabel("x [nm]")
    plt.ylabel("y [nm]")
    plt.title(r"$\rho[\phi_s,\vec\nabla\phi_s]$ in $[\frac{1}{nm^3}]$")
    
    plt.colorbar(img2, ax=ax2,fraction=0.046, pad=0.04)
    
    ax3 = plt.subplot(gs[0,2])
    
    img3 = ax3.imshow((new_charge.T - charge.T), extent = box_extent)
    
    plt.xlabel("x [nm]")
    plt.ylabel("y [nm]")
    plt.title(r"$\delta[\rho_s]$")
    
    plt.colorbar(img3, ax=ax3,fraction=0.046, pad=0.04)
    
    ax4 = plt.subplot(gs[0,3])
    
    img4 = ax4.imshow(new_potential.T/Volt*pow(10,3), extent = box_extent)
    
    plt.xlabel("x [nm]")
    plt.ylabel("y [nm]")
    plt.title(r"$\phi_{s}$ in $[meV]$")
    
    plt.colorbar(img4, ax=ax4,fraction=0.046, pad=0.04)
    
    plt.suptitle("iteration " + str(n_it[0]), y=0.7)
    
    plt.show()
    
    n_it[0] += 1
    
    saved_charges.append(new_charge)
    
    saved_potentials.append(new_potential)
    
    charge = charge.reshape(npts*npts)

    new_charge = new_charge.reshape(npts*npts)

    return  (charge - new_charge)
fes = H1(mesh, order=2, dirichlet="bottom|top")   

gfu  = GridFunction(fes)
gfud = mesh.BoundaryCF( bound_dict, default=0)

fesQ   = L2(mesh, order=2, definedon=mesh.Materials("GNR"))
gfQ    = GridFunction(fesQ)
gfQ.Set(0)


print ("ndof=", fes.ndof)
epsr = mesh.MaterialCF( eps_dict )



#### solve once, generate initial guess



u, v = fes.TnT()
a = BilinearForm(epsilon_0 * epsr * grad(u) * grad(v) * dx).Assemble() 

gfu  = GridFunction(fes)
gfud = mesh.BoundaryCF( bound_dict, default=0)
gfu.Set(gfud, BND)

inv = a.mat.Inverse(freedofs=fes.FreeDofs(), inverse="sparsecholesky")


res = (a.mat * gfu.vec).Evaluate()

gfu.vec.data -= inv * res


charge, potential = generate_charge_density(gfu, alpha = 0.01) # alpha = 1

saved_charges = [charge]

saved_potentials = [potential]

n_it = [0]


#### cycle

    
sol = scipy.optimize.root(residual_of_charge, 
                charge,
                method='df-sane', options = {"fatol": pow(10,-7)}) #method='krylov'
ndof= 6239
../_images/49f47ae547df7b296ee88af54574e79b27afc5cb7f5c067d17c17582de1f19a7.png ../_images/72834751269b9ca5066d3bc5afca3e6555e902616d89a6ae5941e58f83c6ad39.png ../_images/2e2792ecbebb36093124d77789a8c99dc81743c72a4c13c925863a11a700cbdb.png ../_images/8a7a9c3cffebfb96cead8ebc677e3fcca3ddbceff588decae2a7c5e33574e192.png ../_images/30cb2f1d7f257ff5ae66a2e9b3cdee27357642c4a6425e007eac6acab82e553a.png ../_images/0bb45052680f55db05c01aac51ea430101df1aa3b7d8c476a2ea6fbb297c401f.png ../_images/8370a18853cdfb3444c6def2da1eb9f2b9764585fb89452644717c5e2a4c59b4.png ../_images/8664dbcd893d33f4a55d77e4ae788dd0efdff06a81d4bea287b1b44999a98fe8.png ../_images/a768e8976cb6a2bebc5314577403df81ab1f093fed1a3f13f7f8d022d6172d3e.png ../_images/9784ce77166d4c69e4c8704eb56bdcdc12eb5db12df0499de0a47c6d39e1ffba.png ../_images/4c7e69a7cfa3ddc6404031c7bbd3a6b92e101949883e55877ee6d2e4e5c977eb.png ../_images/3d04fda30a01247fc7f417819f3d30c9346a81ebb361dda037250db814648485.png ../_images/93525ecf0d7440df4ed1e0b9edfc315e6c0c1027b8e939bd7e58d71b670d21c9.png ../_images/f6be68fcdc9f6e8f03d864e1f82f268ee98c2b9f6d5c43dbd2778aa1539c9508.png ../_images/a285c1665953ae71e0dda76bf7f3fa01fe20547406fd4f6c81e007274b9da14b.png ../_images/cd3c604d4f9b1f43ebd3a4a6e2166414b2e9a595e80e411f9ecce04ece6e5976.png ../_images/338b17848eea79a818eeaaacb1af5947123d7da217eec9429c92e2fe165f5e31.png ../_images/01849ac1b5fd41f1afdc9f9ded1769a5ee7d75b97c0f8fc0f85451c771e93145.png ../_images/e4dbc57e32d5dfaba968461b622f20c2cbfeea9a1579218bf2922afdedf1017c.png ../_images/eb85282806400d8453c68eed42fd79072e8b9bd7c5ae1157ca49e45aa1593e4c.png ../_images/d19b096bd0007970b636a5baea647edfee271eea83774876509f73c3e6d176ff.png ../_images/e177a69d6e75c89a5ac14fc039071b883c41a6ac2398182d34d3594a18c7f74e.png ../_images/7c1eaf9cc1a49e0decb20ad5bf8424044a03513ac5b58d355bf40a85c536b634.png ../_images/63f08d711b83d63d868b8a3d223a03e7f90ba4f505f3afffe50d390230bcea28.png ../_images/1f5d54f611e5ba4f30fb38f036ee9031b824979bf4e9a2d68b9f351cdf88e8fa.png ../_images/4113ab2d195c6173800b8226d2d96d40f2a240773eb1388e856bda2a3664c712.png ../_images/ce782bcbf1f4f15077b5ac2c354829cc1b51084e32e3569574211b3cc99c47c1.png ../_images/504fb1ef646d49157e8253251220192a961b9c540505146853dd18b432628c82.png
Draw(gfu)
BaseWebGuiScene
n_it = len(saved_charges)

plt.figure(figsize=(11,8))


for i in range(n_it):
    
    if i < 5:
        
        continue
        
    if i != n_it - 1:
    
        plt.plot(np.diag(saved_charges[i])*pow(nm,3), label = str(i))
        
    else:
        
        plt.plot(np.diag(saved_charges[i])*pow(nm,3), label = "final", color="black", linestyle="dashed")
    
plt.title("charge on the main diagonal per iteration")

plt.ylabel(r"$\rho(\vec r)$  $[\frac{1}{nm^3}]$")
    
plt.legend(loc="lower left")

plt.show()
../_images/bc86f8c0f11b8e320146e083e2616dfe2a10d10f509c187469da25223689c9d2.png
n_it = len(saved_charges)

plt.figure(figsize=(11,8))


for i in range(n_it):
    
    if i < 5:
        
        continue
        
    if i != n_it - 1:
    
        plt.plot(np.diag(saved_potentials[i])*pow(10,3)/Volt, label = str(i))
        
    else:
        
        plt.plot(np.diag(saved_potentials[i])*pow(10,3)/Volt, label = "final", color="black", linestyle="dashed")
    
plt.title("potential on the main diagonal per iteration")

plt.ylabel(r"$\phi(\vec r)$  $[meV]$")
    
plt.legend(loc = "lower left")

plt.show()
../_images/21b1bc6572e552b9165b2c92621bae162ce949233d9cd92b149f97dd4782e033.png
charge_in_GNR = Integrate(gfQ, mesh, definedon=mesh.Materials("GNR"))

print("Charge in the graphene nano ribbon is", charge_in_GNR, "electrons")
Charge in the graphene nano ribbon is -1.7226442978734058 electrons
# find bandgap:

incr = (pot_max - pot_min) / (en_steps - 1)

found_left = False

found_right = False

threshold = pow(10,-7)

for i in range(int(en_steps/2)):
    
    if found_left == True and found_right == True:
        
        break
    
    left_val = values_charge_density_GNR[int(en_steps/2) - i]
    
    right_val = values_charge_density_GNR[int(en_steps/2) + i]
    
    if np.abs(left_val) > threshold and found_left == False:
        
        found_left = True
        
        left_index = int(en_steps/2) - i
        
    if np.abs(right_val) > threshold and found_right == False:
        
        found_right = True
        
        right_index = int(en_steps/2) + i
        

band_gap = (right_index - left_index) * incr

print("The bandgap is", band_gap*pow(10,3), "meV")
The bandgap is 831.0415520776039 meV
plt.figure(figsize=(11,8))

plt.plot(np.diag(saved_potentials[0])*pow(10,3)/Volt, label = r"$\phi$ $first$", color = "magenta")

plt.plot(np.diag(saved_potentials[-1])*pow(10,3)/Volt, label = r"$\phi$ $final$", color = "black")

plt.hlines(band_gap/2*pow(10,3), 0, npts, color = "blue", label = r"$E_{G}/2$")

plt.hlines(-band_gap/2*pow(10,3), 0, npts, color = "red", label = r"$-E_{G}/2$")
    
plt.title("potential and bandgap on the main diagonal")

plt.ylabel(r"$\phi(\vec r)$  $[meV]$")
    
plt.legend()

plt.show()
../_images/9d577713fafe27f898be2df76c3dd48a5d913b8b9a347bd4da2cce16f06e458d.png