Runnable examples

These examples use only the Python standard library and an installed gmatpy wheel. Download each Python file and run it with that wheel’s interpreter:

python quickstart.py

Run all six from a source checkout after installing a matching wheel:

python docs/run_examples.py

The runner starts each example with isolated Python (-I), a fresh process, and a temporary working directory. It does not build or install the package. Each example includes assertions of API behavior or scientific invariants. These checks are limited numerical regressions, not operational qualification or a claim of equivalence with a NASA GMAT installation.

Quickstart: circular orbit

Explicit point-mass settings make the circular-orbit position check independent of the default perturbations. Change the force model deliberately when moving to a perturbed orbit; circular geometry will then no longer be the appropriate invariant.

Download quickstart.py

"""Propagate a circular orbit with point-mass Earth gravity."""

import math

import gmatpy as gmat

epoch = "2020-07-20T12:00:00Z"
mu = 398600.4415  # Earth's GMAT gravitational parameter, km^3/s^2.
radius_km = 7000.0
state = (radius_km, 0.0, 0.0, 0.0, math.sqrt(mu / radius_km), 0.0)
forces = gmat.ForceModel(degree=0, sun=False, moon=False, srp=False)

# These are elapsed seconds from epoch, not incremental step lengths.
samples = gmat.propagate(state, epoch, [0, 300, 600], forces=forces)

assert [sample.time_s for sample in samples] == [0, 300, 600]
assert math.dist(samples[0].state, state) < 1e-9
angle = math.sqrt(mu / radius_km**3) * 600
expected_position = (radius_km * math.cos(angle), radius_km * math.sin(angle), 0.0)
assert math.dist(samples[-1].position_km, expected_position) < 1e-5
assert abs(math.hypot(*samples[-1].position_km) - radius_km) < 1e-5

last = samples[-1]
print(f"t = {last.time_s:.0f} s; radius = {math.hypot(*last.position_km):.6f} km")
print("position (km):", tuple(round(value, 6) for value in last.position_km))

Time, frames, elements, and ephemeris

This combines a conventional MJD check, osculating Keplerian elements, a complete position/velocity frame conversion, and the geometric Earth–Moon state. EarthFixed uses GMAT’s FK5/IAU1980 convention; it is not advertised as ITRF or TEME. The offsets checked below apply to the specific July 2020 epoch.

Download utilities.py

"""Convert time, orbital elements, coordinate frames, and a Moon state."""

from datetime import datetime, timezone
import math

import gmatpy as gmat

epoch = "2020-07-20T12:00:00Z"
state = (7000.0, 100.0, -200.0, -0.5, 7.5, 1.0)

# Conventional MJD uses the 1858-11-17 origin in this interface.
utc_mjd = gmat.epoch_mjd(epoch)
expected_mjd = (
    datetime(2020, 7, 20, 12, tzinfo=timezone.utc)
    - datetime(1858, 11, 17, tzinfo=timezone.utc)
).total_seconds() / 86400
assert utc_mjd == expected_mjd
tai_mjd = gmat.epoch_mjd(epoch, scale="TAI")
tt_mjd = gmat.epoch_mjd(epoch, scale="TT")
assert abs((tai_mjd - utc_mjd) * 86400 - 37.0) < 2e-6
assert abs((tt_mjd - utc_mjd) * 86400 - 69.184) < 2e-6

# Elements are [SMA_km, e, i_deg, RAAN_deg, AOP_deg, true_anomaly_deg].
elements = gmat.convert_state(state, to_type="Keplerian")
restored = gmat.convert_state(elements, from_type="Keplerian", to_type="Cartesian")
assert math.dist(restored, state) < 2e-8

# Convert all six components: EarthFixed velocity includes frame rotation.
fixed = gmat.transform_state(state, epoch, to_frame="EarthFixed")
inertial = gmat.transform_state(
    fixed, epoch, from_frame="EarthFixed", to_frame="EarthMJ2000Eq"
)
assert math.dist(inertial, state) < 1e-6
assert abs(math.hypot(*fixed[:3]) - math.hypot(*state[:3])) < 1e-6

# This is a geometric MJ2000Eq state, with no light-time correction.
moon = gmat.body_state("Moon", epoch, origin="Earth")
assert 300000 < math.hypot(*moon[:3]) < 450000
assert gmat.body_state("Earth", epoch, origin="Moon") == tuple(-v for v in moon)

print(f"UTC MJD = {utc_mjd:.6f}; TT MJD = {tt_mjd:.9f}")
print(f"Osculating SMA = {elements[0]:.6f} km")
print(f"Geometric Earth–Moon distance = {math.hypot(*moon[:3]):.1f} km")

Impulses and two spacecraft

A 10 m/s impulse in the velocity direction affects only the selected spacecraft. Samples at the burn epoch are post-burn, and duplicate output times return identical values. Impulses do not consume mass. The single-spacecraft and multi-spacecraft helpers are checked against each other.

