В качестве первого этапа более крупной цели проекта я пытаюсь создать сима с двумя телами на Python с pygame для отображения объектов на экране.
Моя главная проблема на данный момент заключается в том, что орбитальный спутник вращается вокруг целевой планеты, хотя он должен находиться на стабильной круговой орбите длиной 320 км.
Я сделал четыре разные функции для четырёх разных интеграций. Эйлер, Чехарда, Верлет и RK4. Я ожидаю, что Эйлер и чехарда будут иметь некоторую неточность, но не RK4 или Верлет. Мои знания в области математического анализа ограничены, поэтому мне нужны дополнительные глаза для проверки.
Они заключаются в следующем:
def leapfrog_integration(satellite, planet, dt): #most accurate under 5 orbits
# Update velocity by half-step
satellite.velocity += 0.5 * satellite.acceleration * dt
# Update position
satellite.position += (satellite.velocity / 1000)
# Calculate new acceleration
satellite.acceleration = satellite.acceleration_due_to_gravity(planet)
# Update velocity by another half-step
satellite.velocity += 0.5 * satellite.acceleration * dt
def euler_integration(satellite, planet, dt):
# satellite.accleration = satellite.calculate_gravity(planet)
satellite.acceleration = satellite.acceleration_due_to_gravity(planet)
satellite.velocity += satellite.acceleration * dt
satellite.position += (satellite.velocity / 1000) #convert into kilometers
def verlet_integration(satellite, planet, dt):
acc_c = (satellite.acceleration_due_to_gravity(planet) / 1000)#convert to km/s
satellite.velocity = (satellite.position - satellite.previous_position)
new_pos = 2 * satellite.position - satellite.previous_position + (acc_c * dt)
satellite.previous_position = satellite.position #km
satellite.position = new_pos #km
satellite.velocity = (satellite.position - satellite.previous_position)
def rk4_integration(satellite, planet, dt):# need to resolve the conversion to km for position. If i remove the DT from the kx_r then it's the excat same as Verlet and Euler
def get_acceleration(position, velocity):
temp_mass = PointMass(position,satellite.mass,satellite.radius,satellite.colour,(velocity), np.zeros_like(satellite.acceleration))
return temp_mass.acceleration_due_to_gravity(planet)
k1_v = dt * get_acceleration(satellite.position, (satellite.velocity))
k1_r = (satellite.velocity / 1000)
k2_v = dt * get_acceleration(satellite.position + 0.5 * k1_r, (satellite.velocity) + 0.5 * k1_v)
k2_r = (satellite.velocity + 0.5 * k1_v) / 1000
k3_v = dt * get_acceleration(satellite.position + 0.5 * k2_r, (satellite.velocity) + 0.5 * k2_v)
k3_r = (satellite.velocity + 0.5 * k2_v) / 1000
k4_v = dt * get_acceleration(satellite.position + 0.5 * k3_r, (satellite.velocity) + 0.5 * k3_v)
k4_r = (satellite.velocity + 0.5 * k3_v) / 1000
satellite.position +=(k1_r + 2*k2_r + 2*k3_r + k4_r) / 6
satellite.velocity +=(k1_v + 2*k2_v + 2*k3_v + k4_v) / 6
Чтобы дать вам класс для точечных масс:
class PointMass:
def __init__(self, position, mass, radius, colour, velocity, acceleration):
self.position = np.array(position) #in KM
self.mass = mass #in Kilograms
self.radius = radius #in meters
self.colour = colour
self.velocity = np.array(velocity) #in m/s
self.acceleration = np.array(acceleration) #in m/s per second
self.gForce = None #This is in Newtons
self.previous_position = self.position - (self.velocity / 1000) # Initialize previous position for Verlet integration
def acceleration_due_to_gravity(self,other):
dVector = self.position - other.position # distance vector from self to the other point mass in pixels(km)
distance_km = np.linalg.norm(dVector) #Compute the Euclidean distance in km
distance_m = distance_km * 1000 + other.radius #the distance including the radius to the centre in meters
unit_vector = (dVector / distance_km) #the unit vector for the direction of the force
acceleration_magnitude = -Constants.mu_earth / distance_m**2
return acceleration_magnitude * (unit_vector * 1000) #Return the acceleration vector by multiplying the magnitude with the unit vector(converted to meters)
#the returned acceleration vector is in m/s
При начальных условиях:
planet = PointMass(
position=[600.0,400.0,0.0],
mass=5.9722e24,
radius=6.371e6,
velocity=[0.0,0.0,0.0], #in m/s
acceleration=[0.0,0.0,0.0], #in m/s^2
colour=[125,100,100]
)
satellite = PointMass(
position=[280.0,400.0,0.0],
mass=100,
radius=1,
velocity=[0.0,7718.0,0.0], #need an intial velocity or else it'll jsut fall to the central mass
acceleration=[1.0,0.0,0.0],#added an non-zero acceleration jsut to make sure there's no issues with the integrations.
colour=[255,255,255]
)
Код немного беспорядочный, поскольку я постоянно пробую что-то новое и еще не форматировал его.
Я не на 100% уверен, что проблема связана с интеграцией, но я думал, что начну с этого и пойду оттуда. Это может быть моя функция силы тяжести, неправильное применение дельты времени, проблема с преобразованием единиц измерения или тем, как оно размещается на экране.
Выше приведена ссылка на репо, чтобы все было в порядке, поскольку это может быть что угодно. Скриншоты тестов и т.д.
Теперь можно было бы просто ожидать поведения от Python с потерей точности с числами с плавающей запятой, но я бы нажал X, поскольку это, скорее всего, мои плохие знания в области исчисления.
После тщательного тестирования я получил DT 0,021, что является лучшим результатом для разницы времени. Leapfrog кажется наиболее точным как для подсчета низких, так и для высоких витков.
Кажется, что Эйлер и Верле совершают около 100 витков, где Эйлер немного колеблется, а RK4 кажется нестабильным, поскольку медленно замедляется, и, таким образом, орбиты становятся все больше и больше.
У меня есть преобразование метров в км в разных местах для каждой интеграции, поскольку они не будут работать правильно в зависимости от того, где я это поместил.
Мне пришлось удалить DT из некоторых интеграционных частей, поскольку, если бы я оставил их в объекте, объект просто слегка закрутился бы по спирали и упал бы на центральную массу.






