pycontrails 0.58.0__cp314-cp314-macosx_10_13_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 (122) 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 +2931 -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 +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,599 @@
1
+ """Non-volatile particulate matter (nvPM) calculations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+
8
+ from pycontrails.physics import constants, jet, units
9
+ from pycontrails.utils.types import ArrayScalarLike
10
+
11
+ # ---------------------------------------------------------
12
+ # nvPM Mass Emissions Index: Formation and Oxidation Method
13
+ # ---------------------------------------------------------
14
+
15
+
16
+ def mass_emissions_index_fox(
17
+ air_pressure: npt.NDArray[np.floating],
18
+ air_temperature: npt.NDArray[np.floating],
19
+ true_airspeed: npt.NDArray[np.floating],
20
+ fuel_flow_per_engine: npt.NDArray[np.floating],
21
+ thrust_setting: npt.NDArray[np.floating],
22
+ pressure_ratio: float,
23
+ *,
24
+ comp_efficiency: float = 0.9,
25
+ ) -> npt.NDArray[np.floating]:
26
+ r"""
27
+ Calculate the black carbon mass emissions index using the Formation and Oxidation Method (FOX).
28
+
29
+ Parameters
30
+ ----------
31
+ air_pressure: npt.NDArray[np.floating]
32
+ Pressure altitude at each waypoint, [:math:`Pa`]
33
+ air_temperature: npt.NDArray[np.floating]
34
+ Ambient temperature for each waypoint, [:math:`K`]
35
+ true_airspeed: npt.NDArray[np.floating]
36
+ True airspeed for each waypoint, [:math:`m s^{-1}`]
37
+ fuel_flow_per_engine: npt.NDArray[np.floating]
38
+ Fuel mass flow rate per engine, [:math:`kg s^{-1}`]
39
+ thrust_setting: npt.NDArray[np.floating]
40
+ Engine thrust setting, which is the fuel mass flow rate divided by
41
+ the maximum fuel mass flow rate
42
+ pressure_ratio: float
43
+ Engine pressure ratio from the ICAO EDB
44
+ comp_efficiency: float
45
+ Engine compressor efficiency, assumed to be 0.9
46
+
47
+ Returns
48
+ -------
49
+ npt.NDArray[np.floating]
50
+ Black carbon mass emissions index, [:math:`mg \ kg_{fuel}^{-1}`]
51
+
52
+ References
53
+ ----------
54
+ - :cite:`stettlerGlobalCivilAviation2013`
55
+ """
56
+ # Reference conditions: 100% thrust setting
57
+ p_3_ref = jet.combustor_inlet_pressure(pressure_ratio, constants.p_surface, 1.0)
58
+ t_3_ref = jet.combustor_inlet_temperature(
59
+ comp_efficiency, constants.T_msl, constants.p_surface, p_3_ref
60
+ )
61
+ t_fl_ref = flame_temperature(t_3_ref)
62
+ afr_ref = jet.air_to_fuel_ratio(1.0, cruise=False)
63
+ fuel_flow_max = fuel_flow_per_engine / thrust_setting
64
+ c_bc_ref = bc_mass_concentration_fox(fuel_flow_max, t_fl_ref, afr_ref)
65
+
66
+ # Cruise conditions
67
+ mach_num = units.tas_to_mach_number(true_airspeed, air_temperature)
68
+ t_2_cru = jet.compressor_inlet_temperature(air_temperature, mach_num)
69
+ p_2_cru = jet.compressor_inlet_pressure(air_pressure, mach_num)
70
+ p_3_cru = jet.combustor_inlet_pressure(pressure_ratio, p_2_cru, thrust_setting)
71
+ t_3_cru = jet.combustor_inlet_temperature(comp_efficiency, t_2_cru, p_2_cru, p_3_cru)
72
+ t_fl_cru = flame_temperature(t_3_cru)
73
+ afr_cru = jet.air_to_fuel_ratio(thrust_setting, cruise=True, T_compressor_inlet=t_2_cru)
74
+ c_bc_cru = bc_mass_concentration_cruise_fox(
75
+ c_bc_ref=c_bc_ref,
76
+ t_fl_cru=t_fl_cru,
77
+ t_fl_ref=t_fl_ref,
78
+ p_3_cru=p_3_cru,
79
+ p_3_ref=p_3_ref,
80
+ afr_cru=afr_cru,
81
+ afr_ref=afr_ref,
82
+ )
83
+ q_exhaust_cru = exhaust_gas_volume_per_kg_fuel(afr_cru)
84
+ return bc_mass_emissions_index(c_bc_cru, q_exhaust_cru)
85
+
86
+
87
+ def flame_temperature(t_3: ArrayScalarLike) -> ArrayScalarLike:
88
+ """
89
+ Calculate the flame temperature at the combustion chamber (t_fl).
90
+
91
+ Parameters
92
+ ----------
93
+ t_3: ArrayScalarLike
94
+ Combustor inlet temperature, [:math:`K`]
95
+
96
+ Returns
97
+ -------
98
+ ArrayScalarLike
99
+ Flame temperature at the combustion chamber, [:math:`K`]
100
+ """
101
+ return 0.9 * t_3 + 2120.0
102
+
103
+
104
+ def bc_mass_concentration_fox(
105
+ fuel_flow: npt.NDArray[np.floating],
106
+ t_fl: npt.NDArray[np.floating] | float,
107
+ afr: npt.NDArray[np.floating] | float,
108
+ ) -> npt.NDArray[np.floating]:
109
+ """Calculate the black carbon mass concentration for ground conditions (``c_bc_ref``).
110
+
111
+ This quantity is computed at the instrument sampling point without correcting
112
+ for particle line losses.
113
+
114
+ Parameters
115
+ ----------
116
+ fuel_flow: npt.NDArray[np.floating]
117
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
118
+ t_fl: npt.NDArray[np.floating] | float
119
+ Flame temperature at the combustion chamber, [:math:`K`]
120
+ afr: npt.NDArray[np.floating] | float
121
+ Air-to-fuel ratio
122
+
123
+ Returns
124
+ -------
125
+ npt.NDArray[np.floating]:
126
+ Black carbon mass concentration for ground conditions, [:math:`mg m^{-3}`]
127
+ """
128
+ # avoid float32 -> float64 promotion
129
+ coeff = 356.0 * np.exp(np.float32(-6390.0) / t_fl) - 608.0 * afr * np.exp(
130
+ np.float32(-19778.0) / t_fl
131
+ )
132
+ return fuel_flow * coeff
133
+
134
+
135
+ def bc_mass_concentration_cruise_fox(
136
+ c_bc_ref: npt.NDArray[np.floating],
137
+ t_fl_cru: npt.NDArray[np.floating],
138
+ t_fl_ref: npt.NDArray[np.floating] | float,
139
+ p_3_cru: npt.NDArray[np.floating],
140
+ p_3_ref: npt.NDArray[np.floating] | float,
141
+ afr_cru: npt.NDArray[np.floating],
142
+ afr_ref: npt.NDArray[np.floating] | float,
143
+ ) -> npt.NDArray[np.floating]:
144
+ """Calculate the black carbon mass concentration for cruise conditions (``c_bc_cru``).
145
+
146
+ This quantity is computed at the instrument sampling point without correcting
147
+ for particle line losses.
148
+
149
+ Parameters
150
+ ----------
151
+ c_bc_ref: npt.NDArray[np.floating]
152
+ Black carbon mass concentration at reference conditions, [:math:`mg m^{-3}`]
153
+ t_fl_cru: npt.NDArray[np.floating]
154
+ Flame temperature at cruise conditions, [:math:`K`]
155
+ t_fl_ref: npt.NDArray[np.floating] | float
156
+ Flame temperature at reference conditions, [:math:`K`]
157
+ p_3_cru: npt.NDArray[np.floating]
158
+ Combustor inlet pressure at cruise conditions, [:math:`Pa`]
159
+ p_3_ref: npt.NDArray[np.floating] | float
160
+ Combustor inlet pressure at reference conditions, [:math:`Pa`]
161
+ afr_cru: npt.NDArray[np.floating]
162
+ Air-to-fuel ratio at cruise conditions
163
+ afr_ref: npt.NDArray[np.floating] | float
164
+ Air-to-fuel ratio at reference conditions
165
+
166
+ Returns
167
+ -------
168
+ npt.NDArray[np.floating]:
169
+ Black carbon mass concentration for cruise conditions, [:math:`mg m^{-3}`]
170
+ """
171
+ scaling_factor = dopelheuer_lecht_scaling_factor(
172
+ t_fl_cru=t_fl_cru,
173
+ t_fl_ref=t_fl_ref,
174
+ p_3_cru=p_3_cru,
175
+ p_3_ref=p_3_ref,
176
+ afr_cru=afr_cru,
177
+ afr_ref=afr_ref,
178
+ )
179
+ return c_bc_ref * scaling_factor
180
+
181
+
182
+ def dopelheuer_lecht_scaling_factor(
183
+ t_fl_cru: npt.NDArray[np.floating],
184
+ t_fl_ref: npt.NDArray[np.floating] | float,
185
+ p_3_cru: npt.NDArray[np.floating],
186
+ p_3_ref: npt.NDArray[np.floating] | float,
187
+ afr_cru: npt.NDArray[np.floating],
188
+ afr_ref: npt.NDArray[np.floating] | float,
189
+ ) -> npt.NDArray[np.floating]:
190
+ """Estimate scaling factor to convert the reference BC mass concentration from ground to cruise.
191
+
192
+ Parameters
193
+ ----------
194
+ t_fl_cru: npt.NDArray[np.floating]
195
+ Flame temperature at cruise conditions, [:math:`K`]
196
+ t_fl_ref: npt.NDArray[np.floating] | float
197
+ Flame temperature at reference conditions, [:math:`K`]
198
+ p_3_cru: npt.NDArray[np.floating]
199
+ Combustor inlet pressure at cruise conditions, [:math:`Pa`]
200
+ p_3_ref: npt.NDArray[np.floating] | float
201
+ Combustor inlet pressure at reference conditions, [:math:`Pa`]
202
+ afr_cru: npt.NDArray[np.floating]
203
+ Air-to-fuel ratio at cruise conditions
204
+ afr_ref: npt.NDArray[np.floating] | float
205
+ Air-to-fuel ratio at reference conditions
206
+
207
+ Returns
208
+ -------
209
+ npt.NDArray[np.floating]
210
+ Dopelheuer & Lecht scaling factor
211
+
212
+ References
213
+ ----------
214
+ - :cite:`dopelheuerInfluenceEnginePerformance1998`
215
+ """
216
+ exp_term = np.exp(20000.0 / t_fl_cru - 20000.0 / t_fl_ref)
217
+ return (afr_ref / afr_cru) ** 2.5 * (p_3_cru / p_3_ref) ** 1.35 * exp_term
218
+
219
+
220
+ # ----------------------------------------------------------------------------
221
+ # nvPM Mass Emissions Index: "Improved" Formation and Oxidation Method (ImFOX)
222
+ # ----------------------------------------------------------------------------
223
+
224
+
225
+ def mass_emissions_index_imfox(
226
+ fuel_flow_per_engine: npt.NDArray[np.floating],
227
+ thrust_setting: npt.NDArray[np.floating],
228
+ fuel_hydrogen: float,
229
+ ) -> npt.NDArray[np.floating]:
230
+ r"""Calculate the BC mass EI using the "Improved" Formation and Oxidation Method (ImFOX).
231
+
232
+ Parameters
233
+ ----------
234
+ fuel_flow_per_engine: npt.NDArray[np.floating]
235
+ Fuel mass flow rate per engine, [:math:`kg s^{-1}`]
236
+ thrust_setting: npt.NDArray[np.floating]
237
+ Engine thrust setting, which is the fuel mass flow rate divided by the
238
+ maximum fuel mass flow rate
239
+ fuel_hydrogen: float
240
+ Percentage of hydrogen mass content in the fuel (13.8% for conventional Jet A-1 fuel)
241
+
242
+ Returns
243
+ -------
244
+ npt.NDArray[np.floating]
245
+ Black carbon mass emissions index, [:math:`mg \ kg_{fuel}^{-1}`]
246
+
247
+ References
248
+ ----------
249
+ - :cite:`abrahamsonPredictiveModelDevelopment2016`
250
+ """
251
+ afr_cru = air_to_fuel_ratio_imfox(thrust_setting)
252
+ t_4_cru = turbine_inlet_temperature_imfox(afr_cru)
253
+ c_bc_cru = bc_mass_concentration_imfox(
254
+ fuel_flow_per_engine, afr_cru, t_4_cru, fuel_hydrogen=fuel_hydrogen
255
+ )
256
+ q_exhaust_cru = exhaust_gas_volume_per_kg_fuel(afr_cru)
257
+ return bc_mass_emissions_index(c_bc_cru, q_exhaust_cru)
258
+
259
+
260
+ def air_to_fuel_ratio_imfox(thrust_setting: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
261
+ """Calculate the air-to-fuel ratio at cruise conditions via Abrahamson's method.
262
+
263
+ See Eq. (11) in :cite:`abrahamsonPredictiveModelDevelopment2016`.
264
+
265
+ Parameters
266
+ ----------
267
+ thrust_setting: npt.NDArray[np.floating]
268
+ Engine thrust setting, which is the fuel mass flow rate divided by
269
+ the maximum fuel mass flow rate
270
+
271
+ Returns
272
+ -------
273
+ npt.NDArray[np.floating]
274
+ Air-to-fuel ratio at cruise conditions
275
+
276
+ References
277
+ ----------
278
+ - :cite:`abrahamsonPredictiveModelDevelopment2016`
279
+ """
280
+ return 55.4 - 30.8 * thrust_setting
281
+
282
+
283
+ def turbine_inlet_temperature_imfox(afr: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
284
+ """Calculate the turbine inlet temperature using Abrahamson's method.
285
+
286
+ See Eq. (13) in :cite:`abrahamsonPredictiveModelDevelopment2016`.
287
+
288
+ Parameters
289
+ ----------
290
+ afr: npt.NDArray[np.floating]
291
+ air-to-fuel ratio at cruise conditions
292
+
293
+ Returns
294
+ -------
295
+ npt.NDArray[np.floating]
296
+ turbine inlet temperature, [:math:`K`]
297
+
298
+ References
299
+ ----------
300
+ - :cite:`abrahamsonPredictiveModelDevelopment2016`
301
+ """
302
+ return 490.0 + 42266.0 / afr
303
+
304
+
305
+ def bc_mass_concentration_imfox(
306
+ fuel_flow_per_engine: npt.NDArray[np.floating],
307
+ afr: npt.NDArray[np.floating],
308
+ t_4: npt.NDArray[np.floating],
309
+ fuel_hydrogen: float,
310
+ ) -> npt.NDArray[np.floating]:
311
+ """Calculate the BC mass concentration for ground and cruise conditions with ImFOX methodology.
312
+
313
+ This quantity is computed at the instrument sampling point without
314
+ correcting for particle line losses.
315
+
316
+ Parameters
317
+ ----------
318
+ fuel_flow_per_engine: npt.NDArray[np.floating]
319
+ fuel mass flow rate per engine, [:math:`kg s^{-1}`]
320
+ afr: npt.NDArray[np.floating]
321
+ air-to-fuel ratio
322
+ t_4: npt.NDArray[np.floating]
323
+ turbine inlet temperature, [:math:`K`]
324
+ fuel_hydrogen: float
325
+ percentage of hydrogen mass content in the fuel (13.8% for conventional Jet A-1 fuel)
326
+
327
+ Returns
328
+ -------
329
+ npt.NDArray[np.floating]
330
+ Black carbon mass concentration, [:math:`mg m^{-3}`]
331
+ """
332
+ # avoid float32 -> float64 promotion
333
+ exp_term = np.exp(np.float32(13.6) - fuel_hydrogen)
334
+ formation_term = 295.0 * np.exp(np.float32(-6390.0) / t_4)
335
+ oxidation_term = 608.0 * afr * np.exp(np.float32(-19778.0) / t_4)
336
+ return fuel_flow_per_engine * exp_term * (formation_term - oxidation_term)
337
+
338
+
339
+ # ---------------------------------------------------------
340
+ # Commonly Used Black Carbon Mass Emissions Index Functions
341
+ # ---------------------------------------------------------
342
+
343
+
344
+ def exhaust_gas_volume_per_kg_fuel(afr: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
345
+ """
346
+ Calculate the volume of exhaust gas per mass of fuel burnt.
347
+
348
+ Parameters
349
+ ----------
350
+ afr: npt.NDArray[np.floating]
351
+ Air-to-fuel ratio
352
+
353
+ Returns
354
+ -------
355
+ npt.NDArray[np.floating]
356
+ Volume of exhaust gas per mass of fuel burnt, [:math:`m^{3}/kg_{fuel}`]
357
+
358
+ References
359
+ ----------
360
+ - :cite:`stettlerGlobalCivilAviation2013`
361
+ """
362
+ return 0.776 * afr + 0.877
363
+
364
+
365
+ def bc_mass_emissions_index(
366
+ c_bc: npt.NDArray[np.floating], q_exhaust: npt.NDArray[np.floating]
367
+ ) -> npt.NDArray[np.floating]:
368
+ """
369
+ Calculate the black carbon mass emissions index.
370
+
371
+ Parameters
372
+ ----------
373
+ c_bc: npt.NDArray[np.floating]
374
+ Black carbon mass concentration, [:math:`mg m^{-3}`]
375
+ q_exhaust: npt.NDArray[np.floating]
376
+ Volume of exhaust gas per mass of fuel burnt, [:math:`m^{3}/kg_{fuel}`]
377
+
378
+ Returns
379
+ -------
380
+ npt.NDArray[np.floating]
381
+ Black carbon mass emissions index, [:math:`mg/kg_{fuel}`]
382
+
383
+ References
384
+ ----------
385
+ - :cite:`stettlerGlobalCivilAviation2013`
386
+ """
387
+ return c_bc * q_exhaust
388
+
389
+
390
+ # ----------------------------------------------------------
391
+ # nvPM Number Emissions Index: Fractal Aggregates (FA) model
392
+ # ----------------------------------------------------------
393
+
394
+
395
+ def geometric_mean_diameter_sac(
396
+ air_pressure: npt.NDArray[np.floating],
397
+ air_temperature: npt.NDArray[np.floating],
398
+ true_airspeed: npt.NDArray[np.floating],
399
+ thrust_setting: npt.NDArray[np.floating],
400
+ pressure_ratio: float,
401
+ q_fuel: float,
402
+ *,
403
+ comp_efficiency: float = 0.9,
404
+ delta_loss: float = 5.75,
405
+ cruise: bool = True,
406
+ ) -> npt.NDArray[np.floating]:
407
+ r"""Calculate the BC GMD for singular annular combustor (SAC) engines.
408
+
409
+ The BC (black carbon) GMD (geometric mean diameter) is estimated using
410
+ the non-dimensionalized engine thrust setting, the ratio of turbine inlet
411
+ to the compressor inlet temperature (``t4_t2``).
412
+
413
+ Parameters
414
+ ----------
415
+ air_pressure: npt.NDArray[np.floating]
416
+ Pressure altitude at each waypoint, [:math:`Pa`]
417
+ air_temperature: npt.NDArray[np.floating]
418
+ Ambient temperature for each waypoint, [:math:`K`]
419
+ true_airspeed: npt.NDArray[np.floating]
420
+ True airspeed for each waypoint, [:math:`m s^{-1}`]
421
+ thrust_setting: npt.NDArray[np.floating]
422
+ Engine thrust setting, which is the fuel mass flow rate divided by the
423
+ maximum fuel mass flow rate
424
+ pressure_ratio: float
425
+ Engine pressure ratio from the ICAO EDB
426
+ q_fuel : float
427
+ Lower calorific value (LCV) of fuel, [:math:`J \ kg_{fuel}^{-1}`]
428
+ comp_efficiency: float
429
+ Engine compressor efficiency (assumed to be 0.9)
430
+ delta_loss: float
431
+ Correction factor accounting for particle line losses (assumed to be 5.75 nm), [:math:`nm`]
432
+ cruise: bool
433
+ Set to true when the aircraft is not on the ground.
434
+
435
+ Returns
436
+ -------
437
+ npt.NDArray[np.floating]
438
+ black carbon geometric mean diameter, [:math:`nm`]
439
+
440
+ References
441
+ ----------
442
+ - :cite:`teohMitigatingClimateForcing2020`
443
+ """
444
+ t4_t2 = jet.thrust_setting_nd(
445
+ true_airspeed,
446
+ thrust_setting,
447
+ air_temperature,
448
+ air_pressure,
449
+ pressure_ratio,
450
+ q_fuel,
451
+ comp_efficiency=comp_efficiency,
452
+ cruise=cruise,
453
+ )
454
+ return (2.5883 * t4_t2**2) - (5.3723 * t4_t2) + 16.721 - delta_loss
455
+
456
+
457
+ def number_emissions_index_fractal_aggregates(
458
+ nvpm_ei_m: npt.NDArray[np.floating],
459
+ gmd: npt.NDArray[np.floating],
460
+ *,
461
+ gsd: float | np.floating | npt.NDArray[np.floating] = np.float32(1.80), # avoid promotion
462
+ rho_bc: float | np.floating = np.float32(1770.0),
463
+ k_tem: float | np.floating = np.float32(1.621e-5),
464
+ d_tem: float | np.floating = np.float32(0.39),
465
+ d_fm: float | np.floating = np.float32(2.76),
466
+ ) -> npt.NDArray[np.floating]:
467
+ """
468
+ Estimate the black carbon number emission index using the fractal aggregates (FA) model.
469
+
470
+ The FA model estimates the number emissions index from the mass emissions index,
471
+ particle size distribution, and morphology.
472
+
473
+ Parameters
474
+ ----------
475
+ nvpm_ei_m: npt.NDArray[np.floating]
476
+ Black carbon mass emissions index, [:math:`kg/kg_{fuel}`]
477
+ gmd: npt.NDArray[np.floating]
478
+ Black carbon geometric mean diameter, [:math:`m`]
479
+ gsd: float
480
+ Black carbon geometric standard deviation (assumed to be 1.80)
481
+ rho_bc: float
482
+ Black carbon material density (1770 kg/m**3), [:math:`kg m^{-3}`]
483
+ k_tem: float
484
+ Transmission electron microscopy prefactor coefficient (assumed to be 1.621e-5)
485
+ d_tem: float
486
+ Transmission electron microscopy exponent coefficient (assumed to be 0.39)
487
+ d_fm: float
488
+ Mass-mobility exponent (assumed to be 2.76)
489
+
490
+ Returns
491
+ -------
492
+ npt.NDArray[np.floating]
493
+ Black carbon number emissions index, [:math:`kg_{fuel}^{-1}`]
494
+
495
+ References
496
+ ----------
497
+ - FA model: :cite:`teohMethodologyRelateBlack2019`
498
+ - ``gmd``, ``gsd``, ``d_fm``: :cite:`teohMitigatingClimateForcing2020`
499
+ - ``rho_bc``: :cite:`parkMeasurementInherentMaterial2004`
500
+ - ``k_tem``, ``d_tem``: :cite:`dastanpourObservationsCorrelationPrimary2014`
501
+ """
502
+ phi = 3.0 * d_tem + (1.0 - d_tem) * d_fm
503
+ exponential_term = np.exp(0.5 * phi**2 * np.log(gsd) ** 2)
504
+ denom = rho_bc * (np.pi / 6.0) * k_tem ** (3.0 - d_fm) * gmd**phi * exponential_term
505
+ return nvpm_ei_m / denom
506
+
507
+
508
+ # -------------------------------------------------------------
509
+ # Scale nvPM emissions indices due to sustainable aviation fuel
510
+ # -------------------------------------------------------------
511
+
512
+
513
+ def nvpm_number_ei_pct_reduction_due_to_saf(
514
+ hydrogen_content: float | npt.NDArray[np.floating],
515
+ thrust_setting: npt.NDArray[np.floating],
516
+ ) -> npt.NDArray[np.floating]:
517
+ """
518
+ Adjust nvPM number emissions index to account for the effects of sustainable aviation fuels.
519
+
520
+ Parameters
521
+ ----------
522
+ hydrogen_content: float
523
+ The percentage of hydrogen mass content in the fuel.
524
+ thrust_setting: npt.NDArray[np.floating]
525
+ Engine thrust setting, where the equivalent fuel mass flow rate per engine at
526
+ sea level, :math:`[0 - 1]`.
527
+
528
+ Returns
529
+ -------
530
+ npt.NDArray[np.floating]
531
+ Percentage reduction in nvPM number emissions index
532
+
533
+ References
534
+ ----------
535
+ - :cite:`teohTargetedUseSustainable2022`
536
+ - :cite:`bremEffectsFuelAromatic2015`
537
+ """
538
+ a0 = -114.21
539
+ a1 = 1.06
540
+ a2 = 0.5
541
+ return _template_saf_reduction(hydrogen_content, thrust_setting, a0, a1, a2)
542
+
543
+
544
+ def nvpm_mass_ei_pct_reduction_due_to_saf(
545
+ hydrogen_content: float | npt.NDArray[np.floating],
546
+ thrust_setting: npt.NDArray[np.floating],
547
+ ) -> npt.NDArray[np.floating]:
548
+ """
549
+ Adjust nvPM mass emissions index to account for the effects of sustainable aviation fuels.
550
+
551
+ For fuel with hydrogen mass content > 14.3, the adjustment factor is adopted from
552
+ Teoh et al. (2022), which was used to calculate the change in nvPM EIn.
553
+
554
+ Parameters
555
+ ----------
556
+ hydrogen_content: float
557
+ The percentage of hydrogen mass content in the fuel.
558
+ thrust_setting: npt.NDArray[np.floating]
559
+ Engine thrust setting, where the equivalent fuel mass flow rate per engine at
560
+ sea level, :math:`[0 - 1]`.
561
+
562
+ Returns
563
+ -------
564
+ npt.NDArray[np.floating]
565
+ Percentage reduction in nvPM number emissions index
566
+
567
+ References
568
+ ----------
569
+ - :cite:`teohTargetedUseSustainable2022`
570
+ - :cite:`bremEffectsFuelAromatic2015`
571
+ """
572
+ a0 = -124.05
573
+ a1 = 1.02
574
+ a2 = 0.6
575
+ return _template_saf_reduction(hydrogen_content, thrust_setting, a0, a1, a2)
576
+
577
+
578
+ def _template_saf_reduction(
579
+ hydrogen_content: float | npt.NDArray[np.floating],
580
+ thrust_setting: npt.NDArray[np.floating],
581
+ a0: float,
582
+ a1: float,
583
+ a2: float,
584
+ ) -> npt.NDArray[np.floating]:
585
+ # Thrust setting cannot be computed when engine data is not provided in
586
+ # the ICAO EDB, so set default to 45% thrust.
587
+ thrust_setting = np.nan_to_num(thrust_setting, nan=0.45)
588
+ delta_h = hydrogen_content - 13.80
589
+ d_nvpm_ein_pct = (a0 + a1 * (thrust_setting * 100.0)) * delta_h
590
+
591
+ # Adjust when delta_h is large
592
+ if isinstance(delta_h, np.ndarray):
593
+ filt = delta_h > 0.5
594
+ d_nvpm_ein_pct[filt] *= np.exp(0.5 * (a2 - delta_h[filt]))
595
+ elif delta_h > 0.5:
596
+ d_nvpm_ein_pct *= np.exp(0.5 * (a2 - delta_h))
597
+
598
+ d_nvpm_ein_pct.clip(min=-90.0, max=0.0, out=d_nvpm_ein_pct)
599
+ return d_nvpm_ein_pct