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.

Files changed (42) hide show
  1. pycontrails/__init__.py +1 -1
  2. pycontrails/_version.py +2 -2
  3. pycontrails/core/aircraft_performance.py +46 -46
  4. pycontrails/core/airports.py +7 -5
  5. pycontrails/core/flight.py +6 -8
  6. pycontrails/core/flightplan.py +11 -11
  7. pycontrails/core/met.py +41 -33
  8. pycontrails/core/met_var.py +80 -0
  9. pycontrails/core/models.py +80 -3
  10. pycontrails/core/rgi_cython.cpython-312-darwin.so +0 -0
  11. pycontrails/core/vector.py +66 -0
  12. pycontrails/datalib/_met_utils/metsource.py +1 -1
  13. pycontrails/datalib/ecmwf/era5.py +5 -6
  14. pycontrails/datalib/ecmwf/era5_model_level.py +4 -5
  15. pycontrails/datalib/ecmwf/ifs.py +1 -3
  16. pycontrails/datalib/gfs/gfs.py +1 -3
  17. pycontrails/datalib/spire/__init__.py +5 -0
  18. pycontrails/datalib/spire/exceptions.py +62 -0
  19. pycontrails/datalib/spire/spire.py +606 -0
  20. pycontrails/models/accf.py +4 -4
  21. pycontrails/models/cocip/cocip.py +116 -19
  22. pycontrails/models/cocip/cocip_params.py +10 -1
  23. pycontrails/models/cocip/output_formats.py +1 -0
  24. pycontrails/models/cocip/unterstrasser_wake_vortex.py +132 -30
  25. pycontrails/models/cocipgrid/cocip_grid.py +3 -0
  26. pycontrails/models/dry_advection.py +51 -19
  27. pycontrails/models/emissions/black_carbon.py +19 -14
  28. pycontrails/models/emissions/emissions.py +8 -8
  29. pycontrails/models/humidity_scaling/humidity_scaling.py +1 -1
  30. pycontrails/models/pcc.py +1 -2
  31. pycontrails/models/ps_model/ps_model.py +3 -31
  32. pycontrails/models/ps_model/ps_operational_limits.py +2 -6
  33. pycontrails/models/tau_cirrus.py +13 -6
  34. pycontrails/physics/constants.py +2 -1
  35. pycontrails/physics/geo.py +3 -3
  36. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/METADATA +5 -6
  37. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/NOTICE +1 -1
  38. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/RECORD +41 -39
  39. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/WHEEL +1 -1
  40. pycontrails/datalib/spire.py +0 -739
  41. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/LICENSE +0 -0
  42. {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 (e.g. ECMWF, GFS).
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
- (ecmwf.SpecificCloudIceWaterContent, gfs.CloudIceWaterMixingRatio),
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
- (ecmwf.TopNetSolarRadiation, gfs.TOAUpwardShortwaveRadiation),
224
- (ecmwf.TopNetThermalRadiation, gfs.TOAUpwardLongwaveRadiation),
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
- (ecmwf.CloudAreaFractionInLayer, gfs.TotalCloudCoverIsobaric),
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
- else:
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 ``"toa_upward_shortwave_flux"`` or
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 = f"'rad' data must contain either '{gfs_key}' or '{ecmwf_key}' (ECMWF) variable."
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 "top_net_solar_radiation" which is SDR - RSR
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 ``"toa_upward_longwave_flux"``
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 None
2054
+ return
1967
2055
 
1968
- # GFS contains OLR (toa_upward_longwave_flux) variable directly
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 = f"'rad' data must contain either '{gfs_key}' or '{ecmwf_key}' (ECMWF) variable."
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:`unterstrasserPropertiesYoungContrails2016`, who developed a
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.
@@ -2259,3 +2259,4 @@ def compare_cocip_with_goes(
2259
2259
  plt.close()
2260
2260
 
2261
2261
  return output_path
2262
+ return None
@@ -1,11 +1,16 @@
1
- """Wave-vortex downwash functions from Unterstrasser (2016).
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 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.
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
- 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)
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
- alpha_base = (aei_n / 2.8e14) ** (-0.18)
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
- z_total = alpha_atm * z_atm + alpha_emit * z_emit - 0.6 * z_desc
184
+ # Only perform operation when the ambient condition is supersaturated w.r.t. ice
185
+ issr = rhi_0 > 1.0
118
186
 
119
- z_total.clip(min=0.0, out=z_total)
120
- return z_total
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 z_atm_length_scale(
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 eq. (5) in :cite:`unterstrasserPropertiesYoungContrails2016`.
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 z_emit_length_scale(
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 eq. (7) in :cite:`unterstrasserPropertiesYoungContrails2016`.
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)) + rho_emit
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:`unterstrasserPropertiesYoungContrails2016`.
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 * 2.0 * np.pi * r_plume**2
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.45 + (1.19 / np.pi) * np.arctan(-1.35 + (z_total / 100.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