Panel logo

KvaziSSM - Расчётная модель Солнечной системы

О проекте

KvaziSSM — это программная расчётная модель для моделирования и визуализации гравитационных систем, таких как Солнечная система. Проект предоставляет широкие возможности для экспериментов с различными сценариями: добавление новых объектов, изменение масс, радиусов, начальных координат и скоростей. Это позволяет исследовать влияние дополнительных тел на устойчивость системы и анализировать динамику гравитационных взаимодействий в трехмерном пространстве.

Основой для изучения этих процессов служит закон всемирного тяготения Ньютона и метод численного интегрирования (метод интегрирования в предположении постоянного ускорения на шаге). Проект разрабатывался в нескольких версиях:
v0.0 — Прототип с интерфейсом на Tkinter и 2D-визуализацией с помощью PyGame.
v0.1 — Усовершенствованная версия с интерфейсом на PyQt6 и полноценной 3D-визуализацией средствами VTK.

Ключевые особенности архитектуры

Скриншоты интерфейса и визуализации

Анализ точности вычислений (на 12.04.2031)

Ниже представлено сравнение данных, рассчитанных программой KvaziSSM (спустя 10 лет симуляции с шагом 900 с), с эталонными данными Nasa JPL Horizons System (NJHS). Данные приведены в метрах и м/с в гелиоцентрической эклиптической декартовой системе координат J2000.

Таблица 1. Сравнение абсолютных значений координат и скоростей

Объект / Источник X Y Z Vx Vy Vz
Меркурий NJHS-5.21E+10-4.05E+101.44E+092.01E+04-3.62E+04-4.80E+03
Меркурий рассч.-8.16E-025.26E+103.72E+09-5.49E+047.25E+035.62E+03
Венера NJHS-5.99E+108.95E+104.68E+09-2.92E+04-1.97E+041.42E+03
Венера рассч.-1.49E+112.20E+10-4.01E+08-4.79E+03-2.94E+041.40E+02
Земля NJHS-1.40E+11-5.47E+101.57E+071.04E+04-2.78E+042.06E+00
Земля рассч.-1.49E+112.20E+10-4.01E+08-4.79E+03-2.94E+041.40E+02
Марс NJHS-2.01E+11-1.28E+112.25E+091.39E+04-1.84E+04-7.26E+02
Марс рассч.-2.12E+11-1.14E+112.79E+091.24E+04-1.92E+04-7.07E+02
Юпитер NJHS-1.58E+11-7.77E+116.78E+091.26E+04-2.00E+03-2.75E+02
Юпитер рассч.-1.58E+11-7.77E+116.78E+091.26E+04-2.00E+03-2.75E+02
Сатурн NJHS4.62E+111.27E+12-4.05E+10-9.60E+033.28E+033.25E+02
Сатурн рассч.4.62E+111.27E+12-4.05E+10-9.60E+033.28E+033.25E+02

Таблица 2. Отклонение данных (Дельта)

Вывод: Расчёт положений газовых гигантов очень точен. Однако для планет земной группы наблюдаются отклонения из-за их высокой скорости орбитального вращения и малого шага интегрирования. Требуется оптимизация метода.

Объект Δ X Δ Y Δ Z Δ Vx Δ Vy Δ Vz
Меркурий1525519.86248164.38206993.540.01470.13840.0007
Венера5209120553493021554617227569026074945.2443411.4010420.33
Земля171349197089917943338961114835930329862.2154006.34980.27
Марс920741096676669488993416411743.615200.841584.85138.30
Юпитер1036688321213459867741536377658.11488.69834.2019.01
Сатурн129141321.979966656.682381789.5820.06451.85000.0059
📥 Скачать прототип v0.0 (PyGame + Tkinter) 📥 Скачать версию v0.1 (VTK + PyQt6) 💻 Репозиторий на GitHub

Полный исходный код проекта

Нажмите на название файла, чтобы развернуть и посмотреть полный исходный код.

build.py Ядро / Многопроцессность
from multiprocessing import Process, Queue
from datetime import datetime, timedelta
import time

# Импорты из других файлов проекта
from calculating_advanced import num_integr, Body, bodies, downloader  # Функция численного интегрирования
from new_gui import managment  # Функция графического интерфейса
ifstart = False
from kvazissm_visual import visualization  # Функция визуализации
# Глобальные переменные
start_date = datetime.strptime("00:00:00/12/04/2021", "%H:%M:%S/%d/%m/%Y")
now_date = "00:00:00/12/04/2021"  # Текущая дата
current_date = datetime.strptime(now_date, "%H:%M:%S/%d/%m/%Y")  # Начальная дата
calc_speed = 365 * 24 * 3600  # Скорость расчета (в секундах)
step = 900#3600
file = 'info.txt'
q = Queue()  # Очередь для обмена данными
qifs = Queue()
qvis = Queue()
qbod = Queue()
qvis_date = Queue()
qnewb = Queue()
data = []
# Функция для запуска численного интегрирования
# Функция для запуска обновления даты
def run_num_integr():
    global now_date
    global current_date
    global calc_speed
    global step
    global ifstart
    global file
    global data
    lasttime = time.time()
    lasttime2 = time.time()
    while True:
        if not q.empty():
            command, data = q.get()
            if command == "update_ifstart":
                ifstart = data
                if ifstart:
                    current_date = start_date
                    downloader()
            elif command == "update_date":
                now_date = data  # Обновление начальной даты
            elif command == "update_end_date":
                end_date = datetime.strptime(data,   "%H:%M:%S/%d/%m/%Y")# Обновление конечной даты
            elif command == "update_speed":
                calc_speed = data
            elif command == "update_file":
                file = data
            elif command == "update_step":
                step = 900#data
        #print(ifstart, now_date != end_date, time.time() - lasttime > step / calc_speed)
        if ifstart and current_date.timestamp() <= end_date.timestamp() and time.time() - lasttime > step/calc_speed:
            num_integr(step, qbod, qnewb, ifstart)  # Вызов функции интегрирования с шагом step
            current_date += timedelta(seconds=step)
            now_date = current_date.strftime("%H:%M:%S/%d/%m/%Y")
            qvis.put(("update_end_date", end_date))
            qvis.put(("update_step", step))
            qvis.put(("update_file", file))
            if end_date:
                if current_date.timestamp() >= end_date.timestamp():
                    ifstart = False
            lasttime = time.time()
            if time.time() - lasttime2 > 0.02 or ifstart == False:
                qvis_date.put(now_date)  # Отправка новой даты в очередь
                lasttime2 = time.time()
