pycontrails 0.54.6__cp313-cp313-win_amd64.whl → 0.54.8__cp313-cp313-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/__init__.py +1 -1
- pycontrails/_version.py +9 -4
- pycontrails/core/aircraft_performance.py +12 -30
- pycontrails/core/airports.py +4 -1
- pycontrails/core/cache.py +4 -0
- pycontrails/core/flight.py +4 -4
- pycontrails/core/flightplan.py +10 -2
- pycontrails/core/met.py +53 -40
- pycontrails/core/met_var.py +18 -0
- pycontrails/core/models.py +79 -3
- pycontrails/core/rgi_cython.cp313-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +74 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/models/accf.py +4 -4
- pycontrails/models/cocip/cocip.py +52 -6
- pycontrails/models/cocip/cocip_params.py +10 -1
- pycontrails/models/cocip/contrail_properties.py +4 -6
- pycontrails/models/cocip/output_formats.py +12 -4
- pycontrails/models/cocip/radiative_forcing.py +2 -8
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +132 -30
- pycontrails/models/cocipgrid/cocip_grid.py +14 -11
- pycontrails/models/emissions/black_carbon.py +19 -14
- pycontrails/models/emissions/emissions.py +8 -8
- pycontrails/models/humidity_scaling/humidity_scaling.py +49 -4
- pycontrails/models/ps_model/ps_aircraft_params.py +1 -1
- pycontrails/models/ps_model/ps_grid.py +22 -22
- pycontrails/models/ps_model/ps_model.py +4 -7
- pycontrails/models/ps_model/static/{ps-aircraft-params-20240524.csv → ps-aircraft-params-20250328.csv} +58 -57
- pycontrails/models/ps_model/static/{ps-synonym-list-20240524.csv → ps-synonym-list-20250328.csv} +1 -0
- pycontrails/models/tau_cirrus.py +1 -0
- pycontrails/physics/constants.py +2 -1
- pycontrails/physics/jet.py +5 -4
- pycontrails/physics/static/{iata-cargo-load-factors-20241115.csv → iata-cargo-load-factors-20250221.csv} +3 -0
- pycontrails/physics/static/{iata-passenger-load-factors-20241115.csv → iata-passenger-load-factors-20250221.csv} +3 -0
- {pycontrails-0.54.6.dist-info → pycontrails-0.54.8.dist-info}/METADATA +5 -4
- {pycontrails-0.54.6.dist-info → pycontrails-0.54.8.dist-info}/RECORD +42 -40
- {pycontrails-0.54.6.dist-info → pycontrails-0.54.8.dist-info}/WHEEL +1 -1
- {pycontrails-0.54.6.dist-info → pycontrails-0.54.8.dist-info/licenses}/NOTICE +1 -1
- pycontrails/datalib/spire.py +0 -739
- {pycontrails-0.54.6.dist-info → pycontrails-0.54.8.dist-info/licenses}/LICENSE +0 -0
- {pycontrails-0.54.6.dist-info → pycontrails-0.54.8.dist-info}/top_level.txt +0 -0
|
@@ -18,11 +18,12 @@ import numpy.typing as npt
|
|
|
18
18
|
import pandas as pd
|
|
19
19
|
import xarray as xr
|
|
20
20
|
|
|
21
|
-
from pycontrails.core import met_var
|
|
21
|
+
from pycontrails.core import met_var, models
|
|
22
22
|
from pycontrails.core.aircraft_performance import AircraftPerformance
|
|
23
23
|
from pycontrails.core.fleet import Fleet
|
|
24
24
|
from pycontrails.core.flight import Flight
|
|
25
25
|
from pycontrails.core.met import MetDataset
|
|
26
|
+
from pycontrails.core.met_var import MetVariable
|
|
26
27
|
from pycontrails.core.models import Model, interpolate_met
|
|
27
28
|
from pycontrails.core.vector import GeoVectorDataset, VectorDataDict
|
|
28
29
|
from pycontrails.datalib import ecmwf, gfs
|
|
@@ -482,6 +483,42 @@ class Cocip(Model):
|
|
|
482
483
|
|
|
483
484
|
return self.source
|
|
484
485
|
|
|
486
|
+
@classmethod
|
|
487
|
+
def generic_rad_variables(cls) -> tuple[MetVariable, ...]:
|
|
488
|
+
"""Return a model-agnostic list of required radiation variables.
|
|
489
|
+
|
|
490
|
+
Returns
|
|
491
|
+
-------
|
|
492
|
+
tuple[MetVariable]
|
|
493
|
+
List of model-agnostic variants of required variables
|
|
494
|
+
"""
|
|
495
|
+
available = set(met_var.MET_VARIABLES)
|
|
496
|
+
return tuple(models._find_match(required, available) for required in cls.rad_variables)
|
|
497
|
+
|
|
498
|
+
@classmethod
|
|
499
|
+
def ecmwf_rad_variables(cls) -> tuple[MetVariable, ...]:
|
|
500
|
+
"""Return an ECMWF-specific list of required radiation variables.
|
|
501
|
+
|
|
502
|
+
Returns
|
|
503
|
+
-------
|
|
504
|
+
tuple[MetVariable]
|
|
505
|
+
List of ECMWF-specific variants of required variables
|
|
506
|
+
"""
|
|
507
|
+
available = set(ecmwf.ECMWF_VARIABLES)
|
|
508
|
+
return tuple(models._find_match(required, available) for required in cls.rad_variables)
|
|
509
|
+
|
|
510
|
+
@classmethod
|
|
511
|
+
def gfs_rad_variables(cls) -> tuple[MetVariable, ...]:
|
|
512
|
+
"""Return a GFS-specific list of required radiation variables.
|
|
513
|
+
|
|
514
|
+
Returns
|
|
515
|
+
-------
|
|
516
|
+
tuple[MetVariable]
|
|
517
|
+
List of GFS-specific variants of required variables
|
|
518
|
+
"""
|
|
519
|
+
available = set(gfs.GFS_VARIABLES)
|
|
520
|
+
return tuple(models._find_match(required, available) for required in cls.rad_variables)
|
|
521
|
+
|
|
485
522
|
def _set_timesteps(self) -> None:
|
|
486
523
|
"""Set the :attr:`timesteps` based on the ``source`` time range.
|
|
487
524
|
|
|
@@ -960,14 +997,14 @@ class Cocip(Model):
|
|
|
960
997
|
else:
|
|
961
998
|
f_surv = contrail_properties.ice_particle_survival_fraction(iwc, iwc_1)
|
|
962
999
|
|
|
963
|
-
|
|
1000
|
+
n_ice_per_m_0 = contrail_properties.initial_ice_particle_number(
|
|
964
1001
|
nvpm_ei_n=nvpm_ei_n,
|
|
965
1002
|
fuel_dist=fuel_dist,
|
|
966
|
-
f_surv=f_surv,
|
|
967
1003
|
air_temperature=air_temperature,
|
|
968
1004
|
T_crit_sac=T_critical_sac,
|
|
969
1005
|
min_ice_particle_number_nvpm_ei_n=self.params["min_ice_particle_number_nvpm_ei_n"],
|
|
970
1006
|
)
|
|
1007
|
+
n_ice_per_m_1 = n_ice_per_m_0 * f_surv
|
|
971
1008
|
|
|
972
1009
|
# Check for persistent initial_contrails
|
|
973
1010
|
persistent_1 = contrail_properties.initial_persistent(iwc_1, rhi_1)
|
|
@@ -981,6 +1018,8 @@ class Cocip(Model):
|
|
|
981
1018
|
self._sac_flight["rho_air_1"] = rho_air_1
|
|
982
1019
|
self._sac_flight["rhi_1"] = rhi_1
|
|
983
1020
|
self._sac_flight["iwc_1"] = iwc_1
|
|
1021
|
+
self._sac_flight["f_surv"] = f_surv
|
|
1022
|
+
self._sac_flight["n_ice_per_m_0"] = n_ice_per_m_0
|
|
984
1023
|
self._sac_flight["n_ice_per_m_1"] = n_ice_per_m_1
|
|
985
1024
|
self._sac_flight["persistent_1"] = persistent_1
|
|
986
1025
|
|
|
@@ -1347,7 +1386,13 @@ class Cocip(Model):
|
|
|
1347
1386
|
if verbose_outputs:
|
|
1348
1387
|
sac_cols += ["dT_dz", "ds_dz", "dz_max"]
|
|
1349
1388
|
|
|
1350
|
-
downwash_cols = [
|
|
1389
|
+
downwash_cols = [
|
|
1390
|
+
"rho_air_1",
|
|
1391
|
+
"iwc_1",
|
|
1392
|
+
"f_surv",
|
|
1393
|
+
"n_ice_per_m_0",
|
|
1394
|
+
"n_ice_per_m_1",
|
|
1395
|
+
]
|
|
1351
1396
|
df = pd.concat(
|
|
1352
1397
|
[
|
|
1353
1398
|
self.source.dataframe.set_index(col_idx),
|
|
@@ -2174,8 +2219,6 @@ def calc_contrail_properties(
|
|
|
2174
2219
|
air_temperature += contrail["cumul_heat"]
|
|
2175
2220
|
|
|
2176
2221
|
# get required radiation
|
|
2177
|
-
theta_rad = geo.orbital_position(time)
|
|
2178
|
-
sd0 = geo.solar_constant(theta_rad)
|
|
2179
2222
|
sdr = contrail["sdr"]
|
|
2180
2223
|
rsr = contrail["rsr"]
|
|
2181
2224
|
olr = contrail["olr"]
|
|
@@ -2210,6 +2253,9 @@ def calc_contrail_properties(
|
|
|
2210
2253
|
diffuse_h = contrail_properties.horizontal_diffusivity(ds_dz, depth)
|
|
2211
2254
|
|
|
2212
2255
|
if radiative_heating_effects:
|
|
2256
|
+
# theta_rad has float64 dtype, convert back to float32 if needed
|
|
2257
|
+
theta_rad = geo.orbital_position(time).astype(sdr.dtype, copy=False)
|
|
2258
|
+
sd0 = geo.solar_constant(theta_rad)
|
|
2213
2259
|
heat_rate = radiative_heating.heating_rate(
|
|
2214
2260
|
air_temperature=air_temperature,
|
|
2215
2261
|
rhi=rhi,
|
|
@@ -103,6 +103,8 @@ class CocipParams(AdvectionBuffers):
|
|
|
103
103
|
#: Cocip output with ``preprocess_lowmem=True`` is only guaranteed to match output
|
|
104
104
|
#: with ``preprocess_lowmem=False`` when run with ``interpolation_bounds_error=True``
|
|
105
105
|
#: to ensure no out-of-bounds interpolation occurs.
|
|
106
|
+
#:
|
|
107
|
+
#: .. versionadded:: 0.52.3
|
|
106
108
|
preprocess_lowmem: bool = False
|
|
107
109
|
|
|
108
110
|
# --------------
|
|
@@ -114,6 +116,8 @@ class CocipParams(AdvectionBuffers):
|
|
|
114
116
|
#: ``"auto"``, ``"tau_cirrus"`` will be computed during model initialization
|
|
115
117
|
#: iff the met data is dask-backed. Otherwise, it will be computed during model
|
|
116
118
|
#: evaluation after the met data is downselected.
|
|
119
|
+
#:
|
|
120
|
+
#: .. versionadded:: 0.47.1
|
|
117
121
|
compute_tau_cirrus_in_model_init: bool | str = "auto"
|
|
118
122
|
|
|
119
123
|
# ---------
|
|
@@ -152,10 +156,14 @@ class CocipParams(AdvectionBuffers):
|
|
|
152
156
|
#: These are not standard CoCiP outputs but based on the derivation used
|
|
153
157
|
#: in the first supplement to :cite:`yinPredictingClimateImpact2023`. ATR20 is defined
|
|
154
158
|
#: as the average temperature response over a 20 year horizon.
|
|
159
|
+
#:
|
|
160
|
+
#: .. versionadded:: 0.50.0
|
|
155
161
|
compute_atr20: bool = False
|
|
156
162
|
|
|
157
163
|
#: Constant factor used to convert global- and year-mean RF, [:math:`W m^{-2}`],
|
|
158
164
|
#: to ATR20, [:math:`K`], given by :cite:`yinPredictingClimateImpact2023`.
|
|
165
|
+
#:
|
|
166
|
+
#: .. versionadded:: 0.50.0
|
|
159
167
|
global_rf_to_atr20_factor: float = 0.0151
|
|
160
168
|
|
|
161
169
|
# ----------------
|
|
@@ -197,12 +205,13 @@ class CocipParams(AdvectionBuffers):
|
|
|
197
205
|
max_depth: float = 1500.0
|
|
198
206
|
|
|
199
207
|
#: Experimental. Improved ice crystal number survival fraction in the wake vortex phase.
|
|
200
|
-
#: Implement :cite:`
|
|
208
|
+
#: Implement :cite:`lottermoserHighResolutionEarlyContrails2025`, who developed a
|
|
201
209
|
#: parametric model that estimates the survival fraction of the contrail ice crystal
|
|
202
210
|
#: number after the wake vortex phase based on the results from large eddy simulations.
|
|
203
211
|
#: This replicates Fig. 4 of :cite:`karcherFormationRadiativeForcing2018`.
|
|
204
212
|
#:
|
|
205
213
|
#: .. versionadded:: 0.50.1
|
|
214
|
+
#: .. versionchanged:: 0.54.7
|
|
206
215
|
unterstrasser_ice_survival_fraction: bool = False
|
|
207
216
|
|
|
208
217
|
#: Experimental. Radiative heating effects on contrail cirrus properties.
|
|
@@ -201,10 +201,9 @@ def iwc_post_wake_vortex(
|
|
|
201
201
|
return np.maximum(iwc - iwc_ad, 0.0)
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
def
|
|
204
|
+
def initial_ice_particle_number(
|
|
205
205
|
nvpm_ei_n: npt.NDArray[np.floating],
|
|
206
206
|
fuel_dist: npt.NDArray[np.floating],
|
|
207
|
-
f_surv: npt.NDArray[np.floating],
|
|
208
207
|
air_temperature: npt.NDArray[np.floating],
|
|
209
208
|
T_crit_sac: npt.NDArray[np.floating],
|
|
210
209
|
min_ice_particle_number_nvpm_ei_n: float,
|
|
@@ -222,8 +221,6 @@ def ice_particle_number(
|
|
|
222
221
|
black carbon number emissions index, [:math:`kg^{-1}`]
|
|
223
222
|
fuel_dist : npt.NDArray[np.floating]
|
|
224
223
|
fuel consumption of the flight segment per distance traveled, [:math:`kg m^{-1}`]
|
|
225
|
-
f_surv : npt.NDArray[np.floating]
|
|
226
|
-
Fraction of contrail ice particle number that survive the wake vortex phase.
|
|
227
224
|
air_temperature : npt.NDArray[np.floating]
|
|
228
225
|
ambient temperature for each waypoint, [:math:`K`]
|
|
229
226
|
T_crit_sac : npt.NDArray[np.floating]
|
|
@@ -235,11 +232,12 @@ def ice_particle_number(
|
|
|
235
232
|
Returns
|
|
236
233
|
-------
|
|
237
234
|
npt.NDArray[np.floating]
|
|
238
|
-
initial number of ice particles per distance
|
|
235
|
+
The initial number of ice particles per distance before the wake vortex
|
|
236
|
+
phase, [:math:`# m^{-1}`]
|
|
239
237
|
"""
|
|
240
238
|
f_activation = ice_particle_activation_rate(air_temperature, T_crit_sac)
|
|
241
239
|
nvpm_ei_n_activated = nvpm_ei_n * f_activation
|
|
242
|
-
return fuel_dist * np.maximum(nvpm_ei_n_activated, min_ice_particle_number_nvpm_ei_n)
|
|
240
|
+
return fuel_dist * np.maximum(nvpm_ei_n_activated, min_ice_particle_number_nvpm_ei_n)
|
|
243
241
|
|
|
244
242
|
|
|
245
243
|
def ice_particle_activation_rate(
|
|
@@ -1188,10 +1188,18 @@ def meteorological_time_slice_statistics(
|
|
|
1188
1188
|
)
|
|
1189
1189
|
|
|
1190
1190
|
# ISSR: Volume of airspace with RHi > 100% between FL300 and FL450
|
|
1191
|
-
|
|
1192
|
-
rhi =
|
|
1193
|
-
|
|
1194
|
-
|
|
1191
|
+
met_cruise = MetDataset(met.data.sel(level=slice(150, 300)))
|
|
1192
|
+
rhi = humidity_scaling.eval(met_cruise)["rhi"].data
|
|
1193
|
+
|
|
1194
|
+
try:
|
|
1195
|
+
# If the given time is already in the dataset, select the time slice
|
|
1196
|
+
i = rhi.get_index("time").get_loc(time)
|
|
1197
|
+
except KeyError:
|
|
1198
|
+
rhi = rhi.interp(time=time)
|
|
1199
|
+
else:
|
|
1200
|
+
rhi = rhi.isel(time=i)
|
|
1201
|
+
|
|
1202
|
+
is_issr = rhi > 1.0
|
|
1195
1203
|
|
|
1196
1204
|
# Cirrus in a longitude-latitude grid
|
|
1197
1205
|
if cirrus_coverage is None:
|
|
@@ -1104,10 +1104,7 @@ def _contrail_optical_depth_above_contrail_layer(
|
|
|
1104
1104
|
|
|
1105
1105
|
da_surface_area = geo.grid_surface_area(da["longitude"].values, da["latitude"].values)
|
|
1106
1106
|
da = da / da_surface_area
|
|
1107
|
-
|
|
1108
|
-
lon_coords = da["longitude"].values
|
|
1109
|
-
wrap_longitude = (lon_coords[-1] + lon_coords[0] + np.diff(lon_coords)[0]) == 0.0
|
|
1110
|
-
mda = MetDataArray(da, wrap_longitude=wrap_longitude, copy=False)
|
|
1107
|
+
mda = MetDataArray(da)
|
|
1111
1108
|
|
|
1112
1109
|
# Interpolate to contrails_level
|
|
1113
1110
|
contrails_level["tau_contrails_above"] = contrails_level.intersect_met(mda)
|
|
@@ -1133,10 +1130,7 @@ def _rsr_and_olr_with_contrail_overlap(
|
|
|
1133
1130
|
Contrail waypoints at the current altitude layer with `rsr_overlap` and
|
|
1134
1131
|
`olr_overlap` attached.
|
|
1135
1132
|
"""
|
|
1136
|
-
|
|
1137
|
-
lon_coords = delta_rad_t["longitude"].values
|
|
1138
|
-
wrap_longitude = (lon_coords[-1] + lon_coords[0] + np.diff(lon_coords)[0]) == 0.0
|
|
1139
|
-
mds = MetDataset(delta_rad_t, wrap_longitude=wrap_longitude, copy=False)
|
|
1133
|
+
mds = MetDataset(delta_rad_t)
|
|
1140
1134
|
|
|
1141
1135
|
# Interpolate radiation fields to obtain `rsr_overlap` and `olr_overlap`
|
|
1142
1136
|
delta_rsr = contrails_level.intersect_met(mds["rsr"])
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
"""Wave-vortex downwash functions from Unterstrasser (
|
|
1
|
+
"""Wave-vortex downwash functions from Lottermoser & Unterstrasser (2025).
|
|
2
2
|
|
|
3
3
|
Notes
|
|
4
4
|
-----
|
|
5
5
|
:cite:`unterstrasserPropertiesYoungContrails2016` provides a parameterized model of the
|
|
6
6
|
survival fraction of the contrail ice crystal number ``f_surv`` during the wake-vortex phase.
|
|
7
|
-
The model
|
|
8
|
-
|
|
7
|
+
The model has since been updated in :cite:`lottermoserHighResolutionEarlyContrails2025`. This update
|
|
8
|
+
improves the goodness-of-fit between the parameterised model and LES, and expands the parameter
|
|
9
|
+
space and can now be used for very low and very high soot inputs, different fuel types (where the
|
|
10
|
+
EI H2Os are different), and higher ambient temperatures (up to 235 K) to accomodate for contrails
|
|
11
|
+
formed by liquid hydrogen aircraft. The model was developed based on output from large eddy
|
|
12
|
+
simulations, and improves agreement with LES outputs relative to the default survival fraction
|
|
13
|
+
parameterization used in CoCiP.
|
|
9
14
|
|
|
10
15
|
For comparison, CoCiP assumes that ``f_surv`` is equal to the change in the contrail ice water
|
|
11
16
|
content (by mass) before and after the wake vortex phase. However, for larger (smaller) ice
|
|
@@ -14,6 +19,9 @@ by mass. This is particularly important in the "soot-poor" scenario, for example
|
|
|
14
19
|
lean-burn engines where their soot emissions can be 3-4 orders of magnitude lower than conventional
|
|
15
20
|
RQL engines.
|
|
16
21
|
|
|
22
|
+
ADD CITATION TO BIBTEX: :cite:`lottermoserHighResolutionEarlyContrails2025`
|
|
23
|
+
Lottermoser, A. and Unterstraßer, S.: High-resolution modelling of early contrail evolution from
|
|
24
|
+
hydrogen-powered aircraft, EGUsphere [preprint], https://doi.org/10.5194/egusphere-2024-3859, 2025.
|
|
17
25
|
"""
|
|
18
26
|
|
|
19
27
|
from __future__ import annotations
|
|
@@ -34,6 +42,8 @@ def ice_particle_number_survival_fraction(
|
|
|
34
42
|
fuel_flow: npt.NDArray[np.floating],
|
|
35
43
|
aei_n: npt.NDArray[np.floating],
|
|
36
44
|
z_desc: npt.NDArray[np.floating],
|
|
45
|
+
*,
|
|
46
|
+
analytical_solution: bool = True,
|
|
37
47
|
) -> npt.NDArray[np.floating]:
|
|
38
48
|
r"""
|
|
39
49
|
Calculate fraction of ice particle number surviving the wake vortex phase and required inputs.
|
|
@@ -61,6 +71,8 @@ def ice_particle_number_survival_fraction(
|
|
|
61
71
|
z_desc : npt.NDArray[np.floating]
|
|
62
72
|
Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
|
|
63
73
|
[:math:`m`].
|
|
74
|
+
analytical_solution : bool
|
|
75
|
+
Use analytical solution to calculate ``z_atm`` and ``z_emit`` instead of numerical solution.
|
|
64
76
|
|
|
65
77
|
Returns
|
|
66
78
|
-------
|
|
@@ -70,57 +82,114 @@ def ice_particle_number_survival_fraction(
|
|
|
70
82
|
References
|
|
71
83
|
----------
|
|
72
84
|
- :cite:`unterstrasserPropertiesYoungContrails2016`
|
|
85
|
+
- :cite:`lottermoserHighResolutionEarlyContrails2025`
|
|
73
86
|
|
|
74
87
|
Notes
|
|
75
88
|
-----
|
|
76
|
-
- See eq. (3), (9), and (10) in :cite:`unterstrasserPropertiesYoungContrails2016`.
|
|
77
89
|
- For consistency in CoCiP, ``z_desc`` should be calculated using :func:`dz_max` instead of
|
|
78
90
|
using :func:`z_desc_length_scale`.
|
|
79
91
|
"""
|
|
80
|
-
# Length scales
|
|
81
|
-
z_atm = z_atm_length_scale(air_temperature, rhi_0)
|
|
82
92
|
rho_emit = emitted_water_vapour_concentration(ei_h2o, wingspan, true_airspeed, fuel_flow)
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
|
|
94
|
+
# Length scales
|
|
95
|
+
if analytical_solution:
|
|
96
|
+
z_atm = z_atm_length_scale_analytical(air_temperature, rhi_0)
|
|
97
|
+
z_emit = z_emit_length_scale_analytical(rho_emit, air_temperature)
|
|
98
|
+
|
|
99
|
+
else:
|
|
100
|
+
z_atm = z_atm_length_scale_numerical(air_temperature, rhi_0)
|
|
101
|
+
z_emit = z_emit_length_scale_numerical(rho_emit, air_temperature)
|
|
102
|
+
|
|
103
|
+
z_total = z_total_length_scale(z_atm, z_emit, z_desc, true_airspeed, fuel_flow, aei_n, wingspan)
|
|
85
104
|
return _survival_fraction_from_length_scale(z_total)
|
|
86
105
|
|
|
87
106
|
|
|
88
107
|
def z_total_length_scale(
|
|
89
|
-
aei_n: npt.NDArray[np.floating],
|
|
90
108
|
z_atm: npt.NDArray[np.floating],
|
|
91
109
|
z_emit: npt.NDArray[np.floating],
|
|
92
110
|
z_desc: npt.NDArray[np.floating],
|
|
111
|
+
true_airspeed: npt.NDArray[np.floating],
|
|
112
|
+
fuel_flow: npt.NDArray[np.floating],
|
|
113
|
+
aei_n: npt.NDArray[np.floating],
|
|
114
|
+
wingspan: npt.NDArray[np.floating] | float,
|
|
93
115
|
) -> npt.NDArray[np.floating]:
|
|
94
116
|
"""
|
|
95
117
|
Calculate the total length-scale effect of the wake vortex downwash.
|
|
96
118
|
|
|
97
119
|
Parameters
|
|
98
120
|
----------
|
|
99
|
-
aei_n : npt.NDArray[np.floating]
|
|
100
|
-
Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
|
|
101
121
|
z_atm : npt.NDArray[np.floating]
|
|
102
122
|
Length-scale effect of ambient supersaturation on the ice crystal mass budget, [:math:`m`]
|
|
103
123
|
z_emit : npt.NDArray[np.floating]
|
|
104
124
|
Length-scale effect of water vapour emissions on the ice crystal mass budget, [:math:`m`]
|
|
105
125
|
z_desc : npt.NDArray[np.floating]
|
|
106
126
|
Final vertical displacement of the wake vortex, `dz_max` in `wake_vortex.py`, [:math:`m`]
|
|
127
|
+
true_airspeed : npt.NDArray[np.floating]
|
|
128
|
+
true airspeed for each waypoint, [:math:`m s^{-1}`]
|
|
129
|
+
fuel_flow : npt.NDArray[np.floating]
|
|
130
|
+
Fuel mass flow rate, [:math:`kg s^{-1}`]
|
|
131
|
+
aei_n : npt.NDArray[np.floating]
|
|
132
|
+
Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
|
|
133
|
+
wingspan : npt.NDArray[np.floating] | float
|
|
134
|
+
aircraft wingspan, [:math:`m`]
|
|
107
135
|
|
|
108
136
|
Returns
|
|
109
137
|
-------
|
|
110
138
|
npt.NDArray[np.floating]
|
|
111
139
|
Total length-scale effect of the wake vortex downwash, [:math:`m`]
|
|
140
|
+
|
|
141
|
+
Notes
|
|
142
|
+
-----
|
|
143
|
+
- For `psi`, see Appendix A1 in :cite:`lottermoserHighResolutionEarlyContrails2025`.
|
|
144
|
+
- For `z_total`, see Eq. (9) and (10) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
|
|
145
|
+
"""
|
|
146
|
+
# Calculate psi term
|
|
147
|
+
fuel_dist = fuel_flow / true_airspeed # Units: [:math:`kg m^{-1}`]
|
|
148
|
+
n_ice_dist = fuel_dist * aei_n # Units: [:math:`m^{-1}`]
|
|
149
|
+
|
|
150
|
+
n_ice_per_vol = n_ice_dist / plume_area(wingspan) # Units: [:math:`m^{-3}`]
|
|
151
|
+
n_ice_per_vol_ref = 3.38e12 / plume_area(60.3)
|
|
152
|
+
|
|
153
|
+
psi = (n_ice_per_vol_ref / n_ice_per_vol) ** 0.16
|
|
154
|
+
|
|
155
|
+
# Calculate total length-scale effect
|
|
156
|
+
return psi * (1.27 * z_atm + 0.42 * z_emit) - 0.49 * z_desc
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def z_atm_length_scale_analytical(
|
|
160
|
+
air_temperature: npt.NDArray[np.floating],
|
|
161
|
+
rhi_0: npt.NDArray[np.floating],
|
|
162
|
+
) -> npt.NDArray[np.floating]:
|
|
163
|
+
"""Calculate the length-scale effect of ambient supersaturation on the ice crystal mass budget.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
air_temperature : npt.NDArray[np.floating]
|
|
168
|
+
Ambient temperature for each waypoint, [:math:`K`].
|
|
169
|
+
rhi_0 : npt.NDArray[np.floating]
|
|
170
|
+
Relative humidity with respect to ice at the flight waypoint.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
npt.NDArray[np.floating]
|
|
175
|
+
The effect of the ambient supersaturation on the ice crystal mass budget,
|
|
176
|
+
provided as a length scale equivalent, estimated with analytical fit [:math:`m`].
|
|
177
|
+
|
|
178
|
+
Notes
|
|
179
|
+
-----
|
|
180
|
+
- See Eq. (A2) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
|
|
112
181
|
"""
|
|
113
|
-
|
|
114
|
-
alpha_atm = 1.7 * alpha_base
|
|
115
|
-
alpha_emit = 1.15 * alpha_base
|
|
182
|
+
z_atm = np.zeros_like(rhi_0)
|
|
116
183
|
|
|
117
|
-
|
|
184
|
+
# Only perform operation when the ambient condition is supersaturated w.r.t. ice
|
|
185
|
+
issr = rhi_0 > 1.0
|
|
118
186
|
|
|
119
|
-
|
|
120
|
-
|
|
187
|
+
s_i = rhi_0 - 1.0
|
|
188
|
+
z_atm[issr] = 607.46 * s_i[issr] ** 0.897 * (air_temperature[issr] / 205.0) ** 2.225
|
|
189
|
+
return z_atm
|
|
121
190
|
|
|
122
191
|
|
|
123
|
-
def
|
|
192
|
+
def z_atm_length_scale_numerical(
|
|
124
193
|
air_temperature: npt.NDArray[np.floating],
|
|
125
194
|
rhi_0: npt.NDArray[np.floating],
|
|
126
195
|
*,
|
|
@@ -141,11 +210,11 @@ def z_atm_length_scale(
|
|
|
141
210
|
-------
|
|
142
211
|
npt.NDArray[np.floating]
|
|
143
212
|
The effect of the ambient supersaturation on the ice crystal mass budget,
|
|
144
|
-
provided as a length scale equivalent, [:math:`m`].
|
|
213
|
+
provided as a length scale equivalent, estimated with numerical methods [:math:`m`].
|
|
145
214
|
|
|
146
215
|
Notes
|
|
147
216
|
-----
|
|
148
|
-
- See
|
|
217
|
+
- See Eq. (6) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
|
|
149
218
|
"""
|
|
150
219
|
# Only perform operation when the ambient condition is supersaturated w.r.t. ice
|
|
151
220
|
issr = rhi_0 > 1.0
|
|
@@ -157,14 +226,14 @@ def z_atm_length_scale(
|
|
|
157
226
|
# Did not use scipy functions because it is unstable when dealing with np.arrays
|
|
158
227
|
z_1 = np.zeros_like(rhi_issr)
|
|
159
228
|
z_2 = np.full_like(rhi_issr, 1000.0)
|
|
160
|
-
lhs = rhi_issr * thermo.e_sat_ice(air_temperature_issr) / air_temperature_issr
|
|
229
|
+
lhs = rhi_issr * thermo.e_sat_ice(air_temperature_issr) / (air_temperature_issr**3.5)
|
|
161
230
|
|
|
162
231
|
dry_adiabatic_lapse_rate = constants.g / constants.c_pd
|
|
163
232
|
for _ in range(n_iter):
|
|
164
233
|
z_est = 0.5 * (z_1 + z_2)
|
|
165
234
|
rhs = (thermo.e_sat_ice(air_temperature_issr + dry_adiabatic_lapse_rate * z_est)) / (
|
|
166
235
|
air_temperature_issr + dry_adiabatic_lapse_rate * z_est
|
|
167
|
-
)
|
|
236
|
+
) ** 3.5
|
|
168
237
|
z_1[lhs > rhs] = z_est[lhs > rhs]
|
|
169
238
|
z_2[lhs < rhs] = z_est[lhs < rhs]
|
|
170
239
|
|
|
@@ -207,7 +276,38 @@ def emitted_water_vapour_concentration(
|
|
|
207
276
|
return h2o_per_dist / area_p
|
|
208
277
|
|
|
209
278
|
|
|
210
|
-
def
|
|
279
|
+
def z_emit_length_scale_analytical(
|
|
280
|
+
rho_emit: npt.NDArray[np.floating],
|
|
281
|
+
air_temperature: npt.NDArray[np.floating],
|
|
282
|
+
) -> npt.NDArray[np.floating]:
|
|
283
|
+
"""Calculate the length-scale effect of water vapour emissions on the ice crystal mass budget.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
rho_emit : npt.NDArray[np.floating] | float
|
|
288
|
+
Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
|
|
289
|
+
air_temperature : npt.NDArray[np.floating]
|
|
290
|
+
ambient temperature for each waypoint, [:math:`K`]
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
npt.NDArray[np.floating]
|
|
295
|
+
The effect of the aircraft water vapour emission on the ice crystal mass budget,
|
|
296
|
+
provided as a length scale equivalent, estimated with analytical fit [:math:`m`]
|
|
297
|
+
|
|
298
|
+
Notes
|
|
299
|
+
-----
|
|
300
|
+
- See Eq. (A3) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
|
|
301
|
+
"""
|
|
302
|
+
t_205 = air_temperature - 205.0
|
|
303
|
+
return (
|
|
304
|
+
1106.6
|
|
305
|
+
* ((rho_emit * 1e5) ** (0.678 + 0.0116 * t_205))
|
|
306
|
+
* np.exp(-(0.0807 + 0.000428 * t_205) * t_205)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def z_emit_length_scale_numerical(
|
|
211
311
|
rho_emit: npt.NDArray[np.floating],
|
|
212
312
|
air_temperature: npt.NDArray[np.floating],
|
|
213
313
|
*,
|
|
@@ -228,24 +328,26 @@ def z_emit_length_scale(
|
|
|
228
328
|
-------
|
|
229
329
|
npt.NDArray[np.floating]
|
|
230
330
|
The effect of the aircraft water vapour emission on the ice crystal mass budget,
|
|
231
|
-
provided as a length scale equivalent, [:math:`m`]
|
|
331
|
+
provided as a length scale equivalent, estimated with numerical methods [:math:`m`]
|
|
232
332
|
|
|
233
333
|
Notes
|
|
234
334
|
-----
|
|
235
|
-
- See
|
|
335
|
+
- See Eq. (7) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
|
|
236
336
|
"""
|
|
237
337
|
# Solve non-linear equation numerically using the bisection method
|
|
238
338
|
# Did not use scipy functions because it is unstable when dealing with np.arrays
|
|
239
339
|
z_1 = np.zeros_like(rho_emit)
|
|
240
340
|
z_2 = np.full_like(rho_emit, 1000.0)
|
|
241
341
|
|
|
242
|
-
lhs = (thermo.e_sat_ice(air_temperature) / (constants.R_v * air_temperature)) +
|
|
342
|
+
lhs = (thermo.e_sat_ice(air_temperature) / (constants.R_v * air_temperature**3.5)) + (
|
|
343
|
+
rho_emit / (air_temperature**2.5)
|
|
344
|
+
)
|
|
243
345
|
|
|
244
346
|
dry_adiabatic_lapse_rate = constants.g / constants.c_pd
|
|
245
347
|
for _ in range(n_iter):
|
|
246
348
|
z_est = 0.5 * (z_1 + z_2)
|
|
247
349
|
rhs = thermo.e_sat_ice(air_temperature + dry_adiabatic_lapse_rate * z_est) / (
|
|
248
|
-
constants.R_v * (air_temperature + dry_adiabatic_lapse_rate * z_est)
|
|
350
|
+
constants.R_v * (air_temperature + dry_adiabatic_lapse_rate * z_est) ** 3.5
|
|
249
351
|
)
|
|
250
352
|
z_1[lhs > rhs] = z_est[lhs > rhs]
|
|
251
353
|
z_2[lhs < rhs] = z_est[lhs < rhs]
|
|
@@ -268,10 +370,10 @@ def plume_area(wingspan: npt.NDArray[np.floating] | float) -> npt.NDArray[np.flo
|
|
|
268
370
|
|
|
269
371
|
Notes
|
|
270
372
|
-----
|
|
271
|
-
- See eq. (A6) and (A7) in :cite:`
|
|
373
|
+
- See Appendix A2 in eq. (A6) and (A7) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
|
|
272
374
|
"""
|
|
273
375
|
r_plume = 1.5 + 0.314 * wingspan
|
|
274
|
-
return 2.0 *
|
|
376
|
+
return 2.0 * np.pi * r_plume**2
|
|
275
377
|
|
|
276
378
|
|
|
277
379
|
def z_desc_length_scale(
|
|
@@ -368,7 +470,7 @@ def _survival_fraction_from_length_scale(
|
|
|
368
470
|
npt.NDArray[np.floating]
|
|
369
471
|
Fraction of ice particle number surviving the wake vortex phase
|
|
370
472
|
"""
|
|
371
|
-
f_surv = 0.
|
|
473
|
+
f_surv = 0.42 + (1.31 / np.pi) * np.arctan(-1.00 + (z_total / 100.0))
|
|
372
474
|
np.clip(f_surv, 0.0, 1.0, out=f_surv)
|
|
373
475
|
return f_surv
|
|
374
476
|
|
|
@@ -88,6 +88,9 @@ class CocipGrid(models.Model):
|
|
|
88
88
|
met_variables = cocip.Cocip.met_variables
|
|
89
89
|
rad_variables = cocip.Cocip.rad_variables
|
|
90
90
|
processed_met_variables = cocip.Cocip.processed_met_variables
|
|
91
|
+
generic_rad_variables = cocip.Cocip.generic_rad_variables
|
|
92
|
+
ecmwf_rad_variables = cocip.Cocip.ecmwf_rad_variables
|
|
93
|
+
gfs_rad_variables = cocip.Cocip.gfs_rad_variables
|
|
91
94
|
|
|
92
95
|
#: Met data is not optional
|
|
93
96
|
met: MetDataset
|
|
@@ -1485,18 +1488,18 @@ def find_initial_persistent_contrails(
|
|
|
1485
1488
|
)
|
|
1486
1489
|
iwc_1 = contrail_properties.iwc_post_wake_vortex(iwc, iwc_ad)
|
|
1487
1490
|
f_surv = contrail_properties.ice_particle_survival_fraction(iwc, iwc_1)
|
|
1488
|
-
|
|
1491
|
+
n_ice_per_m_0 = contrail_properties.initial_ice_particle_number(
|
|
1489
1492
|
nvpm_ei_n=nvpm_ei_n,
|
|
1490
1493
|
fuel_dist=fuel_dist,
|
|
1491
|
-
f_surv=f_surv,
|
|
1492
1494
|
air_temperature=air_temperature,
|
|
1493
1495
|
T_crit_sac=T_crit_sac,
|
|
1494
1496
|
min_ice_particle_number_nvpm_ei_n=params["min_ice_particle_number_nvpm_ei_n"],
|
|
1495
1497
|
)
|
|
1498
|
+
n_ice_per_m_1 = n_ice_per_m_0 * f_surv
|
|
1496
1499
|
|
|
1497
1500
|
# The logic below corresponds to Cocip._create_downwash_contrail (roughly)
|
|
1498
1501
|
contrail["iwc"] = iwc_1
|
|
1499
|
-
contrail["n_ice_per_m"] =
|
|
1502
|
+
contrail["n_ice_per_m"] = n_ice_per_m_1
|
|
1500
1503
|
|
|
1501
1504
|
# Check for persistent initial_contrails
|
|
1502
1505
|
rhi_1 = contrail["rhi"]
|
|
@@ -2149,18 +2152,18 @@ def result_to_metdataset(
|
|
|
2149
2152
|
size = np.prod(shape)
|
|
2150
2153
|
|
|
2151
2154
|
dtype = result["ef"].dtype if result else np.float32
|
|
2152
|
-
|
|
2153
|
-
|
|
2155
|
+
contrail_age_1d = np.zeros(size, dtype=np.float32)
|
|
2156
|
+
ef_per_m_1d = np.zeros(size, dtype=dtype)
|
|
2154
2157
|
|
|
2155
2158
|
if result:
|
|
2156
2159
|
contrail_idx = result["index"]
|
|
2157
2160
|
# Step 1: Contrail age. Convert from timedelta to float
|
|
2158
|
-
|
|
2161
|
+
contrail_age_1d[contrail_idx] = result["age"] / np.timedelta64(1, "h")
|
|
2159
2162
|
# Step 2: EF
|
|
2160
|
-
|
|
2163
|
+
ef_per_m_1d[contrail_idx] = result["ef"] / nominal_segment_length
|
|
2161
2164
|
|
|
2162
|
-
|
|
2163
|
-
|
|
2165
|
+
contrail_age_4d = contrail_age_1d.reshape(shape)
|
|
2166
|
+
ef_per_m_4d = ef_per_m_1d.reshape(shape)
|
|
2164
2167
|
|
|
2165
2168
|
# Step 3: Dataset dims and attrs
|
|
2166
2169
|
dims = tuple(source.coords)
|
|
@@ -2168,8 +2171,8 @@ def result_to_metdataset(
|
|
|
2168
2171
|
|
|
2169
2172
|
# Step 4: Dataset core variables
|
|
2170
2173
|
data_vars = {
|
|
2171
|
-
"contrail_age": (dims,
|
|
2172
|
-
"ef_per_m": (dims,
|
|
2174
|
+
"contrail_age": (dims, contrail_age_4d, local_attrs["contrail_age"]),
|
|
2175
|
+
"ef_per_m": (dims, ef_per_m_4d, local_attrs["ef_per_m"]),
|
|
2173
2176
|
}
|
|
2174
2177
|
|
|
2175
2178
|
# Step 5: Dataset variables from verbose_dicts
|