├── .gitignore ├── html_documentation ├── example1.png ├── example2.png ├── example3.png ├── example4.png ├── example5.png └── index.html ├── README.txt ├── LICENSE.txt ├── setup.py └── eq_band_diagram.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | eq_band_diagram.egg-info/ 3 | *.zip 4 | *.pyc 5 | -------------------------------------------------------------------------------- /html_documentation/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbyrnes321/eq_band_diagram/HEAD/html_documentation/example1.png -------------------------------------------------------------------------------- /html_documentation/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbyrnes321/eq_band_diagram/HEAD/html_documentation/example2.png -------------------------------------------------------------------------------- /html_documentation/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbyrnes321/eq_band_diagram/HEAD/html_documentation/example3.png -------------------------------------------------------------------------------- /html_documentation/example4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbyrnes321/eq_band_diagram/HEAD/html_documentation/example4.png -------------------------------------------------------------------------------- /html_documentation/example5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbyrnes321/eq_band_diagram/HEAD/html_documentation/example5.png -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Python program for calculating 1D (i.e. planar) equilibrium semiconductor band diagrams. 2 | 3 | For information go to: http://pythonhosted.org/eq_band_diagram/ 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Steven Byrnes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from setuptools import setup 5 | 6 | long_description = ("Calculates equilibrium band diagrams for planar " 7 | "multilayer semiconductor structures. To learn " 8 | "more go to " 9 | "http://pythonhosted.org/eq_band_diagram/") 10 | 11 | descrip = ("Calculates equilibrium band diagrams for planar multilayer " 12 | "semiconductor structures.") 13 | 14 | setup( 15 | name = "eq_band_diagram", 16 | version = "0.1.0", 17 | author = "Steven Byrnes", 18 | author_email = "steven.byrnes@gmail.com", 19 | description = descrip, 20 | license = "MIT", 21 | keywords = "semiconductor physics, poisson equation, boltzmann equation, finite differences", 22 | url = "http://pythonhosted.org/eq_band_diagram/", 23 | py_modules=['eq_band_diagram'], 24 | long_description=long_description, 25 | classifiers=[ 26 | "Development Status :: 4 - Beta", 27 | "Intended Audience :: Science/Research", 28 | "Topic :: Scientific/Engineering", 29 | "Topic :: Scientific/Engineering :: Physics", 30 | "License :: OSI Approved :: MIT License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python :: 2", 33 | "Programming Language :: Python :: 3", 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /html_documentation/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | Documentation for eq_band_diagram 8 | 9 | 10 | 11 |

Documentation for eq_band_diagram

12 | 13 |

Download the software at http://pypi.python.org/pypi/eq_band_diagram - or submit complaints and corrections at its GitHub page

14 | 15 |

