pycontrails 0.53.0__cp313-cp313-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 (109) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +16 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +641 -0
  5. pycontrails/core/airports.py +226 -0
  6. pycontrails/core/cache.py +881 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +470 -0
  9. pycontrails/core/flight.py +2312 -0
  10. pycontrails/core/flightplan.py +220 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +721 -0
  13. pycontrails/core/met.py +2833 -0
  14. pycontrails/core/met_var.py +307 -0
  15. pycontrails/core/models.py +1181 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-313-darwin.so +0 -0
  18. pycontrails/core/vector.py +2191 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_leo_utils/search.py +250 -0
  21. pycontrails/datalib/_leo_utils/static/bq_roi_query.sql +6 -0
  22. pycontrails/datalib/_leo_utils/vis.py +59 -0
  23. pycontrails/datalib/_met_utils/metsource.py +743 -0
  24. pycontrails/datalib/ecmwf/__init__.py +53 -0
  25. pycontrails/datalib/ecmwf/arco_era5.py +527 -0
  26. pycontrails/datalib/ecmwf/common.py +109 -0
  27. pycontrails/datalib/ecmwf/era5.py +538 -0
  28. pycontrails/datalib/ecmwf/era5_model_level.py +482 -0
  29. pycontrails/datalib/ecmwf/hres.py +782 -0
  30. pycontrails/datalib/ecmwf/hres_model_level.py +495 -0
  31. pycontrails/datalib/ecmwf/ifs.py +284 -0
  32. pycontrails/datalib/ecmwf/model_levels.py +79 -0
  33. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  34. pycontrails/datalib/ecmwf/variables.py +256 -0
  35. pycontrails/datalib/gfs/__init__.py +28 -0
  36. pycontrails/datalib/gfs/gfs.py +646 -0
  37. pycontrails/datalib/gfs/variables.py +100 -0
  38. pycontrails/datalib/goes.py +772 -0
  39. pycontrails/datalib/landsat.py +568 -0
  40. pycontrails/datalib/sentinel.py +512 -0
  41. pycontrails/datalib/spire.py +739 -0
  42. pycontrails/ext/bada.py +41 -0
  43. pycontrails/ext/cirium.py +14 -0
  44. pycontrails/ext/empirical_grid.py +140 -0
  45. pycontrails/ext/synthetic_flight.py +426 -0
  46. pycontrails/models/__init__.py +1 -0
  47. pycontrails/models/accf.py +406 -0
  48. pycontrails/models/apcemm/__init__.py +8 -0
  49. pycontrails/models/apcemm/apcemm.py +983 -0
  50. pycontrails/models/apcemm/inputs.py +226 -0
  51. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  52. pycontrails/models/apcemm/utils.py +437 -0
  53. pycontrails/models/cocip/__init__.py +29 -0
  54. pycontrails/models/cocip/cocip.py +2617 -0
  55. pycontrails/models/cocip/cocip_params.py +299 -0
  56. pycontrails/models/cocip/cocip_uncertainty.py +285 -0
  57. pycontrails/models/cocip/contrail_properties.py +1517 -0
  58. pycontrails/models/cocip/output_formats.py +2261 -0
  59. pycontrails/models/cocip/radiative_forcing.py +1262 -0
  60. pycontrails/models/cocip/radiative_heating.py +520 -0
  61. pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
  62. pycontrails/models/cocip/wake_vortex.py +396 -0
  63. pycontrails/models/cocip/wind_shear.py +120 -0
  64. pycontrails/models/cocipgrid/__init__.py +9 -0
  65. pycontrails/models/cocipgrid/cocip_grid.py +2573 -0
  66. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  67. pycontrails/models/dry_advection.py +486 -0
  68. pycontrails/models/emissions/__init__.py +21 -0
  69. pycontrails/models/emissions/black_carbon.py +594 -0
  70. pycontrails/models/emissions/emissions.py +1353 -0
  71. pycontrails/models/emissions/ffm2.py +336 -0
  72. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  73. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  74. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  75. pycontrails/models/humidity_scaling/__init__.py +37 -0
  76. pycontrails/models/humidity_scaling/humidity_scaling.py +1025 -0
  77. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  78. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  79. pycontrails/models/issr.py +210 -0
  80. pycontrails/models/pcc.py +327 -0
  81. pycontrails/models/pcr.py +154 -0
  82. pycontrails/models/ps_model/__init__.py +17 -0
  83. pycontrails/models/ps_model/ps_aircraft_params.py +376 -0
  84. pycontrails/models/ps_model/ps_grid.py +505 -0
  85. pycontrails/models/ps_model/ps_model.py +1017 -0
  86. pycontrails/models/ps_model/ps_operational_limits.py +540 -0
  87. pycontrails/models/ps_model/static/ps-aircraft-params-20240524.csv +68 -0
  88. pycontrails/models/ps_model/static/ps-synonym-list-20240524.csv +103 -0
  89. pycontrails/models/sac.py +459 -0
  90. pycontrails/models/tau_cirrus.py +168 -0
  91. pycontrails/physics/__init__.py +1 -0
  92. pycontrails/physics/constants.py +116 -0
  93. pycontrails/physics/geo.py +989 -0
  94. pycontrails/physics/jet.py +837 -0
  95. pycontrails/physics/thermo.py +451 -0
  96. pycontrails/physics/units.py +472 -0
  97. pycontrails/py.typed +0 -0
  98. pycontrails/utils/__init__.py +1 -0
  99. pycontrails/utils/dependencies.py +66 -0
  100. pycontrails/utils/iteration.py +13 -0
  101. pycontrails/utils/json.py +188 -0
  102. pycontrails/utils/temp.py +50 -0
  103. pycontrails/utils/types.py +165 -0
  104. pycontrails-0.53.0.dist-info/LICENSE +178 -0
  105. pycontrails-0.53.0.dist-info/METADATA +181 -0
  106. pycontrails-0.53.0.dist-info/NOTICE +43 -0
  107. pycontrails-0.53.0.dist-info/RECORD +109 -0
  108. pycontrails-0.53.0.dist-info/WHEEL +5 -0
  109. pycontrails-0.53.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,1262 @@
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.float64],
161
+ habit_distributions: npt.NDArray[np.float64],
162
+ radius_threshold_um: npt.NDArray[np.float64],
163
+ ) -> npt.NDArray[np.float64]:
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.float64]
179
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
180
+ habit_distributions : npt.NDArray[np.float64]
181
+ Habit weight distributions.
182
+ See :attr:`CocipParams().habit_distributions`
183
+ radius_threshold_um : npt.NDArray[np.float64]
184
+ Radius thresholds for habit distributions.
185
+ See :attr:`CocipParams.radius_threshold_um`
186
+
187
+ Returns
188
+ -------
189
+ npt.NDArray[np.float64]
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.float64], radius_threshold_um: npt.NDArray[np.float64]
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.float64]
223
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
224
+ radius_threshold_um : npt.NDArray[np.float64]
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.float64], habit_idx: npt.NDArray[np.intp]
244
+ ) -> np.ndarray:
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.float64]
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.float64]
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.float64]) -> npt.NDArray[np.float64]:
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.float64]
300
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
301
+
302
+ Returns
303
+ -------
304
+ npt.NDArray[np.float64]
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.float64]) -> npt.NDArray[np.float64]:
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.float64]
317
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
318
+
319
+ Returns
320
+ -------
321
+ npt.NDArray[np.float64]
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.float64]) -> npt.NDArray[np.float64]:
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.float64]
338
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
339
+
340
+ Returns
341
+ -------
342
+ npt.NDArray[np.float64]
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(r_vol_um: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
354
+ r"""Calculate the effective radius of ice particles assuming a rough aggregate particle habit.
355
+
356
+ Parameters
357
+ ----------
358
+ r_vol_um : npt.NDArray[np.float64]
359
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
360
+
361
+ Returns
362
+ -------
363
+ npt.NDArray[np.float64]
364
+ Effective radius, [:math:`\mu m`]
365
+ """
366
+ r_eff_um = 0.574 * r_vol_um
367
+ return np.minimum(r_eff_um, 45.0)
368
+
369
+
370
+ def effective_radius_rosette(r_vol_um: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
371
+ r"""
372
+ Calculate the effective radius of contrail ice particles assuming a rosette particle habit.
373
+
374
+ Parameters
375
+ ----------
376
+ r_vol_um : npt.NDArray[np.float64]
377
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
378
+
379
+ Returns
380
+ -------
381
+ npt.NDArray[np.float64]
382
+ Effective radius, [:math:`\mu m`]
383
+ """
384
+ r_eff_um = r_vol_um * (
385
+ 0.1770 * np.exp(-(2.144e-2 * r_vol_um)) + 0.4267 * np.exp(-(3.562e-4 * r_vol_um))
386
+ )
387
+ return np.minimum(r_eff_um, 45.0)
388
+
389
+
390
+ def effective_radius_plate(r_vol_um: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
391
+ r"""
392
+ Calculate the effective radius of contrail ice particles assuming a plate particle habit.
393
+
394
+ Parameters
395
+ ----------
396
+ r_vol_um : npt.NDArray[np.float64]
397
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
398
+
399
+ Returns
400
+ -------
401
+ npt.NDArray[np.float64]
402
+ Effective radius, [:math:`\mu m`]
403
+ """
404
+ r_eff_um = r_vol_um * (
405
+ 0.1663 + 0.3713 * np.exp(-(0.0336 * r_vol_um)) + 0.3309 * np.exp(-(0.0035 * r_vol_um))
406
+ )
407
+ return np.minimum(r_eff_um, 45.0)
408
+
409
+
410
+ def effective_radius_droxtal(r_vol_um: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
411
+ r"""
412
+ Calculate the effective radius of contrail ice particles assuming a droxtal particle habit.
413
+
414
+ Parameters
415
+ ----------
416
+ r_vol_um : npt.NDArray[np.float64]
417
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
418
+
419
+ Returns
420
+ -------
421
+ npt.NDArray[np.float64]
422
+ Effective radius, [:math:`\mu m`]
423
+ """
424
+ r_eff_um = 0.94 * r_vol_um
425
+ return np.minimum(r_eff_um, 45.0)
426
+
427
+
428
+ def effective_radius_myhre(r_vol_um: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
429
+ r"""
430
+ Calculate the effective radius of contrail ice particles assuming a sphere particle habit.
431
+
432
+ Parameters
433
+ ----------
434
+ r_vol_um : npt.NDArray[np.float64]
435
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
436
+
437
+ Returns
438
+ -------
439
+ npt.NDArray[np.float64]
440
+ Effective radius, [:math:`\mu m`]
441
+ """
442
+ return np.minimum(r_vol_um, 45.0)
443
+
444
+
445
+ # -----------------
446
+ # Radiative Forcing
447
+ # -----------------
448
+
449
+
450
+ def longwave_radiative_forcing(
451
+ r_vol_um: npt.NDArray[np.float64],
452
+ olr: npt.NDArray[np.float64],
453
+ air_temperature: npt.NDArray[np.float64],
454
+ tau_contrail: npt.NDArray[np.float64],
455
+ tau_cirrus: npt.NDArray[np.float64],
456
+ habit_weights_: npt.NDArray[np.float64],
457
+ r_eff_um: npt.NDArray[np.float64] | None = None,
458
+ ) -> npt.NDArray[np.float64]:
459
+ r"""
460
+ Calculate the local contrail longwave radiative forcing (:math:`RF_{LW}`).
461
+
462
+ All returned values are positive.
463
+
464
+ Parameters
465
+ ----------
466
+ r_vol_um : npt.NDArray[np.float64]
467
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
468
+ olr : npt.NDArray[np.float64]
469
+ Outgoing longwave radiation at each waypoint, [:math:`W m^{-2}`]
470
+ air_temperature : npt.NDArray[np.float64]
471
+ Ambient temperature at each waypoint, [:math:`K`]
472
+ tau_contrail : npt.NDArray[np.float64]
473
+ Contrail optical depth at each waypoint
474
+ tau_cirrus : npt.NDArray[np.float64]
475
+ Optical depth of numerical weather prediction (NWP) cirrus above the
476
+ contrail at each waypoint
477
+ habit_weights_ : npt.NDArray[np.float64]
478
+ Weights to different ice particle habits for each waypoint,
479
+ ``n_waypoints x 8`` (habit) columns, [:math:`[0 - 1]`]
480
+ r_eff_um : npt.NDArray[np.float64], optional
481
+ Provide effective radius corresponding to elements in ``r_vol_um``, [:math:`\mu m`].
482
+ Defaults to None, which means the effective radius will be calculated using ``r_vol_um``
483
+ and habit types in :func:`effective_radius_by_habit`.
484
+
485
+ Returns
486
+ -------
487
+ npt.NDArray[np.float64]
488
+ Local contrail longwave radiative forcing (positive), [:math:`W m^{-2}`]
489
+
490
+ Raises
491
+ ------
492
+ ValueError
493
+ If `r_eff_um` and `olr` have different shapes.
494
+
495
+ References
496
+ ----------
497
+ - :cite:`schumannParametricRadiativeForcing2012`
498
+ """
499
+ # get list of habit weight indexs where the weights > 0
500
+ # this is a tuple of (np.array[waypoint index], np.array[habit type index])
501
+ habit_weight_mask = habit_weights_ > 0.0
502
+ idx0, idx1 = np.nonzero(habit_weight_mask)
503
+
504
+ # Convert parametric coefficients for vectorized operations
505
+ delta_t = RF_CONST.delta_t[idx1]
506
+ delta_lc = RF_CONST.delta_lc[idx1]
507
+ delta_lr = RF_CONST.delta_lr[idx1]
508
+ k_t = RF_CONST.k_t[idx1]
509
+ T_0 = RF_CONST.T_0[idx1]
510
+
511
+ olr_h = olr[idx0]
512
+ tau_cirrus_h = tau_cirrus[idx0]
513
+ tau_contrail_h = tau_contrail[idx0]
514
+ air_temperature_h = air_temperature[idx0]
515
+
516
+ # effective radius
517
+ if r_eff_um is None:
518
+ r_vol_um_h = r_vol_um[idx0]
519
+ r_eff_um_h = effective_radius_by_habit(r_vol_um_h, idx1)
520
+ else:
521
+ if r_eff_um.shape != olr.shape:
522
+ raise ValueError(
523
+ "User provided effective radius (`r_eff_um`) must have the same shape as `olr`"
524
+ f" {olr.shape}"
525
+ )
526
+
527
+ r_eff_um_h = r_eff_um[idx0]
528
+
529
+ # Longwave radiation calculations
530
+ e_lw = olr_reduction_natural_cirrus(tau_cirrus_h, delta_lc)
531
+ f_lw = contrail_effective_emissivity(r_eff_um_h, delta_lr)
532
+
533
+ # calculate the RF LW per habit type
534
+ # see eqn (2) in :cite:`schumannParametricRadiativeForcing2012`
535
+ rf_lw_per_habit = (
536
+ (olr_h - k_t * (air_temperature_h - T_0))
537
+ * e_lw
538
+ * (1.0 - np.exp(-delta_t * f_lw * tau_contrail_h))
539
+ )
540
+ rf_lw_per_habit.clip(min=0.0, out=rf_lw_per_habit)
541
+
542
+ # Weight and sum the RF contributions of each habit type according the habit weight
543
+ # regime at the waypoint
544
+ # see eqn (12) in :cite:`schumannParametricRadiativeForcing2012`
545
+ # use fancy indexing to re-assign values to 2d array of waypoint x habit type
546
+ rf_lw_weighted = np.zeros_like(habit_weights_)
547
+ rf_lw_weighted[idx0, idx1] = rf_lw_per_habit * habit_weights_[habit_weight_mask]
548
+ return np.sum(rf_lw_weighted, axis=1)
549
+
550
+
551
+ def shortwave_radiative_forcing(
552
+ r_vol_um: npt.NDArray[np.float64],
553
+ sdr: npt.NDArray[np.float64],
554
+ rsr: npt.NDArray[np.float64],
555
+ sd0: npt.NDArray[np.float64],
556
+ tau_contrail: npt.NDArray[np.float64],
557
+ tau_cirrus: npt.NDArray[np.float64],
558
+ habit_weights_: npt.NDArray[np.float64],
559
+ r_eff_um: npt.NDArray[np.float64] | None = None,
560
+ ) -> npt.NDArray[np.float64]:
561
+ r"""
562
+ Calculate the local contrail shortwave radiative forcing (:math:`RF_{SW}`).
563
+
564
+ All returned values are negative.
565
+
566
+ Parameters
567
+ ----------
568
+ r_vol_um : npt.NDArray[np.float64]
569
+ Contrail ice particle volume mean radius, [:math:`\mu m`]
570
+ sdr : npt.NDArray[np.float64]
571
+ Solar direct radiation, [:math:`W m^{-2}`]
572
+ rsr : npt.NDArray[np.float64]
573
+ Reflected solar radiation, [:math:`W m^{-2}`]
574
+ sd0 : npt.NDArray[np.float64]
575
+ Solar constant, [:math:`W m^{-2}`]
576
+ tau_contrail : npt.NDArray[np.float64]
577
+ Contrail optical depth for each waypoint
578
+ tau_cirrus : npt.NDArray[np.float64]
579
+ Optical depth of numerical weather prediction (NWP) cirrus above the
580
+ contrail for each waypoint.
581
+ habit_weights_ : npt.NDArray[np.float64]
582
+ Weights to different ice particle habits for each waypoint,
583
+ ``n_waypoints x 8`` (habit) columns, [:math:`[0 - 1]`]
584
+ r_eff_um : npt.NDArray[np.float64], optional
585
+ Provide effective radius corresponding to elements in ``r_vol_um``, [:math:`\mu m`].
586
+ Defaults to None, which means the effective radius will be calculated using ``r_vol_um``
587
+ and habit types in :func:`effective_radius_by_habit`.
588
+
589
+ Returns
590
+ -------
591
+ npt.NDArray[np.float64]
592
+ Local contrail shortwave radiative forcing (negative), [:math:`W m^{-2}`]
593
+
594
+ Raises
595
+ ------
596
+ ValueError
597
+ If `r_eff_um` and `sdr` have different shapes.
598
+
599
+ References
600
+ ----------
601
+ - :cite:`schumannParametricRadiativeForcing2012`
602
+ """
603
+ # create mask for daytime (sdr > 0)
604
+ day = sdr > 0.0
605
+
606
+ # short circuit if no waypoints occur during the day
607
+ if not day.any():
608
+ return np.zeros_like(sdr)
609
+
610
+ # get list of habit weight indexs where the weights > 0
611
+ # this is a tuple of (np.array[waypoint index], np.array[habit type index])
612
+ habit_weight_mask = day.reshape(day.size, 1) & (habit_weights_ > 0.0)
613
+ idx0, idx1 = np.nonzero(habit_weight_mask)
614
+
615
+ # Convert parametric coefficients for vectorized operations
616
+ t_a = RF_CONST.t_a[idx1]
617
+ A_mu = RF_CONST.A_mu[idx1]
618
+ B_mu = RF_CONST.B_mu[idx1]
619
+ C_mu = RF_CONST.C_mu[idx1]
620
+ delta_sr = RF_CONST.delta_sr[idx1]
621
+ F_r = RF_CONST.F_r[idx1]
622
+ gamma_lower = RF_CONST.gamma_lower[idx1]
623
+ gamma_upper = RF_CONST.gamma_upper[idx1]
624
+ delta_sc = RF_CONST.delta_sc[idx1]
625
+ delta_sc_aps = RF_CONST.delta_sc_aps[idx1]
626
+
627
+ sdr_h = sdr[idx0]
628
+ rsr_h = rsr[idx0]
629
+ sd0_h = sd0[idx0]
630
+ tau_contrail_h = tau_contrail[idx0]
631
+ tau_cirrus_h = tau_cirrus[idx0]
632
+
633
+ albedo_ = albedo(sdr_h, rsr_h)
634
+ mue = np.minimum(sdr_h / sd0_h, 1.0)
635
+
636
+ # effective radius
637
+ if r_eff_um is None:
638
+ r_vol_um_h = r_vol_um[idx0]
639
+ r_eff_um_h = effective_radius_by_habit(r_vol_um_h, idx1)
640
+ else:
641
+ if r_eff_um.shape != sdr.shape:
642
+ raise ValueError(
643
+ "User provided effective radius (`r_eff_um`) must have the same shape as `sdr`"
644
+ f" {sdr.shape}"
645
+ )
646
+
647
+ r_eff_um_h = r_eff_um[idx0]
648
+
649
+ # Local contrail shortwave radiative forcing calculations
650
+ alpha_c = contrail_albedo(
651
+ tau_contrail_h,
652
+ mue,
653
+ r_eff_um_h,
654
+ A_mu,
655
+ B_mu,
656
+ C_mu,
657
+ delta_sr,
658
+ F_r,
659
+ gamma_lower,
660
+ gamma_upper,
661
+ )
662
+
663
+ e_sw = effective_tau_cirrus(tau_cirrus_h, mue, delta_sc, delta_sc_aps)
664
+
665
+ # calculate the RF SW per habit type
666
+ # see eqn (5) in :cite:`schumannParametricRadiativeForcing2012`
667
+ rf_sw_per_habit = np.minimum(-sdr_h * ((t_a - albedo_) ** 2) * alpha_c * e_sw, 0.0)
668
+
669
+ # Weight and sum the RF contributions of each habit type according the
670
+ # habit weight regime at the waypoint
671
+ # see eqn (12) in :cite:`schumannParametricRadiativeForcing2012`
672
+ # use fancy indexing to re-assign values to 2d array of waypoint x habit type
673
+ rf_sw_weighted = np.zeros_like(habit_weights_)
674
+ rf_sw_weighted[idx0, idx1] = rf_sw_per_habit * habit_weights_[habit_weight_mask]
675
+
676
+ return np.sum(rf_sw_weighted, axis=1)
677
+
678
+
679
+ def net_radiative_forcing(
680
+ rf_lw: npt.NDArray[np.float64], rf_sw: npt.NDArray[np.float64]
681
+ ) -> npt.NDArray[np.float64]:
682
+ """
683
+ Calculate the local contrail net radiative forcing (rf_net).
684
+
685
+ RF Net = Longwave RF (positive) + Shortwave RF (negative)
686
+
687
+ Parameters
688
+ ----------
689
+ rf_lw : npt.NDArray[np.float64]
690
+ local contrail longwave radiative forcing, [:math:`W m^{-2}`]
691
+ rf_sw : npt.NDArray[np.float64]
692
+ local contrail shortwave radiative forcing, [:math:`W m^{-2}`]
693
+
694
+ Returns
695
+ -------
696
+ npt.NDArray[np.float64]
697
+ local contrail net radiative forcing, [:math:`W m^{-2}`]
698
+ """
699
+ return rf_lw + rf_sw
700
+
701
+
702
+ def olr_reduction_natural_cirrus(
703
+ tau_cirrus: npt.NDArray[np.float64], delta_lc: npt.NDArray[np.float64]
704
+ ) -> npt.NDArray[np.float64]:
705
+ """
706
+ Calculate reduction in outgoing longwave radiation (OLR) due to the presence of natural cirrus.
707
+
708
+ Natural cirrus has optical depth ``tau_cirrus`` above the contrail.
709
+ See ``e_lw`` in Eq. (4) of Schumann et al. (2012).
710
+
711
+ Parameters
712
+ ----------
713
+ tau_cirrus : npt.NDArray[np.float64]
714
+ Optical depth of numerical weather prediction (NWP) cirrus above the
715
+ contrail for each waypoint.
716
+ delta_lc : npt.NDArray[np.float64]
717
+ Habit specific parameter to approximate the reduction of the outgoing
718
+ longwave radiation at the contrail level due to natural cirrus above the contrail.
719
+
720
+ Returns
721
+ -------
722
+ npt.NDArray[np.float64]
723
+ Reduction of outgoing longwave radiation
724
+ """
725
+ # e_lw calculations
726
+ return np.exp(-delta_lc * tau_cirrus)
727
+
728
+
729
+ def contrail_effective_emissivity(
730
+ r_eff_um: npt.NDArray[np.float64], delta_lr: npt.NDArray[np.float64]
731
+ ) -> npt.NDArray[np.float64]:
732
+ r"""Calculate the effective emissivity of the contrail, ``f_lw``.
733
+
734
+ Refer to Eq. (3) of Schumann et al. (2012).
735
+
736
+ Parameters
737
+ ----------
738
+ r_eff_um : npt.NDArray[np.float64]
739
+ Effective radius for each waypoint, n_waypoints x 8 (habit) columns, [:math:`\mu m`]
740
+ See :func:`effective_radius_habit`.
741
+ delta_lr : npt.NDArray[np.float64]
742
+ Habit specific parameter to approximate the effective emissivity of the contrail.
743
+
744
+ Returns
745
+ -------
746
+ npt.NDArray[np.float64]
747
+ Effective emissivity of the contrail
748
+ """
749
+ # f_lw calculations
750
+ return 1.0 - np.exp(-delta_lr * r_eff_um)
751
+
752
+
753
+ def albedo(sdr: npt.NDArray[np.float64], rsr: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
754
+ """
755
+ Calculate albedo along contrail waypoint.
756
+
757
+ Albedo, the diffuse reflection of solar radiation out of the total solar radiation,
758
+ is computed based on the solar direct radiation (`sdr`) and reflected solar radiation (`rsr`).
759
+
760
+ Output values range between 0 (corresponding to a black body that absorbs
761
+ all incident radiation) and 1 (a body that reflects all incident radiation).
762
+
763
+ Parameters
764
+ ----------
765
+ sdr : npt.NDArray[np.float64]
766
+ Solar direct radiation, [:math:`W m^{-2}`]
767
+ rsr : npt.NDArray[np.float64]
768
+ Reflected solar radiation, [:math:`W m^{-2}`]
769
+
770
+ Returns
771
+ -------
772
+ npt.NDArray[np.float64]
773
+ Albedo value, [:math:`[0 - 1]`]
774
+ """
775
+ day = sdr > 0.0
776
+ albedo_ = np.zeros(sdr.shape)
777
+ albedo_[day] = rsr[day] / sdr[day]
778
+ albedo_.clip(0.0, 1.0, out=albedo_)
779
+ return albedo_
780
+
781
+
782
+ def contrail_albedo(
783
+ tau_contrail: npt.NDArray[np.float64],
784
+ mue: npt.NDArray[np.float64],
785
+ r_eff_um: npt.NDArray[np.float64],
786
+ A_mu: npt.NDArray[np.float64],
787
+ B_mu: npt.NDArray[np.float64],
788
+ C_mu: npt.NDArray[np.float64],
789
+ delta_sr: npt.NDArray[np.float64],
790
+ F_r: npt.NDArray[np.float64],
791
+ gamma_lower: npt.NDArray[np.float64],
792
+ gamma_upper: npt.NDArray[np.float64],
793
+ ) -> npt.NDArray[np.float64]:
794
+ r"""
795
+ Calculate the contrail albedo, ``alpha_c``.
796
+
797
+ Refer to Eq. (6) of Schumann et al. (2012),
798
+
799
+ Parameters
800
+ ----------
801
+ tau_contrail : npt.NDArray[np.float64]
802
+ Contrail optical depth for each waypoint
803
+ mue : npt.NDArray[np.float64]
804
+ Cosine of the solar zenith angle (theta), mue = cos(theta) = sdr/sd0
805
+ r_eff_um : npt.NDArray[np.float64]
806
+ Effective radius for each waypoint, n_waypoints x 8 (habit) columns, [:math:`\mu m`]
807
+ See :func:`effective_radius_habit`.
808
+ A_mu : npt.NDArray[np.float64]
809
+ Habit-specific parameter to approximate the albedo of the contrail
810
+ B_mu : npt.NDArray[np.float64]
811
+ Habit-specific parameter to approximate the SZA-dependent contrail sideward scattering
812
+ C_mu : npt.NDArray[np.float64]
813
+ Habit-specific parameter to approximate the albedo of the contrail
814
+ delta_sr : npt.NDArray[np.float64]
815
+ Habit-specific parameter to approximate the effective contrail optical depth
816
+ F_r : npt.NDArray[np.float64]
817
+ Habit-specific parameter to approximate the effective contrail optical depth
818
+ gamma_lower : npt.NDArray[np.float64]
819
+ Habit-specific parameter to approximate the contrail reflectances
820
+ gamma_upper : npt.NDArray[np.float64]
821
+ Habit-specific parameter to approximate the contrail reflectances
822
+
823
+ Returns
824
+ -------
825
+ npt.NDArray[np.float64]
826
+ Contrail albedo for each waypoint and ice particle habit
827
+ """
828
+ tau_aps = tau_contrail * (1.0 - F_r * (1 - np.exp(-delta_sr * r_eff_um)))
829
+ tau_eff = tau_aps / (mue + 1e-6)
830
+ r_c = 1.0 - np.exp(-gamma_upper * tau_eff)
831
+ r_c_aps = np.exp(-gamma_lower * tau_eff)
832
+
833
+ f_mu = (2.0 * (1.0 - mue)) ** B_mu - 1.0
834
+ return r_c * (C_mu + (A_mu * r_c_aps * f_mu))
835
+
836
+
837
+ def effective_tau_cirrus(
838
+ tau_cirrus: npt.NDArray[np.float64],
839
+ mue: npt.NDArray[np.float64],
840
+ delta_sc: npt.NDArray[np.float64],
841
+ delta_sc_aps: npt.NDArray[np.float64],
842
+ ) -> npt.NDArray[np.float64]:
843
+ r"""
844
+ Calculate the effective optical depth of natural cirrus above the contrail, ``e_sw``.
845
+
846
+ Refer to Eq. (11) of :cite:`schumannParametricRadiativeForcing2012`. See Notes for
847
+ a correction to the equation.
848
+
849
+ Parameters
850
+ ----------
851
+ tau_cirrus : npt.NDArray[np.float64]
852
+ Optical depth of numerical weather prediction (NWP) cirrus above the
853
+ contrail for each waypoint.
854
+ mue : npt.NDArray[np.float64]
855
+ Cosine of the solar zenith angle (theta), mue = cos(theta) = sdr/sd0
856
+ delta_sc : npt.NDArray[np.float64]
857
+ Habit-specific parameter to account for the optical depth of natural
858
+ cirrus above the contrail
859
+ delta_sc_aps : npt.NDArray[np.float64]
860
+ Habit-specific parameter to account for the optical depth of natural
861
+ cirrus above the contrail
862
+
863
+ Returns
864
+ -------
865
+ npt.NDArray[np.float64]
866
+ Effective optical depth of natural cirrus above the contrail,
867
+ ``n_waypoints x 8`` (habit) columns.
868
+
869
+ Notes
870
+ -----
871
+ - In a personal correspondence, Dr. Schumann identified a print error in Eq. (11) in
872
+ :cite:`schumannParametricRadiativeForcing2012`, where the positions of ``delta_sc_aps``
873
+ and ``delta_sc`` should be swapped. The correct function is provided below.
874
+ """
875
+ tau_cirrus_eff = tau_cirrus / (mue + 1e-6)
876
+ return np.exp(tau_cirrus * delta_sc_aps - tau_cirrus_eff * delta_sc)
877
+
878
+
879
+ # -----------------------------
880
+ # Contrail-contrail overlapping
881
+ # -----------------------------
882
+
883
+
884
+ def contrail_contrail_overlap_radiative_effects(
885
+ contrails: GeoVectorDataset,
886
+ habit_distributions: npt.NDArray[np.float64],
887
+ radius_threshold_um: npt.NDArray[np.float64],
888
+ *,
889
+ min_altitude_m: float = 6000.0,
890
+ max_altitude_m: float = 13000.0,
891
+ dz_overlap_m: float = 500.0,
892
+ spatial_grid_res: float = 0.25,
893
+ ) -> GeoVectorDataset:
894
+ r"""
895
+ Calculate radiative properties after accounting for contrail overlapping.
896
+
897
+ This function mutates the ``contrails`` parameter.
898
+
899
+ Parameters
900
+ ----------
901
+ contrails : GeoVectorDataset
902
+ Contrail waypoints at a given time. Must include the following variables:
903
+ - segment_length
904
+ - width
905
+ - r_ice_vol
906
+ - tau_contrail
907
+ - tau_cirrus
908
+ - air_temperature
909
+ - sdr
910
+ - rsr
911
+ - olr
912
+
913
+ habit_distributions : npt.NDArray[np.float64]
914
+ Habit weight distributions.
915
+ See :attr:`CocipParams.habit_distributions`
916
+ radius_threshold_um : npt.NDArray[np.float64]
917
+ Radius thresholds for habit distributions.
918
+ See :attr:`CocipParams.radius_threshold_um`
919
+ min_altitude_m : float
920
+ Minimum altitude domain in simulation, [:math:`m`]
921
+ See :attr:`CocipParams.min_altitude_m`
922
+ max_altitude_m
923
+ Maximum altitude domain in simulation, [:math:`m`]
924
+ See :attr:`CocipParams.min_altitude_m`
925
+ dz_overlap_m : float
926
+ Altitude interval used to segment contrail waypoints, [:math:`m`]
927
+ See :attr:`CocipParams.dz_overlap_m`
928
+ spatial_grid_res : float
929
+ Spatial grid resolution, [:math:`\deg`]
930
+
931
+ Returns
932
+ -------
933
+ GeoVectorDataset
934
+ Contrail waypoints at a given time with additional variables attached, including
935
+ - rsr_overlap
936
+ - olr_overlap
937
+ - tau_cirrus_overlap
938
+ - rf_sw_overlap
939
+ - rf_lw_overlap
940
+ - rf_net_overlap
941
+
942
+ References
943
+ ----------
944
+ - Schumann et al. (2021) Air traffic and contrail changes over Europe during COVID-19:
945
+ A model study, Atmos. Chem. Phys., 21, 7429-7450, https://doi.org/10.5194/ACP-21-7429-2021.
946
+ - Teoh et al. (2023) Global aviation contrail climate effects from 2019 to 2021.
947
+
948
+ Notes
949
+ -----
950
+ - The radiative effects of contrail-contrail overlapping is approximated by changing the
951
+ background RSR and OLR fields, and the overlying cirrus optical depth above the contrail.
952
+ - All contrail segments within each altitude interval are treated as one contrail layer, where
953
+ they do not overlap. Contrail layers are processed starting from the bottom to the top.
954
+ - Refer to the Supporting Information (S4.3) of Teoh et al. (2023)
955
+ """
956
+ assert "segment_length" in contrails
957
+ assert "width" in contrails
958
+ assert "r_ice_vol" in contrails
959
+ assert "tau_contrail" in contrails
960
+ assert "tau_cirrus" in contrails
961
+ assert "air_temperature" in contrails
962
+ assert "sdr" in contrails
963
+ assert "rsr" in contrails
964
+ assert "olr" in contrails
965
+
966
+ if not contrails:
967
+ raise ValueError("Parameter 'contrails' must be non-empty.")
968
+
969
+ time = contrails["time"]
970
+ time0 = time[0]
971
+ if not np.all(time == time0):
972
+ raise ValueError("Contrail waypoints must have a constant time.")
973
+
974
+ longitude = contrails["longitude"]
975
+ latitude = contrails["latitude"]
976
+ altitude = contrails.altitude
977
+
978
+ spatial_bbox = geo.spatial_bounding_box(longitude, latitude)
979
+ west, south, east, north = spatial_bbox
980
+
981
+ assert spatial_grid_res > 0.01
982
+ lon_coords = np.arange(west, east + 0.01, spatial_grid_res)
983
+ lat_coords = np.arange(south, north + 0.01, spatial_grid_res)
984
+
985
+ dims = ["longitude", "latitude", "level", "time"]
986
+ shape = (len(lon_coords), len(lat_coords), 1, 1)
987
+ delta_rad_t = xr.Dataset(
988
+ data_vars={"rsr": (dims, np.zeros(shape)), "olr": (dims, np.zeros(shape))},
989
+ coords={"longitude": lon_coords, "latitude": lat_coords, "level": [-1.0], "time": [time0]},
990
+ )
991
+
992
+ # Initialise radiation fields to store change in background RSR and OLR due to contrails
993
+ rsr_overlap = np.zeros_like(longitude)
994
+ olr_overlap = np.zeros_like(longitude)
995
+ tau_cirrus_overlap = np.zeros_like(longitude)
996
+ rf_sw_overlap = np.zeros_like(longitude)
997
+ rf_lw_overlap = np.zeros_like(longitude)
998
+ rf_net_overlap = np.zeros_like(longitude)
999
+
1000
+ # Account for contrail overlapping starting from bottom to top layers
1001
+ altitude_layers = np.arange(min_altitude_m, max_altitude_m + 1.0, dz_overlap_m)
1002
+
1003
+ for alt_layer0, alt_layer1 in itertools.pairwise(altitude_layers):
1004
+ is_in_layer = (altitude >= alt_layer0) & (altitude < alt_layer1)
1005
+
1006
+ # Get contrail waypoints at current altitude layer
1007
+ contrails_level = contrails.filter(is_in_layer, copy=True)
1008
+
1009
+ # Skip altitude layer if no contrails are present
1010
+ if not contrails_level:
1011
+ continue
1012
+
1013
+ # Get contrails above altitude layer
1014
+ is_above_layer = (altitude >= alt_layer1) & (altitude <= max_altitude_m)
1015
+ contrails_above = contrails.filter(is_above_layer, copy=True)
1016
+
1017
+ contrails_level = _contrail_optical_depth_above_contrail_layer(
1018
+ contrails_level,
1019
+ contrails_above,
1020
+ spatial_bbox=spatial_bbox,
1021
+ spatial_grid_res=spatial_grid_res,
1022
+ )
1023
+
1024
+ # Calculate updated RSR and OLR with contrail overlapping
1025
+ contrails_level = _rsr_and_olr_with_contrail_overlap(contrails_level, delta_rad_t)
1026
+
1027
+ # Calculate local contrail SW and LW RF with contrail overlapping
1028
+ contrails_level = _local_sw_and_lw_rf_with_contrail_overlap(
1029
+ contrails_level, habit_distributions, radius_threshold_um
1030
+ )
1031
+
1032
+ # Cumulative change in background RSR and OLR fields
1033
+ delta_rad_t = _change_in_background_rsr_and_olr(
1034
+ contrails_level,
1035
+ delta_rad_t,
1036
+ spatial_bbox=spatial_bbox,
1037
+ spatial_grid_res=spatial_grid_res,
1038
+ )
1039
+
1040
+ # Save values
1041
+ rsr_overlap[is_in_layer] = contrails_level["rsr_overlap"]
1042
+ olr_overlap[is_in_layer] = contrails_level["olr_overlap"]
1043
+ tau_cirrus_overlap[is_in_layer] = (
1044
+ contrails_level["tau_cirrus"] + contrails_level["tau_contrails_above"]
1045
+ )
1046
+ rf_sw_overlap[is_in_layer] = contrails_level["rf_sw_overlap"]
1047
+ rf_lw_overlap[is_in_layer] = contrails_level["rf_lw_overlap"]
1048
+ rf_net_overlap[is_in_layer] = contrails_level["rf_net_overlap"]
1049
+
1050
+ # Add new variables to contrails
1051
+ contrails["rsr_overlap"] = rsr_overlap
1052
+ contrails["olr_overlap"] = olr_overlap
1053
+ contrails["tau_cirrus_overlap"] = tau_cirrus_overlap
1054
+ contrails["rf_sw_overlap"] = rf_sw_overlap
1055
+ contrails["rf_lw_overlap"] = rf_lw_overlap
1056
+ contrails["rf_net_overlap"] = rf_net_overlap
1057
+
1058
+ return contrails
1059
+
1060
+
1061
+ def _contrail_optical_depth_above_contrail_layer(
1062
+ contrails_level: GeoVectorDataset,
1063
+ contrails_above: GeoVectorDataset,
1064
+ spatial_bbox: tuple[float, float, float, float],
1065
+ spatial_grid_res: float,
1066
+ ) -> GeoVectorDataset:
1067
+ r"""
1068
+ Calculate the contrail optical depth above the contrail waypoints.
1069
+
1070
+ Parameters
1071
+ ----------
1072
+ contrails_level : GeoVectorDataset
1073
+ Contrail waypoints at the current altitude layer.
1074
+ contrails_above : GeoVectorDataset
1075
+ Contrail waypoints above the current altitude layer.
1076
+ spatial_bbox: tuple[float, float, float, float]
1077
+ Spatial bounding box, ``(lon_min, lat_min, lon_max, lat_max)``, [:math:`\deg`]
1078
+ spatial_grid_res : float
1079
+ Spatial grid resolution, [:math:`\deg`]
1080
+
1081
+ Returns
1082
+ -------
1083
+ GeoVectorDataset
1084
+ Contrail waypoints at the current altitude layer with `tau_contrails_above` attached.
1085
+ """
1086
+ contrails_above["tau_contrails_above"] = (
1087
+ contrails_above["tau_contrail"]
1088
+ * contrails_above["segment_length"]
1089
+ * contrails_above["width"]
1090
+ )
1091
+
1092
+ # Aggregate contrail optical depth to a longitude-latitude grid
1093
+ da = contrails_above.to_lon_lat_grid(
1094
+ agg={"tau_contrails_above": "sum"},
1095
+ spatial_bbox=spatial_bbox,
1096
+ spatial_grid_res=spatial_grid_res,
1097
+ )["tau_contrails_above"]
1098
+ da = da.expand_dims(level=[-1.0], time=[contrails_level["time"][0]])
1099
+ da = da.transpose("longitude", "latitude", "level", "time")
1100
+
1101
+ da_surface_area = geo.grid_surface_area(da["longitude"].values, da["latitude"].values)
1102
+ da = da / da_surface_area
1103
+
1104
+ lon_coords = da["longitude"].values
1105
+ wrap_longitude = (lon_coords[-1] + lon_coords[0] + np.diff(lon_coords)[0]) == 0.0
1106
+ mda = MetDataArray(da, wrap_longitude=wrap_longitude, copy=False)
1107
+
1108
+ # Interpolate to contrails_level
1109
+ contrails_level["tau_contrails_above"] = contrails_level.intersect_met(mda)
1110
+ return contrails_level
1111
+
1112
+
1113
+ def _rsr_and_olr_with_contrail_overlap(
1114
+ contrails_level: GeoVectorDataset, delta_rad_t: xr.Dataset
1115
+ ) -> GeoVectorDataset:
1116
+ """
1117
+ Calculate RSR and OLR at contrail waypoints after accounting for contrail overlapping.
1118
+
1119
+ Parameters
1120
+ ----------
1121
+ contrails_level : GeoVectorDataset
1122
+ Contrail waypoints at the current altitude layer.
1123
+ delta_rad_t : xr.Dataset
1124
+ Radiation fields with cumulative change in RSR and OLR due to contrail overlapping.
1125
+
1126
+ Returns
1127
+ -------
1128
+ GeoVectorDataset
1129
+ Contrail waypoints at the current altitude layer with `rsr_overlap` and
1130
+ `olr_overlap` attached.
1131
+ """
1132
+ # Change in radiation fields
1133
+ lon_coords = delta_rad_t["longitude"].values
1134
+ wrap_longitude = (lon_coords[-1] + lon_coords[0] + np.diff(lon_coords)[0]) == 0.0
1135
+ mds = MetDataset(delta_rad_t, wrap_longitude=wrap_longitude, copy=False)
1136
+
1137
+ # Interpolate radiation fields to obtain `rsr_overlap` and `olr_overlap`
1138
+ delta_rsr = contrails_level.intersect_met(mds["rsr"])
1139
+ delta_olr = contrails_level.intersect_met(mds["olr"])
1140
+
1141
+ # Constrain RSR so it is not larger than the SDR
1142
+ contrails_level["rsr_overlap"] = np.minimum(
1143
+ contrails_level["sdr"],
1144
+ contrails_level["rsr"] + delta_rsr,
1145
+ )
1146
+
1147
+ # Constrain OLR so it is not smaller than 80% of the original value
1148
+ contrails_level["olr_overlap"] = np.maximum(
1149
+ 0.8 * contrails_level["olr"],
1150
+ contrails_level["olr"] + delta_olr,
1151
+ )
1152
+ return contrails_level
1153
+
1154
+
1155
+ def _local_sw_and_lw_rf_with_contrail_overlap(
1156
+ contrails_level: GeoVectorDataset,
1157
+ habit_distributions: npt.NDArray[np.float64],
1158
+ radius_threshold_um: npt.NDArray[np.float64],
1159
+ ) -> GeoVectorDataset:
1160
+ """
1161
+ Calculate local contrail SW and LW RF after accounting for contrail overlapping.
1162
+
1163
+ Parameters
1164
+ ----------
1165
+ contrails_level : GeoVectorDataset
1166
+ Contrail waypoints at the current altitude layer.
1167
+ habit_distributions : npt.NDArray[np.float64]
1168
+ Habit weight distributions.
1169
+ See :attr:`CocipParams().habit_distributions`
1170
+ radius_threshold_um : npt.NDArray[np.float64]
1171
+ Radius thresholds for habit distributions.
1172
+ See :attr:`CocipParams.radius_threshold_um`
1173
+
1174
+ Returns
1175
+ -------
1176
+ GeoVectorDataset
1177
+ Contrail waypoints at the current altitude layer with `rf_sw_overlap`,
1178
+ `rf_lw_overlap`, and `rf_net_overlap` attached.
1179
+ """
1180
+ r_vol_um = contrails_level["r_ice_vol"] * 1e6
1181
+ habit_w = habit_weights(r_vol_um, habit_distributions, radius_threshold_um)
1182
+
1183
+ # Calculate solar constant
1184
+ theta_rad = geo.orbital_position(contrails_level["time"])
1185
+ sd0 = geo.solar_constant(theta_rad)
1186
+ tau_contrail = contrails_level["tau_contrail"]
1187
+ tau_cirrus = contrails_level["tau_cirrus"] + contrails_level["tau_contrails_above"]
1188
+
1189
+ # Calculate local SW and LW RF
1190
+ contrails_level["rf_sw_overlap"] = shortwave_radiative_forcing(
1191
+ r_vol_um,
1192
+ contrails_level["sdr"],
1193
+ contrails_level["rsr_overlap"],
1194
+ sd0,
1195
+ tau_contrail,
1196
+ tau_cirrus,
1197
+ habit_w,
1198
+ )
1199
+
1200
+ contrails_level["rf_lw_overlap"] = longwave_radiative_forcing(
1201
+ r_vol_um,
1202
+ contrails_level["olr_overlap"],
1203
+ contrails_level["air_temperature"],
1204
+ tau_contrail,
1205
+ tau_cirrus,
1206
+ habit_w,
1207
+ )
1208
+ contrails_level["rf_net_overlap"] = (
1209
+ contrails_level["rf_lw_overlap"] + contrails_level["rf_sw_overlap"]
1210
+ )
1211
+ return contrails_level
1212
+
1213
+
1214
+ def _change_in_background_rsr_and_olr(
1215
+ contrails_level: GeoVectorDataset,
1216
+ delta_rad_t: xr.Dataset,
1217
+ *,
1218
+ spatial_bbox: tuple[float, float, float, float] = (-180.0, -90.0, 180.0, 90.0),
1219
+ spatial_grid_res: float = 0.5,
1220
+ ) -> xr.Dataset:
1221
+ r"""
1222
+ Calculate change in background RSR and OLR fields.
1223
+
1224
+ Parameters
1225
+ ----------
1226
+ contrails_level : GeoVectorDataset
1227
+ Contrail waypoints at the current altitude layer.
1228
+ delta_rad_t : xr.Dataset
1229
+ Radiation fields with cumulative change in RSR and OLR due to contrail overlapping.
1230
+ spatial_bbox: tuple[float, float, float, float]
1231
+ Spatial bounding box, ``(lon_min, lat_min, lon_max, lat_max)``, [:math:`\deg`]
1232
+ spatial_grid_res : float
1233
+ Spatial grid resolution, [:math:`\deg`]
1234
+
1235
+ Returns
1236
+ -------
1237
+ xr.Dataset
1238
+ Radiation fields with cumulative change in RSR and OLR due to contrail overlapping.
1239
+ """
1240
+ # Calculate SW and LW radiative flux (Units: W)
1241
+ segment_length = contrails_level["segment_length"]
1242
+ width = contrails_level["width"]
1243
+
1244
+ contrails_level["sw_radiative_flux"] = (
1245
+ np.abs(contrails_level["rf_sw_overlap"]) * segment_length * width
1246
+ )
1247
+
1248
+ contrails_level["lw_radiative_flux"] = contrails_level["rf_lw_overlap"] * segment_length * width
1249
+
1250
+ # Aggregate SW and LW radiative flux to a longitude-latitude grid
1251
+ ds = contrails_level.to_lon_lat_grid(
1252
+ agg={"sw_radiative_flux": "sum", "lw_radiative_flux": "sum"},
1253
+ spatial_bbox=spatial_bbox,
1254
+ spatial_grid_res=spatial_grid_res,
1255
+ )
1256
+ ds = ds.expand_dims(level=[-1.0], time=[contrails_level["time"][0]])
1257
+ da_surface_area = geo.grid_surface_area(ds["longitude"].values, ds["latitude"].values)
1258
+
1259
+ # Cumulative change in RSR and OLR
1260
+ delta_rad_t["rsr"] += ds["sw_radiative_flux"] / da_surface_area
1261
+ delta_rad_t["olr"] -= ds["lw_radiative_flux"] / da_surface_area
1262
+ return delta_rad_t