Skip to main content

Trajectory OPtimizer based on OpenAP model

Project description

opentop: Open Trajectory Optimizer

opentop is v2 of openap-top. The package has been renamed and restructured: what used to be installed as openap-top and imported as openap.top now installs as opentop and imports as import opentop. The three headline changes in v2.0:

  • New Opti-stack backend — NLP construction has moved to CasADi's Opti stack, removing ~400 lines of boilerplate and fixing several long-standing bugs.
  • Standalone packageopentop is no longer a namespace extension of openap; it installs as a top-level package and imports as import opentop.
  • Command-line interfaceopentop optimize and opentop gengrid expose the optimizer and grid-cache builder from the shell.

See What's New in 2.0 below for the full migration table.

Flight trajectory optimizer based on the OpenAP aircraft performance model.

opentop uses non-linear optimal control via direct collocation (CasADi + IPOPT) to generate optimal flight trajectories. It provides simple interfaces for:

  • Complete flight trajectories (takeoff → cruise → landing)
  • Individual phases: climb, cruise, descent
  • Fuel-optimal, time-optimal, cost-index, and climate-optimal objectives
  • Wind integration
  • Custom 4D grid cost functions (contrails, weather, airspace)
  • User-defined objective functions and constraints

🕮 User Guide

Detailed guide and examples: https://openap.dev/optimize.

Install

From PyPI:

pip install --upgrade opentop

From the development branch:

pip install --upgrade git+https://github.com/junzis/openap
pip install --upgrade git+https://github.com/junzis/opentop

opentop is a standalone package. Prior to v2.0 it shipped as openap.top, a namespace extension of openap; v2.0 drops that and installs as a top-level opentop package instead.

Quick Start

A simple optimal flight

import opentop

optimizer = opentop.CompleteFlight("A320", "EHAM", "LGAV", m0=0.85)
flight = optimizer.trajectory(objective="fuel")

flight is a Pandas DataFrame with columns for position, altitude, mass, Mach, TAS, vertical rate, heading, and per-segment fuel_cost.

example_optimal_flight

Other built-in objectives

optimizer.trajectory(objective="time")      # minimum time
optimizer.trajectory(objective="ci:30")     # cost index 30
optimizer.trajectory(objective="gwp100")    # 100-yr global warming potential
optimizer.trajectory(objective="gtp100")    # 100-yr global temperature potential

The supported climate metrics are gwp20, gwp50, gwp100, gtp20, gtp50, gtp100.

Choosing a different engine

optimizer = opentop.CompleteFlight(
    "A320", "EHAM", "LGAV", m0=0.85, engine="CFM56-5B4"
)

Flight phase optimizers

cruise    = opentop.Cruise("A320", "EHAM", "LGAV", m0=0.85).trajectory()
climb     = opentop.Climb("A320", "EHAM", "LGAV", m0=0.85).trajectory()
descent   = opentop.Descent("A320", "EHAM", "LGAV", m0=0.85).trajectory()

Cruise also supports constant-altitude, constant-Mach, and fixed-track modes:

opt = opentop.Cruise("A320", "EHAM", "LGAV", m0=0.85)
opt.fix_cruise_altitude()
opt.fix_mach_number()
opt.fix_track_angle()
flight = opt.trajectory()

Wind integration

Download ERA5 (or similar) meteorological data in GRIB format, then:

import opentop

windfield = opentop.tools.read_grids("wind.grib")

optimizer = opentop.CompleteFlight("A320", "EHAM", "LGAV", m0=0.85)
optimizer.enable_wind(windfield)

flight = optimizer.trajectory(objective="fuel")
opentop.vis.trajectory(flight, windfield=windfield, barb_steps=15)

example_optimal_flight

Custom grid cost (contrails, weather, airspace)

Build a CasADi interpolant from a DataFrame with columns longitude, latitude, height (m), cost, and optionally ts:

interpolant = opentop.tools.interpolant_from_dataframe(df_cost)

def contrail_objective(x, u, dt, **kwargs):
    grid_cost = optimizer.obj_grid_cost(
        x, u, dt, interpolant=kwargs["interpolant"], n_dim=3
    )
    fuel_cost = optimizer.obj_fuel(x, u, dt)
    return grid_cost + fuel_cost

flight = optimizer.trajectory(
    objective=contrail_objective,
    interpolant=interpolant,
    n_dim=3,
)

See https://openap.dev/optimize/contrails.html for a full contrail + CO₂ example.

Custom objective functions

Any callable with signature (x, u, dt, **kwargs) -> ca.MX can be used:

def my_objective(x, u, dt, **kwargs):
    # x: state [xp, yp, h, mass, ts]
    # u: control [mach, vs, psi]
    return your_cost_expression

flight = optimizer.trajectory(objective=my_objective)

Multi-start optimization for non-convex objectives

Trajectory optimization with grid costs, blended objectives, or tight constraints can have multiple local minima — a single solve lands in whichever basin is closest to the initial guess. For problems where robustness matters, multi_start_trajectory runs N solves from different randomized initial guesses and returns the best:

