pycontrails 0.54.5__cp312-cp312-macosx_11_0_arm64.whl → 0.54.7__cp312-cp312-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/__init__.py +1 -1
- pycontrails/_version.py +2 -2
- pycontrails/core/aircraft_performance.py +46 -46
- pycontrails/core/airports.py +7 -5
- pycontrails/core/flight.py +6 -8
- pycontrails/core/flightplan.py +11 -11
- pycontrails/core/met.py +41 -33
- pycontrails/core/met_var.py +80 -0
- pycontrails/core/models.py +80 -3
- pycontrails/core/rgi_cython.cpython-312-darwin.so +0 -0
- pycontrails/core/vector.py +66 -0
- pycontrails/datalib/_met_utils/metsource.py +1 -1
- pycontrails/datalib/ecmwf/era5.py +5 -6
- pycontrails/datalib/ecmwf/era5_model_level.py +4 -5
- pycontrails/datalib/ecmwf/ifs.py +1 -3
- pycontrails/datalib/gfs/gfs.py +1 -3
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +606 -0
- pycontrails/models/accf.py +4 -4
- pycontrails/models/cocip/cocip.py +116 -19
- pycontrails/models/cocip/cocip_params.py +10 -1
- pycontrails/models/cocip/output_formats.py +1 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +132 -30
- pycontrails/models/cocipgrid/cocip_grid.py +3 -0
- pycontrails/models/dry_advection.py +51 -19
- pycontrails/models/emissions/black_carbon.py +19 -14
- pycontrails/models/emissions/emissions.py +8 -8
- pycontrails/models/humidity_scaling/humidity_scaling.py +1 -1
- pycontrails/models/pcc.py +1 -2
- pycontrails/models/ps_model/ps_model.py +3 -31
- pycontrails/models/ps_model/ps_operational_limits.py +2 -6
- pycontrails/models/tau_cirrus.py +13 -6
- pycontrails/physics/constants.py +2 -1
- pycontrails/physics/geo.py +3 -3
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/METADATA +5 -6
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/NOTICE +1 -1
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/RECORD +41 -39
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/WHEEL +1 -1
- pycontrails/datalib/spire.py +0 -739
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.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
|
|
@@ -68,12 +69,26 @@ class Cocip(Model):
|
|
|
68
69
|
-----
|
|
69
70
|
**Inputs**
|
|
70
71
|
|
|
71
|
-
The required meteorology variables depend on the data source
|
|
72
|
+
The required meteorology variables depend on the data source. :class:`Cocip`
|
|
73
|
+
supports data-source-specific variables from ECMWF models (HRES, ERA5) and the NCEP GFS, plus
|
|
74
|
+
a generic set of model-agnostic variables.
|
|
72
75
|
|
|
73
76
|
See :attr:`met_variables` and :attr:`rad_variables` for the list of required variables
|
|
74
77
|
to the ``met`` and ``rad`` parameters, respectively.
|
|
75
78
|
When an item in one of these arrays is a :class:`tuple`, variable keys depend on data source.
|
|
76
79
|
|
|
80
|
+
A warning will be raised if meteorology data is from a source not currently supported by
|
|
81
|
+
a pycontrails datalib. In this case it is the responsibility of the user to ensure that
|
|
82
|
+
meteorology data is formatted correctly. The warning can be suppressed with a context manager:
|
|
83
|
+
|
|
84
|
+
.. code-block:: python
|
|
85
|
+
:emphasize-lines: 2,3
|
|
86
|
+
|
|
87
|
+
import warnings
|
|
88
|
+
with warnings.catch_warnings():
|
|
89
|
+
warnings.simplefilter("ignore", category=UserWarning, message="Unknown provider")
|
|
90
|
+
cocip = Cocip(met, rad, ...)
|
|
91
|
+
|
|
77
92
|
The current list of required variables (labelled by ``"standard_name"``):
|
|
78
93
|
|
|
79
94
|
.. list-table:: Variable keys for pressure level data
|
|
@@ -82,24 +97,31 @@ class Cocip(Model):
|
|
|
82
97
|
* - Parameter
|
|
83
98
|
- ECMWF
|
|
84
99
|
- GFS
|
|
100
|
+
- Generic
|
|
85
101
|
* - Air Temperature
|
|
86
102
|
- ``air_temperature``
|
|
87
103
|
- ``air_temperature``
|
|
104
|
+
- ``air_temperature``
|
|
88
105
|
* - Specific Humidity
|
|
89
106
|
- ``specific_humidity``
|
|
90
107
|
- ``specific_humidity``
|
|
108
|
+
- ``specific_humidity``
|
|
91
109
|
* - Eastward wind
|
|
92
110
|
- ``eastward_wind``
|
|
93
111
|
- ``eastward_wind``
|
|
112
|
+
- ``eastward_wind``
|
|
94
113
|
* - Northward wind
|
|
95
114
|
- ``northward_wind``
|
|
96
115
|
- ``northward_wind``
|
|
116
|
+
- ``northward_wind``
|
|
97
117
|
* - Vertical velocity
|
|
98
118
|
- ``lagrangian_tendency_of_air_pressure``
|
|
99
119
|
- ``lagrangian_tendency_of_air_pressure``
|
|
120
|
+
- ``lagrangian_tendency_of_air_pressure``
|
|
100
121
|
* - Ice water content
|
|
101
122
|
- ``specific_cloud_ice_water_content``
|
|
102
123
|
- ``ice_water_mixing_ratio``
|
|
124
|
+
- ``mass_fraction_of_cloud_ice_in_air``
|
|
103
125
|
|
|
104
126
|
.. list-table:: Variable keys for single-level radiation data
|
|
105
127
|
:header-rows: 1
|
|
@@ -107,12 +129,15 @@ class Cocip(Model):
|
|
|
107
129
|
* - Parameter
|
|
108
130
|
- ECMWF
|
|
109
131
|
- GFS
|
|
132
|
+
- Generic
|
|
110
133
|
* - Top solar radiation
|
|
111
134
|
- ``top_net_solar_radiation``
|
|
112
135
|
- ``toa_upward_shortwave_flux``
|
|
136
|
+
- ``toa_net_downward_shortwave_flux``
|
|
113
137
|
* - Top thermal radiation
|
|
114
138
|
- ``top_net_thermal_radiation``
|
|
115
139
|
- ``toa_upward_longwave_flux``
|
|
140
|
+
- ``toa_outgoing_longwave_flux``
|
|
116
141
|
|
|
117
142
|
**Modifications**
|
|
118
143
|
|
|
@@ -214,14 +239,26 @@ class Cocip(Model):
|
|
|
214
239
|
met_var.EastwardWind,
|
|
215
240
|
met_var.NorthwardWind,
|
|
216
241
|
met_var.VerticalVelocity,
|
|
217
|
-
(
|
|
242
|
+
(
|
|
243
|
+
met_var.MassFractionOfCloudIceInAir,
|
|
244
|
+
ecmwf.SpecificCloudIceWaterContent,
|
|
245
|
+
gfs.CloudIceWaterMixingRatio,
|
|
246
|
+
),
|
|
218
247
|
)
|
|
219
248
|
|
|
220
249
|
#: Required single-level top of atmosphere radiation variables.
|
|
221
250
|
#: Variable keys depend on data source (e.g. ECMWF, GFS).
|
|
222
251
|
rad_variables = (
|
|
223
|
-
(
|
|
224
|
-
|
|
252
|
+
(
|
|
253
|
+
met_var.TOANetDownwardShortwaveFlux,
|
|
254
|
+
ecmwf.TopNetSolarRadiation,
|
|
255
|
+
gfs.TOAUpwardShortwaveRadiation,
|
|
256
|
+
),
|
|
257
|
+
(
|
|
258
|
+
met_var.TOAOutgoingLongwaveFlux,
|
|
259
|
+
ecmwf.TopNetThermalRadiation,
|
|
260
|
+
gfs.TOAUpwardLongwaveRadiation,
|
|
261
|
+
),
|
|
225
262
|
)
|
|
226
263
|
|
|
227
264
|
#: Minimal set of met variables needed to run the model after pre-processing.
|
|
@@ -242,7 +279,11 @@ class Cocip(Model):
|
|
|
242
279
|
#: Moved Geopotential from :attr:`met_variables` to :attr:`optional_met_variables`
|
|
243
280
|
optional_met_variables = (
|
|
244
281
|
(met_var.Geopotential, met_var.GeopotentialHeight),
|
|
245
|
-
(
|
|
282
|
+
(
|
|
283
|
+
met_var.CloudAreaFractionInAtmosphereLayer,
|
|
284
|
+
ecmwf.CloudAreaFractionInLayer,
|
|
285
|
+
gfs.TotalCloudCoverIsobaric,
|
|
286
|
+
),
|
|
246
287
|
)
|
|
247
288
|
|
|
248
289
|
#: Met data is not optional
|
|
@@ -442,6 +483,42 @@ class Cocip(Model):
|
|
|
442
483
|
|
|
443
484
|
return self.source
|
|
444
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
|
+
|
|
445
522
|
def _set_timesteps(self) -> None:
|
|
446
523
|
"""Set the :attr:`timesteps` based on the ``source`` time range.
|
|
447
524
|
|
|
@@ -575,10 +652,12 @@ class Cocip(Model):
|
|
|
575
652
|
if verbose_outputs:
|
|
576
653
|
interpolate_met(met, self.source, "tau_cirrus", **interp_kwargs)
|
|
577
654
|
|
|
578
|
-
# handle ECMWF/GFS ciwc variables
|
|
655
|
+
# handle ECMWF/GFS/generic ciwc variables
|
|
579
656
|
if (key := "specific_cloud_ice_water_content") in met: # noqa: SIM114
|
|
580
657
|
interpolate_met(met, self.source, key, **interp_kwargs)
|
|
581
|
-
elif (key := "ice_water_mixing_ratio") in met:
|
|
658
|
+
elif (key := "ice_water_mixing_ratio") in met: # noqa: SIM114
|
|
659
|
+
interpolate_met(met, self.source, key, **interp_kwargs)
|
|
660
|
+
elif (key := "mass_fraction_of_cloud_ice_in_air") in met:
|
|
582
661
|
interpolate_met(met, self.source, key, **interp_kwargs)
|
|
583
662
|
|
|
584
663
|
self.source["rho_air"] = thermo.rho_d(
|
|
@@ -1587,8 +1666,7 @@ def _process_rad(rad: MetDataset) -> MetDataset:
|
|
|
1587
1666
|
rad.data["time"].attrs["shift_radiation_time"] = "variable"
|
|
1588
1667
|
return rad
|
|
1589
1668
|
|
|
1590
|
-
|
|
1591
|
-
shift_radiation_time = -np.timedelta64(30, "m")
|
|
1669
|
+
shift_radiation_time = -np.timedelta64(30, "m")
|
|
1592
1670
|
|
|
1593
1671
|
elif dataset == "ERA5" and product == "ensemble":
|
|
1594
1672
|
shift_radiation_time = -np.timedelta64(90, "m")
|
|
@@ -1893,8 +1971,8 @@ def calc_shortwave_radiation(
|
|
|
1893
1971
|
Raises
|
|
1894
1972
|
------
|
|
1895
1973
|
ValueError
|
|
1896
|
-
If ``rad`` does not contain ``"
|
|
1897
|
-
``"top_net_solar_radiation"`` variable.
|
|
1974
|
+
If ``rad`` does not contain ``"toa_net_downward_shortwave_flux"``,
|
|
1975
|
+
``"toa_upward_shortwave_flux"`` or ``"top_net_solar_radiation"`` variable.
|
|
1898
1976
|
|
|
1899
1977
|
Notes
|
|
1900
1978
|
-----
|
|
@@ -1918,6 +1996,13 @@ def calc_shortwave_radiation(
|
|
|
1918
1996
|
sdr = geo.solar_direct_radiation(longitude, latitude, time, threshold_cos_sza=0.01)
|
|
1919
1997
|
vector["sdr"] = sdr
|
|
1920
1998
|
|
|
1999
|
+
# Generic contains net downward shortwave flux at TOA (SDR - RSR) in W/m2
|
|
2000
|
+
generic_key = "toa_net_downward_shortwave_flux"
|
|
2001
|
+
if generic_key in rad:
|
|
2002
|
+
tnsr = interpolate_met(rad, vector, generic_key, **interp_kwargs)
|
|
2003
|
+
vector["rsr"] = np.maximum(sdr - tnsr, 0.0)
|
|
2004
|
+
return
|
|
2005
|
+
|
|
1921
2006
|
# GFS contains RSR (toa_upward_shortwave_flux) variable directly
|
|
1922
2007
|
gfs_key = "toa_upward_shortwave_flux"
|
|
1923
2008
|
if gfs_key in rad:
|
|
@@ -1926,10 +2011,13 @@ def calc_shortwave_radiation(
|
|
|
1926
2011
|
|
|
1927
2012
|
ecmwf_key = "top_net_solar_radiation"
|
|
1928
2013
|
if ecmwf_key not in rad:
|
|
1929
|
-
msg =
|
|
2014
|
+
msg = (
|
|
2015
|
+
f"'rad' data must contain either '{generic_key}' (generic), "
|
|
2016
|
+
f"'{gfs_key}' (GFS), or '{ecmwf_key}' (ECMWF) variable."
|
|
2017
|
+
)
|
|
1930
2018
|
raise ValueError(msg)
|
|
1931
2019
|
|
|
1932
|
-
# ECMWF contains
|
|
2020
|
+
# ECMWF also contains net downward shortwave flux at TOA, but possibly as an accumulation
|
|
1933
2021
|
tnsr = interpolate_met(rad, vector, ecmwf_key, **interp_kwargs)
|
|
1934
2022
|
tnsr = _rad_accumulation_to_average_instantaneous(rad, ecmwf_key, tnsr)
|
|
1935
2023
|
vector.update({ecmwf_key: tnsr})
|
|
@@ -1958,14 +2046,20 @@ def calc_outgoing_longwave_radiation(
|
|
|
1958
2046
|
Raises
|
|
1959
2047
|
------
|
|
1960
2048
|
ValueError
|
|
1961
|
-
If ``rad`` does not contain a ``"
|
|
1962
|
-
or ``"top_net_thermal_radiation"`` variable.
|
|
2049
|
+
If ``rad`` does not contain a ``"toa_outgoing_longwave_flux"``,
|
|
2050
|
+
``"toa_upward_longwave_flux"`` or ``"top_net_thermal_radiation"`` variable.
|
|
1963
2051
|
"""
|
|
1964
2052
|
|
|
1965
2053
|
if "olr" in vector:
|
|
1966
|
-
return
|
|
2054
|
+
return
|
|
1967
2055
|
|
|
1968
|
-
#
|
|
2056
|
+
# Generic contains OLR (toa_outgoing_longwave_flux) directly
|
|
2057
|
+
generic_key = "toa_outgoing_longwave_flux"
|
|
2058
|
+
if generic_key in rad:
|
|
2059
|
+
interpolate_met(rad, vector, generic_key, "olr", **interp_kwargs)
|
|
2060
|
+
return
|
|
2061
|
+
|
|
2062
|
+
# GFS contains OLR (toa_upward_longwave_flux) directly
|
|
1969
2063
|
gfs_key = "toa_upward_longwave_flux"
|
|
1970
2064
|
if gfs_key in rad:
|
|
1971
2065
|
interpolate_met(rad, vector, gfs_key, "olr", **interp_kwargs)
|
|
@@ -1974,7 +2068,10 @@ def calc_outgoing_longwave_radiation(
|
|
|
1974
2068
|
# ECMWF contains "top_net_thermal_radiation" which is -1 * OLR
|
|
1975
2069
|
ecmwf_key = "top_net_thermal_radiation"
|
|
1976
2070
|
if ecmwf_key not in rad:
|
|
1977
|
-
msg =
|
|
2071
|
+
msg = (
|
|
2072
|
+
f"'rad' data must contain either '{generic_key}' (generic), "
|
|
2073
|
+
f"'{gfs_key}' (GFS), or '{ecmwf_key}' (ECMWF) variable."
|
|
2074
|
+
)
|
|
1978
2075
|
raise ValueError(msg)
|
|
1979
2076
|
|
|
1980
2077
|
tntr = interpolate_met(rad, vector, ecmwf_key, **interp_kwargs)
|
|
@@ -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.
|
|
@@ -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
|