Praktikum zur Vorlesung Numerik partieller Differentialgleichungen I

Mario Ohlberger, Felix Schindler

Blatt 00, 16.10.2019

Aufgabe 0

(i) einmalige Einrichtung

Dazu müssen auf Ihrem Rechner die Befehle python3 und virtualenv verfügbar sein. Das können Sie überprüfen, indem Sie ein Terminal starten (unter Ubuntu: Super/Windows Taste, terminal eingeben, Return) und die folgenden Befehle eingeben:

python3 --version
virtualenv --version

Beide Befehle sollten Ihnen die jeweilige Version des Programms anzeigen (und keine Fehlermeldung). Zur Einrichtung gehen Sie folgendermaßen vor:

  • Ins HOME Verzeichnis wechseln und einen Projekt-Ordner anlegen
cd      # ins HOME Verzeichnis wechseln
pwd     # Ort des aktuellen Verzeichnises anzeigen
ls -lh  # Inhalt des aktuellen Verzeichnisses anzeigen
mkdir NPDGL1 # ein neues Verzeichnis erstellen
cd NPDGL1    # in dieses Verzeichnis wechseln
pwd
ls -lh
  • eine virtuelle Umgebung für die Programmierung mit Python schaffen
virtualenv --python=python3 python_umgebung_NPDGL1
ls -lh
  • die virtuelle Umgebung aktivieren
source python_umgebung_NPDGL1/bin/activate
  • benötigte Software in der virtuellen Umgebung installieren
pip install --upgrade pip
pip install pymor[full]
pip install notebook
du -sh python_umgebung_NPDGL1

Dabei installiert pip install pymor[full] das neueste Release von pyMOR (momentan 0.5.2). Mehr dazu finden Sie auf https://pymor.org (gehen Sie dort auf View on Github und wählen anstelle von branch: master links oben Tags: 0.5.2 aus, um die entsprechende README.md anzuzeigen).

  • Arbeitsverzeichnis erstellen
mkdir notebooks

(ii) ab jetzt jedes mal

  • ins Projektverzeichnis wechseln
cd ~/NPDGL1
  • die virtuelle Umgebung aktivieren
source python_umgebung_NPDGL1/bin/activate
  • Notebook server starten
jupyter-notebook --notebook-dir=notebooks

(iii) Blatt 00 nutzbar machen

  • Laden Sie sich von der Homepage der Vorlesung blatt_00.ipynb herunter und speichern Sie dieses im Verzeichnis NPDGL1/notebooks/ als blatt_00.ipynb.

  • Starten Sie den Notebook server (siehe ii) und führen Sie blatt_00.ipynb aus.

Ab jetzt können Sie direkt im jeweiligen Übungsblatt arbeiten, fügen Sie dazu einfach direkt nach der Aufgabenstellung mit + (oben links) eine neue Code Zelle hinzu. Eine Code Zelle können Sie mit STRG + Return ausführen. Hilfe zu einem Typ Foo oder einer Objekt foo können Sie mit Foo? oder foo? erhalten.

In [1]:
# führen Sie in einem Notebook immer die folgenden Befehle als erstes aus

%matplotlib notebook
import numpy as np
from pymor.basic import *

Ab jetzt ist die numpy Bibliothek unter np verfügbar und alles aus pymor.basic direkt verfügbar.

Definition: vereinfachtes Diffusionsproblem

Wir betrachten ein vereinfachtes stationäres Diffusionsproblem. Sei dazu

  • ein beschränktes polygonales Gebiet $\Omega \subset \mathbb{R}^2$ mit Lipschitz-Rand $\partial \Omega$,
  • eine Diffusion $A: \Omega \to \mathbb{R}$ und
  • eine rechte Seite $f: \Omega \to \mathbb{R}$

gegeben. Gesucht ist die Lösung $u: \Omega \to \mathbb{R}$, sodass

$$\begin{align} -\nabla\cdot( A \nabla u ) &= f &&\text{in } \Omega\\ u &= 0 &&\text{auf } \partial \Omega, \end{align}$$

(im schwachen Sinne, siehe Kapitel 1) erfüllt ist.

