##^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Lance Simms, Stanford University 2009
#FDGrid_1d.py
#
#A class that creates the 1d grid to be used for the finite differencing scheme
#
#INPUTS
#  TP: int
#    The total number of points on the grid
#  Simple: int
#    0 - Approximate diode entirely with lightly doped n material
#    1 - Use p+ n n+ regions with abrupt junctions
#    2 - Use p+ n n+ regions with guassian type grading
#  Mobility: int
#    0 - Use constant mobilities throughout diode
#    1 - Use doping concentration dependent mobilities
#  N_a_max: float
#    Doping concentration in p region
#  N_d_max: float
#    Doping concentration in n region
#  N_bulk: float
#    Doping concentration in bulk region
#  InjectedHoles: float
#    The number of holes added to the p+ implant during a reset
#    (translates to n[0], free carrier density)
#  InjectedElectrons: float
#    The number of electrons added to the p+ implant during a reset
#    (translates to n[0], free carrier density)
#
#############################################

from numpy import *
from pylab import *
import matplotlib.pylab as mplot
import string 
import pdb

class FDGrid_1d:

  #INSTANTIATION######################
  def __init__(self, TP, N_a_max, N_bulk, N_d_max, TvFlag = 0, Simple = 0, \
               InjectedHoles=0, InjectedElectrons=0):
    #Define some constants to be used by objects
    self.e_0   = 8.85419e-12          # F * m^-1 - 8.85419 e-14 F*cm^-1
    self.e_si  = 11.8                 # Relative Permittivity for Si   
    self.q     = double(1.6022e-19)   # Coulombs per electron
    self.k     = double(1.38066e-23)  # Joules per Kelvin
    self.T     = double(300)          # Kelvin
    self.Theta = double(self.q/(self.k*self.T)) # Factor relating energies
    self.n_i   = 1e16                 # intrinsic concentration cm^-3

    ##Built-in potential calculated in Neudeck 
    self.V_bi   = self.k*self.T*log(N_d_max*N_a_max/self.n_i**2)/self.q

    #Junction points
    x_p      = ((2*self.e_si*self.e_0*self.V_bi/self.q)*\
                (N_d_max/(N_a_max*(N_a_max+N_d_max))))**.5
    self.x_p = x_p
    x_n      = ((2*self.e_si*self.e_0*self.V_bi/self.q)*\
                (N_a_max/(N_d_max*(N_a_max+N_d_max))))**.5
    self.x_n = x_n

    #Set up basic parameters and indices
    self.TP   = TP                         # Total number of points in the P region and N
    self.AllP = 4*TP-1                     # Total number of points in entire region
    AllP = self.AllP;  NN = AllP-1         # Total number of points in entire region 
    N_i  = 1.e16                           # Intrinsic concentration

    Num_P   = double(TP)                   # Total number of points in P+ 
    Num_I   = double(TP)                   # Total number of points in I 
    Num_N   = double(TP)                   # Total number of points in N+ 
    Num_Pf  = double(TP)
    Num_PIf = double(TP/8)
    Num_Ic  = double(3*TP/4)
    Num_INf = double(TP/8)
    Num_Nf  = double(TP)
   
    #Define the grid
    self.Dx     = 49e-6
    self.dx_min = 1e-9 
    self.DefineGrid1d(TvFlag=TvFlag)
    self.N_d = zeros(AllP, dtype=double) 
    self.N_a = zeros(AllP, dtype=double)
    self.n = zeros(AllP, dtype=double)
    self.p = zeros(AllP, dtype=double)

    #SIMPLE = 1 =======================Approximate entire Diode as n material
    if Simple == 0:
      self.N_d[:] = double(N_bulk)
      self.N_a[:] = 0
      self.n[:]   = double(N_bulk)
      self.p[:]   = N_i**2/N_bulk
      #Difference 
      self.DC = self.N_d-self.N_a

      self.n[NN]    = self.DC[NN]/2*(1+(1+(2*N_i/self.DC[NN])**2)**.5)
      self.n[0]     = self.DC[0]/2*(1+(1+(2*N_i/self.DC[0])**2)**.5)+InjectedElectrons
      self.p[0]     = N_i**2/self.n[0]+InjectedHoles
      self.p[NN]    = N_i**2/self.n[NN]

    #SIMPLE = 1 =======================Approximate junctions as being abrupt
    elif Simple == 1:
      #Donor concentration
      self.N_d[0:TP]          = double(0)
      self.N_d[TP:3*TP-1]     = double(N_bulk)
      self.N_d[3*TP-1:4*TP-1] = double(N_d_max)  
      #Acceptor concentration
      self.N_a[0:TP]          = double(N_a_max)
      self.N_a[TP:3*TP-1]     = double(0)
      self.N_a[3*TP-1:4*TP-1] = double(0)

      #Now define the hole and electron densities
      self.n[0:TP]          = N_i**2/N_a_max
      self.n[TP:3*TP-1]     = double(N_bulk)
      self.n[3*TP-1:4*TP-1] = double(N_d_max)

      self.p[0:TP]          = N_a_max
      self.p[TP:3*TP-1]     = N_i**2/N_bulk
      self.p[3*TP-1:4*TP-1] = N_i**2/N_d_max

      #Difference 
      self.DC       = self.N_d-self.N_a

      self.n[NN]    = self.DC[NN]/2*(1+(1+(2*N_i/self.DC[NN])**2)**.5)
      self.p[0]     = -self.DC[0]/2*(1+(1+(2*N_i/self.DC[0])**2)**.5)
      self.n[0]     = N_i**2/self.p[0]
      self.p[NN]    = N_i**2/self.n[NN]

    #SIMPLE = 2 =======================Use Gaussian-type doping function
    elif Simple == 2:
  
      #Grade the junction according to p. 55 of Kurata
      x_a = 1.e-6;  #Thickness of p+ region 
      x_b = 1.e-6;  #Thickness of n+ region 
      w = 100.e-6;  #Total width of diode
      m1 = (1./x_a**2)*log(N_a_max/N_bulk)
      m2 = (1./x_b**2)*log(N_d_max/N_bulk)
      
      self.DC = -N_a_max*exp(-m1*self.X**2) +\
                 N_bulk +\
                 N_d_max*exp(-m2*(w-self.X)**2)
      #Now define the hole and electron densities
      self.n[0:TP]          = N_i**2/N_a_max
      self.n[TP:3*TP-1]     = double(N_bulk)
      self.n[3*TP-1:4*TP-1] = double(N_d_max)

      self.p[0:TP]          = N_a_max
      self.p[TP:3*TP-1]     = N_i**2/N_bulk
      self.p[3*TP-1:4*TP-1] = N_i**2/N_d_max

      self.n[NN]    = self.DC[NN]/2*(1+(1+(2*N_i/self.DC[NN])**2)**.5)
      self.p[0]     = -self.DC[0]/2*(1+(1+(2*N_i/self.DC[0])**2)**.5)
      self.n[0]     = N_i**2/self.p[0]
      self.p[NN]    = N_i**2/self.n[NN]


  ########################################################################
  #StretchFunction1d

  #PURPOSE: Generate a 1d grid with fine spacing near the P+ or N+ regions
  #         and smooth transitions to the coarse spacing over the I region
  #
  #         The stretching function is given by
  #         x = Dx*(exp(kx*(I-1)/(IL-1))/(exp(kx)-1))
  #INPUTS: 
  #         I  - The starting index for x in the stretch
  #         IL - The maximum index for x in the stretch
  #         Dx - The total distance (49 microns)
  #         dx_min - The finest spacing in the x mesh
  #        
  def StretchFunction1d(self,I,IL,Dx,dx_min, TvFlag = 0):
 
    TP   = self.TP               #Total amount of points in P, N regions  
    dx_min = 1e-6/TP             #Minimum spacing in mesh
    IFac = double((I-1))/(IL-1)  #The value for the x-exponent
   
    #Initial guess for the stretching coefficient
    kx=1
    for i in arange(10):
     f = dx_min-Dx*(exp(kx*IFac)-1)/(exp(kx)-1)
     f_prime = -Dx*((exp(kx)-1)*IFac*exp(kx*IFac)-(exp(kx*IFac)-1)*exp(kx))/\
                   (exp(kx)-1)**2
     kx = kx - f/f_prime
   
    #Plot the solution to the grid
    if TvFlag == 1:
       i = frange(IL-I+1)+1
       IFac = double((i-1)/(IL-1))
       f = Dx*(exp(kx*IFac)-1)/(exp(kx)-1)
       print 'Maximum x :'+str(f.max())
       print 'Minimum x :'+str(f.min())
       mplot.figure(0)
       mplot.clf()
       mplot.vlines(f, 0, 1)

    #Create the coefficient as a member of the class
    self.kx = kx

  def DefineGrid1d(self, TvFlag=0):

    TP   = self.TP               #Total amount of points in P, N regions  
    AllP = self.AllP             #Total amount of points in entire grid
    dx_min = 1e-6/TP             #Minimum spacing in mesh
    I = 2                        #The second index in the mesh
    i = frange(TP-I+1)+1         #indices for the stretched portion of the grid
    IFac = double((i-1)/(TP-1))  #The exponent in the stretching function
    IFacR = IFac[::-1]           #The reversed set of exponents

    #Generate the stretching coefficient that solves for the grid
    self.StretchFunction1d(2,TP, self.Dx, self.dx_min, TvFlag=TvFlag)
    kx = self.kx
 
    #Create the actual grid values in microns
    X   = zeros(TP*4,dtype=double)
    Xp  = frange(0,1e-6-dx_min, dx_min)
    Xn  = frange(99e-6+dx_min,100e-6, dx_min)
    Xpi = self.Dx*(exp(kx*IFac)-1)/(exp(kx)-1)+1e-6
    Xin = 100e-6-Xpi[::-1]
    X   = concatenate([Xp, Xpi, Xin[1:], Xn])
    self.X    = X
    self.Xpi  = Xpi                                      #KURATA
    self.dx   = X[1:AllP]-X[0:AllP-1]                    #h(M)
    self.dx_p = (self.dx[1:AllP-1]+self.dx[0:AllP-2])/2  #h'(N)
    self.m_a  = self.dx[1:AllP-1]/(2*self.dx_p)          #h(M)/2h'(N)
    self.m_b  = self.dx[0:AllP-2]/(2*self.dx_p)          #h(M-1)/2h'(N)
