Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
junzis
GitHub Repository: junzis/openap
Path: blob/master/scripts/build_fuel_model.py
592 views
1
# %%
2
import pathlib
3
import warnings
4
from glob import glob
5
6
import matplotlib.pyplot as plt
7
from acropole import FuelEstimator
8
from scipy.optimize import curve_fit
9
from sklearn import cluster
10
from tqdm.autonotebook import tqdm
11
from traffic.core import Flight, Traffic
12
13
import numpy as np
14
import openap
15
import pandas as pd
16
17
pd.options.display.max_columns = 100
18
19
warnings.filterwarnings("ignore")
20
21
# %%
22
23
wave_drag = True
24
show_plot = True
25
26
27
data_dir = "/mnt/md16/data/openap-rebuild/2022/"
28
29
# %%
30
# def func_old(ratio, coef):
31
# return -coef * (ratio - 1) ** 2 + coef
32
33
# def func_try_1(x, a, b, c):
34
# return a / (b + np.exp(-c * x) / x)
35
36
37
# def func(x, c1, c2, c3):
38
# return c1 / (1 + np.exp(-c2 * x + c3))
39
40
41
def func(x, c1, c2, c3):
42
return c1 - np.exp(-c2 * (x * np.exp(c3 * x) - np.log(c1) / c2))
43
44
45
fe = FuelEstimator()
46
47
acropole_aircraft = pd.read_csv("acropole_aircraft_params.csv")
48
49
acropole_typecodes = acropole_aircraft.ACFT_ICAO_TYPE.unique()
50
51
# acropole_typecodes = ["CRJ9", "A318"]
52
53
rng = np.random.default_rng(42)
54
55
results = []
56
57
for typecode in acropole_typecodes:
58
if typecode == "B734": # model is bad
59
continue
60
61
if typecode.lower() not in openap.prop.available_aircraft():
62
print(f"{typecode} not in openap")
63
continue
64
65
aac = acropole_aircraft.query(f"ACFT_ICAO_TYPE=='{typecode.upper()}'")
66
if aac.shape[0] == 0:
67
print(f"{typecode} not in acropole")
68
continue
69
70
engine_type = aac.ENGINE_ICAO.iloc[0]
71
72
ac = openap.prop.aircraft(typecode)
73
eng = openap.prop.engine(ac["engine"]["default"])
74
75
drag = openap.Drag(typecode, wave_drag=wave_drag, use_synonym=True)
76
thrust = openap.Thrust(typecode, use_synonym=True)
77
78
print(typecode, engine_type)
79
80
typecode_file = f"{data_dir}/typecodes/{typecode.lower()}.parquet"
81
82
if pathlib.Path(typecode_file).exists():
83
df_typecode = pd.read_parquet(typecode_file)
84
85
else:
86
files_all_flights = rng.permutation(glob(f"{data_dir}/full_flights/*.parquet"))
87
88
t_typecode = None
89
90
for i, f in tqdm(enumerate(files_all_flights)):
91
t = Traffic.from_file(f).assign(flight_id=lambda d: d.flight_id + f"_{i}")
92
93
t_select = (
94
t.query(f"typecode=='{typecode.lower()}'").longer_than("2h").eval()
95
)
96
97
if t_typecode is None:
98
t_typecode = t_select
99
else:
100
t_typecode = t_typecode + t_select
101
102
n_flights = len(t_typecode.flight_ids) if t_typecode is not None else 0
103
print(f"got {n_flights} flights | {pathlib.Path(f).stem}")
104
105
if t_typecode is None and i > 5:
106
break
107
108
if t_typecode is not None and len(t_typecode.flight_ids) > 500:
109
t_typecode = t_typecode.sample(500)
110
break
111
112
if t_typecode is None:
113
print(f"{typecode} no flight data available")
114
continue
115
116
typecode_flights = []
117
118
for flight in tqdm(t_typecode, desc="processing flights"):
119
mass = rng.uniform(ac["limits"]["MTOW"] * 0.7, ac["limits"]["MTOW"] * 0.95)
120
121
df = flight.resample("4s").data.assign(
122
typecode=typecode.upper(),
123
second=lambda d: (d.timestamp - d.timestamp.iloc[0]).dt.total_seconds(),
124
v=lambda d: d.groundspeed * openap.aero.kts,
125
trk=lambda d: np.radians(d.track),
126
vgx=lambda d: d.v * np.sin(d.trk),
127
vgy=lambda d: d.v * np.cos(d.trk),
128
vax=lambda d: d.vgx - d.u_component_of_wind,
129
vay=lambda d: d.vgy - d.v_component_of_wind,
130
tas=lambda d: np.sqrt(d.vax**2 + d.vay**2) / openap.aero.kts,
131
)
132
133
df = fe.estimate(
134
df,
135
timestamp="second",
136
airspeed="tas",
137
)
138
139
# plt.plot(flight_fuel.second, flight_fuel.fuel_flow)
140
# plt.show()
141
142
D = drag.clean(
143
mass=mass,
144
tas=df.tas,
145
alt=df.altitude,
146
vs=df.vertical_rate,
147
)
148
149
T = D + mass * 9.81 * np.sin(
150
np.arctan2(
151
df.vertical_rate.values * openap.aero.fpm,
152
df.tas.values * openap.aero.kts,
153
)
154
)
155
156
T0 = eng["max_thrust"] * ac["engine"]["number"]
157
158
T_idle = thrust.descent_idle(tas=df.tas, alt=df.altitude)
159
T_max = thrust.climb(alt=df.altitude, tas=df.tas, roc=df.vertical_rate)
160
161
mask = (T > T_idle) & (T < T_max)
162
163
df_ = df[mask]
164
ratio_samples = T[mask] / T0
165
166
df_ = df_.assign(thrust_ratio=ratio_samples).query("0<thrust_ratio<1")
167
168
typecode_flights.append(df_)
169
170
df_typecode = (
171
pd.concat(typecode_flights, ignore_index=True)
172
.assign(thrust_ratio=lambda d: d.thrust_ratio.round(2))
173
.assign(fuel_flow=lambda d: d.fuel_flow.round(2))
174
.drop_duplicates(subset=["thrust_ratio", "fuel_flow"])
175
)
176
177
df_typecode.to_parquet(typecode_file, index=False)
178
179
# remove outliers
180
181
df_typecode = df_typecode.assign(
182
engine_fuel_flow=lambda d: d.fuel_flow.round(2) / ac["engine"]["number"]
183
).sample(10000, replace=True)
184
185
X = np.array([df_typecode.thrust_ratio, df_typecode.engine_fuel_flow]).T
186
X_std = (X - X.mean(axis=0)) / X.std(axis=0)
187
clustering = cluster.DBSCAN(eps=0.1, min_samples=100).fit(X)
188
mask = clustering.labels_ == 0
189
190
df0 = (
191
df_typecode[mask]
192
.assign(alt_=lambda d: d.altitude // 2000 * 2000)
193
.assign(tas_=lambda d: d.tas // 20 * 20)
194
)
195
196
# fmt: off
197
df1 = df0.query("vertical_rate<=-1000")
198
df2 = df0.query("-250<=vertical_rate<=250")
199
df3 = df0.query("vertical_rate>=500 and altitude>15000")
200
df4 = df0.query("vertical_rate>=1000 and altitude<10000 and tas<180")
201
df5 = df0.query("vertical_rate>=1000 and altitude<10000 and tas>250")
202
# fmt: on
203
204
df_combine = pd.concat([df1, df2, df3, df4, df5], ignore_index=True)
205
206
popt, pcov = curve_fit(
207
func,
208
df_combine.thrust_ratio,
209
df_combine.engine_fuel_flow,
210
bounds=[(0, 0, 0), (df_combine.engine_fuel_flow.max() * 1.2, np.inf, np.inf)],
211
)
212
213
if show_plot:
214
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5), sharex=True, sharey=True)
215
216
ratio = np.linspace(0, 1, 100)
217
total_ac_fuel = func(ratio, *popt) * ac["engine"]["number"]
218
219
ax1.scatter(df0.thrust_ratio, df0.fuel_flow, alpha=0.1, s=5)
220
ax1.plot(ratio, total_ac_fuel, "r-", label=f"fit: {popt}")
221
ax1.legend()
222
ax1.set_xlim(0, 0.7)
223
ax1.set_xlabel("Thrust ratio")
224
ax1.set_ylabel("Fuel flow")
225
226
ax2.scatter(df1.thrust_ratio, df1.fuel_flow, s=5, alpha=0.1, c="tab:green")
227
ax2.scatter(df2.thrust_ratio, df2.fuel_flow, s=5, alpha=0.1, c="tab:orange")
228
ax2.scatter(df3.thrust_ratio, df3.fuel_flow, s=5, alpha=0.1, c="tab:purple")
229
ax2.scatter(df4.thrust_ratio, df4.fuel_flow, s=5, alpha=0.1, c="tab:red")
230
ax2.scatter(df5.thrust_ratio, df5.fuel_flow, s=5, alpha=0.1, c="gray")
231
ax2.plot(ratio, total_ac_fuel, "r-")
232
ax2.set_xlim(0, 0.7)
233
ax2.set_xlabel("Thrust ratio")
234
ax2.set_ylabel("Fuel flow")
235
236
plt.suptitle(typecode)
237
plt.tight_layout()
238
plt.show()
239
240
results.append(
241
{
242
"typecode": typecode,
243
"engine_type": engine_type,
244
"c1": popt[0],
245
"c2": popt[1],
246
"c3": popt[2],
247
}
248
)
249
250
251
# %%
252
# import seaborn as sns
253
254
# sns.relplot(
255
# data=df3,
256
# x="thrust_ratio",
257
# y="fuel_flow",
258
# # col="alt_",
259
# # row="tas_",
260
# col="tas_",
261
# col_wrap=3,
262
# kind="scatter",
263
# alpha=0.02,
264
# edgecolor=None,
265
# )
266
267
# plt.xlim(0, 1)
268
# plt.ylim(0, 2.2)
269
270
# %%
271
fuel_models = pd.DataFrame(results)
272
273
274
# build default model when aircraft is not available
275
# fuel flow normalized by takeoff fuel flow, ff_to
276
277
ff_norms = []
278
279
ratio_samples = np.arange(0, 1, 0.01)
280
281
for i, row in fuel_models.iterrows():
282
coef = row[["c1", "c2", "c3"]].to_list()
283
284
fuel = func(ratio_samples, *coef)
285
286
typecode = row["typecode"]
287
ff_to = openap.prop.engine(row["engine_type"])["ff_to"]
288
289
ff_norm = fuel / ff_to
290
291
ff_norms.append(ff_norm)
292
293
plt.plot(ratio_samples, ff_norm, label=row["typecode"], alpha=0.5)
294
295
df = pd.DataFrame(
296
np.array(ff_norms), index=fuel_models.typecode.to_list(), columns=ratio_samples
297
)
298
299
plt.scatter(ratio_samples, df.mean(axis=0), label="mean", color="b", s=5)
300
301
popt, pcov = curve_fit(func, ratio_samples, df.mean(axis=0))
302
303
print("default", popt)
304
305
x = np.linspace(0, 1, 100)
306
plt.plot(x, func(x, *popt), "b-")
307
plt.xlabel("Thrust ratio")
308
plt.ylabel("Fuel flow")
309
310
311
plt.legend(ncol=3)
312
plt.show()
313
314
315
# %%
316
317
fuel_models = pd.concat(
318
[
319
fuel_models,
320
pd.DataFrame(
321
[
322
{
323
"typecode": "default",
324
"engine_type": "default",
325
"c1": popt[0],
326
"c2": popt[1],
327
"c3": popt[2],
328
}
329
]
330
),
331
],
332
ignore_index=True,
333
)
334
335
# %%
336
337
fuel_models.to_csv("fuel_models.csv", index=False)
338
339
# %%
340
341