Praktikum zur Vorlesung Numerik partieller Differentialgleichungen I

Mario Ohlberger, Felix Schindler

Blatt 02, 23.10.2019

  • Aktivieren Sie wie gewohnt ihre Arbeitsumgebung und starten Sie den Jupyter Notebook server, siehe zB Blatt 1, Aufgabe 0).
  • Laden Sie dieses Blatt von der Homepage herunter und speichern Sie es unter ~/NPDGL1/notebooks/blatt_02.ipynb.
  • Importieren Sie numpy und pymor.basic und machen Sie matplotlib für das Notebook nutzbar.
In [1]:
%matplotlib notebook
import numpy as np
from pymor.basic import *

äquidistante Gitter in 1d

Als Partitionierung eines zusammenhängenden beschränkten Gebietes $\Omega \subset \mathbb{R}$ (also eines Intervals $(a, b)$ für $a < b$) betrachten wir ein äquidistantes Gitter $\mathcal{T}_h := \{ T_0, \dots, T_{I - 1}\}$

  • für $I \in \mathbb{N}$, mit
  • Gitterweite $h := \frac{|b - a|}{I}$,
  • $I + 1$ Knotenpunkten $x_i := a + ih$ für $0 \leq i \leq I$, sodass $x_0 = a$ und $x_I = b$ und
  • $I$ Elementen $T_i := (x_i, x_{i + 1}) \in \mathcal{T}_h$ für $0 \leq i \leq I - 1$.

Das Gitter ist also

  • eine Ãœberdeckung von $\Omega$, d.h. $\bigcup_{i = 0}^{I - 1}\overline{T_i} = \overline{\Omega}$, und zwar
  • in disjunkte Elemente, d.h. $\overline{T_i} \cap \overline{T_j} = \begin{cases}\text{Knotenpunkt }x_i,&\text{falls } j = i - 1\\\overline{T_i},&\text{falls } j = i\\\text{Knotenpunkt } x_{i + i},&\text{falls } j = i + 1\\\emptyset,&\text{sonst}\end{cases}.$

stückweise lineare Funktionen in 1d

Für ein zusammenhängendes beschränktes Gebiet $T \subset \mathbb{R}$ sei $\mathbb{P}_k(T)$ der Raum der Polynome über $T$ vom Grad höchstens $k \in \mathbb{N}_0$.

Zu einem gegebenen Gitter $\mathcal{T}_h$ betrachten wir den Raum der stetigen stückweise linearen Funktionen bezüglich des Gitters:

$$S_h^1 := \big\{ u \in C^0(\Omega) \;\big|\; u|_T \in \mathbb{P}_1(T) \big\}$$

Jede Funktion $u_h \in S_h^1$ ist dabei eindeutig definiert durch ihre Werte in den Knotenpunkten des Gitters. Wir können also der Funktion $u_h$ ihren Vektor an Freiheitsgeraden (DoF Vektor, von degree of freedom) $\underline{u_h} \in \mathbb{R}^{I + 1}$ zuordnen, sodass $(\underline{u_h})_i = u_h(x_i)$ für $0 \leq i \leq I$. Es besteht also die folgende Isomorphie:

$$\begin{align*}S_h^1 &\cong \mathbb{R}^{I + 1}\\ u_h&\leftrightarrow \underline{u_h}\end{align*}$$

Aufgabe 1: Interpolation

Da wir beliebige Funktionen $f \in C^0(\Omega)$ nicht fehlerfrei handhaben können (zum Beispiel Integrieren oder visualisieren) betrachten wir geeignete Approximationen $f_h \in S_h^1$. Dazu definieren wir den zu $S_h^1$ passenden Interpolationsoperator

$$\begin{align*}\Pi_{S_h^1}; C^0(\Omega) &\to S_h^1,\\f&\mapsto \Pi_{S_h^1}[f] =: f_h,\end{align*}$$

wobei $f_h$ eindeutig bestimmt ist durch die Auswertung von $f$ an den Knotenpunkten des Gitters:

$$\begin{align*}f_h(x_i) = f(x_i)&&\text{für } 0 \leq i \leq I\end{align*}$$

Implementieren Sie den Interpolationsoperator $\Pi_{S_h^1}$ als Python Funktion und testen Sie Ihr Program für $\Omega = (0, 2\pi)$, $I = 4$ und $f(x) = sin(x)$!

Hinweis: Sie können der Einfachheit halber for-Schleifen verwenden!

  1. Schreiben Sie eine Python Funktion f_h = interpolate(grid, f), die für ein gegebenes pyMOR Gitter in einer Raumdimension, grid$ = \mathcal{T}_h$, und eine gegebene pyMOR Funktion, f$ = f$, den DoF-Vektor der Interpolierten, f_h$ = \underline{f_h}$, als Numpy-Array entsprechender länge zurück gibt.

    • Stellen Sie dazu in der Funktion sicher, dass das gegebene Gitter die Partition eines Gebietes in einer Raumdimension ist.
    • Speichern Sie in der Funktion alle Knotenpunkte des Gitters in vertices ab.
    • Werten Sie die Funktion f and allen Knotenpunkten aus (siehe Dokumentation von FunctionInterface) und speichern Sie die Werte in values.
    • Stellen Sie sicher, dass Sie genauso viele Funktionswerte wie Knotenpunkte berechnet haben.
    • Geben Sie values zurück.