Aufgabe 1: Gebiete und Gitter

  1. Suchen Sie in der pyMOR Dokumentation auf docs.pymor.org/ nach einer Klasse, die das Gebiet $$\Omega = [0, 1]^2$$ mit Dirichlet-Randwerten modellieren kann und legen Sie ein entsprechendes Objekt mit Namen omega an. Nutzen Sie dazu auch die Möglichkeit des jupyter Notebooks, sich zu einem beliebigen Objekt mit Hilfe des ?-postfix die Dokumentation anzeigen zu lassen (z.B. np.array?).
In [2]:
# => API Dokumentation http://docs.pymor.org/en/0.5.2/index.html#api-documentation
# => Modul "domaindescriptions": http://docs.pymor.org/en/0.5.2/generated/pymor.domaindescriptions.html
# => Klasse RectDomain, diese ist Dank des * imports direkt verfügbar
# => Hilfe zu RectDomain anzeigen lassen: RectDomain?
# => enthält schon Dirichlet Randwerttyp überall

omega = RectDomain(([0, 0], [1, 1]))
  1. Suchen Sie nach einer Möglichkeit ein konformes Dreiecksgitter $\mathcal{T}_h$ als Partitionierung des Gebiets $\Omega$ mit 16 Dreiecken zu erstellen, und geben Sie folgende Informationen aus:

    • Dimension d des Gitters
    • Anzahl der Elemente (Kodimension 0 Entitäten, in diesem Fall Dreiecke) im Gitter
    • Anzahl der Kanten (Kodimension 1 Entitäten, in diesem Fall Intervalle) im Gitter
    • Anzahl der Knoten (Kodimension d Entitäten, Punkte) im Gitter
    • die Position eines jeden Knoten des Gitters mit Hilfe einer for-Schleife

    Vergleichen Sie die Positionen der Knoten mit der Dokumentation.

In [3]:
# => API Dokumentation, Modul grid, Klasse TriaGrid
# => Hilfe anzeigen lassen: TriaGrid?

grid = TriaGrid(num_intervals=(2, 2), domain=omega.domain)
print(grid)
print()
print('grid lives in a {}-dimensional space'.format(grid.dim))
print('grid has {} simplices'.format(grid.size(0)))
print('grid has {} edges'.format(grid.size(1)))
print('grid has {} vertices'.format(grid.size(2)))

vertices = grid.centers(grid.dim)
print()
print(type(vertices))
print(vertices.shape)

print()
print('vertices:')
counter = 0
for vertex in vertices:
    print('vertex {} has position {}'.format(counter, vertex))
    counter += 1
Tria-Grid on domain [0,1] x [0,1]
x0-intervals: 2, x1-intervals: 2
elements: 16, edges: 28, vertices: 13

grid lives in a 2-dimensional space
grid has 16 simplices
grid has 28 edges
grid has 13 vertices

<class 'numpy.ndarray'>
(13, 2)

vertices:
vertex 0 has position [5.55111512e-17 5.55111512e-17]
vertex 1 has position [5.00000000e-01 5.55111512e-17]
vertex 2 has position [1.00000000e+00 5.55111512e-17]
vertex 3 has position [5.55111512e-17 5.00000000e-01]
vertex 4 has position [0.5 0.5]
vertex 5 has position [1.  0.5]
vertex 6 has position [5.55111512e-17 1.00000000e+00]
vertex 7 has position [0.5 1. ]
vertex 8 has position [1. 1.]
vertex 9 has position [0.25 0.25]
vertex 10 has position [0.75 0.25]
vertex 11 has position [0.25 0.75]
vertex 12 has position [0.75 0.75]

Aufgabe 2: analytische Funktionen vektorisiert implementieren

Sei $f: \Omega \to \mathbb{R}$ gegeben durch $f(x_0, x_1) := x_0 \cdot x_1$.

  1. Schreiben Sie eine Funktion f, die die Auswertung von $f$ and einem Punkt $x$ modelliert und werten Sie die Funktion f für jeden Knotenpunkt des Gitters einzeln mit Hilfe einer for-Schleife aus.
In [4]:
def f(x):
    return x[0]*x[1]

