Download Electrostatics with partial differential equations

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts

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 wikipedia , lookup

Partial differential equation wikipedia , lookup

Transcript
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