In [2]:
def interpolate(grid, f):
    assert grid.dim == 1
    vertices = grid.centers(1)
    num_vertices = len(vertices)
    # loop-based variant
    #values = np.zeros(num_vertices)
    #for i in range(num_vertices):
    #    values[i] = f.evaluate(vertices[i])
    # vectorized variant
    values = f.evaluate(vertices)
    assert len(values) == len(vertices)
    return values
  1. Ein äquidistantes Gitter in einer Raumdimension wird in pyMOR durch die Klasse OnedGrid modelliert.

    • Legen Sie ein entsprechendes Objekt mit Namen grid an,
    • geben Sie die Dimension des Gitters,
    • die Anzahl der Knotenpunkte sowie
    • die Position jedes Knotenpunktes aus (Wiederholung von Blatt 00).
In [3]:
grid = OnedGrid(domain=(0, 2*np.pi), num_intervals=4)
dim = grid.dim
print('grid lives in a {}-dimension space'.format(dim))
I = grid.size(0)
h = 2*np.pi / I
print('grid has {} elements, h = {}'.format(I, h))
vertices = grid.centers(dim)
print('grid has {} vertices:'.format(len(vertices)))
for i in range(len(vertices)):
    x_i = vertices[i]
    print('  x_{} = {}'.format(i, x_i))
grid lives in a 1-dimension space
grid has 4 elements, h = 1.5707963267948966
grid has 5 vertices:
  x_0 = [0.]
  x_1 = [1.57079633]
  x_2 = [3.14159265]
  x_3 = [4.71238898]
  x_4 = [6.28318531]
  1. Analytische Funktionen werden in pyMOR durch ExpressionFunction modelliert. Legen Sie ein entsprechendes Objekt mit Namen f an.
In [4]:
f = ExpressionFunction('sin(x)', dim_domain=1, shape_range=())
  1. Berechnen Sie die Interpolation von $f$ auf dem Gitter $\mathcal{T}_h$ mittels Ihrer interpolate Funktion.

    • Rufen Sie Ihre Funktion auf und speichern Sie das Resultat in f_h ab.
    • Geben Sie den Python-Typ von f_h aus.
    • Geben Sie die Länge von f_h aus.
    • Geben Sie mit Hilfe einer for-Schleife die Koordinaten aller Knotenpunkte des Gitters zusammen mit den Werten von $f$ aus (Hinweis: Nutzen Sie zip).
In [5]:
f_h = interpolate(grid, f)
print(type(f_h))
print(f_h.shape)
print(len(f_h))
print()
for x, value in zip(vertices, f_h):
    print('f_h({}) = {}'.format(x, value))
<class 'numpy.ndarray'>
(5,)
5

f_h([0.]) = 0.0
f_h([1.57079633]) = 1.0
f_h([3.14159265]) = 1.2246467991473532e-16
f_h([4.71238898]) = -1.0
f_h([6.28318531]) = -2.4492935982947064e-16

Aufgabe 2: Visualisierung

Schreiben Sie eine Python Funktion visualize(grid, f_h, name=''), welche die Interpolation einer Funktion über einem Gitter mit Hilfe von matplotlib visualisiert und testen Sie Ihr Program für $\Omega = (0, 2\pi)$, $f(x) = sin(x)$ und Gitter mit $I = 2^2, 2^3, 2^4, 2^5$ Elementen!

  1. Machen Sie das benötigte Modul mit dem Befehl from matplotlib import pyplot as plt unter dem Namen plt verfügbar und machen Sie sich mit plt.plot und plt.legend vertraut.
In [6]:
from matplotlib import pyplot as plt

# Dokumentation von plot anzeigen
#plt.plot?
# Eines der Beispiele aus der Dokumentation, x = [0, 1, 2, 3], y = x^2
#plt.plot([0, 1, 2, 3], [0, 1, 4, 9])

# Dokumentation von legend anzeigen
#plt.legend?
# Eine Möglichkeit besteht darin, bei plot direkt ein label mit zu geben, anstelle des Aufrufs von oben nutzt man also zB
plt.plot([0, 1, 2, 3], [0, 1, 4, 9], label='f')
plt.legend()

# Andere nützliche Funktionen
plt.title('Plot von f(x) = x^2')
plt.xlabel('x')
plt.ylabel('f(x)')
Out[6]:
Text(0, 0.5, 'f(x)')
  1. Schreiben Sie die Funktion visualize, welche eine Funktion visualisiert.

    • Erstellen Sie in der Funktion mit plt.figure() eine neue (leere) Grafik.
    • Stellen Sie in der Funktion sicher, dass das gegebene Gitter die Partition eines Gebietes in einer Raumdimension ist.
    • Stellen Sie in der Funktion sicher, dass die Länge von f_h mit der Anzahl der Knotenpunkte des Gitters überein stimmt.
    • Nutzen Sie die plot Funktion aus dem plt Modul, um die Werte von f_h an den Knotenpunktes des Gitters zu visualisieren und linear zu interpolieren, und den Namen der Funktion zu übergeben.
    • Nutzen Sie die legend Funktion aus dem plt Modul, um die Legende anzeigen zu lassen.
