Betrachten das Lösen er Wellengleichung in einer Raumdimension mittels Finite-Differenzen-Verfahren genauer. Sei dazu das betrachtete Gebiet, sei das betrachtete Zeitintervall und sei die Ausbreitungsgeschwindigkeit der Wellen. Gesucht ist eine Funktion , die
erfüllt. Eine Anfangsbedingung sei durch
mit gegeben.
8.1Randbedingungen¶
Wir betrachten verschiedene Randbedingungen:
beidseitig feste Einspannung (Dirichlet),
beidseitig keine Übertragung von Vertikalkräften auf den Rand (Neumann),
linkes Ende ohne vertikale Kraftübertragung (Neumann), rechtes Ende frei.
Im ersten Fall soll also
gelten.
Im zweiten Fall sollen keine Vertikalkräfte auf den Rand übertragen werden. Stellt man sich die Funktion als Verlauf einer Gitarrensaite vor, soll die Zugkraft also nur horizontal auf die Befestigung am Rand wirken. In vertikale Richtung ist die Befestigung frei beweglich. Erhaltene aus diesen Überlegungen
Im dritten zu betrachtenden Fall soll die Welle am rechten Rand frei auslaufen können. Das Verhalten am Rand soll also so sein, als würde der betrachtete Bereich nach rechts beliebig groß sein, wir aber nur den Ausschnitt sehen. Zusammen mit der Neumann-Bedingung für den linken Rand erhält man
Dass diese Randbedingung für unsere Zweck die richtige ist, sieht man, indem man für eine beliebige Funktion einsetzt. Diese Wahl von löst die Wellengleichung einschließlich Randbedinung am rechten Rand und entspricht einer nach rechts laufenden Welle (IDVID 810). Nach rechts laufende Wellen können also ungehindert den betrachteten Bereich verlassen. Verzichtet man auf diese Randbedingung, so ist die Lösung der Wellengleichung nicht mehr eindeutig bestimmt.
8.2Diskretisierung¶
8.2.1Gitterpunkte¶
Setzen
und erhalten so Gitterpunkte mit
für und .
Gesucht sind die Werte
von an den Gitterpunkten.
Die Gitterwerte der Funktion in der Anfangsbedingung bezeichnen wir entsprechend mit
8.2.2Innere Punkte¶
Für Punkte im Inneren des betrachteten Gebietes soll die Wellengleichung gelten, wobei wir die zweiten Ableitungen durch entsprechende Differenzenquotienten ersetzen:
für und .
Umstellen nach liefert
Sind für ein festes die und die für alle bekannt, so erhält man aus dieser Formel die für . Wir können also Zeitschritt für Zeitschritt berechnen ohne ein Gleichungssystem lösen zu müssen.
Die Obergrenze für die Zeit spielt hier keine Rolle. Theoretisch kann die Iteration beliebig lang laufen.
8.2.3Anfangsbedingung¶
Wir haben
für .
Die Ableitungen ersetzen wir durch die zentrale Differenz
Im Vergleich zu einem einseitigen Differenzenquotient erscheint dies sinnvoller, da der Zeitbereich nur einen Ausschnitt aus einem größeren Zeitbereich darstellt, der beobachtete Prozess also auch schon vor läuft. Andererseits bringt diese Wahl das (scheinbare) Problem mit sich, dass wir nicht kennen. Aus der kontinuierlichen Anfangsbedingung wird nun die diskrete Anfangsbedingung
In Formel (11) für eingesetzt erhalten wir für den ersten Zeitschritt die Formel
Auflösen nach liefert nun
sodass die unbekannten Werte gar keine Rolle spielen.
8.2.4Randbedingungen¶
8.2.4.1Dirichlet¶
Die diskretisierten Dirichlet-Randbedingungen sind
8.2.4.2Neumann links¶
Für die Neumann-Randbedingung links verwenden wir analog zur Zeitableitung in der Anfangsbedingung die zentrale Differenz
welche zur Bedingung
führt. Betrachten wir nun (11) für und setzen dort diese Beziehung ein, so erhalten wir mit
eine Formel zur Berechnung des Randwertes , welche für funktioniert. Für kann mittels Einsetzen von (14) für und Auflösen nach daraus widerum
gewonnen werden.
8.2.4.3Neumann rechts¶
Analog zum linken Rand erhält man für den rechten Rand die Berechnungsvorschriften
für und
8.2.4.4Freier Rand rechts¶
Im Prinzip können die beiden Ableitungen in der Bedingung wie oben durch zentrale Differenzenquotienten ersetzt werden. Die auftretenden Funktionswerte außerhalb der Orts- und Zeitintervalle und können dann wieder eliminiert werden, was allerdings recht aufwendig ist. Der Einfacheit halber beschränken wir uns hier auf einseitige Differenzenquotienten (vorwärts in der Zeit, rückwärts im Ort):
Nullsetzen und Umstellen nach liefert
für .
8.2.4.5Zusammenfassung¶
Zur Berechnung aller bietet sich nun folgendes Vorgehen an:
Für die Werte verwenden.
Für und die aus der Anfangsbedingung abgeleitete Formel für innere Punkte verwenden.
Für und die Formeln aus den Randbedingungen nutzen.
Für :
Für die Formel für innere Punkte nutzen.
Für die Formeln aus den Randbedingungen nutzen.
8.3Kondition¶
Man kann zeigen, dass die Berechnung der Funktion auf dem Gitter gut konditioniert ist, wenn
gilt. Der Wert auf der linken Seite heißt auch Courant-Zahl.
8.4Konvergenz¶
Man kann zeigen, dass für
die auf dem Gitter (ohne Rundungsfehler) berechneten Werte für exakt sind. Werden und gleichmäßig verkleinert, so konvergiert die Lösung des diskretisierten Problems also gegen die exakte Lösung.
Ist die Courant-Zahl kleiner als 1, so treten im Laufe der Zeit immer größere Abweichungen zwischen diskretisierter und exakter Lösung auf.
8.5Implementierung¶
Die Implementierung kann entsprechend den Formeln oben erfolgen.
import numpy as np
import matplotlib.pyplot as plt
def wave1d(a, T, c, f, nx, nt, left='d', right='d'):
delta_x = a / nx
delta_t = T / nt
C = c * delta_t / delta_x
print(f'Courant-Zahl: {C:.2f}')
x = np.linspace(0, a, nx + 1)
u = np.empty((nt + 1, nx + 1), dtype=float)
# Anfangsbedingungen
u[0, :] = f(x)
u[1, 1:-1] = 0.5 * C ** 2 * (u[0, :-2] - 2 * u[0, 1:-1] + u[0, 2:]) + u[0, 1:-1]
if left == 'd':
u[1, 0] = 0
else:
u[1, 0] = 0.5 * C ** 2 * (2 * u[0, 1] - 2 * u[0, 0]) + u[0, 0]
if right == 'd':
u[1, -1] = 0
elif right == 'n':
u[1, -1] = 0.5 * C ** 2 * (2 * u[0, -2] - 2 * u[0, -1]) + u[0, -1]
else:
u[1, -1] = u[0, -1] - C * (u[0, -1] - u[0, -2])
# Zeitschritte
for j in range(1, nt):
# innere Punkte
u[j + 1, 1:-1] = C ** 2 * (u[j, :-2] - 2 * u[j, 1:-1] + u[j, 2:]) \
- (u[j - 1, 1:-1] - 2 * u[j, 1:-1])
# Randbedingungen
if left == 'd':
u[j + 1, 0] = 0
else:
u[j + 1, 0] = 0.5 * C ** 2 * (2 * u[j, 1] - 2 * u[j, 0]) \
- (u[j - 1, 0] - 2 * u[j, 0])
if right == 'd':
u[j + 1, -1] = 0
elif right == 'n':
u[j + 1, -1] = 0.5 * C ** 2 * (2 * u[j, -2] - 2 * u[j, -1]) \
- (u[j - 1, -1] - 2 * u[j, -1])
else:
u[j + 1, -1] = u[j, -1] - C * (u[j, -1] - u[j, -2])
return uDie einzelnen Zeitschritt können als Animation visualisiert werden.
import matplotlib.animation as anim
from IPython.display import HTML
def show_anim(u, a, T):
nt = u.shape[0] - 1
nx = u.shape[1] - 1
x = np.linspace(0, a, nx + 1)
# Bilsrate auf 25 fps begrenzen
fps_real = nt / T
fps_show = 25
skip_frames = int(np.floor(fps_real / fps_show))
n_frames = (nt + 1) // (skip_frames + 1)
# Figure/Axes erstellen
fig, ax = plt.subplots()
ax.set_xlim(0, a)
ax.set_ylim(-1.1, 1.1)
ax.set_xlabel('x')
ax.set_ylabel('u(x,t)')
ax.grid()
# Linienobjekt erstellen (wird während Animation verändert)
line = ax.plot(0, 0, '-b')[0]
# Update-Funktion
def update(frame):
line.set_data(x, u[(skip_frames + 1) * frame, :])
# Animation erstellen und anzeigen
fa = anim.FuncAnimation(fig, update, frames=n_frames, interval=1000/fps_show)
display(HTML(fa.to_jshtml()))
plt.close() # verhindert automatischen Aufruf von plt.show() durch Jupyter8.6Experimente¶
Verwenden folgende Parameter:
a = 2
T = 4
c = 1
nx = 100
nt = int(np.ceil(c * nx * T / a)) # Courant = 1 oder wenigstens <= 1
print(f'{nt} Zeitschritte')200 Zeitschritte
8.6.1Dirichlet-Randbedingungen¶
f = lambda x: np.sin(1/a * np.pi * x)
u = wave1d(a, T, c, f, nx, nt, 'd', 'd')
show_anim(u, a, T)Courant-Zahl: 1.00
8.6.2Neumann-Randbedingungen¶
Möchten wir Neumann-Randbedingungen verwenden, so sollte die Anfangsbedingung diese auch erfüllen:
f = lambda x: np.cos(2/a * np.pi * x)
u = wave1d(a, T, c, f, nx, nt, 'n', 'n')
show_anim(u, a, T)Courant-Zahl: 1.00
8.6.3Links Neumann-, rechts freie Randbedingung¶
f = lambda x: np.exp(-(x - 0.5 * a) ** 2 / 0.05)
u = wave1d(a, T, c, f, nx, nt, 'n', 'f')
show_anim(u, a, T)Courant-Zahl: 1.00
8.6.4Links Dirichlet-, rechts Neumann-Randbedingung¶
Verlängern den Beobachtungszeitraum um eine volle Periode des Schwingungsvorgangs zu sehen:
T = 8
nt = int(np.ceil(c * nx * T / a)) # Courant = 1 oder wenigstens <= 1
print(f'{nt} Zeitschritte')
u = wave1d(a, T, c, f, nx, nt, 'd', 'n')
show_anim(u, a, T)400 Zeitschritte
Courant-Zahl: 1.00
8.6.5Courant-Zahl zu groß¶
T = 4
nt = int(np.ceil(0.99 * c * nx * T / a)) # Courant > 1
print(f'{nt} Zeitschritte')
f = lambda x: np.sin(1/a * np.pi * x)
u = wave1d(a, T, c, f, nx, nt, 'd', 'd')
show_anim(u, a, T)198 Zeitschritte
Courant-Zahl: 1.01
Obwohl die Courant-Zahl hier nur ganz wenig über der Eins liegt, wird die Berechnung schlecht konditioniert, sodass die unmeidbaren Rundungsfehler innerhalb weniger Rechenschritte extrem verstärkt werden und die numerische Lösung nichts mehr mit der exakten Lösungen des diskretisierten Problems zu tun hat.