Задача N тел: прямое суммирование

Несколько тел под взаимным притяжением: прямое суммирование и законы сохранения.

Задача N тел — движение N масс, каждая из которых притягивает каждую; при N≥3 точного решения нет, и единственный путь — численное интегрирование.

Почему три тела — это уже хаос

Два тела решаются точно. Стоит добавить третье — и аналитическое решение исчезает (это знаменитая проблема трёх тел). Система становится хаотической: малое изменение начальных условий меняет всю последующую судьбу. Поэтому движение звёздных скоплений, галактик и даже долгосрочную эволюцию Солнечной системы считают только численно.

Прямое суммирование O(N²)

Самый честный метод: для каждого тела складываем притяжение от всех остальных. Если тел N, то на каждом шаге нужно N·(N-1) вычислений силы — сложность O(N²). Для трёх тел это пустяк, для миллиарда — катастрофа (об этом позже). Прогоним три тела, отслеживая энергию и импульс:

import math
G = 1.0
bodies = [
    {"m":2.0, "x":0.0,  "y":0.0,  "vx":0.0,  "vy":0.0},
    {"m":1.0, "x":1.0,  "y":0.0,  "vx":0.0,  "vy":1.3},
    {"m":1.0, "x":-1.0, "y":0.0,  "vx":0.0,  "vy":-1.3},
]
def accelerations(bs):
    n=len(bs); ax=[0.0]*n; ay=[0.0]*n
    for i in range(n):
        for j in range(n):
            if i==j: continue
            dx=bs[j]["x"]-bs[i]["x"]; dy=bs[j]["y"]-bs[i]["y"]
            r=math.hypot(dx,dy)+1e-9
            f=G*bs[j]["m"]/r**3
            ax[i]+=f*dx; ay[i]+=f*dy
    return ax, ay
def energy(bs):
    E=sum(0.5*b["m"]*(b["vx"]**2+b["vy"]**2) for b in bs)
    n=len(bs)
    for i in range(n):
        for j in range(i+1,n):
            dx=bs[j]["x"]-bs[i]["x"]; dy=bs[j]["y"]-bs[i]["y"]
            r=math.hypot(dx,dy)+1e-9
            E-=G*bs[i]["m"]*bs[j]["m"]/r
    return E
def momentum(bs):
    return sum(b["m"]*b["vx"] for b in bs), sum(b["m"]*b["vy"] for b in bs)
dt=0.0005
ax,ay=accelerations(bodies)
E0=energy(bodies); p0=momentum(bodies)
for step in range(20000):
    for i,b in enumerate(bodies):
        b["x"]+=b["vx"]*dt+0.5*ax[i]*dt*dt
        b["y"]+=b["vy"]*dt+0.5*ay[i]*dt*dt
    ax2,ay2=accelerations(bodies)
    for i,b in enumerate(bodies):
        b["vx"]+=0.5*(ax[i]+ax2[i])*dt; b["vy"]+=0.5*(ay[i]+ay2[i])*dt
    ax,ay=ax2,ay2
E1=energy(bodies); p1=momentum(bodies)
print(f"Шагов: 20000, прошло t={20000*dt:.1f}")
print(f"Энергия:  старт {E0:+.4f}  конец {E1:+.4f}")
print(f"Импульс X: {p0[0]:+.4f} -> {p1[0]:+.4f}")
print(f"Импульс Y: {p0[1]:+.4f} -> {p1[1]:+.4f}")

Вывод:

Шагов: 20000, прошло t=10.0
Энергия:  старт -2.8100  конец -2.8100
Импульс X: +0.0000 -> +0.0000
Импульс Y: +0.0000 -> +0.0000

За 20 000 шагов и довольно сложную динамику энергия не сдвинулась (−2.8100 → −2.8100), а импульс остался строго нулевым. Импульс сохраняется точно (третий закон Ньютона: силы между телами равны и противоположны, в сумме ноль), энергия — почти точно благодаря симплектическому Верле. Два независимых закона сохранения подтверждают, что симуляция верна.

Устойчивость конфигураций

Не всякая система N тел устойчива. Большинство случайных начальных условий приводят к тому, что одно тело набирает скорость и выбрасывается прочь, а остальные образуют тесную пару. Но есть и удивительно устойчивые решения: точки Лагранжа, орбиты-«восьмёрки» трёх равных масс. Симуляция — единственный способ их найти и проверить на устойчивость.

Как работает под капотом

Сохранение импульса в коде — прямое следствие симметрии: вклад тела j в ускорение i равен по величине и противоположен вкладу i в ускорение j. При суммировании по всем парам эти вклады в полный импульс взаимно уничтожаются. Поэтому импульс сохраняется машинно точно, до последнего бита, независимо от шага dt. Энергия же сохраняется лишь приближённо (зависит от метода и шага) — вот почему её дрейф используют как индикатор качества, а импульс — как индикатор корректности кода.

Частые ошибки

  • Считать силу дважды или забыть пару. Двойной цикл по i и j должен покрыть все упорядоченные пары, кроме i=j.
  • Не смягчать особенность при сближении. При r→0 сила взрывается; малое слагаемое в знаменателе (softening) спасает.
  • Обновлять позиции и скорости вперемешку с расчётом сил. Сначала считают все ускорения, потом обновляют — иначе нарушается синхронность.

Итоги

  • Задача N тел при N≥3 не имеет точного решения — только численно.
  • Прямое суммирование имеет сложность O(N²).
  • Импульс сохраняется точно (третий закон Ньютона), энергия — приближённо.
  • Большинство конфигураций неустойчивы; устойчивые ищут симуляцией.
Проверьте себя
1. Какова вычислительная сложность прямого суммирования сил в задаче N тел?
AO(N)
BO(N log N)
CO(N²)
DO(2^N)
2. Почему импульс системы N тел сохраняется машинно точно?
AИз-за симплектичности интегратора
BСилы между телами равны и противоположны (третий закон Ньютона), их вклады в полный импульс сокращаются
CИз-за малого шага dt
DЭто случайность
3. Почему задача трёх тел требует численного решения?
AСлишком много уравнений для записи
BПри N≥3 точного аналитического решения не существует, система хаотична
CНет подходящих формул сложения
DИз-за округления