OSTk Cross-Platform Validation Against GMAT/Orekit (Mission Sequence Scenario 2)

This tutorial demonstrates how to compare OSTk numerical orbit propagation with other tools like Orekit or GMAT. This example will be for a mission sequence that consists of a coasting phases.

import csv
import os

import numpy as np

import pandas as pd

import plotly.graph_objs as go

from ostk.core.filesystem import Directory

from ostk.mathematics.geometry.d3.object import Cuboid
from ostk.mathematics.geometry.d3.object import Composite
from ostk.mathematics.geometry.d3.object import Point

from ostk.physics import Environment
from ostk.physics.environment.atmospheric import Earth as EarthAtmosphericModel
from ostk.physics.environment.gravitational import Earth as EarthGravitationalModel
from ostk.physics.environment.magnetic import Earth as EarthMagneticModel
from ostk.physics.environment.object.celestial import Earth
from ostk.physics.coordinate import Frame
from ostk.physics.coordinate import Position
from ostk.physics.coordinate import Velocity
from ostk.physics.time import DateTime
from ostk.physics.time import Duration
from ostk.physics.time import Instant
from ostk.physics.time import Interval
from ostk.physics.time import Scale
from ostk.physics.unit import Mass

from ostk.astrodynamics import Dynamics
from ostk.astrodynamics.trajectory.state import NumericalSolver
from ostk.astrodynamics.flight.system import SatelliteSystem
from ostk.astrodynamics.trajectory.state import CoordinateSubset
from ostk.astrodynamics.trajectory.state.coordinate_subset import CartesianPosition
from ostk.astrodynamics.trajectory.state.coordinate_subset import CartesianVelocity
from ostk.astrodynamics.trajectory import StateBuilder
from ostk.astrodynamics.trajectory import Propagator
from ostk.astrodynamics.trajectory import State

Set up Comparison Files

Define array inputs that can be changed to include/exclude gmat/orekit result comparisons

title_prefix = "EGM96 60x60 Gravity"
filenames = [
    "gmat_astrodynamics/scenario002-mission-sequence.csv",
    "orekit_astrodynamics/scenario002-mission-sequence.csv",
]
legend_name_list = ["GMAT", "OREKIT"]
multiplication_factors = [
    np.array([1.0e3, 1.0e3, 1.0e3]),
    np.array([1.0, 1.0, 1.0]),
]  # GMAT and Orekit don't output in the same units

Setup Comparison Scenario in OSTk

Define the physical Environment

earth = Earth.from_models(
    EarthGravitationalModel(
        EarthGravitationalModel.Type.EGM96, Directory.undefined(), 60, 60
    ),
    EarthMagneticModel(EarthMagneticModel.Type.Undefined),
    EarthAtmosphericModel(EarthAtmosphericModel.Type.Undefined),
)
environment = Environment(Instant.J2000(), [earth])

Define the SatelliteSystem

mass = Mass(100.0, Mass.Unit.Kilogram)
satellite_geometry = Composite(
    Cuboid(
        Point(0.0, 0.0, 0.0),
        np.eye(3).tolist(),
        [1.0, 0.0, 0.0],
    )
)
inertia_tensor = np.identity(3)
surface_area = 1.0
drag_coefficient = 2.2

satellite_system = SatelliteSystem(
    mass,
    satellite_geometry,
    inertia_tensor,
    surface_area,
    drag_coefficient,
)

Define the initial State of the SatelliteSystem

start_instant = Instant.date_time(DateTime.parse("2023-01-01T00:00:00.000"), Scale.UTC)
initial_state = State(
    start_instant,
    Position.meters(
        np.array([-4283387.412456233, -4451426.776125101, -2967617.850750065]),
        Frame.GCRF(),
    ),
    Velocity.meters_per_second(
        np.array([4948.074939732174, -957.3429532772124, -5721.173027553034]),
        Frame.GCRF(),
    ),
)
state_builder = StateBuilder(
    frame=Frame.GCRF(),
    coordinate_subsets=[
        CartesianPosition.default(),
        CartesianVelocity.default(),
        CoordinateSubset.mass(),
        CoordinateSubset.surface_area(),
        CoordinateSubset.drag_coefficient(),
    ],
)

coordinates = [
    *initial_state.get_coordinates().tolist(),
    mass.in_kilograms(),
    satellite_system.get_cross_sectional_surface_area(),
    satellite_system.get_drag_coefficient(),
]
start_state = state_builder.build(initial_state.get_instant(), coordinates)

Define the Dynamics to consider for Propagation

dynamics = Dynamics.from_environment(environment)

Define a NumericalSolver

numerical_solver = NumericalSolver(
    NumericalSolver.LogType.NoLog,
    NumericalSolver.StepperType.RungeKuttaDopri5,
    30.0,
    1.0e-12,
    1.0e-12,
)

Define a Propagator

propagator = Propagator(numerical_solver, dynamics)

Propagate with OSTk

Define the propagation instants of interest

instants = Interval.closed(
    start_state.get_instant(), start_state.get_instant() + Duration.seconds(86400.0)
).generate_grid(Duration.seconds(120.0))

Now that everything is set up, we can calculate the state arrays from the desired time instant grid

ostk_states = propagator.calculate_states_at(start_state, instants)

Process Cross Platform Results

