Интеграция задачи двух тел в Python

В качестве первого этапа более крупной цели проекта я пытаюсь создать сима с двумя телами на 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% уверен, что проблема связана с интеграцией, но я думал, что начну с этого и пойду оттуда. Это может быть моя функция силы тяжести, неправильное применение дельты времени, проблема с преобразованием единиц измерения или тем, как оно размещается на экране.

Ссылка на Github

Выше приведена ссылка на репо, чтобы все было в порядке, поскольку это может быть что угодно. Скриншоты тестов и т.д.

Теперь можно было бы просто ожидать поведения от Python с потерей точности с числами с плавающей запятой, но я бы нажал X, поскольку это, скорее всего, мои плохие знания в области исчисления.

После тщательного тестирования я получил DT 0,021, что является лучшим результатом для разницы времени. Leapfrog кажется наиболее точным как для подсчета низких, так и для высоких витков.

Кажется, что Эйлер и Верле совершают около 100 витков, где Эйлер немного колеблется, а RK4 кажется нестабильным, поскольку медленно замедляется, и, таким образом, орбиты становятся все больше и больше.

У меня есть преобразование метров в км в разных местах для каждой интеграции, поскольку они не будут работать правильно в зависимости от того, где я это поместил.

Мне пришлось удалить DT из некоторых интеграционных частей, поскольку, если бы я оставил их в объекте, объект просто слегка закрутился бы по спирали и упал бы на центральную массу.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
146
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я не знаю, исправит ли это все, но вам, возможно, стоит обратить внимание на то, как вы назначаете 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()

Две орбиты совпадают:

Хорошо, используя ваши обновления, я получил тот же результат. У меня есть пара вопросов по поводу того, что вы сделали. Почему начальная скорость равна квадрату? Я добавил радиус планеты, чтобы добраться до ее центра (я стремился к высоте 320 км над поверхностью планеты), почему тогда я его убираю в вашем случае?

Nzjeux 07.06.2024 02:38

Начальная скорость не квадратична. (Я случайно оставил его умноженным на коэффициент 2 во время отладки). На самом деле для кругового движения оно должно быть намного больше: установите гравитационное ускорение, равное центростремительному ускорению v^2/r, чтобы получить круговую орбиту. Здесь вам действительно придется следить за преобразованием км! Конечно, можно начать с высоты 320 км над поверхностью планеты, но это не должно входить в последующие уравнения движения.

lastchance 07.06.2024 06:25

извините за задержку, меня не было дома, и до сих пор у меня не было времени по-настоящему изучить вопрос. Почему скорости такие высокие? Используя орбитальный калькулятор на высоте 320 км над поверхностью Земли, мой объект должен двигаться со скоростью 7,72 км/с, но мне нужно сделать скорость 35,29 км/с, чтобы она совпадала. От центра к центру необходимо учитывать радиус планеты, а это не атм, это 6371 км от центра Земли до поверхности, и я добавляю 320 км сверху, но все действует так, как будто орбитальный объект находится всего в 320 км над центром Земли. . Я просто немного запутался, как это совместить

Nzjeux 13.06.2024 00:34

Сила гравитации между сферическими объектами зависит от расстояния между их ЦЕНТРАМИ. В вашем коде «положение» соответствует центру и измеряется в км. Таким образом, начальные «позиции» спутника и Земли должны находиться на расстоянии 6691 единицы друг от друга. На данный момент их разделяет 320 единиц. Если вы хотите построить что-либо из этого, вам придется значительно масштабироваться от физических координат до координат построения.

lastchance 13.06.2024 09:38

Другие вопросы по теме