# Основной блок для сборки проекта
if __name__ == "__main__":
    # Создание процессов
    process_integ = Process(target=run_num_integr)  # Процесс для обновления даты
    process_gui = Process(target=managment, args=(q, qnewb))  # Процесс для GUI
    process_visualization = Process(target=visualization, args=(qbod, qvis, qvis_date,))  # Процесс для визуализации

    # Запуск процессов
    process_gui.start()
    process_visualization.start()
    process_integ.start()
    # Основной цикл для обработки данных из очереди
new_gui.py Интерфейс v0.1 (PyQt6)
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QComboBox, QPushButton,
                             QFormLayout, QGroupBox, QGridLayout, QMessageBox)
from PyQt6.QtCore import Qt

# Значения по умолчанию (как в оригинале)
DEFAULT_CALC_SYST = 'Гелиоцентрическая декартова'
DEFAULT_SPEED = str(365 * 24 * 3600)
DEFAULT_STEP = '3600'
DEFAULT_FILE = 'info.txt'
DEFAULT_START_DATE = '00:00:00/12/04/2021'


class ControlWindow(QWidget):
    def __init__(self, q, qnewb):
        super().__init__()
        self.q = q
        self.qnewb = qnewb
        self.setWindowTitle("Управление моделью")
        self.resize(700, 600)

        self.setup_ui()

    def setup_ui(self):
        # Основной вертикальный контейнер
        main_layout = QVBoxLayout()
        self.setLayout(main_layout)

        # --- Группа настроек ---
        settings_group = QGroupBox("Настройки симуляции")
        settings_layout = QFormLayout()

        # Система координат
        self.coord_combo = QComboBox()
        self.coord_combo.addItems(['Эклиптическая', 'Экваториальная',
                                   'Гелиоцентрическая полярная', 'Гелиоцентрическая декартова'])
        self.coord_combo.setCurrentText(DEFAULT_CALC_SYST)
        settings_layout.addRow("Система координат:", self.coord_combo)

        # Скорость расчёта
        self.speed_entry = QLineEdit(DEFAULT_SPEED)
        settings_layout.addRow("Скорость расчёта (сек):", self.speed_entry)

        # Шаг интегрирования
        self.step_entry = QLineEdit(DEFAULT_STEP)
        settings_layout.addRow("Шаг интегрирования (сек):", self.step_entry)

        # Файл данных
        self.file_entry = QLineEdit(DEFAULT_FILE)
        settings_layout.addRow("Файл данных:", self.file_entry)

        # Даты
        self.start_date_entry = QLineEdit(DEFAULT_START_DATE)
        self.start_date_entry.setPlaceholderText("ЧЧ:MM:СС/ДД/ММ/ГГГГ")
        settings_layout.addRow("Стартовая дата:", self.start_date_entry)

        self.end_date_entry = QLineEdit()
        self.end_date_entry.setPlaceholderText("ЧЧ:MM:СС/ДД/ММ/ГГГГ")
        settings_layout.addRow("Дата окончания:", self.end_date_entry)

        settings_group.setLayout(settings_layout)
        main_layout.addWidget(settings_group)

        # --- Кнопки управления расчётом ---
        btn_layout = QHBoxLayout()

        self.btn_start = QPushButton("Начать расчёт")
        self.btn_start.setStyleSheet("background-color: green; color: white; font-weight: bold;")
        self.btn_start.clicked.connect(self.starter)

        self.btn_stop = QPushButton("Остановить расчёт")
        self.btn_stop.setStyleSheet("background-color: red; color: white; font-weight: bold;")
        self.btn_stop.clicked.connect(self.stopper)

        btn_layout.addWidget(self.btn_start)
        btn_layout.addWidget(self.btn_stop)
        main_layout.addLayout(btn_layout)

        # Лейбл для статуса
        self.status_label = QLabel("Готов к работе")
        self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        main_layout.addWidget(self.status_label)

        # --- Группа добавления объекта ---
        obj_group = QGroupBox("Меню добавления объекта")
        obj_layout = QVBoxLayout()

        # Форма для основных параметров
        obj_form = QFormLayout()
        self.name_entry = QLineEdit()
        obj_form.addRow("Имя объекта:", self.name_entry)

        self.radius_entry = QLineEdit()
        obj_form.addRow("Радиус объекта (м):", self.radius_entry)

        self.mass_entry = QLineEdit()
        obj_form.addRow("Масса объекта (кг):", self.mass_entry)
        obj_layout.addLayout(obj_form)

        # Сетка для координат и скоростей
        grid_vals = QGridLayout()

        grid_vals.addWidget(QLabel("Координаты (м):"), 0, 0)
        self.x_entry = QLineEdit();
        self.x_entry.setPlaceholderText("X")
        self.y_entry = QLineEdit();
        self.y_entry.setPlaceholderText("Y")
        self.z_entry = QLineEdit();
        self.z_entry.setPlaceholderText("Z")
        grid_vals.addWidget(self.x_entry, 0, 1)
        grid_vals.addWidget(self.y_entry, 0, 2)
        grid_vals.addWidget(self.z_entry, 0, 3)

        grid_vals.addWidget(QLabel("Скорости (м/с):"), 1, 0)
        self.vx_entry = QLineEdit();
        self.vx_entry.setPlaceholderText("Vx")
        self.vy_entry = QLineEdit();
        self.vy_entry.setPlaceholderText("Vy")
        self.vz_entry = QLineEdit();
        self.vz_entry.setPlaceholderText("Vz")
        grid_vals.addWidget(self.vx_entry, 1, 1)
        grid_vals.addWidget(self.vy_entry, 1, 2)
        grid_vals.addWidget(self.vz_entry, 1, 3)

        obj_layout.addLayout(grid_vals)

        # Кнопки для объектов
        self.btn_add_obj = QPushButton("Добавить объект")
        self.btn_add_obj.setStyleSheet("background-color: blue; color: white;")
        self.btn_add_obj.clicked.connect(self.add_body)

        self.btn_save_data = QPushButton("Сохранить текущие данные в файл")
        self.btn_save_data.setStyleSheet("background-color: blue; color: white;")
        self.btn_save_data.clicked.connect(self.save_data)

        obj_layout.addWidget(self.btn_add_obj)
        obj_layout.addWidget(self.btn_save_data)

        obj_group.setLayout(obj_layout)
        main_layout.addWidget(obj_group)

    def starter(self):
        """Логика запуска расчёта"""
        start_date = self.start_date_entry.text()
        end_date = self.end_date_entry.text()

        # Валидация
        if not start_date:
            self.status_label.setText("Ошибка: Введите стартовую дату")
            self.status_label.setStyleSheet("color: red; font-weight: bold;")
            return

        if not end_date:
            self.status_label.setText("Ошибка: Введите дату окончания")
            self.status_label.setStyleSheet("color: red; font-weight: bold;")
            return

        # Отправка данных в очередь
        self.q.put(("update_calcsyst", self.coord_combo.currentText()))

        if self.speed_entry.text():
            try:
                self.q.put(("update_speed", float(self.speed_entry.text())))
            except ValueError:
                pass  # Или обработка ошибки числа

        if self.step_entry.text():
            try:
                self.q.put(("update_step", float(self.step_entry.text())))
            except ValueError:
                pass

        if self.file_entry.text():
            self.q.put(("update_file", self.file_entry.text()))

        self.q.put(("update_start_date", start_date))
        self.q.put(("update_end_date", end_date))

        # Успешный запуск
        self.q.put(('update_ifstart', True))
        self.status_label.setText("Расчёт запущен")
        self.status_label.setStyleSheet("color: green; font-weight: bold;")

    def stopper(self):
        """Логика остановки"""
        self.q.put(('update_ifstart', False))
        self.status_label.setText("Расчёт остановлен")
        self.status_label.setStyleSheet("color: red;")

    def add_body(self):
        """Добавление нового тела"""
        try:
            name = self.name_entry.text()
            radius = float(self.radius_entry.text())
            mass = float(self.mass_entry.text())
            x = float(self.x_entry.text())
            y = float(self.y_entry.text())
            z = float(self.z_entry.text())
            vx = float(self.vx_entry.text())
            vy = float(self.vy_entry.text())
            vz = float(self.vz_entry.text())

            # Кортеж данных (структура из вашего примера)
            new_body = (name, x, y, z, vx, vy, vz, 0, 0, 0, radius, mass)

            # В оригинале new_body никуда не отправлялся, здесь отправляем в qnewb
            self.qnewb.put(new_body)

            self.status_label.setText(f"Объект '{name}' добавлен")
            self.status_label.setStyleSheet("color: blue;")

            # Очистка полей (опционально)
            # self.name_entry.clear()

        except ValueError:
            self.status_label.setText("Ошибка: Проверьте числовые поля")
            self.status_label.setStyleSheet("color: red;")

    def save_data(self):
        """Сохранение данных"""
        # В оригинале эта кнопка вызывала add_body, что скорее всего было ошибкой.
        # Здесь отправляем сигнал на сохранение, если это предусмотрено логикой симуляции.
        self.q.put("save_current_data")
        self.status_label.setText("Запрос на сохранение отправлен")
        self.status_label.setStyleSheet("color: black;")