Я не знаю, исправит ли это все, но вам, возможно, стоит обратить внимание на то, как вы назначаете dVector в PointMass.acceleration_due_to_gravity.
У вас это определено как self.position - other.position. Это противоположно тому, что говорится в вашем комментарии; обычно, если вам нужен вектор от self до other, вы берете other.position - self.position (см. эту диаграмму).
Это потому, что если бы вам нужен вектор от self до other, вы бы хотели иметь возможность добраться до other, добавив vector к self. То есть вы хотите:
self + vector = other
Который вы можете использовать свойство вычитания, чтобы переписать как vector = other - self.
Изменение порядка означает, что ваш вектор ускорения будет указывать в противоположном направлении, в котором вы хотите, что может испортить движение ваших точечных масс.
TL;DR: переключите назначение dVector на dVector = other.position - self.position и посмотрите, изменится ли что-нибудь.
В вашем коде много отдельных проблем, некоторые из которых устраняются! Самая большая проблема с точки зрения переполнения стека заключается в том, что вы не минимизировали пример: в вашем коде много постороннего материала, который не имеет отношения к делу.
Ваш RK4 просто неправильный. В обновлениях позиций отсутствуют необходимые dt*. Более незначительным является то, что k4_v должно зависеть только от k3_r и т. д., а не от 0.5*k3_r. Аналогично для k4_r.
Как отметил Эндрю Йим, вектор смещения направлен в направлении, противоположном предполагаемому. Однако эта ошибка как раз и компенсируется ошибками в процедурах ускорения и, собственно, тем, какой объект здесь притягивается (см. return planet.acceleration_due_to_gravity(temp_mass), а не наоборот).
Для сферически-симметричных тел «расстояние» должно быть между центрами и не должно корректироваться по радиусу.
Использовать км для определения местоположения, а м для скорости и ускорения — это настоящий кошмар!
Ваша начальная скорость будет определять, будет ли конечная орбита эллипсом или кругом. То, что у вас есть на данный момент, дает эллипс (это нормально). Если вам нужна правильная скорость для круговой орбиты, сбалансируйте центростремительную силу с гравитационной силой.
Вот сокращенная версия вашего кода в виде одного файла. Он опускает ненужную информацию. Последняя орбита закрыта.
import pygame
import sys
import time
import numpy as np
import math
class Constants:
gravitational_constant = 6.6740e-11
mu_earth = 3.986004418e14 # in m^3 s^-2
class PointMass:
def __init__(self, position, mass, radius, colour, velocity, acceleration):
self.position = np.array(position) # in KM (!OUCH!)
self.mass = mass # in kg
self.radius = radius # in meters (bizarre, given that position is in km!)
self.colour = colour
self.velocity = np.array(velocity) # in m/s
self.acceleration = np.array(acceleration) # in m/s^2
def acceleration_due_to_gravity( self, other ):
dVector = self.position - other.position
distance_km = np.linalg.norm(dVector)
distance_m = distance_km * 1000
unit_vector = (dVector / distance_km)
acceleration_magnitude = Constants.mu_earth / distance_m**2
return acceleration_magnitude * unit_vector
# Initialize Pygame
pygame.init()
window_size = (1200, 800)
window = pygame.display.set_mode(window_size)
pygame.display.set_caption("Two Body Simulation")
font = pygame.font.SysFont(None, 24)
planet = PointMass(
position=[600.0,400.0,0.0],
mass=5.9722e24,
radius=6.371e6,
velocity=[0.0,0.0,0.0],
acceleration=[0.0,0.0,0.0],
colour=[125,100,100]
)
satellite = PointMass(
position=[280.0,400.0,0.0],
mass=100,
radius=1,
velocity=[0.0,7718.0*2,0.0],
acceleration=[0.0,0.0,0.0],
colour=[255,255,255]
)
grid_spacing = 50
# Function to draw the grid
def draw_grid():
for x in range(0, window_size[0], grid_spacing):
pygame.draw.line(window, [255,255,255], (x, 0), (x, window_size[1]))
for y in range(0, window_size[1], grid_spacing):
pygame.draw.line(window, [255,255,255], (0, y), (window_size[0], y))
def rk4_integration(satellite, planet, dt):
def get_acceleration(position, velocity):
temp_mass = PointMass(position,satellite.mass,satellite.radius,satellite.colour,(velocity), np.zeros_like(satellite.acceleration))
return planet.acceleration_due_to_gravity(temp_mass)
k1_v = dt * get_acceleration(satellite.position, (satellite.velocity))
k1_r = dt * (satellite.velocity / 1000)
k2_v = dt * get_acceleration(satellite.position + 0.5 * k1_r, (satellite.velocity) + 0.5 * k1_v)
k2_r = dt * (satellite.velocity + 0.5 * k1_v) / 1000
k3_v = dt * get_acceleration(satellite.position + 0.5 * k2_r, (satellite.velocity) + 0.5 * k2_v)
k3_r = dt * (satellite.velocity + 0.5 * k2_v) / 1000
k4_v = dt * get_acceleration(satellite.position + k3_r, (satellite.velocity) + k3_v)
k4_r = dt * (satellite.velocity + k3_v) / 1000
satellite.position +=(k1_r + 2*k2_r + 2*k3_r + k4_r) / 6
satellite.velocity +=(k1_v + 2*k2_v + 2*k3_v + k4_v) / 6
#actual drawing of objects on screen
circle_radius = 10
c1_pos = (planet.position[0],planet.position[1])
c2_pos = (int(satellite.position[0]), int(satellite.position[1]))
clock = pygame.time.Clock()
fps = 50
positions = []
# Main loop
running = True
while running:
# Event handling
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
dt = clock.tick(fps) / 1000.0
rk4_integration(satellite, planet, dt)
positions.append((int(satellite.position[0]), satellite.position[1]))
#update the position of the drawn object on screen
c1_pos = (planet.position[0],planet.position[1])
c2_pos = (int(satellite.position[0]), int(satellite.position[1]))
#Draw
window.fill((0,0,0))
pygame.draw.circle(window, planet.colour, c1_pos, 25)
pygame.draw.circle(window, satellite.colour, c2_pos, 5)
if len(positions) > 1:
for i in range(len(positions) - 1):
pygame.draw.line(window,(205,205,205),positions[i], positions[i + 1], 1)
draw_grid()
#Refresh display
pygame.display.flip()
clock.tick(fps)
pygame.quit()
sys.exit()
Две орбиты совпадают:
Начальная скорость не квадратична. (Я случайно оставил его умноженным на коэффициент 2 во время отладки). На самом деле для кругового движения оно должно быть намного больше: установите гравитационное ускорение, равное центростремительному ускорению v^2/r, чтобы получить круговую орбиту. Здесь вам действительно придется следить за преобразованием км! Конечно, можно начать с высоты 320 км над поверхностью планеты, но это не должно входить в последующие уравнения движения.
извините за задержку, меня не было дома, и до сих пор у меня не было времени по-настоящему изучить вопрос. Почему скорости такие высокие? Используя орбитальный калькулятор на высоте 320 км над поверхностью Земли, мой объект должен двигаться со скоростью 7,72 км/с, но мне нужно сделать скорость 35,29 км/с, чтобы она совпадала. От центра к центру необходимо учитывать радиус планеты, а это не атм, это 6371 км от центра Земли до поверхности, и я добавляю 320 км сверху, но все действует так, как будто орбитальный объект находится всего в 320 км над центром Земли. . Я просто немного запутался, как это совместить
Сила гравитации между сферическими объектами зависит от расстояния между их ЦЕНТРАМИ. В вашем коде «положение» соответствует центру и измеряется в км. Таким образом, начальные «позиции» спутника и Земли должны находиться на расстоянии 6691 единицы друг от друга. На данный момент их разделяет 320 единиц. Если вы хотите построить что-либо из этого, вам придется значительно масштабироваться от физических координат до координат построения.
Хорошо, используя ваши обновления, я получил тот же результат. У меня есть пара вопросов по поводу того, что вы сделали. Почему начальная скорость равна квадрату? Я добавил радиус планеты, чтобы добраться до ее центра (я стремился к высоте 320 км над поверхностью планеты), почему тогда я его убираю в вашем случае?