(CFD) wird oft als Black Field komplexer kommerzieller Software program angesehen. Die Implementierung eines Lösers „von Grund auf“ ist jedoch eine der wirkungsvollsten Möglichkeiten, die Physik der Flüssigkeitsbewegung zu erlernen. Ich habe damit als persönliches Projekt begonnen und es als Teil eines Kurses über Biophysik zum Anlass genommen, endlich zu verstehen, wie diese schönen Simulationen funktionieren.

Dieser Leitfaden richtet sich an Datenwissenschaftler und Ingenieure, die über Excessive-Degree-Bibliotheken hinausgehen und die zugrunde liegende Mechanik numerischer Simulationen verstehen möchten, indem sie partielle Differentialgleichungen in diskretisierten Python-Code übersetzen. Wir werden auch grundlegende Programmierkonzepte wie vektorisierte Operationen mit NumPy und stochastische Konvergenz untersuchen, die wesentliche Fähigkeiten für jeden sind, der sich für umfassendere wissenschaftliche Pc- und maschinelle Lernarchitekturen interessiert.

Wir werden die Ableitung und Python-Implementierung eines einfachen inkompressiblen Navier-Stokes-Lösers (NS) durchgehen. Und dann werden wir diesen Solver anwenden, um den Luftstrom um das Flügelprofil eines Vogels zu simulieren.

Die Physik: Inkompressible Navier-Stokes

Die grundlegenden Gleichungen der CFD sind die Navier-Stokes-Gleichungen, die beschreiben, wie sich Geschwindigkeit und Druck in einer Flüssigkeit entwickeln. Für einen gleichmäßigen Flug (wie ein gleitender Vogel) gehen wir davon aus, dass die Luft inkompressibel (konstante Dichte) und laminar ist. Sie können nur als Newtons Bewegungsgesetz verstanden werden, allerdings für ein verschwindend kleines Factor einer Flüssigkeit mit den Kräften, die darauf einwirken. Dabei handelt es sich hauptsächlich um Druck und Viskosität, aber je nach Kontext können Sie auch Schwerkraft, mechanische Spannungen oder sogar Elektromagnetismus hinzufügen, wenn Sie Lust dazu haben. Der Autor kann bestätigen, dass dies für ein erstes Projekt absolut nicht zu empfehlen ist.

Die Gleichungen in Vektorform lauten:

(
frac{partial mathbf{v}}{partial t} + (mathbf{v} cdot nabla)mathbf{v} = -frac{1}{rho}nabla p + nu nabla^2 mathbf{v}
nablacdotmathbf{v}=0
)

Wo:

  • v: Geschwindigkeitsfeld (u,v)
  • P: Druck
  • ρ: Flüssigkeitsdichte
  • ν: Kinematische Viskosität

Die erste Gleichung (Impuls) gleicht Trägheit gegen Druckgradienten und viskose Diffusion aus. Die zweite Gleichung (Kontinuität) erzwingt, dass die Flüssigkeitsdichte konstant bleibt.

Das Druckkupplungsproblem

Eine große Herausforderung bei CFD besteht darin, dass Druck und Geschwindigkeit gekoppelt sind: Das Druckfeld muss sich ständig anpassen, um sicherzustellen, dass die Flüssigkeit inkompressibel bleibt.

Um dies zu lösen, leiten wir a ab Druck-Poisson-Gleichung indem man die Divergenz der Impulsgleichung nimmt. In einem diskretisierten Löser lösen wir diese Poisson-Gleichung in jedem einzelnen Zeitschritt, um den Druck zu aktualisieren und sicherzustellen, dass das Geschwindigkeitsfeld divergenzfrei bleibt.

Diskretisierung: Von der Mathematik zum Raster

Um diese Gleichungen auf einem Pc zu lösen, verwenden wir Endliche Differenz Schemata auf einem einheitlichen Raster.

  • Zeit: Vorwärtsdifferenz (Explizite Euler).
  • Advektion (nichtlineare Terme): Rückwärts-/Aufwind-Unterschied (für Stabilität).
  • Diffusion und Druck: Zentraler Unterschied.