trajectory, candidates = optimizer.multi_start_trajectory(
    objective=contrail_objective,
    interpolant=interp,
    max_fuel=6500,
    n_starts=5,
    lateral_jitter_km=100.0,
    altitude_jitter_ft=3000.0,
    seed=0,
)

Start 0 uses the canonical initial guess (your initial_guess= if provided, otherwise the default great-circle). Starts 1..N-1 are random perturbations of the canonical: sinusoidal lateral bulges (endpoints preserved) and constant altitude offsets.

Returned is a (trajectory, candidates) tuple. trajectory is the winning DataFrame (feasibility-first, then lowest objective). candidates is a best-first ordered list of dicts with per-start metadata (objective, fuel, grid_cost, success, iters, perturbation, wall_time_s, trajectory).

n_starts=1 is identical to calling trajectory() directly, so adding multi_start_trajectory to existing code is additive.

Precomputed grid caches (recommended for contrail + CO₂)

Linear interpolation over a 4D contrail-cost grid has discontinuous derivatives at every grid cell boundary, which can cause IPOPT's line search to oscillate on non-convex blended objectives. The fix is to use a cubic B-spline interpolant, which has continuous derivatives. But building a bspline over a large grid can take several minutes, so we expose a cache utility:

from opentop.tools import cached_interpolant_from_dataframe

interpolant = cached_interpolant_from_dataframe(
    df_cost, "cache/contrail.casadi", shape="bspline"
)

First call builds the bspline and writes it to disk (~1-3 minutes for a 60k-point slice); subsequent calls load the cache in under a second.

If your grid only covers the altitude band where contrails actually form (typically FL200-FL440), extend it with zero-cost levels outside that band before building the interpolant — otherwise opentop.CompleteFlight trajectories that start and end on the ground will query the interpolant outside its data range. The opentop CLI has a helper for this:

opentop gengrid --in raw_grid.parquet --out grid.casadi \
    --bbox 35:57,-9:7 --time 2022-02-20T10:00,2022-02-20T14:00 \
    --pad-altitudes

--pad-altitudes is on by default and adds zero-cost rows at altitudes from 0 to FL480 so the interpolant returns 0 (physically correct — no contrails below ~FL200 or above ~FL440) outside the data band.

Command-line interface

Installing opentop also installs the opentop executable, which exposes two subcommands: optimize (run a trajectory optimization) and gengrid (precompute a grid-cost interpolant).

opentop optimize

Run a trajectory optimization without writing any Python:

opentop optimize EHAM EDDF -a A320 --phase cruise --obj fuel

A concise solver summary (status, iterations, objective, fuel burn, max altitude, flight time) is printed to stdout. Pass -o flight.parquet to also save the full trajectory DataFrame.

Supported objectives: fuel, time, ci:N (cost index, any integer), gwp20/gwp50/gwp100, gtp20/gtp50/gtp100, and grid (requires --grid FILE).

Blended objectives are written as a weighted sum:

opentop optimize EHAM EDDF -a A320 --phase all \
    --obj "0.3*fuel+0.7*grid" \
    --grid contrail.casadi

Common flags:

flag purpose
-a, --aircraft aircraft type (required), e.g. A320
--phase all (CompleteFlight, default), cruise, climb, descent
--obj objective expression — single term or weighted sum
--m0 initial mass as a fraction of MTOW (default 0.85)
--grid cost-grid file; .casadi cache preferred, .parquet accepted with a slow-path warning
--max-iter IPOPT iteration cap (default 1500)
-o, --output write the trajectory DataFrame to a parquet file
-v, --debug verbose IPOPT output

opentop gengrid

Build and cache a CasADi interpolant from a raw cost grid:

opentop gengrid --in raw_grid.parquet --out contrail.casadi \
    --bbox 35:57,-9:7 \
    --time 2022-02-20T10:00,2022-02-20T14:00 \
    --shape bspline

The resulting .casadi file loads in under a second (vs. minutes to rebuild a bspline from raw grid data), so keep it on disk and pass it to opentop optimize --grid.

Use opentop --help, opentop optimize --help, and opentop gengrid --help for the full option list.

opentop replay

Replay a real flight by callsign: pulls trajectory from OpenSky, ERA5 meteo from fastmeteo, and runs an optimization side-by-side with the actual flight.

# Install with the replay extras
pip install "opentop[replay]"

# Flag mode
opentop replay RYR880W --date 2023-01-05 --obj fuel -o ./results/

# Interactive wizard (no arguments)
opentop replay

Outputs under the output directory:

  • actual.parquet — cleaned OpenSky trace
  • optimized.parquet — optimized trajectory
  • trajectory.png — actual-vs-optimized overlay