for x in vertices:
    print('f({}) = {}'.format(x, f(x)))
f([5.55111512e-17 5.55111512e-17]) = 3.0814879110195774e-33
f([5.00000000e-01 5.55111512e-17]) = 2.775557561562891e-17
f([1.00000000e+00 5.55111512e-17]) = 5.551115123125783e-17
f([5.55111512e-17 5.00000000e-01]) = 2.775557561562891e-17
f([0.5 0.5]) = 0.24999999999999994
f([1.  0.5]) = 0.49999999999999994
f([5.55111512e-17 1.00000000e+00]) = 5.551115123125783e-17
f([0.5 1. ]) = 0.4999999999999999
f([1. 1.]) = 0.9999999999999999
f([0.25 0.25]) = 0.0625
f([0.75 0.25]) = 0.1875
f([0.25 0.75]) = 0.1875
f([0.75 0.75]) = 0.5625
  1. Erstellen Sie ein feineres Gitter mit $1024^2$ Intervallen und messen Sie die benötigte Zeit, um f an jedem Knotenpunkt des feineren Gitters auszuwerten.
In [5]:
fine_grid = TriaGrid(num_intervals=(1024, 1024), domain=omega.domain)
print(fine_grid)

import time
tic = time.time()
values = f(fine_grid.centers(grid.dim))
toc = time.time() - tic
print()
print('evaluating non-vectorized function took {}s'.format(toc))
Tria-Grid on domain [0,1] x [0,1]
x0-intervals: 1024, x1-intervals: 1024
elements: 4194304, edges: 6293504, vertices: 2099201

evaluating non-vectorized function took 17.777233600616455s
  1. Versuchen Sie, f an allen Knotenpunkten des groben Gitters gleichzeitig auszuwerten. Warum funktioniert das nicht?
In [6]:
# => an allen Punkten gleichzeitig auswerten
values = f(vertices)
print(values)
print(len(vertices))
print(len(values))
[2.77555756e-17 3.08148791e-33]
13
2
In [7]:
# Warum kommt das raus?
# Beim Aufruf f(vertices) passiert
vertices[0]*vertices[1]
# also eine elementweise Multiplikation des 0ten und 1ten Knotens
Out[7]:
array([2.77555756e-17, 3.08148791e-33])
  1. Schreiben Sie eine vektorisierte Variante von f, welche die elementweisen Operationen des numpy.ndarray ausnutzt und werten Sie diese Variante für alle Knotenpunkte des feineren Gitters gleichzeitig aus und messen Sie die benötigte Zeit.
In [8]:
# Gegeben eine Menge von Punkten als Matrix
print(vertices)
print(vertices.shape)

# extrahiere 0te und 1te Komponente für alle Punkte gleichzeitig
x_0s = vertices[..., 0] # 0te Spalte
x_1s = vertices[..., 1] # 1te Spalte
print()
print(type(x_0s))
print(x_0s)
print(x_1s)
[[5.55111512e-17 5.55111512e-17]
 [5.00000000e-01 5.55111512e-17]
 [1.00000000e+00 5.55111512e-17]
 [5.55111512e-17 5.00000000e-01]
 [5.00000000e-01 5.00000000e-01]
 [1.00000000e+00 5.00000000e-01]
 [5.55111512e-17 1.00000000e+00]
 [5.00000000e-01 1.00000000e+00]
 [1.00000000e+00 1.00000000e+00]
 [2.50000000e-01 2.50000000e-01]
 [7.50000000e-01 2.50000000e-01]
 [2.50000000e-01 7.50000000e-01]
 [7.50000000e-01 7.50000000e-01]]
(13, 2)

<class 'numpy.ndarray'>
[5.55111512e-17 5.00000000e-01 1.00000000e+00 5.55111512e-17
 5.00000000e-01 1.00000000e+00 5.55111512e-17 5.00000000e-01
 1.00000000e+00 2.50000000e-01 7.50000000e-01 2.50000000e-01
 7.50000000e-01]