Download impulses.py

"""Compare a maneuvering spacecraft with an unburned companion."""

import math

import gmatpy as gmat

epoch = "2020-07-20T12:00:00Z"

mu = 398600.4415
state = (7000.0, 0.0, 0.0, 0.0, math.sqrt(mu / 7000), 0.0)
forces = gmat.ForceModel(degree=0, sun=False, moon=False, srp=False)
times = [0, 60, 60, 120]
# VNB's first component follows the instantaneous velocity; 0.01 km/s = 10 m/s.
burn = gmat.Impulse(time_s=60, delta_v_km_s=(0.01, 0.0, 0.0), frame="VNB")
result = gmat.propagate_many(
    {"maneuvering": state, "companion": state},
    epoch,
    times,
    forces=forces,
    impulses={"maneuvering": [burn]},
)
baseline = gmat.propagate(state, epoch, times, forces=forces)
for companion, reference in zip(result["companion"], baseline):
    assert math.dist(companion.state, reference.state) < 1e-7

before = baseline[1]
after = result["maneuvering"][1]
assert math.dist(after.position_km, before.position_km) < 1e-7
expected_velocity = tuple(v + 0.01 * v / math.hypot(*before.velocity_km_s)
                          for v in before.velocity_km_s)
assert math.dist(after.velocity_km_s, expected_velocity) < 1e-10
assert result["maneuvering"][1] == result["maneuvering"][2]

# The single-spacecraft helper uses the same maneuver semantics.
single = gmat.propagate_mission(state, epoch, times, forces=forces, impulses=[burn])
assert math.dist(single[-1].state, result["maneuvering"][-1].state) < 1e-7
separation = math.dist(result["maneuvering"][-1].position_km,
                       result["companion"][-1].position_km)
assert separation > 0.5
print(f"Post-burn samples at 60 s agree; separation at 120 s = {separation:.6f} km")

Target an instantaneous SMA change

target_sma uses the native differential corrector to solve a tangential VNB burn at the input epoch. It supports Earth-centered elliptic starting orbits; the requested post-burn SMA is not a transfer trajectory or time-of-flight constraint. This circular starting orbit also permits an independent vis-viva check of the solved delta-v.

Download target_sma.py

"""Solve for an instantaneous tangential burn to a specified SMA."""

import math

import gmatpy as gmat

epoch = "2020-07-20T12:00:00Z"

mu = 398600.4415
radius_km = 7000.0
state = (radius_km, 0.0, 0.0, 0.0, math.sqrt(mu / radius_km), 0.0)
target_km = 7200.0
result = gmat.target_sma(state, epoch, target_km, tolerance_km=1e-5)

# Independent vis-viva check for a tangential burn at the original radius.
expected_dv = math.sqrt(mu * (2 / radius_km - 1 / target_km)) - state[4]
assert abs(result.delta_v_km_s - expected_dv) < 2e-8
assert abs(result.residual_km) <= 1e-5
specific_energy = (sum(v * v for v in result.final_state[3:]) / 2
                   - mu / math.hypot(*result.final_state[:3]))
assert abs(-mu / (2 * specific_energy) - target_km) < 1e-5
print(f"Tangential delta-v = {result.delta_v_km_s * 1000:.6f} m/s")
print(f"Post-burn SMA = {result.achieved_sma_km:.6f} km")

Trusted script: finite burn

Download both files into the same directory. The Python driver reads the script beside itself and replaces the gravity-file placeholder with the installed wheel’s data path in a temporary script. Run trusted_script.py to execute a 1 N, 10 s chemical-thruster burn with propellant depletion, then compare its mass flow and velocity change with independent calculations. No optional propulsion plugin is required for this core script.

run_script executes trusted GMAT commands with the process’s permissions. Scripts may write files and run command loops; they are not sandboxed. Its outputs mapping requests runtime fields and receives copied strings. A script using unavailable plugins or arbitrary other GMAT facilities is outside this example’s coverage.

Download trusted_script.py and finite_burn.script.

"""Run the accompanying trusted finite-burn script and copy its outputs."""

import math
from pathlib import Path
import tempfile

import gmatpy as gmat


# Resolve bundled data from the installed package, never a local GMAT checkout.
gravity_file = Path(gmat.__file__).resolve().parent / "data/gravity/earth/EGM96.cof"
template = Path(__file__).with_name("finite_burn.script").read_text(encoding="utf-8")
fields = ["X", "Y", "Z", "VX", "VY", "VZ", "TotalMass"]
with tempfile.TemporaryDirectory(prefix="gmatpy-example-") as directory:
    script = Path(directory) / "finite_burn.script"
    script.write_text(template.replace("__GMATPY_EGM96__", gravity_file.as_posix()),
                      encoding="utf-8")
    copied = gmat.run_script(script, outputs={"Sat": fields})["Sat"]