def managment(q, qnewb):
    """Функция-обертка для запуска Qt приложения"""
    # Создаем экземпляр приложения.
    # sys.argv нужен для обработки аргументов командной строки Qt
    app = QApplication(sys.argv)

    # Применяем стиль Fusion для одинакового вида на всех ОС
    app.setStyle('Fusion')

    window = ControlWindow(q, qnewb)
    window.show()

    # Запускаем цикл событий.
    # В Python 3.14+ и современных PyQt используется .exec() вместо .exec_()
    sys.exit(app.exec())


# Для тестирования интерфейса отдельно от основной программы раскомментируйте строки ниже:
"""if __name__ == "__main__":
    from queue import Queue

    # Заглушки очередей для теста
    q_test = Queue()
    qnewb_test = Queue()
    managment(q_test, qnewb_test)"""
old_gui.py Интерфейс v0.0 (Tkinter)
import tkinter as tk
from tkinter import ttk
# Глобальные переменные
CalcSyst = 'Гелиоцентрическая декартова'
CalcSpeed = 365 * 24 * 3600
file_name = 'info.txt'
start_date = '00:00:00/12/04/2021'
end_date = ''
now_date = start_date
setupdtime = 1
bodies = []
step = 3600
ifstart = False
mess = 1
window = ''