Requires OpenSky credentials at ~/.config/traffic/traffic.conf (see traffic's docs). ERA5 data is fetched via fastmeteo's ArcoEra5; the default local store is <system tmp>/opentop-era5 — first use will download several GB on a large bbox.

Accessing Solver Results

After calling .trajectory(), the optimizer exposes:

optimizer.stats           # solver statistics dict ("success", "iter_count", ...)
optimizer.success         # True if the most recent solve succeeded
optimizer.objective_value # final objective value (float)

For full structured access including status, iteration count, fuel, and grid cost in one dataclass:

result = optimizer.trajectory(objective="fuel", result_object=True)
# result.df, result.success, result.status, result.objective,
# result.iters, result.fuel, result.grid_cost, result.stats

optimizer.solver still works in v2.2 with a DeprecationWarning; it will be removed in v2.3.

Benchmarks

Run benchmarks across versions to verify performance:

./benchmark.sh                 # Benchmark HEAD (local dev code)
./benchmark.sh v2.0.0          # Benchmark a specific PyPI release
./benchmark.sh v1.11.0 v2.0.0  # Benchmark multiple versions sequentially

Reports land in tests/benchmarks/<version>.txt.

Migrating to 2.2

Version 2.2 makes several structural changes that may require small updates to existing code:

v2.1 v2.2
opentop.MultiPhase(...) opentop.CompleteFlight(...)MultiPhase has been removed
trajectory(objective="fuel", foo=bar) silently tolerates unknown kwargs Unknown kwargs raise TypeError; only documented names are accepted
optimizer.solver.stats() optimizer.stats (dict) or optimizer.success (bool)
opentop.vis.map(df, ...) opentop.vis.plot_map(df, ...)
New: trajectory(..., result_object=True) returns a TrajectoryResult dataclass
New: opentop replay CALLSIGN CLI — fetches real flight from OpenSky, runs comparison optimization (see ### opentop replay above)
New: `opentop.vis.trajectory(df

Internally, opentop/ has been split into focused modules: _dynamics.py, _objectives.py, _trajectory.py, _options.py, _multi_start.py. If you were importing internal helpers, they have moved — use the public API on Base / Cruise / CompleteFlight where possible.

MultiPhase was rarely used and its functionality is fully covered by CompleteFlight. If you had code using MultiPhase, replace it with:

full = opentop.CompleteFlight("A320", "EHAM", "LGAV", m0=0.85).trajectory()

optimizer.solver still works in v2.2 with a DeprecationWarning. It will be removed in v2.3 — prefer optimizer.stats / optimizer.success.

Type annotations are now enforced in CI via pyright (basic mode). Public API signatures are fully annotated; see opentop/_options.py for the new SolveOptions, GridOptions, and TrajectoryResult dataclasses.

What's New in 2.0

Version 2.0 is a major refactor. Most user code keeps the same shape, but a few things have moved:

v1.x v2.0
pip install openap-top pip install opentop
from openap import top import opentop
top.Cruise(...) opentop.Cruise(...)
optimizer.change_engine() dropped opentop.Cruise(..., engine="CFM56-5B4")
optimizer.solution["f"] optimizer.objective_value
optimizer.solver was a ca.nlpsol callable now a ca.OptiSol object
setup(max_iteration=...) setup(max_iter=...)
new CLI: opentop optimize ORIGIN DEST ... and opentop gengrid ...
new opentop.tools.cached_interpolant_from_dataframe() for disk-cached bspline interpolants

The NLP construction moved to CasADi's Opti stack, which removed ~400 lines of boilerplate and cleaned up several bugs. The module rename from openap.top to opentop eliminates the namespace-extension install mode that used to require .pth tricks.

See the changelog for details.

License

GNU LGPL v3

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

opentop-2.2.0.tar.gz (4.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

opentop-2.2.0-py3-none-any.whl (63.0 kB view details)

Uploaded Python 3

File details

Details for the file opentop-2.2.0.tar.gz.

File metadata

  • Download URL: opentop-2.2.0.tar.gz
  • Upload date:
  • Size: 4.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for opentop-2.2.0.tar.gz
Algorithm Hash digest
SHA256 fe1c6bc227ef93c9325ca1c8d419d559c047d1a6418a4b74b55283ebcb941723
MD5 c5c4cf61cab3245965775303c975d4b5
BLAKE2b-256 3a61432d57a5e55616dc1ef53ff3e270e8350f3078da64f08e321cbedd0adec6

See more details on using hashes here.

Provenance

The following attestation bundles were made for opentop-2.2.0.tar.gz:

Publisher: release-publish.yml on junzis/opentop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file opentop-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: opentop-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 63.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for opentop-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ba6241b4d3f5ca6731010853a42807c6b6340c3ad27b7b59b7d1ad82479d3948
MD5 7837a7ecb9159c72b2d9272084f7d290
BLAKE2b-256 23da38c70e470f67b447125ecb657160de5e075fe71def88e5a3c6a440831f18

See more details on using hashes here.

Provenance

The following attestation bundles were made for opentop-2.2.0-py3-none-any.whl:

Publisher: release-publish.yml on junzis/opentop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page