Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Navier–Stokes equations wikipedia , lookup
Equations of motion wikipedia , lookup
Euler equations (fluid dynamics) wikipedia , lookup
Electric charge wikipedia , lookup
Derivation of the Navier–Stokes equations wikipedia , lookup
Equation of state wikipedia , lookup
Relativistic quantum mechanics wikipedia , lookup
Electrostatics with partial differential equations – A numerical example using Jacobi iterations Jørgen Midtbø and Jørgen Trømborg September 2011 This text deals with numerical solutions of two-dimensional problems in electrostatics. We begin by formulating the generic problem as a partial differential equation, then we solve the equation by Jacobi’s method. We use a package called Cython to improve computational speed. 1 Introduction By now you should be familiar with Coulomb’s law, which describes the force between charged particles with relative positions r. It has the form F= 1 q1 q 2 r̂, 4πε0 r2 (1) q1 and q2 being the charges of the particles and ε0 being the vacuum permittivity. We have seen that Coulomb’s law can be derived from the more general Gauss’s law, which is the first of the four Maxwell equations. Gauss’s law states that the total electrical flux through a closed surface has to be proportional to the electric charge contained within the surface. Gauss’s law can be expressed in integral form as I Z Q 1 E · dA = ρ dV, (2) = ε0 ε0 V A where A is the surface encapsulating the volume V , Q is the net charge in this volume and ρ is the charge density. By applying the Divergence Theorem, we can write equation (2) on differential form as ρ ∇·E= . (3) ε0 Defining electric potential V by the relation E = −∇V (4) and inserting this in (3), we get ∇ · E = ∇ · (−∇V ) = −∇ · ∇V = −∇2 V = 1 ρ , ε0 (5) where ∇2 ≡ ∇ · ∇ = ∂2 ∂2 + ∂x2 ∂y 2 (6) is the Laplace operator in two dimensions. It has a 3D form with a ∂ 2 /∂z 2 term, but here we will restrict ourselves to 2D. Now we have an equation relating the electrical potential in a point in space to the charge density in that point. This is a partial differential equation, which becomes clear if we write it out as ∂ 2 V (x, y) ∂ 2 V (x, y) 1 + = − ρ(x, y). 2 2 ∂x ∂y ε0 (7) An equation on this form is known as Poisson’s equation. If we are able to solve this equation for a given charge distribution, we know what the potential is anywhere in space. By taking the gradient of the potential we can find the electric field. 2 Numerical solution of Poisson’s equation In previous courses you have worked extensively with ordinary differential equations (ODEs), i.e. equations for functions of one variable. You have learned how to solve these on the computer, using for instance Euler’s method, the Euler-Cromer method or some order of the RungeKutta method. If we are given an ODE with initial conditions, it is pretty straightforward to solve it numerically — it is just a question of computer power. A partial differential equation (PDE) requires a bit more care, but the underlying strategy should be familiar. The solution we are after is a scalar field V (x, y), assigning a value to every point on a two-dimensional plane. 2.1 Dissecting the mathematics We remember, e.g. from Euler’s method for ODEs, that we solve our differential equation in discrete steps, and use the function’s value computed in the previous step to find the value in the next step. With this in mind, it is reasonable to expect that we use the function values in neighbouring points to compute the function value for a PDE as well — you will see that this is indeed the case. In this example we utilise what is called Jacobi’s method to solve the Poisson equation for a particular charge distribution and set of boundary conditions.1 Briefly explained, the method works as follows: we represent the xy-plane by a grid of points, conveniently stored in matrix form. Our goal is to find the matrix V containing the values of the potential V for all points (i, j), corresponding to discrete x and y values. It should fulfill equation (7) everywhere (in some numerical sense, to be explained shortly), as well as the imposed boundary conditions, which we’ll take to be V = 0 at the outer boundary of our domain. 1 Boundary conditions are analogous to initial conditions of ODEs: they constitue the problem spesific information that picks a unique solution of the differential equation from the infinite set of possible solutions. 2 If we happen to have a solution that we believe is correct, this can be easily tested: simply check if the matrix fulfills the Poisson equation and the boundary conditions. If it does, you’re done. If it doesn’t, you can use your calculation to improve the initial guess. As long as your guess isn’t wildly misbehaving (a mathematician can define what this would mean for the Jacobi method), it doesn’t even have to be any good. By improving and improving again according to the right rules, your guess will eventually converge to the correct solution. In the end, you will have a sequence of V s usually called generations of V and denoted V (n) after n improvements (not to be confused with an exponent). 2 2 ∂ ∂ The second order derivatives ∂x 2 and ∂y 2 can be discretised by second order central differences of step length or resolution h, which you’ll find in any book on numerical methods, typically written f (x + h) − 2f (x) + f (x − h) h2 f 00 (x) ≈ (8) for a generic function f . We write have ∂2V ∂x2 ≡ Vxx to save some typing. If the step length is the same in both directions, we V (x + h, y) − 2V (x, y) + V (x − h, y) h2 V (x, y + h) − 2V (x, y) + V (x, y − h) . ≈ h2 Vxx ≈ Vyy (9) (10) By inserting this in equation (7) and solving for V (x, y) (you should do this), we get V (x, y) ≈ h2 1 V (x + h, y) + V (x − h, y) + V (x, y + h) + V (x, y − h) + ρ(x, y) (11) 4 4ε0 or, with discrete indices, Vi,j h2 1 = Vi+1,j + Vi−1,j + Vi,j+1 + Vi,j−1 + ρi,j . 4 4ε0 (12) If we have the correct solution, the left-hand and right-hand sides of equation (12) will be equal. However, if our guess of V was off, the equation doesn’t hold and the two sides will have different values. The trick of Jacobi’s method is to say that the right-hand side is the better guess. If we were at V (0) , we’ll let each entry in V (1) be defined by equation (12), with V (0) on the right-hand side. When we have found all the entries of V (1) , we’ll use them to find V (2) , and so on, i.e. (n) Vi,j = (n−1) 1 h2 Vi+1,j + Vi−1,j + Vi,j+1 + Vi,j−1 + ρi,j . 4 4ε0 (13) We’ll accept the mathematicians’ claim that this converges to the correct solution. Note that ρi,j is assumed to be fixed and therefore has no need of generations. We’ll stop iterating when the difference between V (n−1) and V (n) becomes small, which can e.g. be defined as (n) (n−1) |Vi,j − Vi,j |< ∀i, j (14) 3 or 1 X (n) (n−1) |Vi,j − Vi,j | < 2 N i,j (15) for some treshold . When you start looking closely at equation (13) you’ll find that it works well in the interior of the domain, but that it’s ill defined at the boundaries: if you’re at the left-hand edge of your domain, there is not point to your left to use in the formula. However, we already know the values of V in the boundary points: they are given by the boundary conditions, and we don’t have to calculate them at all. 2.2 Code Now that we have a machinery for solving our PDE numerically, it is time to put it to use. We begin by looking at a point charge q = 3 (we use dimensionless units) situated in the center of a square box with sides of length L = 2. We divide our box into a grid with N = 21 points to each side, giving the resolution h = L/(N − 1). We then get a charge density matrix ρ which is zero everywhere except in one point: ρ10,10 (matrix indexing in Python starts at zero). Since we are dealing with discrete points, the matrix element (10, 10) represents a small area h2 around the spatial point (L/2, L/2), and ρ10,10 = q h2 (16) is the charge density in this small area. The boundary conditions are V = 0 at the sides of the square. Even if we have some idea of what V (x, y) will be from our experience with the point charge in a vacuum, we’ll use V = 0 everywhere as our initial guess V (0) . Example code in Python is given below. # point_charge . py - Iterative solution of 2 D Poisson equation import numpy as np import matplotlib import matplotlib . pyplot as plt # L N h System size and resolution = 2.0 = 21 = L /( N -1) # Make the charge density matrix q = 3.0 rho0 = q / h **2 rho = np . zeros (( N , N )) rho [ N /2 , N /2] = rho0 # Intentional integer division # Make the initial guess for solution matrix V = np . zeros (( N , N )) # Solver iterations = 0 eps = 1e -8 # Convergence threshold 4 error = 2* eps # Dummy error to enter the first loop while iterations < 1 e4 and error > eps : V_temp = np . copy ( V ) error = 0.0 # We make this accumulate in the loop for j in range (1 ,N -1): # Avoid updating the boundaries for i in range (1 ,N -1): V [i , j ] = 0.25*( V_temp [ i +1 , j ] + V_temp [i -1 , j ] + V_temp [i ,j -1] + V_temp [i , j +1] + rho [i , j ]* h **2) error += abs ( V [i , j ] - V_temp [i , j ]) iterations += 1 error /= float ( N **2) print " iterations = " , iterations # Define arrays used for plotting x = np . linspace (0 ,L , N ) y = np . linspace (0 ,L , N ) # meshgrid () by default orders the arrays differently from what # I prefer . This way , ( X [i , j ] , Y [i , j ]) equals ( x [ i ] , y [ j ]) Y , X = np . meshgrid (y , x ) # Plot the potential V matplotlib . rcParams [ ’ xtick . direction ’] = ’ out ’ matplotlib . rcParams [ ’ ytick . direction ’] = ’ out ’ CS = plt . contour (X ,Y ,V ,30) # Make a contour plot # plt . clabel ( CS , inline =1 , fontsize =10) # Contour labels plt . title ( ’ Point charge in the cavity of a grounded box ’) CB = plt . colorbar ( CS , shrink =0.8 , extend = ’ both ’) plt . show () Figure 1 shows selected equipotential lines. The exercises analyse this physical problem in more depth. When you run this program yourself you may find that it is a bit slow, even with the relatively poor resolution of 21 × 21 points. Much more efficient algorithms exist, but the main bottleneck here is Python. Python is a high-level language, meaning that it is easy to write and understand, but comes with lots of built-in systems to protect the user from having to think of everything. These systems produce what is known as overhead, and this slows Python down. For this reason, professional simulations are always done in low-level languages such as C, C++ or Fortran. These are much faster, but more difficult to use. 5 2.0 Point charge in the cavity of a grounded box 1.76 1.52 1.5 1.28 1.04 1.0 0.80 0.56 0.5 0.32 0.08 0.0 0.0 0.5 1.5 1.0 2.0 Figure 1: Equipotential lines (V = constant) around a point charge in the center of a grounded metallic box. Dimensionless units. q = 3, L = 2, N = 21. 6 3 Code speedup with Cython (optional) Luckily, there are brilliant people out there, and a few of them have taken the trouble to write the Python package Cython. Cython provides a link between Python and C, making it possible to utilise some of C’s fastness without having to write C code yourself. The next portion of this text deals with implementing our code in Cython. This will give us a Python program capable of handling much larger grids. 3.1 Installing Cython The first thing we need to do is install Cython. We assume you are using a Linux computer at UiO with Python installed. (If you are using your personal computer with Ubuntu, look further down.) Begin by pasting the following lines into your terminal and executing them, one at a time (beware of additional spaces, they have to be removed manually): cd ~/ wget http :// cython . org / release / Cython -0.14.1. tar . gz tar xzvf Cython -0.14.1. tar . gz cd Cython -0.14.1 python setup . py install This will install Cython to a directory in your home folder. You should get an error message during the installation, since you don’t have write access to the directory where Python is installed. This requires a little tweak to get around. Paste and execute the following lines: cd ~/ echo " export PATH =~/ Cython -0.14.1/ bin : $PATH " >> . bashrc echo " export PYTHONPATH =~/ Cython -0.14.1: $PYTHONPATH " >> . bashrc source . bashrc This adds two lines to your terminal shell configuration and reloads the configuration file, to make bash tell Python that Cython is installed. And with that, we are good to go. Personal Ubuntu computer: The procedure is very similar, but since you do have write access you won’t need the .bashrc tweak. You just have to make sure that you run the install command as super user. So instead of python setup.py install, you should run sudo python setup . py install Some people have reportedly run into problems when trying to install Cython on Ubuntu with this approach. You may have to install the Python developer version, which you can get in apt by the command sudo apt - get install python - dev 3.2 Implementing our code in Cython The great thing with Cython is that it makes our progam faster with only minor code changes and no algorithmic changes. Below is the modified Python program, with the changes highlighted. The unfamiliar parts are explained after the code example. 7 # point_charge . pyx - Iterative solution of 2 D Poisson equation import numpy as np import matplotlib import matplotlib . pyplot as plt # Cython - specific imports cimport numpy as np cimport cython ctypedef np . float_t FTYPE_t @cython . boundscheck ( False ) def main (): # Declare variables cdef int N , iterations , i , j cdef double L , h , rho0 , eps , error cdef np . ndarray [ FTYPE_t ] x , y cdef np . ndarray [ FTYPE_t , ndim =2] X , Y , rho , V , V_temp # L N h System size and resolution = 2.0 = 21 = L /( N -1) # Make the charge density matrix q = 3.0 rho0 = q / h **2 rho = np . zeros (( N , N )) rho [ N /2 , N /2] = rho0 # Intentional integer division # Make the initial guess for solution matrix V = np . zeros (( N , N )) # Solver iterations = 0 eps = 1e -8 # Convergence threshold error = 2* eps # Dummy error to enter the first loop while iterations < 1 e4 and error > eps : V_temp = np . copy ( V ) error = 0.0 # We make this accumulate in the loop for j in range (1 ,N -1): # Avoid updating the boundaries for i in range (1 ,N -1): V [i , j ] = 0.25*( V_temp [ i +1 , j ] + V_temp [i -1 , j ] + V_temp [i ,j -1] + V_temp [i , j +1] + rho [i , j ]* h **2) error += abs ( V [i , j ] - V_temp [i , j ]) iterations += 1 error /= float ( N **2) print " iterations = " , iterations # Define arrays used for plotting x = np . linspace (0 ,L , N ) y = np . linspace (0 ,L , N ) # meshgrid () by default orders the arrays differently from what # I prefer . This way , ( X [i , j ] , Y [i , j ]) equals ( x [ i ] , y [ j ]) Y , X = np . meshgrid (y , x ) # Plot the potential V matplotlib . rcParams [ ’ xtick . direction ’] = ’ out ’ matplotlib . rcParams [ ’ ytick . direction ’] = ’ out ’ CS = plt . contour (X ,Y ,V ,30) # Make a contour plot 8 # plt . clabel ( CS , inline =1 , fontsize =10) # Contour labels plt . title ( ’ Point charge in the cavity of a grounded box ’) CB = plt . colorbar ( CS , shrink =0.8 , extend = ’ both ’) plt . show () main () So, what has changed? Starting from the top, we see that we have some additional import statements. cimport is the Cython import statement, and we need numpy and, of course, Cython, to be imported and understood by the C compiler. Directly below is the line ctypedef np.float_t FTYPE_t. This specifies to Cython that we are going to be defining arrays of floats, not integers or strings. The next line is a little cryptic: @cython.boundscheck(False). This is the magical command. It tells Cython not to bother checking our code for errors (like trying to index arrays beyond their length). With Python, such an error produces a message like Index out of bounds. With C, all you get is Segmentation fault. C knows that something went wrong, but does not bother to find out what, which does not help us with debugging. When we have a working script, removing error checking greatly reduces the overhead discussed above. The observant reader will notice that we have put our program inside a function main(), which is then called at the end of the script. This is a workaround neccessary to use Cython. The program works exactly the same. The last changes are the four lines of cdef int N, .... This is essential! Here we list all variables used in our program, and specify what kinds of objects they are going to be. The first line are the integers (notice the counter variables i and j, they are integers too). Double is C-language for float. np.ndarray specifies an array, and here we use the FTYPE_t definition from above to specify float. The last line defines matrices, distinguished by ndim=2. 3.3 Running Cython To use our Cython program, it needs to be compiled. Begin by saving your Cython script (you may just copy-paste the one above) as a .pyx file (as opposed to .py!). Then, open an empty document named setup.py and paste the following code into it: # setup . py from distutils . core import setup from distutils . extension import Extension from Cython . Distutils import build_ext numpy = " / local / lib / python2 .5/ site - packages / numpy / core / include / " setup ( cmdclass = { ’ build_ext ’: build_ext } , ext_modules = [ Extension ( " point_charge_lib " , [ " point_charge . pyx " ] , include_dirs = [ numpy ])] ) You should replace the file name (“point_charge.pyx”) with the name of your Cython program. The library name (“point_charge_lib”) is arbitrary – you can call it whatever you want – but it should be descriptive. (For the record: The name “setup.py” is arbitrary too, and if your make 9 multiple Cython scripts you might want to have designated setup files – “setup_point_charge.py”, etc.) Now we are ready to compile. This is done by one line (you obviously have to be in the directory where you saved your files): python setup . py build_ext -- inplace With this, Cython translates our Python-esque code into C. Our program can now be run by the command python -c " import point_charge_lib " where “point_charge_lib” is your library name from above. And that’s it! We are now running C without running C (at least without having to think about it). 10 4 Exercises a) Copy or rewrite the code from Section 2.2. Verify that it works and that you can produce something similar to Figure 1. b) Find the electric field E. If you use the gradient() function, remember that the step length h is part of the numerical derivative, and cannot be omitted. c) Verify that Gauss’s law is satisfied by calculating the flux of E out of the domain and comparing this to the charge q within. d) What does the boundary condition V = 0 represent physically, i.e. what material(s) could you use to build something with this property? 11