pycontrails 0.50.0__cp39-cp39-win_amd64.whl → 0.50.2__cp39-cp39-win_amd64.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/_version.py +2 -2
- pycontrails/core/flight.py +108 -6
- pycontrails/core/interpolation.py +39 -1
- pycontrails/core/met.py +10 -12
- pycontrails/core/rgi_cython.cp39-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +10 -8
- pycontrails/datalib/goes.py +14 -12
- pycontrails/models/cocip/cocip.py +22 -5
- pycontrails/models/cocip/cocip_params.py +11 -2
- pycontrails/models/cocip/contrail_properties.py +4 -9
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
- pycontrails/models/cocip/wake_vortex.py +22 -1
- pycontrails/models/cocipgrid/cocip_grid.py +103 -6
- pycontrails/models/emissions/emissions.py +2 -2
- pycontrails/models/emissions/static/default-engine-uids.csv +1 -1
- pycontrails/models/emissions/static/{edb-gaseous-v28c-engines.csv → edb-gaseous-v29b-engines.csv} +49 -11
- pycontrails/models/emissions/static/{edb-nvpm-v28c-engines.csv → edb-nvpm-v29b-engines.csv} +90 -54
- pycontrails/models/issr.py +1 -1
- pycontrails/models/ps_model/ps_aircraft_params.py +13 -1
- pycontrails/models/ps_model/ps_grid.py +20 -20
- pycontrails/models/ps_model/ps_model.py +1 -1
- pycontrails/models/ps_model/ps_operational_limits.py +202 -1
- pycontrails/models/ps_model/static/ps-aircraft-params-20240417.csv +64 -0
- pycontrails/physics/units.py +2 -2
- pycontrails/utils/types.py +3 -1
- {pycontrails-0.50.0.dist-info → pycontrails-0.50.2.dist-info}/METADATA +1 -1
- {pycontrails-0.50.0.dist-info → pycontrails-0.50.2.dist-info}/RECORD +31 -30
- pycontrails/models/ps_model/static/ps-aircraft-params-20240209.csv +0 -63
- {pycontrails-0.50.0.dist-info → pycontrails-0.50.2.dist-info}/LICENSE +0 -0
- {pycontrails-0.50.0.dist-info → pycontrails-0.50.2.dist-info}/NOTICE +0 -0
- {pycontrails-0.50.0.dist-info → pycontrails-0.50.2.dist-info}/WHEEL +0 -0
- {pycontrails-0.50.0.dist-info → pycontrails-0.50.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Wave-vortex downwash functions from Unterstrasser (2016).
|
|
2
|
+
|
|
3
|
+
Notes
|
|
4
|
+
-----
|
|
5
|
+
:cite:`unterstrasserPropertiesYoungContrails2016` provides a parameterized model of the
|
|
6
|
+
survival fraction of the contrail ice crystal number ``f_surv`` during the wake-vortex phase.
|
|
7
|
+
The model was developed based on output from large eddy simulations, and improves agreement with
|
|
8
|
+
LES outputs relative to the default survival fraction parameterization used in CoCiP.
|
|
9
|
+
|
|
10
|
+
For comparison, CoCiP assumes that ``f_surv`` is equal to the change in the contrail ice water
|
|
11
|
+
content (by mass) before and after the wake vortex phase. However, for larger (smaller) ice
|
|
12
|
+
particles, their survival fraction by number could be smaller (larger) than their survival fraction
|
|
13
|
+
by mass. This is particularly important in the "soot-poor" scenario, for example, in cleaner
|
|
14
|
+
lean-burn engines where their soot emissions can be 3-4 orders of magnitude lower than conventional
|
|
15
|
+
RQL engines.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import numpy.typing as npt
|
|
23
|
+
|
|
24
|
+
from pycontrails.models.cocip.wake_vortex import wake_vortex_separation
|
|
25
|
+
from pycontrails.physics import constants, thermo
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ice_particle_number_survival_fraction(
|
|
29
|
+
air_temperature: npt.NDArray[np.float64],
|
|
30
|
+
rhi_0: npt.NDArray[np.float64],
|
|
31
|
+
ei_h2o: npt.NDArray[np.float64] | float,
|
|
32
|
+
wingspan: npt.NDArray[np.float64] | float,
|
|
33
|
+
true_airspeed: npt.NDArray[np.float64],
|
|
34
|
+
fuel_flow: npt.NDArray[np.float64],
|
|
35
|
+
aei_n: npt.NDArray[np.float64],
|
|
36
|
+
z_desc: npt.NDArray[np.float64],
|
|
37
|
+
) -> npt.NDArray[np.float64]:
|
|
38
|
+
r"""
|
|
39
|
+
Calculate fraction of ice particle number surviving the wake vortex phase and required inputs.
|
|
40
|
+
|
|
41
|
+
This implementation is based on the work of :cite:`unterstrasserPropertiesYoungContrails2016`
|
|
42
|
+
and is an improved estimation compared with
|
|
43
|
+
:func:`contrail_properties.ice_particle_survival_fraction`.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
air_temperature : npt.NDArray[np.float64]
|
|
48
|
+
ambient temperature for each waypoint, [:math:`K`]
|
|
49
|
+
rhi_0: npt.NDArray[np.float64]
|
|
50
|
+
Relative humidity with respect to ice at the flight waypoint
|
|
51
|
+
ei_h2o : npt.NDArray[np.float64] | float
|
|
52
|
+
Emission index of water vapor, [:math:`kg \ kg^{-1}`]
|
|
53
|
+
wingspan : npt.NDArray[np.float64] | float
|
|
54
|
+
aircraft wingspan, [:math:`m`]
|
|
55
|
+
true_airspeed : npt.NDArray[np.float64]
|
|
56
|
+
true airspeed for each waypoint, [:math:`m s^{-1}`]
|
|
57
|
+
fuel_flow : npt.NDArray[np.float64]
|
|
58
|
+
Fuel mass flow rate, [:math:`kg s^{-1}`]
|
|
59
|
+
aei_n : npt.NDArray[np.float64]
|
|
60
|
+
Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
|
|
61
|
+
z_desc : npt.NDArray[np.float64]
|
|
62
|
+
Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
|
|
63
|
+
[:math:`m`].
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
npt.NDArray[np.float64]
|
|
68
|
+
Fraction of contrail ice particle number that survive the wake vortex phase.
|
|
69
|
+
|
|
70
|
+
References
|
|
71
|
+
----------
|
|
72
|
+
- :cite:`unterstrasserPropertiesYoungContrails2016`
|
|
73
|
+
|
|
74
|
+
Notes
|
|
75
|
+
-----
|
|
76
|
+
- See eq. (3), (9), and (10) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
77
|
+
- For consistency in CoCiP, ``z_desc`` should be calculated using :func:`dz_max` instead of
|
|
78
|
+
using :func:`z_desc_length_scale`.
|
|
79
|
+
"""
|
|
80
|
+
# Length scales
|
|
81
|
+
z_atm = z_atm_length_scale(air_temperature, rhi_0)
|
|
82
|
+
rho_emit = emitted_water_vapour_concentration(ei_h2o, wingspan, true_airspeed, fuel_flow)
|
|
83
|
+
z_emit = z_emit_length_scale(rho_emit, air_temperature)
|
|
84
|
+
z_total = z_total_length_scale(aei_n, z_atm, z_emit, z_desc)
|
|
85
|
+
return _survival_fraction_from_length_scale(z_total)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def z_total_length_scale(
|
|
89
|
+
aei_n: npt.NDArray[np.float64],
|
|
90
|
+
z_atm: npt.NDArray[np.float64],
|
|
91
|
+
z_emit: npt.NDArray[np.float64],
|
|
92
|
+
z_desc: npt.NDArray[np.float64],
|
|
93
|
+
) -> npt.NDArray[np.float64]:
|
|
94
|
+
"""
|
|
95
|
+
Calculate the total length-scale effect of the wake vortex downwash.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
aei_n : npt.NDArray[np.float64]
|
|
100
|
+
Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
|
|
101
|
+
z_atm : npt.NDArray[np.float64]
|
|
102
|
+
Length-scale effect of ambient supersaturation on the ice crystal mass budget, [:math:`m`]
|
|
103
|
+
z_emit : npt.NDArray[np.float64]
|
|
104
|
+
Length-scale effect of water vapour emissions on the ice crystal mass budget, [:math:`m`]
|
|
105
|
+
z_desc : npt.NDArray[np.float64]
|
|
106
|
+
Final vertical displacement of the wake vortex, `dz_max` in `wake_vortex.py`, [:math:`m`]
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
npt.NDArray[np.float64]
|
|
111
|
+
Total length-scale effect of the wake vortex downwash, [:math:`m`]
|
|
112
|
+
"""
|
|
113
|
+
alpha_base = (aei_n / 2.8e14) ** (-0.18)
|
|
114
|
+
alpha_atm = 1.7 * alpha_base
|
|
115
|
+
alpha_emit = 1.15 * alpha_base
|
|
116
|
+
|
|
117
|
+
z_total = alpha_atm * z_atm + alpha_emit * z_emit - 0.6 * z_desc
|
|
118
|
+
|
|
119
|
+
z_total.clip(min=0.0, out=z_total)
|
|
120
|
+
return z_total
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def z_atm_length_scale(
|
|
124
|
+
air_temperature: npt.NDArray[np.float64],
|
|
125
|
+
rhi_0: npt.NDArray[np.float64],
|
|
126
|
+
*,
|
|
127
|
+
n_iter: int = 10,
|
|
128
|
+
) -> npt.NDArray[np.float64]:
|
|
129
|
+
"""Calculate the length-scale effect of ambient supersaturation on the ice crystal mass budget.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
air_temperature : npt.NDArray[np.float64]
|
|
134
|
+
Ambient temperature for each waypoint, [:math:`K`].
|
|
135
|
+
rhi_0 : npt.NDArray[np.float64]
|
|
136
|
+
Relative humidity with respect to ice at the flight waypoint.
|
|
137
|
+
n_iter : int
|
|
138
|
+
Number of iterations, set to 10 as default where ``z_atm`` is accurate to within +-1 m.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
npt.NDArray[np.float64]
|
|
143
|
+
The effect of the ambient supersaturation on the ice crystal mass budget,
|
|
144
|
+
provided as a length scale equivalent, [:math:`m`].
|
|
145
|
+
|
|
146
|
+
Notes
|
|
147
|
+
-----
|
|
148
|
+
- See eq. (5) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
149
|
+
"""
|
|
150
|
+
# Only perform operation when the ambient condition is supersaturated w.r.t. ice
|
|
151
|
+
issr = rhi_0 > 1.0
|
|
152
|
+
|
|
153
|
+
rhi_issr = rhi_0[issr]
|
|
154
|
+
air_temperature_issr = air_temperature[issr]
|
|
155
|
+
|
|
156
|
+
# Solve non-linear equation numerically using the bisection method
|
|
157
|
+
# Did not use scipy functions because it is unstable when dealing with np.arrays
|
|
158
|
+
z_1 = np.zeros_like(rhi_issr)
|
|
159
|
+
z_2 = np.full_like(rhi_issr, 1000.0)
|
|
160
|
+
lhs = rhi_issr * thermo.e_sat_ice(air_temperature_issr) / air_temperature_issr
|
|
161
|
+
|
|
162
|
+
dry_adiabatic_lapse_rate = constants.g / constants.c_pd
|
|
163
|
+
for _ in range(n_iter):
|
|
164
|
+
z_est = 0.5 * (z_1 + z_2)
|
|
165
|
+
rhs = (thermo.e_sat_ice(air_temperature_issr + dry_adiabatic_lapse_rate * z_est)) / (
|
|
166
|
+
air_temperature_issr + dry_adiabatic_lapse_rate * z_est
|
|
167
|
+
)
|
|
168
|
+
z_1[lhs > rhs] = z_est[lhs > rhs]
|
|
169
|
+
z_2[lhs < rhs] = z_est[lhs < rhs]
|
|
170
|
+
|
|
171
|
+
out = np.zeros_like(rhi_0)
|
|
172
|
+
out[issr] = 0.5 * (z_1 + z_2)
|
|
173
|
+
return out
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def emitted_water_vapour_concentration(
|
|
177
|
+
ei_h2o: npt.NDArray[np.float64] | float,
|
|
178
|
+
wingspan: npt.NDArray[np.float64] | float,
|
|
179
|
+
true_airspeed: npt.NDArray[np.float64],
|
|
180
|
+
fuel_flow: npt.NDArray[np.float64],
|
|
181
|
+
) -> npt.NDArray[np.float64]:
|
|
182
|
+
r"""
|
|
183
|
+
Calculate aircraft-emitted water vapour concentration in the plume.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
ei_h2o : npt.NDArray[np.float64] | float
|
|
188
|
+
Emission index of water vapor, [:math:`kg \ kg^{-1}`]
|
|
189
|
+
wingspan : npt.NDArray[np.float64] | float
|
|
190
|
+
aircraft wingspan, [:math:`m`]
|
|
191
|
+
true_airspeed : npt.NDArray[np.float64]
|
|
192
|
+
true airspeed for each waypoint, [:math:`m s^{-1}`]
|
|
193
|
+
fuel_flow : npt.NDArray[np.float64]
|
|
194
|
+
Fuel mass flow rate, [:math:`kg s^{-1}`]
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
npt.NDArray[np.float64]
|
|
199
|
+
Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
|
|
200
|
+
|
|
201
|
+
Notes
|
|
202
|
+
-----
|
|
203
|
+
- See eq. (6) and (A8) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
204
|
+
"""
|
|
205
|
+
h2o_per_dist = (ei_h2o * fuel_flow) / true_airspeed
|
|
206
|
+
area_p = plume_area(wingspan)
|
|
207
|
+
return h2o_per_dist / area_p
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def z_emit_length_scale(
|
|
211
|
+
rho_emit: npt.NDArray[np.float64], air_temperature: npt.NDArray[np.float64], *, n_iter: int = 10
|
|
212
|
+
) -> npt.NDArray[np.float64]:
|
|
213
|
+
"""Calculate the length-scale effect of water vapour emissions on the ice crystal mass budget.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
rho_emit : npt.NDArray[np.float64] | float
|
|
218
|
+
Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
|
|
219
|
+
air_temperature : npt.NDArray[np.float64]
|
|
220
|
+
ambient temperature for each waypoint, [:math:`K`]
|
|
221
|
+
n_iter : int
|
|
222
|
+
Number of iterations, set to 10 as default where ``z_emit`` is accurate to within +-1 m.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
npt.NDArray[np.float64]
|
|
227
|
+
The effect of the aircraft water vapour emission on the ice crystal mass budget,
|
|
228
|
+
provided as a length scale equivalent, [:math:`m`]
|
|
229
|
+
|
|
230
|
+
Notes
|
|
231
|
+
-----
|
|
232
|
+
- See eq. (7) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
233
|
+
"""
|
|
234
|
+
# Solve non-linear equation numerically using the bisection method
|
|
235
|
+
# Did not use scipy functions because it is unstable when dealing with np.arrays
|
|
236
|
+
z_1 = np.zeros_like(rho_emit)
|
|
237
|
+
z_2 = np.full_like(rho_emit, 1000.0)
|
|
238
|
+
|
|
239
|
+
lhs = (thermo.e_sat_ice(air_temperature) / (constants.R_v * air_temperature)) + rho_emit
|
|
240
|
+
|
|
241
|
+
dry_adiabatic_lapse_rate = constants.g / constants.c_pd
|
|
242
|
+
for _ in range(n_iter):
|
|
243
|
+
z_est = 0.5 * (z_1 + z_2)
|
|
244
|
+
rhs = thermo.e_sat_ice(air_temperature + dry_adiabatic_lapse_rate * z_est) / (
|
|
245
|
+
constants.R_v * (air_temperature + dry_adiabatic_lapse_rate * z_est)
|
|
246
|
+
)
|
|
247
|
+
z_1[lhs > rhs] = z_est[lhs > rhs]
|
|
248
|
+
z_2[lhs < rhs] = z_est[lhs < rhs]
|
|
249
|
+
|
|
250
|
+
return 0.5 * (z_1 + z_2)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def plume_area(wingspan: npt.NDArray[np.float64] | float) -> npt.NDArray[np.float64] | float:
|
|
254
|
+
"""Calculate area of the wake-vortex plume.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
wingspan : npt.NDArray[np.float64] | float
|
|
259
|
+
aircraft wingspan, [:math:`m`]
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
float
|
|
264
|
+
Area of two wake-vortex plumes, [:math:`m^{2}`]
|
|
265
|
+
|
|
266
|
+
Notes
|
|
267
|
+
-----
|
|
268
|
+
- See eq. (A6) and (A7) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
269
|
+
"""
|
|
270
|
+
r_plume = 1.5 + 0.314 * wingspan
|
|
271
|
+
return 2.0 * 2.0 * np.pi * r_plume**2
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def z_desc_length_scale(
|
|
275
|
+
wingspan: npt.NDArray[np.float64] | float,
|
|
276
|
+
air_temperature: npt.NDArray[np.float64],
|
|
277
|
+
air_pressure: npt.NDArray[np.float64],
|
|
278
|
+
true_airspeed: npt.NDArray[np.float64],
|
|
279
|
+
aircraft_mass: npt.NDArray[np.float64],
|
|
280
|
+
dT_dz: npt.NDArray[np.float64],
|
|
281
|
+
) -> npt.NDArray[np.float64]:
|
|
282
|
+
"""Calculate the final vertical displacement of the wake vortex.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
wingspan : npt.NDArray[np.float64] | float
|
|
287
|
+
aircraft wingspan, [:math:`m`]
|
|
288
|
+
air_temperature : npt.NDArray[np.float64]
|
|
289
|
+
ambient temperature for each waypoint, [:math:`K`]
|
|
290
|
+
air_pressure : npt.NDArray[np.float64]
|
|
291
|
+
pressure altitude at each waypoint, [:math:`Pa`]
|
|
292
|
+
true_airspeed : npt.NDArray[np.float64]
|
|
293
|
+
true airspeed for each waypoint, [:math:`m s^{-1}`]
|
|
294
|
+
aircraft_mass : npt.NDArray[np.float64] | float
|
|
295
|
+
aircraft mass for each waypoint, [:math:`kg`]
|
|
296
|
+
dT_dz : npt.NDArray[np.float64]
|
|
297
|
+
potential temperature gradient, [:math:`K m^{-1}`]
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
npt.NDArray[np.float64]
|
|
302
|
+
Final vertical displacement of the wake vortex, [:math:`m`]
|
|
303
|
+
|
|
304
|
+
Notes
|
|
305
|
+
-----
|
|
306
|
+
- See eq. (4) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
307
|
+
"""
|
|
308
|
+
gamma_0 = _initial_wake_vortex_circulation(
|
|
309
|
+
wingspan, air_temperature, air_pressure, true_airspeed, aircraft_mass
|
|
310
|
+
)
|
|
311
|
+
n_bv = thermo.brunt_vaisala_frequency(air_pressure, air_temperature, dT_dz)
|
|
312
|
+
return ((8.0 * gamma_0) / (np.pi * n_bv)) ** 0.5
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _initial_wake_vortex_circulation(
|
|
316
|
+
wingspan: npt.NDArray[np.float64] | float,
|
|
317
|
+
air_temperature: npt.NDArray[np.float64],
|
|
318
|
+
air_pressure: npt.NDArray[np.float64],
|
|
319
|
+
true_airspeed: npt.NDArray[np.float64],
|
|
320
|
+
aircraft_mass: npt.NDArray[np.float64],
|
|
321
|
+
) -> npt.NDArray[np.float64]:
|
|
322
|
+
"""Calculate initial wake vortex circulation.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
wingspan : npt.NDArray[np.float64] | float
|
|
327
|
+
aircraft wingspan, [:math:`m`]
|
|
328
|
+
air_temperature : npt.NDArray[np.float64]
|
|
329
|
+
ambient temperature for each waypoint, [:math:`K`]
|
|
330
|
+
air_pressure : npt.NDArray[np.float64]
|
|
331
|
+
pressure altitude at each waypoint, [:math:`Pa`]
|
|
332
|
+
true_airspeed : npt.NDArray[np.float64]
|
|
333
|
+
true airspeed for each waypoint, [:math:`m s^{-1}`]
|
|
334
|
+
aircraft_mass : npt.NDArray[np.float64] | float
|
|
335
|
+
aircraft mass for each waypoint, [:math:`kg`]
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
npt.NDArray[np.float64]
|
|
340
|
+
Initial wake vortex circulation, [:math:`m^{2} s^{-1}`]
|
|
341
|
+
|
|
342
|
+
Notes
|
|
343
|
+
-----
|
|
344
|
+
- This is a measure of the strength/intensity of the wake vortex circulation.
|
|
345
|
+
- See eq. (A1) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
346
|
+
"""
|
|
347
|
+
b_0 = wake_vortex_separation(wingspan)
|
|
348
|
+
rho_air = thermo.rho_d(air_temperature, air_pressure)
|
|
349
|
+
return (constants.g * aircraft_mass) / (rho_air * b_0 * true_airspeed)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _survival_fraction_from_length_scale(
|
|
353
|
+
z_total: npt.NDArray[np.float64],
|
|
354
|
+
) -> npt.NDArray[np.float64]:
|
|
355
|
+
"""
|
|
356
|
+
Calculate fraction of ice particle number surviving the wake vortex phase.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
z_total : npt.NDArray[np.float64]
|
|
361
|
+
Total length-scale effect of the wake vortex downwash, [:math:`m`]
|
|
362
|
+
|
|
363
|
+
Returns
|
|
364
|
+
-------
|
|
365
|
+
npt.NDArray[np.float64]
|
|
366
|
+
Fraction of ice particle number surviving the wake vortex phase
|
|
367
|
+
"""
|
|
368
|
+
f_surv = 0.45 + (1.19 / np.pi) * np.arctan(-1.35 + (z_total / 100.0))
|
|
369
|
+
np.clip(f_surv, 0.0, 1.0, out=f_surv)
|
|
370
|
+
return f_surv
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def initial_contrail_depth(
|
|
374
|
+
z_desc: npt.NDArray[np.float64],
|
|
375
|
+
f_surv: npt.NDArray[np.float64],
|
|
376
|
+
) -> npt.NDArray[np.float64]:
|
|
377
|
+
"""Calculate initial contrail depth using :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
z_desc : npt.NDArray[np.float64]
|
|
382
|
+
Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
|
|
383
|
+
[:math:`m`].
|
|
384
|
+
f_surv : npt.NDArray[np.float64]
|
|
385
|
+
Fraction of contrail ice particle number that survive the wake vortex phase.
|
|
386
|
+
See :func:`ice_particle_survival_fraction`.
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
npt.NDArray[np.float64]
|
|
391
|
+
Initial contrail depth, [:math:`m`]
|
|
392
|
+
|
|
393
|
+
Notes
|
|
394
|
+
-----
|
|
395
|
+
- See eq. (12), and (13) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
396
|
+
- For consistency in CoCiP, `z_desc` should be calculated using :func:`dz_max` instead of
|
|
397
|
+
using :func:`z_desc_length_scale`.
|
|
398
|
+
"""
|
|
399
|
+
return z_desc * np.where(
|
|
400
|
+
f_surv <= 0.2,
|
|
401
|
+
6.0 * f_surv,
|
|
402
|
+
0.15 * f_surv + (6.0 - 0.15) * 0.2,
|
|
403
|
+
)
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
"""Wave-vortex downwash functions.
|
|
1
|
+
"""Wave-vortex downwash functions.
|
|
2
|
+
|
|
3
|
+
This module includes equations from the original CoCiP model
|
|
4
|
+
:cite:`schumannContrailCirrusPrediction2012`. An alternative set of equations based on
|
|
5
|
+
:cite:`unterstrasserPropertiesYoungContrails2016` is available in
|
|
6
|
+
:py:mod:`unterstrasser_wake_vortex`.
|
|
7
|
+
|
|
8
|
+
Unterstrasser Notes
|
|
9
|
+
-------------------
|
|
10
|
+
|
|
11
|
+
Improved estimation of the survival fraction of the contrail ice crystal number ``f_surv``
|
|
12
|
+
during the wake-vortex phase. This is a parameterised model that is developed based on
|
|
13
|
+
outputs provided by large eddy simulations.
|
|
14
|
+
|
|
15
|
+
For comparison, CoCiP assumes that ``f_surv`` is equal to the change in the contrail ice water
|
|
16
|
+
content (by mass) before and after the wake vortex phase. However, for larger (smaller) ice
|
|
17
|
+
particles, their survival fraction by number could be smaller (larger) than their survival fraction
|
|
18
|
+
by mass. This is particularly important in the "soot-poor" scenario, for example, in cleaner
|
|
19
|
+
lean-burn engines where their soot emissions can be 3-4 orders of magnitude lower than conventional
|
|
20
|
+
RQL engines.
|
|
21
|
+
|
|
22
|
+
"""
|
|
2
23
|
|
|
3
24
|
from __future__ import annotations
|
|
4
25
|
|
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, NoReturn, TypeVar, overload
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import numpy.typing as npt
|
|
13
13
|
import pandas as pd
|
|
14
|
+
import xarray as xr
|
|
14
15
|
|
|
15
16
|
import pycontrails
|
|
16
17
|
from pycontrails.core import models
|
|
@@ -20,7 +21,7 @@ from pycontrails.models import humidity_scaling, sac
|
|
|
20
21
|
from pycontrails.models.cocip import cocip, contrail_properties, wake_vortex, wind_shear
|
|
21
22
|
from pycontrails.models.cocipgrid.cocip_grid_params import CocipGridParams
|
|
22
23
|
from pycontrails.models.emissions import Emissions
|
|
23
|
-
from pycontrails.physics import geo, thermo, units
|
|
24
|
+
from pycontrails.physics import constants, geo, thermo, units
|
|
24
25
|
from pycontrails.utils import dependencies
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
@@ -122,6 +123,13 @@ class CocipGrid(models.Model):
|
|
|
122
123
|
msg = "Parameter 'radiative_heating_effects' is not yet implemented in CocipGrid"
|
|
123
124
|
raise NotImplementedError(msg)
|
|
124
125
|
|
|
126
|
+
if self.params["unterstrasser_ice_survival_fraction"]:
|
|
127
|
+
msg = (
|
|
128
|
+
"Parameter 'unterstrasser_ice_survival_fraction' is not "
|
|
129
|
+
"yet implemented in CocipGrid"
|
|
130
|
+
)
|
|
131
|
+
raise NotImplementedError(msg)
|
|
132
|
+
|
|
125
133
|
self._target_dtype = np.result_type(*self.met.data.values())
|
|
126
134
|
|
|
127
135
|
@overload
|
|
@@ -304,23 +312,76 @@ class CocipGrid(models.Model):
|
|
|
304
312
|
``time_end``, new slices are selected from the larger ``self.met`` and
|
|
305
313
|
``self.rad`` data. The slicing only occurs in the time domain.
|
|
306
314
|
|
|
315
|
+
The end of currently-used ``met`` and ``rad`` will be used as the start
|
|
316
|
+
of newly-selected met slices when possible to avoid losing and re-loading
|
|
317
|
+
already-loaded met data.
|
|
318
|
+
|
|
307
319
|
If ``self.params["downselect_met"]`` is True, :func:`_downselect_met` has
|
|
308
320
|
already performed a spatial downselection of the met data.
|
|
309
321
|
"""
|
|
310
|
-
|
|
322
|
+
|
|
323
|
+
if met is None:
|
|
311
324
|
# idx is the first index at which self.met.variables["time"].to_numpy() >= time_end
|
|
312
325
|
idx = np.searchsorted(self.met.indexes["time"].to_numpy(), time_end)
|
|
313
326
|
sl = slice(max(0, idx - 1), idx + 1)
|
|
314
327
|
logger.debug("Select met slice %s", sl)
|
|
315
328
|
met = MetDataset(self.met.data.isel(time=sl), copy=False)
|
|
316
329
|
|
|
317
|
-
|
|
330
|
+
elif time_end > met.indexes["time"].to_numpy()[-1]:
|
|
331
|
+
current_times = met.indexes["time"].to_numpy()
|
|
332
|
+
all_times = self.met.indexes["time"].to_numpy()
|
|
333
|
+
# idx is the first index at which all_times >= time_end
|
|
334
|
+
idx = np.searchsorted(all_times, time_end)
|
|
335
|
+
sl = slice(max(0, idx - 1), idx + 1)
|
|
336
|
+
|
|
337
|
+
# case 1: cannot re-use end of current met as start of new met
|
|
338
|
+
if current_times[-1] != all_times[sl.start]:
|
|
339
|
+
logger.debug("Select met slice %s", sl)
|
|
340
|
+
met = MetDataset(self.met.data.isel(time=sl), copy=False)
|
|
341
|
+
# case 2: can re-use end of current met plus one step of new met
|
|
342
|
+
elif sl.start < all_times.size - 1:
|
|
343
|
+
sl = slice(sl.start + 1, sl.stop)
|
|
344
|
+
logger.debug("Reuse end of met and select met slice %s", sl)
|
|
345
|
+
met = MetDataset(
|
|
346
|
+
xr.concat((met.data.isel(time=[-1]), self.met.data.isel(time=sl)), dim="time"),
|
|
347
|
+
copy=False,
|
|
348
|
+
)
|
|
349
|
+
# case 3: can re-use end of current met and nothing else
|
|
350
|
+
else:
|
|
351
|
+
logger.debug("Reuse end of met")
|
|
352
|
+
met = MetDataset(met.data.isel(time=[-1]), copy=False)
|
|
353
|
+
|
|
354
|
+
if rad is None:
|
|
318
355
|
# idx is the first index at which self.rad.variables["time"].to_numpy() >= time_end
|
|
319
356
|
idx = np.searchsorted(self.rad.indexes["time"].to_numpy(), time_end)
|
|
320
357
|
sl = slice(max(0, idx - 1), idx + 1)
|
|
321
358
|
logger.debug("Select rad slice %s", sl)
|
|
322
359
|
rad = MetDataset(self.rad.data.isel(time=sl), copy=False)
|
|
323
360
|
|
|
361
|
+
elif time_end > rad.indexes["time"].to_numpy()[-1]:
|
|
362
|
+
current_times = rad.indexes["time"].to_numpy()
|
|
363
|
+
all_times = self.rad.indexes["time"].to_numpy()
|
|
364
|
+
# idx is the first index at which all_times >= time_end
|
|
365
|
+
idx = np.searchsorted(all_times, time_end)
|
|
366
|
+
sl = slice(max(0, idx - 1), idx + 1)
|
|
367
|
+
|
|
368
|
+
# case 1: cannot re-use end of current rad as start of new rad
|
|
369
|
+
if current_times[-1] != all_times[sl.start]:
|
|
370
|
+
logger.debug("Select rad slice %s", sl)
|
|
371
|
+
rad = MetDataset(self.rad.data.isel(time=sl), copy=False)
|
|
372
|
+
# case 2: can re-use end of current rad plus one step of new rad
|
|
373
|
+
elif sl.start < all_times.size - 1:
|
|
374
|
+
sl = slice(sl.start + 1, sl.stop)
|
|
375
|
+
logger.debug("Reuse end of rad and select rad slice %s", sl)
|
|
376
|
+
rad = MetDataset(
|
|
377
|
+
xr.concat((rad.data.isel(time=[-1]), self.rad.data.isel(time=sl)), dim="time"),
|
|
378
|
+
copy=False,
|
|
379
|
+
)
|
|
380
|
+
# case 3: can re-use end of current rad and nothing else
|
|
381
|
+
else:
|
|
382
|
+
logger.debug("Reuse end of rad")
|
|
383
|
+
rad = MetDataset(rad.data.isel(time=[-1]), copy=False)
|
|
384
|
+
|
|
324
385
|
return met, rad
|
|
325
386
|
|
|
326
387
|
def _attach_verbose_outputs_evolution(self, contrail_list: list[GeoVectorDataset]) -> None:
|
|
@@ -398,6 +459,17 @@ class CocipGrid(models.Model):
|
|
|
398
459
|
nominal_segment_length=segment_length,
|
|
399
460
|
attrs=attrs,
|
|
400
461
|
)
|
|
462
|
+
|
|
463
|
+
if self.params["compute_atr20"]:
|
|
464
|
+
self.source["global_yearly_mean_rf_per_m"] = (
|
|
465
|
+
self.source["ef_per_m"].data
|
|
466
|
+
/ constants.surface_area_earth
|
|
467
|
+
/ constants.seconds_per_year
|
|
468
|
+
)
|
|
469
|
+
self.source["atr20_per_m"] = (
|
|
470
|
+
self.params["global_rf_to_atr20_factor"]
|
|
471
|
+
* self.source["global_yearly_mean_rf_per_m"].data
|
|
472
|
+
)
|
|
401
473
|
else:
|
|
402
474
|
self.source = result_merge_source(
|
|
403
475
|
result=summary,
|
|
@@ -406,6 +478,18 @@ class CocipGrid(models.Model):
|
|
|
406
478
|
nominal_segment_length=segment_length,
|
|
407
479
|
attrs=attrs,
|
|
408
480
|
)
|
|
481
|
+
|
|
482
|
+
if self.params["compute_atr20"]:
|
|
483
|
+
self.source["global_yearly_mean_rf_per_m"] = (
|
|
484
|
+
self.source["ef_per_m"]
|
|
485
|
+
/ constants.surface_area_earth
|
|
486
|
+
/ constants.seconds_per_year
|
|
487
|
+
)
|
|
488
|
+
self.source["atr20_per_m"] = (
|
|
489
|
+
self.params["global_rf_to_atr20_factor"]
|
|
490
|
+
* self.source["global_yearly_mean_rf_per_m"]
|
|
491
|
+
)
|
|
492
|
+
|
|
409
493
|
return self.source
|
|
410
494
|
|
|
411
495
|
# ---------------------------
|
|
@@ -1459,11 +1543,11 @@ def find_initial_persistent_contrails(
|
|
|
1459
1543
|
air_pressure_1=air_pressure_1,
|
|
1460
1544
|
)
|
|
1461
1545
|
iwc_1 = contrail_properties.iwc_post_wake_vortex(iwc, iwc_ad)
|
|
1546
|
+
f_surv = contrail_properties.ice_particle_survival_fraction(iwc, iwc_1)
|
|
1462
1547
|
n_ice_per_m = contrail_properties.ice_particle_number(
|
|
1463
1548
|
nvpm_ei_n=nvpm_ei_n,
|
|
1464
1549
|
fuel_dist=fuel_dist,
|
|
1465
|
-
|
|
1466
|
-
iwc_1=iwc_1,
|
|
1550
|
+
f_surv=f_surv,
|
|
1467
1551
|
air_temperature=air_temperature,
|
|
1468
1552
|
T_crit_sac=T_crit_sac,
|
|
1469
1553
|
min_ice_particle_number_nvpm_ei_n=params["min_ice_particle_number_nvpm_ei_n"],
|
|
@@ -2241,6 +2325,14 @@ def _contrail_grid_variable_attrs() -> dict[str, dict[str, str]]:
|
|
|
2241
2325
|
"long_name": "Ice water content after the wake vortex phase",
|
|
2242
2326
|
"units": "kg_h2o / kg_air",
|
|
2243
2327
|
},
|
|
2328
|
+
"global_yearly_mean_rf_per_m": {
|
|
2329
|
+
"long_name": "Global yearly mean RF per meter of flight trajectory",
|
|
2330
|
+
"units": "W / m**2 / m",
|
|
2331
|
+
},
|
|
2332
|
+
"atr20_per_m": {
|
|
2333
|
+
"long_name": "Average Temperature Response over a 20 year horizon",
|
|
2334
|
+
"units": "K / m",
|
|
2335
|
+
},
|
|
2244
2336
|
}
|
|
2245
2337
|
|
|
2246
2338
|
|
|
@@ -2249,7 +2341,12 @@ def _supported_verbose_outputs_formation() -> set[str]:
|
|
|
2249
2341
|
|
|
2250
2342
|
Uses output of :func:`_contrail_grid_variable_attrs` as a source of truth.
|
|
2251
2343
|
"""
|
|
2252
|
-
return set(_contrail_grid_variable_attrs()) - {
|
|
2344
|
+
return set(_contrail_grid_variable_attrs()) - {
|
|
2345
|
+
"contrail_age",
|
|
2346
|
+
"ef_per_m",
|
|
2347
|
+
"global_yearly_mean_rf_per_m",
|
|
2348
|
+
"atr20_per_m",
|
|
2349
|
+
}
|
|
2253
2350
|
|
|
2254
2351
|
|
|
2255
2352
|
def _warn_not_wrap(met: MetDataset) -> None:
|
|
@@ -27,8 +27,8 @@ from pycontrails.models.humidity_scaling import HumidityScaling
|
|
|
27
27
|
from pycontrails.physics import constants, jet, units
|
|
28
28
|
|
|
29
29
|
_path_to_static = pathlib.Path(__file__).parent / "static"
|
|
30
|
-
EDB_ENGINE_PATH = _path_to_static / "edb-gaseous-
|
|
31
|
-
EDB_NVPM_PATH = _path_to_static / "edb-nvpm-
|
|
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
32
|
ENGINE_UID_PATH = _path_to_static / "default-engine-uids.csv"
|
|
33
33
|
|
|
34
34
|
|
|
@@ -11,7 +11,7 @@ A310,1GE016,CF6-80C2A2,2
|
|
|
11
11
|
A318,01P08CM110,CFM56-5B9,2
|
|
12
12
|
A319,01P10IA019,IAE V2522-A5,2
|
|
13
13
|
A320,01P08CM105,CFM56-5B4/P SAC,2
|
|
14
|
-
A321,
|
|
14
|
+
A321,04P10IA027,IAE V2530-A5,2
|
|
15
15
|
A330,01P14RR102,Trent 772B,2
|
|
16
16
|
A332,01P14RR102,RR TRENT 772B,2
|
|
17
17
|
A333,1GE033,CF6-80E1A2,2
|