N-тіла та гравітація: від O(N²) до Barnes-Hut
Закон тяжіння Ньютона, softening-параметр — і раптом 100 000 тіл зависають браузер. Дізнайтесь, як Oct-Tree Barnes-Hut знижує складність до O(N log N), а Leapfrog-інтегратор зберігає стабільні орбіти без накопичення помилок.
Закон тяжіння Ньютона
Гравітаційна сила між двома матеріальними точками масами m₁ та m₂, розділеними відстанню r, описується формулою Ньютона:
G ≈ 6.674 × 10⁻¹¹ Н·м²/кг² — гравітаційна стала
У симуляціях часто використовують безрозмірні одиниці або нормалізують G = 1, щоб уникнути проблем із числовою точністю float32. Прискорення тіла i від решти N–1 тіл:
Навіть проста сумація вимагає O(N²) обчислень за крок — для 10 000 тіл це вже 10⁸ операцій, що виводить за межі реального часу.
Softening: уникнення сингулярності
Якщо два тіла близько зближуються, знаменник r³ підходить до нуля, і прискорення стає нескінченним — числова катастрофа. Рішення: додати softening-параметр ε (зазвичай позначають ε або ε²):
Фізичний сенс: тіла поводяться як м'які об'єкти зі «скругленим» потенціалом. Типові значення ε — від 0.01 до 1.0 у безрозмірних одиницях, залежно від масштабу задачі.
Наїв 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⁹ | Недосяжно ✗ |
Leapfrog-інтегратор
Для N-body задач симплектичний Leapfrog (Störmer-Verlet DKD) є стандартом: він зберігає енергію набагато краще за метод Ейлера і дешевший за RK4.
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 точно зберігає симплектичну структуру фазового простору. Це значить, що орбіти не розкручуються і не спіралізуються навіть після мільйонів кроків.
Oct-Tree і Barnes-Hut
Алгоритм Barnes-Hut (1986) — ключова ідея: далекі тіла можна апроксимувати одним центром мас, а не підсумовувати кожне окремо.
Побудова Oct-Tree
3D простір рекурсивно ділиться на 8 октантів доти, доки кожна клітина містить не більше одного тіла. Одночасно в кожній внутрішній ноді зберігають:
- Загальну масу M всіх тіл у клітині
- Позицію центру мас r̄
- Розмір клітини s (довжина ребра куба)
Побудова Oct-Tree для N тіл: O(N log N)
Обхід дерева і критерій θ
Для кожного тіла i дерево обходять рекурсивно. Для поточної ноди з розміром s і відстанню d від тіла перевіряють умову:
інакше → рекурсивно обійти 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-інтегратор, тисячі частинок, формування галактик у реальному часі