import numpy as np
import pandas as pd
def gaussian(x, y, z):
return (
-((x + 3) ** 2 + (y - 1) ** 2)) + np.exp(-((x - 3) ** 2 + (y + 2) ** 2))
np.exp(* z**2
)
= np.linspace(-10, 10, 40)
x = np.linspace(-5, 5, 40)
y = np.linspace(1, 2, 32)
z
= np.meshgrid(x, y, z)
X, Y, Z = gaussian(X, Y, Z)
costs3d = (costs3d - costs3d.min()) / (costs3d.max() - costs3d.min())
costs3d
= np.interp(x, (min(x), max(x)), (-10, 40))
lon = np.interp(y, (min(y), max(y)), (35, 60))
lat = np.interp(z, (min(z), max(z)), (0, 40_000))
alt
= np.meshgrid(lon, lat, alt) lons, lats, alts
10 🙀 Using cost grids
In many trajectory optimization scenarios, the cost function is area-dependent. For example, we may need to avoid areas of convective weather or regions with sensitive climate conditions. In these cases, the TOP optimizer considers a 3D or 4D cost grid as part of the objective function.
To find the optimal set of control variables (i.e., speed, heading, and vertical rate), the cost grid is converted into an interpolant. This allows the non-linear optimal control process to evaluate the combined cost based on flight states (time, positions, and altitudes) throughout the entire trajectory. Subject to the constraints defined in the flight dynamics, the optimizer computes a trajectory that minimizes the total cost across the airspace.
Practically, in order to prevent the trajectory from completely deviating from the fuel-optimal route, the cost is combined with the fuel cost to provide a more realistic trajectory. In this chapter, I will explain how one can use a predefined cost grid (in both 3D and 4D) to perform the optimization of trajectories.
10.1 Cost grid in 3D
To demonstrate the use of 3D cost in flight optimization, we will first set up an example cost grid. The example flight we want to optimize is still based on the one from previous chapters, flight with the origin of EHAM
and the destination of LGAV
.
We first generate an artificial 2D cost grid using a Gaussian function, with the x
range of (-10, 10)
and y
range of (-5, 5)
. Then the range is transformed to the longitude and latitude bounds for our flight. The 2D cost grids at different levels are dependent on the altitude, where the higher the altitude, the higher the cost.
We can visualize the cost grid at different altitude as follows:
Show the code used for visulization
import matplotlib.pyplot as plt
= plt.subplots(2, 3, figsize=(12, 8), subplot_kw={"projection": "3d"})
fig, axes for i, ax in enumerate(axes.flatten()):
ax.plot_surface(* 6],
lons[:, :, i * 6],
lats[:, :, i * 6],
costs3d[:, :, i ="tab:blue",
edgecolor=0.5,
lw=0.3,
alpha
)0, 1)
ax.set_zlim(= int(alt[i * 6] // 1000 * 10)
flight_level f"FL{flight_level}")
ax.set_title("longitude")
ax.set_xlabel("latitude")
ax.set_ylabel("cost", rotation=90)
ax.set_zlabel(
plt.show()
We can construct the cost as a flattened 2D DataFrame
. It is important that the DataFrame
has the columns height
, latitude
, longitude
. The height
unit must be in meters.
There is also a built-in logic that raises a warning if the maximum height is higher than 20,000, which can be a common mistake between ft
and meters
when referring to the altitude.
Example of the cost grid as apd.DataFrame
:
= pd.DataFrame(
df_cost_3d 4, -1).T,
np.array([lons, lats, alts, costs3d]).reshape(=["longitude", "latitude", "altitude", "cost"],
columns=lambda x: x.altitude * 0.3048)
).assign(height
df_cost_3d
longitude | latitude | altitude | cost | height | |
---|---|---|---|---|---|
0 | -10.0 | 35.0 | 0.000000 | 3.158413e-38 | 0.000000 |
1 | -10.0 | 35.0 | 1290.322581 | 3.365469e-38 | 393.290323 |
2 | -10.0 | 35.0 | 2580.645161 | 3.579098e-38 | 786.580645 |
3 | -10.0 | 35.0 | 3870.967742 | 3.799300e-38 | 1179.870968 |
4 | -10.0 | 35.0 | 5161.290323 | 4.026075e-38 | 1573.161290 |
... | ... | ... | ... | ... | ... |
51195 | 40.0 | 60.0 | 34838.709677 | 1.785139e-43 | 10618.838710 |
51196 | 40.0 | 60.0 | 36129.032258 | 1.872056e-43 | 11012.129032 |
51197 | 40.0 | 60.0 | 37419.354839 | 1.960458e-43 | 11405.419355 |
51198 | 40.0 | 60.0 | 38709.677419 | 2.050347e-43 | 11798.709677 |
51199 | 40.0 | 60.0 | 40000.000000 | 2.141721e-43 | 12192.000000 |
51200 rows × 5 columns
With this grid cost, we need to define an interpolant
based on this grid and an objective
function which is a combination of the grid cost and fuel cost.
from openap import top, aero
= top.CompleteFlight("A320", "EHAM", "LGAV", m0=0.85)
optimizer # optimizer.setup(debug=True)
= top.tools.interpolant_from_dataframe(df_cost_3d)
interpolant
def objective(x, u, dt, **kwargs):
"""The final objective is the compound of grid cost and fuel"""
= optimizer.obj_grid_cost(x, u, dt, time_dependent=True, **kwargs)
grid_cost = optimizer.obj_fuel(x, u, dt, **kwargs)
fuel_cost return grid_cost + fuel_cost * 2
# generate optimal flight trajectory
# interpolant is passed to trajectory(), and internally used by obj_grid()
= optimizer.trajectory(objective=objective, interpolant=interpolant) flight
Once the optimization is complete, we can visualize the trajetory with the builtin vis.trajectory()
function:
top.vis.trajectory(flight) plt.show()
We can create visualization code so that the trajectory is plotted along the cost grid at different altitudes. In the following figure, you can see the flight avoided the regions with highest cost, while maintaining a small detour to avoid excess fuel consumption.
Show the function flight_level_cost_3d() used for visulization
def flight_level_cost_3d(flight, df_cost):
from cartopy import crs as ccrs
from cartopy.feature import BORDERS
= ccrs.PlateCarree()
proj
= plt.subplots(
fig, axes 3,
2,
=(9, 9),
figsize=dict(
subplot_kw=ccrs.TransverseMercator(
projection=15, central_latitude=45
central_longitude
)
),
)
for i, ax in enumerate(axes.flatten()):
-10, 40, 32, 60])
ax.set_extent([=0.5, color="gray")
ax.add_feature(BORDERS, lw="110m", lw=0.5, color="gray")
ax.coastlines(resolution
= df_cost.height.unique()[i * 6]
h = int(h / 0.3048 // 1000 * 10)
fl
= df_cost.query(f"height=={h}").pivot(
df_cost_pivot ="latitude", columns="longitude", values="cost"
index
)
= (
lat, lon, val
df_cost_pivot.index.values,
df_cost_pivot.columns.values,
df_cost_pivot.values,
)
ax.contourf(=proj, alpha=0.8, cmap="Reds", vmin=0, vmax=1
lon, lat, val, transform
)
0.03, 0.9, f"FL{fl}", transform=ax.transAxes, fontsize=14)
ax.text(
="k", lw=1, transform=proj)
ax.plot(flight.longitude, flight.latitude, color
for r, p in (flight.iloc[[0, -1]]).iterrows():
="k", transform=proj)
ax.scatter(p.longitude, p.latitude, c
plt.tight_layout() plt.show()
flight_level_cost_3d(flight, df_cost_3d)
Next, we modify the combined objective (grid and fuel) so that the weight of the grid cost is higher. And we can see that the optimized trajectory takes a longer detour to avoid regions with high grid cost with more fuel burnt.
def objective_2(x, u, dt, **kwargs):
"""The final objective is the compound of grid cost and fuel"""
= optimizer.obj_grid_cost(x, u, dt, time_dependent=True, **kwargs)
grid_cost = optimizer.obj_fuel(x, u, dt, **kwargs)
fuel_cost return grid_cost * 2 + fuel_cost
= optimizer.trajectory(objective=objective_2, interpolant=interpolant)
flight_2
flight_level_cost_3d(flight_2, df_cost_3d)
10.2 Cost grid in 4D
The usage of 4D cost grid is similar to 3D cost grid. The only difference is that the 4D cost grid includes a time dimension.
In the following example, we extend the 3D cost grid by adding a time dimension. The time dimension can be represented as a sequence of 3D cost grids at different time steps.
import numpy as np
import pandas as pd
def gaussian(x, y, z, t):
return (
-((x + 3 - i) ** 2 + (y - 1) ** 2))
np.exp(+ np.exp(-((x - 3 + i / 2) ** 2 + (y + 2) ** 2))
* z**2
)
= np.linspace(-8, 8, 40)
x = np.linspace(-4, 4, 40)
y = np.linspace(1, 2, 32)
z = np.meshgrid(x, y, z)
X, Y, Z
# add the time dimension
= np.arange(0, 8 * 1800, 1800) # every 30 minutes
ts = np.zeros((len(x), len(y), len(z), len(ts)))
costs4d
for i, ts_ in enumerate(ts):
= gaussian(X, Y, Z, ts)
costs3d = costs3d
costs4d[:, :, :, i]
= (costs4d - costs4d.min()) / (costs4d.max() - costs4d.min())
costs4d
# scale the x,y to lon,lat bound
= np.interp(x, (min(x), max(x)), (-10, 40))
lon = np.interp(y, (min(y), max(y)), (35, 60))
lat = np.interp(z, (min(z), max(z)), (0, 40_000))
alt
= np.meshgrid(lon, lat, alt, ts) lons, lats, alts, tss
The 4D cost grid can be visualized at different time steps and altitudes as follows. Each rows represent the cost grid at different flight levels, and each column represents the cost grid at different time steps.
Show the code used for visulization
import matplotlib.pyplot as plt
= plt.subplots(4, 4, figsize=(16, 18), subplot_kw={"projection": "3d"})
fig, axes for i, ax in enumerate(axes.flatten()):
ax.plot_surface(// 4 * 6, i % 4 * 2],
lons[:, :, i // 4 * 6, i % 4 * 2],
lats[:, :, i // 4 * 6, i % 4 * 2],
costs4d[:, :, i ="tab:blue",
edgecolor=0.5,
lw=0.3,
alpha
)0, 1)
ax.set_zlim(= int(alt[i // 4 * 6] // 1000 * 10)
flight_level = ts[i % 4 * 2] / 1800 / 2
time f"FL{flight_level} | {int(time)}h")
ax.set_title("longitude")
ax.set_xlabel("latitude")
ax.set_ylabel("cost", rotation=90)
ax.set_zlabel(
plt.show()
Similar to the 3D cost grid, we can construct the 4D cost grid as a pd.DataFrame
. The DataFrame
has the columns height
, latitude
, longitude
, and ts
. The height
unit must be in meters.
= pd.DataFrame(
df_cost_4d 5, -1).T,
np.array([lons, lats, alts, tss, costs4d]).reshape(=["longitude", "latitude", "altitude", "ts", "cost"],
columns=lambda x: x.altitude * 0.3048)
).assign(height
df_cost_4d
longitude | latitude | altitude | ts | cost | height | |
---|---|---|---|---|---|---|
0 | -10.0 | 35.0 | 0.0 | 0.0 | 4.836940e-23 | 0.0 |
1 | -10.0 | 35.0 | 0.0 | 1800.0 | 8.078512e-28 | 0.0 |
2 | -10.0 | 35.0 | 0.0 | 3600.0 | 1.826010e-33 | 0.0 |
3 | -10.0 | 35.0 | 0.0 | 5400.0 | 5.615118e-40 | 0.0 |
4 | -10.0 | 35.0 | 0.0 | 7200.0 | 3.049747e-38 | 0.0 |
... | ... | ... | ... | ... | ... | ... |
409595 | 40.0 | 60.0 | 40000.0 | 5400.0 | 1.995862e-32 | 12192.0 |
409596 | 40.0 | 60.0 | 40000.0 | 7200.0 | 6.490450e-26 | 12192.0 |
409597 | 40.0 | 60.0 | 40000.0 | 9000.0 | 2.871462e-20 | 12192.0 |
409598 | 40.0 | 60.0 | 40000.0 | 10800.0 | 1.719263e-15 | 12192.0 |
409599 | 40.0 | 60.0 | 40000.0 | 12600.0 | 1.393133e-11 | 12192.0 |
409600 rows × 6 columns
With the 4D cost grid, we can again define an interpolant
based on this grid and an objective
function which is a combination of the grid cost and fuel cost.
from openap import top, aero
= top.CompleteFlight("A320", "EHAM", "LGAV", m0=0.85)
optimizer # optimizer.setup(debug=True)
= top.tools.interpolant_from_dataframe(df_cost_4d)
interpolant
def objective(x, u, dt, **kwargs):
"""The final objective is the compound of grid cost and fuel"""
= optimizer.obj_grid_cost(
grid_cost =4, time_dependent=True, **kwargs
x, u, dt, n_dim
)= optimizer.obj_fuel(x, u, dt, **kwargs)
fuel_cost return grid_cost + fuel_cost * 2
# generate the flight trajectory
# interpolant is passed to trajectory(), and internally used by obj_grid()
= optimizer.trajectory(objective=objective, interpolant=interpolant) flight
Once the optimization is complete, we can visualize the trajetory with the builtin vis.trajectory()
function:
top.vis.trajectory(flight) plt.show()
We can create visualization code so that the trajectory is plotted along the cost grid at different altitudes and time steps. In the following figure, you can see the flight avoided the regions with highest cost, while maintaining a small detour to avoid excess fuel consumption. This is similar to the 3D cost grid, but with the added time dimension.
Show the function flight_level_cost_4d() used for visulization
def flight_level_cost_4d(flight, df_cost):
from cartopy import crs as ccrs
from cartopy.feature import BORDERS
= ccrs.PlateCarree()
proj
= plt.subplots(
fig, axes 3,
2,
=(9, 9),
figsize=dict(
subplot_kw=ccrs.TransverseMercator(
projection=15, central_latitude=45
central_longitude
)
),
)
for i, ax in enumerate(axes.flatten()):
-10, 40, 32, 60])
ax.set_extent([=0.5, color="gray")
ax.add_feature(BORDERS, lw="110m", lw=0.5, color="gray")
ax.coastlines(resolution
= df_cost.query(
df_cost_pivot f"height=={df_cost.height.max()} and ts=={i*1800}"
="latitude", columns="longitude", values="cost")
).pivot(index
= (
lat, lon, val
df_cost_pivot.index.values,
df_cost_pivot.columns.values,
df_cost_pivot.values,
)
=proj, alpha=0.7, cmap="Purples")
ax.contourf(lon, lat, val, transform
= flight.query(f"{i*1800}<ts<{i*1800+600}").iloc[0]
current
ax.text(0.03, 0.9, f"Time={int(current.ts)}s", transform=ax.transAxes, fontsize=14
)
="r", lw=5, transform=proj)
ax.scatter(current.longitude, current.latitude, color
="k", lw=1, transform=proj)
ax.plot(flight.longitude, flight.latitude, color
for r, p in flight.iloc[[0, -1]].iterrows():
="k", transform=proj)
ax.scatter(p.longitude, p.latitude, c
plt.tight_layout() plt.show()
flight_level_cost_4d(flight, df_cost_4d)