9.1. Finite Elemente Methode Einstiegsbeispiele#
9.1.1. Eindimensionaler Fall#
Zum Einstieg in die Methode der finite Elemente kommen wir auf das skalare Randwertproblem (1.1), dem Poisson Problem zurück
mit \(f(x) = 1\).
Die analytische Lösung erhalten wir in dem Fall leicht. Durch zweimaliges Integrieren der rechten Seite erhalten wir ein Polynom 2. Grades
Durch Einsetzen der Randbedingungen folgt die analytische Lösung
def uanalytic(x):
return -0.5*x*(x-1)
Für die numerische Lösung multiplitzieren wir die Differentialgleichung mit einer beliebigen Testfunktion \(v(x)\in C_0^\infty(0,1)\) und integrieren über das Intervall \((0,1)\)
Mit Hilfe der partiellen Integration erhalten wir die schwache Gleichung
Gesucht ist eine Funktion \(u(x)\) so, dass \(u(0)=u(1)=0\) und
Um der Gleichung zu genügen, muss die Lösung \(u\) nicht zwingend eine zweimal stetig differenzierbare Funktion sein. Anstelle dessen werden \(u\) und \(v\) jeweils einmal differenziert. Die Funktionen liegen im Sobolev-Raum \(H_0^1(0,1)\). Die rechte Seite muss ebenfalls nicht mehr zwingend stetig sein. Es reicht, wenn die Funktion \(f\) quadratisch integrierbar ist, daher \(f\in L_2(0,1)\). Die finite Elemente Methode benutzt die eingeführte verallgemeinerte Ableitung (5.1). Wir erhalten Lösungen, welche nicht immer zwingend auch Lösung der starken Gleichung (9.1) sein müssen.
Wir diskretisieren nun das Intervall \((0,1)\) in Teilintervalle \((x_{i-1},x_i)\) für \(i=1,\ldots, n\). Auf dieser Zerlegung definieren wir die Basisfunktionen
mit welchen wir den Sobolevraum \(H_0^1(0,1)\) approximieren.
Direkte (numpy) Lösung#
In der Abbildung Abb. 9.1 ist das Intervall in 5 Teilintervalle zerlegt. Entsprechend haben wir 5 +1 Basisfunktionen. Abgesehen von der ersten und letzten Basisfunktion erstreckt sich der Support jeweilen über zwei Teilintervalle. Die Basisfunktionen sind genau in einem Knoten \(x_i\) eins, in allen anderen beträgt der Funktionswert 0.
Abb. 9.1 FEM 1d affine Basisfunktionen#
Für die schwache Gleichung (9.2) erhalten wir das endlich dimensionale mit finite Elemente diskrete Problem
Gesucht ist eine Funktion
so, dass \(u(0) = u(1) = 0\) und
Setzt man die Darstellung für \(u_h\) ein, so folgt
Wir definieren die Matrix \(A\) und den Vektor \(b\)
und erhalten so das (reduzierte) lineare Gleichungssystem für die Koeffizienten \(u_1, \ldots, u_{n-1}\)
Um die Matrix \(A\) aufzustellen werden die Ableitungen der Basisfunktionen \(\varphi_i\) benötigt. Gehen wir wie im Beispiel oben Abb. 9.1 von einer konstanten Unterteilung aus, so gilt
Abb. 9.2 Ableitung FEM 1d affine Basisfunktionen#
Für das konkrete Beispiel erhalten wir für die Matrixkoeffizienten die Matrix \(A\):
array([[ 5., -5., 0., 0., 0., 0.],
[-5., 10., -5., 0., 0., 0.],
[ 0., -5., 10., -5., 0., 0.],
[ 0., 0., -5., 10., -5., 0.],
[ 0., 0., 0., -5., 10., -5.],
[ 0., 0., 0., 0., -5., 5.]])
Aufgabe
Berechne die Koeffizienten von Hand.
Analog zur Matrix \(A\) folgt für die rechte Seite mit konkreter Funktion \(f(x) = 1\):
array([0.1, 0.2, 0.2, 0.2, 0.2, 0.1])
Die FEM Lösung \(u(x)\) ist somit gegeben durch die Lösung des Gleichungssystems (9.5) mit der berechneten Matrix und Vektor. Da die Randwerte gegeben sind, werden nur die inneren Freiheitsgrade benutzt. Es folgt
u = np.zeros_like(xi)
# um die Wahl der linearen Gleichungslöser kümmern wir uns später:
u[1:-1] = np.linalg.solve(A[1:-1,1:-1],b[1:-1])
u
array([0. , 0.08, 0.12, 0.12, 0.08, 0. ])
Abb. 9.3 Lösung des Randwertproblem mit Hilfe 1. Ordnung FEM#
Aufgaben
Wie lautet das Gleichungssystem, wenn für die Integration der rechten Seite die Trapezregel benutzt wird?
Berechne die Lösung mit Hilfe der finiten Differenzen Methode und vergleiche die beiden Systeme.
NGSolve Lösung#
Das selbe nun mit NGSolve
Wir erstellen als erstes ein eindimensionales Mesh.
from netgen.meshing import Mesh as NGMesh # Vorsicht es gibt Mesh auch in ngsolve!
from netgen.meshing import MeshPoint, Pnt, Element1D, Element0D
from ngsolve import *
m = NGMesh(dim=1)
# Anzahl Teilintervalle
N = 5
# Punkte für die Zerlegung auf dem Intervall [0,1]
pnums = []
for i in range(0, N+1):
pnums.append (m.Add (MeshPoint (Pnt(i/N, 0, 0))))
# Jedes 1D-Element (Teilintervall) kann einem Material zugeordnet
# werden. In unserem Fall gibt es nur ein Material.
idx = m.AddRegion("material", dim=1)
for i in range(0,N):
m.Add (Element1D ([pnums[i],pnums[i+1]], index=idx))
# Linkes und Rechtes Ende sind Randwertpunkte (0D-Elemente)
idx_left = m.AddRegion("left", dim=0)
idx_right = m.AddRegion("right", dim=0)
m.Add (Element0D (pnums[0], index=idx_left))
m.Add (Element0D (pnums[N], index=idx_right))
# Damit haben wir das Mesh definiert
mesh = Mesh(m)
Nun erstellen wir einen \(H^1\) Funktionenraum mit Hilfe dieses 1D Mesh und den Dirichlet Randpunkte left und right.
V = H1(mesh,order = 1, dirichlet='left|right')
u = V.TrialFunction()
v = V.TestFunction()
# abgekürzt
# u,v = V.TnT()
\(u,v\) sind Trial und Test Funktionen für die Definition der Linear- und Bilinearfunktion
a = BilinearForm(V)
a += grad(u)*grad(v)*dx
f = CoefficientFunction(1)
b = LinearForm(V)
b += f*v*dx
Damit sind die beiden Operatoren definiert, jedoch noch nicht berechnet. Das Berechnen nennt man auch assembling Zusammenstellen. Wir werden später sehen, was damit gemeint ist.
a.Assemble()
b.Assemble();
Die Matrix und der Vektor der Bilinear- und Linearform beinhaltet sämtliche Freiheitsgrade, insbesondere also auch die Randpunkte. Diese sind jedoch durch die Dirichletrandwerte gegeben und müssen nicht berechnet werden. Dem werden wir beim Lösen des Systems rechnungtragen.
print(a.mat)
Row 0: 0: 5 1: -5
Row 1: 0: -5 1: 10 2: -5
Row 2: 1: -5 2: 10 3: -5
Row 3: 2: -5 3: 10 4: -5
Row 4: 3: -5 4: 10 5: -5
Row 5: 4: -5 5: 5
print(b.vec)
0.1
0.2
0.2
0.2
0.2
0.1
Die Lösung selber wird in einer GridFunction gespeichert. Die Trial und Test Functions haben zwar die gleiche Struktur, jedoch keinen Memory. Hier sind nur die Freiheitsgrade etc. des FE-Raumes gespeichert. Für die Lösung benötigen wir eine GridFunction. In der Auswertung dieser wird Linearkombination der Basisfunktionen automatisch berechnet.
gfu = GridFunction(V)
Berechnung der FEM Lösung mit NGSolve:
gfu.vec.data = a.mat.Inverse(freedofs=V.FreeDofs())*b.vec
Bei der Ausführung des Befehls wird nicht die Matrix Invertiert und von links an den Vektor multipliziert. Das wäre numerisch viel zu aufwändig. Auch wenn die Notation anderes behauptet, es wird nur das Gleichungssystem gelöst.
Es folgt das selbe Resultat wie oben:
9.1.2. Zweidimensionaler Fall#
Wir betrachten nun das Beispiel Randwertproblem
auf dem einheits Rechteck im \(\mathbb{R}^2\). Analog zum Einstiegsbeispiel Abschnitt 1.1.2 folgt die schwache Gleichung
wobei mit \(J\) die Basisfunktionen bezeichnet sind, deren Maximum im Innern des Einheitsquadrats angenommen werden.
from ngsolve import *
from ngsolve.webgui import Draw
Wir generieren ein unstrukturiertes Mesh mit der maximalen Kantenlänge von 0.25:
mesh = Mesh(unit_square.GenerateMesh(maxh=0.25))
Damit erhalten wir das Mesh, wobei die Feinheit über den maxh Parameter gesteuert werden kann.
Draw(mesh);
Wie sehen im zweidimensionalen die Basisfunktionen aus? Dazu erstellen wir einen FEM Funktionenraum und visualisieren die Basisfunktionen. Der Rand des Einheitsquadrats besteht aus 4 Linien, welche verschiedene Labels haben:
mesh.GetBoundaries()
('bottom', 'right', 'top', 'left')
Wir definieren den H1 FEM Funktionenraum mit der Dirichletrandbedingung und initialisieren eine GridFunction, eine FEM Funktion aus dem Funktionenraum:
V = H1(mesh,order=1,dirichlet='bottom|right|top|left')
gfu = GridFunction(V)
In der Gridfunction wird der Lösungsvektor, die Koeffizienten der Linearkombination der Basisfunktionen gespeichert. In diesen Vektor schreiben wir nun an unterschiedlichen Stellen eine 1, womit wir die verschiedenen Basisfunktionen visualisieren können.
gfu.vec.FV()[:] = 0 # alle Einträge mit 0 initialisieren
gfu.vec.FV()[20] = 1
Draw(gfu,mesh,'u');
Die inneren Freiheitsgrade sind in unserem Fall die freien Freiheitsgrade des FEM Raumes, daher in der Zahl 9 Stück:
freedofs = V.FreeDofs()
print (freedofs)
freedofsnp = np.array([i for i in freedofs])
0: 00000000000000001111111111
Wir setzen nun alle inneren Freiheitsgrade auf 1 und erhalten damit die Linearkombination der Basisfunktionen:
gfu.vec.FV().NumPy()[freedofsnp] = 1.
Draw(gfu);
Die Ableitungen der Basisfunktionen sind stückweise konstante Funktionen. Überschlagen wir hier kurz die Steigung: \(1/4\)-tel ist die Seitenlänge der Zerlegung. Das bedeutet, dass die Steigung in \(x/y\) Richtung 4 beträgt.
Visualisieren wir von \(\nabla u\) die \(x\)-Komponente, so sollte, das einen Graph mit stückweise \(\pm 4\) in den Elementen ergeben, welche in \(x\)-Richtung steigen bzw. fallen.
Draw(grad(gfu)[0],mesh);
und in \(y\) Richtung
Draw(grad(gfu)[1],mesh);
Mit Hilfe von ngsolve können wir nun die Bilinearfunktion \(A: V \times V \to \mathbb{R}\) und die Linearfunktion \(f: V \to \mathbb{R}\) sehr einfach berechnen.
Es folgt für
wobei hier \(u,v\) stellvertretend für \(\varphi_i, \varphi_j\in V\) steht. Für die Definition der Bilinearfunktion stehen in ngsolve sogenannte Proxy-Funktionen zur Verfügung. Wobei zwischen \(u\) und \(v\) unterschieden werden muss. Für \(u\) benutzen wir TrialFunctions und \(v\) TestFunctions.
u = V.TrialFunction()
v = V.TestFunction()
Damit können wir die Bilinearfunktion definieren, wobei in ngsolve das Volumenintegral \(dV\) mit dx bezeichnet wird. Für Oberflächenintegrale steht ds zur Verfügung.
A = BilinearForm(V)
A += grad(u) * grad(v)*dx
und analog für die Linearform
f = CoefficientFunction(1)
b = LinearForm(V)
b += f*v*dx
Die gegebene Funktion \(f(x)\) der rechten Seite können wir mit sogenannten CoefficientFunction definieren.
Damit haben wir die Bilinearform, welche im endlichdimensionalen mit Hilfe einer Matrix und die Linearform, welche mit einem Vektor beschrieben werden kann definiert, aber noch nicht berechnet. Die Berechnung derer nennt man auch Assembling.
A.Assemble()
b.Assemble();
Die Matrix \(A\) ist „sparse“ gespeichert. Das bedeutet, dass nur die Matrix Einträge gespeichert werden, für welche wir potentiell einen Eintrag erhalten. Für den ganzen Rest der Matrix wird der Speicher gar nicht allokiert. Grundsätzlich haben wir hier alle Freiheitsgrade:
print(A.mat)
Row 0: 0: 1 4: -0.5 15: -0.5
Row 1: 1: 0.86894 6: -0.334407 7: -0.340112 18: -0.194421
Row 2: 2: 1 9: -0.5 10: -0.5
Row 3: 3: 1 12: -0.5 13: -0.5
Row 4: 0: -0.5 4: 1.94059 5: -0.253013 15: -0.195933 16: -0.991648
Row 5: 4: -0.253013 5: 1.85565 6: -0.121477 16: -0.442635 17: -1.03853
Row 6: 1: -0.334407 5: -0.121477 6: 1.80282 17: -0.52515 18: -0.821783
Row 7: 1: -0.340112 7: 1.82984 8: -0.322038 18: -0.917253 19: -0.250436
Row 8: 7: -0.322038 8: 1.78086 9: -0.3125 19: -0.80891 20: -0.337408
Row 9: 2: -0.5 8: -0.3125 9: 1.94408 10: -0.122305 20: -1.00928
Row 10: 2: -0.5 9: -0.122305 10: 1.78246 11: -0.280447 20: -0.314625 21: -0.565078
Row 11: 10: -0.280447 11: 1.90245 12: -0.468205 21: -0.982932 22: -0.170861
Row 12: 3: -0.5 11: -0.468205 12: 1.88891 13: -0.155019 22: -0.76569
Row 13: 3: -0.5 12: -0.155019 13: 1.91895 14: -0.584678 22: -0.679256
Row 14: 13: -0.584678 14: 1.93727 15: -0.518278 22: -0.119626 23: -0.714689
Row 15: 0: -0.5 4: -0.195933 14: -0.518278 15: 1.82478 16: -0.300749 23: -0.309817
Row 16: 4: -0.991648 5: -0.442635 15: -0.300749 16: 3.70407 17: -0.493319 23: -0.944411 24: -0.531306
Row 17: 5: -1.03853 6: -0.52515 16: -0.493319 17: 3.74705 18: -0.76898 24: -0.921068
Row 18: 1: -0.194421 6: -0.821783 7: -0.917253 17: -0.76898 18: 3.70785 19: -0.780535 24: -0.224877
Row 19: 7: -0.250436 8: -0.80891 18: -0.780535 19: 3.57658 20: -0.687921 24: -0.665071 25: -0.383706
Row 20: 8: -0.337408 9: -1.00928 10: -0.314625 19: -0.687921 20: 3.66577 21: -0.81092 25: -0.505615
Row 21: 10: -0.565078 11: -0.982932 20: -0.81092 21: 3.74442 22: -0.57375 25: -0.811744
Row 22: 11: -0.170861 12: -0.76569 13: -0.679256 14: -0.119626 21: -0.57375 22: 3.55039 23: -0.745422 25: -0.495789
Row 23: 14: -0.714689 15: -0.309817 16: -0.944411 22: -0.745422 23: 3.62946 24: -0.418399 25: -0.496719
Row 24: 16: -0.531306 17: -0.921068 18: -0.224877 19: -0.665071 23: -0.418399 24: 3.63253 25: -0.871808
Row 25: 19: -0.383706 20: -0.505615 21: -0.811744 22: -0.495789 23: -0.496719 24: -0.871808 25: 3.56538
Wir können diese Matrix (zumindest solange sie klein ist!) auch als vollbesetzte „dense“ Matrix betrachten:
rows,cols,vals = A.mat.COO()
denseA = np.zeros((np.max(rows)+1,np.max(rows)+1))
k=0
for i,j in zip(rows,cols):
denseA[i,j] = vals[k]
k+=1
plt.spy(denseA)
plt.show()
Das Pattern der Matrixeinträge hängt von der Nummerierung der Knoten (aus dem Meshing) und damit der Freiheitsgrade für den FEM Ansatz erster Ordnung ab.
for e in mesh.edges:
line = np.array([mesh.vertices[v.nr].point for v in e.vertices])
plt.plot(line[:,0],line[:,1],c='gray',alpha=0.75)
for v in mesh.vertices:
plt.text(*v.point,v,color='red')
plt.gca().set_axis_off()
plt.gca().set_aspect(1)
plt.show()
ind = np.arange(freedofsnp.shape[0])[freedofsnp]
plt.spy(denseA[np.ix_(ind,ind)])
plt.show()
Lösen wir das System für die inneren Freiheitsgrade:
gfu.vec.data = A.mat.Inverse(freedofs=V.FreeDofs())*b.vec
Draw(gfu,mesh,'u');