In [7]:
def visualize(grid, f_h, name=''):
    plt.figure()
    assert grid.dim == 1
    assert len(f_h) == grid.size(1)
    plt.plot(grid.centers(1), f_h, label=name)
    plt.legend()
  1. Testen Sie Ihre Funktion für das Gitter und die Interpolation aus Aufgabe 1.
In [8]:
visualize(grid, f_h, 'f_h')
  1. Erweitern Sie Ihre Funktion, sodass Sie mehrere Interpolierte gleichzeitig visualisieren können.

    • Erstellen Sie vor Ihrer Funktion folgendermaßen eine Farbskala:

      colors = list(plt.get_cmap('tab20c').colors)
      colors.reverse()
      
    • Erstellen Sie in Ihrer Funktion mit plt.figure() einmalig eine neue (leere) Grafik.
    • Testen Sie in Ihrer Funktion jedes der Argumente grid, f_h und name, ob es sich um eine Python Liste handelt (Hinweis: nutzen Sie dafür isinstance), und wandeln Sie es gegebenenfalls in eine Liste der Länge der 1 um.
    • Stellen Sie in Ihrer Funktion sicher, dass alle drei Listen dieselbe Länge haben.
    • Iterieren Sie in Ihrer Funktion über diese Listen (Hinweis: nutzen Sie zip) und gehen Sie wie oben vor, um jede Interpolierte bezüglich ihres Gitters zu visualisieren. Geben Sie in Ihrer Funktion zusätzlich beim Aufruf von plot mit Hilfe von color=colors.pop() jeder visualisierung eine andere Farbe.
    • Rufen Sie danchach in Ihrer Funktion einmal legend() auf, um die Legend anzeigen zu lassen.
In [9]:
colors = list(plt.get_cmap('tab20c').colors)
colors.reverse()

def visualize(grid, f_h, name=''):
    plt.figure()
    if not isinstance(grid, list):
        grid = [grid,]
    if not isinstance(f_h, list):
        f_h = [f_h,]
    if not isinstance(name, list):
        name = [name,]
    assert len(grid) == len(f_h) == len(name)
    for gg, ff, nn in zip(grid, f_h, name):
        assert gg.dim == 1
        assert len(ff) == gg.size(1)
        plt.plot(gg.centers(1), ff, color=colors.pop(), label=nn)
    plt.legend()
  1. Testen Sie Ihre Funktion für die oben angegeben Gitter. Erstellen Sie dazu drei leere Listen und füllen Sie diese mit Hilfe einer for-Schleife mit den entsprechenden Gittern, Interpolierten und Namen (welche die Anzahl der Gitterelemente enthalten).
In [10]:
grids = []
funcs = []
names = []
for I in (2**2, 2**3, 2**4, 2**5):
    gg = OnedGrid(domain=(0, 2*np.pi), num_intervals=I)
    grids.append(gg)
    funcs.append(interpolate(gg, f))
    names.append('f_h ({} grid elements)'.format(I))

visualize(grids, funcs, names)

Aufgabe 3: Finite Differenzen Verfahren in einer Raumdimension

Wir betrachten das vereinfachte Diffusionsproblems aus Blatt 00 in einer Raumdimension mit $A = 1$ (Poisson Problem), gesucht ist also $u \in C^2(\Omega)$, sodass

$$\begin{align*}-\partial_{xx} u(x) &= f(x) &&\text{für } x \in \Omega\\ u(x) &= g_\text{D}(x) &&\text{für } x \in \partial\Omega.\end{align*}$$

Zur Diskretisierung mit dem Verfahren der Finiten Differenzen für ein gegebenes äquidistantes Gitter $\mathcal{T}_h$ gehen wir folgendermaßen vor:

  1. Wir betrachten den Differentialoperator $\partial_{xx}:C^2(\Omega) \to C^0(\Omega)$, $u \mapsto \partial_{xx}u$. Gesucht ist eine Approximation, welche für Funktionen in $S_h^1$ wohldefiniert ist: $\partial_{h,h}: S_h^1 \to S_h^1$.

    • Betrachten Sie dazu für eine Funktion $u \in C^\infty(\Omega)$ die Taylor-Entwicklung von $u$ um einen Gitterpunkt $x_i$, ausgewertet an einem beliebigen Punkt $y \in \Omega$:

      $$u_(y) = u(x_i) + u'(x_i) \cdot (y - x_i) + \tfrac{1}{2} u''(x_i)\cdot(y - x_i)^2 + \mathcal{O}\big((y - x_i)^3\big)$$

    • Nutzen Sie diese Entwicklung, ausgewertet an den benachbarten Gitterpunkten $x_{i - 1}$ und $x_{i + 1}$, um einen Differenzenquotionten als Approximation von $u''(x_i)$ der Güte $\mathcal{O}(h)$ herzuleiten (Hinweis: nuzten Sie die Gitterweite $h$, um die Darstellung zu vereinfachen).

    • Definieren Sie $\partial_{h, h}$ mit Hilfe dieses Differenzenquotient.

