Python for Scientific Computing - WordPress.com · Python for Scientific Computing a tutorial...
Transcript of Python for Scientific Computing - WordPress.com · Python for Scientific Computing a tutorial...
Wuppertal 2018
Python for Scientific Computing . F O T I O S K A S O L I S .
Python ● NumPy ● SciPy ● Matplotlib● FEniCS
1
Python for Scientific Computing a tutorial introduction
FOTIOS KASOLIS. Version 15/05/2018
§1. The fundamentals of Python
Simple arithmetic operations
Python is a general-purpose programming language. The built-in data types in Python are
scalars, strings, lists, tuples, dictionaries, and sets. To do scalar arithmetic as we would do with
a common calculator, we use + for addition, – for subtraction, * for multiplication, / for divi-
sion, and ** for exponentiation. Further, to associate a value with a symbolic name, we use
the assignment operator =, while the separation marker . is used to separate the integer part
of a number from its fractional part. Python distinguishes between integers and floating point
numbers and thus, we have to explicitly enforce a floating point representation by using the
separation marker or float, as depicted in the example below;
In [1]: x, y = 1/2, 1.0/2
In [2]: x, y
Out[2]: (0, 0.5)
In [3]: type(x), type(y)
Out[3]: (int, float)
where the difference between integer and floating point numbers becomes apparent, when
using type to obtain the type of the objects. Further, it is possible to enter a complex number
𝑥 + i𝑦, where 𝑥, 𝑦 ∈ ℝ and i2 = −1, as shown below.
In [4]: z, w = 3 + 4j, complex(3,-4)
In [5]: z*w
Out[5]: (25+0j)
Symbolic names or identifiers, such as x, y, z and w in the examples above, can start with upper-
or lowercase letters, or with an underscore, followed by none or more letters, underscores
and numerical digits. Python is case sensitive, which means that fun and Fun are distinct iden-
tifiers.
In [6]: peanut_butter = 1
In [7]: _9 = 3 # not a nice name for a variable
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
Here, the hash character # signals a single-line comment. Further, we can access the docstrings
with help, autocomplete with the tab key , and look for previously entered user input with
the up-arrow key ↑, as shown below, when using the IPython console.
In [8]: import math
In [9]: math.s
math.sin math.sinh math.sqrt
In [9]: help(math.sin)
Help on built-in function sin in module math:
sin(...)
sin(x)
Return the sine of x (measured in radians). Q
In the last example, we use import to import the math module; after requesting and reading the
built-in information for the math.sin function, we return to the prompt by hitting the key Q.
Some simple computations follow.
In [10]: math.pi
Out[10]: 3.141592653589793
In [11]: 4.0 * math.atan(1)
Out[11]: 3.141592653589793
In [12]: d = math.sqrt(3.**2 + 4.**2)
In [13]: d
Out[13]: 5.0
In [14]: math.log(math.exp(1000))
---------------------------------------------------------------------------
OverflowError Traceback (most recent call last)
<ipython-input-22-30dfb1a2d2e6> in <module>()
----> 1 math.log(math.exp(1000))
OverflowError: math range error
Ordered data containers: strings, lists, and tuples
Here, we shortly present strings, lists, and tuples. These data types are ordered, which means
that each element has a position index, starting at zero, and hence, particular parts of these
containers can be extracted using their identifiers followed by the position index enclosed by
square brackets [ ], and possibly in conjunction with the slicing operator :. When slicing,
Python uses left closed – right open ranges; that is, 1:4 refers to positions 1, 2, and 3. Further,
Python allows backward indexing and slicing, with the index of the last element of an ordered
data container being -1.
Data container with n elements
Forward position index 0 1 2 3 4 . n-1
Backward position index -n . . . . . -1
3
● Strings are ordered sequences of symbols that are enclosed by single or double quotes;
that is, ' ' or " ", respectively. Strings are immutable, meaning that they cannot be
modified. In the example below, type s. to find out functions that perform string op-
erations.
In [1]: s = 'Red rum, sir, is murder'
In [2]: s[0], s[1], s[2], s[-1], s[-2], s[-3]
Out[2]: ('R', 'e', 'd', 'r', 'e', 'd')
In [3]: s[0:3], s[:3], s[-3:]
Out[3]: ('Red', 'Red', 'der')
● Lists are ordered sequences of data of arbitrary type that are enclosed by square brackets
[ ]. Lists are mutable, meaning that items can be added, removed, and replaced.
In [4]: l = [2, 'b', ['or', 'not']]
In [5]: l[0], l[1], l[2][0], l[2][1], l[0], l[1]
Out[5]: (2, 'b', 'or', 'not', 2, 'b')
In [6]: len(l)
Out[6]: 3
In [7]: l.
l.append l.extend l.insert l.remove l.sort
l.count l.index l.pop l.reverse
● Tuples are ordered sequences of arbitrary data that are enclosed by round brackets ( ).
Tuples are immutable and hence, no changes are allowed.
In [7]: t = ('1', 2, ['3', 4, '5'])
In [8]: print(t[2][0]) # prints on screen
3
In [9]: t[0] = 0
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-137-6e5fc4c1fe4b> in <module>()
----> 1 t[0] = 0
TypeError: 'tuple' object does not support item assignment
§2. NumPy array objects
Introducing the ndarray data type
For tasks that involve numerical matrix computations, the third party data type numpy.ndarray
provides ordered, homogeneous, and efficient multidimensional arrays. To access the ndarray
data type, we enter at IPython’s prompt import numpy as np . Then, all functionalities pro-
vided by NumPy can be found by np. . A NumPy array can be created from a list as shown
below, while the standard Python indexing and slicing rules apply.
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
In [1]: a = np.array([1.0, 2, 0, 3, 4])
In [2]: type(a)
Out[2]: numpy.ndarray
In [3]: a[:3]
Out[3]: array([ 1., 2., 0.])
In [4]: a[-1]
Out[4]: 4.0
NumPy provides several array manipulation methods; for instance, we can retrieve the di-
mension, the number of elements, the number of dimensions (also called axes), the maximum
value, and the sum of the elements of an array using the following statements.
In [5]: a.shape # dimension
Out[5]: (5,)
In [6]: a.size # number of elements a.shape[0]*a.shape[1]
Out[6]: 5
In [7]: a.ndim # number of axes/dimensions len(a.shape)
Out[7]: 1
In [8]: a.max() # maximum value
Out[8]: 4.0
In [9]: a.sum() # sum
Out[9]: 10.0
Matrices can be created from lists of lists, while the standard Python indexing and slicing rules
apply. For instance,
In [10]: A = np.array([[1.0, 2], [0, 3], [4, 0]])
In [11]: A
Out[11]:
array([[ 1., 2.],
[ 0., 3.],
[ 4., 0.]])
In [12]: A[0,0]
Out[12]: 1.0
In [13]: A[0,1]
Out[13]: 2.0
In [14]: A[:,0]
Out[14]: array([ 1., 0., 4.])
NumPy uses row major storing order, which means that contiguous elements in a row are
stored next to each other in the physical (linear) memory. The access pattern on an array
affects the performance significantly, since contiguous elements can be accessed faster, due to
the physical proximity. As an example, the matrix A defined in In[10] is stored as
Physical memory . 1.0 2.0 . 0.0 3.0 . 4.0 0.0 .
The row major order affects several array manipulations and hence, some care is necessary, if
the user is familiar with languages that use column major storing order, such as GNU Octave
and Fortran.
5
In [15]: A.flatten()
Out[15]: array([ 1., 2., 0., 3., 4., 0.])
In [16]: A.reshape(2, 3)
Out[16]:
array([[ 1., 2., 0.],
[ 3., 4., 0.]])
When senseful, NumPy allows the user to define the axis along which an operation is per-
formed, as depicted in the following code snippet.
In [17]: A
Out[17]:
array([[ 1., 2.],
[ 0., 3.],
[ 4., 0.]])
In [18]: A.sum() # sum of all elements
Out[18]: 10.0
In [19]: A.sum(axis = 0) # sum of the elements of each column
Out[19]: array([ 5., 5.])
In [20]: A.sum(axis = 1) # sum of the elements of each row
Out[20]: array([ 3., 3., 4.])
In [21]: A.max() # maximum value of all elements
Out[21]: 4.0
In [22]: A.max(axis = 0) # maximum value of each column
Out[22]: array([ 4., 3.])
In [23]: A.max(axis = 1) # maximum value of each row
Out[23]: array([ 2., 3., 4.])
Linear algebra
By default, all operations between NumPy arrays are performed element-by-element; for in-
stance,
In [1]: A = np.random.randint(10, size = 16).reshape(4, 4)
In [2]: A
Out[2]:
array([[3, 7, 4, 8],
[6, 7, 2, 8],
[9, 2, 2, 5],
[6, 9, 8, 8]])
In [3]: B = np.random.randint(10, size = 16).reshape(4, 4)
In [4]: B
Out[4]:
array([[5, 6, 0, 3],
[6, 5, 2, 2],
[4, 6, 9, 7],
[5, 8, 4, 6]])
In [5]: A*B
Out[5]:
array([[15, 42, 0, 24],
[36, 35, 4, 16],
[36, 12, 18, 35],
[30, 72, 32, 48]])
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
whereas matrix multiplication is performed with dot, matmul, or in Python 3, also with the
operator @;
In [6]: np.matmul(A, B)
Out[6]:
array([[113, 141, 82, 99],
[120, 147, 64, 94],
[ 90, 116, 42, 75],
[156, 193, 122, 140]])
NumPy supports a set of linear algebra routines, which can be found in numpy.linalg;
In [7]: np.linalg.
np.linalg.LinAlgError np.linalg.eigvalsh np.linalg.pinv
np.linalg.absolute_import np.linalg.info np.linalg.print_function
np.linalg.bench np.linalg.inv np.linalg.qr
np.linalg.cholesky np.linalg.lapack_lite np.linalg.slogdet
np.linalg.cond np.linalg.linalg np.linalg.solve
np.linalg.det np.linalg.lstsq np.linalg.svd
np.linalg.division np.linalg.matrix_power np.linalg.tensorinv
np.linalg.eig np.linalg.matrix_rank np.linalg.tensorsolve
np.linalg.eigh np.linalg.multi_dot np.linalg.test
np.linalg.eigvals np.linalg.norm
For instance, the singular values of a matrix 𝐀 ∈ ℝ5×5 can be computed as depicted below.
In [7]: A = np.arange(25).reshape(5, 5)
In [8]: s = np.linalg.svd(A, compute_uv = False)
In [9]: print(s)
[ 6.99085940e+01 3.57609824e+00 6.39273548e-15 1.24087941e-15
2.39254844e-16]
SciPy provides a bigger set of optimized linear algebra routines under scipy.linalg, which are
the recommended routines. For instance, the following code snippet shows how to solve the
linear system 𝐀𝒙 = 𝒃, where 𝐀 ∈ ℝ10×10.
In [10]: from scipy import linalg
In [11]: A = A = np.random.rand(10, 10)
In [12]: b = np.ones(10)
In [13]: x = linalg.solve(A, b)
In [14]: x
Out[14]:
array([ 1.71750424, 3.2303496 , -0.73462578, -3.93415278, 9.90822961,
-3.38400644, -4.27047453, 5.47057225, -0.36802176, -5.54578902])
NumPy and SciPy provide a complete data processing environment, which, complemented
with Matplotlib visualization facilities, offers an integrated scientific platform.
7
§3. Basic graphs with Matplotlib
The basic plotting function offered by matplotlib.pyplot is plot. To plot the graph of a func-
tion 𝑓: 𝑋 → 𝑌 we define a discrete version 𝑋ℎ of the domain 𝑋 using linspace , see In[5] below,
and we compute the function values at 𝑋ℎ; for instance, see In[6]. If we want to display the
generated figure on the screen, we call show.
In [1]: import numpy as np
In [2]: import matplotlib.pyplot as plt
In [3]: plt.rc('text', usetex = True)
In [4]: plt.rc('font', family = 'serif', size = 16)
In [5]: t = np.linspace(0.0, 5.0, 100)
In [6]: y = np.cos(2 * np.pi * t) * np.exp(-t)
In [7]: plt.plot(t, y, '-ok', color = [0.92549, 0, 0.54902])
In [8]: plt.xlabel(r'$\mathrm{time~(s)}$')
In [9]: plt.ylabel(r'$\mathrm{voltage~(mV)}$')
In [10]: plt.title(r'$\mathrm{The~function~with~values~}$'
r'$f(t) = \mathrm{e}^{-t}\cos{t}$')
In [11]: plt.grid(True)
In [12]: plt.show()
Matplotlib provides control over many properties of the objects comprising the figure, while
text can be rendered with LaTeX, as shown in the example. An example, showing how to plot
a curve that is given in parametric form, follows.
In [13]: plt.rcParams['axes.facecolor'] = [0.9, 0.9, 0.9]
In [14]: import matplotlib.pyplot as plt
In [15]: t = np.linspace(0.0, 2*np.pi, 1000)
In [16]: x = np.cos(20*t) * (0.5 - 0.5*np.sin(t))
In [17]: y = np.sin(20*t)
In [18]: plt.plot(x, y, linewidth = 2.0,
color = [0.92549, 0, 0.54902])lt.rc('text', usetex = True) In [19]: plt.show()
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
To generate graphs of functions of two independent variables, say 𝑥, 𝑦, we first generate two
matrices X, Y that contain the coordinates of the points at which the function values are eval-
uated, using meshgrid; that is, once more, we generate a discrete version 𝑋ℎ of the domain 𝑋 =
[𝑎, 𝑏] × [𝑐, 𝑑] of the function 𝑓.
In [20]: t = np.linspace(-2.0, 2.0, 25)
In [21]: X, Y = np.meshgrid(t, t)
In [22]: plt.plot(X, Y, 'ok', color = [0.92549, 0, 0.54902])
In [23]: plt.show()
In [24]: X[0,0], Y[0,0]
Out[24]: (-2.0, -2.0)
In [25]: X[-1,-1], Y[-1,-1]
Out[25]: (2.0, 2.0)
Then, the standard procedure to obtain the values of 𝑓 follows and for instance, a filled con-
tour plot is generated with contourf.
9
In [26]: t = np.linspace(-2.0, 2.0, 200)
In [27]: X, Y = np.meshgrid(t, t)
In [28]: Z = X * np.exp(-X**2 - Y**2)
In [29]: plt.contourf(X, Y, Z, 40, cmap = 'RdPu')
In [30]: plt.colorbar() In [31]: plt.show()
§4. Programming elements: functions, conditionals, and loops
Scripts and functions
Scripts are simple text files that contain Python statements, and have the extension .py. If a
script xmpl.py starts with the line #!/usr/bin/python, under the assumption that the Python
executable is located at /usr/bin/, then giving execution rights with chmod +x xmpl.py, results
in an executable file; that is, the code can be called by typing at the command line ./xmpl.py.
Python files may contain several function definitions. Python functions start with the key-
word def, as shown below;
def <function name> (<arg1>, <arg2>, ...):
<documentation string>
<function body>
return <expression>
The return statement exits a function, while optionally passing an expression to the caller. To
set the default value, for instance, for the second input argument, <arg2>=<value> is used in the
function definition. We assume that the following function is defined in the file xmplfun.py.
def fun(x, p = 2.0):
'Function with name fun, required input x, and optional input p'
return np.append(x, p*x**2)
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
After entering in IPython run xmplfun.py, the fun function is called as any other Python func-
tion; that is,
In [1]: x = np.arange(3.)
In [2]: print(fun(x))
[ 0. 1. 2. 0. 2. 8.]
In [3]: print(fun(x, 1))
[ 0. 1. 2. 0. 1. 4.]
In [4]: print(fun(x, 2))
[ 0. 1. 2. 0. 2. 8.]
Single statement functions can be also defined using lambda functions; that is, anonymous
functions that take any number of input arguments and return a single expression;
<function name> = lambda <arg1>, <arg2>, ...: <expression>
As with regular functions, we can also assign default values to the input arguments of a lambda
function, as in the following example.
In [5]: find = lambda a, v = 0: np.nonzero(a > v)
In [6]: a = np.random.randn(5) In [7]: a
Out[7]: array([-1.50181503, -0.42390544, 0.56960244, 0.22188803, 2.16811881])
In [8]: idx = find(a)
In [9]: idx
Out[9]: (array([2, 3, 4]),)
In [10]: a[idx]
Out[10]: array([ 0.56960244, 0.22188803, 2.16811881])
Boolean expressions and if statements
A mathematical statement is a sentence that evaluates to either true or false. In computing
science, expressions that evaluate to either True or False are called Boolean and the operators
==, !=, <>, >, <, >=, <=, combined with the operators not, and, and or, are often used within if
statements in Python. Further, Python provides the membership operator (not) in, the iden-
tity operator is (not), and the bitwise operators >>, <<, &, ^, ~ that can be used to compare
integers in their binary formats.
def fun(x, y, tol = 1e-8):
print('Tolerance value: {}'.format(tol))
if np.abs(x - y) <= tol:
print('The given numbers are sufficiently close.')
else:
print('The given numbers are different.')
return
fun(np.sin(0.), np.cos(0.5*np.pi))
fun(np.sin(0.), np.cos(0.5*np.pi), 1e-20)
11
Iterating with while and for statements
● A while loop repeatedly executes a set of statements, as long as a given condition evalu-
ates to True. The syntax of a while loop that we are mostly interested in is shown below.
<cnt> = <0> # initialize counter
while <expression>:
<statements>
<cnt> += <1> # increase counter by one, counter = counter + 1
● A for loop repeatedly executes a block of code, given a fixed number of times; that is, it
iterates over the members of a given sequence and hence, the membership operator in is em-
ployed.
for <cnt> in <sequence>:
<statements>
The loop statements can be terminated with the break statement, meaning that execution is
transferred to the statement that immediately follows the loop. Further, the continue state-
ment causes the loop to skip the remainder of its body and to immediately re-enter the loop.
We consider the following example.
s = ''
for char in 'no thing':
if char == ' ':
continue
s += char
print(s) # prints on the screen the word nothing, removes the space character
A1. The Van der Pol oscillator using SciPy
One of the most studied nonlinear stiff ODE problems is the nonlinear so-called Van der Pol
oscillator �̈� = 𝜇(1 − 𝑥2)�̇� − 𝑥, where 𝜇 > 0. This equation has been used in the study of cir-
cuits containing thermo-ionic valves, such as cathodic tubes in television sets or magnetrons
in microwave ovens. If we set 𝑦 = �̇�, we can write the Van der Pol equation as a first order
system; that is, �̇� = 𝑦 and �̇� = 𝜇(1 − 𝑥2)𝑦 − 𝑥. Below we define and solve an initial value
problem using the SciPy routine odeint.
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
plt.rc('text', usetex = True)
plt.rc('font', family = 'serif', size = 16)
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
# Define the ODE
def vdp(v, t, mu):
x, y = v
dvdt = [y, mu*(1.0 - x**2)*y - x]
return dvdt
# Initial condition
v0 = [0.1, 0.0]
# Time instances
t = np.linspace(0, 30, 500)
# Solver call
v = odeint(vdp, v0, t, args = (1.,))
# Graphical representation of the solution
plt.subplot(121)
plt.plot(t, v[:,0], linewidth = 2.0, color = [0.92549, 0, 0.54902], label = r'$x(t)$')
plt.plot(t, v[:,1], '--k', linewidth = 2.0, label = r'$y(t)$')
plt.xlabel(r'$t$')
plt.legend(loc = 'best')
plt.grid(True)
plt.subplot(122)
plt.plot(v[:,0], v[:,1], linewidth = 2.0, color = [0.92549, 0, 0.54902])
plt.xlabel(r'$x(t)$')
plt.ylabel(r'$y(t)$')
plt.grid(True)
plt.show()
A2. Sparse matrices and boundary value problems
A matrix is called sparse, when most of its elements are vanishing, otherwise it is called dense.
Sparse matrices can be stored using specialized techniques. A well-known sparse (banded, and
tridiagonal) matrix arises, when we approximate the second derivative of a function using
Taylor’s expansion; that is, 𝑓(𝑥 ± ℎ) ≈ 𝑓(𝑥) ± ℎ𝑓′(𝑥) + ℎ2𝑓′′(𝑥)/2. Addition of these
13
approximations results in 𝑓(𝑥 + ℎ) − 2𝑓(𝑥) + 𝑓(𝑥 − ℎ) ≈ ℎ2𝑓′′(𝑥). Given the function values
𝒇 = [𝑓1, … , 𝑓𝑛]⊤ at the points 𝒙 = [𝑥1, … , 𝑥𝑛]⊤, we obtain 𝒇′′ ≈ 𝐓𝒇/ℎ2, where
𝐓 =
[ −2 1 0 ⋯ 0
1 −2 1 ⋱ 00 1 −2 ⋱ 0⋮ ⋱ ⋱ ⋱ 10 0 0 1 −2]
is the so-called Toeplitz matrix. To construct this matrix in Python, we build the first column
of 𝐓 ∈ ℝ5×5 and we call scipy.linalg.toeplitz, as follows.
In [1]: import numpy as np
In [2]: from scipy.linalg import toeplitz
In [3]: c = np.concatenate((np.array([-2., 1.]), np.zeros(3)), axis = 0)
In [4]: c
Out[4]: array([-2., 1., 0., 0., 0.])
In [5]: T = toeplitz(c)
In [6]: T
Out[6]:
array([[-2., 1., 0., 0., 0.],
[ 1., -2., 1., 0., 0.],
[ 0., 1., -2., 1., 0.],
[ 0., 0., 1., -2., 1.],
[ 0., 0., 0., 1., -2.]])
Since the Toeplitz matrix is dominated by zeros, we only need to store the non-zero entries.
SciPy provides several sparse storing formats. Here, we use the compressed column format
(CSC), meaning that the position indices and the values of the non-zero entries are stored, in
a column major order.
In [7]: from scipy.sparse import csc_matrix
In [8]: Ts = csc_matrix(T)
In [9]: print(Ts)
(0, 0) -2.0
(1, 0) 1.0
(0, 1) 1.0
(1, 1) -2.0
(2, 1) 1.0
(1, 2) 1.0
(2, 2) -2.0
(3, 2) 1.0
(2, 3) 1.0
(3, 3) -2.0
(4, 3) 1.0
(3, 4) 1.0
(4, 4) -2.0
In [10]: Ts.nnz # number of nonzero elements
Out[10]: 13
Given 𝐓, a numerical solution to the one-dimensional boundary value problem (BVP)
𝑢′′(𝑥) = −1 for all 𝑥 ∈ (0,1), 𝑢(0) = 𝑢(1) = 0, can be computed by solving the linear sparse
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
system 𝐓𝒖 = −ℎ2diag 𝐈 with one of the solvers that are available in scipy.sparse.linalg, such
as spsolve (direct), bicg, bicgstab, cg, cgs, gmres, lgmres, minres, qmr, gcrotmk (iterative).
import numpy as np
from scipy.linalg import toeplitz
from scipy.sparse import csc_matrix
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
plt.rc('text', usetex = True)
plt.rc('font', family = 'serif', size = 16)
h = 1e-3 # defines grid resolution
n = int(1/h) - 1 # number of points in [h, 1-h]
x = np.linspace(h, 1. - h, n)
c = np.concatenate((np.array([-2., 1.]), np.zeros(n-2)), axis = 0)
Ts = csc_matrix(toeplitz(c))
b = -h*h*np.ones(n)
u = np.concatenate(([0], spsolve(Ts, b), [0]), axis = 0)
# Graphical representation of the solution
x = np.concatenate(([0], x, [1]), axis = 0) # add boundary points
plt.plot(x, u, linewidth = 2.0, color = [0.92549, 0, 0.54902])
plt.xlabel(r'$x$')
plt.ylabel(r'$u(x)$')
plt.grid(True)
plt.show()
A3. The finite element method with FEniCS
Variational form of BVPs
The finite element method (FEM) is a flexible strategy for solving BVPs in complex-shaped
domains; it is based on the variational (or weak) form of a BVP and has rigid mathematical
foundations. The procedure for solving a BVP with the FEM can be decomposed into the
following sequential steps.
15
● Derive the variational form of the given BVP.
● Generate a discrete version of the computational domain; for instance, subdivide the
domain into triangles.
● Choose trial functions 𝜙𝑖 and look for solutions of the form 𝑢 = ∑𝑢𝑖𝜙𝑖.
● Choose appropriate test functions.
● Assemble the matrix 𝐀 and the load vector 𝒃.
● Solve 𝐀𝒖 = 𝒃.
We introduce the FEM for the following BVP on a bounded domain Ω ⊂ ℝ2 with sufficiently
smooth boundary 𝜕Ω,
−Δ𝑢 = 𝑓 in Ω, 𝑢 = 𝑔 at ΓD,𝜕𝑢
𝜕𝑛= ℎ at ΓN.
Here, 𝑓, 𝑔, ℎ are known functions, while Δ ≡ ∇ ⋅ ∇ is the Laplace operator. The boundary 𝜕Ω
is divided into two parts ΓD, ΓN, according to the imposed boundary conditions (BCs). More
precisely, the BC at ΓD is a condition that defines the values of the state function 𝑢 itself; such
a condition is called a Dirichlet condition. On the other hand, the BC at ΓN prescribes the
values of the normal derivative 𝜕𝑢/𝜕𝑛 of the state function; such a condition is called a Neu-
mann condition. Here, 𝒏 = [𝑛𝑥, 𝑛𝑦]⊤
is the outward-directed unit normal at ΓN and the normal
derivative of 𝑢 at ΓN is
𝜕𝑢
𝜕𝑛= 𝒏⊤∇𝑢 = [𝑛𝑥, 𝑛𝑦] [
𝜕𝑢
𝜕𝑥,𝜕𝑢
𝜕𝑦]⊤
= 𝑛𝑥
𝜕𝑢
𝜕𝑥+ 𝑛𝑦
𝜕𝑢
𝜕𝑦.
When 𝑓 ≠ 0, the model problem is known as a Poisson BVP, whereas if 𝑓 = 0, the given
problem becomes a so-called Laplace BVP. The Poisson equation models a wide range of phe-
nomena, including heat conduction, electrostatics, diffusion of substances, twisting of elastic
rods, etc. A classical (or strong) solution to the model problem at hand is a function 𝑢 that is
at least twice continuously differentiable inside Ω and its first derivatives are continuous up
to the boundary. Reducing the regularity requirements for 𝑢 enables a generalization of clas-
sical solutions to what we call variational (or weak, due to weaker regularity assumptions)
solutions. To derive the variational problem associated with the given prototype model prob-
lem, the following steps are required.
● We define a set of smooth functions 𝑣 on Ω̅ = Ω ∪ 𝜕Ω, the so-called test functions, that
vanish at ΓD, written 𝑣|ΓD= 0. We always choose test functions that vanish at the bound-
aries, where Dirichlet BCs are imposed.
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
● Given an arbitrary test function 𝑣, we multiply −Δ𝑢 = 𝑓 with 𝑣 and we integrate
throughout Ω; that is, −∫ 𝑣Δ𝑢Ω
= ∫ 𝑣𝑓Ω
. Using Green’s first identity ∫ 𝑣 ∂𝑢 𝜕𝑛⁄∂Ω
=
∫ ∇𝑣 ⋅ ∇𝑢Ω
+ ∫ 𝑣Δ𝑢Ω
we obtain ∫ ∇𝑣 ⋅ ∇𝑢Ω
− ∫ 𝑣 ∂𝑢 𝜕𝑛⁄∂Ω
= ∫ 𝑣𝑓Ω
.
● Finally, we use the Neumann BC at ΓN and the test function property 𝑣|ΓD= 0 to recast
the boundary integral ∫ 𝑣 𝜕𝑢/𝜕𝑛∂Ω
; that is, ∫ 𝑣 ∂𝑢 𝜕𝑛⁄∂Ω
= ∫ 𝑣 ∂𝑢 𝜕𝑛⁄ΓD
+ ∫ 𝑣 ∂𝑢 𝜕𝑛⁄ΓN
=
∫ 𝑣ℎΓN
.
Collecting the presented material, we conclude that each classical solution 𝑢 satisfies the so-
called variational problem;
find 𝑢 such that 𝑢|ΓD= 𝑔 and
∫∇𝑣 ⋅ ∇𝑢Ω
= ∫𝑣𝑓Ω
+ ∫ 𝑣ℎΓN
for all smooth 𝑣 that vanish at ΓD.
The functional space in which the solution to a variational problem is defined is called a trial
space. If 𝑔 = 0, then the trial space coincides with the test space; that is, the space of test
functions. Note that the Dirichlet BC is incorporated in the trial space; such a condition is
called an essential BC. On the other hand, the Neumann BC appears as a boundary integral in
the variational form and is called a natural BC. Here, for simplicity, we assume that 𝑔 = 0 and
hence, the trial and test spaces coincide. We denote this common space as
𝑉 = {𝑣: Ω → ℝ:∫ |𝑣|2 +Ω
∫ |∇𝑣|2
Ω
< ∞ and 𝑣|ΓD= 0},
where |∇𝑣| is the length of the gradient vector, |∇𝑣|2 = (𝜕𝑥𝑣)2 + (𝜕𝑥𝑣)2. Note that, although
defined on Ω, the functions 𝑣 in the definition of 𝑉 are continuously extended up to the bound-
ary with the restriction operator (⋅)|ΓD. Given 𝑉, the variational problem is compactly written:
find 𝑢 ∈ 𝑉 such that 𝑎(𝑣, 𝑢) = ℓ(𝑣) for all 𝑣 ∈ 𝑉, where 𝑎 and ℓ are the so-called bilinear and
linear forms; here, 𝑎(𝑣, 𝑢) = ∫ ∇𝑣 ⋅ ∇𝑢Ω
, ℓ(𝑣) = ∫ 𝑣𝑓Ω
+ ∫ 𝑣ℎΓN
.
The finite element discretization
The FEM discretizes the variational problem of a BVP by introducing a finite-dimensional
space 𝑉ℎ ⊂ 𝑉. The space 𝑉ℎ is equipped with a basis {𝜙𝑖}𝑖=1𝑁 , where dim(𝑉ℎ) = 𝑁. The discrete
problem reads: find 𝑢ℎ ∈ 𝑉ℎ such that 𝑎(𝑣, 𝑢ℎ) = ℓ(𝑣) for all 𝑣 ∈ 𝑉ℎ . Note that subtracting the
continuous and discrete versions of the variational problem, we obtain the so-called Galerkin
orthogonality
𝑎(𝑣, 𝑢 − 𝑢ℎ) = 𝑎(𝑣, 𝑢) − 𝑎(𝑣, 𝑢ℎ) = ℓ(𝑣) − ℓ(𝑣) = 0 for all 𝑣 ∈ 𝑉ℎ.
We express 𝑢ℎ as a finite sum using the chosen basis of 𝑉ℎ; that is, 𝑢ℎ = ∑ 𝑢𝑖𝜙𝑖𝑁𝑖=1 . Further,
since 𝑎(𝑣, 𝑢ℎ) = ℓ(𝑣) for all 𝑣 ∈ 𝑉ℎ, then 𝑎(𝑣, 𝑢ℎ) = ℓ(𝑣) holds true for the choice 𝑣 = 𝜙𝑖 and
17
hence, for all 𝑖 ∈ {1,2, … , 𝑁} we obtain 𝑎(𝜙𝑖 , 𝑢1𝜙1 + 𝑢2𝜙2 + ⋯+ 𝑢𝑁𝜙𝑁) = ℓ(𝜙𝑖), which, due
to the linearity of 𝑎, can be written 𝑎(𝜙𝑖 , 𝑢1𝜙1) + 𝑎(𝜙𝑖 , 𝑢2𝜙2) + ⋯+ 𝑎(𝜙𝑖 , 𝑢𝑁𝜙𝑁) = ℓ(𝜙𝑖).
Since 𝑢𝑖 are scalars, we conclude that 𝑢1𝑎(𝜙𝑖, 𝜙1) + 𝑢2𝑎(𝜙𝑖, 𝜙2) + ⋯ + 𝑢𝑁𝑎(𝜙𝑖 , 𝜙𝑁) = ℓ(𝜙𝑖).
The last expression is the inner product of the vectors 𝒖 = [𝑢1, … , 𝑢𝑁]⊤ and 𝒂𝑖 =
[𝑎(𝜙𝑖 , 𝜙1), … , 𝑎(𝜙𝑖 , 𝜙𝑁)]⊤; that is, 𝒂𝑖⊤𝒖, or in matrix form, 𝐀𝒖 = 𝒃, where
𝐀 = [𝒂1
⊤
⋮𝒂𝑁
⊤] , 𝒃 = [
ℓ(𝜙1)⋮
ℓ(𝜙𝑁)].
Often, the basis functions {𝜙𝑖} are chosen to be element wise-defined polynomials of (rela-
tively low) degree 𝑛, resulting in the so-called Lagrange elements ℙ𝑛.
Python/FEniCS implementation
We consider a capacitor consisting of two elliptical plates with centers located at (±1,0) of
the 𝑥 axis, major axis equal to 2 and perpendicular to the 𝑥 axis, while the minor axis of each
plate is equal to 0.1. The capacitor is placed in free space. To perform computations, we trun-
cate the physical (infinite) domain using a circle of radius 5, centered at (0,0); the resulting
computational domain is denoted Ω, and does not contain the domains occupied by the plates
of the capacitor, denoted 𝜔L, 𝜔R. The truncating boundary Γt = 𝜕Ω ∖ (𝜕𝜔L ∪ 𝜕𝜔R) is assumed
to be sufficiently far and hence, grounded; that is, the potential is vanishing at Γt. Further, the
potential values at the plates are ±1 V, respectively. The strong form of the BVP describing
the physical setting is stated below;
Δ𝑢 = 0 in Ω, 𝑢|𝜕𝜔L= −1, 𝑢|𝜕𝜔R
= 1, 𝑢|Γt= 0.
To solve this problem using Python/FEniCS, we need to derive the variational form; find 𝑢 ∈
𝑉 such that 𝑎(𝑣, 𝑢) = ℓ(𝑣) for all 𝑣 ∈ 𝐻01(Ω), where 𝑉 is a function space that incorporates the
Dirichlet data, 𝐻01(Ω) = {𝑣: Ω → ℝ: ∫ |𝑣|2 +
Ω∫ |∇𝑣|2Ω
< ∞ and 𝑣|𝜕Ω = 0}, and 𝑎(𝑣, 𝑢) = ∫ ∇𝑣 ⋅Ω
∇𝑢 and ℓ(𝑣) = 0 are the forms that correspond to the BVP at hand. In FEniCS, we first define
the truncating domain Ω ∪ �̅�L ∪ �̅�R with Circle, from which we subtract the elliptical plates,
D1-D2-D3, while generating the mesh with generate_mesh. Three classes are defined and used to
identify the different Dirichlet boundaries, by assigning a unique boundary indicator to the
corresponding instances out, inl, inr using mark and MeshFunction. To define the finite element
space throughout the triangulation T we use FunctionSpace, while DirichletBC is used to assign
the corresponding Dirichlet data, given the constructed boundary indicators. The solution
object u is generated with TrialFunction, while the test functions v with TestFunction. The as-
signments a=dot(grad(u), grad(v))*dx and L=f*v*dx define the bi-linear and the linear form of
the variational problem, respectively. The rest of the code is self-explanatory and if not, we
suggest you consult “The FEniCS Tutorial” that can be found at https://fenicsproject.org/tu-
torial/.
FOTIOS KASOLIS | PYTHON FOR SCIENTIFIC COMPUTING
from fenics import *
from mshr import *
import numpy as np
import matplotlib.pyplot as plt
plt.rc('text', usetex = True)
plt.rc('font', family = 'serif', size = 16)
# Generate domains
D1 = Circle(Point(0.0, 0.0), 5.0)
D2 = Ellipse(Point(-1.0, 0.0), 0.1, 2.0)
D3 = Ellipse(Point(1.0, 0.0), 0.1, 2.0)
# Generate mesh
T = generate_mesh(D1 - D2 - D3, 64)
# Identify the boundaries
class OUT(SubDomain):
def inside(self, x, on_boundary):
return x[0]**2 + x[1]**2 > 24 and on_boundary
class INL(SubDomain):
def inside(self, x, on_boundary):
return x[0] < 0.0 and x[0] > -2.0 and x[1] < 2.5 and x[1] > -2.5 and on_boundary
class INR(SubDomain):
def inside(self, x, on_boundary):
return x[0] > 0.0 and x[0] < 2.0 and x[1] < 2.5 and x[1] > -2.5 and on_boundary
out = OUT()
inl = INL()
inr = INR()
# Assign boundary indicators
boundaries = MeshFunction('size_t', T, T.topology().dim() - 1)
out.mark(boundaries, 0)
inl.mark(boundaries, 1)
inr.mark(boundaries, 2)
# Define finite element space
V = FunctionSpace(T, 'P', 1)
# Assign Dirichlet conditions
bcD1 = DirichletBC(V, 0.0, boundaries, 0)
bcD2 = DirichletBC(V, -1.0, boundaries, 1)
bcD3 = DirichletBC(V, +1.0, boundaries, 2)
bc = [bcD1, bcD2, bcD3]
# Define variational problem
u = TrialFunction(V)
v = TestFunction(V)
f = Constant(0.0)
a = dot(grad(u), grad(v))*dx
L = f*v*dx
# Compute solution
u = Function(V)
19
solve(a == L, u, bc)
# Plot solution
p = plot(u, cmap = 'RdPu')
plt.colorbar(p)
plt.show()