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()
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()