# run_script returns copied GetField strings; convert numerical fields explicitly.
assert all(isinstance(value, str) for value in copied.values())
final = {name: float(value) for name, value in copied.items()}
assert all(math.isfinite(value) for value in final.values())
# 1 N for 10 s, Isp = 300 s, native thruster gravity constant = 9.81 m/s^2.
expected_mass = 1010 - 10 / (300 * 9.81)
assert abs(final["TotalMass"] - expected_mass) < 2e-7

initial = (7000.0, 0.0, 0.0, 0.0, 7.546053287267836, 0.0)
coast = gmat.propagate(
    initial, "2020-07-20T12:00:00Z", [10], max_step_s=1,
    forces=gmat.ForceModel(degree=0, sun=False, moon=False, srp=False),
)[0]
expected_dv = 300 * 9.81 * math.log(1010 / expected_mass) / 1000
assert abs(final["VY"] - coast.velocity_km_s[1] - expected_dv) < 1e-7
assert final["Y"] > coast.position_km[1]
print(f"Final mass = {final['TotalMass']:.9f} kg")
print(f"Along-thrust velocity change ≈ {expected_dv * 1000:.6f} m/s")

Accompanying GMAT script

Create Spacecraft Sat;
Sat.DateFormat = UTCGregorian;
Sat.Epoch = '20 Jul 2020 12:00:00.000';
Sat.CoordinateSystem = EarthMJ2000Eq;
Sat.DisplayStateType = Cartesian;
Sat.X = 7000;
Sat.Y = 0;
Sat.Z = 0;
Sat.VX = 0;
Sat.VY = 7.546053287267836;
Sat.VZ = 0;
Sat.DryMass = 1000;
Create ChemicalTank Tank;
Tank.FuelMass = 10;
Create ChemicalThruster Thrust;
Thrust.CoordinateSystem = Local;
Thrust.Origin = Earth;
Thrust.Axes = MJ2000Eq;
Thrust.ThrustDirection1 = 0;
Thrust.ThrustDirection2 = 1;
Thrust.ThrustDirection3 = 0;
Thrust.Tank = {Tank};
Thrust.DecrementMass = true;
Thrust.C1 = 1;
Thrust.K1 = 300;
Sat.Tanks = {Tank};
Sat.Thrusters = {Thrust};
Create FiniteBurn Burn;
Burn.Thrusters = {Thrust};
Create ForceModel Forces;
Forces.CentralBody = Earth;
Forces.PrimaryBodies = {Earth};
Forces.PointMasses = {};
Forces.Drag = None;
Forces.SRP = Off;
Forces.RelativisticCorrection = Off;
Forces.GravityField.Earth.Degree = 0;
Forces.GravityField.Earth.Order = 0;
Forces.GravityField.Earth.PotentialFile = '__GMATPY_EGM96__';
Create Propagator Prop;
Prop.FM = Forces;
Prop.Type = PrinceDormand78;
Prop.Accuracy = 1e-12;
Prop.InitialStepSize = 1;
Prop.MaxStep = 1;
BeginMissionSequence;
BeginFiniteBurn Burn(Sat);
Propagate Prop(Sat) {Sat.ElapsedSecs = 10};
EndFiniteBurn Burn(Sat);

Scoped native API and error recovery

Native handles belong to their native_session and must never be used afterward. Copy ordinary Python values before leaving the context. Convenience calls that enter the engine and another native session cannot be nested inside an active session. Cleanup still runs when a native exception escapes.

The session exposes bundled SWIG bindings with their native ownership rules; constructing a simple spacecraft does not establish coverage for the full GMAT API or omitted plugins.

Download native_session.py

"""Use scoped native objects and recover cleanly from validation errors."""

import math

import gmatpy as gmat

epoch = "2020-07-20T12:00:00Z"
with gmat.native_session() as api:
    sat = api.Construct("Spacecraft", "ExampleSat")
    sat.SetField("DryMass", 125.0)
    mass_kg = float(sat.GetField("DryMass"))  # Copy a Python value inside the session.
    assert mass_kg == 125.0
    try:
        gmat.epoch_mjd(epoch)
    except RuntimeError as error:
        assert "cannot be nested" in str(error)
    else:
        raise AssertionError("Convenience calls must reject an active native session")
    del sat  # No native object handle is retained after this context exits.

with gmat.native_session() as api:
    assert api.Moderator.Instance().GetConfiguredObject("ExampleSat") is None

try:
    with gmat.native_session() as api:
        api.Construct("GravityField", "InvalidGravity").SetField("Degree", -1)
except Exception as error:
    # Native API exceptions vary by operation; inspect rather than suppress them.
    assert "Degree" in str(error)
else:
    raise AssertionError("Negative gravity degree should be rejected")

# Cleanup runs even when the native operation raises. A fresh helper call works.
assert math.isfinite(gmat.epoch_mjd(epoch))
print(f"Copied spacecraft mass = {mass_kg:.0f} kg; engine recovered after an error")