Lösung: Obige Taylor-Entwicklung ausgewertet an $x_{i - 1}$ und $x_{i + 1}$ ergibt $$\begin{align*} u(x_{i - 1}) &= u(x_i) + u'(x_i)\cdot -h + \tfrac{1}{2}u''(x_i)\cdot h^2 + \mathcal{O}(h^3)\\ u(x_{i + 1}) &= u(x_i) + u'(x_i)\cdot h + \tfrac{1}{2}u''(x_i)\cdot h^2 + \mathcal{O}(h^3). \end{align*}$$ Nach Addition bleiben nur noch Terme $0$-ter und $2$-ter Ordnung, $$\begin{align*} u(x_{i - 1}) + u(x_{i + 1}) = 2 u(x_i) + u''(x_i) + \mathcal{O}(h^3), \end{align*}$$ sodass wir nach Umordnen und Division durch $h^2$ den Differenzenquotienten $$\begin{align*} u' '(x_{i}) = \frac{u(x_{i - 1}) - 2u(x_i) + u(x_{i + 1})}{h^2} + \mathcal{O}(h) \end{align*}$$ für die zweite Ableitung von $u$ in einem Gitterpunkt mit Fehler der Ordnung $h$ erhalten.

Lösung: Dadurch motiviert definieren wir als Approximation des Differentialoperators $\partial_{xx}$ den diskreten Operator auf dem Unterraum der diskreten Funktionen mit Null-Randwerten (obiger Differenzenquotient ist nur für innere Gitterpunkte sinnvoll) $$\begin{align*} \partial_{h, h}: S_h^1 \cap H^1_0(\Omega) &\to S_h^1 \cap H^1_0(\Omega),\\ u_h &\mapsto v_h := \partial_{h, h}\; u_h, \end{align*}$$ der Aufgrund oben genannter Isomorphie zwischen $S_h^1$ und $\mathbb{R}^{I 1}$ eindeutig durch die Angabe des DoF-Vektors von $v_h$ definiert ist (zur Erinnerung: $\underline{u_h}$ ist der DoF-Vektor von $u_h$): $$\begin{align*} (\underline{v_h})_i := \begin{cases} h^{-2} \big((\underline{u_h})_{i - 1} - 2\, (\underline{u_h})_i + (\underline{u_h})_{i + 1}\big)&\text{, für } 1 \leq i < I,\\ 0&\text{, für } i \in \{0, I\} \end{cases}. \end{align*}$$

  1. Leiten Sie eine Approximationen obiger Differentialgleichung für die approximative Lösung $u_h \in S_h^1$ her.

    • Formulieren Sie dazu die PDGL um, indem Sie $\partial_{h, h}$ verwenden, und Gleichheit nur noch an allen Knotenpunkten des Gitters fordern.
    • Leiten Sie daraus $I - 1$ Gleichungen für den Zusammenhang zwischen der gesuchten Funktion $u_h$ und der gegebenen Funktion $f$ an allen inneren Gitterpunkten $x_i$, für $1 \leq i \leq I$, her.
    • Leiten Sie mit Hilfe der Randbedingung zwei Bedingungen für $u_h$ an den Randpunkten des Gitters, $x_0$ und $x_{I + 1}$, her.

Lösung: Gesucht ist $u_h \in S_h^1$, sodass $$\begin{align*} -\partial_{h, h}\, u_h(x_i) &= f(x_i) &&\text{für } 1 \leq i < I,\\ u_h(x_i) &= g_\text{D}(x_i) &&\text{für } i \in \{0, I\}. \end{align*}$$

