orbitopt flight log — a scroll is a mission
Open-source · orbital trajectory optimization

Screen fast.
Verify exactly.

orbitopt pairs pykep + pygmo's cheap, population-based search with tudatpy's high-fidelity numerical propagation — CUDA-batched where it counts — to find and confirm real trajectories, from a single Lambert transfer to a GTO → GEO orbit-raising campaign.

T+00:04 · Staging

Every mission runs the same two-burn sequence

A screening optimizer explores a huge search space cheaply; a numerical verifier re-flies only the winner under real physics. Neither stage does the other's job.

Stage 1 — Screen

pykep + pygmo

Cheap, analytic patched-conic models — Lambert arcs, multi-gravity-assist sequences — searched with population-based global optimizers (pso_gen, cmaes, nsga2). Fast enough to sweep a huge search space; blind to perturbations.

Stage 2 — Verify

tudatpy

Numerical, N-body-capable propagation confirms a screened solution actually holds once Earth/Moon/Sun gravity — J2+J22 for GEO — is included. Expensive per call, so it only ever re-checks the winner.

Throughout — Batch

CUDA via CuPy

Thousands to millions of independent candidates per run, batched into vectorized array ops — GPU when available, numpy otherwise, same code path either way (orbitopt.core.gpu).

T+00:11 · Manifest

Four real missions, one viewer

Every mission — built-in or your own — is the same SceneData JSON document, opened by the same PyVista/PySide6 renderer: Mission Control.

Solar System

Live heliocentric snapshot of the Sun and eight planets — real 2K textures, true sidereal spin rates, positions from pykep ephemerides.

orbitopt view solar-system

Artemis II Free Return

Lambert-seeded, GPU-screened, tudatpy-refined Earth–Moon free return. Independently re-solved, not fit to unpublished flight data.

perilune target 6,545 km — ~10 km in 3 Newton iterations

GOES — GTO to GEO

Real Atlas V/Centaur launch-to-GTO profile, continuously propagated through an optimized minimum-burn-count apogee-raising campaign.

min. finite-burn-feasible schedule, J2+J22+Sun/Moon verified

Config-driven runs

NEW

orbitopt validate / orbitopt run: describe a mission in YAML, get a schema-checked, physically-verified scene file.

strict JSON-Schema config + the real verify_geo_raising pass
⇠ swipe / scroll for the full manifest ⇢
T+00:19 · Telemetry

Measured, not assumed

Speedup grows with batch size — GPU kernel-launch overhead dominates at small N, so a batched routine only pays off once the batch is large enough to amortize it. That threshold is measured per-routine.

4.7×

# 2,000,000 independent Lambert solves — 82.8s CPU (24.2k/s) → 17.7s GPU (112.8k/s), RTX A3000 Laptop 6GB

90,000-cell Earth–Mars porkchop grid: 1.08s · 83.2k solves/s, one batched GPU call
at 200k candidates the Lambert speedup was ~2.5×; at 2M it's 4.7×
Earth to Mars porkchop plot: departure date vs. arrival date contour grid of total delta-v, generated from one batched GPU call
FIG. 1 — Earth→Mars porkchop grid, one batched GPU call — examples/04_porkchop_gpu.py
T+00:27 · Systems

Three layers, one scene format

Reading and viewing a scene someone already computed doesn't need pykep, pygmo, or tudatpy — the package is split so that's true in practice, not just in principle.

01 · base install

Scene format

pip install orbitopt — stdlib + jsonschema only. read_scene / write_scene / validate_scene against a versioned, independent JSON Schema.

02 · +viewer extra

3D viewer + Mission Control

pip install orbitopt[viewer] — real PyPI wheels (numpy, PyVista, PySide6). Opens any scene file, yours or a colleague's.

03 · conda environment

Compute stack

pykep + pygmo + tudatpy ship only on conda-forge/tudat-team. environment.yml builds the layer that actually optimizes and verifies trajectories.

# src/orbitopt/
core/        gpu.py, problem.py        # numpy/cupy switch, pygmo UDP base
lambert/     gpu_batch.py, cpu.py      # batched + reference Izzo solvers
dynamics/    nbody_gpu.py              # GPU-batched coarse Earth-Moon propagator
problems/    transfer_2body.py, mga.py
             free_return.py, geo_raising.py
optimize/    gpu_bfe.py, runner.py     # pygmo + custom batch fitness evaluator
verify/      tudat_propagate.py
             differential_correction.py
             geo_insertion.py          # tudatpy-based, high-fidelity checks
viz/         scene_renderer.py         # one PyVista draw path, every scene
             app.py                    # Mission Control (PySide6 + pyvistaqt)
             solar_system.py, mission_timeline.py, geo_raising.py
mission_config.py              # YAML config -> validated -> built scene
missions.py                    # named-mission registry + plugin entry points
scene_format.py                # the format's entire read/write/validate surface
T+00:35 · Anomaly log

Bugs that cost real debugging time

Kept in the README on purpose — root-caused and measured, not smoothed over.

01
pygmo · silent 15× slowdown

Attach bfe before wrapping. It has to go on the raw UDA (uda.set_bfe(bfe)) before pg.algorithm(uda). Attaching it after silently falls back to evaluating one candidate at a time — ~15× slower, no error raised anywhere.

02
SPICE · frame mismatch

J2000ECLIPJ2000. They're related by Earth's ~23.4° obliquity and aren't interchangeable — mixing states queried in one against the other silently produces errors of hundreds of thousands of km.

03
RK4 · step size at close approach

Fine for a coast, not for a flyby. A 600s step reported an 81,733 km closest lunar approach for a trajectory whose true, converged (≤15s step) closest approach was 4,379 km.

T+00:42 · Go / No-go

From a YAML file to a verified mission

No source edits: describe a mission's orbit elements, spacecraft, and optimizer settings, then validate and run it from the command line.

flight-terminal — orbitopt
# view any scene -- no conda, real PyPI wheels only
pip install orbitopt[viewer]
orbitopt view solar-system

# describe a mission -- no source edits
cat my_mission.yaml
mission:
  id: my-geo-mission
  title: My GEO raising campaign
  kind: geo-raising
target:
  geostationary_longitude_deg: -101.0
campaign_search:
  n_burns: { min: 2, max: 6 }
output:
  scene_file: scenes/my-geo-mission.json

# schema-check, then optimize + tudatpy-verify + write
orbitopt validate my_mission.yaml
orbitopt run my_mission.yaml
orbitopt view scenes/my-geo-mission.json

Every field falls back to the real GOES-16 numbers if omitted — the minimal config just re-derives the built-in mission. See the full config schema on GitHub.