Written by Steve Byrnes, 2012 (http://sjbyrnes.com)

16 | 17 |

Software is released under MIT license.

18 | 19 |

Overview

20 | 21 |

This is a program written in Python / NumPy that calculates equilibrium band structures of planar multilayer semiconductor structures. (It solves the "Poisson-Boltzmann equation" via finite differences.)

22 | 23 |

"Equilibrium" means "flat fermi level", with V=0 and I=0. This assumption makes the calculation much much simpler. If you look at just the essential algorithm (leaving out my explanations, examples, plots, tests, etc.), it would not even be 50 lines of code.

24 | 25 |

So this is a relatively simple program for a relatively simple calculation. You can understand how it works, play around with it, etc. But if you're interested in serious, accurate semiconductor simulation, then don't waste your time with this. Get a real TCAD program!

26 | 27 |

Examples

28 | 29 |

example1(): A p / n junction

30 | example1() 31 | 32 |

example2(): A n+ / n junction

33 | example2() 34 | 35 |

example3(): A bipolar transistor structure

36 | example3() 37 |

(This one is not expected to be quantitatively accurate, because it involves very heavily doped layers. See below.)

38 | 39 |

example4(): A depletion region

40 | example4() 41 | 42 |

example5(): Comparing to (analytical) depletion approximation for p-n junction

43 | example5() 44 | 45 |

Package setup and installation

46 | 47 |

All the code is a single python module. (Written in Python 2.7, but as far as I can tell it should be compatible with Python 3 also. Please email me if you have tried.) It requires numpy and matplotlib. The inner-loop calculation is vectorized in numpy, so the calculation runs quite quickly. (Most plots are generated in ~1 second.) The module requires no setup or compilation, so you can just download it and use it. Or, if you prefer, run

48 |
pip install eq_band_diagram
49 |

to automatically download it into the default folder.

50 | 51 |

What's in the module?

52 | 53 |

The functions are all described with docstrings, so if you read the source code you'll hopefully see what's going on. (There is no other documentation besides this page.)

54 | 55 |

The top of the module contains calc_core(), which has the main algorithm, along with its helper functions local_charge() (which infers charge from vacuum level) and Evac_minus_EF_from_charge() (which infers vacuum level from charge)

56 | 57 |

The second section of the module contains a more convenient interface / wrapper to the main algorithm. There is a Material class containing density-of-states, electron affinity, and similar such information. (Two Materials are already defined: GaAs and Si.) There is a Layer class that holds a material, thickness, and doping. calc_layer_stack() takes a list of Layers and calls the main algorithm, while plot_bands() displays the result.

58 | 59 |

The third section of the module contains examples: example1(), ..., example5(). The results are plotted above. This section also has a special function that makes a plot comparing a numerical solution to the depletion approximation, as shown in example5().

60 | 61 |

The final section of the module has a few test scripts.

62 | 63 |

Calculation method

64 | 65 |

Since this is equilibrium, the fermi level is flat. We set it as the zero of energy. Start with the Poisson equation (i.e. Gauss's law):

66 | 67 |
Evac'' = net_charge / epsilon
68 | 69 |

(where '' is second derivative in space and Evac is the vacuum energy level.)

70 | 71 |

(Remember Evac = -electric potential + constant. The minus sign is because Evac is related to electron energy, and electrons have negative charge.) Using finite differences:

72 | 73 |
(1/2) * dx^2 * Evac''[i] = (Evac[i+1] + Evac[i-1])/2 - Evac[i]
74 | 75 |

Therefore, the main equation we solve is:

76 | 77 |
Evac[i] = (Evac[i+1] + Evac[i-1])/2 - (1/2) * dx^2 * net_charge[i] / epsilon
78 | 79 |

Algorithm: The right-hand-side at the previous time-step gives the left-hand-side at the next time step. A little twist, which suppresses a numerical oscillation, is that net_charge[i] is inferred not from the Evac[i] at the last time step, but instead from the (Evac[i+1] + Evac[i-1])/2 at the last time step.

80 | 81 |

Boundary values: The boundary values of Evac (i.e., the values at the start of the leftmost layer and the end of the rightmost layer) are kept fixed at predetermined values. You can specify the boundary values yourself, or you can use the default, wherein values are chosen that make the material is charge-neutral at the boundaries.

82 | 83 |

Seed: Start with the Evac profile wherein everything is charge-neutral, except possibly the first and last points.

84 | 85 |

Assumptions

86 | 87 |

Anderson's rule: The vacuum level is assumed to be continuous, so that band alignments are related to electron affinities. Well, that's how the program is written, but you can always lie about the electron affinity in order to get whatever band alignment you prefer.

88 | 89 |

Nondegenerate electron and hole concentrations: We use formulas like n ~ exp((EF - EC) / kT). So band-filling and nonparabolicity is ignored. This will not be accurate if the electron and hole concentrations are too high.

90 | 91 |

100% ionized donors and acceptors: It's assumed that all dopants are ionized. Usually that is a pretty safe assumption for common dopants in common semiconductors at higher-than-cryogenic-temperature.

92 | 93 |

Others: Quantum effects is neglected, etc. etc.

94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /eq_band_diagram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Written by Steve Byrnes, 2012 -- http://sjbyrnes.com - steven.byrnes@gmail.com 4 | 5 | Calculates equilibrium band structures of 1D semiconductor stacks. (i.e., it 6 | solves the poisson-boltzmann equation by the finite differences method.) 7 | 8 | This is the only module in the package. It's written in Python 2.7. 9 | 10 | See http://packages.python.org/eq_band_diagram for general discussion and 11 | overview. The functions are all described in their docstrings (below). There 12 | is no other documentation besides that. 13 | 14 | Try running example1(), example2(), ..., example5(). If you look at the code 15 | for those, it can be a starting-point for your own calculations. 16 | """ 17 | #Copyright (C) 2012 Steven Byrnes 18 | # 19 | #Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | # 21 | #The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | # 23 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | from __future__ import division, print_function 26 | 27 | import numpy as np 28 | import math 29 | import matplotlib.pyplot as plt 30 | inf = float('inf') 31 | 32 | # Boltzmann constant times the semiconductor temperature, expressed in eV 33 | # I'm assuming 300 K. 34 | kT_in_eV = 0.02585 35 | 36 | # e > 0 is the elementary charge. We are expressing charge densities in 37 | # e/cm^3, voltages in volts, and distances in nanometers. So the value of 38 | # epsilon_0 (permittivity of free space) is expressed in the strange units 39 | # of ((e/cm^3) / (V/nm^2)) 40 | eps0_in_e_per_cm3_over_V_per_nm2 = 5.5263e19 41 | 42 | 43 | ############################################################################ 44 | ############################# CORE CALCULATION ############################# 45 | ############################################################################ 46 | 47 | 48 | def local_charge(Evac_minus_Ei, ni, charge_from_dopants, Evac_minus_EF): 49 | """ 50 | Calculate local charge. This function is VECTORIZED, i.e. if all the 51 | inputs are numpy arrays of the same size, so is the output. (But the 52 | function also works for float inputs.) (!!!!! CHECK !!!!) 53 | 54 | Inputs 55 | ------ 56 | 57 | * Evac_minus_Ei is the [positive] difference (in eV) between the local 58 | vacuum level and the intrinsic fermi level (level at which p=n). 59 | 60 | * ni = intrinsic electron concentration in cm^-3 (defined by p = n = ni 61 | when the undoped material is in thermal equilibrium) 62 | 63 | * charge_from_dopants (in e/cm^3) equals the density of ionized donors 64 | minus the density of ionized acceptors. (e>0 is the elementary charge.) 65 | 66 | * Evac_minus_EF is the [positive] difference (in eV) between the local 67 | vacuum level and fermi level. 68 | 69 | Output 70 | ------ 71 | 72 | Outputs a dictionary with entries: 73 | 74 | * n, the density of free electrons in cm^-3; 75 | 76 | * p, and density of free holes in cm^-3; 77 | 78 | * net_charge, the net space charge in e/cm^3. 79 | """ 80 | EF_minus_Ei = Evac_minus_Ei - Evac_minus_EF 81 | n = ni * np.exp(EF_minus_Ei / kT_in_eV) 82 | p = ni**2 / n 83 | return {'n':n, 'p':p, 'net_charge':(p - n + charge_from_dopants)} 84 | 85 | def Evac_minus_EF_from_charge(Evac_minus_Ei, ni, charge_from_dopants, net_charge): 86 | """ 87 | What value of (vacuum level minus fermi level) yields a local net 88 | charge equal to net_charge? 89 | 90 | See local_charge() for units and definitions related to inputs and 91 | outputs. 92 | 93 | Function is NOT vectorized. Inputs must be floats, not arrays. (This 94 | function is not called in inner loops, so speed doesn't matter.) 95 | """ 96 | # eh_charge is the charge from electrons and holes only 97 | eh_charge = net_charge - charge_from_dopants 98 | 99 | if eh_charge > 30 * ni: 100 | # Plenty of holes, negligible electrons 101 | p = eh_charge 102 | return Evac_minus_Ei + kT_in_eV * math.log(p / ni) 103 | if eh_charge < -30 * ni: 104 | # Plenty of electrons, negligible holes 105 | n = -eh_charge 106 | return Evac_minus_Ei - kT_in_eV * math.log(n / ni) 107 | 108 | # Starting here, we are in the situation where BOTH holes and electrons 109 | # need to be taken into account. Solve the simultaneous equations 110 | # p * n = ni**2 and p - n = eh_charge to get p and n. 111 | 112 | def solve_quadratic_equation(a,b,c): 113 | """ return larger solution to ax^2 + bx + c = 0 """ 114 | delta = b**2 - 4 * a * c 115 | if delta < 0: 116 | raise ValueError("No real solution...that shouldn't happen!") 117 | return (-b + math.sqrt(delta)) / (2*a) 118 | 119 | if eh_charge > 0: 120 | # Slightly more holes than electrons 121 | p = solve_quadratic_equation(1, -eh_charge, -ni**2) 122 | return Evac_minus_Ei + kT_in_eV * math.log(p / ni) 123 | else: 124 | # Slightly more electrons than holes 125 | n = solve_quadratic_equation(1, eh_charge, -ni**2) 126 | return Evac_minus_Ei - kT_in_eV * math.log(n / ni) 127 | 128 | 129 | def calc_core(points, eps, charge_from_dopants, Evac_minus_Ei, ni, 130 | tol=1e-5, max_iterations=inf, Evac_start=None, Evac_end=None): 131 | """ 132 | Core routine for the calculation. Since it's a bit unweildy to input all 133 | these parameters by hand, you should normally use the wrapper 134 | calc_layer_stack() below. 135 | 136 | Inputs 137 | ------ 138 | 139 | * points is a numpy list of coordinates, in nm, where we will find Evac. 140 | They must be in increasing order and equally spaced. 141 | 142 | * eps is a numpy list with the static dielectric constant at each point 143 | (unitless, i.e. epsilon / epsilon0) 144 | 145 | * charge_from_dopants is a numpy list with the net charge (in e/cm^3 146 | where e>0 is the elementary charge) from ionized donors or acceptors 147 | at each point. Normally one assumes all dopants are ionized, so it's 148 | equal to the doping for n-type, or negative the doping for p-type. 149 | 150 | * Evac_minus_Ei is a numpy list of the [positive] energy difference (in 151 | eV) between the vacuum level and the "intrinsic" fermi level at each 152 | point. ("intrinsic" means the fermi level at which p=n). 153 | 154 | * ni is a numpy list with the intrinsic electron concentration (in cm^-3) 155 | at each point. (Defined by p = n = ni in undoped equilibrium) 156 | 157 | * tol (short for tolerance) specifies the stopping point. A smaller 158 | number gives more accurate results. Each iteration step, we check 159 | whether Evac at any point moved by more than tol (in eV). If not, then 160 | terminate. Note: This does NOT mean that the answer will be within 161 | tol of the exact answer. Suggestion: Try 1e-4, 1e-5, 1e-6, etc. until 162 | the answer stops visibly changing. 163 | 164 | * max_iterations: How many iterations to do, before quitting even if the 165 | algorithm has not converged. 166 | 167 | * Evac_start is "vacuum energy minus fermi level in eV" at the first 168 | point. If it's left at the default value (None), we choose the value 169 | that makes it charge-neutral. 170 | 171 | * Evac_end is ditto for the end of the last layer. 172 | 173 | Method 174 | ------ 175 | 176 | Since this is equilibrium, the fermi level is flat. We set it as the 177 | zero of energy. 178 | 179 | Start with Gauss's law: 180 | 181 | Evac'' = net_charge / epsilon 182 | 183 | (where '' is second derivative in space.) 184 | 185 | (Remember Evac = -electric potential + constant. The minus sign is 186 | because Evac is related to electron energy, and electrons have negative 187 | charge.) 188 | 189 | Using finite differences, 190 | (1/2) * dx^2 * Evac''[i] = (Evac[i+1] + Evac[i-1])/2 - Evac[i] 191 | 192 | Therefore, the MAIN EQUATION WE SOLVE: 193 | 194 | Evac[i] = (Evac[i+1] + Evac[i-1])/2 - (1/2) * dx^2 * net_charge[i] / epsilon 195 | 196 | ALGORITHM: The RHS at the previous time-step gives the LHS at the 197 | next time step. A little twist, which suppresses a numerical 198 | oscillation, is that net_charge[i] is inferred not from the Evac[i] at 199 | the last time step, but instead from the (Evac[i+1] + Evac[i-1])/2 at 200 | the last time step. The first and last values of Evac are kept fixed, see 201 | above. 202 | 203 | SEED: Start with the Evac profile wherein everything is charge-neutral. 204 | 205 | Output 206 | ------ 207 | 208 | The final Evac (vacuum energy level) array (in eV). This is equivalent 209 | to minus the electric potential in V. 210 | """ 211 | dx = points[1] - points[0] 212 | if max(np.diff(points)) > 1.001 * dx or min(np.diff(points)) < 0.999 * dx: 213 | raise ValueError('Error! points must be equally spaced!') 214 | if dx <= 0: 215 | raise ValueError('Error! points must be in increasing order!') 216 | 217 | num_points = len(points) 218 | 219 | # Seed for Evac 220 | seed_charge = np.zeros(num_points) 221 | Evac = [Evac_minus_EF_from_charge(Evac_minus_Ei[i], ni[i], 222 | charge_from_dopants[i], seed_charge[i]) 223 | for i in range(num_points)] 224 | Evac = np.array(Evac) 225 | if Evac_start is not None: 226 | Evac[0] = Evac_start 227 | if Evac_end is not None: 228 | Evac[-1] = Evac_end 229 | 230 | ###### MAIN LOOP ###### 231 | 232 | iters=0 233 | err=inf 234 | while err > tol and iters < max_iterations: 235 | iters += 1 236 | 237 | prev_Evac = Evac 238 | 239 | Evac = np.zeros(num_points) 240 | 241 | Evac[0] = prev_Evac[0] 242 | Evac[-1] = prev_Evac[-1] 243 | # Set Evac[i] = (prev_Evac[i-1] + prev_Evac[i+1])/2 244 | Evac[1:-1] = (prev_Evac[0:-2] + prev_Evac[2:])/2 245 | charge = local_charge(Evac_minus_Ei, ni, charge_from_dopants, 246 | Evac)['net_charge'] 247 | Evac[1:-1] -= 0.5 * dx**2 * charge[1:-1] / (eps[1:-1] 248 | * eps0_in_e_per_cm3_over_V_per_nm2) 249 | 250 | err = max(abs(prev_Evac - Evac)) 251 | 252 | if False: 253 | # Optional: graph Evac a few times during the process to see 254 | # how it's going. 255 | if 5 * iters % max_iterations < 5: 256 | plt.figure() 257 | plt.plot(points, prev_Evac, points, Evac) 258 | if iters == max_iterations: 259 | print('Warning! Did not meet error tolerance. Evac changed by up to (' 260 | + '{:e}'.format(err) + ')eV in the last iteration.' ) 261 | else: 262 | print('Met convergence criterion after ' + str(iters) 263 | + ' iterations.') 264 | 265 | return Evac 266 | 267 | 268 | 269 | ############################################################################ 270 | ############# MORE CONVENIENT INTERFACE / WRAPPERS ######################### 271 | ############################################################################ 272 | 273 | class Material: 274 | """ 275 | Semiconductor material with the following properties... 276 | 277 | NC = conduction-band effective density of states in cm^-3 278 | 279 | NV = valence-band effective density of states in cm^-3 280 | 281 | EG = Band gap in eV 282 | 283 | chi = electron affinity in eV (i.e. difference between conduction 284 | band and vacuum level) 285 | 286 | eps = static dielectric constant (epsilon / epsilon0) 287 | 288 | ni = intrinsic electron concentration in cm^-3 (defined by p = n = ni 289 | when the undoped material is in thermal equilibrium) 290 | 291 | Evac_minus_Ei is the [positive] energy difference (in eV) between the 292 | vacuum level and the "intrinsic" fermi level, i.e. the fermi level 293 | at which p=n. 294 | 295 | name = a string describing the material (for plot labels etc.) 296 | """ 297 | 298 | def __init__(self, NC, NV, EG, chi, eps, name=''): 299 | self.NC = NC 300 | self.NV = NV 301 | self.EG = EG 302 | self.chi = chi 303 | self.eps = eps 304 | self.name = name 305 | 306 | # Sze equation (29), p21... 307 | self.ni = math.sqrt(self.NC * self.NV * math.exp(-self.EG / kT_in_eV)) 308 | # Sze equation (27), p20... 309 | self.Evac_minus_Ei = (self.chi + 0.5 * self.EG 310 | + 0.5 * kT_in_eV * math.log(self.NC / self.NV)) 311 | 312 | #Sze Appendix G 313 | 314 | GaAs = Material(NC=4.7e17, 315 | NV=7.0e18, 316 | EG=1.42, 317 | chi=4.07, 318 | eps=12.9, 319 | name='GaAs') 320 | 321 | Si = Material(NC=2.8e19, 322 | NV=2.65e19, 323 | EG=1.12, 324 | chi=4.05, 325 | eps=11.9, 326 | name='Si') 327 | 328 | class Layer: 329 | """ 330 | Layer of semiconductor with the following properties... 331 | 332 | matl = a material (an object with Material class) 333 | 334 | n_or_p = a string, either 'n' or 'p', for the doping polarity 335 | 336 | doping = density of dopants in cm^-3 337 | 338 | thickness = thickness of the layer in nm 339 | """ 340 | def __init__(self, matl, n_or_p, doping, thickness): 341 | self.matl = matl 342 | self.n_or_p = n_or_p 343 | self.doping = doping 344 | self.thickness = thickness 345 | 346 | def where_am_I(layers, distance_from_start): 347 | """ 348 | distance_from_start is the distance from the start of layer 0. 349 | 350 | layers is a list of each layer; each element should be a Layer object. 351 | 352 | Return a dictionary {'current_layer':X, 'distance_into_layer':Y}. 353 | (Note: X is a Layer object, not an integer index.) 354 | """ 355 | d = distance_from_start 356 | if distance_from_start < 0: 357 | raise ValueError('Point is outside all layers!') 358 | layer_index = 0 359 | while layer_index <= (len(layers) - 1): 360 | current_layer = layers[layer_index] 361 | if distance_from_start <= current_layer.thickness: 362 | return {'current_layer':current_layer, 363 | 'distance_into_layer':distance_from_start} 364 | else: 365 | distance_from_start -= current_layer.thickness 366 | layer_index += 1 367 | raise ValueError('Point is outside all layers! distance_from_start=' 368 | + str(d)) 369 | 370 | 371 | def calc_layer_stack(layers, num_points, tol=1e-5, max_iterations=inf, 372 | Evac_start=None, Evac_end=None): 373 | """ 374 | This is a wrapper around calc_core() that makes it more convenient to 375 | use. See example1(), example2(), etc. (below) for samples. 376 | 377 | Inputs 378 | ------ 379 | 380 | * layers is a list of the "layers", where each "layer" is a Layer 381 | object. 382 | 383 | * num_points is the number of points at which to solve for Evac. 384 | (They will be equally spaced.) 385 | 386 | * tol, max_iterations, Evac_start, and Evac_end are defined the same as 387 | in calc_core() above. 388 | 389 | Outputs 390 | ------- 391 | 392 | A dictionary with... 393 | 394 | * 'points', the 1d array of point coordinates (x=0 is the start of 395 | the 0'th layer.) 396 | 397 | * 'Evac', the 1d array of vacuum energy level in eV 398 | """ 399 | total_thickness = sum(layer.thickness for layer in layers) 400 | points = np.linspace(0, total_thickness, num=num_points) 401 | # Note: layer_list is NOT the same as layers = [layer0, layer1, ...], 402 | # layer_list is [layer0, layer0, ... layer1, layer1, ... ], i.e. the 403 | # layer of each successive point. 404 | layer_list = [where_am_I(layers, pt)['current_layer'] 405 | for pt in points] 406 | matl_list = [layer.matl for layer in layer_list] 407 | eps = np.array([matl.eps for matl in matl_list]) 408 | charge_from_dopants = np.zeros(num_points) 409 | for i in range(num_points): 410 | if layer_list[i].n_or_p == 'n': 411 | charge_from_dopants[i] = layer_list[i].doping 412 | elif layer_list[i].n_or_p == 'p': 413 | charge_from_dopants[i] = -layer_list[i].doping 414 | else: 415 | raise ValueError("n_or_p should be either 'n' or 'p'!") 416 | ni = np.array([matl.ni for matl in matl_list]) 417 | Evac_minus_Ei = np.array([matl.Evac_minus_Ei for matl in matl_list]) 418 | 419 | Evac = calc_core(points, eps, charge_from_dopants, Evac_minus_Ei, ni, 420 | tol=tol, max_iterations=max_iterations, 421 | Evac_start=Evac_start, Evac_end=Evac_end) 422 | return {'points':points, 'Evac':Evac} 423 | 424 | 425 | def plot_bands(calc_layer_stack_output, layers): 426 | """ 427 | calc_layer_stack_output is an output you would get from running 428 | calc_layer_stack(). layers is defined as in calc_layer_stack() 429 | """ 430 | points = calc_layer_stack_output['points'] 431 | Evac = calc_layer_stack_output['Evac'] 432 | num_points = len(points) 433 | 434 | # Note: layer_list is NOT the same as layers = [layer0, layer1, ...], 435 | # layer_list is [layer0, layer0, ... layer1, layer1, ... ], i.e. the 436 | # layer of each successive point. 437 | layer_list = [where_am_I(layers, pt)['current_layer'] 438 | for pt in points] 439 | matl_list = [layer.matl for layer in layer_list] 440 | chi_list = [matl.chi for matl in matl_list] 441 | EG_list = [matl.EG for matl in matl_list] 442 | CB_list = [Evac[i] - chi_list[i] for i in range(num_points)] 443 | VB_list = [CB_list[i] - EG_list[i] for i in range(num_points)] 444 | EF_list = [0 for i in range(num_points)] 445 | 446 | plt.figure() 447 | 448 | plt.plot(points,CB_list,'k-', #conduction band: solid black line 449 | points,VB_list,'k-', #valence band: solid black line 450 | points,EF_list,'r--') #fermi level: dashed red line 451 | 452 | # Draw vertical lines at the boundaries of layers 453 | for i in range(len(layers)-1): 454 | plt.axvline(sum(layer.thickness for layer in layers[0:i+1]),color='k') 455 | 456 | # The title of the graph describes the stack 457 | # for example "1.3e18 n-Si / 4.5e16 p-Si / 3.2e17 n-Si" 458 | layer_name_string_list = ['{:.1e}'.format(layer.doping) + ' ' 459 | + layer.n_or_p + '-' + layer.matl.name 460 | for layer in layers] 461 | plt.title(' / '.join(layer_name_string_list)) 462 | plt.xlabel('Position (nm)') 463 | plt.ylabel('Electron energy (eV)') 464 | plt.xlim(0, sum(layer.thickness for layer in layers)) 465 | 466 | ############################################################################ 467 | ############################### EXAMPLES ################################### 468 | ############################################################################ 469 | 470 | 471 | def example1(): 472 | """ 473 | Example 1: Plot the equilibrium band diagram for an p / n junction 474 | """ 475 | # doping density is in cm^-3; thickness is in nm. 476 | layer0 = Layer(matl=Si, n_or_p='p', doping=1e16, thickness=350) 477 | layer1 = Layer(matl=Si, n_or_p='n', doping=2e16, thickness=200) 478 | 479 | layers = [layer0, layer1] 480 | 481 | temp = calc_layer_stack(layers, num_points=100, tol=1e-6, max_iterations=inf) 482 | 483 | plot_bands(temp, layers) 484 | 485 | 486 | def example2(): 487 | """ Example 2: Plot the equilibrium band diagram for an n+ / n junction """ 488 | # doping density is in cm^-3; thickness is in nm. 489 | layer0 = Layer(matl=GaAs, n_or_p='n', doping=1e17, thickness=100) 490 | layer1 = Layer(matl=GaAs, n_or_p='n', doping=1e15, thickness=450) 491 | 492 | layers = [layer0, layer1] 493 | 494 | temp = calc_layer_stack(layers, num_points=100, tol=1e-6, max_iterations=inf) 495 | 496 | plot_bands(temp, layers) 497 | 498 | 499 | def example3(): 500 | """ 501 | Example 3: Plot the equilibrium band diagram for a BJT-like n+ / p / n / n+ 502 | junction. Parameters based on example in Sze chapter 5 page 248. 503 | 504 | Note that num_points is a pretty large number (fine mesh). If you try a 505 | coarser mesh, the algorithm does not converge, but rather diverges with 506 | oscillations. (It's very sensitive when there are heavily-doped layers.) 507 | 508 | Remember that this simulation will NOT be quantitatively accurate, 509 | because I used a formula for n in terms of EF that is not valid at high 510 | concentration (it neglects band-filling and nonparabolicity). 511 | """ 512 | # doping density is in cm^-3; thickness is in nm. 513 | layer0 = Layer(matl=Si, n_or_p='n', doping=1e20, thickness=150) 514 | layer1 = Layer(matl=Si, n_or_p='p', doping=1e18, thickness=100) 515 | layer2 = Layer(matl=Si, n_or_p='n', doping=3e16, thickness=150) 516 | layer3 = Layer(matl=Si, n_or_p='n', doping=1e20, thickness=50) 517 | 518 | layers = [layer0, layer1, layer2, layer3] 519 | 520 | temp = calc_layer_stack(layers, num_points=600, tol=1e-4, max_iterations=inf) 521 | 522 | plot_bands(temp, layers) 523 | 524 | 525 | def example4(): 526 | """ 527 | Example 4: n-silicon with surface depletion (due to a gate or Schottky 528 | contact.) 529 | """ 530 | layer0 = Layer(matl=Si, n_or_p='n', doping=1e15, thickness=1000.) 531 | 532 | layers = [layer0] 533 | 534 | temp = calc_layer_stack(layers, num_points=100, tol=1e-6, 535 | max_iterations=inf, Evac_start=4.7) 536 | 537 | plot_bands(temp, layers) 538 | 539 | 540 | def compare_to_depletion_approx(p_doping, n_doping, matl): 541 | """ 542 | Compare my program to the full-depletion approximation for a p-n junction 543 | (wherein you calculate the potential profile by assuming n=p=0 in a 544 | certain region and that the material is charge-neutral outside that 545 | region). 546 | 547 | Ref: http://ecee.colorado.edu/~bart/book/pnelec.htm 548 | """ 549 | # vacuum level on the p-side and n-side far from the junction 550 | Evac_lim_p = Evac_minus_EF_from_charge(matl.Evac_minus_Ei, matl.ni, 551 | charge_from_dopants=-p_doping, net_charge=0) 552 | Evac_lim_n = Evac_minus_EF_from_charge(matl.Evac_minus_Ei, matl.ni, 553 | charge_from_dopants=n_doping, net_charge=0) 554 | 555 | # built-in voltage in V 556 | built_in_voltage = Evac_lim_p - Evac_lim_n 557 | 558 | # w is the total depletion width in nm in the depletion approximation 559 | w = math.sqrt(2 * matl.eps * eps0_in_e_per_cm3_over_V_per_nm2 560 | * (1/n_doping + 1/p_doping) * built_in_voltage) 561 | 562 | # Here is the numerical calculation... 563 | p_layer_width = 1.5 * w 564 | n_layer_width = 1.5 * w 565 | layer0 = Layer(matl=matl, n_or_p='p', doping=p_doping, thickness=p_layer_width) 566 | layer1 = Layer(matl=matl, n_or_p='n', doping=n_doping, thickness=n_layer_width) 567 | layers = [layer0, layer1] 568 | temp = calc_layer_stack(layers, num_points=300, tol = 1e-6, 569 | max_iterations=inf) 570 | Evac_numerical = temp['Evac'] 571 | points = temp['points'] 572 | 573 | # Back to the analytical, depletion-approximation calculation 574 | 575 | # xp and xn are depletion widths on n and p sides respectively 576 | xp = w * n_doping / (p_doping + n_doping) 577 | xn = w * p_doping / (p_doping + n_doping) 578 | 579 | # second derivative of Evac in the depletion region 580 | Evac_2nd_deriv_p = -p_doping / (matl.eps * eps0_in_e_per_cm3_over_V_per_nm2) 581 | Evac_2nd_deriv_n = n_doping / (matl.eps * eps0_in_e_per_cm3_over_V_per_nm2) 582 | 583 | # Coordinates for the start and end of the depletion region 584 | depletion_edge_p = p_layer_width - xp 585 | depletion_edge_n = p_layer_width + xn 586 | 587 | # Function giving the analytical value for Evac 588 | def Evac_analytical_fn(x): 589 | if x < depletion_edge_p: 590 | # Point is outside depletion region on p-side 591 | return Evac_lim_p 592 | if x > depletion_edge_n: 593 | # Point is outside depletion region on n-side 594 | return Evac_lim_n 595 | if x < p_layer_width: 596 | # Point is in depletion region on p-side 597 | return Evac_lim_p + 0.5 * Evac_2nd_deriv_p * (x - depletion_edge_p)**2 598 | else: 599 | # Point is in depletion region on n-side 600 | return Evac_lim_n + 0.5 * Evac_2nd_deriv_n * (x - depletion_edge_n)**2 601 | 602 | Evac_analytical = [Evac_analytical_fn(x) for x in points] 603 | plt.figure() 604 | plt.plot(points, Evac_analytical, 'b', points, Evac_numerical, 'r') 605 | layer_name_string_list = ['{:.1e}'.format(layer.doping) + ' ' 606 | + layer.n_or_p + '-' + layer.matl.name 607 | for layer in layers] 608 | plt.title('Blue: Depletion approximation. Red: Numerical calculation.\n' 609 | + ' / '.join(layer_name_string_list)) 610 | plt.xlabel('Position (nm)') 611 | plt.ylabel('Vacuum level (eV)') 612 | plt.xlim(0,p_layer_width + n_layer_width) 613 | # Draw vertical line at boundary 614 | plt.axvline(p_layer_width,color='k') 615 | 616 | def example5(): 617 | """ 618 | Example 4: A p-n junction: Does the numerical calculation agree with the 619 | depletion approximation? 620 | """ 621 | compare_to_depletion_approx(1e16, 2e16, Si) 622 | 623 | ############################################################################ 624 | ########## SOME TESTS THAT HELPER-FUNCTIONS ARE CODED CORRECTLY ############ 625 | ############################################################################ 626 | 627 | 628 | def local_charge_and_Ei_test(): 629 | """ Test that Ei calculation is consistent with local_charge()""" 630 | print('p should equal n here...') 631 | print(local_charge(GaAs.Evac_minus_Ei, GaAs.ni, 0, GaAs.Evac_minus_Ei)) 632 | 633 | def Evac_minus_EF_from_charge_test(): 634 | """ 635 | Check that Evac_minus_EF_from_charge() is correct 636 | """ 637 | matl = GaAs 638 | Evac_minus_Ei = matl.Evac_minus_Ei 639 | ni = matl.ni 640 | for doping_type in ['n','p']: 641 | for doping in [0, matl.ni, matl.ni*1e3]: 642 | charge_from_dopants = doping * (1 if doping_type=='n' else -1) 643 | for target_net_charge in set([-doping, 0, doping]): 644 | temp1 = Evac_minus_EF_from_charge(Evac_minus_Ei, ni, 645 | charge_from_dopants, 646 | target_net_charge) 647 | temp2 = local_charge(Evac_minus_Ei, ni, charge_from_dopants, 648 | temp1) 649 | print('\nDoping: ' '{:.3e}'.format(doping), doping_type, 650 | ', Net charge goal:', '{:.3e}'.format(target_net_charge)) 651 | print('n:', '{:.3e}'.format(temp2['n']), 652 | ' p:', '{:.3e}'.format(temp2['p']), 653 | ' net charge:', '{:.3e}'.format(temp2['net_charge'])) 654 | 655 | def where_am_I_test(): 656 | """Test that where_am_I() is coded correctly""" 657 | # doping density should be in cm^-3; thickness should be in nm. 658 | layer0 = Layer(matl=GaAs, n_or_p='n', doping=1e18, thickness=100) 659 | layer1 = Layer(matl=Si, n_or_p='n', doping=1e16, thickness=50) 660 | layers = [layer0, layer1] 661 | print('The following should be True...') 662 | print(where_am_I(layers, 23)['current_layer'] is layer0) 663 | print(where_am_I(layers, 23)['distance_into_layer'] == 23) 664 | print(where_am_I(layers,123)['current_layer'] is layer1) 665 | print(where_am_I(layers,123)['distance_into_layer'] == 23) 666 | --------------------------------------------------------------------------------