pycontrails 0.58.0__cp314-cp314-macosx_11_0_arm64.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.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +6 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.58.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)
|