Астрофізика · Алгоритми
📅 Березень 2026 ⏱ ≈ 13 хв читання 🎯 Середньо-Складно

N-тіла та гравітація: від O(N²) до Barnes-Hut

Закон тяжіння Ньютона, softening-параметр — і раптом 100 000 тіл зависають браузер. Дізнайтесь, як Oct-Tree Barnes-Hut знижує складність до O(N log N), а Leapfrog-інтегратор зберігає стабільні орбіти без накопичення помилок.

Закон тяжіння Ньютона

Гравітаційна сила між двома матеріальними точками масами m₁ та m₂, розділеними відстанню r, описується формулою Ньютона:

F = G · m₁ · m₂ / r²
G ≈ 6.674 × 10⁻¹¹ Н·м²/кг² — гравітаційна стала

У симуляціях часто використовують безрозмірні одиниці або нормалізують G = 1, щоб уникнути проблем із числовою точністю float32. Прискорення тіла i від решти N–1 тіл:

aᵢ = Σⱼ≠ᵢ G · mⱼ · (rⱼ − rᵢ) / |rⱼ − rᵢ|³

Навіть проста сумація вимагає O(N²) обчислень за крок — для 10 000 тіл це вже 10⁸ операцій, що виводить за межі реального часу.

Softening: уникнення сингулярності

Якщо два тіла близько зближуються, знаменник підходить до нуля, і прискорення стає нескінченним — числова катастрофа. Рішення: додати softening-параметр ε (зазвичай позначають ε або ε²):

aᵢ = Σⱼ≠ᵢ G · mⱼ · (rⱼ − rᵢ) / (|rⱼ − rᵢ|² + ε²)^(3/2)

Фізичний сенс: тіла поводяться як м'які об'єкти зі «скругленим» потенціалом. Типові значення ε — від 0.01 до 1.0 у безрозмірних одиницях, залежно від масштабу задачі.

Вибір ε: занадто велике — гравітація ослаблена, тіла не утворюють щільні скупчення. Занадто мале — часті близькі зближення повертають чисельну нестабільність. Для галактичних симуляцій зазвичай ε ≈ 0.01–0.1 від середньої відстані між тілами.

Наїв O(N²) алгоритм

Базовий алгоритм — подвійний цикл: для кожної пари (i, j) обчислити силу і додати до вектора прискорення обох тіл. Завдяки 3-му закону Ньютона (actio = reactio) достатньо перебирати лише унікальні пари j > i:

N тіл Пар Кроків/сек @60fps Стан
100 4 950 ≈ 297 000 Легко ✓
1 000 499 500 ≈ 30 000 000Повільно ⚠️
10 000 49 995 000 ≈ 3×10⁹ Недосяжно ✗
Оптимізація O(N²): навіть у наївному алгоритмі виграш дає кешування відстані, SIMD-операції (Float32Array) та відмова від Math.sqrt там, де достатньо r² (порівняння). Але асимптотичну складність це не зменшує.

Leapfrog-інтегратор

Для N-body задач симплектичний Leapfrog (Störmer-Verlet DKD) є стандартом: він зберігає енергію набагато краще за метод Ейлера і дешевший за RK4.

Kick: v(t + Δt/2) = v(t) + a(t) · Δt/2
Drift: x(t + Δt) = x(t) + v(t + Δt/2) · Δt
Force: обчислити a(t + Δt) на нових позиціях
Kick: v(t + Δt) = v(t + Δt/2) + a(t + Δt) · Δt/2

Ключова властивість: симплектичність — Leapfrog точно зберігає симплектичну структуру фазового простору. Це значить, що орбіти не розкручуються і не спіралізуються навіть після мільйонів кроків.

Порівняння з RK4: Leapfrog — 2 оцінки сили (або 1 якщо "kick" ділять), RK4 — 4. Але для довгих симуляцій Leapfrog виграє через симплектичність, тоді як RK4 поступово "дрейфує" у фазовому просторі.

Oct-Tree і Barnes-Hut

Алгоритм Barnes-Hut (1986) — ключова ідея: далекі тіла можна апроксимувати одним центром мас, а не підсумовувати кожне окремо.

Побудова Oct-Tree

3D простір рекурсивно ділиться на 8 октантів доти, доки кожна клітина містить не більше одного тіла. Одночасно в кожній внутрішній ноді зберігають:

r̄ = Σᵢ mᵢ · rᵢ / M

Побудова Oct-Tree для N тіл: O(N log N)

Обхід дерева і критерій θ

Для кожного тіла i дерево обходять рекурсивно. Для поточної ноди з розміром s і відстанню d від тіла перевіряють умову:

якщо s / d < θ → апроксимувати ноду як одне тіло з масою M та координатою r̄
інакше → рекурсивно обійти 8 дочірніх нод

Де θ ≈ 0.5–1.0 — параметр точності. При θ = 0 отримуємо точний O(N²) алгоритм. При θ = 1.0 — максимальна швидкість, але помітна похибка в щільних регіонах.

Критерій θ та точність

θ Складність Відносна похибка сили Типове використання
0.0 O(N²) 0% (точно) Верифікація
0.3 ~O(N^1.2) < 0.1% Наукові розрахунки
0.5 ~O(N^1.1) ~0.5% Стандарт (космологія)
1.0 ≈O(N log N)~5% Реальний час, ігри

