Das Mehrkörperproblem in der Astronomie/ Hierarchische Algorithmen/ Räumliche Hierarchie
Idee
BearbeitenUm die in einem Mehrkörpersystem herrschenden Kräfte exakt zu bestimmen, muss man wie schon mehrfach erwähnt sämtliche Massenpaare individuell betrachten, wodurch der Rechenaufwand quadratisch mit der Anzahl der Mitglieder wächst. Dieser rapide Anstieg wird jedoch vermieden, wenn man stattdessen nur die in der näheren Umgebung eines Körpers sich befindlichen Nachbarn exakt behandelt, weiter entfernte Massenpunkte hingegen zu Gruppen zusammenfasst und anstelle der Einzelobjekte nur noch die Schwerpunkte dieser Cluster zur Kräfteberechnung heranzieht. Je weiter eine solche Gruppe entfernt ist, umso größer darf deren räumliche Ausdehnung sein, ohne die von ihr ausgehende Gravitation zu ungenau abzuschätzen. Auf Grundlage dieser Idee lässt sich ein Algorithmus entwickeln, dessen Rechenaufwand in der gewünschten Größenordnung liegt.
Um Gruppen von Massenpunkten zu definieren, die je nach Entfernung unterschiedlich groß gehalten werden können, wird gemäß dem Verfahren von Barnes und Hut (1986) [1] das Ensemble in Würfel (Oktanten) verschiedener Kantenlänge eingeteilt, wobei nun unterschiedliche Raumebenen darstellt. Im Gegensatz zur Konstruktion der Zeithierarchie startet man jetzt nicht auf der niedrigsten, sondern der höchsten Ebene (welche nun aber umgekehrt durch gekennzeichnet ist und nicht durch den höchsten vorkommenden Wert von ). Um das System auf der obersten Ebene mit 8 Würfeln der Kantenlänge sicher zu überdecken, muss sich nach dem Abstand des fernsten Körpers vom Gesamtschwerpunkt des Ensembles richten.
Für jeden dieser Würfel wird die Anzahl der darin sich befindlichen Objekte ermittelt. Ist mehr als ein solches in einem Oktanten vorhanden, wird dieser in 8 kleinere Würfel der Größe unterteilt, es tritt die Raumebene hinzu. Im nächsten Schritt werden diese kleineren Kuben überprüft. Findet sich in einem solchen trotz der kleineren Kantenlänge erneut mehr als ein Massenpunkt, erfolgt für diesen eine neuerliche Aufteilung in noch kleinere Würfel mit einer Ausdehnung von nunmehr entsprechend der Ebene . Dieses Verfahren wird solange fortgesetzt, bis in jedem Oktanten nur noch ein Körper anzutreffen ist.
Rücken im Verlauf einer Simulation Massenpunkte eng zusammen, so werden dort u.U. sehr kleine Würfel benötigt, um auf ein Objekt pro solchem zu kommen, d.h. eventuell müssen gleich mehrere Raumebenen angelegt werden. Entfernen sie sich wieder voneinander, sind solch kleine Oktanten und entsprechend tiefe Ebenen nicht mehr erforderlich.
Zwischen Raum- und Zeithierarchie besteht ein fundamentaler Unterschied. Bei letzterer genügt es, die Zeitebenen und die dazugehörigen dynamischen Zeitschritte der einzelnen Körper zu kennen. Hingegen ist für die räumliche Hierarchie die Kenntnis allein der Ebenen und der Dazugehörigkeiten der Massenpunkte zu den entsprechenden Würfeln unzureichend. Wie nachfolgend diskutiert wird, müssen für jeden belegten Kubus auch die Positionen und sofern die Simulation auf dem Hermite-Polynome-Verfahren beruht, auch die Geschwindigkeiten ihrer Schwerpunkte bekannt sein, welche sich mit jedem Simulationsschritt ändern.
Nach jeder Ausführung des Prädiktors muss daher die Raumhierarchie aktualisiert werden. Zunächst überprüft man, ob durch den Prädiktor zumindest ein Massenpunkt bezogen auf die niedrigste Raumebene in einen anderen Würfel überwechselt. Ist das nicht der Fall, können die momentan definierten Oktanten beibehalten werden, nur die Positionen und gegebenenfalls die Geschwindigkeiten ihrer Schwerpunkte müssen neu berechnet werden. Ansonsten muss das hierarchische Gitter selbst neu aufgebaut werden.
Prozeduren im Würfelgitter
BearbeitenEinordnung der Massenpunkte
BearbeitenDie Einstufung der Massenpunkte in die Raumhierarchie ist viel anspruchsvoller als deren Zuordnung zur Zeithierarchie. Bei letzterer genügt es, für jeden Körper nur die Zeitebene vorzuhalten, auf welcher er sich tatsächlich befindet. Im Falle der Raumhierarchie jedoch muss für jedes Objekt nicht nur die Lage bezüglich der tatsächlichen, sondern auch aller darüber liegenden Ebenen bekannt sein. Befindet sich nämlich ein Körper nahe an einem Massenpunkt, dessen Beschleunigung bestimmt werden soll, muss jener als Einzelobjekt oder bestenfalls als zu einer Gruppe geringer Ausdehnung (die schon mit einem kleinen Würfel überdeckt werden kann) zugehörig betrachtet werden. Ist der gleiche Körper weit von einem anderen Massenpunkt entfernt, darf er von diesem aus gesehen als Mitglied eines weiträumigen Clusters (der nur von einem großen Oktanten abgedeckt wird) aufgefasst werden. Betrachtet man in obiger Abbildung z.B. den Massenpunkt oben rechts, so ist dessen nächster Nachbar als Einzelobjekt zu behandeln. Aus der Sicht des Massenpunktes unten links dürfen die beiden Körper oben rechts hingegen zu einer Gruppe zusammengefasst werden. Je nach Perspektive ist ein Objekt somit unterschiedlichen Raumebenen zugehörig.
Die Lage eines Massenpunktes in der Raumhierarchie kann durch ganzzahlige kartesische Koordinaten angegeben werden. Auf der höchsten Ebene sind nur 2*2*2 Würfel vorhanden, die möglichen Werte pro Koordinate also 0 oder 1. Auf der nächsten Ebene existieren 4*4*4 Kuben, so dass sich der mögliche Wertebereich von 0 bis 3 erstreckt. Allgemein weist eine Ebene Oktanten auf, so dass jede Koordinate Werte von 0 bis annehmen kann. In obigem zweidimensionalen Beispiel hat der Massenpunkt rechts oben auf der Ebene 0 die Position (1,1), auf der Ebene 1 die Lage (3,2) und auf seiner niedrigsten Ebene 2 die Koordinaten (7,5).
Die Einordnung der Massenpunkte in das Würfelgitter beruht auf folgenden Algorithmus, für welchen zunächst nur die Struktur erläutert wird. Der dazugehörige C-Code wird im letzten Abschnitt dieses Unterkapitels angegeben.
Start auf höchster Ebene e = 0
while (zumindest zwei Massenpunkte im gleichen Würfel)
{
Berechnung der Positionen aller Körper i im Würfelgitter bezüglich der aktuell betrachteten Ebene e
for (i = 0;i < N;i ++)
{
if (Ebene des Körpers i = aktuell betrachtete Ebene e)
{
Neuer Würfel für Körper i
for (j = i + 1;j < N;j ++)
{
if (Körper j im gleichen Würfel wie Körper i)
{
Aufnahme des Körpers j in den Würfel von Körper i
Verschieben beider Körper i und j auf die nächsttiefere Ebene
}
}
}
}
if (zumindest ein Paar i,j mit gemeinsamem Würfel auf aktuell betrachteter Ebene gefunden)
Analyse auf nächsttieferer Ebene
e++
}
Zu Beginn auf der obersten Ebene, wird für den ersten Massenpunkt sogleich ein Würfel der Kantenlänge angelegt. Die Positionen aller anderen Objekte auf der aktuell untersuchten Ebene werden mit derjenigen des Körpers verglichen. Bei gleicher Position (d.h. Zugehörigkeit zum gleichen Oktanten wie Massenpunkt ) wird zum Kubus von hinzugefügt, gleichzeitig beide Körper und der nächsttieferen Ebene zugeordnet. Solche Objekte werden von der laufenden -Schleife nicht mehr bearbeitet, da diese nur Massenpunkte berücksichtigt, welche der aktuell analysierten Ebene angehören. Dies bedeutet aber, dass nicht alle möglichen Paare geprüft werden müssen, um die Mitglieder des Ensembles in die Raumhierarchie einzuordnen.
Bleibt ein Körper in seinem Würfel allein, wird er nicht auf die nächsttiefere Ebene geschoben. Geht der Algorithmus zur nächsten Ebene über, so wird ein solches Einzelobjekt nicht mehr von der -Schleife beachtet, da es im Vergleich zur aktuellen Ebene um eine Stufe zurückgeblieben ist. Hingegen erfasst der Algorithmus nun solche Massenpunkte, welche auf der Ebene zuvor von der -Schleife um eine Stufe nach unten versetzt wurden.
Auf jeder Ebene werden für alle belegten Würfel laufende Nummer, Masse, die Position und eventuell auch die Geschwindigkeit des Schwerpunkts festgehalten, zudem auch die laufende Nummer des jeweils übergeordneten Würfels auf der nächsthöheren Ebene. Um die Schwerpunkte der Oktanten schnell neu berechnen zu können, wird für jeden auch eine Liste mit den Nummern der enthaltenen Massenpunkte angelegt.
Bestimmung von Beschleunigung und Ruck
BearbeitenUm die auf einen Körper ausgeübte Beschleunigung und für das Hermite-Polynome-Verfahren auch den Ruck zu ermitteln, werden jetzt nur die Massenpunkte in dessen unmittelbarer Umgebung als Einzelobjekte betrachtet. Von einer gewissen Entfernung an werden alle in einem Würfel sich befindlichen Objekte kollektiv behandelt, indem die Gesamtmasse des Oktanten sowie die Position und gegebenenfalls auch die Geschwindigkeit dessen Schwerpunkts zur Berechnung herangezogen werden anstatt die entsprechenden Größen der individuellen Mitglieder. Diese Näherung ist zulässig, sofern die Kantenlänge eines solchen Würfels nicht zu groß ist im Vergleich zu seiner Entfernung von dem soeben untersuchten Körper, d.h. das Verhältnis zwischen den beiden Längenskalen einen gewissen Wert nicht überschreitet:
Je kleiner dieser Schwellwert gesetzt wird, umso genauer werden die Kräfte zwischen den Mitgliedern eines Mehrkörpersystems bestimmt, umso größer ist jedoch auch der Rechenaufwand. Der hierarchische Algorithmus greift dann erst auf recht tiefer Ebene, da ein im Vergleich zur Kantenlänge eines Kubus erheblicher Abstand desselben verlangt wird. Als Folge davon werden weiterhin viele in der Umgebung eines Körpers sich befindliche Massenpunkte individuell betrachtet, und selbst zu größeren Entfernungen hin zumeist nur kleine Würfel, die lediglich wenige Objekte zu einer Gruppe zusammenfassen. Mit = 0 wird auf eine Gruppenbildung komplett verzichtet.
Umgekehrt bedeutet ein großer Schwellwert, dass nur noch wenige Massenpunkte in der unmittelbaren Umgebung eines Körpers einzeln behandelt werden. Mit zunehmendem Abstand geht das Verfahren zügig zu Würfeln großer Kantenlänge über, die viele Objekte enthalten können. Die nun sehr effiziente Gruppenbildung und damit kürzere Rechenzeit ist jedoch mit einem größeren Fehler bei der Berechnung der Kräfte im Ensemble erkauft.
Gemäß Barnes und Hut (1986) [1] sollte maximal auf 1 gesetzt werden. Den Autoren zufolge werden dann die auf die einzelnen Mitglieder eines Mehrkörpersystems einwirkenden Kräfte mit einer relativen Genauigkeit von 1% bestimmt.
Das Vorgehen bei der Berechnung der auf einen Körper einwirkenden Beschleunigung und des Rucks wird wie der Algorithmus zur Einordnung der Massenpunkte in die räumliche Hierarchie zunächst rein qualitativ vorgestellt.
for (e = 0;e < Anzahl der Ebenen;e ++)
{
for (o = 0;o < Anzahl der belegten Oktanten auf Ebene e;o ++)
{
if (übergeordneter Oktant bereits abgearbeitet)
dann auch aktueller Oktant abgearbeitet
else
{
Betrachte Abstand r zwischen aktuellem Oktant und Körper i
if (Kantenlänge (a / 2^e) / r < alpha oder Oktant enthält nur 1 Massenpunkt)
{
Bestimme Beschleunigung und Ruck, welche der aktuelle Oktant auf Körper i ausübt
aktueller Oktant abgearbeitet
}
}
}
}
Aus der Sicht eines Körpers werden beginnend mit der höchsten Ebene die gesamte Hierarchie und innerhalb jeder Ebene alle belegten Würfel abgearbeitet. Zuerst wird für den gerade betrachteten Oktanten geprüft, ob der darüber liegende bereits in die Berechnung eingegangen ist. In diesem Fall kann der aktuelle Kubus einfach übergangen und sogleich als abgearbeitet gekennzeichnet werden. Ansonsten wird der Abstand zwischen dessen Schwerpunkt und dem Körper ermittelt. Ist im Vergleich zur Kantenlänge des Würfels groß genug, so werden alle darin vorhandenen Massenpunkte wie soeben beschrieben als Kollektiv behandelt und dieser wiederum als abgearbeitet markiert. Kann der Oktant wegen einer im Vergleich zu zu großen Kantenlänge nicht abgearbeitet werden, erfasst der Algorithmus auf der nächsttieferen Ebene dessen Teilwürfel. Enthält ein Kubus nur 1 Massenpunkt, kann er natürlich unabhängig von seiner Größe und Entfernung berücksichtigt werden. Auf diese Weise werden auch diejenigen Objekte einbezogen, welche sich nicht zu Gruppen zusammenfassen lassen, weil sie sich entweder in der unmittelbaren Umgebung des Körpers oder in Bereichen geringer Dichte aufhalten.
Die Aussagen von Barnes und Hut (1986) [1] über die Genauigkeit ihres Verfahrens gelten nur im Mittel. Im Einzelfall hängt der durch die Gruppenbildung bewirkte Fehler stark von der Verteilung der Massenpunkte in einem Würfel ab. Als Beispiele seien die in folgender Skizze dargestellten Fälle diskutiert, in denen jeweils drei Massenpunkte der Masse durch ihren Schwerpunkt ersetzt werden.
Im ersten Fall stehen diese hintereinander und üben die Beschleunigungen , und auf. Mit lauten diese , und . Ersetzt man die Körper durch ihren Schwerpunkt, so ergibt sich die genäherte Beschleunigung . Vergleicht man den exakten Wert mit der Näherung , so gilt folgendes Verhältnis:
Im zweiten Fall sind die Massenpunkte nebeneinander postiert. Die Beschleunigungen sind nun , und wiederum bzw. , und nochmals . Der Vergleich mit der Näherung liefert nun:
Stehen die Massenpunkte hintereinander, so wird durch eine Gruppenbildung die von diesen ausgeübte Beschleunigung mit einer im Vergleich zur Entfernung zunehmenden Größe des Würfels weit unterschätzt. Mit = 0.5 liegt der relative Fehler bei knapp 20%, mit = 1 aber schon bei etwa 80%. Die Ursache für diese erhebliche Abweichung liegt in der -Abhängigkeit der Gravitation. Liegt der Schwerpunkt eines Oktanten weiter entfernt als ein individueller Massenpunkt, so wird mit der Gruppenbildung die vom einzelnen Körper ausgehende Kraft unterschätzt. Liegt der Schwerpunkt näher als ein Einzelobjekt, so verhält es sich umgekehrt. Die -Abhängigkeit sorgt jedoch dafür, dass der Fehler im ersten Fall deutlich schwerer wiegt als im letzteren.
Liegen die Massenpunkte nebeneinander, so verhält sich das Verfahren wesentlich stabiler. Mit = 0.5 und 1 finden sich nun relative Fehler von einigen % und etwas mehr als 10%. Mit einer Gruppenbildung wird die ausgeübte Beschleunigung leicht überschätzt, da die Entfernung zum Schwerpunkt etwas geringer ist als zu den Mittelpunkten der seitlichen Flächen des Würfels.
Gemäß den hier erörterten Beispielen scheint der Fehler des Barnes-Hut-Algorithmus deutlich höher zu liegen als von den Autoren ursprünglich selbst genannt. In einer weiteren Arbeit aus dem Jahr 1989 [2] geben diese in der Tat an, dass bei einer geringen Anzahl von Massenpunkten pro Würfel (weniger als 10) mit einem relativen Fehler der Kraft von bis zu 10% gerechnet werden muss. Für die Genauigkeit des Verfahrens ist nicht allein entscheidend, sondern auch die Zahl der Objekte pro Würfel. Je größer diese ist, umso geringer ist der durch Inhomogenitäten bewirkte Fehler bei der Kräfteberechnung. Vor allem im dichten Zentralbereich eines Mehrkörpersystems sind somit zuverlässige Ergebnisse zu erwarten. Man kann zudem davon ausgehen, dass die dort sich befindlichen Massenpunkte von allen Seiten von Würfeln mit relativ vielen Einzelkörpern umgeben sind und sich die pro Oktant eingeführten Fehler so in erheblichem Maße gegenseitig aufheben. Finden sich mehr als 1000 Massenpunkte pro Würfel, ist selbst mit einem von 1 ein relativer Fehler der Kraft von nur einigen 0.1% gegeben.
Wird die Anziehungskraft gemäß der Methode von Aarseth geglättet, wirkt sich dies zusätzlich stabilisierend auf den Barnes-Hut-Algorithmus aus. Dessen relativer Fehler hängt jetzt auch davon ab, wie weit ein Oktant im Vergleich zum Plummerradius entfernt ist, also vom Verhältnis . Betrachtet man obiges Beispiel abermals für den ungünstigsten Fall hintereinander stehender Massenpunkte, so gilt:
Beträgt der Abstand des Oktanten das doppelte des Plummerradius, so tritt bei einem von 0.5 ein relativer Fehler von nur noch circa 3% Prozent in Erscheinung. Wird ein gleich 1 zugelassen, steigt bei unveränderter Glättung der Gravitation die Abweichung auf immerhin noch etwa 15%. Ist der Distanz des Würfels gleich, so liegen die entsprechenden relativen Fehler nur noch bei circa 2% und 10%. Zudem kehrt der durch die Gruppenbildung begangene Fehler bei der Kräfteberechnung jetzt seine Richtung um: Diese liefert jetzt größere Beschleunigungen als die exakte Betrachtung, da für Abstände kleiner als der Plummerradius Aarseth's Glättungsformel eine um so geringere Kraft liefert, je geringer die Entfernung zwischen zwei Massenpunkten ist. Gerade dann, wenn eine stark inhomogene Verteilung von Massenpunkten gleichzeitig von sehr kleinen Abständen zwischen einzelnen Körpern (d.h. in der Größenordnung des Plummerradius) begleitet wird, erweist sich für ein geglättetes Gravitationsgesetz das Barnes-Hut-Verfahren also als recht stabil.
Insgesamt wird die Genauigkeit einer Mehrkörpersimulation mittels hierarchischer Kräfteberechnung zumeist von deren Fehlern dominiert und nicht von der Wahl der Lösungsformel für die sukzessive Berechnung von zurückgelegten Strecken und Geschwindigkeiten. Auch dies rechtfertigt, das nominell ungenauere Leapfrog-Verfahren den Hermite-Polynomen vorzuziehen.
Abschließend sei als praktisches Beispiel ein weiteres Mal das schon wiederholt besprochene Drei-Körper-System herangezogen. Der Parameter wird auf 0.1 gesetzt, d.h. aus der Sicht eines Massenpunktes die beiden übrigen Objekte erst dann zu einer Gruppe zusammengefasst, wenn der überdeckende Würfel mindestens 10 Mal kleiner ist als dessen Entfernung. Obwohl damit bei der Berechnung der von einem solchen Paar ausgehenden Gesamtkraft lediglich ein geringer Fehler in der Größenordnung von zumeist nur 0.1% begangen wird, sind die Auswirkungen der Näherung bereits drastisch. Die originalen und die genäherten Bahnen weichen im Laufe der Zeit immer mehr voneinander ab, bis schließlich jede Ähnlichkeit verschwunden ist. Für die Bearbeitung von Problemen, wo es auf eine möglichst exakte Vorhersage der einzelnen Orbits ankommt (z.B. der Frage nach der Langzeitstabilität der Planetenbahnen), ist der Barnes-Hut-Algorithmus also völlig ungeeignet. Erfolgreich kann er dagegen bei aus sehr vielen Einzelobjekten bestehenden Systemen eingesetzt werden, wo die genaue individuelle Bahn eines Körpers in der Regel nicht von Bedeutung ist. Bei Kugelsternhaufen oder Galaxien beispielsweise ist es meist ausreichend, wenn großräumige Eigenschaften wie z.B. die Verteilung der Dichte oder die Häufigkeit von Geschwindigkeiten richtig modelliert werden. Welche Auswirkungen hierbei unterschiedliche Einstellungen von haben, wird im Praxiskapitel gezeigt.
Aktualisierung der Raumhierarchie
BearbeitenNach jeder Anwendung des Prädiktors muss die Raumhierarchie aktualisiert werden. Da die Massenpunkte durch eine einmalige Anwendung der Prognoseformel nur kurze Strecken zurücklegen, bleiben diese selbst auf der niedrigsten Raumebene oft allesamt in ihren aktuellen Würfeln. Für viele Simulationsschritte genügt es somit, die Schwerpunkte der momentan definierten Oktanten neu zu bestimmen. Dazu dient das folgende Verfahren, dessen Code wiederum im letzten Abschnitt dieses Unterkapitels gegeben wird.
for (e = Anzahl der Ebenen - 1;e > 0;e --)
{
for (o = 0;o < Anzahl der belegten Oktanten auf Ebene e;o ++)
{
Summiere für Schwerpunktberechnung über alle im aktuellen Oktanten vorhandenen Massenpunkte i
sofern diese nicht schon abgearbeitet sind
Markiere jeden vorhandenen Massenpunkt i als abgearbeitet
}
if (e > 0)
{
for (o = 0;o < Anzahl der belegten Oktenten auf Ebene e;o ++)
{
Betrachte die soeben aktualisierten Oktanten auf Ebene e selbst als Massenpunkte
Addiere sie zu ihren übergeordneten Oktanten auf Ebene e - 1 hinzu
}
}
}
Die Neuberechnung der Würfelschwerpunkte startet im Gegensatz zu den bisher vorgestellten Algorithmen auf der untersten Raumebene. Zuerst werden die kleinsten Kuben aktualisiert, indem über die dort vorhandenen Massenpunkte (auf der niedrigsten Ebene zwangsläufig immer nur einer pro Würfel) aufsummiert wird, wobei jeder Körper als abgearbeitet markiert wird. Die Oktanten können nun selbst als Massenpunkte angesehen und zu den ihnen übergeordneten Würfeln addiert werden. Dieses Prinzip wird auf allen Ebenen wiederholt. Massenpunkte , welche tiefer eingeordnet sind als die momentan abgearbeitete Ebene, müssen somit nicht nochmals individuell berücksichtigt werden. Auf jeder Ebene wird der Inhalt eines Oktanten jeweils an seinen übergeordneten Würfel weitergereicht.
C-Code: Raumhierarchie mit Hermite-Polynome-Verfahren
Alle bisher eingeführten Variablen werden übernommen, doch treten für die Raumhierarchie zahlreiche neue hinzu. Die unsigned Integer e und o dienen im Folgenden dazu, Raumebenen und belegte Würfel (Oktanten) innerhalb einer solchen abzuzählen. Nebene gibt die Anzahl der Raumebenen, das eindimensionale Array Noktant die Anzahl belegter Kuben pro Ebene an.
Für jeden Massenpunkt wird seine tiefste Raumebene durch das einfache Array ebene_raum angezeigt. Das zweidimensionale Array status_raum listet die entsprechenden Koordinaten im Würfelgitter auf (wobei als Datentyp aber unsigned Long anstatt unsigned Integer verwendet wird). Der erste Index des Arrays bezieht sich erwartungsgemäß auf den Körper, der zweite auf die Koordinate. Als weiteres zweidimensionales Array hält nr_raum für jedes Objekt auf jeder Raumebene die Nummer des Würfels vor, in welchem es sich befindet. Hier bezieht sich der erste Index auf die Ebene und erst der zweite auf den Massenpunkt.
Bei den Variablen, welche verschiedene Größen der belegten Oktanten bezeichnen, bezieht sich der erste Index stets auf die Ebene, der zweiter auf den Würfel und bei Vektorgrößen ein dritter auf die Koordinate. Die zweidimensionalen Arrays No (für unsigned Integer) und Mo (für Double) geben auf jeder Ebene für jeden Kubus die Anzahl dort vorhandener Körper und deren Gesamtmasse an. Die dreidimensionalen Arrays ro und vo (wieder für Double) halten die Schwerpunktpositionen und -geschwindigkeiten der Oktanten vor. Das einfache Array Ao liefert pro Raumebene die Kantenlänge der Würfel. nruebero und flago stellen wieder zweidimensionale Arrays für unsigned Integer dar. nruebero kennzeichnet auf jeder Ebene für jeden Würfel die Nummer des übergeordneten Oktanten. flago zeigt an, ob ein Würfel im Laufe einer Kräfteberechnung bereits erfasst wurde oder nicht.
Um die in einem Oktanten vorhandenen Massenpunkte festzuhalten, wird das dreidimensionale Array nrobjekto für unsigned Integer eingeführt. Ein weiteres Mal repräsentieren die ersten beiden Indizes Raumebene und Würfel. Der dritte Index dient nun dazu, die Nummern der im Kubus sich befindlichen Körper aufzulisten. Um festzustellen, ob ein Einzelobjekt bei einer Neuberechnung der Schwerpunktpositionen und -geschwindigkeiten der Oktanten bereits einbezogen wurde, dient das eindimensionale Array flag.
Als letzte Variable ist alpha zu nennen, das für die Gruppenbildung entscheidende Verhältnis zwischen Kantenlänge und Entfernung eines Würfels zu dem zu simulierenden Massenpunkt.
/* Alte globale Variablen */
unsigned int i,k;
unsigned int N;
double *m;
double ***r;
double ***v;
double ***a;
double ***j;
double s[3];
double c[3];
double *tdyn;
int *ebene_zeit;
unsigned int potenz[16];
double *status_zeit;
double t,dt,dt2,dt3,dt4,dt5;
double dtmin;
unsigned long Z;
double tdynmin;
double T;
double gamma;
double epsilon,epsilon2,epsilon3;
/* Neue globale Variablen */
unsigned int e,o;
unsigned int Nebene;
unsigned int *Noktant;
unsigned int *ebene_raum;
unsigned long **status_raum;
unsigned int **nr_raum;
unsigned int **No;
double **Mo;
double ***ro;
double ***vo;
double *Ao;
unsigned int **nruebero;
unsigned int **flago;
unsigned int ***nrobjekto;
unsigned int *flag;
double alpha;
Der Hauptcode bleibt - auch mit Einbeziehung der Zeithierarchie - fast unverändert gültig. Während der Ausführung des Prädiktors wird nun aber überprüft, ob dadurch die Massenpunkte bezogen auf die unterste Raumebene in ihren Würfeln bleiben oder nicht. Dies geschieht mittels der lokalen unsigned Integer status_raum_neu, die immer um 1 hochgezählt wird, sobald ein Übergang eines Körpers zu einem anderen Oktanten festgestellt wird. Im allerersten Simulationsschritt erfolgt diese Prüfung noch nicht, da die Raumhierarchie an sich erst einmal angelegt werden muss. Der Prädiktorschritt sieht nun folgendermaßen aus:
status_raum_neu = 0;
for (i = 0;i < N;i ++)
{
dt = t - status_zeit[i] + dtmin;
dt2 = pow (dt,2);
dt3 = pow (dt,3);
for (k = 0;k < 3;k ++)
{
r[1][i][k] = r[0][i][k] + v[0][i][k] * dt + a[0][i][k] * dt2 / 2 + j[0][i][k] * dt3 / 6;
if (t > 0 && (unsigned long) floor ((r[1][i][k] + Ao[0]) / Ao[Nebene-1]) != status_raum[i][k])
status_raum_neu ++;
v[1][i][k] = v[0][i][k] + a[0][i][k] * dt + j[0][i][k] * dt2 / 2;
}
}
Halten sich alle Körper des Ensembles weiterhin in ihren bisherigen Würfeln auf (was durch status_raum_neu = 0 erkannt wird), so reicht vor dem Korrektorschritt eine Neuberechnung der Oktantenschwerpunkte aus. Anderenfalls muss die Raumhierarchie neu konstruiert werden. Dementsprechend lauten die nächsten Anweisungen:
if (t > 0 && status_raum_neu == 0)
updatehierarchie (N,m,r[1],v[1]);
else
{
speicherfreigabehierarchie ();
raumhierarchie (N,m,r[1],v[1]);
}
Für die Neudefinition der Raumhierarchie dient eine gesonderte Prozedur, welche mit raumhierarchie (N,m,r[1],v[1]) aufgerufen wird. Übergeben werden dieser die Anzahl der Objekte, deren Massen uund die mittels des Prädiktors vorhergesagten Positionen und Geschwindigkeiten.
Für die Konstruktion des Würfelgitters ist eine dynamische Speicherverwaltung zwingend erforderlich, da zu Beginn weder die Anzahl der Raumebenen noch die Anzahl der belegten Oktanten pro Ebene bekannt sind. Deshalb sind die entsprechenden Anweisungen im nachfolgenden Code mit angegeben. Bevor eine neue Hierarchie definiert werden kann, muss der von der bisherigen belegte Speicher freigegeben werden, was wiederum durch eine eigene, hier nicht weiter ausgeführte Prozedur geschehen kann.
Die lokalen Variablen innerhalb der Prozedur haben folgende Bedeutung. Die Double abstand gibt die Entfernung eines Körpers vom Koordinatenursprung (Schwerpunkt des Gesamtsystems) an. Die unsigned Integer gleich zählt ab, wie oft auf der gerade analysierten Raumebene zwei Körper innerhalb des gleichen Würfels gefunden wurden. i und j zählen die Massenpunkte selbst ab.
void raumhierarchie (unsigned int N, double *m, double **r, double **v)
{
double abstand;
unsigned int gleich;
unsigned int i,j;
/* Kantenlänge eines Würfels auf Ebene 0 gleich Abstand des fernsten Massenpunkts vom Ursprung */
Ao = malloc (sizeof (double));
Ao[0] = 0;
for (i = 0;i < N;i ++)
{
ebene_raum[i] = 0;
abstand = betrag (r[i]);
if (Ao[0] < abstand)
Ao[0] = abstand;
}
/* Speicherzuweisung und Initialisierung des ersten Würfels auf Ebene 0 */
/* Anzahl der Würfel pro Ebene */
Noktant = malloc (sizeof (double));
/* Anzahl der Massenpunkte pro Würfel */
No = malloc (sizeof (double *));
No[0] = malloc (sizeof (double));
No[0][0] = 0;
/* Masse pro Würfel */
Mo = malloc (sizeof (double *));
Mo[0] = malloc (sizeof (double));
Mo[0][0] = 0;
/* Schwerpunktposition pro Würfel */
ro = malloc (sizeof (double **));
ro[0] = malloc (sizeof (double *));
ro[0][0] = malloc (3 * sizeof (double));
for (k = 0;k < 3;k ++)
ro[0][0][k] = 0;
/* Schwerpunktgeschwindigkeit pro Würfel */
vo = malloc (sizeof (double **));
vo[0] = malloc (sizeof (double *));
vo[0][0] = malloc (3 * sizeof (double));
for (k = 0;k < 3;k ++)
vo[0][0][k] = 0;
/* Nummer des übergeordneten Oktanten pro Würfel */
nruebero = malloc (sizeof (unsigned int *));
nruebero[0] = malloc (sizeof (unsigned int));
nruebero[0][0] = 0;
/* Zur späteren Berechnung von Beschleunigung und Rucks verwendetes Flag pro Würfel */
flago = malloc (sizeof (unsigned int *));
flago[0] = malloc (sizeof (unsigned int));
/* Nummer des Würfels in welchem sich ein bestimmter Massenpunkt aufhält */
nr_raum = malloc (sizeof (unsigned int *));
nr_raum[0] = malloc (N * sizeof (int));
/* Liste vorhandenen Massenpunkte pro Würfel */
nrobjekto = malloc (sizeof (unsigned int **));
nrobjekto[0] = malloc (sizeof (unsigned int *));
nrobjekto[0][0] = malloc (sizeof (unsigned int));
/* Start des Algorithmus */
gleich = 65535;
e = 0;
/* Anlegen einer neuen Ebene, falls auf letzter Ebene zumindest 1 Würfel mit mehr als 1 Massenpunkt vorhanden */
while (gleich > 0)
{
/* Kantenlänge eines Würfels auf Ebene e halb so groß wie auf Ebene e-1 */
if (e > 0)
{
Ao = realloc (Ao,(e + 1) * sizeof (double));
Ao[e] = Ao[e-1] / 2;
}
/* Für alle Massenpunkte Umrechnen der (kontinuierlichen) kartesischen in (diskrete) Gitterkoordinaten */
for (i = 0;i < N;i ++)
{
for (k = 0;k < 3;k ++)
status_raum[i][k] = (unsigned int) floor ((r[i][k] + Ao[0]) / Ao[e]);
}
if (e > 0)
{
/* Speicherzuweisung und Initialisierung des ersten Würfels auf Ebene e */
/* Folgt dem gleichen Schema wie auf Ebene 0 */
/* Man beachte, dass bereits e Ebenen vorhanden sind */
Noktant = realloc (Noktant,(e + 1) * sizeof (double));
No = realloc (No, (e + 1) * sizeof (double *));
No[e] = malloc (sizeof (double));
No[e][0] = 0;
Mo = realloc (Mo, (e + 1) * sizeof (double *));
Mo[e] = malloc (sizeof (double));
Mo[e][0] = 0;
ro = realloc (ro, (e + 1) * sizeof (double **));
ro[e] = malloc (sizeof (double *));
ro[e][0] = malloc (3 * sizeof (double));
for (k = 0;k < 3;k ++)
ro[e][0][k] = 0;
vo = realloc (vo, (e + 1) * sizeof (double **));
vo[e] = malloc (sizeof (double *));
vo[e][0] = malloc (3 * sizeof (double));
for (k = 0;k < 3;k ++)
vo[e][0][k] = 0;
nruebero = realloc (nruebero, (e + 1) * sizeof (unsigned int *));
nruebero[e] = malloc (sizeof (unsigned int));
nruebero[e][0] = 0;
flago = realloc (flago, (e + 1) * sizeof (unsigned int *));
flago[e] = malloc (sizeof (unsigned int));
nr_raum = realloc (nr_raum, (e + 1) * sizeof (unsigned int *));
nr_raum[e] = malloc (N * sizeof (int));
nrobjekto = realloc (nrobjekto, (e + 1) * sizeof (unsigned int **));
nrobjekto[e] = malloc (sizeof (unsigned int *));
nrobjekto[e][0] = malloc (sizeof (unsigned int));
}
Noktant[e] = 0;
/* Abarbeiten aller Massenpunkte */
gleich = 0;
for (i = 0;i < N;i ++)
{
/* Anlegen eines Würfels für Massenpunkt i falls */
/* dieser auf Ebene e-1 die Ebene e zugewiesen bekommen hat (dort nicht allein in seinem Würfel war) */
if (ebene_raum[i] == e)
{
/* Hinzufügen des Massenpunkts i zu dem neuen Würfel */
/* Aufnahme in die Liste der Massenpunkte des Würfels */
if (No[e][Noktant[e]] > 0)
nrobjekto[e][Noktant[e]] = realloc (nrobjekto[e][Noktant[e]],
(No[e][Noktant[e]] + 1) * sizeof (unsigned int *));
nrobjekto[e][Noktant[e]][No[e][Noktant[e]]] = i;
/* Aktualisierte Anzahl der Massenpunkte im Würfel */
No[e][Noktant[e]] ++;
/* Aktualisierte Masse */
Mo[e][Noktant[e]] += m[i];
/* Aktualisierte Schwerpunktposition und -geschwindigkeit */
for (k = 0;k < 3;k ++)
{
ro[e][Noktant[e]][k] += m[i] * r[i][k];
vo[e][Noktant[e]][k] += m[i] * v[i][k];
}
/* Nummer des übergeordneten Würfels */
if (e > 0)
nruebero[e][Noktant[e]] = nr_raum[e-1][i];
/* Vorhalten der Nummer des neuen Würfels für den Massenpunkt i */
nr_raum[e][i] = Noktant[e];
/* Abarbeiten aller Massenpunktpaare */
for (j = i + 1;j < N;j ++)
{
/* Vergleich der Gitterkoordinaten für das Paar (i,j) */
/* Falls Koordinaten identisch und auch Massenpunkt j auf Ebene e */
/* Verschieben beider auf nächsttiefere Ebene, da im aktuellen Würfel nicht allein */
if (status_raum[j][0] == status_raum[i][0] && status_raum[j][1] == status_raum[i][1] &&
status_raum[j][2] == status_raum[i][2] && ebene_raum[j] == e)
{
ebene_raum[i] = e + 1;
ebene_raum[j] = e + 1;
/* Abzählen der Ereignisse, dass Paare innerhalb des gleichen Würfels gefunden wurden */
gleich ++;
/* Hinzufügen des Massenpunktes j zu dem gleichen Würfel wie Massenpunkt i */
/* Folgt dem gleichen Schema wie für Massenpunkt i */
nrobjekto[e][Noktant[e]] = realloc (nrobjekto[e][Noktant[e]],
(No[e][Noktant[e]] + 1) * sizeof (unsigned int *));
nrobjekto[e][Noktant[e]][No[e][Noktant[e]]] = j;
No[e][Noktant[e]] ++;
Mo[e][Noktant[e]] += m[j];
for (k = 0;k < 3;k ++)
{
ro[e][Noktant[e]][k] += m[j] * r[j][k];
vo[e][Noktant[e]][k] += m[j] * v[j][k];
}
nr_raum[e][j] = Noktant[e];
}
}
/* Nach Ende der j-Schleife Speicherzuweisung und Initialisierung des nächsten Würfels auf Ebene e */
/* Folgt abermals dem gleichen Schema wie die vorherigen Zuweisungen */
/* Man beachte, dass bereits Noktant[e] Würfel auf Ebene e vorhanden sind */
Noktant[e] ++;
No[e] = realloc (No[e], (Noktant[e] + 1) * sizeof (double));
No[e][Noktant[e]] = 0;
Mo[e] = realloc (Mo[e], (Noktant[e] + 1) * sizeof (double));
Mo[e][Noktant[e]] = 0;
ro[e] = realloc (ro[e], (Noktant[e] + 1) * sizeof (double *));
ro[e][Noktant[e]] = malloc (3 * sizeof (double));
for (k = 0;k < 3;k ++)
ro[e][Noktant[e]][k] = 0;
vo[e] = realloc (vo[e], (Noktant[e] + 1) * sizeof (double *));
vo[e][Noktant[e]] = malloc (3 * sizeof (double));
for (k = 0;k < 3;k ++)
vo[e][Noktant[e]][k] = 0;
nruebero[e] = realloc (nruebero[e], (Noktant[e] + 1) * sizeof (unsigned int));
nruebero[e][Noktant[e]] = 0;
flago[e] = realloc (flago[e], (Noktant[e] + 1) * sizeof (unsigned int));
nrobjekto[e] = realloc (nrobjekto[e], (Noktant[e] + 1) * sizeof (unsigned int *));
nrobjekto[e][Noktant[e]] = malloc (sizeof (unsigned int));
}
}
/* Nach Ende der i-Schleife Abschluss der Berechnung von Schwerpunktposition und -geschwindigkeit */
/* für jeden Würfel auf Ebene e */
for (o = 0;o < Noktant[e];o ++)
{
for (k = 0;k < 3;k ++)
{
ro[e][o][k] /= Mo[e][o];
vo[e][o][k] /= Mo[e][o];
}
}
/* Anlegen einer neuen Ebene, falls auf letzter Ebene zumindest 1 Würfel mit mehr als 1 Massenpunkt vorhanden */
if (gleich > 0)
e ++;
}
Nebene = e + 1;
}
Die Aktualisierung der Schwerpunkte einer schon vorgegebenen Hierarchie von Oktanten geschieht durch eine Prozedur updatehierarchie (N,m,r[1],v[1]), welcher die gleichen Werte übergeben werden wie der Raumhierarchie-Prozedur. Die lokale unsigned Integer n zählt die in einem Würfel vorhandenen Körper ab.
void updatehierarchie (unsigned int N, double *m, double **r, double **v)
{
unsigned int n;
/* Initialisierung der Schwerpunkte nur für Positionen und Geschwindigkeiten */
/* Gesamtmassen der belegten Oktanten bei gleich gebliebener Hierarchie unverändert */
for (e = 0;e < Nebene;e ++)
{
for (o = 0;o < Noktant[e];o ++)
{
for (k = 0;k < 3;k ++)
{
ro[e][o][k] = 0;
vo[e][o][k] = 0;
}
}
}
for (i = 0;i < N;i ++)
flag[i] = 0;
/* Aktualisierung der Schwerpunkte der Oktanten auf aktueller Ebene e */
/* Betrachte individuelle Mitglieder des Ensembles als Massenpunkte innerhalb der Würfel */
for (e = Nebene - 1;e > 0;e --)
{
for (o = 0;o < Noktant[e];o ++)
{
for (n = 0;n < No[e][o];n ++)
{
/* Individueller Massenpunkt muss nur berücksichtigt werden */
/* falls nicht schon auf tieferer Ebene abgearbeitet */
if (flag[Nr_objekt_o[e][o][n]] == 0)
{
for (k = 0;k < 3;k ++)
{
ro[e][o][k] += m[nrobjekto[e][o][n]] * r[nrobjekto[e][o][n]][k];
vo[e][o][k] += m[nrobjekto[e][o][n]] * v[nrobjekto[e][o][n]][k];
}
/* Markiere individuellen Massenpunkt nach Addieren zu seinem Würfel als abgearbeitet */
flag[nrobjekto[e][o][n]] = 1;
}
}
}
for (o = 0;o < Noktant[e];o ++)
{
for (k = 0;k < 3;k ++)
{
ro[e][o][k] /= Mo[e][o];
vo[e][o][k] /= Mo[e][o];
}
}
/* Aktualisierung der Schwerpunkte der übergeordneten Oktanten auf Ebene e - 1 */
/* Betrachte Oktanten auf Ebene e wiederum als Massenpunkte innerhalb der übergeordneten Würfel */
/* An dieser Stelle KEINE Division von Positions- und Geschwindigkeitswerten mit Oktantenmassen */
/* Noch fehlende individuelle Mitglieder des Ensembles werden auf nächster Ebene hinzuaddiert */
if (e > 0)
{
for (o = 0;o < Noktant[e];o ++)
{
for (k = 0;k < 3;k ++)
{
ro[e-1][nruebero[e][o]][k] += Mo[e][o] * ro[e][o][k];
vo[e-1][nruebero[e][o]][k] += Mo[e][o] * vo[e][o][k];
}
}
}
}
}
Alle übrigen Simulationsanweisungen laufen ab wie bisher. Die Bestimmung von Beschleunigung und Ruck erfolgt nun jedoch mit einer modifizierten Prozedur ruckhierarchie (i,epsilon,r[1][i],v[1][i],a[1][i],j[1][i]), welche sich die Raumhierarchie zu Nutze macht. Dieser werden wie der Standardprozedur die Nummer des untersuchten Körpers, der Plummerradius sowie sämtliche vorläufige Positionen und Geschwindigkeiten übergeben. Die Anzahl der Massenpunkte und deren Massen tauchen hingegen als Übergabeparameter hier nicht auf, da ja jetzt nicht über die einzelnen Mitglieder des Ensembles summiert wird, sondern über die von diesen belegten Würfel. Zurückgegeben werden wie üblich Beschleunigung und Ruck, welche wie bisher in den Korrektor eingesetzt werden, um verbesserte Positionen und Geschwindigkeiten zu erhalten. Im Vergleich zum bisherigen Vorgehen werden keine neuen lokalen Variablen benötigt.
void ruckhierarchie (unsigned int objekt, double epsilon, double *r, double *v, double *a, double *j)
{
/* Lokale Variablen */
unsigned int k;
double dr[3];
double d,d2,d3,d5,d6,d8;
double dv[3];
double skalar;
/* Initialisierung von Beschleunigung und Ruck */
for (k = 0;k < 3;k ++)
{
a[k] = 0;
j[k] = 0;
}
/* Initialisierung aller Würfel als unbearbeitet */
for (e = 0;e < Nebene;e ++)
{
for (o = 0;o < Noktant[e];o ++)
flago[e][o] = 0;
}
/* Berechnung von Beschleunigung und Ruck */
/* Abarbeiten aller Raumebenen */
for (e = 0;e < Nebene;e ++)
{
/* Abarbeiten aller belegten Würfel innerhalb einer Ebene */
for (o = 0;o < Noktant[e];o ++)
{
/* Betrachte Würfel o falls */
/* 1) Übergeordneter Würfel von o noch nicht betrachtet UND */
/* 2) o enthält objekt nicht */
/* 1) wird sichergestellt durch */
/* a) Aktuelle Ebene = 0 -> kein übergeordneter Würfel vorhanden */
/* b) Sonst: Flag des übergeordneten Würfels steht auf 0 */
/* 2) wird sichergestellt durch */
/* a) Aktuelle Ebene tiefer als Ebene des objekt -> objekt in seinem Würfel allein */
/* b) Sonst: o nicht Nummer des Würfels, in welchem objekt sich befindet */
if ((e == 0 || e > 0 && flago[e-1][nruebero[e][o]] == 0) &&
(ebene_raum[objekt] < e || ebene_raum[objekt] >= e && nr_raum[e][objekt] != o))
{
/* Abstand zwischen Würfel o und objekt */
for (k = 0;k < 3;k ++)
dr[k] = ro[e][o][k] - r[k];
d = betrag (dr);
/* Betrachte Würfel o falls */
/* Verhältnis Kantenlänge / Entfernung erfüllt alpha-Kriterium ODER */
/* o enthält nur einen Massenpunkt */
if (Ao[e] / d < alpha || No[e][o] == 1)
{
/* Markieren von Würfel o als abgearbeitet */
flago[e][o] = 1;
/* Geschwindigkeitsdifferenz zwischen Würfel o und objekt */
for (k = 0;k < 3;k ++)
dv[k] = vo[e][o][k] - v[k];
/* Weitere Berechnungen analog zur Standardprozedur */
skalar = skalarprodukt (dr,dv);
if (d > 2 * epsilon)
{
d3 = pow (d,3);
d5 = pow (d,5);
}
else
{
d2 = pow (d,2) + 4 * epsilon2;
d6 = pow (d2,3);
d8 = pow (d2,4);
}
for (k = 0;k < 3;k ++)
{
if (d > 2 * epsilon)
{
a[k] += G * Mo[e][o] * dr[k] / d3;
j[k] += G * Mo[e][o] * (dv[k] / d3 - 3 * skalar * dr[k] / d5);
}
else
{
a[k] += G * Mo[e][o] * 64 * epsilon3 * dr[k] / d6;
j[k] += G * Mo[e][o] * 64 * epsilon3 * (dv[k] / d6 - 6 * dr[k] * skalar / d8);
}
}
}
}
/* Würfel o automatisch abgearbeitet, falls schon dessen übergeordneter Würfel abgearbeitet */
if (e > 0 && flago[e-1][nruebero[e][o]] == 1)
flago[e][o] = 1;
}
}
}
Der Barnes-Hut-Algorithmus kann selbstverständlich auch mit dem Leapfrog-Verfahren kombiniert werden. In diesem Fall werden nur die Schwerpunktpositionen der Würfel benötigt, nicht aber auch deren Geschwindigkeiten. Für jeden Massenpunkt muss dann lediglich die von der Hierarchie ausgeübte Beschleunigung bestimmt werden, nicht aber auch der Ruck. Die entsprechenden Codes sind hier nicht weiter aufgelistet, sie können aus den gegebenen Prozeduren durch Fortlassen der nicht erforderlichen Berechnungen leicht abgeleitet werden.