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.
"""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.
"""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.
"""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.
"""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.
"""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")