Для браузерної симуляції в реальному часі θ = 0.7–1.0 дає непомітну для ока похибку й достатню продуктивність. Наукові гідродинамічні коди зазвичай використовують θ = 0.5–0.7.

Складність і бенчмарки

N тіл Direct O(N²) Barnes-Hut O(N log N) Прискорення ×
1 000 ~0.5 мс ~0.1 мс ×5
10 000 ~50 мс ~1.5 мс ×33
100 000 ~5 000 мс~20 мс ×250

Для 100 000 тіл Barnes-Hut дозволяє утримувати 50+ FPS, тоді як O(N²) займає 5 секунд на крок — повний гальм. Побудова дерева додає ~O(N log N) накладних витрат, але вони окупаються вже від ~500 тіл.

Реалізація у JavaScript

Наїв O(N²) з Float32Array

// Float32Array для кешування у CPU:
const pos = new Float32Array(N * 3);   // x, y, z
const vel = new Float32Array(N * 3);
const acc = new Float32Array(N * 3);
const mass = new Float32Array(N);

function computeForces() {
  acc.fill(0);
  const eps2 = EPS * EPS;
  for (let i = 0; i < N; i++) {
    const ix = i * 3, iy = ix + 1, iz = ix + 2;
    for (let j = i + 1; j < N; j++) {
      const jx = j * 3, jy = jx + 1, jz = jx + 2;
      const dx = pos[jx] - pos[ix];
      const dy = pos[jy] - pos[iy];
      const dz = pos[jz] - pos[iz];
      const r2 = dx*dx + dy*dy + dz*dz + eps2;
      const inv = G * mass[j] / (r2 * Math.sqrt(r2));  // 1/r³
      acc[ix] += dx * inv;  acc[iy] += dy * inv;  acc[iz] += dz * inv;
      // Newton 3: сила j від i — протилежна
      const invI = G * mass[i] / (r2 * Math.sqrt(r2));
      acc[jx] -= dx * invI; acc[jy] -= dy * invI; acc[jz] -= dz * invI;
    }
  }
}

function leapfrogStep(dt) {
  const hdt = dt * 0.5;
  // Kick: v += a * dt/2
  for (let k = 0; k < N * 3; k++) vel[k] += acc[k] * hdt;
  // Drift: x += v * dt
  for (let k = 0; k < N * 3; k++) pos[k] += vel[k] * dt;
  // Нові сили на нових позиціях
  computeForces();
  // Kick: v += a * dt/2
  for (let k = 0; k < N * 3; k++) vel[k] += acc[k] * hdt;
}

Oct-Tree нода

class OctNode {
  constructor(cx, cy, cz, halfSize) {
    this.cx = cx; this.cy = cy; this.cz = cz;  // центр куба
    this.halfSize = halfSize;
    this.mass = 0;
    this.comX = 0; this.comY = 0; this.comZ = 0;   // центр мас
    this.bodyIdx = -1;    // -1 = внутрішня нода
    this.children = null; // null або Array(8)
  }

  insert(i) {
    if (this.mass === 0) {          // порожня нода
      this.bodyIdx = i;
      this._updateCOM(i);
      return;
    }
    if (this.children === null) {  // листова нода з 1 тілом
      this._subdivide();
      this._insertIn(this.bodyIdx); // перемістити старе тіло
      this.bodyIdx = -1;
    }
    this._insertIn(i);
    this._updateCOM(i);
  }

  calcAcc(i, ax, ay, az, theta) {
    if (this.mass === 0 || this.bodyIdx === i) return;
    const dx = this.comX - pos[i*3];
    const dy = this.comY - pos[i*3+1];
    const dz = this.comZ - pos[i*3+2];
    const d2 = dx*dx + dy*dy + dz*dz + EPS*EPS;
    // Критерій Barnes-Hut: s/d < θ
    if ((this.bodyIdx >= 0) || (4 * this.halfSize * this.halfSize) / d2 < theta * theta) {
      const inv = G * this.mass / (d2 * Math.sqrt(d2));
      ax[i] += dx * inv;
      ay[i] += dy * inv;
      az[i] += dz * inv;
    } else {
      for (const child of this.children) {
        if (child) child.calcAcc(i, ax, ay, az, theta);
      }
    }
  }
}

Розширення та GPU

WebGPU та compute shaders

Наступний крок після Barnes-Hut — перенесення обчислень на GPU через WebGPU Compute Shaders. Пряме O(N²) на GPU (тисячі ядер) обганяє CPU Barnes-Hut до N ≈ 50 000. Вище — більш складні GPU-BVH або FMM алгоритми.

Fast Multipole Method (FMM)

FMM — наступник Barnes-Hut із справжньою O(N) складністю. Замість однієї апроксимації (маса + COM), кожна нода зберігає мультипольний розклад, що дозволяє точніше і швидше оцінювати далекі взаємодії. Однак реалізація значно складніша.

Adaptive time stepping

Тіла в щільних скупченнях потребують малого dt, тоді як далекі — великого. Block-based адаптивний крок часу, популярний у Gadget-2 та аналогах, може ще у 10–50 разів пришвидшити симуляцію без втрати точності в ключових регіонах.

🪐 Запустити N-Body симуляцію

Leapfrog-інтегратор, тисячі частинок, формування галактик у реальному часі

Відкрити симуляцію →