Привет, Хабр! Сегодня мы отправимся в увлекательное путешествие по просторам Вселенной, не вставая из-за компьютера. Задумывались ли вы, как планеты удерживаются на своих орбитах, почему галактики не разлетаются в разные стороны, и что заставляет яблоки падать на землю (да-да, снова этот старина Ньютон)? Ответ один – гравитация! Эта невидимая, но всемогущая сила правит бал во Вселенной, от мельчайших пылинок до гигантских скоплений галактик. Мы разберёмся, как из простых законов рождаются сложные и красивые взаимодействия, напишем код, который оживит наши виртуальные миры, и, возможно, даже почувствуем себя немного демиургами, управляющими судьбами звёзд.

Что такое гравитация и с чем её едят?
Итак, прежде чем мы начнём кодить наш симулятор, давайте немного освежим в памяти, что же такое гравитация. Если отбросить сложные формулы и заумные определения, то гравитация – это фундаментальное взаимодействие, которое заставляет любые два объекта, обладающие массой, притягиваться друг к другу. Да-да, не только Земля притягивает яблоко, но и яблоко (пусть и очень-очень слабо) притягивает Землю!
Впервые математически описал этот закон Исаак Ньютон в своём знаменитом труде "Математические начала натуральной философии". Его закон всемирного тяготения гласит, что сила притяжения F между двумя телами пропорциональна произведению их масс (m1 и m2) и обратно пропорциональна квадрату расстояния (r) между ними:
Где G – это гравитационная постоянная, эдакий коэффициент пропорциональности, который определяет силу гравитационного взаимодействия во Вселенной. Звучит просто, не так ли? Но именно этот, на первый взгляд, незамысловатый закон управляет движением планет, формированием звёзд и галактик, и даже такими экзотическими объектами, как чёрные дыры.
Конечно, позже Альберт Эйнштейн в своей Общей теории относительности (ОТО) предложил более глубокое понимание гравитации, описав её не как силу, а как проявление искривления пространства-времени массивными телами. Представьте себе натянутую простыню, на которую положили тяжёлый шар – он создаст углубление, и любой другой, более лёгкий шарик, катящийся мимо, будет отклоняться к нему, как будто притягиваясь. Это очень упрощённая аналогия, но она помогает понять суть ОТО. Для наших целей, симуляции движения планет или звёзд в "обычных" условиях, ньютоновской гравитации будет более чем достаточно. Она проще в расчётах и даёт очень точные результаты для большинства астрофизических задач, не связанных с экстремальными гравитационными полями или скоростями, близкими к скорости света.
От теории к симуляции: как заставить компьютеры считать гравитацию?
Хорошо, с теорией разобрались. Но как заставить компьютер симулировать это взаимодействие для множества тел? Ведь в реальных системах, будь то Солнечная система или целая галактика, тел может быть очень много, и каждое из них взаимодействует с каждым другим! Это так называемая "задача N тел" (N-body problem).
Если тел всего два (например, Земля и Солнце, если пренебречь влиянием других планет), то задача решается аналитически, то есть можно вывести точные формулы, описывающие их движение. Но как только тел становится три и более, аналитического решения в общем виде уже не существует (за редкими исключениями). Тут-то на помощь и приходят численные методы и компьютерное моделирование.
Основная идея численного моделирования гравитационного взаимодействия заключается в следующем:
Задаём начальные условия: для каждого тела (частицы) в нашей системе мы определяем его массу, начальное положение (координаты x, y, z) и начальную скорость (компоненты vx, vy, vz).
Рассчитываем силы: на каждом шаге симуляции для каждой частицы мы вычисляем суммарную гравитационную силу, действующую на неё со стороны всех остальных частиц в системе. Для этого мы используем закон всемирного тяготения Ньютона.
Вычисляем ускорение: зная силу и массу частицы (второй закон Ньютона, F=ma), мы находим её ускорение (a = F/m).
Обновляем скорость и положение: используя вычисленное ускорение и небольшой временной шаг (dt), мы обновляем скорость частицы, а затем, зная новую скорость, обновляем её положение. Существуют различные численные методы для этого шага (например, метод Эйлера, метод Верле, методы Рунге-Кутты), которые отличаются точностью и вычислительной сложностью.
Повторяем: шаги 2-4 повторяются многократно, продвигая систему во времени и позволяя нам наблюдать за эволюцией её состояния.
Звучит довольно прямолинейно, но дьявол, как всегда, в деталях. Основная вычислительная сложность кроется в шаге 2 – расчёте сил. Если у нас N частиц, то для каждой из них нужно рассчитать N-1 взаимодействие. Итого, на каждом шаге нам нужно выполнить порядка N^2 операций. Это называется прямым суммированием. Для небольшого количества тел (десятки, сотни) это вполне приемлемо. Но что если мы хотим смоделировать галактику, состоящую из миллионов или миллиардов звёзд? N^2 становится астрономически большим числом, и прямой расчёт займёт вечность даже на самых мощных суперкомпьютерах.
Ускоряем расчёты
Именно здесь на сцену выходят более продвинутые и хитрые алгоритмы, которые позволяют значительно сократить количество вычислений без существенной потери точности. Два наиболее известных из них – это метод Барнса-Хата (Barnes-Hut) и метод Particle Mesh (PM).
Метод Барнса-Хата (Barnes-Hut):
Идея этого метода гениальна в своей простоте. Вместо того чтобы рассчитывать взаимодействие каждой частицы с каждой другой по отдельности, мы группируем далёкие частицы и рассматриваем их как один большой объект с суммарной массой, расположенный в их центре масс.
Представьте, что вы смотрите на далёкое скопление звёзд. Вы не видите каждую звезду по отдельности, а воспринимаете их как единое светящееся пятно. Метод Барнса-Хата делает нечто похожее: он рекурсивно делит всё пространство симуляции на ячейки (обычно с помощью дерева октантов в 3D или квадродерева в 2D). Если ячейка находится достаточно далеко от рассматриваемой частицы и её размер достаточно мал по сравнению с расстоянием до неё, то все частицы внутри этой ячейки аппроксимируются одной суперчастицей. Если же ячейка близко или слишком велика, мы продолжаем деление, пока не дойдём до отдельных частиц или достаточно мелких ячеек.
Этот подход позволяет сократить количество вычислений с O(N^2) до O(N log N), что является огромным выигрышем для больших N. Конечно, это вносит некоторую погрешность, но для многих космологических симуляций она вполне допустима.
Метод Particle Mesh (PM):
Этот метод использует совершенно другой подход. Вместо прямого расчёта сил между частицами, он сначала распределяет массу частиц по узлам регулярной сетки (mesh), наложенной на пространство симуляции. Затем, используя эту сеточную плотность, он решает уравнение Пуассона (которое связывает гравитационный потенциал с распределением масс) с помощью быстрых алгоритмов, таких как Быстрое Преобразование Фурье (БПФ, FFT). Получив гравитационный потенциал на сетке, можно вычислить силу гравитации в каждом узле сетки. Наконец, сила, действующая на каждую частицу, интерполируется из значений в ближайших узлах сетки.
Метод PM очень эффективен для моделирования крупномасштабной структуры Вселенной, где важны дальнодействующие гравитационные силы. Его сложность составляет примерно O(N log N) или даже O(N), если N сопоставимо с количеством узлов сетки. Однако он хуже справляется с моделированием плотных областей и близких взаимодействий частиц, так как разрешение ограничено размером ячейки сетки. Часто его комбинируют с другими методами (например, Particle-Particle Particle-Mesh, или P3M), где дальнодействующие силы считаются через сетку, а близкодействующие – прямым суммированием или методом Барнса-Хата.
Для нашей сегодняшней статьи мы сосредоточимся на прямом суммировании, так как оно наиболее просто для понимания и реализации, и отлично подходит для симуляции небольших систем, таких как наша Солнечная система. Но знание о существовании более продвинутых методов, таких как Барнс-Хат и PM, важно для понимания того, как учёные моделируют действительно большие космические структуры.
Кодим гравитацию

