Sechster Teil der Serie über diskrete Quantengravitation
Im fünften Artikel habe ich geschrieben: Ich habe kein Modell, nur eine Idee. Dann passierte etwas Unerwartetes.
Jemand (LLM) hat angefangen zu rechnen.
Genauer: jemand hat die Ideen aus dieser Serie in Python übersetzt — in ein lauffähiges Programm, das einen kausalen Graphen erzeugt, ihn wachsen lässt, Teilchen darauf simuliert und testet, ob die Signalgeschwindigkeit richtungsabhängig ist. Das Programm ist kein Beweis. Es ist kein physikalisches Modell im strengen Sinn. Aber es ist der erste Schritt vom Bild zur Berechnung — und das ist mehr, als ich erwartet hatte.
Dieser Artikel erklärt, was das Programm tut, was es zeigt, und wo es noch nicht hinkommt.
01 · Was das Programm macht
Das Programm erzeugt einen gerichteten azyklischen Graphen — genau die Struktur, die ich im ersten Artikel beschrieben habe. Ein Knoten pro Zustand, eine Kante pro Übergang, kein Pfad rückwärts in der Zeit. Der Graph wächst schichtweise: Zeitscheibe 0 hat einen Knoten, jede folgende Schicht entsteht durch Verzweigung der vorherigen.
Das ist keine Metapher mehr. Das ist Code:
def create_causal_graph(steps=20, p=0.6, branching_factor=2):
G = nx.DiGraph()
G.add_node(0, time=0) # Startknoten: der "Urknall"
nodes_by_time = {0: [0]}
next_id = 1
for t in range(steps-1):
nodes_at_t = nodes_by_time[t]
new_nodes = []
for parent in nodes_at_t:
if np.random.random() < p: # mit Wahrscheinlichkeit p...
for _ in range(branching_factor): # ...entstehen neue Knoten
child = next_id
next_id += 1
G.add_node(child, time=t+1)
G.add_edge(parent, child)
new_nodes.append(child)
nodes_by_time[t+1] = new_nodes
return G, nodes_by_time
Der Parameter branching_factor ist genau das b aus dem zweiten Artikel — die Verzweigungsrate, die ich als Kandidaten für die kosmologische Expansionsrate vorgeschlagen hatte. Hier ist er eine konkrete Zahl, kein Symbol.
02 · Expansion als Messgröße
Das Programm berechnet eine diskrete Analogie zur Hubble-Konstante — der Rate, mit der das Universum sich ausdehnt. Im Graphenmodell ist das einfach das Verhältnis der Knotenanzahl zwischen zwei aufeinanderfolgenden Zeitscheiben:
H(t) = log( N(t+1) / N(t) )
Das ist direkt messbar. Wenn der Verzweigungsfaktor konstant ist, ist H konstant — ein Universum, das gleichmäßig wächst. Wenn der Verzweigungsfaktor mit der Zeit zunimmt, beschleunigt sich die Expansion — genau wie im beobachteten Universum.
Das Programm implementiert auch eine beschleunigte Variante:
branching_factor = branching_factor_base + 0.1 * t # wächst mit der Zeit
Das ist noch keine Erklärung für Dunkle Energie. Aber es ist eine präzise Formulierung der Hypothese: Dunkle Energie als wachsende Verzweigungsrate — und die ist simulierbar und damit vergleichbar mit Beobachtungen.
03 · Dunkle Materie als topologischer Defekt
Der interessanteste Teil des Programms ist die Simulation topologischer Defekte. Die Idee aus dem fünften Artikel war: Dunkle Materie ist kein Teilchen, sondern eine Eigenschaft der Graphstruktur — Bereiche mit ungewöhnlich hoher Vernetzung.
Das Programm setzt das um, indem es zusätzliche Kanten einfügt — Verbindungen zwischen Knoten, die nach der normalen Kausalstruktur nicht verbunden wären:
def add_defects(G, nodes_by_time, defect_prob=0.05, same_time_edges=True):
# Fügt Kanten zwischen gleichzeitigen Knoten ein
# → "überzählige Vernetzung" als Dunkle-Materie-Proxy
Dann simuliert das Programm Testteilchen — Random Walks auf dem Graphen — und misst, wo sie sich häufen. Die Hypothese: Knoten mit hoher Aufenthaltswahrscheinlichkeit entsprechen Masseansammlungen. Knoten in Defektbereichen sollten Teilchen stärker anziehen als normale Knoten.
Das ist noch kein Gravitationsgesetz. Aber es ist ein messbarer Unterschied — mit Defekten gegen ohne Defekte — der zeigt, ob die Idee überhaupt konsistent ist.
04 · Der Lorentz-Test
Im dritten Artikel war der Lorentz-Einwand einer der schwersten: Ein diskretes Modell mit festen Zeitscheiben baut eine bevorzugte Zeit ein — und die widerspricht der speziellen Relativitätstheorie.
Das Programm testet genau das. Es misst, wie viele Schritte ein Signal braucht, um einen Knoten in der Zukunft zu erreichen — und vergleicht das mit der Zeit, die es braucht, um einen gleichzeitigen oder vergangenen Knoten zu erreichen:
def direction_dependent_speed(G, start_node):
# Misst durchschnittliche Schritte in drei Richtungen:
# - vorwärts (in die Zukunft)
# - rückwärts (in die Vergangenheit)
# - gleichzeitig (gleiche Zeitscheibe)
# Wenn vorwärts != rückwärts: Lorentz-Verletzung
Wenn die Ausbreitungsgeschwindigkeit richtungsabhängig ist — wenn Signale in die Zukunft schneller laufen als in die Vergangenheit — dann verletzt das Modell die Lorentz-Symmetrie. Das wäre ein Problem. Aber es wäre ein messbares Problem — und das Fermi-Gammateleskop sucht nach genau dieser Signatur in echten Daten.
05 · Was das Programm noch nicht kann
Das Programm ist ein Toy-Modell — und es benennt seine eigenen Grenzen klar. Das schätze ich.
Es gibt noch keine echte Metrik. Der Abstand zwischen zwei Knoten wird als kürzester Pfad im Graphen gemessen — das ist eine kombinatorische Distanz, keine Raumzeit-Metrik. Lorentz-Invarianz, Krümmung, der Zusammenhang zur Einsteinschen Feldgleichung — das alles fehlt noch. Der Code enthält bereits eine Skizze, wie man das angehen könnte: das Regge-Kalkül, das Raumzeit als Netz von Dreiecken und Tetraedern beschreibt und daraus Krümmung berechnet. Aber implementiert ist es noch nicht.
Es gibt noch keine echte Quantenmechanik. Die Teilchensimulation verwendet klassische Random Walks — zufällige Sprünge von Knoten zu Knoten. Echte Quantenmechanik braucht komplexe Amplituden und Interferenz. Auch hier enthält der Code eine Skizze:
def quantum_walk(G, start, end, steps=10):
amplitudes = {node: 0.0 + 0.0j for node in G.nodes()}
amplitudes[start] = 1.0 + 0.0j # Startzustand
for _ in range(steps):
new_amplitudes = {...}
for node in G.nodes():
for neighbor in G.neighbors(node):
# Amplitude propagiert zu Nachbarn
new_amplitudes[neighbor] += amplitudes[node] / len(neighbors)
amplitudes = new_amplitudes
return abs(amplitudes[end])**2 # Wahrscheinlichkeit am Zielknoten
Das ist der Ansatz von Feynmans Pfadintegral — summiere alle Pfade, gewichtet mit ihrer Amplitude. Auf einem Graphen statt im Kontinuum. Noch nicht fertig. Aber die Richtung ist klar.
06 · Was dieser Schritt bedeutet
Ich habe diese Serie als Sprachler geschrieben, der nach einer Fernsehsendung angefangen hat zu tippen. Ich habe ein Modell aufgebaut, es eingerissen, die Trümmer untersucht und am Ende geschrieben: Ich habe keine Idee, wie man das mathematisch löst.
Jetzt liegt Code auf dem Tisch. Lauffähiger Code, der die Kernideen simuliert, ihre Grenzen benennt und skizziert, wie man sie überwindet.
Das ist der Unterschied zwischen einer Metapher und einem Modell. Eine Metapher erklärt — ein Modell macht Vorhersagen, die man messen kann. Das Programm ist noch kein vollständiges Modell. Aber es ist der erste Schritt in diese Richtung.
„Eine Idee, die niemand berechnen kann, bleibt eine Idee. Eine Idee, die jemand zu simulieren beginnt, wird zu einer Frage — und Fragen kann man beantworten.“
Was als nächstes kommt: Das Programm muss laufen, die Ergebnisse müssen angeschaut werden, und dann muss man ehrlich fragen — stimmt irgendetwas davon mit dem überein, was wir beobachten? Oder ist b = 20 einfach falsch?
Das wäre dann Physik.
Dies ist der sechste Teil der Serie über diskrete Quantengravitation. Der vollständige Python-Code steht hier drunter. Die Serie begann mit Warum das Universum vielleicht ein Baum ist.
"""
Toy-Modell: Diskreter Graph als Raumzeit-Ersatz
- Keine externen Koordinaten
- Metrik = kürzeste Pfadlänge
- Test auf Homogenität, Expansion, Lorentz-Verletzung
- Topologische Defekte als Dunkle-Materie-Analogon
"""
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
from collections import defaultdict, Counter
import random
# ------------------------------
# 1. Grapherzeugung (kausales Wachstum)
# ------------------------------
def create_causal_graph(steps=20, p=0.6, branching_factor=2, seed=42):
"""
Erzeugt einen gerichteten azyklischen Graphen (DAG) durch kausales Wachstum.
steps: Anzahl Zeitscheiben (t=0..steps-1)
p: Wahrscheinlichkeit, dass ein Knoten Nachkommen erzeugt
branching_factor: Anzahl neuer Knoten pro aktivem Elter
"""
np.random.seed(seed)
G = nx.DiGraph()
# Startknoten zur Zeit 0
G.add_node(0, time=0)
nodes_by_time = {0: [0]}
next_id = 1
for t in range(steps-1):
nodes_at_t = nodes_by_time[t]
new_nodes = []
for parent in nodes_at_t:
if np.random.random() < p:
for _ in range(branching_factor):
child = next_id
next_id += 1
G.add_node(child, time=t+1)
G.add_edge(parent, child)
new_nodes.append(child)
nodes_by_time[t+1] = new_nodes
# Falls keine neuen Knoten: Abbruch (kein weiteres Wachstum)
if not new_nodes and t+1 < steps:
break
return G, nodes_by_time
# ------------------------------
# 2. Metrik: kürzeste Pfadlänge (ungerichtet)
# ------------------------------
def graph_distance(G, u, v):
"""Abstand im ungerichteten Graphen (kürzester Pfad)"""
try:
return nx.shortest_path_length(G.to_undirected(), u, v)
except nx.NetworkXNoPath:
return np.inf
def mean_pairwise_distance(G, nodes):
"""Mittlerer Abstand zwischen allen Paaren einer Knotenliste"""
if len(nodes) < 2:
return 0.0
dists = []
for i, u in enumerate(nodes):
for v in nodes[i+1:]:
d = graph_distance(G, u, v)
if d != np.inf:
dists.append(d)
return np.mean(dists) if dists else 0.0
# ------------------------------
# 3. Expansionstate (Hubble-ähnlich)
# ------------------------------
def expansion_rates(nodes_by_time):
"""Berechnet H(t) = log(N(t+1)/N(t)) für jede Zeit"""
times = sorted(nodes_by_time.keys())
rates = {}
for i in range(len(times)-1):
t = times[i]
N_t = len(nodes_by_time[t])
N_t1 = len(nodes_by_time[t+1])
if N_t > 0:
rates[t] = np.log(N_t1 / N_t) if N_t1 > 0 else -np.inf
else:
rates[t] = np.nan
return rates
# ------------------------------
# 4. Topologische Defekte einfügen (Dunkle Materie)
# ------------------------------
def add_defects(G, nodes_by_time, defect_prob=0.05, same_time_edges=True, back_edges=False):
"""
Fügt zusätzliche Kanten hinzu, die nicht der Zeitrichtung folgen.
defect_prob: Wahrscheinlichkeit pro Knotenpaar (innerhalb gewisser Grenzen)
same_time_edges: Erlaube Kanten zwischen Knoten derselben Zeit
back_edges: Erlaube Kanten rückwärts in der Zeit (von jung zu alt) – bricht Kausalität
"""
G_def = G.copy()
times = list(nodes_by_time.keys())
# Mögliche zusätzliche Kanten: zwischen Knoten mit Zeitdifferenz 0 oder 1 (sonst zu weit)
for t_idx, t in enumerate(times):
nodes_t = nodes_by_time[t]
# Gleichzeitige Knoten
if same_time_edges and len(nodes_t) > 1:
for i in range(len(nodes_t)):
for j in range(i+1, len(nodes_t)):
if np.random.random() < defect_prob:
G_def.add_edge(nodes_t[i], nodes_t[j]) # ungerichtet? DiGraph erlaubt beide Richtungen
# Für echte Ungerichtetheit müsste man beide Richtungen hinzufügen – hier nur eine
# Kanten zur nächsten Zeitscheibe sind bereits vorhanden, wir könnten zusätzliche Querverbindungen hinzufügen
if t_idx < len(times)-1:
nodes_t1 = nodes_by_time[times[t_idx+1]]
# Zusätzliche Kanten von t nach t+1 (über die normale Verzweigung hinaus)
for u in nodes_t:
for v in nodes_t1:
if not G_def.has_edge(u, v) and np.random.random() < defect_prob*0.5:
G_def.add_edge(u, v)
# Rückwärtskanten (von jung zu alt) – optional, bricht Kausalität
if back_edges and t_idx > 0:
nodes_t_prev = nodes_by_time[times[t_idx-1]]
for u in nodes_t:
for v in nodes_t_prev:
if np.random.random() < defect_prob*0.2:
G_def.add_edge(u, v)
return G_def
# ------------------------------
# 5. Testteilchen-Simulation (Random Walk)
# ------------------------------
def simulate_particles(G, start_nodes, steps=50, n_particles=100):
"""
Simuliert n_particles Teilchen, die sich per Random Walk (gleichverteilte Nachbarn) bewegen.
Misst die Aufenthaltswahrscheinlichkeit pro Knoten (als Proxy für effektive Gravitation).
"""
# Ungerichtete Version für Bewegung (Teilchen können in beide Richtungen)
G_und = G.to_undirected()
visits = defaultdict(int)
for _ in range(n_particles):
current = np.random.choice(start_nodes)
for _ in range(steps):
visits[current] += 1
neighbors = list(G_und.neighbors(current))
if neighbors:
current = np.random.choice(neighbors)
else:
break # stecken geblieben
# Normalisieren
total_visits = sum(visits.values())
if total_visits > 0:
probs = {node: visits[node]/total_visits for node in visits}
else:
probs = {}
return probs
# ------------------------------
# 6. Lorentz-Verletzung: Richtungsabhängigkeit der Signalgeschwindigkeit
# ------------------------------
def direction_dependent_speed(G, start_node, max_steps=20, n_samples=100):
"""
Simuliert von start_node ausgehende "Signale" (Random Walks) und misst die
mittlere Zeit, um Knoten in verschiedenen "Richtungen" zu erreichen.
Richtung wird definiert über die Zeitdifferenz:
- vorwärts: Knoten mit time > start_time
- rückwärts: time < start_time
- gleichzeitig: time == start_time (nur falls es solche Kanten gibt)
Für jede Richtung wird die durchschnittliche Schrittanzahl zum Erreichen eines
Knotens mit der gewünschten Zeitdifferenz gemessen.
"""
start_time = G.nodes[start_node]['time']
G_und = G.to_undirected()
results = {'forward': [], 'backward': [], 'same': []}
for _ in range(n_samples):
current = start_node
steps = 0
# Zufälliger Walk, bis wir einen Knoten mit anderer Zeit erreichen (oder max_steps)
for step in range(max_steps):
neighbors = list(G_und.neighbors(current))
if not neighbors:
break
current = np.random.choice(neighbors)
steps += 1
curr_t = G.nodes[current]['time']
if curr_t > start_time:
results['forward'].append(steps)
break
elif curr_t < start_time:
results['backward'].append(steps)
break
elif curr_t == start_time and current != start_node:
results['same'].append(steps)
break
# Mittlere Schrittzahl pro Richtung
means = {}
for key in results:
if results[key]:
means[key] = np.mean(results[key])
else:
means[key] = np.nan
return means
# ------------------------------
# 7. Hauptausführung
# ------------------------------
if __name__ == "__main__":
print("=== Graph-basierte Raumzeit-Simulation ===")
# 1. Graphen erzeugen
steps = 12
G, nodes_by_time = create_causal_graph(steps=steps, p=0.7, branching_factor=2, seed=123)
print(f"Erzeugter Graph: {G.number_of_nodes()} Knoten, {G.number_of_edges()} Kanten")
print("Knoten pro Zeitscheibe:", {t: len(nodes_by_time[t]) for t in nodes_by_time})
# 2. Metrik: mittlere Pfaddistanz in der letzten Zeitscheibe
last_time = max(nodes_by_time.keys())
nodes_last = nodes_by_time[last_time]
if len(nodes_last) >= 2:
mean_dist = mean_pairwise_distance(G, nodes_last)
print(f"\nMittlere Pfaddistanz zwischen Knoten der letzten Zeitscheibe (t={last_time}): {mean_dist:.3f}")
# 3. Expansionsrate
rates = expansion_rates(nodes_by_time)
print("\nExpansionsraten H(t) = log(N(t+1)/N(t)):")
for t, h in rates.items():
if np.isfinite(h):
print(f" t={t}: H={h:.4f}")
# 4. Topologische Defekte einfügen
G_def = add_defects(G, nodes_by_time, defect_prob=0.08, same_time_edges=True, back_edges=False)
print(f"\nNach Defekt-Einfügung: {G_def.number_of_edges()} Kanten (vorher {G.number_of_edges()})")
# 5. Testteilchen-Simulation: Vergleich ohne/mit Defekten
start_nodes = nodes_by_time[0] # Starte am Urknall
print("\nSimuliere Testteilchen (Random Walk) ...")
probs_no_defect = simulate_particles(G, start_nodes, steps=30, n_particles=200)
probs_defect = simulate_particles(G_def, start_nodes, steps=30, n_particles=200)
# Welche Knoten sind besonders wahrscheinlich? Das sind potentielle "Masse"-Ansammlungen
top_defect = sorted(probs_defect.items(), key=lambda x: x[1], reverse=True)[:5]
top_node = sorted(probs_no_defect.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top 5 Knoten mit höchster Aufenthaltswahrscheinlichkeit (mit Defekten):", top_defect)
print("Top 5 Knoten (ohne Defekte):", top_node)
# 6. Lorentz-Verletzungstest an einem zentralen Knoten (z.B. erster Knoten)
start_node = 0
print(f"\nLorentz-Verletzungstest für Startknoten {start_node} (Zeit={G.nodes[start_node]['time']}):")
speeds = direction_dependent_speed(G, start_node, max_steps=15, n_samples=80)
for direction, mean_steps in speeds.items():
if not np.isnan(mean_steps):
print(f" Richtung {direction}: durchschnittlich {mean_steps:.2f} Schritte bis Erreichen")
else:
print(f" Richtung {direction}: keine Samples")
# 7. Visualisierung (kleiner Graph für bessere Darstellung)
if G.number_of_nodes() <= 50:
plt.figure(figsize=(10, 8))
pos = nx.spring_layout(G, seed=42) # nur für Visualisierung, nicht physikalisch
nx.draw_networkx_nodes(G, pos, node_color='lightblue', node_size=200)
nx.draw_networkx_edges(G, pos, edge_color='gray', arrows=True, arrowsize=10)
nx.draw_networkx_labels(G, pos, font_size=8)
plt.title("Kausaler Graph (DAG) – Raumzeit als Netzwerk")
plt.axis('off')
plt.show()
else:
print("\nGraph zu groß für Visualisierung.")