def managment(q, qnewb):
    global CalcSyst
    global CalcSpeed
    global file_name
    global start_date
    global end_date
    global now_date
    global bodies
    global window

    def starter():
        iferror = False
        global ifstart
        global mess
        mess = 1
        ttk.Label(settings_frame, text="Сообщения:").grid(row=17, column=0, sticky='W')
        q.put(("update_calcsyst", coord_combo.get()))
        if speed_entry.get():
            q.put("update_speed", float(speed_entry.get()))
        if step_entry.get():
            q.put(("update_step", float(step_entry.get())))
        if file_entry.get():
            q.put(("update_file", file_entry.get()))
        if start_date_entry.get():
            q.put(("update_start_date", start_date_entry.get()))
        else:
            iferror = True
            tk.Label(settings_frame, text='Введите стартовую дату        ', bg='red', fg='white').grid(row=17, column=mess, sticky='W')
            mess += 1
        if end_date_entry.get():
            q.put(('update_end_date', end_date_entry.get()))
        else:
            iferror = True
            tk.Label(settings_frame, text='Введите дату окончания', bg='red', fg='white').grid(row=17, column=mess, sticky='W')
            mess += 1
        if not iferror:
            tk.Label(settings_frame, text='Запущена подготовка к расчёту', bg='green', fg='white').grid(row=17, column=mess, sticky='W')
            mess += 1
            tk.Label(settings_frame, text='                             ').grid(row=17, column=mess, sticky='W')
            mess += 1
            ifstart = True
        q.put(('update_ifstart', ifstart))
    def stopper():
        q.put(('update_ifstart', False))
    def add_body():
        name = name_entry.get()
        radius = float(radius_entry.get())
        mass = float(mass_entry.get())
        x = float(x_entry.get())
        y = float(y_entry.get())
        z = float(z_entry.get())
        Vx = float(Vx_entry.get())
        Vy = float(Vy_entry.get())
        Vz = float(Vz_entry.get())
        new_body = (name, x, y, z, Vx, Vy, Vz, 0, 0, 0, radius, mass)
    # Создание окна
    window = tk.Tk()
    window.title("Управление моделью")
    window.geometry('700x600')
    # Фрейм для настроек
    settings_frame = ttk.Frame(window, padding=10)
    settings_frame.grid(row=0, column=0, sticky="nsew")

    # Настройка системы координат
    ttk.Label(settings_frame, text="Система координат:").grid(row=0, column=0, sticky="w")
    coord_combo = ttk.Combobox(settings_frame, values=['Эклиптическая', 'Экваториальная', 'Гелиоцентрическая полярная', 'Гелиоцентрическая декартова'])
    coord_combo.grid(row=0, column=1, sticky="w")
    coord_combo.set(CalcSyst)

    ttk.Label(settings_frame, text="Файл данных:").grid(row=3, column=0, sticky="w")
    file_entry = ttk.Entry(settings_frame, width=10)
    file_entry.grid(row=3, column=1, sticky="w")

    # Настройка скорости расчёта
    ttk.Label(settings_frame, text="Скорость расчёта (сек):").grid(row=1, column=0, sticky="w")
    speed_entry = ttk.Entry(settings_frame, width=10)
    speed_entry.grid(row=1, column=1, sticky="w")
    # Шаг интегрирования
    ttk.Label(settings_frame, text="Шаг интегрирования (сек):").grid(row=2, column=0, sticky="w")
    step_entry = ttk.Entry(settings_frame, width=10)
    step_entry.grid(row=2, column=1, sticky="w")
    # Настройка даты
    ttk.Label(settings_frame, text="Стартовая дата:").grid(row=4, column=0, sticky="w")
    ttk.Label(settings_frame, text="ЧЧ:MM:СС/ДД/ММ/ГГГГ").grid(row=4, column=2, sticky="w")
    start_date_entry = ttk.Entry(settings_frame, width=20)
    start_date_entry.grid(row=4, column=1, sticky="w")
    ttk.Label(settings_frame, text="Дата окончания:").grid(row=5, column=0, sticky="w")
    end_date_entry = ttk.Entry(settings_frame, width=20)
    end_date_entry.grid(row=5, column=1, sticky="w")
    ttk.Label(settings_frame, text="").grid(row=6, column=0, sticky="w")
    tk.Button(settings_frame, bg="green", fg = "white", text="Начать расчёт", command=starter).grid(row=7, column=0, sticky="w")
    tk.Button(settings_frame, bg="red", fg = "white", text="Остановить расчёт", command=stopper).grid(row=7, column=1, sticky="w")
    ttk.Label(settings_frame, text="").grid(row=8, column=0, sticky="w")
    # Добавление нового объекта
    ttk.Label(settings_frame, text="Меню добавления объекта").grid(row=9, column=0, sticky="w")
    ttk.Label(settings_frame, text="Имя объекта:").grid(row=10, column=0, sticky="w")
    name_entry = ttk.Entry(settings_frame, width=10)
    name_entry.grid(row=10, column=1, sticky="w")
    ttk.Label(settings_frame, text="Радиус объекта (м):").grid(row=11, column=0, sticky="w")
    radius_entry = ttk.Entry(settings_frame, width=10)
    radius_entry.grid(row=11, column=1, sticky="w")
    ttk.Label(settings_frame, text="Масса объекта (кг):").grid(row=12, column=0, sticky="w")
    mass_entry = ttk.Entry(settings_frame, width=10)
    mass_entry.grid(row=12, column=1, sticky="w")
    ttk.Label(settings_frame, text="Координаты (x, y, z ) (м):").grid(row=13, column=0, sticky="w")
    x_entry = ttk.Entry(settings_frame, width=5)
    x_entry.grid(row=13, column=1, sticky="w")
    y_entry = ttk.Entry(settings_frame, width=5)
    y_entry.grid(row=13, column=2, sticky="w")
    z_entry = ttk.Entry(settings_frame, width=5)
    z_entry.grid(row=13, column=3, sticky="w")
    ttk.Label(settings_frame, text="Скорости (Vx, Vy, Vz) (м/c):").grid(row=14, column=0, sticky="w")
    Vx_entry = ttk.Entry(settings_frame, width=5)
    Vx_entry.grid(row=14, column=1, sticky="w")
    Vy_entry = ttk.Entry(settings_frame, width=5)
    Vy_entry.grid(row=14, column=2, sticky="w")
    Vz_entry = ttk.Entry(settings_frame, width=5)
    Vz_entry.grid(row=14, column=3, sticky="w")
    tk.Button(settings_frame, text="Добавить объект", command=add_body, bg='blue', fg='white').grid(row=15, column=0, columnspan=4, sticky="w")
    tk.Button(settings_frame, text="Сохранить текущие данные в файл", command=add_body, bg='blue', fg='white').grid(row=16, column=0, columnspan=4, sticky="w")
    window.mainloop()

