pycontrails 0.58.0__cp314-cp314-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 (122) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2931 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,1260 @@
1
+ """
2
+ Module for calculating radiative forcing of contrail cirrus.
3
+
4
+ References
5
+ ----------
6
+ - :cite:`schumannEffectiveRadiusIce2011`
7
+ - :cite:`schumannParametricRadiativeForcing2012`
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import dataclasses
13
+ import itertools
14
+
15
+ import numpy as np
16
+ import numpy.typing as npt
17
+ import xarray as xr
18
+
19
+ from pycontrails.core.met import MetDataArray, MetDataset
20
+ from pycontrails.core.vector import GeoVectorDataset
21
+ from pycontrails.physics import geo
22
+
23
+
24
+ @dataclasses.dataclass(frozen=True)
25
+ class RFConstants:
26
+ """
27
+ Constants that are used to calculate the local contrail radiative forcing.
28
+
29
+ See Table 1 of :cite:`schumannParametricRadiativeForcing2012`.
30
+
31
+ Each coefficient has 8 elements, one corresponding to each contrail ice particle habit (shape)::
32
+
33
+ [
34
+ Sphere,
35
+ Solid column,
36
+ Hollow column,
37
+ Rough aggregate,
38
+ Rosette-6,
39
+ Plate,
40
+ Droxtal,
41
+ Myhre,
42
+ ]
43
+
44
+ For each waypoint, the distinct mix of ice particle habits are approximated using the mean
45
+ contrail ice particle radius (``r_vol_um``) relative to ``radius_threshold_um``.
46
+
47
+ For example:
48
+
49
+ - if ``r_vol_um`` for a waypoint < 5 um, the mix of ice particle habits will be 100% droxtals.
50
+ - if ``r_vol_um`` for a waypoint between 5 and 9.5 um, the mix of ice particle habits will
51
+ be 30% solid columns, 70% droxtals.
52
+
53
+ See Table 2 from :cite:`schumannEffectiveRadiusIce2011`.
54
+
55
+
56
+ References
57
+ ----------
58
+ - :cite:`schumannEffectiveRadiusIce2011`
59
+ - :cite:`schumannParametricRadiativeForcing2012`
60
+ """
61
+
62
+ # -----
63
+ # Variables/coefficients used to calculate the local contrail longwave radiative forcing.
64
+ # -----
65
+
66
+ #: Linear approximation of Stefan-Boltzmann Law
67
+ #: :math:`k_t` in Eq. (2) in :cite:`schumannParametricRadiativeForcing2012`
68
+ k_t = np.array([1.93466, 1.95456, 1.95994, 1.95906, 1.94397, 1.95123, 2.30363, 1.94611])
69
+
70
+ #: Approximates the temperature of the atmosphere without contrails
71
+ #: :math:`T_{0}` in Eq. (2) in :cite:`schumannParametricRadiativeForcing2012`
72
+ T_0 = np.array([152.237, 152.724, 152.923, 152.360, 151.879, 152.318, 165.692, 153.073])
73
+
74
+ #: Approximate the effective emmissivity factor
75
+ #: :math:`\delta_{\tau} in :cite:`schumannParametricRadiativeForcing2012`
76
+ delta_t = np.array(
77
+ [0.940846, 0.808397, 0.736222, 0.675591, 0.748757, 0.708515, 0.927592, 0.795527]
78
+ )
79
+
80
+ #: Effective radius scaling factor for optical properties (extinction relative to scattering)
81
+ #: :math:`\delta_{lr} in Eq. (3) in :cite:`schumannParametricRadiativeForcing2012`
82
+ delta_lr = np.array([0.211276, 0.341194, 0.325496, 0.255921, 0.170265, 1.65441, 0.201949, 0])
83
+
84
+ #: Optical depth scaling factor for reduction of the OLR at the contrail level due
85
+ #: to existing cirrus above the contrail
86
+ #: :math:`\delta_{lc} in Eq. (4) in :cite:`schumannParametricRadiativeForcing2012`
87
+ delta_lc = np.array(
88
+ [0.159942, 0.0958129, 0.0924850, 0.0462023, 0.132925, 0.0870067, 0.0626339, 0.0665289]
89
+ )
90
+
91
+ # -----
92
+ # Variables/coefficients used to calculate the local contrail shortwave radiative forcing.
93
+ # -----
94
+
95
+ #: Approximates the dependence on the effective albedo
96
+ #: :math:`t_a`: Eq. (5) in :cite:`schumannParametricRadiativeForcing2012`
97
+ t_a = np.array([0.879119, 0.901701, 0.881812, 0.899144, 0.879896, 0.883212, 0.899096, 1.00744])
98
+
99
+ # Approximates the albedo of the contrail
100
+ #: :math:`A_{\mu}` in Eq. (6) in :cite:`schumannParametricRadiativeForcing2012`
101
+ A_mu = np.array(
102
+ [0.361226, 0.294072, 0.343894, 0.317866, 0.337227, 0.310978, 0.342593, 0.269179]
103
+ )
104
+
105
+ # Approximates the albedo of the contrail
106
+ #: :math:`C_{\mu}` in Eq. (6) in :cite:`schumannParametricRadiativeForcing2012`
107
+ C_mu = np.array(
108
+ [0.709300, 0.678016, 0.687546, 0.675315, 0.712041, 0.713317, 0.660267, 0.545716]
109
+ )
110
+
111
+ #: Approximates the effective contrail optical depth
112
+ #: :math:`delta_sr` in Eq. (7) and (8) in :cite:`schumannParametricRadiativeForcing2012`
113
+ delta_sr = np.array(
114
+ [0.149851, 0.0254270, 0.0238836, 0.0463724, 0.0478892, 0.0700234, 0.0517942, 0]
115
+ )
116
+
117
+ #: Approximates the effective contrail optical depth
118
+ #: :math:`F_r` in Eq. (7) and (8) in :cite:`schumannParametricRadiativeForcing2012`
119
+ F_r = np.array([0.511852, 0.576911, 0.597351, 0.225750, 0.550734, 0.817858, 0.249004, 0])
120
+
121
+ #: Approximates the contrail reflectances
122
+ #: :math:`\gamma` in Eq. (9) in :cite:`schumannParametricRadiativeForcing2012`
123
+ gamma_lower = np.array(
124
+ [0.323166, 0.392598, 0.356189, 0.345040, 0.407515, 0.523604, 0.310853, 0.274741]
125
+ )
126
+
127
+ #: Approximates the contrail reflectances
128
+ #: :math:`\Gamma` in Eq. (9) in :cite:`schumannParametricRadiativeForcing2012`
129
+ gamma_upper = np.array(
130
+ [0.241507, 0.347023, 0.288452, 0.296813, 0.327857, 0.437560, 0.274710, 0.208154]
131
+ )
132
+
133
+ #: Approximate the SZA-dependent contrail sideward scattering
134
+ #: :math:`B_{\mu}` in Eq. (10) in :cite:`schumannParametricRadiativeForcing2012`
135
+ B_mu = np.array([1.67592, 1.55687, 1.71065, 1.55843, 1.70782, 1.71789, 1.56399, 1.59015])
136
+
137
+ #: Account for the optical depth of natural cirrus above the contrail
138
+ #: :math:`\delta_{sc}` in Eq. (11) in :cite:`schumannParametricRadiativeForcing2012`
139
+ delta_sc = np.array(
140
+ [0.157017, 0.143274, 0.167995, 0.148547, 0.173036, 0.162442, 0.171855, 0.213488]
141
+ )
142
+
143
+ #: Account for the optical depth of natural cirrus above the contrail
144
+ # :math:`\delta'_{sc}` in Eq. (11) in :cite:`schumannParametricRadiativeForcing2012`
145
+ delta_sc_aps = np.array(
146
+ [0.229574, 0.197611, 0.245036, 0.204875, 0.248328, 0.254029, 0.244051, 0.302246]
147
+ )
148
+
149
+
150
+ # create a new constants class to use within module
151
+ RF_CONST = RFConstants()
152
+
153
+
154
+ # ----------
155
+ # Ice Habits
156
+ # ----------
157
+
158
+
159
+ def habit_weights(
160
+ r_vol_um: npt.NDArray[np.floating],
161
+ habit_distributions: npt.NDArray[np.floating],
162
+ radius_threshold_um: npt.NDArray[np.floating],
163
+ ) -> npt.NDArray[np.floating]:
164
+ r"""Assign weights to different ice particle habits for each waypoint.
165
+
166
+ For each waypoint, the distinct mix of ice particle habits are approximated
167
+ using the mean contrail ice particle radius (``r_vol_um``) binned by ``radius_threshold_um``.
168
+
169
+ For example:
170
+
171
+ - For waypoints with r_vol_um < 5 um, the mix of ice particle habits will
172
+ be from Group 1 (100% Droxtals, refer to :attr:`CocipParams().habit_distributions`).
173
+ - For waypoints with 5 um <= ``r_vol_um`` < 9.5 um, the mix of ice particle
174
+ habits will be from Group 2 (30% solid columns, 70% droxtals)
175
+
176
+ Parameters
177
+ ----------
178
+ r_vol_um : npt.NDArray[np.floating]
179
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
180
+ habit_distributions : npt.NDArray[np.floating]
181
+ Habit weight distributions.
182
+ See :attr:`CocipParams().habit_distributions`
183
+ radius_threshold_um : npt.NDArray[np.floating]
184
+ Radius thresholds for habit distributions.
185
+ See :attr:`CocipParams.radius_threshold_um`
186
+
187
+ Returns
188
+ -------
189
+ npt.NDArray[np.floating]
190
+ Array with shape ``n_waypoints x 8 columns``, where each column is the weights to the ice
191
+ particle habits, [:math:`[0 - 1]`], and the sum of each column should be equal to 1.
192
+
193
+ Raises
194
+ ------
195
+ ValueError
196
+ Raises when ``habit_distributions`` do not sum to 1 across columns or
197
+ if there is a size mismatch with ``radius_threshold_um``.
198
+ """
199
+ # all rows of the habit weights should sum to 1
200
+ if not np.allclose(np.sum(habit_distributions, axis=1), 1.0, atol=1e-3):
201
+ raise ValueError("Habit weight distributions must sum to 1 across columns")
202
+
203
+ if habit_distributions.shape[0] != (radius_threshold_um.size + 1):
204
+ raise ValueError(
205
+ "The number of rows in `habit_distributions` must equal 1 + the "
206
+ "size of `radius_threshold_um`"
207
+ )
208
+
209
+ # assign ice particle habits for each waypoint
210
+ idx = habit_weight_regime_idx(r_vol_um, radius_threshold_um)
211
+ return habit_distributions[idx]
212
+
213
+
214
+ def habit_weight_regime_idx(
215
+ r_vol_um: npt.NDArray[np.floating], radius_threshold_um: npt.NDArray[np.floating]
216
+ ) -> npt.NDArray[np.intp]:
217
+ r"""
218
+ Determine regime of ice particle habits based on contrail ice particle volume mean radius.
219
+
220
+ Parameters
221
+ ----------
222
+ r_vol_um : npt.NDArray[np.floating]
223
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
224
+ radius_threshold_um : npt.NDArray[np.floating]
225
+ Radius thresholds for habit distributions.
226
+ See :attr:`CocipParams.radius_threshold_um`
227
+
228
+ Returns
229
+ -------
230
+ npt.NDArray[np.intp]
231
+ Row index of the habit distribution in array :attr:`CocipParams().habit_distributions`
232
+ """
233
+ # find the regime for each waypoint using thresholds
234
+ idx = np.digitize(r_vol_um, radius_threshold_um)
235
+
236
+ # set any nan values to the "0" type
237
+ idx[np.isnan(r_vol_um)] = 0
238
+
239
+ return idx
240
+
241
+
242
+ def effective_radius_by_habit(
243
+ r_vol_um: npt.NDArray[np.floating], habit_idx: npt.NDArray[np.intp]
244
+ ) -> npt.NDArray[np.floating]:
245
+ r"""Calculate the effective radius ``r_eff_um`` via the mean ice particle radius and habit type.
246
+
247
+ The ``habit_idx`` corresponds to the habit types in ``rf_const.habits``.
248
+ Each habit type has a specific parameterization to calculate ``r_eff_um`` based on ``r_vol_um``.
249
+ derived from :cite:`schumannEffectiveRadiusIce2011`.
250
+
251
+ Parameters
252
+ ----------
253
+ r_vol_um : npt.NDArray[np.floating]
254
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
255
+ habit_idx : npt.NDArray[np.intp]
256
+ Habit type index for the contrail ice particle, corresponding to the
257
+ habits in ``rf_const.habits``.
258
+
259
+ Returns
260
+ -------
261
+ npt.NDArray[np.floating]
262
+ Effective radius of ice particles for each combination of ``r_vol_um``
263
+ and ``habit_idx``, [:math:`\mu m`]
264
+
265
+ References
266
+ ----------
267
+ - :cite:`schumannEffectiveRadiusIce2011`
268
+ """
269
+ cond_list = [
270
+ habit_idx == 0,
271
+ habit_idx == 1,
272
+ habit_idx == 2,
273
+ habit_idx == 3,
274
+ habit_idx == 4,
275
+ habit_idx == 5,
276
+ habit_idx == 6,
277
+ habit_idx == 7,
278
+ ]
279
+ func_list = [
280
+ effective_radius_sphere,
281
+ effective_radius_solid_column,
282
+ effective_radius_hollow_column,
283
+ effective_radius_rough_aggregate,
284
+ effective_radius_rosette,
285
+ effective_radius_plate,
286
+ effective_radius_droxtal,
287
+ effective_radius_myhre,
288
+ 0.0,
289
+ ]
290
+ return np.piecewise(r_vol_um, cond_list, func_list)
291
+
292
+
293
+ def effective_radius_sphere(r_vol_um: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
294
+ r"""
295
+ Calculate the effective radius of contrail ice particles assuming a sphere particle habit.
296
+
297
+ Parameters
298
+ ----------
299
+ r_vol_um : npt.NDArray[np.floating]
300
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
301
+
302
+ Returns
303
+ -------
304
+ npt.NDArray[np.floating]
305
+ Effective radius, [:math:`\mu m`]
306
+ """
307
+ return np.minimum(r_vol_um, 25.0)
308
+
309
+
310
+ def effective_radius_solid_column(r_vol_um: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
311
+ r"""
312
+ Calculate the effective radius of contrail ice particles assuming a solid column particle habit.
313
+
314
+ Parameters
315
+ ----------
316
+ r_vol_um : npt.NDArray[np.floating]
317
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
318
+
319
+ Returns
320
+ -------
321
+ npt.NDArray[np.floating]
322
+ Effective radius, [:math:`\mu m`]
323
+ """
324
+ r_eff_um = (
325
+ 0.2588 * np.exp(-(6.912e-3 * r_vol_um)) + 0.6372 * np.exp(-(3.142e-4 * r_vol_um))
326
+ ) * r_vol_um
327
+ is_small = r_vol_um <= 42.2
328
+ r_eff_um[is_small] = 0.824 * r_vol_um[is_small]
329
+ return np.minimum(r_eff_um, 45.0)
330
+
331
+
332
+ def effective_radius_hollow_column(r_vol_um: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
333
+ r"""Calculate the effective radius of ice particles assuming a hollow column particle habit.
334
+
335
+ Parameters
336
+ ----------
337
+ r_vol_um : npt.NDArray[np.floating]
338
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
339
+
340
+ Returns
341
+ -------
342
+ npt.NDArray[np.floating]
343
+ Effective radius, [:math:`\mu m`]
344
+ """
345
+ r_eff_um = (
346
+ 0.2281 * np.exp(-(7.359e-3 * r_vol_um)) + 0.5651 * np.exp(-(3.350e-4 * r_vol_um))
347
+ ) * r_vol_um
348
+ is_small = r_vol_um <= 39.7
349
+ r_eff_um[is_small] = 0.729 * r_vol_um[is_small]
350
+ return np.minimum(r_eff_um, 45.0)
351
+
352
+
353
+ def effective_radius_rough_aggregate(
354
+ r_vol_um: npt.NDArray[np.floating],
355
+ ) -> npt.NDArray[np.floating]:
356
+ r"""Calculate the effective radius of ice particles assuming a rough aggregate particle habit.
357
+
358
+ Parameters
359
+ ----------
360
+ r_vol_um : npt.NDArray[np.floating]
361
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
362
+
363
+ Returns
364
+ -------
365
+ npt.NDArray[np.floating]
366
+ Effective radius, [:math:`\mu m`]
367
+ """
368
+ r_eff_um = 0.574 * r_vol_um
369
+ return np.minimum(r_eff_um, 45.0)
370
+
371
+
372
+ def effective_radius_rosette(r_vol_um: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
373
+ r"""
374
+ Calculate the effective radius of contrail ice particles assuming a rosette particle habit.
375
+
376
+ Parameters
377
+ ----------
378
+ r_vol_um : npt.NDArray[np.floating]
379
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
380
+
381
+ Returns
382
+ -------
383
+ npt.NDArray[np.floating]
384
+ Effective radius, [:math:`\mu m`]
385
+ """
386
+ r_eff_um = r_vol_um * (
387
+ 0.1770 * np.exp(-(2.144e-2 * r_vol_um)) + 0.4267 * np.exp(-(3.562e-4 * r_vol_um))
388
+ )
389
+ return np.minimum(r_eff_um, 45.0)
390
+
391
+
392
+ def effective_radius_plate(r_vol_um: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
393
+ r"""
394
+ Calculate the effective radius of contrail ice particles assuming a plate particle habit.
395
+
396
+ Parameters
397
+ ----------
398
+ r_vol_um : npt.NDArray[np.floating]
399
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
400
+
401
+ Returns
402
+ -------
403
+ npt.NDArray[np.floating]
404
+ Effective radius, [:math:`\mu m`]
405
+ """
406
+ r_eff_um = r_vol_um * (
407
+ 0.1663 + 0.3713 * np.exp(-(0.0336 * r_vol_um)) + 0.3309 * np.exp(-(0.0035 * r_vol_um))
408
+ )
409
+ return np.minimum(r_eff_um, 45.0)
410
+
411
+
412
+ def effective_radius_droxtal(r_vol_um: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
413
+ r"""
414
+ Calculate the effective radius of contrail ice particles assuming a droxtal particle habit.
415
+
416
+ Parameters
417
+ ----------
418
+ r_vol_um : npt.NDArray[np.floating]
419
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
420
+
421
+ Returns
422
+ -------
423
+ npt.NDArray[np.floating]
424
+ Effective radius, [:math:`\mu m`]
425
+ """
426
+ r_eff_um = 0.94 * r_vol_um
427
+ return np.minimum(r_eff_um, 45.0)
428
+
429
+
430
+ def effective_radius_myhre(r_vol_um: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
431
+ r"""
432
+ Calculate the effective radius of contrail ice particles assuming a sphere particle habit.
433
+
434
+ Parameters
435
+ ----------
436
+ r_vol_um : npt.NDArray[np.floating]
437
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
438
+
439
+ Returns
440
+ -------
441
+ npt.NDArray[np.floating]
442
+ Effective radius, [:math:`\mu m`]
443
+ """
444
+ return np.minimum(r_vol_um, 45.0)
445
+
446
+
447
+ # -----------------
448
+ # Radiative Forcing
449
+ # -----------------
450
+
451
+
452
+ def longwave_radiative_forcing(
453
+ r_vol_um: npt.NDArray[np.floating],
454
+ olr: npt.NDArray[np.floating],
455
+ air_temperature: npt.NDArray[np.floating],
456
+ tau_contrail: npt.NDArray[np.floating],
457
+ tau_cirrus: npt.NDArray[np.floating],
458
+ habit_weights_: npt.NDArray[np.floating],
459
+ r_eff_um: npt.NDArray[np.floating] | None = None,
460
+ ) -> npt.NDArray[np.floating]:
461
+ r"""
462
+ Calculate the local contrail longwave radiative forcing (:math:`RF_{LW}`).
463
+
464
+ All returned values are positive.
465
+
466
+ Parameters
467
+ ----------
468
+ r_vol_um : npt.NDArray[np.floating]
469
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
470
+ olr : npt.NDArray[np.floating]
471
+ Outgoing longwave radiation at each waypoint, [:math:`W m^{-2}`]
472
+ air_temperature : npt.NDArray[np.floating]
473
+ Ambient temperature at each waypoint, [:math:`K`]
474
+ tau_contrail : npt.NDArray[np.floating]
475
+ Contrail optical depth at each waypoint
476
+ tau_cirrus : npt.NDArray[np.floating]
477
+ Optical depth of numerical weather prediction (NWP) cirrus above the
478
+ contrail at each waypoint
479
+ habit_weights_ : npt.NDArray[np.floating]
480
+ Weights to different ice particle habits for each waypoint,
481
+ ``n_waypoints x 8`` (habit) columns, [:math:`[0 - 1]`]
482
+ r_eff_um : npt.NDArray[np.floating] | None, optional
483
+ Provide effective radius corresponding to elements in ``r_vol_um``, [:math:`\mu m`].
484
+ Defaults to None, which means the effective radius will be calculated using ``r_vol_um``
485
+ and habit types in :func:`effective_radius_by_habit`.
486
+
487
+ Returns
488
+ -------
489
+ npt.NDArray[np.floating]
490
+ Local contrail longwave radiative forcing (positive), [:math:`W m^{-2}`]
491
+
492
+ Raises
493
+ ------
494
+ ValueError
495
+ If `r_eff_um` and `olr` have different shapes.
496
+
497
+ References
498
+ ----------
499
+ - :cite:`schumannParametricRadiativeForcing2012`
500
+ """
501
+ # get list of habit weight indexs where the weights > 0
502
+ # this is a tuple of (np.array[waypoint index], np.array[habit type index])
503
+ habit_weight_mask = habit_weights_ > 0.0
504
+ idx0, idx1 = np.nonzero(habit_weight_mask)
505
+
506
+ # Convert parametric coefficients for vectorized operations
507
+ delta_t = RF_CONST.delta_t[idx1]
508
+ delta_lc = RF_CONST.delta_lc[idx1]
509
+ delta_lr = RF_CONST.delta_lr[idx1]
510
+ k_t = RF_CONST.k_t[idx1]
511
+ T_0 = RF_CONST.T_0[idx1]
512
+
513
+ olr_h = olr[idx0]
514
+ tau_cirrus_h = tau_cirrus[idx0]
515
+ tau_contrail_h = tau_contrail[idx0]
516
+ air_temperature_h = air_temperature[idx0]
517
+
518
+ # effective radius
519
+ if r_eff_um is None:
520
+ r_vol_um_h = r_vol_um[idx0]
521
+ r_eff_um_h = effective_radius_by_habit(r_vol_um_h, idx1)
522
+ else:
523
+ if r_eff_um.shape != olr.shape:
524
+ raise ValueError(
525
+ "User provided effective radius (`r_eff_um`) must have the same shape as `olr`"
526
+ f" {olr.shape}"
527
+ )
528
+
529
+ r_eff_um_h = r_eff_um[idx0]
530
+
531
+ # Longwave radiation calculations
532
+ e_lw = olr_reduction_natural_cirrus(tau_cirrus_h, delta_lc)
533
+ f_lw = contrail_effective_emissivity(r_eff_um_h, delta_lr)
534
+
535
+ # calculate the RF LW per habit type
536
+ # see eqn (2) in :cite:`schumannParametricRadiativeForcing2012`
537
+ rf_lw_per_habit = (
538
+ (olr_h - k_t * (air_temperature_h - T_0))
539
+ * e_lw
540
+ * (1.0 - np.exp(-delta_t * f_lw * tau_contrail_h))
541
+ )
542
+ rf_lw_per_habit.clip(min=0.0, out=rf_lw_per_habit)
543
+
544
+ # Weight and sum the RF contributions of each habit type according the habit weight
545
+ # regime at the waypoint
546
+ # see eqn (12) in :cite:`schumannParametricRadiativeForcing2012`
547
+ # use fancy indexing to re-assign values to 2d array of waypoint x habit type
548
+ rf_lw_weighted = np.zeros_like(habit_weights_)
549
+ rf_lw_weighted[idx0, idx1] = rf_lw_per_habit * habit_weights_[habit_weight_mask]
550
+ return np.sum(rf_lw_weighted, axis=1)
551
+
552
+
553
+ def shortwave_radiative_forcing(
554
+ r_vol_um: npt.NDArray[np.floating],
555
+ sdr: npt.NDArray[np.floating],
556
+ rsr: npt.NDArray[np.floating],
557
+ sd0: npt.NDArray[np.floating],
558
+ tau_contrail: npt.NDArray[np.floating],
559
+ tau_cirrus: npt.NDArray[np.floating],
560
+ habit_weights_: npt.NDArray[np.floating],
561
+ r_eff_um: npt.NDArray[np.floating] | None = None,
562
+ ) -> npt.NDArray[np.floating]:
563
+ r"""
564
+ Calculate the local contrail shortwave radiative forcing (:math:`RF_{SW}`).
565
+
566
+ All returned values are negative.
567
+
568
+ Parameters
569
+ ----------
570
+ r_vol_um : npt.NDArray[np.floating]
571
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
572
+ sdr : npt.NDArray[np.floating]
573
+ Solar direct radiation, [:math:`W m^{-2}`]
574
+ rsr : npt.NDArray[np.floating]
575
+ Reflected solar radiation, [:math:`W m^{-2}`]
576
+ sd0 : npt.NDArray[np.floating]
577
+ Solar constant, [:math:`W m^{-2}`]
578
+ tau_contrail : npt.NDArray[np.floating]
579
+ Contrail optical depth for each waypoint
580
+ tau_cirrus : npt.NDArray[np.floating]
581
+ Optical depth of numerical weather prediction (NWP) cirrus above the
582
+ contrail for each waypoint.
583
+ habit_weights_ : npt.NDArray[np.floating]
584
+ Weights to different ice particle habits for each waypoint,
585
+ ``n_waypoints x 8`` (habit) columns, [:math:`[0 - 1]`]
586
+ r_eff_um : npt.NDArray[np.floating] | None, optional
587
+ Provide effective radius corresponding to elements in ``r_vol_um``, [:math:`\mu m`].
588
+ Defaults to None, which means the effective radius will be calculated using ``r_vol_um``
589
+ and habit types in :func:`effective_radius_by_habit`.
590
+
591
+ Returns
592
+ -------
593
+ npt.NDArray[np.floating]
594
+ Local contrail shortwave radiative forcing (negative), [:math:`W m^{-2}`]
595
+
596
+ Raises
597
+ ------
598
+ ValueError
599
+ If `r_eff_um` and `sdr` have different shapes.
600
+
601
+ References
602
+ ----------
603
+ - :cite:`schumannParametricRadiativeForcing2012`
604
+ """
605
+ # create mask for daytime (sdr > 0)
606
+ day = sdr > 0.0
607
+
608
+ # short circuit if no waypoints occur during the day
609
+ if not day.any():
610
+ return np.zeros_like(sdr)
611
+
612
+ # get list of habit weight indexs where the weights > 0
613
+ # this is a tuple of (np.array[waypoint index], np.array[habit type index])
614
+ habit_weight_mask = day.reshape(day.size, 1) & (habit_weights_ > 0.0)
615
+ idx0, idx1 = np.nonzero(habit_weight_mask)
616
+
617
+ # Convert parametric coefficients for vectorized operations
618
+ t_a = RF_CONST.t_a[idx1]
619
+ A_mu = RF_CONST.A_mu[idx1]
620
+ B_mu = RF_CONST.B_mu[idx1]
621
+ C_mu = RF_CONST.C_mu[idx1]
622
+ delta_sr = RF_CONST.delta_sr[idx1]
623
+ F_r = RF_CONST.F_r[idx1]
624
+ gamma_lower = RF_CONST.gamma_lower[idx1]
625
+ gamma_upper = RF_CONST.gamma_upper[idx1]
626
+ delta_sc = RF_CONST.delta_sc[idx1]
627
+ delta_sc_aps = RF_CONST.delta_sc_aps[idx1]
628
+
629
+ sdr_h = sdr[idx0]
630
+ rsr_h = rsr[idx0]
631
+ sd0_h = sd0[idx0]
632
+ tau_contrail_h = tau_contrail[idx0]
633
+ tau_cirrus_h = tau_cirrus[idx0]
634
+
635
+ albedo_ = albedo(sdr_h, rsr_h)
636
+ mue = np.minimum(sdr_h / sd0_h, 1.0)
637
+
638
+ # effective radius
639
+ if r_eff_um is None:
640
+ r_vol_um_h = r_vol_um[idx0]
641
+ r_eff_um_h = effective_radius_by_habit(r_vol_um_h, idx1)
642
+ else:
643
+ if r_eff_um.shape != sdr.shape:
644
+ raise ValueError(
645
+ "User provided effective radius (`r_eff_um`) must have the same shape as `sdr`"
646
+ f" {sdr.shape}"
647
+ )
648
+
649
+ r_eff_um_h = r_eff_um[idx0]
650
+
651
+ # Local contrail shortwave radiative forcing calculations
652
+ alpha_c = contrail_albedo(
653
+ tau_contrail_h,
654
+ mue,
655
+ r_eff_um_h,
656
+ A_mu,
657
+ B_mu,
658
+ C_mu,
659
+ delta_sr,
660
+ F_r,
661
+ gamma_lower,
662
+ gamma_upper,
663
+ )
664
+
665
+ e_sw = effective_tau_cirrus(tau_cirrus_h, mue, delta_sc, delta_sc_aps)
666
+
667
+ # calculate the RF SW per habit type
668
+ # see eqn (5) in :cite:`schumannParametricRadiativeForcing2012`
669
+ rf_sw_per_habit = np.minimum(-sdr_h * ((t_a - albedo_) ** 2) * alpha_c * e_sw, 0.0)
670
+
671
+ # Weight and sum the RF contributions of each habit type according the
672
+ # habit weight regime at the waypoint
673
+ # see eqn (12) in :cite:`schumannParametricRadiativeForcing2012`
674
+ # use fancy indexing to re-assign values to 2d array of waypoint x habit type
675
+ rf_sw_weighted = np.zeros_like(habit_weights_)
676
+ rf_sw_weighted[idx0, idx1] = rf_sw_per_habit * habit_weights_[habit_weight_mask]
677
+
678
+ return np.sum(rf_sw_weighted, axis=1)
679
+
680
+
681
+ def net_radiative_forcing(
682
+ rf_lw: npt.NDArray[np.floating], rf_sw: npt.NDArray[np.floating]
683
+ ) -> npt.NDArray[np.floating]:
684
+ """
685
+ Calculate the local contrail net radiative forcing (rf_net).
686
+
687
+ RF Net = Longwave RF (positive) + Shortwave RF (negative)
688
+
689
+ Parameters
690
+ ----------
691
+ rf_lw : npt.NDArray[np.floating]
692
+ local contrail longwave radiative forcing, [:math:`W m^{-2}`]
693
+ rf_sw : npt.NDArray[np.floating]
694
+ local contrail shortwave radiative forcing, [:math:`W m^{-2}`]
695
+
696
+ Returns
697
+ -------
698
+ npt.NDArray[np.floating]
699
+ local contrail net radiative forcing, [:math:`W m^{-2}`]
700
+ """
701
+ return rf_lw + rf_sw
702
+
703
+
704
+ def olr_reduction_natural_cirrus(
705
+ tau_cirrus: npt.NDArray[np.floating], delta_lc: npt.NDArray[np.floating]
706
+ ) -> npt.NDArray[np.floating]:
707
+ """
708
+ Calculate reduction in outgoing longwave radiation (OLR) due to the presence of natural cirrus.
709
+
710
+ Natural cirrus has optical depth ``tau_cirrus`` above the contrail.
711
+ See ``e_lw`` in Eq. (4) of Schumann et al. (2012).
712
+
713
+ Parameters
714
+ ----------
715
+ tau_cirrus : npt.NDArray[np.floating]
716
+ Optical depth of numerical weather prediction (NWP) cirrus above the
717
+ contrail for each waypoint.
718
+ delta_lc : npt.NDArray[np.floating]
719
+ Habit specific parameter to approximate the reduction of the outgoing
720
+ longwave radiation at the contrail level due to natural cirrus above the contrail.
721
+
722
+ Returns
723
+ -------
724
+ npt.NDArray[np.floating]
725
+ Reduction of outgoing longwave radiation
726
+ """
727
+ # e_lw calculations
728
+ return np.exp(-delta_lc * tau_cirrus)
729
+
730
+
731
+ def contrail_effective_emissivity(
732
+ r_eff_um: npt.NDArray[np.floating], delta_lr: npt.NDArray[np.floating]
733
+ ) -> npt.NDArray[np.floating]:
734
+ r"""Calculate the effective emissivity of the contrail, ``f_lw``.
735
+
736
+ Refer to Eq. (3) of Schumann et al. (2012).
737
+
738
+ Parameters
739
+ ----------
740
+ r_eff_um : npt.NDArray[np.floating]
741
+ Effective radius for each waypoint, n_waypoints x 8 (habit) columns, [:math:`\mu m`]
742
+ See :func:`effective_radius_habit`.
743
+ delta_lr : npt.NDArray[np.floating]
744
+ Habit specific parameter to approximate the effective emissivity of the contrail.
745
+
746
+ Returns
747
+ -------
748
+ npt.NDArray[np.floating]
749
+ Effective emissivity of the contrail
750
+ """
751
+ # f_lw calculations
752
+ return 1.0 - np.exp(-delta_lr * r_eff_um)
753
+
754
+
755
+ def albedo(
756
+ sdr: npt.NDArray[np.floating], rsr: npt.NDArray[np.floating]
757
+ ) -> npt.NDArray[np.floating]:
758
+ """
759
+ Calculate albedo along contrail waypoint.
760
+
761
+ Albedo, the diffuse reflection of solar radiation out of the total solar radiation,
762
+ is computed based on the solar direct radiation (`sdr`) and reflected solar radiation (`rsr`).
763
+
764
+ Output values range between 0 (corresponding to a black body that absorbs
765
+ all incident radiation) and 1 (a body that reflects all incident radiation).
766
+
767
+ Parameters
768
+ ----------
769
+ sdr : npt.NDArray[np.floating]
770
+ Solar direct radiation, [:math:`W m^{-2}`]
771
+ rsr : npt.NDArray[np.floating]
772
+ Reflected solar radiation, [:math:`W m^{-2}`]
773
+
774
+ Returns
775
+ -------
776
+ npt.NDArray[np.floating]
777
+ Albedo value, [:math:`[0 - 1]`]
778
+ """
779
+ day = sdr > 0.0
780
+ albedo_ = np.zeros(sdr.shape)
781
+ albedo_[day] = rsr[day] / sdr[day]
782
+ albedo_.clip(0.0, 1.0, out=albedo_)
783
+ return albedo_
784
+
785
+
786
+ def contrail_albedo(
787
+ tau_contrail: npt.NDArray[np.floating],
788
+ mue: npt.NDArray[np.floating],
789
+ r_eff_um: npt.NDArray[np.floating],
790
+ A_mu: npt.NDArray[np.floating],
791
+ B_mu: npt.NDArray[np.floating],
792
+ C_mu: npt.NDArray[np.floating],
793
+ delta_sr: npt.NDArray[np.floating],
794
+ F_r: npt.NDArray[np.floating],
795
+ gamma_lower: npt.NDArray[np.floating],
796
+ gamma_upper: npt.NDArray[np.floating],
797
+ ) -> npt.NDArray[np.floating]:
798
+ r"""
799
+ Calculate the contrail albedo, ``alpha_c``.
800
+
801
+ Refer to Eq. (6) of Schumann et al. (2012),
802
+
803
+ Parameters
804
+ ----------
805
+ tau_contrail : npt.NDArray[np.floating]
806
+ Contrail optical depth for each waypoint
807
+ mue : npt.NDArray[np.floating]
808
+ Cosine of the solar zenith angle (theta), mue = cos(theta) = sdr/sd0
809
+ r_eff_um : npt.NDArray[np.floating]
810
+ Effective radius for each waypoint, n_waypoints x 8 (habit) columns, [:math:`\mu m`]
811
+ See :func:`effective_radius_habit`.
812
+ A_mu : npt.NDArray[np.floating]
813
+ Habit-specific parameter to approximate the albedo of the contrail
814
+ B_mu : npt.NDArray[np.floating]
815
+ Habit-specific parameter to approximate the SZA-dependent contrail sideward scattering
816
+ C_mu : npt.NDArray[np.floating]
817
+ Habit-specific parameter to approximate the albedo of the contrail
818
+ delta_sr : npt.NDArray[np.floating]
819
+ Habit-specific parameter to approximate the effective contrail optical depth
820
+ F_r : npt.NDArray[np.floating]
821
+ Habit-specific parameter to approximate the effective contrail optical depth
822
+ gamma_lower : npt.NDArray[np.floating]
823
+ Habit-specific parameter to approximate the contrail reflectances
824
+ gamma_upper : npt.NDArray[np.floating]
825
+ Habit-specific parameter to approximate the contrail reflectances
826
+
827
+ Returns
828
+ -------
829
+ npt.NDArray[np.floating]
830
+ Contrail albedo for each waypoint and ice particle habit
831
+ """
832
+ tau_aps = tau_contrail * (1.0 - F_r * (1 - np.exp(-delta_sr * r_eff_um)))
833
+ tau_eff = tau_aps / (mue + 1e-6)
834
+ r_c = 1.0 - np.exp(-gamma_upper * tau_eff)
835
+ r_c_aps = np.exp(-gamma_lower * tau_eff)
836
+
837
+ f_mu = (2.0 * (1.0 - mue)) ** B_mu - 1.0
838
+ return r_c * (C_mu + (A_mu * r_c_aps * f_mu))
839
+
840
+
841
+ def effective_tau_cirrus(
842
+ tau_cirrus: npt.NDArray[np.floating],
843
+ mue: npt.NDArray[np.floating],
844
+ delta_sc: npt.NDArray[np.floating],
845
+ delta_sc_aps: npt.NDArray[np.floating],
846
+ ) -> npt.NDArray[np.floating]:
847
+ r"""
848
+ Calculate the effective optical depth of natural cirrus above the contrail, ``e_sw``.
849
+
850
+ Refer to Eq. (11) of :cite:`schumannParametricRadiativeForcing2012`. See Notes for
851
+ a correction to the equation.
852
+
853
+ Parameters
854
+ ----------
855
+ tau_cirrus : npt.NDArray[np.floating]
856
+ Optical depth of numerical weather prediction (NWP) cirrus above the
857
+ contrail for each waypoint.
858
+ mue : npt.NDArray[np.floating]
859
+ Cosine of the solar zenith angle (theta), mue = cos(theta) = sdr/sd0
860
+ delta_sc : npt.NDArray[np.floating]
861
+ Habit-specific parameter to account for the optical depth of natural
862
+ cirrus above the contrail
863
+ delta_sc_aps : npt.NDArray[np.floating]
864
+ Habit-specific parameter to account for the optical depth of natural
865
+ cirrus above the contrail
866
+
867
+ Returns
868
+ -------
869
+ npt.NDArray[np.floating]
870
+ Effective optical depth of natural cirrus above the contrail,
871
+ ``n_waypoints x 8`` (habit) columns.
872
+
873
+ Notes
874
+ -----
875
+ - In a personal correspondence, Dr. Schumann identified a print error in Eq. (11) in
876
+ :cite:`schumannParametricRadiativeForcing2012`, where the positions of ``delta_sc_aps``
877
+ and ``delta_sc`` should be swapped. The correct function is provided below.
878
+ """
879
+ tau_cirrus_eff = tau_cirrus / (mue + 1e-6)
880
+ return np.exp(tau_cirrus * delta_sc_aps - tau_cirrus_eff * delta_sc)
881
+
882
+
883
+ # -----------------------------
884
+ # Contrail-contrail overlapping
885
+ # -----------------------------
886
+
887
+
888
+ def contrail_contrail_overlap_radiative_effects(
889
+ contrails: GeoVectorDataset,
890
+ habit_distributions: npt.NDArray[np.floating],
891
+ radius_threshold_um: npt.NDArray[np.floating],
892
+ *,
893
+ min_altitude_m: float = 6000.0,
894
+ max_altitude_m: float = 13000.0,
895
+ dz_overlap_m: float = 500.0,
896
+ spatial_grid_res: float = 0.25,
897
+ ) -> GeoVectorDataset:
898
+ r"""
899
+ Calculate radiative properties after accounting for contrail overlapping.
900
+
901
+ This function mutates the ``contrails`` parameter.
902
+
903
+ Parameters
904
+ ----------
905
+ contrails : GeoVectorDataset
906
+ Contrail waypoints at a given time. Must include the following variables:
907
+ - segment_length
908
+ - width
909
+ - r_ice_vol
910
+ - tau_contrail
911
+ - tau_cirrus
912
+ - air_temperature
913
+ - sdr
914
+ - rsr
915
+ - olr
916
+
917
+ habit_distributions : npt.NDArray[np.floating]
918
+ Habit weight distributions.
919
+ See :attr:`CocipParams.habit_distributions`
920
+ radius_threshold_um : npt.NDArray[np.floating]
921
+ Radius thresholds for habit distributions.
922
+ See :attr:`CocipParams.radius_threshold_um`
923
+ min_altitude_m : float
924
+ Minimum altitude domain in simulation, [:math:`m`]
925
+ See :attr:`CocipParams.min_altitude_m`
926
+ max_altitude_m : float
927
+ Maximum altitude domain in simulation, [:math:`m`]
928
+ See :attr:`CocipParams.min_altitude_m`
929
+ dz_overlap_m : float
930
+ Altitude interval used to segment contrail waypoints, [:math:`m`]
931
+ See :attr:`CocipParams.dz_overlap_m`
932
+ spatial_grid_res : float
933
+ Spatial grid resolution, [:math:`\deg`]
934
+
935
+ Returns
936
+ -------
937
+ GeoVectorDataset
938
+ Contrail waypoints at a given time with additional variables attached, including
939
+ - rsr_overlap
940
+ - olr_overlap
941
+ - tau_cirrus_overlap
942
+ - rf_sw_overlap
943
+ - rf_lw_overlap
944
+ - rf_net_overlap
945
+
946
+ References
947
+ ----------
948
+ - Schumann et al. (2021) Air traffic and contrail changes over Europe during COVID-19:
949
+ A model study, Atmos. Chem. Phys., 21, 7429-7450, https://doi.org/10.5194/ACP-21-7429-2021.
950
+ - Teoh et al. (2023) Global aviation contrail climate effects from 2019 to 2021.
951
+
952
+ Notes
953
+ -----
954
+ - The radiative effects of contrail-contrail overlapping is approximated by changing the
955
+ background RSR and OLR fields, and the overlying cirrus optical depth above the contrail.
956
+ - All contrail segments within each altitude interval are treated as one contrail layer, where
957
+ they do not overlap. Contrail layers are processed starting from the bottom to the top.
958
+ - Refer to the Supporting Information (S4.3) of Teoh et al. (2023)
959
+ """
960
+ assert "segment_length" in contrails
961
+ assert "width" in contrails
962
+ assert "r_ice_vol" in contrails
963
+ assert "tau_contrail" in contrails
964
+ assert "tau_cirrus" in contrails
965
+ assert "air_temperature" in contrails
966
+ assert "sdr" in contrails
967
+ assert "rsr" in contrails
968
+ assert "olr" in contrails
969
+
970
+ if not contrails:
971
+ raise ValueError("Parameter 'contrails' must be non-empty.")
972
+
973
+ time = contrails["time"]
974
+ time0 = time[0]
975
+ if not np.all(time == time0):
976
+ raise ValueError("Contrail waypoints must have a constant time.")
977
+
978
+ longitude = contrails["longitude"]
979
+ latitude = contrails["latitude"]
980
+ altitude = contrails.altitude
981
+
982
+ spatial_bbox = geo.spatial_bounding_box(longitude, latitude)
983
+ west, south, east, north = spatial_bbox
984
+
985
+ assert spatial_grid_res > 0.01
986
+ lon_coords = np.arange(west, east + 0.01, spatial_grid_res)
987
+ lat_coords = np.arange(south, north + 0.01, spatial_grid_res)
988
+
989
+ dims = ["longitude", "latitude", "level", "time"]
990
+ shape = (len(lon_coords), len(lat_coords), 1, 1)
991
+ delta_rad_t = xr.Dataset(
992
+ data_vars={"rsr": (dims, np.zeros(shape)), "olr": (dims, np.zeros(shape))},
993
+ coords={"longitude": lon_coords, "latitude": lat_coords, "level": [-1.0], "time": [time0]},
994
+ )
995
+
996
+ # Initialise radiation fields to store change in background RSR and OLR due to contrails
997
+ rsr_overlap = np.zeros_like(longitude)
998
+ olr_overlap = np.zeros_like(longitude)
999
+ tau_cirrus_overlap = np.zeros_like(longitude)
1000
+ rf_sw_overlap = np.zeros_like(longitude)
1001
+ rf_lw_overlap = np.zeros_like(longitude)
1002
+ rf_net_overlap = np.zeros_like(longitude)
1003
+
1004
+ # Account for contrail overlapping starting from bottom to top layers
1005
+ altitude_layers = np.arange(min_altitude_m, max_altitude_m + 1.0, dz_overlap_m)
1006
+
1007
+ for alt_layer0, alt_layer1 in itertools.pairwise(altitude_layers):
1008
+ is_in_layer = (altitude >= alt_layer0) & (altitude < alt_layer1)
1009
+
1010
+ # Get contrail waypoints at current altitude layer
1011
+ contrails_level = contrails.filter(is_in_layer, copy=True)
1012
+
1013
+ # Skip altitude layer if no contrails are present
1014
+ if not contrails_level:
1015
+ continue
1016
+
1017
+ # Get contrails above altitude layer
1018
+ is_above_layer = (altitude >= alt_layer1) & (altitude <= max_altitude_m)
1019
+ contrails_above = contrails.filter(is_above_layer, copy=True)
1020
+
1021
+ contrails_level = _contrail_optical_depth_above_contrail_layer(
1022
+ contrails_level,
1023
+ contrails_above,
1024
+ spatial_bbox=spatial_bbox,
1025
+ spatial_grid_res=spatial_grid_res,
1026
+ )
1027
+
1028
+ # Calculate updated RSR and OLR with contrail overlapping
1029
+ contrails_level = _rsr_and_olr_with_contrail_overlap(contrails_level, delta_rad_t)
1030
+
1031
+ # Calculate local contrail SW and LW RF with contrail overlapping
1032
+ contrails_level = _local_sw_and_lw_rf_with_contrail_overlap(
1033
+ contrails_level, habit_distributions, radius_threshold_um
1034
+ )
1035
+
1036
+ # Cumulative change in background RSR and OLR fields
1037
+ delta_rad_t = _change_in_background_rsr_and_olr(
1038
+ contrails_level,
1039
+ delta_rad_t,
1040
+ spatial_bbox=spatial_bbox,
1041
+ spatial_grid_res=spatial_grid_res,
1042
+ )
1043
+
1044
+ # Save values
1045
+ rsr_overlap[is_in_layer] = contrails_level["rsr_overlap"]
1046
+ olr_overlap[is_in_layer] = contrails_level["olr_overlap"]
1047
+ tau_cirrus_overlap[is_in_layer] = (
1048
+ contrails_level["tau_cirrus"] + contrails_level["tau_contrails_above"]
1049
+ )
1050
+ rf_sw_overlap[is_in_layer] = contrails_level["rf_sw_overlap"]
1051
+ rf_lw_overlap[is_in_layer] = contrails_level["rf_lw_overlap"]
1052
+ rf_net_overlap[is_in_layer] = contrails_level["rf_net_overlap"]
1053
+
1054
+ # Add new variables to contrails
1055
+ contrails["rsr_overlap"] = rsr_overlap
1056
+ contrails["olr_overlap"] = olr_overlap
1057
+ contrails["tau_cirrus_overlap"] = tau_cirrus_overlap
1058
+ contrails["rf_sw_overlap"] = rf_sw_overlap
1059
+ contrails["rf_lw_overlap"] = rf_lw_overlap
1060
+ contrails["rf_net_overlap"] = rf_net_overlap
1061
+
1062
+ return contrails
1063
+
1064
+
1065
+ def _contrail_optical_depth_above_contrail_layer(
1066
+ contrails_level: GeoVectorDataset,
1067
+ contrails_above: GeoVectorDataset,
1068
+ spatial_bbox: tuple[float, float, float, float],
1069
+ spatial_grid_res: float,
1070
+ ) -> GeoVectorDataset:
1071
+ r"""
1072
+ Calculate the contrail optical depth above the contrail waypoints.
1073
+
1074
+ Parameters
1075
+ ----------
1076
+ contrails_level : GeoVectorDataset
1077
+ Contrail waypoints at the current altitude layer.
1078
+ contrails_above : GeoVectorDataset
1079
+ Contrail waypoints above the current altitude layer.
1080
+ spatial_bbox: tuple[float, float, float, float]
1081
+ Spatial bounding box, ``(lon_min, lat_min, lon_max, lat_max)``, [:math:`\deg`]
1082
+ spatial_grid_res : float
1083
+ Spatial grid resolution, [:math:`\deg`]
1084
+
1085
+ Returns
1086
+ -------
1087
+ GeoVectorDataset
1088
+ Contrail waypoints at the current altitude layer with `tau_contrails_above` attached.
1089
+ """
1090
+ contrails_above["tau_contrails_above"] = (
1091
+ contrails_above["tau_contrail"]
1092
+ * contrails_above["segment_length"]
1093
+ * contrails_above["width"]
1094
+ )
1095
+
1096
+ # Aggregate contrail optical depth to a longitude-latitude grid
1097
+ da = contrails_above.to_lon_lat_grid(
1098
+ agg={"tau_contrails_above": "sum"},
1099
+ spatial_bbox=spatial_bbox,
1100
+ spatial_grid_res=spatial_grid_res,
1101
+ )["tau_contrails_above"]
1102
+ da = da.expand_dims(level=[-1.0], time=[contrails_level["time"][0]])
1103
+ da = da.transpose("longitude", "latitude", "level", "time")
1104
+
1105
+ da_surface_area = geo.grid_surface_area(da["longitude"].values, da["latitude"].values)
1106
+ da = da / da_surface_area
1107
+ mda = MetDataArray(da)
1108
+
1109
+ # Interpolate to contrails_level
1110
+ contrails_level["tau_contrails_above"] = contrails_level.intersect_met(mda)
1111
+ return contrails_level
1112
+
1113
+
1114
+ def _rsr_and_olr_with_contrail_overlap(
1115
+ contrails_level: GeoVectorDataset, delta_rad_t: xr.Dataset
1116
+ ) -> GeoVectorDataset:
1117
+ """
1118
+ Calculate RSR and OLR at contrail waypoints after accounting for contrail overlapping.
1119
+
1120
+ Parameters
1121
+ ----------
1122
+ contrails_level : GeoVectorDataset
1123
+ Contrail waypoints at the current altitude layer.
1124
+ delta_rad_t : xr.Dataset
1125
+ Radiation fields with cumulative change in RSR and OLR due to contrail overlapping.
1126
+
1127
+ Returns
1128
+ -------
1129
+ GeoVectorDataset
1130
+ Contrail waypoints at the current altitude layer with `rsr_overlap` and
1131
+ `olr_overlap` attached.
1132
+ """
1133
+ mds = MetDataset(delta_rad_t)
1134
+
1135
+ # Interpolate radiation fields to obtain `rsr_overlap` and `olr_overlap`
1136
+ delta_rsr = contrails_level.intersect_met(mds["rsr"])
1137
+ delta_olr = contrails_level.intersect_met(mds["olr"])
1138
+
1139
+ # Constrain RSR so it is not larger than the SDR
1140
+ contrails_level["rsr_overlap"] = np.minimum(
1141
+ contrails_level["sdr"],
1142
+ contrails_level["rsr"] + delta_rsr,
1143
+ )
1144
+
1145
+ # Constrain OLR so it is not smaller than 80% of the original value
1146
+ contrails_level["olr_overlap"] = np.maximum(
1147
+ 0.8 * contrails_level["olr"],
1148
+ contrails_level["olr"] + delta_olr,
1149
+ )
1150
+ return contrails_level
1151
+
1152
+
1153
+ def _local_sw_and_lw_rf_with_contrail_overlap(
1154
+ contrails_level: GeoVectorDataset,
1155
+ habit_distributions: npt.NDArray[np.floating],
1156
+ radius_threshold_um: npt.NDArray[np.floating],
1157
+ ) -> GeoVectorDataset:
1158
+ """
1159
+ Calculate local contrail SW and LW RF after accounting for contrail overlapping.
1160
+
1161
+ Parameters
1162
+ ----------
1163
+ contrails_level : GeoVectorDataset
1164
+ Contrail waypoints at the current altitude layer.
1165
+ habit_distributions : npt.NDArray[np.floating]
1166
+ Habit weight distributions.
1167
+ See :attr:`CocipParams().habit_distributions`
1168
+ radius_threshold_um : npt.NDArray[np.floating]
1169
+ Radius thresholds for habit distributions.
1170
+ See :attr:`CocipParams.radius_threshold_um`
1171
+
1172
+ Returns
1173
+ -------
1174
+ GeoVectorDataset
1175
+ Contrail waypoints at the current altitude layer with `rf_sw_overlap`,
1176
+ `rf_lw_overlap`, and `rf_net_overlap` attached.
1177
+ """
1178
+ r_vol_um = contrails_level["r_ice_vol"] * 1e6
1179
+ habit_w = habit_weights(r_vol_um, habit_distributions, radius_threshold_um)
1180
+
1181
+ # Calculate solar constant
1182
+ theta_rad = geo.orbital_position(contrails_level["time"])
1183
+ sd0 = geo.solar_constant(theta_rad)
1184
+ tau_contrail = contrails_level["tau_contrail"]
1185
+ tau_cirrus = contrails_level["tau_cirrus"] + contrails_level["tau_contrails_above"]
1186
+
1187
+ # Calculate local SW and LW RF
1188
+ contrails_level["rf_sw_overlap"] = shortwave_radiative_forcing(
1189
+ r_vol_um,
1190
+ contrails_level["sdr"],
1191
+ contrails_level["rsr_overlap"],
1192
+ sd0,
1193
+ tau_contrail,
1194
+ tau_cirrus,
1195
+ habit_w,
1196
+ )
1197
+
1198
+ contrails_level["rf_lw_overlap"] = longwave_radiative_forcing(
1199
+ r_vol_um,
1200
+ contrails_level["olr_overlap"],
1201
+ contrails_level["air_temperature"],
1202
+ tau_contrail,
1203
+ tau_cirrus,
1204
+ habit_w,
1205
+ )
1206
+ contrails_level["rf_net_overlap"] = (
1207
+ contrails_level["rf_lw_overlap"] + contrails_level["rf_sw_overlap"]
1208
+ )
1209
+ return contrails_level
1210
+
1211
+
1212
+ def _change_in_background_rsr_and_olr(
1213
+ contrails_level: GeoVectorDataset,
1214
+ delta_rad_t: xr.Dataset,
1215
+ *,
1216
+ spatial_bbox: tuple[float, float, float, float] = (-180.0, -90.0, 180.0, 90.0),
1217
+ spatial_grid_res: float = 0.5,
1218
+ ) -> xr.Dataset:
1219
+ r"""
1220
+ Calculate change in background RSR and OLR fields.
1221
+
1222
+ Parameters
1223
+ ----------
1224
+ contrails_level : GeoVectorDataset
1225
+ Contrail waypoints at the current altitude layer.
1226
+ delta_rad_t : xr.Dataset
1227
+ Radiation fields with cumulative change in RSR and OLR due to contrail overlapping.
1228
+ spatial_bbox: tuple[float, float, float, float]
1229
+ Spatial bounding box, ``(lon_min, lat_min, lon_max, lat_max)``, [:math:`\deg`]
1230
+ spatial_grid_res : float
1231
+ Spatial grid resolution, [:math:`\deg`]
1232
+
1233
+ Returns
1234
+ -------
1235
+ xr.Dataset
1236
+ Radiation fields with cumulative change in RSR and OLR due to contrail overlapping.
1237
+ """
1238
+ # Calculate SW and LW radiative flux (Units: W)
1239
+ segment_length = contrails_level["segment_length"]
1240
+ width = contrails_level["width"]
1241
+
1242
+ contrails_level["sw_radiative_flux"] = (
1243
+ np.abs(contrails_level["rf_sw_overlap"]) * segment_length * width
1244
+ )
1245
+
1246
+ contrails_level["lw_radiative_flux"] = contrails_level["rf_lw_overlap"] * segment_length * width
1247
+
1248
+ # Aggregate SW and LW radiative flux to a longitude-latitude grid
1249
+ ds = contrails_level.to_lon_lat_grid(
1250
+ agg={"sw_radiative_flux": "sum", "lw_radiative_flux": "sum"},
1251
+ spatial_bbox=spatial_bbox,
1252
+ spatial_grid_res=spatial_grid_res,
1253
+ )
1254
+ ds = ds.expand_dims(level=[-1.0], time=[contrails_level["time"][0]])
1255
+ da_surface_area = geo.grid_surface_area(ds["longitude"].values, ds["latitude"].values)
1256
+
1257
+ # Cumulative change in RSR and OLR
1258
+ delta_rad_t["rsr"] += ds["sw_radiative_flux"] / da_surface_area
1259
+ delta_rad_t["olr"] -= ds["lw_radiative_flux"] / da_surface_area
1260
+ return delta_rad_t