Ну что ж, друзья, теория – это, конечно, хорошо, но пора переходить к практике! Как говорится, меньше слов – больше кода. Сейчас мы с вами напишем тот самый симулятор гравитации на Python, который позволит нам воочию увидеть, как планеты кружатся вокруг Солнца, и даже поэкспериментировать с собственными звёздными системами. Готовы почувствовать себя немного программистом-астрофизиком? Тогда поехали!
Для начала, нам понадобятся несколько библиотек: numpy
для эффективной работы с массивами (ведь у нас будут координаты, скорости, силы – всё это удобно представлять в виде векторов и матриц) и matplotlib
для визуализации наших космических танцев. Если у вас их ещё нет, то самое время установить:
pip install numpy matplotlib
А теперь – сам код! Я буду приводить его по частям и подробно комментировать каждый шаг, чтобы вы не только скопировали его, но и поняли, что там происходит под капотом.
Шаг 1: Определяем класс для наших частиц (планет, звёзд и т.д.)
Давайте создадим класс Particle
, который будет представлять собой отдельный объект в нашей симуляции. У каждой частицы будут свои свойства: масса, текущие координаты, текущая скорость, имя (для красоты) и цвет (для наглядности на графике). А ещё мы будем хранить историю её перемещений (путь), чтобы потом красиво отрисовать траекторию.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
# Константы
G = 6.67430e-11 # Гравитационная постоянная, м^3 кг^-1 с^-2
DT = 3600 * 24 # Шаг по времени (1 день в секундах)
class Particle:
def __init__(self, mass, x, y, vx, vy, name="Particle", color=\'blue\'):
self.mass = float(mass) # Масса частицы в кг
self.pos = np.array([float(x), float(y)], dtype=float) # Положение [x, y] в метрах
self.vel = np.array([float(vx), float(vy)], dtype=float) # Скорость [vx, vy] в м/с
self.name = name
self.color = color
self.path = [self.pos.copy()] # Список для хранения траектории
self.acc = np.zeros(2, dtype=float) # Текущее ускорение, инициализируем нулями
def update_force(self, force):
# Обновляем ускорение на основе силы (F=ma => a=F/m)
self.acc = force / self.mass
def update_velocity(self, dt):
# Обновляем скорость на основе ускорения и шага времени (v = v0 + a*dt)
self.vel += self.acc * dt
def update_position(self, dt):
# Обновляем положение на основе скорости и шага времени (x = x0 + v*dt)
self.pos += self.vel * dt
self.path.append(self.pos.copy()) # Добавляем новую точку в траекторию
Что тут у нас?
В
init
мы инициализируем все основные параметры частицы. Обратите внимание, что координаты и скорости – этоnumpy
массивы, что упростит нам векторные вычисления.update_force
просто вычисляет ускорение на основе приложенной силы.update_velocity
иupdate_position
– это простейшая реализация метода Эйлера для численного интегрирования. Мы предполагаем, что на небольшом временном шагеdt
ускорение постоянно, и на основе этого обновляем скорость, а затем, используя новую скорость, обновляем положение. Для более точных симуляций можно использовать более сложные методы (например, Верле или Рунге-Кутты), но для наглядности Эйлер нам вполне подойдёт.Мы также определили гравитационную постоянную
G
и временной шагDT
.DT
здесь равен одному дню в секундах – это значит, что на каждом шаге наша симуляция будет продвигаться вперёд на один день.
Шаг 2: Функция для расчёта гравитационной силы
Теперь напишем функцию, которая будет считать ту самую силу притяжения между двумя частицами по закону Ньютона.
def calculate_gravitational_force(p1, p2):
# Вектор, направленный от p1 к p2
r_vec = p2.pos - p1.pos
# Расстояние между частицами
r_mag = np.linalg.norm(r_vec)
# Избегаем деления на ноль, если частицы оказались в одной точке
if r_mag == 0:
return np.zeros(2) # Возвращаем нулевую силу
# Единичный вектор, направленный от p1 к p2
r_hat = r_vec / r_mag
# "Смягчение" для предотвращения слишком больших сил на малых расстояниях
# Это помогает стабилизировать симуляцию при близких прохождениях
# и избежать "улетания" частиц из-за огромных ускорений.
# Значение softening_factor можно подбирать.
softening_factor = 1e9 # Для масштабов Солнечной системы
# Величина силы по закону Ньютона, с учётом смягчения
force_mag = (G * p1.mass * p2.mass) / (r_mag**2 + softening_factor**2)
# Вектор силы, действующей на p1 со стороны p2
force_vec = force_mag * r_hat
return force_vec
Здесь всё по формуле: считаем вектор расстояния r_vec
, его модуль r_mag
, затем единичный вектор r_hat
. Важный момент – softening_factor
. Зачем он нужен? При очень близком сближении частиц знаменатель r_mag^2
становится очень маленьким, а сила – огромной. Это может привести к нереалистично большим ускорениям и
вылету частиц из симуляции. "Смягчение" добавляет небольшую величину к квадрату расстояния, не позволяя знаменателю стать слишком маленьким. Это распространённый трюк в N-body симуляциях.
Шаг 3: Задаём начальные условия – наша Солнечная система (в миниатюре)
Теперь самое интересное – создадим объекты для нашей симуляции. Возьмём упрощённые данные для Солнца и нескольких планет. Массы указаны в килограммах, расстояния – в метрах от Солнца (по оси X, для простоты), скорости – в м/с (начальная скорость по оси Y, чтобы они начали двигаться по орбите).
# --- Начальные условия для симуляции Солнечной системы (упрощенно) ---
# Массы в кг, расстояния в метрах, скорости в м/с
# Солнце (почти неподвижно в центре, но для общности зададим ему нулевую скорость)
sun = Particle(mass=1.989e30, x=0, y=0, vx=0, vy=0, name="Sun", color=\'yellow\')
# Земля
earth_mass = 5.972e24
earth_distance = 1.496e11 # Среднее расстояние от Земли до Солнца (1 астрономическая единица)
earth_velocity = 29780 # Средняя орбитальная скорость Земли
earth = Particle(earth_mass, earth_distance, 0, 0, earth_velocity, name="Earth", color=\'blue\')
# Марс
mars_mass = 0.64171e24
mars_distance = 2.279e11 # Среднее расстояние от Марса до Солнца
mars_velocity = 24070 # Средняя орбитальная скорость Марса
mars = Particle(mars_mass, mars_distance, 0, 0, mars_velocity, name="Mars", color=\'red\')
# Венера
venus_mass = 4.867e24
venus_distance = 1.082e11 # Среднее расстояние от Венеры до Солнца
venus_velocity = 35020 # Средняя орбитальная скорость Венеры
venus = Particle(venus_mass, venus_distance, 0, 0, venus_velocity, name="Venus", color=\'orange\')
# Меркурий
mercury_mass = 0.330e24 # Масса Меркурия
mercury_distance = 0.579e11 # Среднее расстояние от Меркурия до Солнца
mercury_velocity = 47360 # Средняя орбитальная скорость Меркурия
mercury = Particle(mercury_mass, mercury_distance, 0, 0, mercury_velocity, name="Mercury", color=\'gray\')
# Собираем все наши частицы в один список
particles = [sun, earth, mars, venus, mercury]
Конечно, это очень упрощённая модель: мы не учитываем эллиптичность орбит, их наклонения, взаимодействие планет друг с другом на начальном этапе (хотя в цикле симуляции оно будет!). Но для демонстрации принципа – самое то!
Шаг 4: Основной цикл симуляции и магия анимации
А вот и сердце нашего симулятора – цикл, в котором будут происходить все расчёты и обновления, и код для анимации с помощью matplotlib.animation.FuncAnimation
. Эта функция позволяет создавать "живые" графики, обновляя их на каждом кадре.
# --- Основной цикл симуляции и визуализация ---
fig, ax = plt.subplots() # Создаём фигуру и оси для графика
ax.set_aspect(\'equal\') # Делаем масштабы по осям одинаковыми, чтобы круги были кругами
ax.set_xlabel("X (m)")
ax.set_ylabel("Y (m)")
ax.set_title("Симуляция гравитации (Солнечная система)")
# Динамически настраиваем пределы отображения, чтобы все поместилось
# Находим максимальное начальное расстояние от Солнца
max_initial_dist = 0
if len(particles) > 1:
max_initial_dist = max(np.linalg.norm(p.pos) for p in particles if p.name != "Sun")
if max_initial_dist == 0: # Если только Солнце или все в центре
max_initial_dist = 1e11 # Некое значение по умолчанию
ax.set_xlim(-max_initial_dist * 1.5, max_initial_dist * 1.5)
ax.set_ylim(-max_initial_dist * 1.5, max_initial_dist * 1.5)
# Создаём объекты для отображения частиц (точки) и их траекторий (линии)
scatter_plots = [ax.plot([], [], \'o\', markersize=10 if p.name == "Sun" else 5, color=p.color, label=p.name)[0] for p in particles]
path_plots = [ax.plot([], [], \'-\\, color=p.color, alpha=0.5)[0] for p in particles]
ax.legend() # Показываем легенду (имена планет)
ax.grid(True) # Включаем сетку для наглядности
# Функция инициализации для анимации (что нарисовать в самом начале)
def init():
for plot in scatter_plots:
plot.set_data([], [])
for plot in path_plots:
plot.set_data([], [])
return scatter_plots + path_plots
# Функция, которая будет вызываться для каждого кадра анимации
def animate(frame_num):
# 1. Рассчитываем силы, действующие на каждую частицу
forces = {p.name: np.zeros(2, dtype=float) for p in particles} # Словарь для хранения сил
for i in range(len(particles)):
for j in range(i + 1, len(particles)):
p1 = particles[i]
p2 = particles[j]
force_on_p1 = calculate_gravitational_force(p1, p2)
forces[p1.name] += force_on_p1
forces[p2.name] -= force_on_p1 # Сила действия равна силе противодействия
# 2. Обновляем состояние каждой частицы
for p in particles:
p.update_force(forces[p.name])
p.update_velocity(DT)
p.update_position(DT)
# 3. Обновляем данные на графике
for idx, p in enumerate(particles):
scatter_plots[idx].set_data(p.pos[0], p.pos[1]) # Новое положение точки
path_data = np.array(p.path).T # Преобразуем список точек пути в удобный формат
path_plots[idx].set_data(path_data[0], path_data[1]) # Рисуем траекторию
# Динамически обновляем заголовок, чтобы видеть, какой день симуляции
ax.set_title(f"Симуляция гравитации (Солнечная система) - День: {frame_num}")
return scatter_plots + path_plots
# Количество кадров анимации (сколько дней будем симулировать)
num_frames = 365 * 2 # Симулируем два земных года
# Создаём и запускаем анимацию!
# fig - наша фигура
# animate - функция, вызываемая на каждом кадре
# frames - количество кадров
# init_func - функция инициализации
# blit=True - оптимизация отрисовки (перерисовываются только изменившиеся элементы)
# interval - задержка между кадрами в миллисекундах
ani = animation.FuncAnimation(fig, animate, frames=num_frames, init_func=init, blit=True, interval=20)
# Показываем окно с анимацией
plt.show()
print("Симуляция завершена.")
Разберём ключевые моменты в animate(frame_num)
:
Расчёт сил: Мы проходимся по всем уникальным парам частиц (чтобы не считать силу дважды и не считать силу частицы на саму себя). Для каждой пары вызываем нашу
calculate_gravitational_force
. Сила, действующая на первую частицу, равна по модулю и противоположна по направлению силе, действующей на вторую (третий закон Ньютона). Эти силы накапливаются для каждой частицы.Обновление состояния: Для каждой частицы, зная суммарную силу, обновляем её ускорение, затем скорость и, наконец, положение. Это происходит с помощью методов нашего класса
Particle
.Обновление графики: Для каждой частицы мы обновляем положение её точки на графике (
scatter_plots
) и перерисовываем её траекторию (path_plots
), используя накопленные вp.path
координаты.
Функция FuncAnimation
берёт на себя всю рутину по вызову animate
нужное количество раз (num_frames
) и отображению результата.
И вуаля! Если вы всё сделали правильно, после запуска скрипта (python имя_
файла.py
) должно появиться окно, в котором вы увидите, как наши планеты начинают своё грациозное движение вокруг Солнца. Вы увидите, как Земля и другие планеты рисуют свои орбиты. Магия!
Что дальше? Поле для экспериментов!
Этот код – лишь отправная точка. Вот несколько идей, как его можно улучшить или с чем поэкспериментировать:
Добавить больше планет: Юпитер, Сатурн с их огромными массами внесут заметные изменения!
Трёхмерная симуляция: Сейчас у нас всё в 2D. Попробуйте расширить до 3D (добавить z-координату, изменить расчёт силы и визуализацию).
Более точные методы интегрирования: Замените метод Эйлера на метод Верле или Рунге-Кутты 4-го порядка. Сравните результаты и производительность.
Визуализация энергии: Рассчитывайте и отображайте полную кинетическую, потенциальную и суммарную энергию системы. В идеальной замкнутой системе полная энергия должна сохраняться (с учётом погрешностей численного метода).
Как видите, простор для творчества огромен. Главное – не бояться экспериментировать и углубляться в детали.