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 opentop as top
import pandas as pd
warnings.filterwarnings("ignore", message="Warning: Wave drag is experimental")
from opentop.plotting import apply_publication_style # noqa: E402
apply_publication_style()
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:40: 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.999901 | 61587.037907 | 4712.961994 | 23699.999901 | 104.730980 | 35072.0 |
| 1 | cruise: payload optimized | True | Solve_Succeeded | 56600.923280 | 52599.999902 | 4000.923378 | 4000.923280 | 104.610898 | 40013.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.923279707786)}
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()
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:38: 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.999901 | 60965.172400 | 5334.827501 | 23699.999901 | 120.171420 | 35893.0 |
| 1 | complete flight: payload optimized | True | Solve_Succeeded | 57293.421175 | 52599.999902 | 4693.421273 | 4693.421175 | 121.220962 | 40263.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()