Beispielsweise sieht die Aktualisierungsformel für die u-Komponente (x-Geschwindigkeit) in Finite-Differenzen-Type so aus

(
u_{i,j}^n frac{u_{i,j}^n – u_{i-1,j}^n}{Delta x}
)

Im Code verwendet der Advektionsterm u∂x∂u​ eine Rückwärtsdifferenz:

(
u_{i,j}^n frac{u_{i,j}^n – u_{i-1,j}^n}{Delta x}
)

Die Python-Implementierung

Die Implementierung erfolgt in vier verschiedenen Schritten unter Verwendung von NumPy-Arrays.

1. Initialisierung

Wir definieren die Rastergröße (nx, ny), Zeitschritt (dt) und physikalische Parameter (rho, nu). Wir initialisieren Geschwindigkeitsfelder (u,v) und Druck (p) auf Nullen oder einen gleichmäßigen Fluss.

2. Die Flügelgeometrie (eingetauchte Grenze)

Um einen Flügel auf einem kartesischen Gitter zu simulieren, müssen wir markieren, welche Gitterpunkte liegen innen der feste Flügel.

  • Wir laden ein Flügelnetz (z. B. aus einer STL-Datei).
  • Wir erstellen ein boolesches Maskenarray wo True bezeichnet einen Punkt innerhalb des Flügels.
  • Während der Simulation erzwingen wir, dass die Geschwindigkeit an diesen maskierten Punkten auf Null geht (kein Schlupf/keine Durchdringung).

3. Die Hauptlöseschleife

Die Kernschleife wiederholt sich, bis die Lösung einen stationären Zustand erreicht. Die Schritte sind:

  1. Erstellen Sie den Quellterm (b): Berechnen Sie die Divergenz der Geschwindigkeitsterme.
  2. Druck lösen: Lösen Sie die Poisson-Gleichung nach p mithilfe der Jacobi-Iteration.
  3. Geschwindigkeit aktualisieren: Nutzen Sie den neuen Druck, um u und v zu aktualisieren.
  4. Randbedingungen anwenden: Erzwingen Sie die Einlassgeschwindigkeit und Nullgeschwindigkeiten innerhalb des Flügels.

Der Kodex

So sehen die wichtigsten mathematischen Aktualisierungen in Python aus (aus Leistungsgründen vektorisiert).

Schritt A: Aufbau des Druckquellenterms Dies stellt die rechte Seite (RHS) der Poisson-Gleichung dar, die auf aktuellen Geschwindigkeiten basiert.

# b is the supply time period
# u and v are present velocity arrays
b(1:-1, 1:-1) = (rho * (
    1 / dt * ((u(1:-1, 2:) - u(1:-1, 0:-2)) / (2 * dx) +
              (v(2:, 1:-1) - v(0:-2, 1:-1)) / (2 * dy)) -
    ((u(1:-1, 2:) - u(1:-1, 0:-2)) / (2 * dx))**2 -
    2 * ((u(2:, 1:-1) - u(0:-2, 1:-1)) / (2 * dy) *
         (v(1:-1, 2:) - v(1:-1, 0:-2)) / (2 * dx)) -
    ((v(2:, 1:-1) - v(0:-2, 1:-1)) / (2 * dy))**2
))

Schritt B: Auflösen nach Druck (Jacobi-Iteration) Wir iterieren, um das Druckfeld zu glätten, bis es den Quellterm ausgleicht.

for _ in vary(nit):
    pn = p.copy()
    p(1:-1, 1:-1) = (
        (pn(1:-1, 2:) + pn(1:-1, 0:-2)) * dy**2 +
        (pn(2:, 1:-1) + pn(0:-2, 1:-1)) * dx**2 -
        b(1:-1, 1:-1) * dx**2 * dy**2
    ) / (2 * (dx**2 + dy**2))
# Boundary circumstances: p=0 at edges (gauge strain)
    p(:, -1) = 0; p(:, 0) = 0; p(-1, :) = 0; p(0, :) = 0

Schritt C: Geschwindigkeit aktualisieren Schließlich aktualisieren wir die Geschwindigkeit mithilfe der expliziten diskretisierten Impulsgleichungen.

