Lösen ein stationäres Wärmeleitproblem in der Ebene mittels FEM. Dabei bedeutet “stationär”, dass wir nicht die zeitliche Entwicklung simulieren, sondern den nach hinreichend langer Wartezeit vom betrachteten physikalischen System eingenommenen Zustand. Sind Wärmezufluss und -abfluss in einem Körper über einen langen Zeitraum konstant, so wird sich eine stabile Temperaturverteilung im Körper einstellen. Dieser stabile Zustand soll simuliert werden.
12.1Modell¶
Sei die Querschnittsfläche eines Kühlkörpers (z.B. einer CPU). Am unteren Rand
erfolgt ein zeitlich konstanter Wärmeeintrag
Am restlichen Rand liegt die Umgebungstemperatur stets bei
(Luftstrom).
import matplotlib.pyplot as plt
import numpy as np
# Liste von Eckpunkten zur Beschreibung des polygonalen Gebiets
outline = [[-1, 0]]
for x in np.linspace(-1 + 0.1, 1 + 0.1, 5, endpoint=False):
outline.extend([[x, 0.3], [x, 1], [x + 0.2, 1], [x + 0.2, 0.3]])
outline.append([1, 0])
# Randbedingungen
g1 = lambda x, y: 20
g2 = lambda x, y: 100 * (1 - x ** 2)
# Plot der Gebiets
fig, ax = plt.subplots(figsize=(10, 5))
x, y = zip(*(outline + [outline[0]]))
ax.plot(x, y, '-k')
ax.axis('equal')
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.show()
Im Inneren des Kühlkörpers gilt
für die Temperatur .
12.2Schwache Formulierung¶
Die schwache Formulierung erhalten wir als Spezialfall der schwachen Formulierung der Poisson-Gleichung:
Finde , sodass
für alle gilt. Dabei ist
und ist eine Funktion, die auf gerade entspricht. Die gesuchte Lösung ist dann
12.3Gebietszerlegung¶
Für die Zerlegung des Gebiets in Dreieckselemente nutzen wir das Python-Modul pygmsh.
import pygmsh
# Zerlegung in Dreieckselemente
with pygmsh.geo.Geometry() as geom:
geom.add_polygon(outline, mesh_size=0.05)
mesh = geom.generate_mesh()
# Knotenkoordinaten als NumPy-Array
# axis 0: globale Knotennummer (0, 1,...)
# axis 1: Koordinate (0 (x), 1 (y))
nodes = mesh.points[:, :2]
# globale Knotennummern der Randkanten als NumPy-Array
# axis 0: Randkantennummer (0, 1,...)
# axis 1: lokale Knotennummer (0, 1)
boundary_lines = mesh.cells[0].data
# globale Knotennummern der Dreieckselemente als NumPy-Array
# (Elementzusammenhangstabelle)
# axis 0: Elementnummer (0, 1,...)
# axis 1: lokale Knotennummer (0, 1, 2)
triangles = mesh.cells[1].data
del mesh # nicht mehr benötigtZur späteren Verwendung trennen wir die von pygmsh generierten Knoten nach inneren Knoten, Knoten auf dem Dirichlet-Rand und Knoten auf dem Neumann-Rand.
# Knoten nach Lage auswählen (Rand, innen)
b_nodes = set.union(*[{int(n1), int(n2)} for n1, n2 in boundary_lines])
i_nodes = list(set(range(0, nodes.shape[0])) - b_nodes)
# Randknoten in Dirichlet und Neumann trennen
d_nodes = []
n_nodes = []
for n in b_nodes:
if -1 < nodes[n, 0] < 1 and nodes[n, 1] == 0:
n_nodes.append(n)
else:
d_nodes.append(n)
del b_nodes, boundary_lines # nicht mehr benötigtDie Gebietszerlegung sollte immer visuell geprüft werden. Insbesondere sollten die Dreiecke möglichst nah an der Form eines gleichseitigen Dreiecks sein.
# Plot der Gebietszerlegung
fig, ax = plt.subplots(figsize=(10, 5))
# Dreiecke
for n1, n2, n3 in triangles:
n1231 = [n1, n2, n3, n1]
ax.plot(nodes[n1231, 0], nodes[n1231, 1], '-k', lw=1)
# Knoten
ax.plot(nodes[i_nodes, 0], nodes[i_nodes, 1], 'og', label='innen')
ax.plot(nodes[d_nodes, 0], nodes[d_nodes, 1], 'or', label='Dirichlet')
ax.plot(nodes[n_nodes, 0], nodes[n_nodes, 1], 'ob', label='Neumann')
# Knotennummern
#for n, (x, y) in enumerate(nodes):
# ax.text(x + 0.02, y + 0.02, str(n))
ax.axis('equal')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend()
plt.show()
Für den Aufbau der Systemmatrix weiter unten sind die Nachbarschaftsbeziehungen zwischen Knoten sowie zwischen Knoten und Dreieckselementen von Bedeutung.
# alle Knoten außer Dirichlet-Rand
ni_nodes = n_nodes + i_nodes
# Dreiecke für jeden Knoten
# Listenindex ist globale Knotennummer
# Listeneinträge sind Mengen von Elementnummern
tris_of_nodes = [set() for _ in nodes]
for tri, (n1, n2, n3) in enumerate(triangles):
tris_of_nodes[n1].add(tri)
tris_of_nodes[n2].add(tri)
tris_of_nodes[n3].add(tri)
# benachbarte Knoten (kleine Knotennummer zuerst)
neighbors = set()
for node, tris_of_node in enumerate(tris_of_nodes):
for tri in tris_of_node:
for n in set(triangles[tri, :]) - {node}:
neighbors.add((node, n) if node < n else (n, node))12.4Systemmatrix und rechte Seite¶
Für die numerische Berechnung der in der schwachen Formulierung auftretenden Bereichsintegrale verwenden wir stückweise lineare Funktionen auf der Dreieckszerlegung. Für jeden Knoten wird also eine Basisfunktionen eingeführt, die am entsprechenden Knoten den Wert 1 annimmt und an allen anderen Knoten den Wert 0 hat.
12.4.1Dirichlet-Randbedingungen¶
Im kontinuierlichen Modell wird die Lösung zerlegt in die Summe , wobei auf dem Dirichlet-Rand Null ist und auf dem Dirichlet-Rand die Randbedingung erfüllt. Die Funktion ist abseits von frei wählbar. Sei also Null im Inneren des Gebiets und auf dem Neumann-Rand.
Im diskretisierten Zustand hat nur Nicht-Null-Koeffizienten for Basisfunktionen , die zu einem inneren Knoten oder zu einem Knoten auf dem Neumann-Rand gehören. Die Systemmatrix wird als weniger Zeilen bzw. Spalten haben als es Knoten (Basisfunktionen) gibt. Umgekehrt genügen für die diskretisierte Darstellung der Funktion die Basisfunktionen die zu einem Knoten auf dem Dirichlet-Rand gehören.
# Abbildung der Knotennummern (ohne Dirichlet-Rand) auf Matrixzeilen/-spalten
node2idx = {n: idx for idx, n in enumerate(ni_nodes)}Die Systemmatrix wird dünn besetzt sein, sodass wir die Einträge zunächst in Listen sammeln und dann später daraus mit SciPy eine dünn besetzte Matrix generieren.
# leere (dünn besetzte) Matrix, rechte Seite zunächst überall 0
Adata = []
Acols = []
Arows = []
b = np.zeros(len(ni_nodes), dtype=float)12.4.2Gradient der Basisfunktionen¶
Sei der Knoten zur globale Knotennummer . Der Gradient der zugehörigen Basisfunktion ist Null auf allen Dreieckselemente, die nicht an den Knoten grenzen. Auf an den Knoten angrenzenden Dreiecken ist der Gradient konstant.
Die Berechnung des Gradients von auf einem Dreieck mit Eckknoten kann leicht durch Transformation des Dreiecks auf das Referenzdreieck mit den Eckpunkten , , erfolgen:
Die Transformation der Ecken des Referenzdreiecks auf die Eckknoten ist durch
gegeben. Die zum betrachteten Dreieck und zur Basisfunktion passende Formfunktion ist (vgl. Section 11.4.3). Mit ihr gilt
Andererseits liefert die Kettenregel
(IDVID 1210), wobei
die Jacobi-Matrix zu ist. Somit
Analog folgt
12.4.3Bereichsintegrale¶
Für die Berechnung der Einträge der Systemmatrix und der rechten Seite benötigen wir die Bereichsintegrale
für alle Paare von Basisfunktionen und auch für .
Sind entsprechenden Knoten und nicht benachbart, so ist das Integral Null. Bei benachbarten Knoten ist der Integrand nur auf den beiden Dreiecken von Null verschieden, die sowohl als auch als Eckknoten haben. Bezeichnen wir diese beien Dreiecke mit und , so gilt
Auf und ist der Integrand jeweils konstant.
Sei der dritte Eckknoten im Dreieck und sei die Transformation des Referenzdreiecks auf das Dreieck (vgl. oben). Dann gilt
mit
und
Die symmetrische und positiv definite Matrix nimmt für alle und den gleichen Wert
mit Zahlen an. Auch die Gradienten der Formfunktionen hängen nicht von und ab:
Der Integrand ist also konstant und der Integrationsbereich (Referenzdreieck) hat den Flächeninhalt . Somit
Für erhält man völlig analog (zweimal statt mit )
Ist oder ein Knoten auf dem Dirichlet-Rand, so spielt der Wert keine Rolle in der Systemmatrix, taucht aber bei der Berechnung des Bereichsintegrals bzw. in der rechten Seite auf.
# Matrix A ohne Diagonale, Dirichlet-Anteil von b
for n1, n2 in neighbors:
# a(v_n1, v_n2) berechnen
int_gradgrad = 0
for tri in tris_of_nodes[n1] & tris_of_nodes[n2]: # höchstens 2 Läufe
n3 = next(iter(set(triangles[tri, :]) - {n1, n2})) # dritter Knoten des Dreiecks
J = np.vstack((nodes[n2, :] - nodes[n1, :], nodes[n3, :] - nodes[n1, :])).T
JJ = np.matmul(J.T, J)
detJJ = JJ[0, 0] * JJ[1, 1] - JJ[0, 1] ** 2
int_gradgrad += (JJ[0, 1] - JJ[1, 1]) / (2 * np.sqrt(detJJ))
# in Matrix A oder in rechte Seite schreiben?
n1d = n1 in d_nodes
n2d = n2 in d_nodes
if not n1d and not n2d:
i1 = node2idx[n1]
i2 = node2idx[n2]
Acols.append(i1)
Arows.append(i2)
Adata.append(int_gradgrad)
Acols.append(i2)
Arows.append(i1)
Adata.append(int_gradgrad)
elif n1d and not n2d:
i2 = node2idx[n2]
b[i2] += -g1(nodes[n1, 0], nodes[n1, 1]) * int_gradgrad
elif n2d and not n1d:
i1 = node2idx[n1]
b[i1] += -g1(nodes[n2, 0], nodes[n2, 1]) * int_gradgrad
else: # Dirichlet-Randkante
pass
# Diagonale von A
for n1 in ni_nodes:
i1 = node2idx[n1]
# a(v_n1, v_n1) berechnen
int_gradgrad = 0
for tri in tris_of_nodes[n1]:
n2, n3 = set(triangles[tri, :]) - {n1}
J = np.vstack((nodes[n2, :] - nodes[n1, :], nodes[n3, :] - nodes[n1, :])).T
JJ = np.matmul(J.T, J)
detJJ = JJ[0, 0] * JJ[1, 1] - JJ[0, 1] ** 2
int_gradgrad += (JJ[0, 0] + JJ[1, 1] - 2 * JJ[0, 1]) / (2 * np.sqrt(detJJ))
# in Matrix schreiben
Acols.append(i1)
Arows.append(i1)
Adata.append(int_gradgrad)12.4.4Neumann-Randbedingungen¶
In die rechte Seite müssen noch die Integrale
eingebracht werden, wobei die globalen Knotennummern der Knoten auf dem Neumann-Rand durchläuft.
Das Integrationsgebiet kann in einzelne Randkanten zerlegt werden, wobei für jede Kante die Endknoten entweder beide auf dem Neumann-Rand liegen oder ein Endknoten auf dem Neumann- und ein Endknoten auf dem Dirichlet-Rand liegt.
In das Integral über eine Randkante gehen nur zwei Basisfunktionen (zwei Neumann-Knoten) oder nur eine Basisfunktion (Dirichlet-Knoten und Neumann-Knoten) ein.
Seien und die Endknoten einer Randkante . Dann ist durch
eine Abbildung des Intervalls auf die Randkante gegeben. Damit erhalten wir
und
wobei wir
verwendet haben.
Diese Integrale können leicht mittels numerischer Integration berechnet werden.
from scipy.integrate import quad
# Neumann-Anteil von b
for n1, n2 in neighbors:
# mindestens ein Neumann-Randknoten
n1n = n1 in n_nodes
n2n = n2 in n_nodes
n1d = n1 in d_nodes
n2d = n2 in d_nodes
if not((n1n and n2n) or (n1n and n2d) or (n1d and n2n)):
continue # keine Neumann-Randkante
# Integral von g_2 * v_n1 und von g_2 * v_n2
P1 = nodes[n1, :]
P2 = nodes[n2, :]
distP1P2 = np.sqrt((P1[0] - P2[0]) ** 2 + (P1[1] - P2[1]) ** 2)
if n1n: # Basisfunktion bei D-Knoten trägt nicht zum Integral bei
i1 = node2idx[n1]
func = lambda t: g2(*(P1 - t * (P2 - P1))) * (1 - t)
b[i1] += distP1P2 * quad(func, 0, 1)[0]
if n2n:
i2 = node2idx[n2]
func = lambda t: g2(*(P1 - t * (P2 - P1))) * t
b[i2] += distP1P2 * quad(func, 0, 1)[0]12.5Lösen des Gleichungssystems¶
Das Gleichungssystem kann mit Standardverfahren für dünn besetzte Matrizen gelöst werden.
import scipy.sparse as ss
# Matrix A als SciPy-Objekt
A = ss.csr_array(
(Adata, (Arows, Acols)),
shape=(len(ni_nodes), len(ni_nodes))
)
# Gleichungssystem lösen
u0 = ss.linalg.spsolve(A, b)12.6Zusammensetzen der Lösung¶
Das Array u0 enthält nun die Funktionswerte der Lösungsfunktion an den inneren Knoten und an den Randknotes des Neumann-Randes. Auf dem Dirichlet-Rand ist die Lösung bekannt.
# Lösung zusammensetzen
u = np.zeros(len(nodes), dtype=float)
u[d_nodes] = g1(nodes[d_nodes, 0], nodes[d_nodes, 1])
u[ni_nodes] = u0Für die Darstellung der Lösung kann der Bereich zwischen den Knoten linear interpoliert werden. Dies entspricht der kontinuierlichen Darstellung der Lösung als Summe stückweise linearer Ansatzfunktionen.
# Plot der Lösung
fig, ax = plt.subplots(figsize=(10, 5))
# Lösung
contour_plot = ax.tricontourf(
nodes[:, 0], nodes[:, 1], triangles, u, cmap='jet', levels=100
)
fig.colorbar(contour_plot, ax=ax, orientation='vertical')
# Dreiecke
for n1, n2, n3 in triangles:
n1231 = [n1, n2, n3, n1]
ax.plot(nodes[n1231, 0], nodes[n1231, 1], '-w', lw=0.5)
ax.axis('equal')
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.show()
12.7Realitätsnähe¶
Die Modellierung mit Dirichlet-Randbedingungen ist wenig realitätnah. Um die Oberflächentemperatur konstant bei 20°C zu halten, müsste das Bauteil beispielsweise von einer 20°C warmen Luftströmung umströmt werden, die beliebig viel Wärme aufnehmen kann. Realistischer sind Robin-Randbedingungen, die die Wärmeabgabe proportional zur Differenz zwischen Oberflächen- und Umgebungstemperatur modellieren (vgl. Diffusionsgleichung).
Bei der schwachen Formulierung mit Robin-Randbedingungen erhält man sowohl in der Bilinearform als auch in der rechten seite zusätzlich ein Kurvenintegral über den Robinrand.