Daraus ergeben sich die $I - 1$ Gleichungen für die inneren Knoten, $$\begin{align*} -\frac{u_h(x_{i - 1})}{h^2} + 2\, \frac{u_h(x_i)}{h^2} - \frac{u_h(x_{i + 1}}{h^2} = f(x_i)&&\text{für } 1 \leq i < I, \end{align*}$$ und die zwei Gleichungen für die Randknoten, $$\begin{align*} u_h(x_0) = g_\text{D}(x_0) && u_h(x_{I + 1}) = g_\text{D}(x_{I + 1}). \end{align*}$$

  1. Leiten Sie mit Hilfe dieser $I + 1$ Gleichungen ein Gleichungssystem der Form

    $$\underline{b_h} \cdot \underline{u_h} = \underline{f_h}$$

    für den DoF-Vektor der approximativen Lösung $u_h \in S_h^1$ her, indem Sie

    • die Einträge der Matrix $\underline{b_h} \in \mathbb{R}^{I + 1 \times I + 1}$ und
    • die Einträge des Vektors $\underline{f_h} \in \mathbb{R}^{I + 1}$

    bestimmen.

Lösung: Gesucht ist $\underline{u_h} \in \mathbb{R}^{I + 1}$, sodass $$\begin{align*} \underline{b_h} \cdot \underline{u_h} = \underline{f_h}, \end{align*}$$ mit $$\begin{align*} (\underline{b_h})_{ij} := \begin{cases} 1,&\text{für } j = i, i \in \{0, I\},\\ -\tfrac{1}{h^2},&\text{für } j = i - 1, 1 \leq i < I,\\ \tfrac{2}{h^2},&\text{für } j = i, 1 \leq i < I,\\ -\tfrac{1}{h^2},&\text{für } j = i + 1, 1 \leq i < I,\\ 0,&\text{sonst} \end{cases}&&\text{und}&& (\underline{f_h})_j := \begin{cases} g_\text{D}(x_i),&\text{für } i \in \{0, I\},\\ f(x_i),&\text{für } 1 \leq i < I\\ \end{cases}. \end{align*}$$

  1. Assemblieren Sie die Matrix $\underline{b_h}$ und den Vektor $\underline{f_h}$ in Numpy Arrays entsprechender Größe mit Hilfe von for-Schleifen und/oder Ihrer interpolate Funktion.
In [11]:
I = grid.size(0)
h = 2*np.pi / I

b_h = np.zeros((I + 1, I + 1))
f_h = np.zeros(I + 1)

for i in range(1, I):
    b_h[i][i - 1] = -1/h**2
    b_h[i][i] = 2/h**2
    b_h[i][i + 1] = -1/h**2
    f_h[i] = f.evaluate(vertices[i])

b_h[0][0] = 1
f_h[0] = 0.
b_h[I][I] = 1
f_h[I] = 0.

print(b_h)
print()
print(f_h)
[[ 1.          0.          0.          0.          0.        ]
 [-0.40528473  0.81056947 -0.40528473  0.          0.        ]
 [ 0.         -0.40528473  0.81056947 -0.40528473  0.        ]
 [ 0.          0.         -0.40528473  0.81056947 -0.40528473]
 [ 0.          0.          0.          0.          1.        ]]

[ 0.0000000e+00  1.0000000e+00  1.2246468e-16 -1.0000000e+00
  0.0000000e+00]
  1. Lösen Sie das resultierende Gleichungssystem mit Hilfe von numpy.linalg.solve (Hinweis: numpy ist als np verfügbar).
In [12]:
u_h = np.linalg.solve(b_h, f_h)
In [13]:
visualize(grid, u_h, 'u_h')
  1. Fassen Sie Ihr Programm in einer Funktion u_h = discretize_elliptic_1d_fd(grid, f, g_D) zusammen und testen Sie Ihr Programm für $\Omega = (0, 1)$, $f = 1$ und $g_\text{D} = 0$ und für Gitter mit $2^2, 2^3, 2^4, 2^5$ Elementen (dabei soll g_D ein Python Tuple mit zwei Werten sein, also in diesem Beispiel g_D = (0, 0)). Visualisieren Sie die Lösungen mit Hilfe der visualize Funktion aus Aufgabe 2.
In [14]:
def discretize_elliptic_1d_fd(grid, f, g_D):
    vertices = grid.centers(1)
    h = (grid._domain[1] - grid._domain[0]) / I

    b_h = np.zeros((I + 1, I + 1))
    f_h = np.zeros(I + 1)

    for i in range(1, I):
        b_h[i][i - 1] = -1/h**2
        b_h[i][i] = 2/h**2
        b_h[i][i + 1] = -1/h**2
        f_h[i] = f.evaluate(vertices[i])

    b_h[0][0] = 1
    f_h[0] = g_D[0]
    b_h[I][I] = 1
    f_h[I] = g_D[1]
    
    u_h = np.linalg.solve(b_h, f_h)

    return u_h
In [15]:
omega = (0, 1)
f = ConstantFunction(1.)
g_D = (0., 0.)

grids = []
solutions = []
names = []

for I in (2**2, 2**3, 2**4, 2**5):
    grid = OnedGrid(omega, num_intervals=I)
    u_h = discretize_elliptic_1d_fd(grid, f, g_D)
    grids.append(grid)
    solutions.append(u_h)
    names.append('u_h ({} elements)'.format(I))
In [16]:
visualize(grids, solutions, names)
  1. Testen Sie Ihr Programm für $\Omega = (0, 1)$ und ein Gitter mit $2^7$ Elementen und die folgenden Fälle, visualisieren Sie die Lösungen.

    • $f = 1$, $g_D(x) = \begin{cases}0,&x = 0\\1, &x = 1\end{cases}$

    • $g_D = 0$, $f(x) = \begin{cases}1, &x \in [0.2, 0.4]\\0, &\text{sonst}\end{cases}$

In [17]:
omega = (0, 1)
I = 2**7

for f, g_D, name in ((ConstantFunction(1), (0., 1.), 'u_h (case 1)'),
                     (ExpressionFunction('(0.2 <= x[..., 0]) * (x[..., 0] <= 0.4) * 1.'), (0., 0.), 'u_h (case 2)')):
    grid = OnedGrid(omega, num_intervals=I)
    u_h = discretize_elliptic_1d_fd(grid, f, g_D)
    visualize(grid, u_h, name)
  1. Messen Sie die benötigte Zeit sowohl für die Erstellung des Gitters (inklusive einem Aufruf von grid.centers(1)), die Assemblierung als auch für die Lösung des Problems für verschieden feine Gitter und visualisieren Sie die Zeiten in Abhängigkeit von $I$. Experimentieren Sie mit verschiedenen Algorithmen zur Assemblierung (for-Schleifen, Vektorisiert sparse).
In [18]:
import time

assemble_data = {}
solve_data = {}

def measure_timings(discretizer, name, max_refinements=13):

    omega = (0, 1)
    f = ConstantFunction(1)
    g_D = (0, 0)

    data = {'I': [], 'grid': [], 'assemble': [], 'solve': []}
    assemble_data[name] = ([], [])
    solve_data[name] = ([], [])

    for ref in range(2, max_refinements + 1):
        I = 2**ref
        data['I'].append(I)
        timings = discretizer(omega, I, f, g_D)
        for kk, vv in timings.items():
            data[kk].append(vv)
        assemble_data[name][0].append(I)
        assemble_data[name][1].append(timings['assemble'])
        solve_data[name][0].append(I)
        solve_data[name][1].append(timings['solve'])
        print()
        
    plt.figure()
    for id in ('grid', 'assemble', 'solve'):
        plt.semilogy(data['I'], data[id], label=id)
    plt.legend()
    plt.xlabel('I')
    plt.ylabel('time (s)')
In [19]:
def discretize_double_for(omega, I, f, g_D):
    timings = {}
    print('creating grid with {} elements ... '.format(I), end='')
    t = time.time()
    grid = OnedGrid(omega, I)
    vertices = grid.centers(1) # call centers for time measurements, is created on demand
    timings['grid'] = time.time() - t
    print('done (took {}s)'.format(timings['grid']))
    print('assembling ... ', end='')
    t = time.time()    
    h = (omega[1] - omega[0]) / I

    b_h = np.zeros((I + 1, I + 1))
    f_h = np.zeros(I + 1)

    for i in range(0, I + 1):
        for j in range(0, I + 1):
            if i == 0 and j == 0:
                b_h[i][j] = 1.
            elif i == I and j == I:
                b_h[i][j] = 1.
            elif j == i - 1:
                b_h[i][j] = -1/h**2
            elif j == i:
                b_h[i][j] = 2/h**2
            elif j == i + 1:
                b_h[i][j] = -1/h**2
            else:
                b_h[i][j] = 0.

        if i == 0:
            f_h[i] = g_D[0]
        elif i == I:
            f_h[I] = g_D[1]
        else:
            f_h[i] = f.evaluate(vertices[i])
    
    timings['assemble'] = time.time() - t
    print('done (took {}s)'.format(timings['assemble']))
    print('solving ... ', end='')
    t = time.time()
    u_h = np.linalg.solve(b_h, f_h)
    timings['solve'] = time.time() - t
    print('done (took {}s)'.format(timings['solve']))

    return timings

measure_timings(discretize_double_for, 'double_for')
creating grid with 4 elements ... done (took 0.0018470287322998047s)
assembling ... done (took 8.988380432128906e-05s)
solving ... done (took 8.940696716308594e-05s)

creating grid with 8 elements ... done (took 0.0016553401947021484s)
assembling ... done (took 9.131431579589844e-05s)
solving ... done (took 5.459785461425781e-05s)

creating grid with 16 elements ... done (took 0.0021369457244873047s)
assembling ... done (took 0.0003833770751953125s)
solving ... done (took 6.270408630371094e-05s)

creating grid with 32 elements ... done (took 0.001720428466796875s)
assembling ... done (took 0.0011391639709472656s)
solving ... done (took 0.004722118377685547s)

creating grid with 64 elements ... done (took 0.001699686050415039s)
assembling ... done (took 0.0045146942138671875s)
solving ... done (took 0.04490804672241211s)

creating grid with 128 elements ... done (took 0.0024917125701904297s)
assembling ... done (took 0.017461538314819336s)
solving ... done (took 0.0069904327392578125s)

creating grid with 256 elements ... done (took 0.0019199848175048828s)
assembling ... done (took 0.06853365898132324s)
solving ... done (took 0.0018961429595947266s)

creating grid with 512 elements ... done (took 0.0017406940460205078s)
assembling ... done (took 0.17341923713684082s)
solving ... done (took 0.008201122283935547s)

creating grid with 1024 elements ... done (took 0.0022652149200439453s)
assembling ... done (took 0.5321967601776123s)
solving ... done (took 0.028525352478027344s)

creating grid with 2048 elements ... done (took 0.002476215362548828s)
assembling ... done (took 1.9969477653503418s)
solving ... done (took 0.29410529136657715s)

creating grid with 4096 elements ... done (took 0.002365589141845703s)
assembling ... done (took 7.603452682495117s)
solving ... done (took 1.3198964595794678s)

creating grid with 8192 elements ... done (took 0.003214120864868164s)
assembling ... done (took 30.35364270210266s)
solving ... done (took 9.32965874671936s)

In [20]:
def discretize_single_for(omega, I, f, g_D):
    timings = {}
    print('creating grid with {} elements ... '.format(I), end='')
    t = time.time()
    grid = OnedGrid(omega, I)
    vertices = grid.centers(1) # call centers for time measurements, is created on demand
    timings['grid'] = time.time() - t
    print('done (took {}s)'.format(timings['grid']))
    print('assembling ... ', end='')
    t = time.time()    
    h = (omega[1] - omega[0]) / I

    b_h = np.zeros((I + 1, I + 1))
    f_h = np.zeros(I + 1)

    for i in range(1, I):
        b_h[i][i - 1] = -1/h**2
        b_h[i][i] = 2/h**2
        b_h[i][i + 1] = -1/h**2
        f_h[i] = f.evaluate(vertices[i])

    b_h[0][0] = 1
    f_h[0] = g_D[0]
    b_h[I][I] = 1
    f_h[I] = g_D[1]
    
    timings['assemble'] = time.time() - t
    print('done (took {}s)'.format(timings['assemble']))
    print('solving ... ', end='')
    t = time.time()
    u_h = np.linalg.solve(b_h, f_h)
    timings['solve'] = time.time() - t
    print('done (took {}s)'.format(timings['solve']))

    return timings


measure_timings(discretize_single_for, 'single_for')
creating grid with 4 elements ... done (took 0.0018112659454345703s)
assembling ... done (took 4.124641418457031e-05s)
solving ... done (took 4.76837158203125e-05s)

creating grid with 8 elements ... done (took 0.001062631607055664s)
assembling ... done (took 7.653236389160156e-05s)
solving ... done (took 4.935264587402344e-05s)

creating grid with 16 elements ... done (took 0.0012676715850830078s)
assembling ... done (took 0.00010347366333007812s)
solving ... done (took 7.176399230957031e-05s)

creating grid with 32 elements ... done (took 0.0031375885009765625s)
assembling ... done (took 0.0003161430358886719s)
solving ... done (took 0.028785228729248047s)

creating grid with 64 elements ... done (took 0.0016944408416748047s)
assembling ... done (took 0.00039076805114746094s)
solving ... done (took 0.04935264587402344s)

creating grid with 128 elements ... done (took 0.002192258834838867s)
assembling ... done (took 0.001280069351196289s)
solving ... done (took 0.07341432571411133s)

creating grid with 256 elements ... done (took 0.0040721893310546875s)
assembling ... done (took 0.002671480178833008s)
solving ... done (took 0.0324559211730957s)

creating grid with 512 elements ... done (took 0.0021867752075195312s)
assembling ... done (took 0.004328012466430664s)
solving ... done (took 0.05476236343383789s)

creating grid with 1024 elements ... done (took 0.0023088455200195312s)
assembling ... done (took 0.012657403945922852s)
solving ... done (took 0.04793667793273926s)

creating grid with 2048 elements ... done (took 0.0026204586029052734s)
assembling ... done (took 0.0211484432220459s)
solving ... done (took 0.22898054122924805s)

creating grid with 4096 elements ... done (took 0.002778768539428711s)
assembling ... done (took 0.04272603988647461s)
solving ... done (took 1.254960298538208s)

creating grid with 8192 elements ... done (took 0.003379344940185547s)
assembling ... done (took 0.07222843170166016s)
solving ... done (took 9.261070728302002s)

In [21]:
def discretize_vectorized_dense(omega, I, f, g_D):
    timings = {}
    print('creating grid with {} elements ... '.format(I), end='')
    t = time.time()
    grid = OnedGrid(omega, I)
    vertices = grid.centers(1) # call centers for time measurements, is created on demand
    timings['grid'] = time.time() - t
    print('done (took {}s)'.format(timings['grid']))
    print('assembling ... ', end='')
    t = time.time()    
    h = (omega[1] - omega[0]) / I

    b_h = (
        np.diag(np.ones(I)*(-1/h**2), k=-1)
        + np.diag(np.ones(I + 1)*(2/h**2))
        + np.diag(np.ones(I)*(-1/h**2), k=1)
    )
    b_h[0][0] = 1
    b_h[0][1] = 0
    b_h[I - 1][I] = 0
    b_h[I][I] = 1
    
    f_h = interpolate(grid, f)
    f_h[0] = g_D[0]
    f_h[I] = g_D[1]

    timings['assemble'] = time.time() - t
    print('done (took {}s)'.format(timings['assemble']))
    print('solving ... ', end='')
    t = time.time()
    u_h = np.linalg.solve(b_h, f_h)
    timings['solve'] = time.time() - t
    print('done (took {}s)'.format(timings['solve']))

    return timings


measure_timings(discretize_vectorized_dense, 'vectorized_dense')
creating grid with 4 elements ... done (took 0.0013654232025146484s)
assembling ... done (took 0.00025963783264160156s)
solving ... done (took 5.507469177246094e-05s)

creating grid with 8 elements ... done (took 0.0014581680297851562s)
assembling ... done (took 0.0003077983856201172s)
solving ... done (took 5.555152893066406e-05s)

creating grid with 16 elements ... done (took 0.0027196407318115234s)
assembling ... done (took 0.00031065940856933594s)
solving ... done (took 6.151199340820312e-05s)

creating grid with 32 elements ... done (took 0.001918792724609375s)
assembling ... done (took 0.0003039836883544922s)
solving ... done (took 0.0001385211944580078s)

creating grid with 64 elements ... done (took 0.0015380382537841797s)
assembling ... done (took 0.00035881996154785156s)
solving ... done (took 0.000194549560546875s)

creating grid with 128 elements ... done (took 0.0028171539306640625s)
assembling ... done (took 0.0007672309875488281s)
solving ... done (took 0.045174598693847656s)

creating grid with 256 elements ... done (took 0.0032362937927246094s)
assembling ... done (took 0.0016613006591796875s)
solving ... done (took 0.007613182067871094s)

creating grid with 512 elements ... done (took 0.0018956661224365234s)
assembling ... done (took 0.004517078399658203s)
solving ... done (took 0.008663654327392578s)

creating grid with 1024 elements ... done (took 0.0018553733825683594s)
assembling ... done (took 0.020747900009155273s)
solving ... done (took 0.160660982131958s)

creating grid with 2048 elements ... done (took 0.0023720264434814453s)
assembling ... done (took 0.09130215644836426s)
solving ... done (took 0.18542695045471191s)

creating grid with 4096 elements ... done (took 0.0026819705963134766s)
assembling ... done (took 0.22097396850585938s)
solving ... done (took 1.2869422435760498s)

creating grid with 8192 elements ... done (took 0.003643512725830078s)
assembling ... done (took 0.705303430557251s)
solving ... done (took 12.4393150806427s)

In [22]:
import scipy as sp

def discretize_vectorized_sparse(omega, I, f, g_D):
    timings = {}
    print('creating grid with {} elements ... '.format(I), end='')
    t = time.time()
    grid = OnedGrid(omega, I)
    vertices = grid.centers(1) # call centers for time measurements, is created on demand
    timings['grid'] = time.time() - t
    print('done (took {}s)'.format(timings['grid']))
    print('assembling ... ', end='')
    t = time.time()    
    h = (omega[1] - omega[0]) / I

    lower_diag = np.ones(I)*(-1/h**2)
    main_diag = np.ones(I + 1)*(2/h**2)
    upper_diag = np.ones(I)*(-1/h**2)
    main_diag[0] = 1
    upper_diag[0] = 0
    main_diag[I] = 1
    lower_diag[I - 1]
    b_h = sp.sparse.diags(
        [lower_diag, main_diag, upper_diag],
        [-1, 0, 1],
        shape=(I + 1, I + 1))
    
    f_h = interpolate(grid, f)
    f_h[0] = g_D[0]
    f_h[I] = g_D[1]

    timings['assemble'] = time.time() - t
    print('done (took {}s)'.format(timings['assemble']))
    print('solving ... ', end='')
    t = time.time()
    u_h = sp.sparse.linalg.spsolve(b_h, f_h)
    timings['solve'] = time.time() - t
    print('done (took {}s)'.format(timings['solve']))

    return timings


measure_timings(discretize_vectorized_sparse, 'vectorized_sparse')
creating grid with 4 elements ... done (took 0.0013773441314697266s)
assembling ... done (took 0.0005071163177490234s)
solving ... done (took 0.0019154548645019531s)

creating grid with 8 elements ... done (took 0.0017876625061035156s)
assembling ... done (took 0.00034809112548828125s)
solving ... done (took 0.0011038780212402344s)

creating grid with 16 elements ... done (took 0.0010056495666503906s)
assembling ... done (took 0.0004329681396484375s)
solving ... done (took 0.001003265380859375s)

creating grid with 32 elements ... done (took 0.0013573169708251953s)
assembling ... done (took 0.00037789344787597656s)
solving ... done (took 0.0013315677642822266s)

creating grid with 64 elements ... done (took 0.0017156600952148438s)
assembling ... done (took 0.0005342960357666016s)
solving ... done (took 0.0014538764953613281s)

creating grid with 128 elements ... done (took 0.001810312271118164s)
assembling ... done (took 0.0005204677581787109s)
solving ... done (took 0.0009758472442626953s)

creating grid with 256 elements ... done (took 0.0019392967224121094s)
assembling ... done (took 0.00054168701171875s)
solving ... done (took 0.0014748573303222656s)

creating grid with 512 elements ... done (took 0.00350189208984375s)
assembling ... done (took 0.00038814544677734375s)
solving ... done (took 0.0009298324584960938s)

creating grid with 1024 elements ... done (took 0.0013928413391113281s)
assembling ... done (took 0.0004787445068359375s)
solving ... done (took 0.0015652179718017578s)

creating grid with 2048 elements ... done (took 0.0018897056579589844s)
assembling ... done (took 0.0010287761688232422s)
solving ... done (took 0.004473447799682617s)

creating grid with 4096 elements ... done (took 0.006274223327636719s)
assembling ... done (took 0.0006935596466064453s)
solving ... done (took 0.003818988800048828s)

creating grid with 8192 elements ... done (took 0.0020890235900878906s)
assembling ... done (took 0.0006303787231445312s)
solving ... done (took 0.006384134292602539s)

/home/falbr_01/Teaching/1920__praktikum_zur_vorlesung_NPDE1/venv/lib/python3.7/site-packages/scipy/sparse/linalg/dsolve/linsolve.py:133: SparseEfficiencyWarning: spsolve requires A be CSC or CSR matrix format
  SparseEfficiencyWarning)
In [23]:
plt.figure()
for name, data in assemble_data.items():
    plt.semilogy(data[0], data[1], label=name)
plt.legend()
plt.title('assemble')
plt.xlabel('I')
plt.ylabel('time (s)')
Out[23]:
Text(0, 0.5, 'time (s)')
In [24]:
plt.figure()
for name, data in solve_data.items():
    plt.semilogy(data[0], data[1], label=name)
plt.legend()
plt.title('solve')
plt.xlabel('I')
plt.ylabel('time (s)')
Out[24]:
Text(0, 0.5, 'time (s)')