un = u.copy()
vn = v.copy() 
# Replace u (x-velocity)
u(1:-1, 1:-1) = (un(1:-1, 1:-1) -
                 un(1:-1, 1:-1) * dt / dx * (un(1:-1, 1:-1) - un(1:-1, 0:-2)) -
                 vn(1:-1, 1:-1) * dt / dy * (un(1:-1, 1:-1) - un(0:-2, 1:-1)) -
                 dt / (2 * rho * dx) * (p(1:-1, 2:) - p(1:-1, 0:-2)) +
                 nu * (dt / dx**2 * (un(1:-1, 2:) - 2 * un(1:-1, 1:-1) + un(1:-1, 0:-2)) +
                       dt / dy**2 * (un(2:, 1:-1) - 2 * un(1:-1, 1:-1) + un(0:-2, 1:-1))))
# Replace v (y-velocity)
v(1:-1, 1:-1) = (vn(1:-1, 1:-1) -
                 un(1:-1, 1:-1) * dt / dx * (vn(1:-1, 1:-1) - vn(1:-1, 0:-2)) -
                 vn(1:-1, 1:-1) * dt / dy * (vn(1:-1, 1:-1) - vn(0:-2, 1:-1)) -
                 dt / (2 * rho * dy) * (p(2:, 1:-1) - p(0:-2, 1:-1)) +
                 nu * (dt / dx**2 * (vn(1:-1, 2:) - 2 * vn(1:-1, 1:-1) + vn(1:-1, 0:-2)) +
                       dt / dy**2 * (vn(2:, 1:-1) - 2 * vn(1:-1, 1:-1) + vn(0:-2, 1:-1))))

Ergebnisse: Fliegt es?

Wir haben diesen Löser auf einem starren Flügelprofil mit konstantem Fernfeldeinfluss ausgeführt.

Qualitative Beobachtungen Die Ergebnisse stimmen mit den körperlichen Erwartungen überein. Die Simulationen zeigen einen hohen Druck unter dem Flügel und einen niedrigen Druck darüber, was genau der Mechanismus ist, der den Auftrieb erzeugt. Geschwindigkeitsvektoren zeigen die Beschleunigung des Luftstroms über der Oberfläche (Bernoulli-Prinzip).

Kräfte: Auftrieb vs. Widerstand Durch die Integration des Druckfeldes über der Flügeloberfläche können wir den Auftrieb berechnen.

  • Der Löser zeigt das Druckkräfte dominieren viskose Reibungskräfte in Luft um den Faktor quick 1000.
  • Mit zunehmendem Anstellwinkel (von 0∘ auf −20∘) steigt das Verhältnis von Auftrieb zu Luftwiderstand, was den Developments entspricht, die in Windkanälen und professionellen CFD-Paketen wie OpenFOAM beobachtet werden.

Einschränkungen und nächste Schritte

Obwohl sich die Erstellung dieses Lösers hervorragend zum Lernen eignete, hat das Software selbst seine Grenzen:

  • Auflösung: 3D-Simulationen auf einem kartesischen Gitter sind rechenintensiv und erfordern grobe Gitter, wodurch quantitative Ergebnisse weniger zuverlässig sind.
  • Turbulenz: Der Löser ist laminar; Es fehlt ein Turbulenzmodell (wie ok−ϵ), das für schnelle oder komplexe Strömungen erforderlich ist.
  • Diffusion: Upwind-Differenzierungsschemata sind stabil, aber numerisch diffus, wodurch möglicherweise feine Strömungsdetails „verwischt“ werden.

Wohin geht es von hier aus? Dieses Projekt dient als Ausgangspunkt. Zukünftige Verbesserungen könnten die Implementierung von Advektionsschemata höherer Ordnung (wie WENO), das Hinzufügen von Turbulenzmodellierung oder die Umstellung auf Finite-Volumen-Methoden (wie OpenFOAM) für eine bessere Netzhandhabung um komplexe Geometrien herum umfassen. Es gibt viele clevere Techniken, um die Fülle an Szenarien zu umgehen, die Sie möglicherweise implementieren möchten. Dies ist nur ein erster Schritt auf dem Weg zum wirklichen Verständnis von CFD!

Von admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert