Pythonで人工衛星の姿勢運動を可視化する

人工衛星の運動は、剛体の運動としてオイラー運動方程式によって記述することができますが、姿勢表現がクオータニオンだったり角速度だったりでいまいち今どういう運動をしているか直感的にわからなかったりします。この問題を解決するために、衛星の動きをグラフィカルに表現できるように、pythonOpenGL module, pygameを使って表現してみました。

Install

下記のサイトが参考になりそう

https://stackabuse.com/advanced-opengl-in-python-with-pygame-and-pyopengl/

ひとまずOutputだけ

Pythonで下記のようなグラフィックを生成できます。 グラフィックは、姿勢に外乱が加わった際に、三軸姿勢で元に戻す様子です。

f:id:spaquid:20220118162425g:plain
三軸姿勢制御の可視化

コード (一部)

SatelliteやSat_VisualizerなどのClassはどこかで提示しますが、メインのループは下記のようなコードになります。

import numpy as np
from math import *

# from quat import *

import pygame
from pygame.locals import *

from OpenGL.GL import *
from OpenGL.GLU import *
from GainSetting import GainSetting

from dynamics import Dynamics
from Satellite import Satellite, Sat_Visualizer
from GyroSetting import GyroSetting
from SttSetting import SttSetting
from Timer import Timer
from PIL import Image
from PIL import ImageOps

axis_verts = (
    (-7.5, 0.0, 0.0),
    (7.5, 0.0, 0.0),
    (0.0, -7.5, 0.0),
    (0.0, 7.5, 0.0),
    (0.0, 0.0, -7.5),
    (0.0, 0.0, 7.5),
)
axes = ((0, 1), (2, 3), (4, 5))
axis_colors = (
    (1.0, 0.0, 0.0),
    (0.0, 1.0, 0.0),
    (0.0, 0.0, 1.0),
)  # Red  # Green  # Blue


"""
       5____________6
       /           /|
      /           / |
    1/__________2/  |
    |           |   |
    |           |   |
    |           |   7
    |           |  /
    |           | /
    0___________3/
"""

cube_verts = (
    (-0.15, -0.2, 0.25),
    (-0.15, 0.2, 0.25),
    (0.15, 0.2, 0.25),
    (0.15, -0.2, 0.25),
    (-0.15, -0.2, -0.25),
    (-0.15, 0.2, -0.25),
    (0.15, 0.2, -0.25),
    (0.15, -0.2, -0.25),
    (-0.15, 0.7, 0.25),
    (0.15, 0.7, 0.25),
    (-0.15, -0.7, 0.25),
    (0.15, -0.7, 0.25),
)

cube_edges = (
    (0, 1),
    (0, 3),
    (0, 4),
    (2, 1),
    (2, 3),
    (2, 6),
    (5, 1),
    (5, 4),
    (5, 6),
    (7, 3),
    (7, 4),
    (7, 6),
    (8, 1),
    (8, 9),
    (9, 2),
    (10, 0),
    (10, 11),
    (11, 3),
)

cube_surfaces = (
    (0, 1, 2, 3),  # Front
    (3, 2, 6, 7),  # Right
    (7, 6, 5, 4),  # Left
    (4, 5, 1, 0),  # Back
    (1, 5, 6, 2),  # Top
    (4, 0, 3, 7),  # Bottom
    (1, 8, 9, 2),  # Sap1
    (0, 10, 11, 3),  # Sap2
)

cube_colors = (
    (1.0, 1.0, 1.0),  # White
    (1.0, 1.0, 1.0),  # White
    (1.0, 1.0, 1.0),  # White
    (0.769, 0.118, 0.227),  # Red
    (1.0, 1.0, 1.0),  # White
    (1.0, 1.0, 1.0),  # White
    (1.0, 1.0, 1.0),  # White
    (1.0, 1.0, 1.0),  # White
    (0.3, 0.835, 0.3),  # Yellow
    (0.3, 0.835, 0.3),  # Yellow
)


def Axis():
    glBegin(GL_LINES)
    for color, axis in zip(axis_colors, axes):
        glColor3fv(color)
        for point in axis:
            glVertex3fv(axis_verts[point])
    glEnd()


def Cube():
    glBegin(GL_QUADS)
    for color, surface in zip(cube_colors, cube_surfaces):
        glColor3fv(color)
        for vertex in surface:
            glVertex3fv(cube_verts[vertex])
    glEnd()

    glBegin(GL_LINES)
    glColor3fv((0.0, 0.0, 0.0))
    for edge in cube_edges:
        for vertex in edge:
            glVertex3fv(cube_verts[vertex])
    glEnd()


def main():
    step = -1
    intvl = 10
    imgs = []

    inertia = np.array([0.5, 0.32, 0.62])
    init_q = -np.array([1, 0.0, 0, 0])
    init_omega = 0.000001 * np.array([1, 1, 1]) * np.pi / 180  # 1 / 3.2])
    init_rw_vel = -0 * np.array([0.35, 0.35, 0.35]) / 0.000734
    k_omega = 0.8 * np.array([[0.5, 0, 0], [0, 0.32, 0], [0, 0, 0.62]])
    kq = 0.006 * np.array([[0.5, 0, 0], [0, 0.32, 0], [0, 0, 0.62]])
    kd_omega = 0.000001 * np.array([[0.5, 0, 0], [0, 0.32, 0], [0, 0, 0.62]])
    kqi = 0.00002 * np.array([[0.5, 0, 0], [0, 0.32, 0], [0, 0, 0.62]])
    gainsetting = GainSetting(kq, kqi, k_omega, kd_omega)
    gyrosetting = GyroSetting(
        0,
        0.3 / 60 / 60 * 3 * np.random.randn(3) * 2 * np.pi / 180,
        0.15 / 60 * 2 * np.pi / 180,
        0.05,
    )
    sttsetting = SttSetting(2 / 3600, 10 / 3600, 0.1)
    dt = 0.0025
    timer = Timer(dt)
    sat1 = Satellite(
        inertia,
        init_q,
        init_omega,
        init_rw_vel,
        timer,
        gainsetting,
        gyrosetting,
        sttsetting,
    )
    time = 0
    calc_timer = 0
    dynamics = Dynamics(dt, sat1)

    pygame.init()
    display = (1000, 818)
    pygame.display.set_mode(display, DOUBLEBUF | OPENGL)

    # Using depth test to make sure closer colors are shown over further ones
    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LESS)

    # Default view
    glMatrixMode(GL_PROJECTION)
    gluPerspective(40, (display[0] / display[1]), 0.1, 50.0)
    glTranslatef(-0, -1, -8)
    glRotatef(90.0, 0.0, 1.0, 0.0)
    glRotatef(-90.0, 1.0, 0.0, 0.0)
    glRotatef(45.0, 0.0, 0.0, 1.0)

    time = 0

    while True:
        dynamics.update_time()
        sat1.reserve_current_state()
        sat1.update_true_vec()
        sat1.update_observe_vec()
        sat1.estimate_att_by_triad()
        sat1.estimate_att_by_foam()
        sat1.observe_omega()
        sat1.observe_quat()
        sat1.update_rw(dt)
        timer.update_time()
        sat1.calc_torque()
        time = time + dt
        calc_timer += dt
        step += 1

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                vis = Sat_Visualizer(dt, sat1)
                vis.visualize_sat_motion()
                vis.visualize_omega()
                vis.visualize_quat()
                vis.compare_observation()
                return imgs

            if event.type == pygame.KEYDOWN:
                # Rotating about the x axis
                if event.key == pygame.K_DOWN:
                    sat1.omega += np.array([0, 0.3, 0.3 / 3.2])

        x = sat1.q[1]  # -time*0.1
        y = sat1.q[2]  # time*0.1
        z = sat1.q[3]  # np.cos(time*0.1) #0.72-time*0.01
        w = sat1.q[0]  # +time*0.01

        mat4 = np.array(
            [
                [
                    1 - 2 * y * y - 2 * z * z,
                    2 * x * y - 2 * z * w,
                    2 * x * z + 2 * y * w,
                    0,
                ],
                [
                    2 * x * y + 2 * z * w,
                    1 - 2 * x * x - 2 * z * z,
                    2 * y * z - 2 * x * w,
                    0,
                ],
                [
                    2 * x * z - 2 * y * w,
                    2 * y * z + 2 * x * w,
                    1 - 2 * x * x - 2 * y * y,
                    0,
                ],
                [0, 0, 0, 1],
            ],
            "f",
        )

        glMatrixMode(GL_MODELVIEW)
        glLoadMatrixf(mat4)

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        Cube()
        Axis()
        pygame.display.flip()
        if step % intvl != 0:
            continue
        else:
            pad_step = "{0:04d}".format(step)
            savepath = "img/tutorial3_" + pad_step + ".png"

            width = 1000  # glutGet(GLUT_WINDOW_WIDTH)
            height = 800  # glutGet(GLUT_WINDOW_HEIGHT)

            glReadBuffer(GL_FRONT)
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
            data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)

            # image = Image.fromstring("RGBA", (width, height), data)
            image = Image.frombytes("RGBA", (width, height), data)
            image = ImageOps.flip(image)
            # image.save( savepath )
            imgs.append(image)
        # pygame.time.wait(10)

if __name__ == "__main__":
    imgs = main()
    imgs[0].save('gif/no_momentum.gif'
            , save_all=True
            , append_images=imgs[1:]
            , optimize=False
            , duration=100 #40
            , loop=0)
    pygame.quit()
    quit()

こんな感じで運動が見れる