pycontrails 0.59.0__cp314-cp314-macosx_10_15_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pycontrails might be problematic. Click here for more details.

Files changed (123) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2936 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +764 -0
  37. pycontrails/datalib/gruan.py +343 -0
  38. pycontrails/datalib/himawari/__init__.py +27 -0
  39. pycontrails/datalib/himawari/header_struct.py +266 -0
  40. pycontrails/datalib/himawari/himawari.py +671 -0
  41. pycontrails/datalib/landsat.py +589 -0
  42. pycontrails/datalib/leo_utils/__init__.py +5 -0
  43. pycontrails/datalib/leo_utils/correction.py +266 -0
  44. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  45. pycontrails/datalib/leo_utils/search.py +250 -0
  46. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  47. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  48. pycontrails/datalib/leo_utils/vis.py +59 -0
  49. pycontrails/datalib/sentinel.py +650 -0
  50. pycontrails/datalib/spire/__init__.py +5 -0
  51. pycontrails/datalib/spire/exceptions.py +62 -0
  52. pycontrails/datalib/spire/spire.py +604 -0
  53. pycontrails/ext/bada.py +42 -0
  54. pycontrails/ext/cirium.py +14 -0
  55. pycontrails/ext/empirical_grid.py +140 -0
  56. pycontrails/ext/synthetic_flight.py +431 -0
  57. pycontrails/models/__init__.py +1 -0
  58. pycontrails/models/accf.py +425 -0
  59. pycontrails/models/apcemm/__init__.py +8 -0
  60. pycontrails/models/apcemm/apcemm.py +983 -0
  61. pycontrails/models/apcemm/inputs.py +226 -0
  62. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  63. pycontrails/models/apcemm/utils.py +437 -0
  64. pycontrails/models/cocip/__init__.py +29 -0
  65. pycontrails/models/cocip/cocip.py +2742 -0
  66. pycontrails/models/cocip/cocip_params.py +305 -0
  67. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  68. pycontrails/models/cocip/contrail_properties.py +1530 -0
  69. pycontrails/models/cocip/output_formats.py +2270 -0
  70. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  71. pycontrails/models/cocip/radiative_heating.py +520 -0
  72. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  73. pycontrails/models/cocip/wake_vortex.py +396 -0
  74. pycontrails/models/cocip/wind_shear.py +120 -0
  75. pycontrails/models/cocipgrid/__init__.py +9 -0
  76. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  77. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  78. pycontrails/models/dry_advection.py +602 -0
  79. pycontrails/models/emissions/__init__.py +21 -0
  80. pycontrails/models/emissions/black_carbon.py +599 -0
  81. pycontrails/models/emissions/emissions.py +1353 -0
  82. pycontrails/models/emissions/ffm2.py +336 -0
  83. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  84. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  85. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  86. pycontrails/models/extended_k15.py +1327 -0
  87. pycontrails/models/humidity_scaling/__init__.py +37 -0
  88. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  90. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  91. pycontrails/models/issr.py +210 -0
  92. pycontrails/models/pcc.py +326 -0
  93. pycontrails/models/pcr.py +154 -0
  94. pycontrails/models/ps_model/__init__.py +18 -0
  95. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  96. pycontrails/models/ps_model/ps_grid.py +701 -0
  97. pycontrails/models/ps_model/ps_model.py +1000 -0
  98. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  99. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  100. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  101. pycontrails/models/sac.py +442 -0
  102. pycontrails/models/tau_cirrus.py +183 -0
  103. pycontrails/physics/__init__.py +1 -0
  104. pycontrails/physics/constants.py +117 -0
  105. pycontrails/physics/geo.py +1138 -0
  106. pycontrails/physics/jet.py +968 -0
  107. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  109. pycontrails/physics/thermo.py +551 -0
  110. pycontrails/physics/units.py +472 -0
  111. pycontrails/py.typed +0 -0
  112. pycontrails/utils/__init__.py +1 -0
  113. pycontrails/utils/dependencies.py +66 -0
  114. pycontrails/utils/iteration.py +13 -0
  115. pycontrails/utils/json.py +187 -0
  116. pycontrails/utils/temp.py +50 -0
  117. pycontrails/utils/types.py +163 -0
  118. pycontrails-0.59.0.dist-info/METADATA +179 -0
  119. pycontrails-0.59.0.dist-info/RECORD +123 -0
  120. pycontrails-0.59.0.dist-info/WHEEL +6 -0
  121. pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
  122. pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
  123. pycontrails-0.59.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,1353 @@