[5.55111512e-17 5.55111512e-17 5.55111512e-17 5.00000000e-01
 5.00000000e-01 5.00000000e-01 1.00000000e+00 1.00000000e+00
 1.00000000e+00 2.50000000e-01 2.50000000e-01 7.50000000e-01
 7.50000000e-01]
In [9]:
# Das ganze kompakt in einer Funktion
def f(x):
    return x[..., 0]*x[..., 1]

# Kann an einem Punkt ausgewertet werden ...
print(f(vertices[0]))
# ... oder an allen Punkten gleichzeitig
print(f(vertices))
3.0814879110195774e-33
[3.08148791e-33 2.77555756e-17 5.55111512e-17 2.77555756e-17
 2.50000000e-01 5.00000000e-01 5.55111512e-17 5.00000000e-01
 1.00000000e+00 6.25000000e-02 1.87500000e-01 1.87500000e-01
 5.62500000e-01]
In [10]:
tic = time.time()
values = f(fine_grid.centers(grid.dim))
toc = time.time() - tic
print('took {}s'.format(toc))
took 0.014418601989746094s
  1. Erstellen Sie eine ExpressionFunction, um $f$ zu modellieren, werten Sie diese an allen Knotenpunkten des feinen Gitters aus und messen Sie die benötigte Zeit.
In [11]:
f = ExpressionFunction('x[..., 0]*x[..., 1]', dim_domain=2, shape_range=())
tic = time.time()
values = f.evaluate(fine_grid.centers(grid.dim))
toc = time.time() - tic
print('took {}s'.format(toc))
took 0.013719320297241211s

Aufgabe 3: Diffusionsproblem in pyMOR modellieren

Modellieren Sie das vereinfachte Diffusionsproblem mit

  • $A = 1$
  • $f = 1$

mit Hilfe eines analytischen Problems in pyMOR.

  1. Suchen Sie nach einer geeigneten Klasse, um $A$ und $f$ als Funktionen in pyMOR zu modellieren.
In [12]:
#ConstantFunction?
In [13]:
A = ConstantFunction(1, 2)
f = ConstantFunction(value=1, dim_domain=2)
  1. Machen Sie sich mit der Klasse StationaryProblem vertraut und legen Sie ein problem an, welches die gewünschten Datenfunktionen enthält.
In [14]:
#StationaryProblem?
In [15]:
problem = StationaryProblem(
    domain=omega,
    diffusion=ConstantFunction(1, 2),
    rhs=ConstantFunction(1, 2),
)

Aufgabe 4: Diffusionsproblem in pyMOR diskretisieren

  1. Diskretisieren Sie das stationäre Problem mit einer CG Diskretisierung (CG = continuous Galerkin = stetige Finite Elemente, siehe Kapitel 2), indem Sie die entsprechende Funktion aus dem pymor.discretizers Paket verwenden.
    • Rufen Sie diese Funktion mit dem analytischen Problem auf.
    • Was ist der Rückgabewert?
In [16]:
result = discretize_stationary_cg(problem)
00:18 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:18 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:18 DiffusionOperatorP1: Determine global dofs ...
00:18 DiffusionOperatorP1: Boundary treatment ...
00:18 DiffusionOperatorP1: Assemble system matrix ...
00:18 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:18 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:18 DiffusionOperatorP1: Determine global dofs ...
00:19 DiffusionOperatorP1: Boundary treatment ...
00:19 DiffusionOperatorP1: Assemble system matrix ...
00:19 L2ProductP1: Integrate the products of the shape functions on each element
00:19 L2ProductP1: Determine global dofs ...
00:19 L2ProductP1: Boundary treatment ...
00:19 L2ProductP1: Assemble system matrix ...
00:19 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:19 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:19 DiffusionOperatorP1: Determine global dofs ...
00:19 DiffusionOperatorP1: Boundary treatment ...
00:19 DiffusionOperatorP1: Assemble system matrix ...
00:19 L2ProductP1: Integrate the products of the shape functions on each element
00:19 L2ProductP1: Determine global dofs ...
00:19 L2ProductP1: Boundary treatment ...
00:19 L2ProductP1: Assemble system matrix ...
00:19 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:19 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:19 DiffusionOperatorP1: Determine global dofs ...
00:19 DiffusionOperatorP1: Boundary treatment ...
00:19 DiffusionOperatorP1: Assemble system matrix ...
In [17]:
type(result)
print()
for rr in result:
    print(rr)
    print()