Read in reference data from CSV file for GMAT and Orekit

all_comparison_states = []

for ind, filename in enumerate(filenames):
    with open(f"{os.getcwd()}/data/{filename}") as csvfile:
        reader = csv.DictReader(csvfile)
        comparison_states = []
        comparison_instants = []
        for row in reader:
            instant_iter = start_instant + Duration.seconds(
                float(row[reader.fieldnames[0]])
            )
            position_iter = Position.meters(
                [
                    row[reader.fieldnames[1]],
                    row[reader.fieldnames[2]],
                    row[reader.fieldnames[3]],
                ],
                Frame.GCRF(),
            )
            velocity_iter = Velocity.meters_per_second(
                [
                    row[reader.fieldnames[4]],
                    row[reader.fieldnames[5]],
                    row[reader.fieldnames[6]],
                ],
                Frame.GCRF(),
            )

            position_iter_m = np.multiply(
                position_iter.get_coordinates(), multiplication_factors[ind]
            )
            velocity_iter_ms = np.multiply(
                velocity_iter.get_coordinates(), multiplication_factors[ind]
            )

            comparison_instants.append(instant_iter)
            comparison_states.append(
                State(
                    instant_iter,
                    Position.meters(position_iter_m, Frame.GCRF()),
                    Velocity.meters_per_second(velocity_iter_ms, Frame.GCRF()),
                )
            )
    all_comparison_states.append(comparison_states)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[12], line 4
      1 all_comparison_states = []
      3 for ind, filename in enumerate(filenames):
----> 4     with open(f"{os.getcwd()}/data/{filename}") as csvfile:
      5         reader = csv.DictReader(csvfile)
      6         comparison_states = []

File /usr/local/lib/python3.11/dist-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
    317 if file in {0, 1, 2}:
    318     raise ValueError(
    319         f"IPython won't let you open fd={file} by default "
    320         "as it is likely to crash IPython. If you know what you are doing, "
    321         "you can use builtins' open."
    322     )
--> 324 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: '/app/docs/_notebooks/data/gmat_astrodynamics/scenario002-mission-sequence.csv'

Trajectory RMS error vs GMAT run in the GCRF frame

def to_dataframe_RMS(state_ind, list_ind):
    return [
        repr(all_comparison_states[list_ind][state_ind].get_instant()),
        float(
            (
                all_comparison_states[list_ind][state_ind].get_instant()
                - all_comparison_states[list_ind][0].get_instant()
            ).in_seconds()
        ),
        (
            np.linalg.norm(
                (
                    all_comparison_states[list_ind][state_ind]
                    .get_position()
                    .get_coordinates()
                    - ostk_states[state_ind].get_position().get_coordinates()
                )
            )
        ),
        (
            np.linalg.norm(
                (
                    all_comparison_states[list_ind][state_ind]
                    .get_velocity()
                    .get_coordinates()
                    - ostk_states[state_ind].get_velocity().get_coordinates()
                )
            )
        ),
    ]
ostk_states_compared = [
    [
        to_dataframe_RMS(state_ind, list_ind)
        for state_ind in range(0, len(all_comparison_states[list_ind]))
    ]
    for list_ind in range(0, len(all_comparison_states))
]
ostk_states_compared_df = [
    pd.DataFrame(
        ostk_states_compared[list_ind],
        columns=["$Time^{UTC}$", "Elapsed secs", "${\delta}x$", "${\delta}v$"],
    )
    for list_ind in range(0, len(all_comparison_states))
]

Validation Plots

Plot position error

orbit_df_RMS_position_list = [
    ostk_states_compared_df[list_ind][["Elapsed secs", "${\delta}x$"]]
    for list_ind in range(0, len(all_comparison_states))
]

figure = go.Figure()
figure.update_layout(
    title=f"Orbit Propagation with {title_prefix}, Position Difference RMS",
    showlegend=True,
    height=1000,
)
figure.update_xaxes(title_text="Time Elapsed (s)")
figure.update_yaxes(title_text="Position Difference (m)")

for list_ind, orbit_df_RMS_position in enumerate(orbit_df_RMS_position_list):
    figure.add_trace(
        go.Scatter(
            x=orbit_df_RMS_position["Elapsed secs"],
            y=orbit_df_RMS_position["${\delta}x$"],
            name=legend_name_list[list_ind],
            mode="lines",
        )
    )

figure.show("svg")

Plot velocity error

orbit_df_RMS_velocity_list = [
    ostk_states_compared_df[list_ind][["Elapsed secs", "${\delta}v$"]]
    for list_ind in range(0, len(all_comparison_states))
]

figure = go.Figure()
figure.update_layout(
    title=f"Orbit Propagation with {title_prefix}, Velocity Difference RMS",
    showlegend=True,
    height=1000,
)
figure.update_xaxes(title_text="Time Elapsed (s)")
figure.update_yaxes(title_text="Velocity Difference (m/s)")

for list_ind, orbit_df_RMS_velocity in enumerate(orbit_df_RMS_velocity_list):
    figure.add_trace(
        go.Scatter(
            x=orbit_df_RMS_velocity["Elapsed secs"],
            y=orbit_df_RMS_velocity["${\delta}v$"],
            name=legend_name_list[list_ind],
            mode="lines",
        )
    )

figure.show("svg")