kvazissm_visual.py Визуализация v0.1 (VTK 3D)
import sys
import vtk
import time

from calculating_advanced import Body
import calculating_advanced


class SolarSystemVisualizer:
    def __init__(self, qbod, qvis, qvis_date):
        self.qbod = qbod
        self.qvis = qvis
        self.qvis_date = qvis_date

        # --- Переменные состояния ---
        self.bodies_data = []  # Список объектов Body
        self.actors = {}  # Словарь {имя_тела: vtkActor} (сами сферы)
        self.labels = {}  # Словарь {имя_тела: vtkCaptionActor2D} (подписи)
        self.now_date = "00:00:00/12/04/2021"
        self.end_date = ""
        self.calc_speed = 365 * 24 * 3600
        self.step = 3600
        self.file_name = 'info.txt'

        # --- Масштабирование ---
        # VTK плохо работает с числами порядка 10^11, приводим к условным единицам
        self.COORD_SCALE = 1.0e-9

        # Визуальное увеличение радиусов, иначе планеты будут невидимыми точками
        self.RADIUS_SCALE_SUN = self.COORD_SCALE * 60
        self.RADIUS_SCALE_PLANET = self.COORD_SCALE * 1500

        # --- Настройка VTK ---
        self.renderer = vtk.vtkRenderer()
        self.renderer.SetBackground(0.05, 0.05, 0.1)  # Темно-синий космос

        self.render_window = vtk.vtkRenderWindow()
        self.render_window.AddRenderer(self.renderer)
        self.render_window.SetSize(1200, 900)
        self.render_window.SetWindowName("KvaziSSM 3D Model Visualisation")

        self.interactor = vtk.vtkRenderWindowInteractor()
        self.interactor.SetRenderWindow(self.render_window)

        # Стиль камеры: левая кнопка - вращение, колесо - зум, средняя/shift - панорамирование
        style = vtk.vtkInteractorStyleTrackballCamera()
        self.interactor.SetInteractorStyle(style)
        camera = self.renderer.GetActiveCamera()
        camera.Dolly(0.001)
        # Текстовая информация (HUD) - статический текст в углу экрана
        self.text_actor = vtk.vtkTextActor()
        self.text_actor.GetTextProperty().SetFontSize(14)
        self.text_actor.GetTextProperty().SetColor(0.8, 0.8, 0.8)
        self.text_actor.SetPosition(10, 10)
        self.renderer.AddActor2D(self.text_actor)

        # Инициализация таймера для цикла обновления (примерно 60 FPS)
        self.interactor.Initialize()
        self.interactor.CreateRepeatingTimer(16)
        self.interactor.AddObserver("TimerEvent", self.update_scene)
        self.interactor.AddObserver("InteractionEvent", self.update_scene)

    def get_color_by_name(self, name):
        """Возвращает RGB цвет в зависимости от имени объекта"""
        name = name.lower()
        if 'sun' in name or 'солнце' in name: return (1.0, 0.9, 0.0)  # Желтый
        if 'earth' in name or 'земля' in name: return (0.1, 0.4, 0.9)  # Синий
        if 'mars' in name or 'марс' in name: return (1.0, 0.3, 0.2)  # Красный
        if 'venus' in name or 'венера' in name: return (0.9, 0.8, 0.6)
        if 'mercury' in name or 'меркурий' in name: return (0.6, 0.6, 0.6)
        if 'jupiter' in name or 'юпитер' in name: return (0.8, 0.6, 0.4)
        if 'saturn' in name or 'сатурн' in name: return (0.9, 0.8, 0.5)
        return (0.8, 0.8, 0.8)  # Серый по умолчанию

    def create_body_actor(self, body):
        """Создает сферу и 2D-подпись фиксированного размера для нового тела"""

        # 1. Геометрия сферы
        sphere = vtk.vtkSphereSource()
        sphere.SetThetaResolution(40)  # Детализация
        sphere.SetPhiResolution(40)

        # Выбор масштаба радиуса
        rad_scale = self.RADIUS_SCALE_SUN if body.name in ['Sun', 'Солнце'] else self.RADIUS_SCALE_PLANET
        sphere.SetRadius(body.radius * rad_scale)

        mapper = vtk.vtkPolyDataMapper()
        mapper.SetInputConnection(sphere.GetOutputPort())

        actor = vtk.vtkActor()
        actor.SetMapper(mapper)

        # Настройка цвета и свечения
        r, g, b = self.get_color_by_name(body.name)
        actor.GetProperty().SetColor(r, g, b)

        if body.name in ['Sun', 'Солнце']:
            actor.GetProperty().SetAmbient(1.0)  # Солнце светится само
            actor.GetProperty().SetDiffuse(0.0)
        else:
            actor.GetProperty().SetAmbient(0.1)  # Планеты тусклые в тени
            actor.GetProperty().SetDiffuse(0.8)  # Отражают свет

        # 2. Текстовая метка (CaptionActor2D)
        # Этот объект отображает текст поверх 3D сцены, размер шрифта не зависит от зума
        label = vtk.vtkCaptionActor2D()
        label.SetCaption(body.name)
        label.ThreeDimensionalLeaderOff()  # Убедиться, что он работает в 2D режиме
        label.SetPadding(0)  # Убрать отступы вокруг текста
        label.SetHeight(0.001)
        # Настройка шрифта
        text_prop = label.GetCaptionTextProperty()
        text_prop.SetColor(1, 1, 1)  # Белый текст
        text_prop.BoldOff()
        text_prop.ItalicOff()

        # Отключаем "лидер" (линию) и рамку, оставляем только текст
        label.LeaderOff()
        label.BorderOff()
        

        # Смещение текста, чтобы он не перекрывал планету (в пикселях экрана)
        # Position управляет положением текста относительно точки привязки
        # label.SetPosition(...) - здесь можно подстроить, если текст "сидит" на планете

        return actor, label

    def update_scene(self, obj, event):
        """Основной цикл обновления данных и графики"""

        # --- 1. Обработка очередей управления ---
        if not self.qvis_date.empty():
            self.now_date = self.qvis_date.get()

        if not self.qvis.empty():
            try:
                # Читаем все доступные команды, чтобы не было задержек
                while not self.qvis.empty():
                    command, data = self.qvis.get_nowait()
                    if command == "update_end_date":
                        self.end_date = data
                    elif command == "update_speed":
                        self.calc_speed = data
                    elif command == "update_step":
                        self.step = data
                    elif command == "update_file":
                        self.file_name = data
            except:
                pass

        # --- 2. Получение данных о телах ---
        has_new_data = False
        if not self.qbod.empty():
            # Берем только самый последний кадр, пропуская промежуточные, если рендер не успевает
            while not self.qbod.empty():
                bod_data_raw = self.qbod.get_nowait()

            # Распаковка данных в объекты Body
            self.bodies_data = [Body(*b) for b in bod_data_raw]
            has_new_data = True

        # Если данных нет вообще (первый запуск), пробуем загрузить дефолтные
        if not self.bodies_data and not has_new_data:
            try:
                calculating_advanced.downloader()
                self.bodies_data = calculating_advanced.bodies
            except:
                pass  # Ждем данных от процесса расчета

        # --- 3. Обновление 3D объектов ---
        for body in self.bodies_data:
            # Применяем масштаб координат
            x = body.x * self.COORD_SCALE
            y = body.y * self.COORD_SCALE
            z = body.z * self.COORD_SCALE

            # Если для этого тела еще нет актера, создаем его
            if body.name not in self.actors:
                actor, label = self.create_body_actor(body)
                self.renderer.AddActor(actor)
                self.renderer.AddActor(label)
                self.actors[body.name] = actor
                self.labels[body.name] = label

            # Обновляем позицию сферы
            self.actors[body.name].SetPosition(x, y, z)

            # Обновляем точку привязки текста
            self.labels[body.name].SetAttachmentPoint(x, y, z)

        # --- 4. Обновление интерфейса (HUD) ---
        info_text = (
            f"Date: {self.now_date}\n"
            f"End Date: {self.end_date}\n"
            f"Calc Speed: {self.calc_speed}\n"
            f"Step: {self.step}\n"
            f"Objects: {len(self.bodies_data)}\n"
        )
        self.text_actor.SetInput(info_text)

        # Рендер текущего кадра
        self.render_window.Render()

    def start(self):
        """Запуск цикла визуализации"""
        # Сброс камеры на объекты при старте
        self.renderer.ResetCamera()
        cam = self.renderer.GetActiveCamera()
        cam.Zoom(0.8)  # Немного отдаляем

        self.render_window.Render()
        self.interactor.Start()


def visualization(qbod, qvis, qvis_date):
    """Точка входа для мультипроцессинга"""
    viz = SolarSystemVisualizer(qbod, qvis, qvis_date)
    viz.start()
kvazissm_visual.py Визуализация v0.0 (PyGame 2D)
import pygame
from pygame.locals import *
import sys
import time
from pygame.transform import scale

from calculating_advanced import Body
import calculating_advanced


def visualization(qbod, qvis, qvis_date):
    now_date = "00:00:00/12/04/2021"  # Текущая дата

    # Инициализация PyGame
    pygame.init()

    # Настройки экрана
    SCREEN_WIDTH = 1200
    SCREEN_HEIGHT = 900
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("Визуализация модели")
    DISPLAY_TICK = 60
    clock = pygame.time.Clock()
    vismode = 'xy'
    # Цвета
    WHITE = (255, 255, 255)
    BLACK = (0, 0, 0)
    GRAY = (200, 200, 200)
    BLUE = (0, 0, 255)
    RED = (255, 0, 0)
    GREEN = (0, 255, 0)
    # Переменные отображения данных
    now_date = "00:00:00/12/04/2021"  # Текущая дата
    end_date = ""
    calc_speed = 365 * 24 * 3600
    step = 3600
    file = 'info.txt'
    bod_data = []
    # Шрифты
    font_small = pygame.font.SysFont("Arial", 16)
    font_medium = pygame.font.SysFont("Arial", 24)

    # Масштабирование координат
    SCALE = 1.2e-9  # Масштаб для преобразования метров в пиксели
    PL_REDUCT_SCALE = SCALE * 15  # Для видимости планет
    CAM_DISTANSE = 9e9 * SCALE
    bodies = calculating_advanced.bodies
    running = True
    while running:
        for event in pygame.event.get():  # Обработка событий
            if event.type == pygame.QUIT:
                running = False
        if not qbod.empty():  # Приём данных
            bod_data = qbod.get()
            bodies.clear()
            for body_data in bod_data:
                bodies.append(Body(*body_data))
        if not qvis_date.empty():
            now_date = qvis_date.get()
        if not qvis.empty():
            command, data = qvis.get()
            if command == "update_end_date":
                end_date = data
            elif command == "update_speed":
                calc_speed = data
            elif command == "update_step":
                step = data
            elif command == "update_file":
                file = data
        parameters = [f'Текущая дата: {now_date}', f'Конечная дата: {end_date}', f'Скорость расчёта: {calc_speed}', f'Шаг: {step}', f'Файл данных: {file}']
        # Обновление координат и данных
        img_bodies = bodies[0:len(bodies)]
        if vismode == 'xy':
            for body in img_bodies:
                # print(img_bodies == bodies)
                body.x = (body.x * SCALE) + (SCREEN_WIDTH // 2)
                body.y = (-1 * body.y * SCALE) + (SCREEN_HEIGHT // 2)
                body.z *= SCALE
                if body.name != 'Sun':
                    body.radius *= SCALE * 1500  # * body.z / CAM_DISTANSE
                else:
                    body.radius *= SCALE * 60
        elif vismode == 'yz':
            for body in img_bodies:
                body.y = (body.y * SCALE) + (SCREEN_WIDTH // 2)
                body.z = (-1 * body.z * SCALE) + (SCREEN_HEIGHT // 2)
                body.y *= SCALE
                if body.name != 'Sun':
                    body.radius *= SCALE * 15 * body.x / CAM_DISTANSE
                else:
                    body.radius *= SCALE / 15
        screen.fill(WHITE)  # Очистка экрана
        # Отрисовка
        if vismode == 'xy':
            for body in img_bodies:
                pygame.draw.circle(screen, RED, (body.x, body.y), body.radius)
                text_surface = font_medium.render(body.name, True, BLACK)  # Создаем текстовый маркер
                text_rect = text_surface.get_rect()
                text_rect.center = (body.x, body.y - body.radius - 10)
                screen.blit(text_surface, text_rect)
        elif vismode == 'yz':
            for body in img_bodies:
                pygame.draw.circle(screen, RED, (body.y, body.z), body.radius)
                text_surface = font_medium.render(body.name, True, BLACK)  # Создаем текстовый маркер
                text_rect = text_surface.get_rect()
                text_rect.center = (body.y, body.z - body.radius - 10)
                screen.blit(text_surface, text_rect)
        if bod_data:
            bodies.clear()
            for body_data in bod_data:
                bodies.append(Body(*body_data))
        else:
            calculating_advanced.downloader()
            bodies = calculating_advanced.bodies
        sch = 0
        for p in parameters:
            text_date = font_small.render(p, True, BLACK)
            text_date_rect = text_date.get_rect()
            text_date_rect.topleft = (0, sch*text_date_rect.height)
            screen.blit(text_date, text_date_rect)
            sch += 2
        for bod in bodies:
            text_name = font_small.render(f'Имя объекта: {bod.name}', True, WHITE, BLUE)
            text_name_rect = text_name.get_rect()
            text_name_rect.topleft = (0, sch * text_name_rect.height)
            screen.blit(text_name, text_name_rect)
            sch += 1
            text_mass = font_small.render(f'Масса: {bod.mass}', True, BLACK)
            text_mass_rect = text_mass.get_rect()
            text_mass_rect.topleft = (0, sch * text_mass_rect.height)
            screen.blit(text_mass, text_mass_rect)
            sch += 1
            text_rad = font_small.render(f'Радиус: {bod.radius}', True, BLACK)
            text_rad_rect = text_rad.get_rect()
            text_rad_rect.topleft = (0, sch * text_rad_rect.height)
            screen.blit(text_rad, text_rad_rect)
            sch += 2
            text_x = font_small.render(f'x: {bod.x} Vx: {bod.Vx} ax: {bod.ax}', True, BLACK)
            text_x_rect = text_x.get_rect()
            text_x_rect.topleft = (0, sch * text_x_rect.height)
            screen.blit(text_x, text_x_rect)
            sch += 1
            text_y = font_small.render(f'y: {bod.y} Vy: {bod.Vy} ay: {bod.ay}', True, BLACK)
            text_y_rect = text_y.get_rect()
            text_y_rect.topleft = (0, sch * text_y_rect.height)
            screen.blit(text_y, text_y_rect)
            sch += 3
            text_z = font_small.render(f'z: {bod.z} Vz: {bod.Vz} az: {bod.az}', True, BLACK)
            text_z_rect = text_z.get_rect()
            text_z_rect.topleft = (0, sch * text_z_rect.height)
            screen.blit(text_z, text_z_rect)
            sch += 2
        #Тест
        for h in bodies:
            print(h.name, h.x, h.y, h.z, h.Vx, h.Vy, h.Vz, '\n', sep=' ')
        pygame.display.flip()  # Обновление экрана
        # Ограничение частоты обновления
        clock.tick(DISPLAY_TICK)
    pygame.quit()
    sys.exit()
kvazi_dts.py Утилита
def datasearch(file_name, objectl, parameter):
    file = open(file_name, 'r')
    info = file.read()
    i = info.index(objectl) + len(objectl)
    while info[i+1: i+len(parameter)+2] != parameter+' ':
        i = i+1
        if info[i: i+len('object_end')] == 'object_end':
            print(f'Параметр {parameter} объекта {objectl} не найден в файле {file_name}. Продолжение работы программы невозможно')
            exit(1)
    j = i+len(parameter)+2
    while info[j] == ' ' or  info[j] == '=':
        j = j + 1
    k = j
    while info[k] != '\n':
        k = k + 1
    return info[j:k]
info.txt Конфигурация
General
system_name = Солнечная система
NUM_BODIES = 9
GRAVITY_CONSTANT = 6.67430e-11
STAR = Sun
TIME = 00:00:00/12/04/2021

Body_0
name = Sun
radius = 6.9634e8
mass = 1.9885e30
x = -1.092961103527230E+09
y = 8.038274795669809E+08
z = 1.896419146749307E+07
Vx = -1.049862721076522E+1
Vy = -1.159390400344258E+1
Vz = 3.414155173086606E-01
Body_1
name = Mercury
radius = 2.4397e6
mass = 3.302e23
x = 5.264555000230405E+10
y = -5.694980330962850E+09
z = -5.441551243950481E+09
Vx = -3.584014950066584E+03
Vy = 5.052872269283028E+04
Vz = 4.458128927712359E+03

Body_2
name = Venus
radius = 6.0518e6
mass = 48.685e23
x = 9.047269464662506E+10
y = 5.851336080208322E+10
z = -4.472877141980678E+09
Vx = -1.879172734890263E+04
Vy = 2.946396608965331E+04
Vz = 1.488636224625598E+03

Body_3
name = Earth
radius = 6.371e6
mass = 5.97219e24
x = -1.401034900195547E+11
y = -5.542031473237328E+10
z = 2.203423339415714E+07
Vx = 1.067330726523228E+04
Vy = -2.773887952618739E+04
Vz = 1.625568732631422E+00

Body_4
name = Mars
radius = 3.3895e6
mass = 6.4171e23
x = -1.050799109983333E+11
y = 2.204600324444063E+11
z = 7.172939531547755E+09
Vx = -2.0994657445110788E+04
Vy = -8.320121933089915E+03
Vz = 3.409811280300108E+02

Body_5
name = Jupiter
radius = 6.9911e7
mass = 1.8982e27
x = 5.385412407001685e11
y = -5.305339771309409e11
z = -9.847608346906245e9
Vx = 9.008969888225652e3
Vy = 9.924822355174250e3
Vz = -2.427157105139859e2

Body_6
name = Saturn
radius = 5.8232e7
mass = 5.6834e26
x = 8.847887203091053e11
y = -1.199070595431435e12
z = -1.437678365171337e10
Vx = 7.233161355133544e3
Vy = 5.712737392345746e3
Vz = -3.873722116053260e2

Body_7
name = Uranus
radius = 2.5362e7
mass = 8.6813e25
x = 2.256978216394547e12
y = 1.908111087045525e12
z = -2.215277767491233e10
Vx = -4.446689911063689e3
Vy = 4.883247899821490e3
Vz = 7.574419726570225e1

Body_8
name = Neptune
radius = 2.4622e7
mass = 1.0241e26
x =  4.413959524250453e12
y = -7.347457715532233e11
z = -8.659351770194119e10
Vx = 8.564501236858040e2
Vy = 5.393770243128461e3
Vz = -1.308114620191572e2