Payload-Based Initial Mass Optimization¶

This example shows how to use payload= to let OpenTOP optimize the initial mass. When payload is provided, m0 is used only as the initial guess; the actual initial mass is bounded between OEW + payload and the feasible maximum mass.

In [1]:
import warnings

import matplotlib.pyplot as plt
import pandas as pd

import opentop as top

warnings.filterwarnings("ignore", message="Warning: Wave drag is experimental")

Flight Setup¶

Without payload, m0 fixes the initial mass. With payload, m0 is only an initial guess and the optimizer chooses the initial mass needed for the flight.

In [2]:
actype = "A320"
origin = "EHAM"
destination = "LEMD"

m0 = 0.85
payload = 10_000.0  # kg
nodes = 15
max_iter = 800
In [3]:
def run_case(cls, *, payload=None):
    opt = cls(actype, origin, destination, m0=m0, payload=payload)
    opt.setup(nodes=nodes, max_iter=max_iter)
    df = opt.trajectory(objective="fuel")
    return opt, df


def along_track_distance_km(df):
    if "distance" in df.columns:
        return df.distance / 1000
    segment_m = (df.x.diff().fillna(0) ** 2 + df.y.diff().fillna(0) ** 2) ** 0.5
    return segment_m.cumsum() / 1000


def summarize(label, opt, df):
    dry_payload_mass = opt.oew + (opt.payload or 0.0)
    return {
        "case": label,
        "success": opt.success,
        "status": opt.stats["return_status"],
        "initial_mass_kg": df.mass.iloc[0],
        "final_mass_kg": df.mass.iloc[-1],
        "fuel_burn_kg": df.mass.iloc[0] - df.mass.iloc[-1],
        "estimated_fuel_onboard_kg": df.mass.iloc[0] - dry_payload_mass,
        "flight_time_min": df.ts.iloc[-1] / 60,
        "max_altitude_ft": df.altitude.max(),
    }

Cruise Example¶

In [4]:
cruise_fixed, cruise_fixed_df = run_case(top.Cruise)
cruise_payload, cruise_payload_df = run_case(top.Cruise, payload=payload)

pd.DataFrame(
    [
        summarize("cruise: fixed m0", cruise_fixed, cruise_fixed_df),
        summarize("cruise: payload optimized", cruise_payload, cruise_payload_df),
    ]
)
/home/junzi/arc/code/1-public/opentop/opentop/cruise.py:38: UserWarning: payload is provided; m0 is used only as the initial mass guess and does not fix the initial mass.
  super().__init__(
Out[4]:
case success status initial_mass_kg final_mass_kg fuel_burn_kg estimated_fuel_onboard_kg flight_time_min max_altitude_ft
0 cruise: fixed m0 True Solve_Succeeded 66299.999900 61587.854094 4712.145806 23699.999900 104.725498 35087.0
1 cruise: payload optimized True Solve_Succeeded 56600.404982 52599.999902 4000.405080 4000.404982 104.603878 39991.0

The payload case has a bounded initial mass decision variable. The bounds are visible after init_conditions() or after solving.

In [5]:
{
    "minimum_mass_kg": cruise_payload.mass_min,
    "initial_mass_lower_bound_kg": cruise_payload.mass_init_lb,
    "initial_mass_upper_bound_kg": cruise_payload.mass_init_ub,
    "m0_initial_guess_kg": cruise_payload.mass_init,
    "optimized_initial_mass_kg": cruise_payload_df.mass.iloc[0],
}
Out[5]:
{'minimum_mass_kg': 52600.0,
 'initial_mass_lower_bound_kg': 52600.0,
 'initial_mass_upper_bound_kg': 76810.0,
 'm0_initial_guess_kg': 66300.0,
 'optimized_initial_mass_kg': np.float64(56600.404982006374)}
In [6]:
fig, axes = plt.subplots(2, 1, figsize=(9, 6), sharex=True)

for label, df in {
    "fixed m0": cruise_fixed_df,
    "payload optimized": cruise_payload_df,
}.items():
    distance_km = along_track_distance_km(df)
    axes[0].plot(distance_km, df.altitude, label=label)
    axes[1].plot(distance_km, df.mass, label=label)

axes[0].set_ylabel("Altitude [ft]")
axes[1].set_ylabel("Mass [kg]")
axes[1].set_xlabel("Distance [km]")
axes[0].legend()
axes[1].legend()
fig.tight_layout()
No description has been provided for this image

CompleteFlight Example¶

The same payload= behavior is available for CompleteFlight.

In [7]:
full_fixed, full_fixed_df = run_case(top.CompleteFlight)
full_payload, full_payload_df = run_case(top.CompleteFlight, payload=payload)

pd.DataFrame(
    [
        summarize("complete flight: fixed m0", full_fixed, full_fixed_df),
        summarize("complete flight: payload optimized", full_payload, full_payload_df),
    ]
)
/home/junzi/arc/code/1-public/opentop/opentop/full.py:37: UserWarning: payload is provided; m0 is used only as the initial mass guess and does not fix the initial mass.
  super().__init__(
Out[7]:
case success status initial_mass_kg final_mass_kg fuel_burn_kg estimated_fuel_onboard_kg flight_time_min max_altitude_ft
0 complete flight: fixed m0 True Solve_Succeeded 66299.999905 61004.928140 5295.071765 23699.999905 118.405395 36055.0
1 complete flight: payload optimized True Solve_Succeeded 57270.367327 52599.999901 4670.367427 4670.367327 118.438900 40384.0
In [8]:
fig, axes = plt.subplots(2, 1, figsize=(9, 6), sharex=True)

for label, df in {
    "fixed m0": full_fixed_df,
    "payload optimized": full_payload_df,
}.items():
    distance_km = along_track_distance_km(df)
    axes[0].plot(distance_km, df.altitude, label=label)
    axes[1].plot(distance_km, df.mass, label=label)

axes[0].set_ylabel("Altitude [ft]")
axes[1].set_ylabel("Mass [kg]")
axes[1].set_xlabel("Distance [km]")
axes[0].legend()
axes[1].legend()
fig.tight_layout()
No description has been provided for this image