<pymor.discretizations.basic.StationaryDiscretization object at 0x7f541b342990>

{'grid': TriaGrid((100, 100), [[0 0]
 [1 1]], False, False), 'boundary_info': <pymor.grids.boundaryinfos.BoundaryInfoFromIndicators object at 0x7f544929d310>, 'unassembled_d': <pymor.discretizations.basic.StationaryDiscretization object at 0x7f541b342250>}

In [18]:
disc, _ = discretize_stationary_cg(problem)
00:19 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:19 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:19 DiffusionOperatorP1: Determine global dofs ...
00:19 DiffusionOperatorP1: Boundary treatment ...
00:19 DiffusionOperatorP1: Assemble system matrix ...
00:19 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:19 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:19 DiffusionOperatorP1: Determine global dofs ...
00:19 DiffusionOperatorP1: Boundary treatment ...
00:19 DiffusionOperatorP1: Assemble system matrix ...
00:19 L2ProductP1: Integrate the products of the shape functions on each element
00:19 L2ProductP1: Determine global dofs ...
00:19 L2ProductP1: Boundary treatment ...
00:19 L2ProductP1: Assemble system matrix ...
00:19 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:20 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:20 DiffusionOperatorP1: Determine global dofs ...
00:20 DiffusionOperatorP1: Boundary treatment ...
00:20 DiffusionOperatorP1: Assemble system matrix ...
00:20 L2ProductP1: Integrate the products of the shape functions on each element
00:20 L2ProductP1: Determine global dofs ...
00:20 L2ProductP1: Boundary treatment ...
00:20 L2ProductP1: Assemble system matrix ...
00:20 DiffusionOperatorP1: Calulate gradients of shape functions transformed by reference map ...
00:20 DiffusionOperatorP1: Calculate all local scalar products beween gradients ...
00:20 DiffusionOperatorP1: Determine global dofs ...
00:20 DiffusionOperatorP1: Boundary treatment ...
00:20 DiffusionOperatorP1: Assemble system matrix ...
  1. Berechnen Sie eine approximative Lösung u_h des vereinfachten Diffusionsproblems mit Hilfe der Diskretisierung. Machen Sie sich dazu mit der Dokumentation von StationaryDiscretization vertraut oder finden Sie die entsprechende Funktion interaktiv (um die Methoden eines Objektes foo anzuzeigen, schreiben Sie foo. und drücken Sie Tab).
In [19]:
u_h = disc.solve()
00:20 StationaryDiscretization: Solving StationaryProblem_CG for {} ...
  1. Visualisieren Sie die approximative Lösung mit Hilfe der Diskretisierung
In [20]:
disc.visualize(u_h)

Aufgabe 5: Approximationsgüte

Berechnen Sie Approximationen u_h für verschieden Gitterweiten h = 1/2, 1/4, 1/16, 1/64, indem Sie das diameter Argument der Funktion discretize_stationary_cg nutzen.

  • Berechnen und visualisieren Sie diese Approximationen und vergleichen Sie sie.
  • Unterdrücken Sie die automatische Ausgabe des Diskretizers mit Hilfe von set_log_levels({'pymor': 'WARN'})
  • Geben Sie den jeweiligen diskreten Lösungsraum aus.
In [21]:
set_log_levels({'pymor': 'WARN'})

def discretize_and_visualize(h):
    disc, _ = discretize_stationary_cg(problem, diameter=h)
    print(disc.solution_space)
    u_h = disc.solve()
    disc.visualize(u_h)
In [22]:
for h in (1/2, 1/4, 1/16, 1/64):
    print('h = {}'.format(h))
    discretize_and_visualize(h)
    print()
h = 0.5
NumpyVectorSpace(13, STATE)
h = 0.25
NumpyVectorSpace(41, STATE)
h = 0.0625
NumpyVectorSpace(545, STATE)
h = 0.015625
NumpyVectorSpace(8321, STATE)