1
+ """Calculate jet engine emissions using the ICAO Aircraft Emissions Databank (EDB).
2
+
3
+ Functions without a subscript "_" can be used independently outside .eval()
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import dataclasses
9
+ import functools
10
+ import pathlib
11
+ import warnings
12
+ from typing import Any, NoReturn, overload
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+ import pandas as pd
17
+
18
+ from pycontrails.core.flight import Flight
19
+ from pycontrails.core.fuel import Fuel, SAFBlend
20
+ from pycontrails.core.interpolation import EmissionsProfileInterpolator
21
+ from pycontrails.core.met import MetDataset
22
+ from pycontrails.core.met_var import AirTemperature, MetVariable, SpecificHumidity
23
+ from pycontrails.core.models import Model, ModelParams
24
+ from pycontrails.core.vector import GeoVectorDataset
25
+ from pycontrails.models.emissions import black_carbon, ffm2
26
+ from pycontrails.models.humidity_scaling import HumidityScaling
27
+ from pycontrails.physics import constants, jet, units
28
+
29
+ _path_to_static = pathlib.Path(__file__).parent / "static"
30
+ EDB_ENGINE_PATH = _path_to_static / "edb-gaseous-v29b-engines.csv"
31
+ EDB_NVPM_PATH = _path_to_static / "edb-nvpm-v29b-engines.csv"
32
+ ENGINE_UID_PATH = _path_to_static / "default-engine-uids.csv"
33
+
34
+
35
+ @dataclasses.dataclass
36
+ class EmissionsParams(ModelParams):
37
+ """:class:`Emissions` model parameters."""
38
+
39
+ #: Default nvpm_ei_n value if engine UID is not found
40
+ default_nvpm_ei_n: float = 1e15
41
+
42
+ #: Humidity scaling. If None, no scaling is applied.
43
+ humidity_scaling: HumidityScaling | None = None
44
+
45
+ #: If True, if an engine UID is not provided on the ``source.attrs``, use a
46
+ #: default engine UID based on Teoh's analysis of aircraft engine pairs in
47
+ #: 2019 - 2021 Spire data.
48
+ use_default_engine_uid: bool = True
49
+
50
+
51
+ class Emissions(Model):
52
+ """Emissions handling using ICAO Emissions Databank (EDB) and black carbon correlations.
53
+
54
+ Parameters
55
+ ----------
56
+ met : MetDataset | None, optional
57
+ Met data, by default None.
58
+ params : dict[str, Any] | None, optional
59
+ Model parameters, by default None.
60
+ params_kwargs : Any
61
+ Model parameters passed as keyword arguments.
62
+
63
+ References
64
+ ----------
65
+ - :cite:`leeContributionGlobalAviation2021`
66
+ - :cite:`schumannDehydrationEffectsContrails2015`
67
+ - :cite:`stettlerGlobalCivilAviation2013`
68
+ - :cite:`wilkersonAnalysisEmissionData2010`
69
+
70
+ See Also
71
+ --------
72
+ :mod:`pycontrails.models.emissions.black_carbon`
73
+ :mod:`pycontrails.models.emissions.ffm2`
74
+ """
75
+
76
+ name = "emissions"
77
+ long_name = "ICAO Emissions Databank (EDB)"
78
+ met_variables: tuple[MetVariable, ...] = AirTemperature, SpecificHumidity
79
+ default_params = EmissionsParams
80
+
81
+ source: GeoVectorDataset
82
+
83
+ def __init__(
84
+ self,
85
+ met: MetDataset | None = None,
86
+ params: dict[str, Any] | None = None,
87
+ **params_kwargs: Any,
88
+ ) -> None:
89
+ super().__init__(met, params, **params_kwargs)
90
+
91
+ self.edb_engine_gaseous = load_engine_params_from_edb()
92
+ self.edb_engine_nvpm = load_engine_nvpm_profile_from_edb()
93
+ self.default_engines = load_default_aircraft_engine_mapping()
94
+
95
+ @overload
96
+ def eval(self, source: Flight, **params: Any) -> Flight: ...
97
+
98
+ @overload
99
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
100
+
101
+ @overload
102
+ def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
103
+
104
+ def eval(self, source: GeoVectorDataset | None = None, **params: Any) -> GeoVectorDataset:
105
+ """Calculate the emissions data for ``source``.
106
+
107
+ Parameter ``source`` must contain each of the variables:
108
+ - air_temperature
109
+ - specific_humidity
110
+ - true_airspeed
111
+ - fuel_flow
112
+
113
+ If 'engine_uid' is not provided in ``source.attrs`` or not available in the ICAO EDB,
114
+ constant emission indices will be assumed for NOx, CO, HC, and nvPM mass and number.
115
+
116
+ The computed pollutants include carbon dioxide (CO2), nitrogen oxide (NOx),
117
+ carbon monoxide (CO), hydrocarbons (HC), non-volatile particulate matter
118
+ (nvPM) mass and number, sulphur oxides (SOx), sulphates (S) and organic carbon (OC).
119
+
120
+ .. versionchanged:: 0.47.0
121
+ Support GeoVectorDataset for the ``source`` parameter.
122
+
123
+ Parameters
124
+ ----------
125
+ source : GeoVectorDataset
126
+ Flight to evaluate
127
+ **params : Any
128
+ Overwrite model parameters before eval
129
+
130
+ Returns
131
+ -------
132
+ GeoVectorDataset
133
+ Flight with attached emissions data
134
+ """
135
+ self.update_params(params)
136
+ self.set_source(source)
137
+ self.source = self.require_source_type(GeoVectorDataset)
138
+
139
+ # Set air_temperature and specific_humidity if not already set
140
+ humidity_scaling = self.params["humidity_scaling"]
141
+ scale_humidity = humidity_scaling is not None and "specific_humidity" not in self.source
142
+ self.set_source_met()
143
+
144
+ # Only enhance humidity if it wasn't already present on source
145
+ if scale_humidity:
146
+ humidity_scaling.eval(self.source, copy_source=False)
147
+
148
+ # Ensure that flight has the required AP variables
149
+ self.source.ensure_vars(("true_airspeed", "fuel_flow"))
150
+
151
+ engine_uid = self.source.attrs.get("engine_uid")
152
+ if (
153
+ engine_uid is None
154
+ and self.params["use_default_engine_uid"]
155
+ and (aircraft_type := self.source.attrs.get("aircraft_type"))
156
+ ):
157
+ try:
158
+ engine_uid = self.default_engines.at[aircraft_type, "engine_uid"]
159
+ n_engine = self.default_engines.at[aircraft_type, "n_engine"]
160
+ except KeyError:
161
+ pass
162
+ else:
163
+ self.source.attrs.setdefault("engine_uid", engine_uid)
164
+ self.source.attrs.setdefault("n_engine", n_engine)
165
+
166
+ if engine_uid is None:
167
+ warnings.warn(
168
+ "No 'engine_uid' found on source attrs. A constant emissions will be used."
169
+ )
170
+
171
+ if "n_engine" not in self.source.attrs:
172
+ aircraft_type = self.source.get_constant("aircraft_type", None)
173
+ self.source.attrs["n_engine"] = self.default_engines.at[aircraft_type, "n_engine"]
174
+
175
+ try:
176
+ fuel_flow_per_engine = self.source.get_data_or_attr("fuel_flow_per_engine")
177
+ except KeyError:
178
+ # Try to keep vector and attrs data consistent here
179
+ n_engine = self.source.attrs["n_engine"]
180
+ try:
181
+ ff = self.source["fuel_flow"]
182
+ except KeyError:
183
+ ff = self.source.attrs["fuel_flow"]
184
+ fuel_flow_per_engine = ff / n_engine
185
+ self.source.attrs["fuel_flow_per_engine"] = fuel_flow_per_engine
186
+ else:
187
+ fuel_flow_per_engine = ff / n_engine
188
+ self.source["fuel_flow_per_engine"] = fuel_flow_per_engine
189
+
190
+ # Attach thrust setting
191
+ if "thrust_setting" not in self.source:
192
+ try:
193
+ edb_gaseous = self.edb_engine_gaseous[engine_uid] # type: ignore[index]
194
+ except KeyError:
195
+ self.source["thrust_setting"] = np.full(len(self.source), np.nan, dtype=np.float32)
196
+ else:
197
+ self.source["thrust_setting"] = get_thrust_setting(
198
+ edb_gaseous,
199
+ fuel_flow_per_engine=fuel_flow_per_engine,
200
+ air_pressure=self.source.air_pressure,
201
+ air_temperature=self.source["air_temperature"],
202
+ true_airspeed=self.source.get_data_or_attr("true_airspeed"),
203
+ )
204
+
205
+ self._gaseous_emission_indices(engine_uid)
206
+ self._nvpm_emission_indices(engine_uid)
207
+ self._total_pollutant_emissions()
208
+ return self.source
209
+
210
+ def _gaseous_emission_indices(self, engine_uid: str | None) -> None:
211
+ """Calculate EI's for nitrogen oxide (NOx), carbon monoxide (CO) and hydrocarbons (HC).
212
+
213
+ This method attaches the following variables to the underlying :attr:`flight`:
214
+
215
+ - `nox_ei`
216
+ - `co_ei`
217
+ - `hc_ei`
218
+
219
+ Parameters
220
+ ----------
221
+ engine_uid : str
222
+ Engine unique identification number from the ICAO EDB
223
+ """
224
+ try:
225
+ edb_gaseous = self.edb_engine_gaseous[engine_uid] # type: ignore[index]
226
+ except KeyError:
227
+ self._gaseous_emissions_constant()
228
+ else:
229
+ self._gaseous_emissions_ffm2(edb_gaseous)
230
+
231
+ def _gaseous_emissions_ffm2(self, edb_gaseous: EDBGaseous) -> None:
232
+ """Calculate gaseous emissions using the FFM2 methodology.
233
+
234
+ This method attaches the following variables to the underlying :attr:`flight`:
235
+
236
+ - `nox_ei`
237
+ - `co_ei`
238
+ - `hc_ei`
239
+
240
+ Parameters
241
+ ----------
242
+ edb_gaseous : EDBGaseous
243
+ EDB gaseous data
244
+ """
245
+ self.source.attrs["gaseous_data_source"] = "FFM2"
246
+
247
+ fuel_flow_per_engine = self.source.get_data_or_attr("fuel_flow_per_engine")
248
+ true_airspeed = self.source.get_data_or_attr("true_airspeed")
249
+ air_temperature = self.source["air_temperature"]
250
+
251
+ # Emissions indices
252
+ self.source["nox_ei"] = nitrogen_oxide_emissions_index_ffm2(
253
+ edb_gaseous,
254
+ fuel_flow_per_engine,
255
+ true_airspeed,
256
+ self.source.air_pressure,
257
+ air_temperature,
258
+ self.source["specific_humidity"],
259
+ )
260
+
261
+ self.source["co_ei"] = carbon_monoxide_emissions_index_ffm2(
262
+ edb_gaseous,
263
+ fuel_flow_per_engine,
264
+ true_airspeed,
265
+ self.source.air_pressure,
266
+ air_temperature,
267
+ )
268
+
269
+ self.source["hc_ei"] = hydrocarbon_emissions_index_ffm2(
270
+ edb_gaseous,
271
+ fuel_flow_per_engine,
272
+ true_airspeed,
273
+ self.source.air_pressure,
274
+ air_temperature,
275
+ )
276
+
277
+ def _gaseous_emissions_constant(self) -> None:
278
+ """Fill gaseous emissions data with default values.
279
+
280
+ This method attaches the following variables to the underlying :attr:`flight`:
281
+
282
+ - `nox_ei`
283
+ - `co_ei`
284
+ - `hc_ei`
285
+
286
+ Assumes constant emission indices for nitrogen oxide, carbon monoxide and
287
+ hydrocarbon for a given aircraft-engine pair if data is not available in the ICAO EDB.
288
+
289
+ - NOx EI = 15.14 g-NOx/kg-fuel (Table 1 of Lee et al., 2020)
290
+ - CO EI = 3.61 g-CO/kg-fuel (Table 1 of Wilkerson et al., 2010),
291
+ - HC EI = 0.520 g-HC/kg-fuel (Table 1 of Wilkerson et al., 2010)
292
+
293
+ References
294
+ ----------
295
+ - :cite:`leeContributionGlobalAviation2021`
296
+ - :cite:`wilkersonAnalysisEmissionData2010`
297
+ """
298
+ self.source.attrs["gaseous_data_source"] = "Constant"
299
+
300
+ nox_ei = np.full(shape=len(self.source), fill_value=15.14, dtype=np.float32)
301
+ co_ei = np.full(shape=len(self.source), fill_value=3.61, dtype=np.float32)
302
+ hc_ei = np.full(shape=len(self.source), fill_value=0.520, dtype=np.float32)
303
+
304
+ self.source["nox_ei"] = nox_ei * 1e-3 # g-NOx/kg-fuel to kg-NOx/kg-fuel
305
+ self.source["co_ei"] = co_ei * 1e-3 # g-CO/kg-fuel to kg-CO/kg-fuel
306
+ self.source["hc_ei"] = hc_ei * 1e-3 # g-HC/kg-fuel to kg-HC/kg-fuel
307
+
308
+ def _nvpm_emission_indices(self, engine_uid: str | None) -> None:
309
+ """Calculate emission indices for nvPM mass and number.
310
+
311
+ This method attaches the following variables to the underlying :attr:`source`.
312
+ - nvpm_ei_m
313
+ - nvpm_ei_n
314
+
315
+ In addition, ``nvpm_data_source`` is attached to the ``source.attrs``.
316
+
317
+ Parameters
318
+ ----------
319
+ engine_uid : str
320
+ Engine unique identification number from the ICAO EDB
321
+ """
322
+ if "nvpm_ei_n" in self.source and "nvpm_ei_m" in self.source:
323
+ return # early exit if values already exist
324
+
325
+ if isinstance(self.source, Flight):
326
+ fuel = self.source.fuel
327
+ else:
328
+ try:
329
+ fuel = self.source.attrs["fuel"]
330
+ except KeyError as exc:
331
+ raise KeyError(
332
+ "If running 'Emissions' with a 'GeoVectorDataset' as source, "
333
+ "the fuel type must be provided in the attributes. "
334
+ ) from exc
335
+
336
+ edb_nvpm = self.edb_engine_nvpm.get(engine_uid) if engine_uid else None
337
+ edb_gaseous = self.edb_engine_gaseous.get(engine_uid) if engine_uid else None
338
+
339
+ if edb_nvpm is not None:
340
+ nvpm_data = self._nvpm_emission_indices_edb(edb_nvpm, fuel)
341
+ elif edb_gaseous is not None:
342
+ nvpm_data = self._nvpm_emission_indices_sac(edb_gaseous, fuel)
343
+ else:
344
+ if engine_uid is not None:
345
+ warnings.warn(
346
+ f"Cannot find 'engine_uid' {engine_uid} in EDB. "
347
+ "A constant emissions will be used."
348
+ )
349
+ nvpm_data = self._nvpm_emission_indices_constant()
350
+
351
+ nvpm_data_source, nvpm_ei_m, nvpm_ei_n = nvpm_data
352
+
353
+ # Adjust nvPM emission indices if SAF is used.
354
+ if isinstance(fuel, SAFBlend) and fuel.pct_blend:
355
+ thrust_setting = self.source["thrust_setting"]
356
+ pct_eim_reduction = black_carbon.nvpm_mass_ei_pct_reduction_due_to_saf(
357
+ fuel.hydrogen_content, thrust_setting
358
+ )
359
+ pct_ein_reduction = black_carbon.nvpm_number_ei_pct_reduction_due_to_saf(
360
+ fuel.hydrogen_content, thrust_setting
361
+ )
362
+
363
+ nvpm_ei_m *= 1.0 + pct_eim_reduction / 100.0
364
+ nvpm_ei_n *= 1.0 + pct_ein_reduction / 100.0
365
+
366
+ self.source.attrs["nvpm_data_source"] = nvpm_data_source
367
+ self.source.setdefault("nvpm_ei_m", nvpm_ei_m)
368
+ self.source.setdefault("nvpm_ei_n", nvpm_ei_n)
369
+
370
+ def _nvpm_emission_indices_edb(
371
+ self, edb_nvpm: EDBnvpm, fuel: Fuel
372
+ ) -> tuple[str, npt.NDArray[np.floating], npt.NDArray[np.floating]]:
373
+ """Calculate emission indices for nvPM mass and number.
374
+
375
+ This method uses data from the ICAO EDB along with the T4/T2 methodology.
376
+
377
+ Parameters
378
+ ----------
379
+ edb_nvpm : EDBnvpm
380
+ EDB nvPM data.
381
+ fuel : Fuel
382
+ Fuel type.
383
+
384
+ Returns
385
+ -------
386
+ nvpm_data_source : str
387
+ Source of nvpm data.
388
+ nvpm_ei_m : npt.NDArray[np.floating]
389
+ Non-volatile particulate matter (nvPM) mass emissions index, [:math:`kg/kg_{fuel}`]
390
+ nvpm_ei_n : npt.NDArray[np.floating]
391
+ Black carbon number emissions index, [:math:`kg_{fuel}^{-1}`]
392
+
393
+ References
394
+ ----------
395
+ - :cite:`teohTargetedUseSustainable2022`
396
+ """
397
+ nvpm_data_source = "ICAO EDB"
398
+
399
+ # Emissions indices
400
+ return nvpm_data_source, *get_nvpm_emissions_index_edb(
401
+ edb_nvpm,
402
+ true_airspeed=self.source.get_data_or_attr("true_airspeed"),
403
+ air_temperature=self.source["air_temperature"],
404
+ air_pressure=self.source.air_pressure,
405
+ thrust_setting=self.source["thrust_setting"],
406
+ q_fuel=fuel.q_fuel,
407
+ )
408
+
409
+ def _nvpm_emission_indices_sac(
410
+ self, edb_gaseous: EDBGaseous, fuel: Fuel
411
+ ) -> tuple[str, npt.NDArray[np.floating], npt.NDArray[np.floating]]:
412
+ """Calculate EIs for nvPM mass and number assuming the profile of single annular combustors.
413
+
414
+ nvPM EI_m is calculated using the FOX and ImFOX methods, while the nvPM EI_n
415
+ is calculated using the Fractal Aggregates (FA) model.
416
+
417
+ Parameters
418
+ ----------
419
+ edb_gaseous : EDBGaseous
420
+ EDB gaseous data
421
+ fuel : Fuel
422
+ Fuel type.
423
+
424
+ Returns
425
+ -------
426
+ nvpm_data_source : str
427
+ Source of nvpm data.
428
+ nvpm_ei_m : npt.NDArray[np.floating]
429
+ Non-volatile particulate matter (nvPM) mass emissions index, [:math:`kg/kg_{fuel}`]
430
+ nvpm_ei_n : npt.NDArray[np.floating]
431
+ Black carbon number emissions index, [:math:`kg_{fuel}^{-1}`]
432
+
433
+ References
434
+ ----------
435
+ - :cite:`stettlerGlobalCivilAviation2013`
436
+ - :cite:`abrahamsonPredictiveModelDevelopment2016`
437
+ - :cite:`teohTargetedUseSustainable2022`
438
+ """
439
+ nvpm_data_source = "FA Model"
440
+
441
+ # calculate properties
442
+ thrust_setting = self.source["thrust_setting"]
443
+ fuel_flow_per_engine = self.source.get_data_or_attr("fuel_flow_per_engine")
444
+ true_airspeed = self.source.get_data_or_attr("true_airspeed")
445
+ air_temperature = self.source["air_temperature"]
446
+
447
+ # Emissions indices
448
+ nvpm_ei_m = nvpm_mass_emissions_index_sac(
449
+ edb_gaseous,
450
+ air_pressure=self.source.air_pressure,
451
+ true_airspeed=true_airspeed,
452
+ air_temperature=air_temperature,
453
+ thrust_setting=thrust_setting,
454
+ fuel_flow_per_engine=fuel_flow_per_engine,
455
+ hydrogen_content=fuel.hydrogen_content,
456
+ )
457
+ nvpm_gmd = nvpm_geometric_mean_diameter_sac(
458
+ edb_gaseous,
459
+ air_pressure=self.source.air_pressure,
460
+ true_airspeed=true_airspeed,
461
+ air_temperature=air_temperature,
462
+ thrust_setting=thrust_setting,
463
+ q_fuel=fuel.q_fuel,
464
+ )
465
+ nvpm_ei_n = black_carbon.number_emissions_index_fractal_aggregates(nvpm_ei_m, nvpm_gmd)
466
+ return nvpm_data_source, nvpm_ei_m, nvpm_ei_n
467
+
468
+ def _nvpm_emission_indices_constant(
469
+ self,
470
+ ) -> tuple[str, npt.NDArray[np.floating], npt.NDArray[np.floating]]:
471
+ """
472
+ Assume constant emission indices for nvPM mass and number.
473
+
474
+ (nvpm_ei_n = 1e15 /kg-fuel) for a given aircraft-engine pair if data
475
+ is not available in the ICAO EDB.
476
+
477
+ - nvpm_ei_m = 0.088 g-nvPM/kg-fuel (Table 2 of Stettler et al., 2013)
478
+ - nvpm_ei_n = 1e15 /kg-fuel (Schumann et al., 2015)
479
+
480
+ Returns
481
+ -------
482
+ nvpm_data_source : str
483
+ Source of nvpm data.
484
+ nvpm_ei_m : npt.NDArray[np.floating]
485
+ Non-volatile particulate matter (nvPM) mass emissions index, [:math:`kg/kg_{fuel}`]
486
+ nvpm_ei_n : npt.NDArray[np.floating]
487
+ Black carbon number emissions index, [:math:`kg_{fuel}^{-1}`]
488
+
489
+ References
490
+ ----------
491
+ - :cite:`stettlerGlobalCivilAviation2013`
492
+ - :cite:`wilkersonAnalysisEmissionData2010`
493
+ - :cite:`schumannDehydrationEffectsContrails2015`
494
+ """
495
+ nvpm_data_source = "Constant"
496
+ nvpm_ei_m = np.full(len(self.source), 0.088 * 1e-3, dtype=np.float32) # g to kg
497
+ nvpm_ei_n = np.full(len(self.source), self.params["default_nvpm_ei_n"], dtype=np.float32)
498
+ return nvpm_data_source, nvpm_ei_m, nvpm_ei_n
499
+
500
+ def _total_pollutant_emissions(self) -> None:
501
+ if not isinstance(self.source, Flight):
502
+ return
503
+
504
+ dt_sec = self.source.segment_duration(self.source.altitude_ft.dtype)
505
+ fuel_burn = jet.fuel_burn(self.source.get_data_or_attr("fuel_flow"), dt_sec)
506
+
507
+ # TODO: these currently overwrite values and will throw warnings
508
+
509
+ # Total emissions for each waypoint
510
+ self.source["co2"] = fuel_burn * self.source.fuel.ei_co2
511
+ self.source["h2o"] = fuel_burn * self.source.fuel.ei_h2o
512
+ self.source["so2"] = fuel_burn * self.source.fuel.ei_so2
513
+ self.source["sulphates"] = fuel_burn * self.source.fuel.ei_sulphates
514
+ self.source["oc"] = fuel_burn * self.source.fuel.ei_oc
515
+ self.source["nox"] = fuel_burn * self.source["nox_ei"]
516
+ self.source["co"] = fuel_burn * self.source["co_ei"]
517
+ self.source["hc"] = fuel_burn * self.source["hc_ei"]
518
+ self.source["nvpm_mass"] = fuel_burn * self.source["nvpm_ei_m"]
519
+ self.source["nvpm_number"] = fuel_burn * self.source["nvpm_ei_n"]
520
+
521
+ # Total emissions for the flight
522
+ self.source.attrs["total_co2"] = np.nansum(self.source["co2"])
523
+ self.source.attrs["total_h2o"] = np.nansum(self.source["h2o"])
524
+ self.source.attrs["total_so2"] = np.nansum(self.source["so2"])
525
+ self.source.attrs["total_sulphates"] = np.nansum(self.source["sulphates"])
526
+ self.source.attrs["total_oc"] = np.nansum(self.source["oc"])
527
+ self.source.attrs["total_nox"] = np.nansum(self.source["nox"])
528
+ self.source.attrs["total_co"] = np.nansum(self.source["co"])
529
+ self.source.attrs["total_hc"] = np.nansum(self.source["hc"])
530
+ self.source.attrs["total_nvpm_mass"] = np.nansum(self.source["nvpm_mass"])
531
+ self.source.attrs["total_nvpm_number"] = np.nansum(self.source["nvpm_number"])
532
+
533
+ def _check_edb_gaseous_availability(
534
+ self,
535
+ engine_uid: str,
536
+ raise_error: bool = True,
537
+ ) -> bool:
538
+ """
539
+ Check if the provided engine is available in the gaseous ICAO EDB.
540
+
541
+ Setting ``raise_error`` to True allows functions in this class to be
542
+ used independently outside of :meth:`eval`.
543
+
544
+ Parameters
545
+ ----------
546
+ engine_uid: str
547
+ Engine unique identification number from the ICAO EDB
548
+ raise_error: bool
549
+ Raise a KeyError if engine type is not available.
550
+
551
+ Returns
552
+ -------
553
+ bool
554
+ True if engine type is available in the gaseous ICAO EDB.
555
+
556
+ Raises
557
+ ------
558
+ KeyError
559
+ If engine type is not available in the gaseous ICAO EDB.
560
+ """
561
+ if engine_uid not in self.edb_engine_gaseous:
562
+ if raise_error:
563
+ raise KeyError(
564
+ f"Engine ({engine_uid}) is not available in the ICAO EDB gaseous database"
565
+ )
566
+ return False
567
+ return True
568
+
569
+ def _check_edb_nvpm_availability(
570
+ self,
571
+ engine_uid: str,
572
+ raise_error: bool = True,
573
+ ) -> bool:
574
+ """
575
+ Check if the provided engine is available in the nvPM ICAO EDB.
576
+
577
+ Setting ``raise_error`` to True allows functions in this class to be
578
+ used independently outside of :meth:`eval`.
579
+
580
+ Parameters
581
+ ----------
582
+ engine_uid: str
583
+ Engine unique identification number from the ICAO EDB
584
+ raise_error: bool
585
+ Raise a KeyError if engine type is not available.
586
+
587
+ Returns
588
+ -------
589
+ bool
590
+ True if engine type is available in the nvPM ICAO EDB.
591
+
592
+ Raises
593
+ ------
594
+ KeyError
595
+ If engine type is not available in the nvPM ICAO EDB.
596
+ """
597
+ if engine_uid not in self.edb_engine_nvpm:
598
+ if raise_error:
599
+ raise KeyError(
600
+ f"Engine ({engine_uid}) is not available in the ICAO EDB nvPM database"
601
+ )
602
+ return False
603
+ return True
604
+
605
+
606
+ def nitrogen_oxide_emissions_index_ffm2(
607
+ edb_gaseous: EDBGaseous,
608
+ fuel_flow_per_engine: npt.NDArray[np.floating],
609
+ true_airspeed: npt.NDArray[np.floating],
610
+ air_pressure: npt.NDArray[np.floating],
611
+ air_temperature: npt.NDArray[np.floating],
612
+ specific_humidity: None | npt.NDArray[np.floating] = None,
613
+ ) -> npt.NDArray[np.floating]:
614
+ """
615
+ Estimate the nitrogen oxide (NOx) emissions index (EI) using the Fuel Flow Method 2 (FFM2).
616
+
617
+ Parameters
618
+ ----------
619
+ edb_gaseous : EDBGaseous
620
+ EDB gaseous data
621
+ fuel_flow_per_engine: npt.NDArray[np.floating]
622
+ fuel mass flow rate per engine, [:math:`kg s^{-1}`]
623
+ true_airspeed: npt.NDArray[np.floating]
624
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
625
+ air_pressure : npt.NDArray[np.floating]
626
+ pressure altitude at each waypoint, [:math:`Pa`]
627
+ air_temperature : npt.NDArray[np.floating]
628
+ ambient temperature for each waypoint, [:math:`K`]
629
+ specific_humidity: npt.NDArray[np.floating]
630
+ specific humidity for each waypoint, [:math:`kg_{H_{2}O}/kg_{air}`]
631
+
632
+ Returns
633
+ -------
634
+ npt.NDArray[np.floating]
635
+ Nitrogen oxide emissions index for each waypoint, [:math:`kg_{NO_{X}}/kg_{fuel}`]
636
+ """
637
+ res_nox = ffm2.estimate_nox(
638
+ edb_gaseous.log_ei_nox_profile,
639
+ fuel_flow_per_engine,
640
+ true_airspeed,
641
+ air_pressure,
642
+ air_temperature,
643
+ specific_humidity,
644
+ )
645
+ return res_nox * 1e-3 # g-NOx/kg-fuel to kg-NOx/kg-fuel
646
+
647
+
648
+ def carbon_monoxide_emissions_index_ffm2(
649
+ edb_gaseous: EDBGaseous,
650
+ fuel_flow_per_engine: npt.NDArray[np.floating],
651
+ true_airspeed: npt.NDArray[np.floating],
652
+ air_pressure: npt.NDArray[np.floating],
653
+ air_temperature: npt.NDArray[np.floating],
654
+ ) -> npt.NDArray[np.floating]:
655
+ """
656
+ Estimate the carbon monoxide (CO) emissions index (EI) using the Fuel Flow Method 2 (FFM2).
657
+
658
+ Parameters
659
+ ----------
660
+ edb_gaseous : EDBGaseous
661
+ EDB gaseous data
662
+ fuel_flow_per_engine: npt.NDArray[np.floating]
663
+ fuel mass flow rate per engine, [:math:`kg s^{-1}`]
664
+ true_airspeed: npt.NDArray[np.floating]
665
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
666
+ air_pressure : npt.NDArray[np.floating]
667
+ pressure altitude at each waypoint, [:math:`Pa`]
668
+ air_temperature : npt.NDArray[np.floating]
669
+ ambient temperature for each waypoint, [:math:`K`]
670
+
671
+ Returns
672
+ -------
673
+ npt.NDArray[np.floating]
674
+ Carbon monoxide emissions index for each waypoint, [:math:`kg_{CO}/kg_{fuel}`]
675
+ """
676
+ res_co = ffm2.estimate_ei(
677
+ edb_gaseous.log_ei_co_profile,
678
+ fuel_flow_per_engine,
679
+ true_airspeed,
680
+ air_pressure,
681
+ air_temperature,
682
+ )
683
+ return res_co * 1e-3 # g-CO/kg-fuel to kg-CO/kg-fuel
684
+
685
+
686
+ def hydrocarbon_emissions_index_ffm2(
687
+ edb_gaseous: EDBGaseous,
688
+ fuel_flow_per_engine: npt.NDArray[np.floating],
689
+ true_airspeed: npt.NDArray[np.floating],
690
+ air_pressure: npt.NDArray[np.floating],
691
+ air_temperature: npt.NDArray[np.floating],
692
+ ) -> npt.NDArray[np.floating]:
693
+ """
694
+ Estimate the hydrocarbon (HC) emissions index (EI) using the Fuel Flow Method 2 (FFM2).
695
+
696
+ Parameters
697
+ ----------
698
+ edb_gaseous : EDBGaseous
699
+ EDB gaseous data
700
+ fuel_flow_per_engine: npt.NDArray[np.floating]
701
+ fuel mass flow rate per engine, [:math:`kg s^{-1}`]
702
+ true_airspeed: npt.NDArray[np.floating]
703
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
704
+ air_pressure : npt.NDArray[np.floating]
705
+ pressure altitude at each waypoint, [:math:`Pa`]
706
+ air_temperature : npt.NDArray[np.floating]
707
+ ambient temperature for each waypoint, [:math:`K`]
708
+
709
+ Returns
710
+ -------
711
+ npt.NDArray[np.floating]
712
+ Hydrocarbon emissions index for each waypoint, [:math:`kg_{HC}/kg_{fuel}`]
713
+ """
714
+ res_hc = ffm2.estimate_ei(
715
+ edb_gaseous.log_ei_hc_profile,
716
+ fuel_flow_per_engine,
717
+ true_airspeed,
718
+ air_pressure,
719
+ air_temperature,
720
+ )
721
+ return res_hc * 1e-3 # g-HC/kg-fuel to kg-HC/kg-fuel
722
+
723
+
724
+ def get_nvpm_emissions_index_edb(
725
+ edb_nvpm: EDBnvpm,
726
+ true_airspeed: npt.NDArray[np.floating],
727
+ air_temperature: npt.NDArray[np.floating],
728
+ air_pressure: npt.NDArray[np.floating],
729
+ thrust_setting: npt.NDArray[np.floating],
730
+ q_fuel: float,
731
+ ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
732
+ r"""Calculate nvPM mass emissions index (nvpm_ei_m) and number emissions index (nvpm_ei_n).
733
+
734
+ Interpolate the non-volatile particulate matter (nvPM) mass and number emissions index from
735
+ the emissions profile of a given engine type that is provided by the ICAO EDB.
736
+
737
+ The non-dimensional thrust setting (t4_t2) is clipped to the minimum and maximum t4_t2 values
738
+ that is estimated from the four ICAO EDB datapoints to prevent extrapolating the nvPM values.
739
+
740
+ Parameters
741
+ ----------
742
+ edb_nvpm : EDBnvpm
743
+ EDB nvPM data
744
+ true_airspeed: npt.NDArray[np.floating]
745
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
746
+ fuel_flow_per_engine: npt.NDArray[np.floating]
747
+ fuel mass flow rate per engine, [:math:`kg s^{-1}`]
748
+ air_temperature: npt.NDArray[np.floating]
749
+ ambient temperature for each waypoint, [:math:`K`]
750
+ air_pressure: npt.NDArray[np.floating]
751
+ pressure altitude at each waypoint, [:math:`Pa`]
752
+ thrust_setting : npt.NDArray[np.floating]
753
+ thrust setting
754
+ q_fuel : float
755
+ Lower calorific value (LCV) of fuel, [:math:`J \ kg_{fuel}^{-1}`].
756
+
757
+ Returns
758
+ -------
759
+ nvpm_ei_m : npt.NDArray[np.floating]
760
+ Non-volatile particulate matter (nvPM) mass emissions index, [:math:`kg/kg_{fuel}`]
761
+ nvpm_ei_n : npt.NDArray[np.floating]
762
+ Black carbon number emissions index, [:math:`kg_{fuel}^{-1}`]
763
+ """
764
+ # Non-dimensionalized thrust setting
765
+ t4_t2 = jet.thrust_setting_nd(
766
+ true_airspeed,
767
+ thrust_setting,
768
+ air_temperature,
769
+ air_pressure,
770
+ edb_nvpm.pressure_ratio,
771
+ q_fuel,
772
+ cruise=True,
773
+ )
774
+
775
+ # Interpolate nvPM EI_m and EI_n
776
+ nvpm_ei_m = edb_nvpm.nvpm_ei_m.interp(t4_t2)
777
+ nvpm_ei_m = nvpm_ei_m * 1e-6 # mg-nvPM/kg-fuel to kg-nvPM/kg-fuel
778
+ nvpm_ei_n = edb_nvpm.nvpm_ei_n.interp(t4_t2)
779
+ return nvpm_ei_m, nvpm_ei_n
780
+
781
+
782
+ def nvpm_mass_emissions_index_sac(
783
+ edb_gaseous: EDBGaseous,
784
+ air_pressure: npt.NDArray[np.floating],
785
+ true_airspeed: npt.NDArray[np.floating],
786
+ air_temperature: npt.NDArray[np.floating],
787
+ thrust_setting: npt.NDArray[np.floating],
788
+ fuel_flow_per_engine: npt.NDArray[np.floating],
789
+ hydrogen_content: float,
790
+ ) -> npt.NDArray[np.floating]:
791
+ """Estimate nvPM mass emission index for singular annular combustor (SAC) engines.
792
+
793
+ Here, SAC should not be confused with the Schmidt-Appleman Criterion.
794
+
795
+ The nvpm_ei_m for SAC is estimated as the mean between a lower bound (80% of the FOX-estimated
796
+ nvpm_ei_m) and an upper bound (150% of ImFOX-estimated nvpm_ei_m).
797
+
798
+ Parameters
799
+ ----------
800
+ edb_gaseous : EDBGaseous
801
+ EDB gaseous data
802
+ air_pressure: npt.NDArray[np.floating]
803
+ pressure altitude at each waypoint, [:math:`Pa`]
804
+ true_airspeed: npt.NDArray[np.floating]
805
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
806
+ air_temperature: npt.NDArray[np.floating]
807
+ ambient temperature for each waypoint, [:math:`K`]
808
+ thrust_setting : npt.NDArray[np.floating]
809
+ thrust setting
810
+ fuel_flow_per_engine: npt.NDArray[np.floating]
811
+ fuel mass flow rate per engine, [:math:`kg s^{-1}`]
812
+ hydrogen_content : float
813
+ Engine unique identification number from the ICAO EDB
814
+
815
+ Returns
816
+ -------
817
+ npt.NDArray[np.floating]
818
+ nvPM mass emissions index, [:math:`kg/kg_{fuel}`]
819
+ """
820
+ nvpm_ei_m_fox = black_carbon.mass_emissions_index_fox(
821
+ air_pressure,
822
+ air_temperature,
823
+ true_airspeed,
824
+ fuel_flow_per_engine,
825
+ thrust_setting,
826
+ edb_gaseous.pressure_ratio,
827
+ )
828
+ nvpm_ei_m_imfox = black_carbon.mass_emissions_index_imfox(
829
+ fuel_flow_per_engine, thrust_setting, hydrogen_content
830
+ )
831
+ nvpm_ei_m = 0.5 * (0.8 * nvpm_ei_m_fox + 1.5 * nvpm_ei_m_imfox)
832
+ return nvpm_ei_m * 1e-6 # mg-nvPM/kg-fuel to kg-nvPM/kg-fuel
833
+
834
+
835
+ def nvpm_geometric_mean_diameter_sac(
836
+ edb_gaseous: EDBGaseous,
837
+ air_pressure: npt.NDArray[np.floating],
838
+ true_airspeed: npt.NDArray[np.floating],
839
+ air_temperature: npt.NDArray[np.floating],
840
+ thrust_setting: npt.NDArray[np.floating],
841
+ q_fuel: float,
842
+ ) -> npt.NDArray[np.floating]:
843
+ r"""
844
+ Estimate nvPM geometric mean diameter for singular annular combustor (SAC) engines.
845
+
846
+ Parameters
847
+ ----------
848
+ edb_gaseous : EDBGaseous
849
+ EDB gaseous data
850
+ air_pressure: npt.NDArray[np.floating]
851
+ pressure altitude at each waypoint, [:math:`Pa`]
852
+ true_airspeed: npt.NDArray[np.floating]
853
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
854
+ air_temperature: npt.NDArray[np.floating]
855
+ ambient temperature for each waypoint, [:math:`K`]
856
+ thrust_setting : npt.NDArray[np.floating]
857
+ thrust setting
858
+ q_fuel : float
859
+ Lower calorific value (LCV) of fuel, [:math:`J \ kg_{fuel}^{-1}`].
860
+
861
+ Returns
862
+ -------
863
+ npt.NDArray[np.floating]
864
+ nvPM geometric mean diameter, [:math:`m`]
865
+ """
866
+ nvpm_gmd = black_carbon.geometric_mean_diameter_sac(
867
+ air_pressure,
868
+ air_temperature,
869
+ true_airspeed,
870
+ thrust_setting,
871
+ edb_gaseous.pressure_ratio,
872
+ q_fuel,
873
+ cruise=True,
874
+ )
875
+ return nvpm_gmd * 1e-9 # nm to m
876
+
877
+
878
+ def get_thrust_setting(
879
+ edb_gaseous: EDBGaseous,
880
+ fuel_flow_per_engine: npt.NDArray[np.floating],
881
+ air_pressure: npt.NDArray[np.floating],
882
+ air_temperature: npt.NDArray[np.floating],
883
+ true_airspeed: npt.NDArray[np.floating],
884
+ ) -> npt.NDArray[np.floating]:
885
+ """
886
+ Approximate the engine thrust setting at cruise conditions.
887
+
888
+ The thrust setting is approximated by dividing the fuel mass flow rate
889
+ by the maximum fuel mass flow rate, and clipped to 3% (0.03) and 100% (1)
890
+ respectively to account for unrealistic values.
891
+
892
+ Parameters
893
+ ----------
894
+ edb_gaseous : EDBGaseous
895
+ EDB gaseous data
896
+ fuel_flow_per_engine: npt.NDArray[np.floating]
897
+ Fuel mass flow rate per engine, [:math:`kg s^{-1}`]
898
+ air_pressure: npt.NDArray[np.floating]
899
+ Pressure altitude at each waypoint, [:math:`Pa`]
900
+ air_temperature: npt.NDArray[np.floating]
901
+ Ambient temperature for each waypoint, [:math:`K`]
902
+ true_airspeed: npt.NDArray[np.floating]
903
+ True airspeed for each waypoint, [:math:`m s^{-1}`]
904
+
905
+ Returns
906
+ -------
907
+ npt.NDArray[np.floating]
908
+ Engine thrust setting. Returns ``np.nan`` if engine data is
909
+ not available in the ICAO EDB dataset.
910
+ """
911
+ theta_amb = jet.temperature_ratio(air_temperature)
912
+ delta_amb = jet.pressure_ratio(air_pressure)
913
+ mach_num = units.tas_to_mach_number(true_airspeed, air_temperature)
914
+ fuel_flow_per_engine = jet.equivalent_fuel_flow_rate_at_sea_level(
915
+ fuel_flow_per_engine, theta_amb, delta_amb, mach_num
916
+ )
917
+
918
+ thrust_setting = fuel_flow_per_engine / edb_gaseous.ff_100
919
+ thrust_setting.clip(0.03, 1.0, out=thrust_setting) # clip in place
920
+ return thrust_setting
921
+
922
+
923
+ def _row_to_edb_gaseous(tup: Any) -> tuple[str, EDBGaseous]:
924
+ return tup.engine_uid, EDBGaseous(
925
+ **{k.name: getattr(tup, k.name) for k in dataclasses.fields(EDBGaseous)}
926
+ )
927
+
928
+
929
+ @dataclasses.dataclass(frozen=True)
930
+ class EDBGaseous:
931
+ """Gaseous emissions data.
932
+
933
+ -------------------------------------
934
+ ENGINE IDENTIFICATION AND TYPE:
935
+ -------------------------------------
936
+ manufacturer: str
937
+ engine manufacturer
938
+ engine_name: str
939
+ name of engine
940
+ combustor: str
941
+ description of engine combustor
942
+
943
+ -------------------------------------
944
+ ENGINE CHARACTERISTICS:
945
+ -------------------------------------
946
+ bypass_ratio: float
947
+ engine bypass ratio
948
+ pressure_ratio: float
949
+ engine pressure ratio
950
+ rated_thrust: float
951
+ rated thrust of engine, [:math:`kN`]
952
+
953
+ -------------------------------------
954
+ FUEL CONSUMPTION:
955
+ -------------------------------------
956
+ ff_7: float
957
+ fuel mass flow rate at 7% thrust setting, [:math:`kg s^{-1}`]
958
+ ff_30: float
959
+ fuel mass flow rate at 30% thrust setting, [:math:`kg s^{-1}`]
960
+ ff_85: float
961
+ fuel mass flow rate at 85% thrust setting, [:math:`kg s^{-1}`]
962
+ ff_100: float
963
+ fuel mass flow rate at 100% thrust setting, [:math:`kg s^{-1}`]
964
+
965
+ -------------------------------------
966
+ EMISSIONS:
967
+ -------------------------------------
968
+ ei_nox_7: float
969
+ NOx emissions index at 7% thrust setting, [:math:`g_{NO_{X}}/kg_{fuel}`]
970
+ ei_nox_30: float
971
+ NOx emissions index at 30% thrust setting, [:math:`g_{NO_{X}}/kg_{fuel}`]
972
+ ei_nox_85: float
973
+ NOx emissions index at 85% thrust setting, [:math:`g_{NO_{X}}/kg_{fuel}`]
974
+ ei_nox_100: float
975
+ NOx emissions index at 100% thrust setting, [:math:`g_{NO_{X}}/kg_{fuel}`]
976
+
977
+ ei_co_7: float
978
+ CO emissions index at 7% thrust setting, [:math:`g_{CO}/kg_{fuel}`]
979
+ ei_co_30: float
980
+ CO emissions index at 30% thrust setting, [:math:`g_{CO}/kg_{fuel}`]
981
+ ei_co_85: float
982
+ CO emissions index at 85% thrust setting, [:math:`g_{CO}/kg_{fuel}`]
983
+ ei_co_100: float
984
+ CO emissions index at 100% thrust setting, [:math:`g_{CO}/kg_{fuel}`]
985
+
986
+ ei_hc_7: float
987
+ HC emissions index at 7% thrust setting, [:math:`g_{HC}/kg_{fuel}`]
988
+ ei_hc_30: float
989
+ HC emissions index at 30% thrust setting, [:math:`g_{HC}/kg_{fuel}`]
990
+ ei_hc_85: float
991
+ HC emissions index at 85% thrust setting, [:math:`g_{HC}/kg_{fuel}`]
992
+ ei_hc_100: float
993
+ HC emissions index at 100% thrust setting, [:math:`g_{HC}/kg_{fuel}`]
994
+
995
+ sn_7: float
996
+ smoke number at 7% thrust setting
997
+ sn_30: float
998
+ smoke number at 30% thrust setting
999
+ sn_85: float
1000
+ smoke number at 85% thrust setting
1001
+ sn_100: float
1002
+ smoke number at 100% thrust setting
1003
+ sn_max: float
1004
+ maximum smoke number value across the range of thrust setting
1005
+ """
1006
+
1007
+ # Engine identification and type
1008
+ manufacturer: str
1009
+ engine_name: str
1010
+ combustor: str
1011
+
1012
+ # Engine characteristics
1013
+ bypass_ratio: float
1014
+ pressure_ratio: float
1015
+ rated_thrust: float
1016
+
1017
+ # Fuel consumption
1018
+ ff_7: float
1019
+ ff_30: float
1020
+ ff_85: float
1021
+ ff_100: float
1022
+
1023
+ # Emissions
1024
+ ei_nox_7: float
1025
+ ei_nox_30: float
1026
+ ei_nox_85: float
1027
+ ei_nox_100: float
1028
+
1029
+ ei_co_7: float
1030
+ ei_co_30: float
1031
+ ei_co_85: float
1032
+ ei_co_100: float
1033
+
1034
+ ei_hc_7: float
1035
+ ei_hc_30: float
1036
+ ei_hc_85: float
1037
+ ei_hc_100: float
1038
+
1039
+ sn_7: float
1040
+ sn_30: float
1041
+ sn_85: float
1042
+ sn_100: float
1043
+ sn_max: float
1044
+
1045
+ @property
1046
+ def log_ei_nox_profile(self) -> EmissionsProfileInterpolator:
1047
+ """Get the logarithmic emissions index profile for NOx emissions."""
1048
+ return ffm2.nitrogen_oxide_emissions_index_profile(
1049
+ ff_idle=self.ff_7,
1050
+ ff_approach=self.ff_30,
1051
+ ff_climb=self.ff_85,
1052
+ ff_take_off=self.ff_100,
1053
+ ei_nox_idle=self.ei_nox_7,
1054
+ ei_nox_approach=self.ei_nox_30,
1055
+ ei_nox_climb=self.ei_nox_85,
1056
+ ei_nox_take_off=self.ei_nox_100,
1057
+ )
1058
+
1059
+ @property
1060
+ def log_ei_co_profile(self) -> EmissionsProfileInterpolator:
1061
+ """Get the logarithmic emissions index profile for CO emissions."""
1062
+ return ffm2.co_hc_emissions_index_profile(
1063
+ ff_idle=self.ff_7,
1064
+ ff_approach=self.ff_30,
1065
+ ff_climb=self.ff_85,
1066
+ ff_take_off=self.ff_100,
1067
+ ei_idle=self.ei_co_7,
1068
+ ei_approach=self.ei_co_30,
1069
+ ei_climb=self.ei_co_85,
1070
+ ei_take_off=self.ei_co_100,
1071
+ )
1072
+
1073
+ @property
1074
+ def log_ei_hc_profile(self) -> EmissionsProfileInterpolator:
1075
+ """Get the logarithmic emissions index profile for HC emissions."""
1076
+ return ffm2.co_hc_emissions_index_profile(
1077
+ ff_idle=self.ff_7,
1078
+ ff_approach=self.ff_30,
1079
+ ff_climb=self.ff_85,
1080
+ ff_take_off=self.ff_100,
1081
+ ei_idle=self.ei_hc_7,
1082
+ ei_approach=self.ei_hc_30,
1083
+ ei_climb=self.ei_hc_85,
1084
+ ei_take_off=self.ei_hc_100,
1085
+ )
1086
+
1087
+
1088
+ def _row_to_edb_nvpm(tup: Any) -> tuple[str, EDBnvpm]:
1089
+ return tup.engine_uid, EDBnvpm(
1090
+ **{k.name: getattr(tup, k.name) for k in dataclasses.fields(EDBnvpm)}
1091
+ )
1092
+
1093
+
1094
+ @dataclasses.dataclass
1095
+ class EDBnvpm:
1096
+ """A data class for EDB nvPM data.
1097
+
1098
+ -------------------------------------
1099
+ ENGINE IDENTIFICATION AND TYPE:
1100
+ -------------------------------------
1101
+ manufacturer: str
1102
+ engine manufacturer
1103
+ engine_name: str
1104
+ name of engine
1105
+ combustor: str
1106
+ description of engine combustor
1107
+
1108
+ -------------------------------------
1109
+ ENGINE CHARACTERISTICS:
1110
+ -------------------------------------
1111
+ pressure_ratio: float
1112
+ engine pressure ratio
1113
+
1114
+ -------------------------------------
1115
+ nvPM EMISSIONS:
1116
+ -------------------------------------
1117
+ nvpm_ei_m: EmissionsProfileInterpolator
1118
+ non-volatile PM mass emissions index profile (mg/kg) vs.
1119
+ non-dimensionalized thrust setting (t4_t2)
1120
+ nvpm_ei_n: EmissionsProfileInterpolator
1121
+ non-volatile PM number emissions index profile (1/kg) vs.
1122
+ non-dimensionalized thrust setting (t4_t2)
1123
+ """
1124
+
1125
+ # Engine identification and type
1126
+ manufacturer: str
1127
+ engine_name: str
1128
+ combustor: str
1129
+
1130
+ # Engine characteristics
1131
+ pressure_ratio: float
1132
+ temp_min: float
1133
+ temp_max: float
1134
+ fuel_heat: float
1135
+
1136
+ # Fuel consumption
1137
+ ff_7: float
1138
+ ff_30: float
1139
+ ff_85: float
1140
+ ff_100: float
1141
+
1142
+ # Emissions
1143
+ nvpm_ei_m_7: float
1144
+ nvpm_ei_m_30: float
1145
+ nvpm_ei_m_85: float
1146
+ nvpm_ei_m_100: float
1147
+
1148
+ nvpm_ei_n_7: float
1149
+ nvpm_ei_n_30: float
1150
+ nvpm_ei_n_85: float
1151
+ nvpm_ei_n_100: float
1152
+
1153
+ @property
1154
+ def nvpm_ei_m(self) -> EmissionsProfileInterpolator:
1155
+ """Get the nvPM emissions index mass profile."""
1156
+ return _nvpm_emissions_profiles(
1157
+ pressure_ratio=self.pressure_ratio,
1158
+ combustor=self.combustor,
1159
+ temp_min=self.temp_min,
1160
+ temp_max=self.temp_max,
1161
+ fuel_heat=self.fuel_heat,
1162
+ ff_7=self.ff_7,
1163
+ ff_30=self.ff_30,
1164
+ ff_85=self.ff_85,
1165
+ ff_100=self.ff_100,
1166
+ nvpm_ei_m_7=self.nvpm_ei_m_7,
1167
+ nvpm_ei_m_30=self.nvpm_ei_m_30,
1168
+ nvpm_ei_m_85=self.nvpm_ei_m_85,
1169
+ nvpm_ei_m_100=self.nvpm_ei_m_100,
1170
+ nvpm_ei_n_7=self.nvpm_ei_n_7,
1171
+ nvpm_ei_n_30=self.nvpm_ei_n_30,
1172
+ nvpm_ei_n_85=self.nvpm_ei_n_85,
1173
+ nvpm_ei_n_100=self.nvpm_ei_n_100,
1174
+ )[0]
1175
+
1176
+ @property
1177
+ def nvpm_ei_n(self) -> EmissionsProfileInterpolator:
1178
+ """Get the nvPM emissions index number profile."""
1179
+ return _nvpm_emissions_profiles(
1180
+ pressure_ratio=self.pressure_ratio,
1181
+ combustor=self.combustor,
1182
+ temp_min=self.temp_min,
1183
+ temp_max=self.temp_max,
1184
+ fuel_heat=self.fuel_heat,
1185
+ ff_7=self.ff_7,
1186
+ ff_30=self.ff_30,
1187
+ ff_85=self.ff_85,
1188
+ ff_100=self.ff_100,
1189
+ nvpm_ei_m_7=self.nvpm_ei_m_7,
1190
+ nvpm_ei_m_30=self.nvpm_ei_m_30,
1191
+ nvpm_ei_m_85=self.nvpm_ei_m_85,
1192
+ nvpm_ei_m_100=self.nvpm_ei_m_100,
1193
+ nvpm_ei_n_7=self.nvpm_ei_n_7,
1194
+ nvpm_ei_n_30=self.nvpm_ei_n_30,
1195
+ nvpm_ei_n_85=self.nvpm_ei_n_85,
1196
+ nvpm_ei_n_100=self.nvpm_ei_n_100,
1197
+ )[1]
1198
+
1199
+
1200
+ @functools.cache
1201
+ def _nvpm_emissions_profiles(
1202
+ pressure_ratio: float,
1203
+ combustor: str,
1204
+ temp_min: float,
1205
+ temp_max: float,
1206
+ fuel_heat: float,
1207
+ ff_7: float,
1208
+ ff_30: float,
1209
+ ff_85: float,
1210
+ ff_100: float,
1211
+ nvpm_ei_m_7: float,
1212
+ nvpm_ei_m_30: float,
1213
+ nvpm_ei_m_85: float,
1214
+ nvpm_ei_m_100: float,
1215
+ nvpm_ei_n_7: float,
1216
+ nvpm_ei_n_30: float,
1217
+ nvpm_ei_n_85: float,
1218
+ nvpm_ei_n_100: float,
1219
+ ) -> tuple[EmissionsProfileInterpolator, EmissionsProfileInterpolator]:
1220
+ # Extract fuel flow
1221
+ fuel_flow = np.array([ff_7, ff_30, ff_85, ff_100])
1222
+ fuel_flow_max = fuel_flow[-1]
1223
+
1224
+ # Extract nvPM emissions arrays
1225
+ nvpm_ei_m = np.array([nvpm_ei_m_7, nvpm_ei_m_30, nvpm_ei_m_85, nvpm_ei_m_100])
1226
+ nvpm_ei_n = np.array([nvpm_ei_n_7, nvpm_ei_n_30, nvpm_ei_n_85, nvpm_ei_n_100])
1227
+
1228
+ is_staged_combustor = combustor in ("DAC", "TAPS", "TAPS II")
1229
+ if is_staged_combustor:
1230
+ # In this case, all of our interpolators will have size 5
1231
+ fuel_flow = np.insert(fuel_flow, 2, (fuel_flow[1] * 1.001))
1232
+ nvpm_ei_n_lean_burn = np.mean(nvpm_ei_n[2:])
1233
+ nvpm_ei_n = np.r_[nvpm_ei_n[:2], [nvpm_ei_n_lean_burn] * 3]
1234
+ nvpm_ei_m_lean_burn = np.mean(nvpm_ei_m[2:])
1235
+ nvpm_ei_m = np.r_[nvpm_ei_m[:2], [nvpm_ei_m_lean_burn] * 3]
1236
+
1237
+ thrust_setting = fuel_flow / fuel_flow_max
1238
+ avg_temp = (temp_min + temp_max) / 2.0
1239
+
1240
+ t4_t2 = jet.thrust_setting_nd(
1241
+ true_airspeed=0.0,
1242
+ thrust_setting=thrust_setting,
1243
+ T=avg_temp,
1244
+ p=constants.p_surface,
1245
+ pressure_ratio=pressure_ratio,
1246
+ q_fuel=fuel_heat * 1e6,
1247
+ cruise=False,
1248
+ )
1249
+
1250
+ nvpm_ei_m_interp = EmissionsProfileInterpolator(t4_t2, nvpm_ei_m)
1251
+ nvpm_ei_n_interp = EmissionsProfileInterpolator(t4_t2, nvpm_ei_n)
1252
+ return nvpm_ei_m_interp, nvpm_ei_n_interp
1253
+
1254
+
1255
+ @functools.cache
1256
+ def load_engine_params_from_edb() -> dict[str, EDBGaseous]:
1257
+ """Read EDB file into a dictionary of the form ``{engine_uid: gaseous_data}``.
1258
+
1259
+ Returns
1260
+ -------
1261
+ dict[str, EDBGaseous]
1262
+ Mapping from engine UID to gaseous emissions data for engine.
1263
+ """
1264
+
1265
+ columns = {
1266
+ "UID No": "engine_uid",
1267
+ "Manufacturer": "manufacturer",
1268
+ "Engine Identification": "engine_name",
1269
+ "Combustor Description": "combustor",
1270
+ "B/P Ratio": "bypass_ratio",
1271
+ "Pressure Ratio": "pressure_ratio",
1272
+ "Rated Thrust (kN)": "rated_thrust",
1273
+ "Fuel Flow Idle (kg/sec)": "ff_7",
1274
+ "Fuel Flow App (kg/sec)": "ff_30",
1275
+ "Fuel Flow C/O (kg/sec)": "ff_85",
1276
+ "Fuel Flow T/O (kg/sec)": "ff_100",
1277
+ "NOx EI Idle (g/kg)": "ei_nox_7",
1278
+ "NOx EI App (g/kg)": "ei_nox_30",
1279
+ "NOx EI C/O (g/kg)": "ei_nox_85",
1280
+ "NOx EI T/O (g/kg)": "ei_nox_100",
1281
+ "CO EI Idle (g/kg)": "ei_co_7",
1282
+ "CO EI App (g/kg)": "ei_co_30",
1283
+ "CO EI C/O (g/kg)": "ei_co_85",
1284
+ "CO EI T/O (g/kg)": "ei_co_100",
1285
+ "HC EI Idle (g/kg)": "ei_hc_7",
1286
+ "HC EI App (g/kg)": "ei_hc_30",
1287
+ "HC EI C/O (g/kg)": "ei_hc_85",
1288
+ "HC EI T/O (g/kg)": "ei_hc_100",
1289
+ "SN Idle": "sn_7",
1290
+ "SN App": "sn_30",
1291
+ "SN C/O": "sn_85",
1292
+ "SN T/O": "sn_100",
1293
+ "SN Max": "sn_max",
1294
+ }
1295
+
1296
+ df = pd.read_csv(EDB_ENGINE_PATH)
1297
+ df = df.rename(columns=columns)
1298
+
1299
+ return dict(_row_to_edb_gaseous(tup) for tup in df.itertuples(index=False))
1300
+
1301
+
1302
+ @functools.cache
1303
+ def load_engine_nvpm_profile_from_edb() -> dict[str, EDBnvpm]:
1304
+ """Read EDB file into a dictionary of the form ``{engine_uid: npvm_data}``.
1305
+
1306
+ Returns
1307
+ -------
1308
+ dict[str, EDBnvpm]
1309
+ Mapping from aircraft type to nvPM data for engine.
1310
+ """
1311
+ columns = {
1312
+ "UID No": "engine_uid",
1313
+ "Manufacturer": "manufacturer",
1314
+ "Engine Identification": "engine_name",
1315
+ "Combustor Description": "combustor",
1316
+ "Pressure Ratio": "pressure_ratio",
1317
+ "Ambient Temp Min (K)": "temp_min",
1318
+ "Ambient Temp Max (K)": "temp_max",
1319
+ "Fuel Heat of Combustion (MJ/kg)": "fuel_heat",
1320
+ "Fuel Flow Idle (kg/sec)": "ff_7",
1321
+ "Fuel Flow App (kg/sec)": "ff_30",
1322
+ "Fuel Flow C/O (kg/sec)": "ff_85",
1323
+ "Fuel Flow T/O (kg/sec)": "ff_100",
1324
+ "nvPM EImass_SL Idle (mg/kg)": "nvpm_ei_m_7",
1325
+ "nvPM EImass_SL App (mg/kg)": "nvpm_ei_m_30",
1326
+ "nvPM EImass_SL C/O (mg/kg)": "nvpm_ei_m_85",
1327
+ "nvPM EImass_SL T/O (mg/kg)": "nvpm_ei_m_100",
1328
+ "nvPM EInum_SL Idle (#/kg)": "nvpm_ei_n_7",
1329
+ "nvPM EInum_SL App (#/kg)": "nvpm_ei_n_30",
1330
+ "nvPM EInum_SL C/O (#/kg)": "nvpm_ei_n_85",
1331
+ "nvPM EInum_SL T/O (#/kg)": "nvpm_ei_n_100",
1332
+ }
1333
+
1334
+ df = pd.read_csv(EDB_NVPM_PATH)
1335
+ df = df.rename(columns=columns)
1336
+
1337
+ return dict(_row_to_edb_nvpm(tup) for tup in df.itertuples(index=False))
1338
+
1339
+
1340
+ @functools.cache
1341
+ def load_default_aircraft_engine_mapping() -> pd.DataFrame:
1342
+ """Read default aircraft type -> engine UID assignments.
1343
+
1344
+ Returns
1345
+ -------
1346
+ pd.DataFrame
1347
+ A :class:`pd.DataFrame` whose index is available aircraft types with columns:
1348
+
1349
+ - engine_uid
1350
+ - engine_name
1351
+ - n-engines
1352
+ """
1353
+ return pd.read_csv(ENGINE_UID_PATH, index_col=0)