pycontrails 0.54.0__cp312-cp312-macosx_10_13_x86_64.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 +2314 -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-312-darwin.so +0 -0
  18. pycontrails/core/vector.py +2190 -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 +746 -0
  24. pycontrails/datalib/ecmwf/__init__.py +73 -0
  25. pycontrails/datalib/ecmwf/arco_era5.py +340 -0
  26. pycontrails/datalib/ecmwf/common.py +109 -0
  27. pycontrails/datalib/ecmwf/era5.py +550 -0
  28. pycontrails/datalib/ecmwf/era5_model_level.py +487 -0
  29. pycontrails/datalib/ecmwf/hres.py +782 -0
  30. pycontrails/datalib/ecmwf/hres_model_level.py +459 -0
  31. pycontrails/datalib/ecmwf/ifs.py +284 -0
  32. pycontrails/datalib/ecmwf/model_levels.py +434 -0
  33. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  34. pycontrails/datalib/ecmwf/variables.py +267 -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 +569 -0
  40. pycontrails/datalib/sentinel.py +511 -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 +430 -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 +982 -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 +2616 -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 +494 -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.54.0.dist-info/LICENSE +178 -0
  105. pycontrails-0.54.0.dist-info/METADATA +179 -0
  106. pycontrails-0.54.0.dist-info/NOTICE +43 -0
  107. pycontrails-0.54.0.dist-info/RECORD +109 -0
  108. pycontrails-0.54.0.dist-info/WHEEL +5 -0
  109. pycontrails-0.54.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,1517 @@
1
+ """Contrail Property Calculations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, TypeVar
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+
11
+ from pycontrails.models.cocip import radiative_heating
12
+ from pycontrails.physics import constants, thermo, units
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ ####################
17
+ # Initial Contrail Properties
18
+ ####################
19
+
20
+
21
+ def initial_iwc(
22
+ air_temperature: npt.NDArray[np.float64],
23
+ specific_humidity: npt.NDArray[np.float64],
24
+ air_pressure: npt.NDArray[np.float64],
25
+ fuel_dist: npt.NDArray[np.float64],
26
+ width: npt.NDArray[np.float64],
27
+ depth: npt.NDArray[np.float64],
28
+ ei_h2o: float,
29
+ ) -> npt.NDArray[np.float64]:
30
+ r"""
31
+ Estimate the initial contrail ice water content (iwc) before the wake vortex phase.
32
+
33
+ Note that the ice water content is replaced by zero if it is negative (dry air),
34
+ and this will end the contrail life-cycle in subsequent steps.
35
+
36
+ Parameters
37
+ ----------
38
+ air_temperature : npt.NDArray[np.float64]
39
+ ambient temperature for each waypoint, [:math:`K`]
40
+ specific_humidity : npt.NDArray[np.float64]
41
+ ambient specific humidity for each waypoint, [:math:`kg_{H_{2}O}/kg_{air}`]
42
+ air_pressure : npt.NDArray[np.float64]
43
+ initial pressure altitude at each waypoint, before the wake vortex phase, [:math:`Pa`]
44
+ fuel_dist : npt.NDArray[np.float64]
45
+ fuel consumption of the flight segment per distance traveled, [:math:`kg m^{-1}`]
46
+ width : npt.NDArray[np.float64]
47
+ initial contrail width, [:math:`m`]
48
+ depth : npt.NDArray[np.float64]
49
+ initial contrail depth, [:math:`m`]
50
+ ei_h2o : float
51
+ water vapor emissions index of fuel, [:math:`kg_{H_{2}O} \ kg_{fuel}^{-1}`]
52
+
53
+ Returns
54
+ -------
55
+ npt.NDArray[np.float64]
56
+ Initial contrail ice water content (iwc) at the original waypoint
57
+ before the wake vortex phase, [:math:`kg_{H_{2}O}/kg_{air}`].
58
+ Returns zero if iwc is is negative (dry air).
59
+ """
60
+ q_sat = thermo.q_sat_ice(air_temperature, air_pressure)
61
+ q_exhaust_ = q_exhaust(air_temperature, air_pressure, fuel_dist, width, depth, ei_h2o)
62
+ return np.maximum(q_exhaust_ + specific_humidity - q_sat, 0.0)
63
+
64
+
65
+ def q_exhaust(
66
+ air_temperature: npt.NDArray[np.float64],
67
+ air_pressure: npt.NDArray[np.float64],
68
+ fuel_dist: npt.NDArray[np.float64],
69
+ width: npt.NDArray[np.float64],
70
+ depth: npt.NDArray[np.float64],
71
+ ei_h2o: float,
72
+ ) -> npt.NDArray[np.float64]:
73
+ r"""
74
+ Calculate the specific humidity released by water vapor from aircraft emissions.
75
+
76
+ Parameters
77
+ ----------
78
+ air_temperature : npt.NDArray[np.float64]
79
+ ambient temperature for each waypoint, [:math:`K`]
80
+ air_pressure : npt.NDArray[np.float64]
81
+ initial pressure altitude at each waypoint, before the wake vortex phase, [:math:`Pa`]
82
+ fuel_dist : npt.NDArray[np.float64]
83
+ fuel consumption of the flight segment per distance travelled, [:math:`kg m^{-1}`]
84
+ width : npt.NDArray[np.float64]
85
+ initial contrail width, [:math:`m`]
86
+ depth : npt.NDArray[np.float64]
87
+ initial contrail depth, [:math:`m`]
88
+ ei_h2o : float
89
+ water vapor emissions index of fuel, [:math:`kg_{H_{2}O} \ kg_{fuel}^{-1}`]
90
+
91
+ Returns
92
+ -------
93
+ npt.NDArray[np.float64]
94
+ Humidity released by water vapour from aircraft emissions, [:math:`kg_{H_{2}O}/kg_{air}`]
95
+ """
96
+ rho_air = thermo.rho_d(air_temperature, air_pressure)
97
+ return (ei_h2o * fuel_dist) / ((np.pi / 4.0) * width * depth * rho_air)
98
+
99
+
100
+ def iwc_adiabatic_heating(
101
+ air_temperature: npt.NDArray[np.float64],
102
+ air_pressure: npt.NDArray[np.float64],
103
+ air_pressure_1: npt.NDArray[np.float64],
104
+ ) -> npt.NDArray[np.float64]:
105
+ """
106
+ Calculate the change in ice water content due to adiabatic heating from the wake vortex phase.
107
+
108
+ Parameters
109
+ ----------
110
+ air_temperature : npt.NDArray[np.float64]
111
+ ambient temperature for each waypoint, [:math:`K`]
112
+ air_pressure : npt.NDArray[np.float64]
113
+ initial pressure altitude at each waypoint, before the wake vortex phase, [:math:`Pa`]
114
+ air_pressure_1 : npt.NDArray[np.float64]
115
+ pressure altitude at each waypoint, after the wake vortex phase, [:math:`Pa`]
116
+
117
+ Returns
118
+ -------
119
+ npt.NDArray[np.float64]
120
+ Change in ice water content due to adiabatic heating from the wake
121
+ vortex phase, [:math:`kg_{H_{2}O}/kg_{air}`]
122
+ """
123
+ p_ice = thermo.e_sat_ice(air_temperature)
124
+ air_temperature_1 = temperature_adiabatic_heating(air_temperature, air_pressure, air_pressure_1)
125
+ p_ice_1 = thermo.e_sat_ice(air_temperature_1)
126
+
127
+ out = (constants.R_d / constants.R_v) * ((p_ice_1 / air_pressure_1) - (p_ice / air_pressure))
128
+ out.clip(min=0.0, out=out)
129
+ return out
130
+
131
+
132
+ def temperature_adiabatic_heating(
133
+ air_temperature: npt.NDArray[np.float64],
134
+ air_pressure: npt.NDArray[np.float64],
135
+ air_pressure_1: npt.NDArray[np.float64],
136
+ ) -> npt.NDArray[np.float64]:
137
+ """Calculate the ambient air temperature for each waypoint after the wake vortex phase.
138
+
139
+ This calculation accounts for adiabatic heating.
140
+
141
+ Parameters
142
+ ----------
143
+ air_temperature : npt.NDArray[np.float64]
144
+ ambient temperature for each waypoint, [:math:`K`]
145
+ air_pressure : npt.NDArray[np.float64]
146
+ initial pressure altitude at each waypoint, before the wake vortex phase, [:math:`Pa`]
147
+ air_pressure_1 : npt.NDArray[np.float64]
148
+ pressure altitude at each waypoint, after the wake vortex phase, [:math:`Pa`]
149
+
150
+ Returns
151
+ -------
152
+ npt.NDArray[np.float64]
153
+ ambient air temperature after the wake vortex phase, [:math:`K`]
154
+
155
+ Notes
156
+ -----
157
+ Level 1, see Figure 1 of :cite:`schumannContrailCirrusPrediction2012`
158
+
159
+ References
160
+ ----------
161
+ - :cite:`schumannContrailCirrusPrediction2012`
162
+ """
163
+ exponent = (constants.gamma - 1.0) / constants.gamma
164
+ return air_temperature * (air_pressure_1 / air_pressure) ** exponent
165
+
166
+
167
+ def iwc_post_wake_vortex(
168
+ iwc: npt.NDArray[np.float64], iwc_ad: npt.NDArray[np.float64]
169
+ ) -> npt.NDArray[np.float64]:
170
+ """
171
+ Calculate the ice water content after the wake vortex phase (``iwc_1``).
172
+
173
+ ``iwc_1`` is calculated by subtracting the initial iwc before the wake vortex phase (``iwc``)
174
+ by the change in iwc from adiabatic heating experienced during the wake vortex phase.
175
+
176
+ Note that the iwc is replaced by zero if it is negative (dry air),
177
+ and this will end the contrail lifecycle in subsequent steps.
178
+
179
+ Parameters
180
+ ----------
181
+ iwc : npt.NDArray[np.float64]
182
+ initial ice water content at each waypoint before the wake vortex
183
+ phase, [:math:`kg_{H_{2}O}/kg_{air}`]
184
+ iwc_ad : npt.NDArray[np.float64]
185
+ change in iwc from adiabatic heating during the wake vortex
186
+ phase, [:math:`kg_{H_{2}O}/kg_{air}`]
187
+
188
+ Returns
189
+ -------
190
+ npt.NDArray[np.float64]
191
+ ice water content after the wake vortex phase, ``iwc_1``, [:math:`kg_{H_{2}O}/kg_{air}`]
192
+
193
+ Notes
194
+ -----
195
+ Level 1, see Figure 1 of :cite:`schumannContrailCirrusPrediction2012`
196
+
197
+ References
198
+ ----------
199
+ - :cite:`schumannContrailCirrusPrediction2012`
200
+ """
201
+ return np.maximum(iwc - iwc_ad, 0.0)
202
+
203
+
204
+ def ice_particle_number(
205
+ nvpm_ei_n: npt.NDArray[np.float64],
206
+ fuel_dist: npt.NDArray[np.float64],
207
+ f_surv: npt.NDArray[np.float64],
208
+ air_temperature: npt.NDArray[np.float64],
209
+ T_crit_sac: npt.NDArray[np.float64],
210
+ min_ice_particle_number_nvpm_ei_n: float,
211
+ ) -> npt.NDArray[np.float64]:
212
+ """Calculate the initial number of ice particles per distance after the wake vortex phase.
213
+
214
+ The initial number of ice particle per distance is calculated from the black
215
+ carbon number emissions index ``nvpm_ei_n`` and fuel burn per distance ``fuel_dist``.
216
+ Note that a lower bound for ``nvpm_ei_n`` is set at ``1e13`` :math:`kg^{-1}` to account
217
+ for the activation of ambient aerosol particles and organic volatile particles.
218
+
219
+ Parameters
220
+ ----------
221
+ nvpm_ei_n : npt.NDArray[np.float64]
222
+ black carbon number emissions index, [:math:`kg^{-1}`]
223
+ fuel_dist : npt.NDArray[np.float64]
224
+ fuel consumption of the flight segment per distance traveled, [:math:`kg m^{-1}`]
225
+ f_surv : npt.NDArray[np.float64]
226
+ Fraction of contrail ice particle number that survive the wake vortex phase.
227
+ air_temperature : npt.NDArray[np.float64]
228
+ ambient temperature for each waypoint, [:math:`K`]
229
+ T_crit_sac : npt.NDArray[np.float64]
230
+ estimated Schmidt-Appleman temperature threshold for contrail formation, [:math:`K`]
231
+ min_ice_particle_number_nvpm_ei_n : float
232
+ lower bound for nvpm_ei_n to account for ambient aerosol particles for
233
+ newer engines [:math:`kg^{-1}`]
234
+
235
+ Returns
236
+ -------
237
+ npt.NDArray[np.float64]
238
+ initial number of ice particles per distance after the wake vortex phase, [:math:`# m^{-1}`]
239
+ """
240
+ f_activation = ice_particle_activation_rate(air_temperature, T_crit_sac)
241
+ nvpm_ei_n_activated = nvpm_ei_n * f_activation
242
+ return fuel_dist * np.maximum(nvpm_ei_n_activated, min_ice_particle_number_nvpm_ei_n) * f_surv
243
+
244
+
245
+ def ice_particle_activation_rate(
246
+ air_temperature: npt.NDArray[np.float64], T_crit_sac: npt.NDArray[np.float64]
247
+ ) -> npt.NDArray[np.float64]:
248
+ """
249
+ Calculate the activation rate of black carbon particles to contrail ice crystals.
250
+
251
+ The activation rate is calculated as a function of the difference between
252
+ the ambient temperature and the Schmidt-Appleman threshold temperature ``T_crit_sac``.
253
+
254
+ Parameters
255
+ ----------
256
+ air_temperature : npt.NDArray[np.float64]
257
+ ambient temperature at each waypoint before wake_wortex, [:math:`K`]
258
+ T_crit_sac : npt.NDArray[np.float64]
259
+ estimated Schmidt-Appleman temperature threshold for contrail formation, [:math:`K`]
260
+
261
+ Returns
262
+ -------
263
+ npt.NDArray[np.float64]
264
+ Proportion of black carbon particles that activates to contrail ice parties.
265
+
266
+ Notes
267
+ -----
268
+ The equation is not published but based on the raw data
269
+ from :cite:`brauerAirborneMeasurementsContrail2021`.
270
+
271
+ References
272
+ ----------
273
+ - :cite:`brauerAirborneMeasurementsContrail2021`
274
+ """
275
+ d_temp = air_temperature - T_crit_sac
276
+ d_temp.clip(None, 0.0, out=d_temp)
277
+
278
+ # NOTE: It seems somewhat unnecessary to do this additional "rounding down"
279
+ # of d_temp for values below -5. As d_temp near -5, the activation rate approaches
280
+ # 1. This additional rounding injects a small jump discontinuity into the activation rate that
281
+ # likely does not match reality. I suggest removing the line below. This will change
282
+ # model outputs roughly at 0.001 - 0.1%.
283
+ d_temp[d_temp < -5.0] = -np.inf
284
+ return -0.661 * np.exp(d_temp) + 1.0
285
+
286
+
287
+ def ice_particle_survival_fraction(
288
+ iwc: npt.NDArray[np.float64], iwc_1: npt.NDArray[np.float64]
289
+ ) -> npt.NDArray[np.float64]:
290
+ """
291
+ Estimate the fraction of contrail ice particle number that survive the wake vortex phase.
292
+
293
+ CoCiP assumes that this fraction is proportional to the change in ice water content
294
+ (``iwc_1 - iwc``) before and after the wake vortex phase.
295
+
296
+ Parameters
297
+ ----------
298
+ iwc : npt.NDArray[np.float64]
299
+ initial ice water content at each waypoint before the wake vortex
300
+ phase, [:math:`kg_{H_{2}O}/kg_{air}`]
301
+ iwc_1 : npt.NDArray[np.float64]
302
+ ice water content after the wake vortex phase, [:math:`kg_{H_{2}O}/kg_{air}`]
303
+
304
+ Returns
305
+ -------
306
+ npt.NDArray[np.float64]
307
+ Fraction of contrail ice particle number that survive the wake vortex phase.
308
+ """
309
+ f_surv = np.empty_like(iwc)
310
+ is_positive = (iwc > 0.0) & (iwc_1 > 0.0)
311
+
312
+ ratio = iwc_1[is_positive] / iwc[is_positive]
313
+ ratio.clip(None, 1.0, out=ratio)
314
+
315
+ f_surv[is_positive] = ratio
316
+ f_surv[~is_positive] = 0.5
317
+
318
+ return f_surv
319
+
320
+
321
+ def initial_persistent(
322
+ iwc_1: npt.NDArray[np.float64], rhi_1: npt.NDArray[np.float64]
323
+ ) -> npt.NDArray[np.float64]:
324
+ """
325
+ Determine if waypoints have persistent contrails.
326
+
327
+ Conditions for persistent initial_contrails:
328
+
329
+ 1. ice water content at level 1: ``1e-12 < iwc < 1e10``
330
+ 2. rhi at level 1: ``0 < rhi < 1e10``
331
+
332
+ .. versionchanged:: 0.25.1
333
+ Returned array now has floating dtype. This is consistent with other filtering
334
+ steps in the CoCiP model (ie, ``sac``).
335
+
336
+ Parameters
337
+ ----------
338
+ iwc_1 : npt.NDArray[np.float64]
339
+ ice water content after the wake vortex phase, [:math:`kg_{H_{2}O}/kg_{air}`]
340
+ rhi_1 : npt.NDArray[np.float64]
341
+ relative humidity with respect to ice after the wake vortex phase
342
+
343
+ Returns
344
+ -------
345
+ npt.NDArray[np.float64]
346
+ Mask of waypoints with persistent contrails. Waypoints with persistent contrails
347
+ will have value 1.
348
+
349
+ Notes
350
+ -----
351
+ The RHi at level 1 does not need to be above 100% if the iwc > 0 kg/kg. If iwc > 0
352
+ and RHi < 100%, the contrail lifetime will not end immediately as the ice particles
353
+ will gradually evaporate with the rate depending on the background RHi.
354
+ """
355
+ out = (iwc_1 > 1e-12) & (iwc_1 < 1e10) & (rhi_1 > 0.0) & (rhi_1 < 1e10)
356
+ dtype = np.result_type(iwc_1, rhi_1)
357
+ return out.astype(dtype)
358
+
359
+
360
+ def contrail_persistent(
361
+ latitude: npt.NDArray[np.float64],
362
+ altitude: npt.NDArray[np.float64],
363
+ segment_length: npt.NDArray[np.float64],
364
+ age: npt.NDArray[np.timedelta64],
365
+ tau_contrail: npt.NDArray[np.float64],
366
+ n_ice_per_m3: npt.NDArray[np.float64],
367
+ params: dict[str, Any],
368
+ ) -> npt.NDArray[np.bool_]:
369
+ r"""
370
+ Determine surviving contrail segments after time integration step.
371
+
372
+ A contrail waypoint reaches its end of life if any of the following conditions hold:
373
+
374
+ 1. Contrail age exceeds ``max_age``
375
+ 2. Contrail optical depth lies outside of interval ``[min_tau, max_tau]``
376
+ 3. Ice number density lies outside of interval ``[min_n_ice_per_m3, max_n_ice_per_m3]``
377
+ 4. Altitude lies outside of the interval ``[min_altitude_m, max_altitude_m]``
378
+ 5. Segment length exceeds ``max_seg_length_m``
379
+ 6. Latitude values are within 1 degree of the north or south pole
380
+
381
+ This function warns if all values in the ``tau_contrail`` array are nan.
382
+
383
+ .. versionchanged:: 0.25.10
384
+
385
+ Extreme values of ``latitude`` (ie, close to the north or south pole) now
386
+ create an end of life condition. This check helps address issues related to
387
+ divergence in the polar regions. (With large enough integration time delta,
388
+ it is still possible for post-advection latitude values to lie outside of
389
+ [-90, 90], but this is no longer possible with typical parameters and wind
390
+ speeds.)
391
+
392
+ Parameters
393
+ ----------
394
+ latitude : npt.NDArray[np.float64]
395
+ Contrail latitude, [:math:`\deg`]
396
+ altitude : npt.NDArray[np.float64]
397
+ Contrail altitude, [:math:`m`]
398
+ segment_length : npt.NDArray[np.float64]
399
+ Contrail segment length, [:math:`m`]
400
+ age : npt.NDArray[np.timedelta64]
401
+ Contrail age
402
+ tau_contrail : npt.NDArray[np.float64]
403
+ Contrail optical depth
404
+ n_ice_per_m3 : npt.NDArray[np.float64]
405
+ Contrail ice particle number per volume of air, [:math:`# m^{-3}`]
406
+ params : dict[str, Any]
407
+ Dictionary of :class:`CocipParams` parameters determining the
408
+ conditions for end of contrail life.
409
+
410
+ Returns
411
+ -------
412
+ npt.NDArray[np.bool_]
413
+ Boolean array indicating surviving contrails. Persisting contrails
414
+ will be marked as True.
415
+ """
416
+ status_1 = _within_range(age, max=params["max_age"])
417
+ status_2 = _within_range(tau_contrail, max=params["max_tau"], min=params["min_tau"])
418
+ status_3 = _within_range(
419
+ n_ice_per_m3, max=params["max_n_ice_per_m3"], min=params["min_n_ice_per_m3"]
420
+ )
421
+ status_4 = _within_range(altitude, max=params["max_altitude_m"], min=params["min_altitude_m"])
422
+ status_5 = _within_range(segment_length, max=params["max_seg_length_m"])
423
+ status_6 = _within_range(latitude, max=89.0, min=-89.0) # type: ignore[type-var]
424
+
425
+ logger.debug(
426
+ "Survival stats. age: %s, tau: %s, ice: %s, altitude: %s, segment: %s, latitude: %s",
427
+ np.sum(status_1),
428
+ np.sum(status_2),
429
+ np.sum(status_3),
430
+ np.sum(status_4),
431
+ np.sum(status_5),
432
+ np.sum(status_6),
433
+ )
434
+ return status_1 & status_2 & status_3 & status_4 & status_5 & status_6
435
+
436
+
437
+ T = TypeVar("T", np.float64, np.timedelta64)
438
+
439
+
440
+ def _within_range(
441
+ val: npt.NDArray[T],
442
+ max: npt.NDArray[T] | T | None = None,
443
+ min: npt.NDArray[T] | T | None = None,
444
+ ) -> npt.NDArray[np.bool_]:
445
+ """
446
+ Check if the input values (val) are each within the specified range.
447
+
448
+ If both ``max`` and ``min`` are None, a literal constant True is returned.
449
+
450
+ Parameters
451
+ ----------
452
+ val : np.ndarray
453
+ value of selected contrail property
454
+ max : np.ndarray | float | np.timedelta64 | None, optional
455
+ Upper bound. If None, no upper bound is imposed. None by default.
456
+ min : np.ndarray | float | np.timedelta64 | None, optional
457
+ Lower bound. If None, no lower bound is imposed. None by default.
458
+
459
+ Returns
460
+ -------
461
+ npt.NDArray[np.bool_]
462
+ Mask of waypoints. Waypoints with values within the specified range will be marked as true.
463
+ """
464
+ cond: npt.NDArray[np.bool_] = True # type: ignore[assignment]
465
+ if min is not None:
466
+ cond &= val >= min
467
+ if max is not None:
468
+ cond &= val <= max
469
+ return cond
470
+
471
+
472
+ ####################
473
+ # Contrail Properties
474
+ ####################
475
+
476
+
477
+ def contrail_edges(
478
+ lon: npt.NDArray[np.float64],
479
+ lat: npt.NDArray[np.float64],
480
+ sin_a: npt.NDArray[np.float64],
481
+ cos_a: npt.NDArray[np.float64],
482
+ width: npt.NDArray[np.float64],
483
+ ) -> tuple[
484
+ npt.NDArray[np.float64],
485
+ npt.NDArray[np.float64],
486
+ npt.NDArray[np.float64],
487
+ npt.NDArray[np.float64],
488
+ ]:
489
+ """
490
+ Calculate the longitude and latitude of the contrail edges to account for contrail spreading.
491
+
492
+ (lon_edge_l, lat_edge_l) x---------------------
493
+
494
+ (Contrail midpoint: lon, lat) X===================== ->
495
+
496
+ (lon_edge_r, lat_edge_r) x---------------------
497
+
498
+
499
+ Parameters
500
+ ----------
501
+ lon : npt.NDArray[np.float64]
502
+ longitude of contrail waypoint, degrees
503
+ lat : npt.NDArray[np.float64]
504
+ latitude of contrail waypoint, degrees
505
+ sin_a : npt.NDArray[np.float64]
506
+ sin(a), where a is the angle between the plume and the longitudinal axis
507
+ cos_a : npt.NDArray[np.float64]
508
+ cos(a), where a is the angle between the plume and the longitudinal axis
509
+ width : npt.NDArray[np.float64]
510
+ contrail width at each waypoint, [:math:`m`]
511
+
512
+ Returns
513
+ -------
514
+ tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]
515
+ (lon_edge_l, lat_edge_l, lon_edge_r, lat_edge_r), longitudes and latitudes
516
+ at the left and right edges of the contrail, degrees
517
+ """ # noqa: E501
518
+ dlon = units.m_to_longitude_distance(width * sin_a * 0.5, lat)
519
+ dlat = units.m_to_latitude_distance(width * cos_a * 0.5)
520
+
521
+ lon_edge_l = lon - dlon
522
+ lat_edge_l = lat + dlat
523
+ lon_edge_r = lon + dlon
524
+ lat_edge_r = lat - dlat
525
+
526
+ return lon_edge_l, lat_edge_l, lon_edge_r, lat_edge_r
527
+
528
+
529
+ def contrail_vertices(
530
+ lon: npt.NDArray[np.float64],
531
+ lat: npt.NDArray[np.float64],
532
+ sin_a: npt.NDArray[np.float64],
533
+ cos_a: npt.NDArray[np.float64],
534
+ width: npt.NDArray[np.float64],
535
+ segment_length: npt.NDArray[np.float64],
536
+ ) -> tuple[
537
+ npt.NDArray[np.float64],
538
+ npt.NDArray[np.float64],
539
+ npt.NDArray[np.float64],
540
+ npt.NDArray[np.float64],
541
+ npt.NDArray[np.float64],
542
+ npt.NDArray[np.float64],
543
+ npt.NDArray[np.float64],
544
+ npt.NDArray[np.float64],
545
+ ]:
546
+ """
547
+ Calculate the longitude and latitude of the contrail vertices.
548
+
549
+ This is equivalent to running :meth:`contrail_edges` at each contrail waypoint
550
+ and associating the next continuous waypoint with the previous.
551
+ This method is helpful when you want to treat each contrail waypoint independently.
552
+
553
+ (lon_1, lat_1) x--------------------x (lon_4, lat_4)
554
+
555
+ (Contrail waypoint: lon, lat) X==================== ->
556
+
557
+ (lon_2, lat_2) x--------------------x (lon_3, lat_3)
558
+
559
+
560
+ Parameters
561
+ ----------
562
+ lon : npt.NDArray[np.float64]
563
+ longitude of contrail waypoint, degrees
564
+ lat : npt.NDArray[np.float64]
565
+ latitude of contrail waypoint, degrees
566
+ sin_a : npt.NDArray[np.float64]
567
+ sin(a), where a is the angle between the plume and the longitudinal axis
568
+ cos_a : npt.NDArray[np.float64]
569
+ cos(a), where a is the angle between the plume and the longitudinal axis
570
+ width : npt.NDArray[np.float64]
571
+ contrail width at each waypoint, [:math:`m`]
572
+ segment_length : npt.NDArray[np.float64]
573
+ contrail length at each waypoint, [:math:`m`]
574
+
575
+ Returns
576
+ -------
577
+ tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]
578
+ (lon_1, lat_1, lon_2, lat_2, lon_3, lat_3, lon_4, lat_4) degrees
579
+ """ # noqa: E501
580
+ dlon_width = units.m_to_longitude_distance(width * sin_a * 0.5, lat)
581
+ dlat_width = units.m_to_latitude_distance(width * cos_a * 0.5)
582
+
583
+ # using "lat" as mean here is a little inaccurate, but its a close approx
584
+ dlon_length = units.m_to_longitude_distance(segment_length * cos_a, lat)
585
+ dlat_length = units.m_to_latitude_distance(segment_length * sin_a)
586
+
587
+ lon_1 = lon - dlon_width
588
+ lon_2 = lon + dlon_width
589
+ lon_3 = lon + dlon_width + dlon_length
590
+ lon_4 = lon - dlon_width + dlon_length
591
+
592
+ lat_1 = lat + dlat_width
593
+ lat_2 = lat - dlat_width
594
+ lat_3 = lat - dlat_width + dlat_length
595
+ lat_4 = lat + dlat_width + dlat_length
596
+
597
+ return lon_1, lat_1, lon_2, lat_2, lon_3, lat_3, lon_4, lat_4
598
+
599
+
600
+ def plume_effective_cross_sectional_area(
601
+ width: npt.NDArray[np.float64],
602
+ depth: npt.NDArray[np.float64],
603
+ sigma_yz: npt.NDArray[np.float64] | float,
604
+ ) -> npt.NDArray[np.float64]:
605
+ """
606
+ Calculate the effective cross-sectional area of the contrail plume (``area_eff``).
607
+
608
+ ``sigma_yy``, ``sigma_zz`` and ``sigma_yz`` are the parameters governing the
609
+ contrail plume's temporal evolution.
610
+
611
+ Parameters
612
+ ----------
613
+ width : npt.NDArray[np.float64]
614
+ contrail width at each waypoint, [:math:`m`]
615
+ depth : npt.NDArray[np.float64]
616
+ contrail depth at each waypoint, [:math:`m`]
617
+ sigma_yz : npt.NDArray[np.float64] | float
618
+ temporal evolution of the contrail plume parameters
619
+
620
+ Returns
621
+ -------
622
+ npt.NDArray[np.float64]
623
+ effective cross-sectional area of the contrail plume, [:math:`m^{2}`]
624
+ """
625
+ sigma_yy = 0.125 * (width**2)
626
+ sigma_zz = 0.125 * (depth**2)
627
+ return new_effective_area_from_sigma(sigma_yy, sigma_zz, sigma_yz)
628
+
629
+
630
+ def plume_effective_depth(
631
+ width: npt.NDArray[np.float64], area_eff: npt.NDArray[np.float64]
632
+ ) -> npt.NDArray[np.float64]:
633
+ """
634
+ Calculate the effective depth of the contrail plume (``depth_eff``).
635
+
636
+ ``depth_eff`` is calculated from the effective cross-sectional area (``area_eff``)
637
+ and the contrail width.
638
+
639
+ Parameters
640
+ ----------
641
+ width : npt.NDArray[np.float64]
642
+ contrail width at each waypoint, [:math:`m`]
643
+ area_eff : npt.NDArray[np.float64]
644
+ effective cross-sectional area of the contrail plume, [:math:`m^{2}`]
645
+
646
+ Returns
647
+ -------
648
+ npt.NDArray[np.float64]
649
+ effective depth of the contrail plume, [:math:`m`]
650
+ """
651
+ return area_eff / width
652
+
653
+
654
+ def plume_mass_per_distance(
655
+ area_eff: npt.NDArray[np.float64], rho_air: npt.NDArray[np.float64]
656
+ ) -> npt.NDArray[np.float64]:
657
+ """
658
+ Calculate the contrail plume mass per unit length.
659
+
660
+ Parameters
661
+ ----------
662
+ area_eff : npt.NDArray[np.float64]
663
+ effective cross-sectional area of the contrail plume, [:math:`m^{2}`]
664
+ rho_air : npt.NDArray[np.float64]
665
+ density of air for each waypoint, [:math:`kg m^{-3}`]
666
+
667
+ Returns
668
+ -------
669
+ npt.NDArray[np.float64]
670
+ contrail plume mass per unit length, [:math:`kg m^{-1}`]
671
+ """
672
+ return area_eff * rho_air
673
+
674
+
675
+ def ice_particle_number_per_volume_of_plume(
676
+ n_ice_per_m: npt.NDArray[np.float64], area_eff: npt.NDArray[np.float64]
677
+ ) -> npt.NDArray[np.float64]:
678
+ """
679
+ Calculate the number of contrail ice particles per volume of plume (``n_ice_per_vol``).
680
+
681
+ Parameters
682
+ ----------
683
+ n_ice_per_m : npt.NDArray[np.float64]
684
+ number of ice particles per distance at time t, [:math:`m^{-1}`]
685
+ area_eff : npt.NDArray[np.float64]
686
+ effective cross-sectional area of the contrail plume, [:math:`m^{2}`]
687
+
688
+ Returns
689
+ -------
690
+ npt.NDArray[np.float64]
691
+ number of ice particles per volume of contrail plume at time t, [:math:`# m^{-3}`]
692
+ """
693
+ return n_ice_per_m / area_eff
694
+
695
+
696
+ def ice_particle_number_per_mass_of_air(
697
+ n_ice_per_vol: npt.NDArray[np.float64], rho_air: npt.NDArray[np.float64]
698
+ ) -> npt.NDArray[np.float64]:
699
+ """
700
+ Calculate the number of contrail ice particles per mass of air.
701
+
702
+ Parameters
703
+ ----------
704
+ n_ice_per_vol : npt.NDArray[np.float64]
705
+ number of ice particles per volume of contrail plume at time t, [:math:`# m^{-3}`]
706
+ rho_air : npt.NDArray[np.float64]
707
+ density of air for each waypoint, [:math:`kg m^{-3}`]
708
+
709
+ Returns
710
+ -------
711
+ npt.NDArray[np.float64]
712
+ number of ice particles per mass of air at time t, [:math:`# kg^{-1}`]
713
+ """
714
+ return n_ice_per_vol / rho_air
715
+
716
+
717
+ def ice_particle_volume_mean_radius(
718
+ iwc: npt.NDArray[np.float64], n_ice_per_kg_air: npt.NDArray[np.float64]
719
+ ) -> npt.NDArray[np.float64]:
720
+ """
721
+ Calculate the ice particle volume mean radius.
722
+
723
+ Parameters
724
+ ----------
725
+ iwc : npt.NDArray[np.float64]
726
+ contrail ice water content, i.e., contrail ice mass per
727
+ kg of air, [:math:`kg_{H_{2}O}/kg_{air}`]
728
+
729
+ n_ice_per_kg_air : npt.NDArray[np.float64]
730
+ number of ice particles per mass of air, [:math:`# kg^{-1}`]
731
+
732
+ Returns
733
+ -------
734
+ npt.NDArray[np.float64]
735
+ ice particle volume mean radius, [:math:`m`]
736
+
737
+ Notes
738
+ -----
739
+ ``r_ice_vol`` is the mean radius of a sphere that has the same volume as the
740
+ contrail ice particle.
741
+
742
+ ``r_ice_vol`` calculated by dividing the total volume of contrail
743
+ ice particle per kg of air (``total_ice_volume``, :math:`m**3/kg-air`) with the
744
+ number of contrail ice particles per kg of air (``n_ice_per_kg_air``, :math:`#/kg-air`).
745
+ """
746
+ total_ice_volume = iwc / constants.rho_ice
747
+ r_ice_vol = ((3 / (4.0 * np.pi)) * (total_ice_volume / n_ice_per_kg_air)) ** (1 / 3)
748
+ zero_negative_values = iwc <= 0.0
749
+ r_ice_vol[zero_negative_values] = iwc[zero_negative_values]
750
+ r_ice_vol.clip(min=1e-10, out=r_ice_vol)
751
+ return r_ice_vol
752
+
753
+
754
+ def ice_particle_terminal_fall_speed(
755
+ air_pressure: npt.NDArray[np.float64],
756
+ air_temperature: npt.NDArray[np.float64],
757
+ r_ice_vol: npt.NDArray[np.float64],
758
+ ) -> npt.NDArray[np.float64]:
759
+ """
760
+ Calculate the terminal fall speed of contrail ice particles.
761
+
762
+ ``v_t`` is calculated based on a parametric model
763
+ from :cite:`spichtingerModellingCirrusClouds2009`, using inputs of pressure
764
+ level, ambient temperature and the ice particle volume mean radius. See
765
+ Table 2 for the model parameters.
766
+
767
+ Parameters
768
+ ----------
769
+ air_pressure : npt.NDArray[np.float64]
770
+ Pressure altitude at each waypoint, [:math:`Pa`]
771
+ air_temperature : npt.NDArray[np.float64]
772
+ Ambient temperature for each waypoint, [:math:`K`]
773
+ r_ice_vol : npt.NDArray[np.float64]
774
+ Ice particle volume mean radius, [:math:`m`]
775
+
776
+ Returns
777
+ -------
778
+ npt.NDArray[np.float64]
779
+ Terminal fall speed of contrail ice particles, [:math:`m s^{-1}`]
780
+
781
+ References
782
+ ----------
783
+ - :cite:`spichtingerModellingCirrusClouds2009`
784
+ """
785
+ ipm = ice_particle_mass(r_ice_vol)
786
+
787
+ alpha = np.full_like(r_ice_vol, np.nan)
788
+
789
+ # For ice particle mass >= 4.264e-8 kg
790
+ particle_mass = ipm >= 4.264e-8
791
+ alpha[particle_mass] = 8.80 * ipm[particle_mass] ** 0.096
792
+
793
+ # For ice particle mass in [2.166e-9 kg, 4.264e-8 kg)
794
+ particle_mass = (ipm < 4.264e-8) & (ipm >= 2.166e-9)
795
+ alpha[particle_mass] = 329.8 * ipm[particle_mass] ** 0.31
796
+
797
+ # For ice particle mass in [2.146e-13 kg, 2.166e-9 kg)
798
+ particle_mass = (ipm < 2.166e-9) & (ipm >= 2.146e-13)
799
+ alpha[particle_mass] = 63292.4 * ipm[particle_mass] ** 0.57
800
+
801
+ # For ice particle mass < 2.146e-13 kg
802
+ particle_mass = ipm < 2.146e-13
803
+ alpha[particle_mass] = 735.4 * ipm[particle_mass] ** 0.42
804
+
805
+ return alpha * (30000.0 / air_pressure) ** 0.178 * (233.0 / air_temperature) ** 0.394
806
+
807
+
808
+ def ice_particle_mass(r_ice_vol: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
809
+ """
810
+ Calculate the contrail ice particle mass.
811
+
812
+ It is calculated by multiplying the mean ice particle volume with the density of ice
813
+
814
+ Parameters
815
+ ----------
816
+ r_ice_vol : npt.NDArray[np.float64]
817
+ Ice particle volume mean radius, [:math:`m`]
818
+
819
+ Returns
820
+ -------
821
+ npt.NDArray[np.float64]
822
+ Mean contrail ice particle mass, [:math:`kg`]
823
+ """
824
+ return ((4 / 3) * np.pi * r_ice_vol**3) * constants.rho_ice
825
+
826
+
827
+ def horizontal_diffusivity(
828
+ ds_dz: npt.NDArray[np.float64], depth: npt.NDArray[np.float64]
829
+ ) -> npt.NDArray[np.float64]:
830
+ """
831
+ Calculate contrail horizontal diffusivity.
832
+
833
+ Parameters
834
+ ----------
835
+ ds_dz : npt.NDArray[np.float64]
836
+ Total wind shear (eastward and northward winds) with respect
837
+ to altitude (``dz``), [:math:`m s^{-1} / Pa`]
838
+ depth : npt.NDArray[np.float64]
839
+ Contrail depth at each waypoint, [:math:`m`]
840
+
841
+ Returns
842
+ -------
843
+ npt.NDArray[np.float64]
844
+ horizontal diffusivity, [:math:`m^{2} s^{-1}`]
845
+
846
+ References
847
+ ----------
848
+ - :cite:`schumannContrailCirrusPrediction2012`
849
+
850
+ Notes
851
+ -----
852
+ Accounts for the turbulence-induced diffusive contrail spreading in
853
+ the horizontal direction.
854
+ """
855
+ return 0.1 * ds_dz * depth**2
856
+
857
+
858
+ def vertical_diffusivity(
859
+ air_pressure: npt.NDArray[np.float64],
860
+ air_temperature: npt.NDArray[np.float64],
861
+ dT_dz: npt.NDArray[np.float64],
862
+ depth_eff: npt.NDArray[np.float64],
863
+ terminal_fall_speed: npt.NDArray[np.float64] | float,
864
+ sedimentation_impact_factor: npt.NDArray[np.float64] | float,
865
+ eff_heat_rate: npt.NDArray[np.float64] | None,
866
+ ) -> npt.NDArray[np.float64]:
867
+ """
868
+ Calculate contrail vertical diffusivity.
869
+
870
+ Parameters
871
+ ----------
872
+ air_pressure : npt.NDArray[np.float64]
873
+ Pressure altitude at each waypoint, [:math:`Pa`]
874
+ air_temperature : npt.NDArray[np.float64]
875
+ Ambient temperature for each waypoint, [:math:`K`]
876
+ dT_dz : npt.NDArray[np.float64]
877
+ Temperature gradient with respect to altitude (dz), [:math:`K m^{-1}`]
878
+ depth_eff : npt.NDArray[np.float64]
879
+ Effective depth of the contrail plume, [:math:`m`]
880
+ terminal_fall_speed : npt.NDArray[np.float64]
881
+ Terminal fall speed of contrail ice particles, [:math:`m s^{-1}`]
882
+ sedimentation_impact_factor : float
883
+ Enhancement parameter denoted by `f_T` in eq. (35) Schumann (2012).
884
+ eff_heat_rate: npt.NDArray[np.float64] | None
885
+ Effective heating rate, i.e., rate of which the contrail plume
886
+ is heated, [:math:`K s^{-1}`]. If None is passed, the radiative
887
+ heating effects on contrail cirrus properties are not included.
888
+
889
+ Returns
890
+ -------
891
+ npt.NDArray[np.float64]
892
+ vertical diffusivity, [:math:`m^{2} s^{-1}`]
893
+
894
+ References
895
+ ----------
896
+ - :cite:`schumannContrailCirrusPrediction2012`
897
+ - :cite:`schumannAviationinducedCirrusRadiation2013`
898
+
899
+ Notes
900
+ -----
901
+ Accounts for the turbulence-induced diffusive contrail spreading in the vertical direction.
902
+ See eq. (35) of :cite:`schumannContrailCirrusPrediction2012`.
903
+
904
+ The first term in Eq. (35) of :cite:`schumannContrailCirrusPrediction2012` is
905
+ (c_V * w'_N^2 / N_BV, where c_V = 0.2 and w'_N^2 = 0.1) is different
906
+ than outlined below. Here, a constant of 0.01 is used when radiative
907
+ heating effects are not activated. This update comes from
908
+ :cite:`schumannAviationinducedCirrusRadiation2013`
909
+ , which found that the original formulation estimated thinner
910
+ contrails relative to satellite observations. The vertical diffusivity
911
+ was enlarged so that the simulated contrails are more consistent with observations.
912
+ """
913
+ n_bv = thermo.brunt_vaisala_frequency(air_pressure, air_temperature, dT_dz)
914
+ n_bv.clip(min=0.001, out=n_bv)
915
+
916
+ cvs: npt.NDArray[np.float64] | float
917
+ if eff_heat_rate is not None:
918
+ cvs = radiative_heating.convective_velocity_scale(depth_eff, eff_heat_rate, air_temperature)
919
+ cvs.clip(min=0.01, out=cvs)
920
+ else:
921
+ cvs = 0.01
922
+
923
+ return cvs / n_bv + sedimentation_impact_factor * terminal_fall_speed * depth_eff
924
+
925
+
926
+ ####################
927
+ # Ice particle losses
928
+ ####################
929
+
930
+
931
+ def particle_losses_aggregation(
932
+ r_ice_vol: npt.NDArray[np.float64],
933
+ terminal_fall_speed: npt.NDArray[np.float64],
934
+ area_eff: npt.NDArray[np.float64],
935
+ agg_efficiency: float = 1.0,
936
+ ) -> npt.NDArray[np.float64]:
937
+ """
938
+ Calculate the rate of contrail ice particle losses due to sedimentation-induced aggregation.
939
+
940
+ Parameters
941
+ ----------
942
+ r_ice_vol : npt.NDArray[np.float64]
943
+ Ice particle volume mean radius, [:math:`m`]
944
+ terminal_fall_speed : npt.NDArray[np.float64]
945
+ Terminal fall speed of contrail ice particles, [:math:`m s^{-1}`]
946
+ area_eff : npt.NDArray[np.float64]
947
+ Effective cross-sectional area of the contrail plume, [:math:`m^{2}`]
948
+ agg_efficiency : float, optional
949
+ Aggregation efficiency
950
+
951
+ Returns
952
+ -------
953
+ npt.NDArray[np.float64]
954
+ Rate of contrail ice particle losses due to sedimentation-induced
955
+ aggregation, [:math:`# s^{-1}`]
956
+
957
+ Notes
958
+ -----
959
+ The aggregation efficiency (``agg_efficiency = 1``) was calibrated based on
960
+ the observed lifetime and optical properties from the Contrail Library (COLI)
961
+ database (:cite:`schumannPropertiesIndividualContrails2017`).
962
+
963
+ References
964
+ ----------
965
+ - :cite:`schumannPropertiesIndividualContrails2017`
966
+ """
967
+ return (8.0 * agg_efficiency * np.pi * r_ice_vol**2 * terminal_fall_speed) / area_eff
968
+
969
+
970
+ def particle_losses_turbulence(
971
+ width: npt.NDArray[np.float64],
972
+ depth: npt.NDArray[np.float64],
973
+ depth_eff: npt.NDArray[np.float64],
974
+ diffuse_h: npt.NDArray[np.float64],
975
+ diffuse_v: npt.NDArray[np.float64],
976
+ turb_efficiency: float = 0.1,
977
+ ) -> npt.NDArray[np.float64]:
978
+ """
979
+ Calculate the rate of contrail ice particle losses due to plume-internal turbulence.
980
+
981
+ Parameters
982
+ ----------
983
+ width : npt.NDArray[np.float64]
984
+ Contrail width at each waypoint, [:math:`m`]
985
+ depth : npt.NDArray[np.float64]
986
+ Contrail depth at each waypoint, [:math:`m`]
987
+ depth_eff : npt.NDArray[np.float64]
988
+ Effective depth of the contrail plume, [:math:`m`]
989
+ diffuse_h : npt.NDArray[np.float64]
990
+ Horizontal diffusivity, [:math:`m^{2} s^{-1}`]
991
+ diffuse_v : npt.NDArray[np.float64]
992
+ Vertical diffusivity, [:math:`m^{2} s^{-1}`]
993
+ turb_efficiency : float, optional
994
+ Turbulence sublimation efficiency
995
+
996
+ Returns
997
+ -------
998
+ npt.NDArray[np.float64]
999
+ Rate of contrail ice particle losses due to plume-internal turbulence, [:math:`# s^{-1}`]
1000
+
1001
+ Notes
1002
+ -----
1003
+ The turbulence sublimation efficiency (``turb_efficiency = 0.1``) was calibrated
1004
+ based on the observed lifetime and optical properties from the Contrail Library (COLI)
1005
+ database (:cite:`schumannPropertiesIndividualContrails2017`).
1006
+
1007
+ References
1008
+ ----------
1009
+ - :cite:`schumannPropertiesIndividualContrails2017`
1010
+ """
1011
+ inner_term = (diffuse_h / (np.maximum(width, depth)) ** 2) + (diffuse_v / depth_eff**2)
1012
+ return turb_efficiency * np.abs(inner_term)
1013
+
1014
+
1015
+ ####################
1016
+ # Optical properties
1017
+ ####################
1018
+
1019
+
1020
+ def contrail_optical_depth(
1021
+ r_ice_vol: npt.NDArray[np.float64],
1022
+ n_ice_per_m: npt.NDArray[np.float64],
1023
+ width: npt.NDArray[np.float64],
1024
+ ) -> npt.NDArray[np.float64]:
1025
+ """
1026
+ Calculate the contrail optical depth for each waypoint.
1027
+
1028
+ Parameters
1029
+ ----------
1030
+ r_ice_vol : npt.NDArray[np.float64]
1031
+ ice particle volume mean radius, [:math:`m`]
1032
+ n_ice_per_m : npt.NDArray[np.float64]
1033
+ Number of contrail ice particles per distance, [:math:`m^{-1}`]
1034
+ width : npt.NDArray[np.float64]
1035
+ Contrail width, [:math:`m`]
1036
+
1037
+ Returns
1038
+ -------
1039
+ npt.NDArray[np.float64]
1040
+ Contrail optical depth
1041
+ """
1042
+ q_ext = scattering_extinction_efficiency(r_ice_vol)
1043
+ tau_contrail = constants.c_r * np.pi * r_ice_vol**2 * (n_ice_per_m / width) * q_ext
1044
+
1045
+ bool_small = r_ice_vol <= 1e-9
1046
+ tau_contrail[bool_small] = 0.0
1047
+ tau_contrail.clip(min=0.0, out=tau_contrail)
1048
+ return tau_contrail
1049
+
1050
+
1051
+ def scattering_extinction_efficiency(r_ice_vol: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
1052
+ """
1053
+ Calculate the scattering extinction efficiency (``q_ext``) based on Mie-theory.
1054
+
1055
+ Parameters
1056
+ ----------
1057
+ r_ice_vol : npt.NDArray[np.float64]
1058
+ ice particle volume mean radius, [:math:`m`]
1059
+
1060
+ Returns
1061
+ -------
1062
+ npt.NDArray[np.float64]
1063
+ scattering extinction efficiency
1064
+
1065
+ References
1066
+ ----------
1067
+ - https://en.wikipedia.org/wiki/Mie_scattering
1068
+ """
1069
+ phase_delay = light_wave_phase_delay(r_ice_vol)
1070
+ return 2.0 - (4.0 / phase_delay) * (
1071
+ np.sin(phase_delay) - ((1.0 - np.cos(phase_delay)) / phase_delay)
1072
+ )
1073
+
1074
+
1075
+ def light_wave_phase_delay(r_ice_vol: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
1076
+ """
1077
+ Calculate the phase delay of the light wave passing through the contrail ice particle.
1078
+
1079
+ Parameters
1080
+ ----------
1081
+ r_ice_vol : npt.NDArray[np.float64]
1082
+ ice particle volume mean radius, [:math:`m`]
1083
+
1084
+ Returns
1085
+ -------
1086
+ npt.NDArray[np.float64]
1087
+ phase delay of the light wave passing through the contrail ice particle
1088
+
1089
+ References
1090
+ ----------
1091
+ - https://en.wikipedia.org/wiki/Mie_scattering
1092
+ """
1093
+ phase_delay = (4.0 * np.pi * (constants.mu_ice - 1.0) / constants.lambda_light) * r_ice_vol
1094
+ phase_delay.clip(min=None, max=100.0, out=phase_delay)
1095
+ return phase_delay
1096
+
1097
+
1098
+ #######################################################
1099
+ # Contrail evolution: second-order Runge Kutta scheme
1100
+ #######################################################
1101
+ # Notation "t1" implies properties at the start of the time step (before the time integration step)
1102
+ # Notation "t2" implies properties at the end of the time step (after the time integration step)
1103
+
1104
+
1105
+ def segment_length_ratio(
1106
+ seg_length_t1: npt.NDArray[np.float64],
1107
+ seg_length_t2: npt.NDArray[np.float64],
1108
+ ) -> npt.NDArray[np.float64]:
1109
+ """Calculate the ratio of contrail segment length pre-advection to post-advection.
1110
+
1111
+ Parameters
1112
+ ----------
1113
+ seg_length_t1 : npt.NDArray[np.float64]
1114
+ Segment length of contrail waypoint at the start of the time step, [:math:`m`]
1115
+ seg_length_t2 : npt.NDArray[np.float64]
1116
+ Segment length of contrail waypoint after time step and advection, [:math:`m`]
1117
+
1118
+ Returns
1119
+ -------
1120
+ npt.NDArray[np.float64]
1121
+ Ratio of segment length before advection to segment length after advection.
1122
+
1123
+ Notes
1124
+ -----
1125
+ This implementation differs from the original fortran implementation.
1126
+ Instead of taking a geometric mean between
1127
+ the previous and following segments, a simple ratio is computed.
1128
+
1129
+ For terminal waypoints along a flight trajectory, the associated segment length is 0. In this
1130
+ case, the segment ratio is set to 1 (the naive ratio 0 / 0 is undefined). According to CoCiP
1131
+ conventions, terminus waypoints are "discontinuous" within the flight trajectory, and will not
1132
+ contribute to contrail calculations.
1133
+
1134
+ More broadly, any undefined (nan values, or division by 0) segment ratio is set to 1.
1135
+ This convention ensures that the contrail calculations are not affected by undefined
1136
+ segment-based properties.
1137
+
1138
+ Presently, the output of this function is only used by :func:`plume_temporal_evolution`
1139
+ and :func:`new_ice_particle_number` as a scaling term.
1140
+
1141
+ A `seg_ratio` value of 1 is the same as not applying any scaling in these two functions.
1142
+ """
1143
+ is_defined = (seg_length_t2 > 0.0) & np.isfinite(seg_length_t1)
1144
+ default_value = np.ones_like(seg_length_t1)
1145
+ return np.divide(seg_length_t1, seg_length_t2, out=default_value, where=is_defined)
1146
+
1147
+
1148
+ def plume_temporal_evolution(
1149
+ width_t1: npt.NDArray[np.float64],
1150
+ depth_t1: npt.NDArray[np.float64],
1151
+ sigma_yz_t1: npt.NDArray[np.float64],
1152
+ dsn_dz_t1: npt.NDArray[np.float64],
1153
+ diffuse_h_t1: npt.NDArray[np.float64],
1154
+ diffuse_v_t1: npt.NDArray[np.float64],
1155
+ seg_ratio: npt.NDArray[np.float64] | float,
1156
+ dt: npt.NDArray[np.timedelta64] | np.timedelta64,
1157
+ max_depth: float | None,
1158
+ ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]:
1159
+ """
1160
+ Calculate the temporal evolution of the contrail plume parameters.
1161
+
1162
+ Refer to equation (6) of Schumann (2012). See also equations (29), (30), and (31).
1163
+
1164
+ Parameters
1165
+ ----------
1166
+ width_t1 : npt.NDArray[np.float64]
1167
+ contrail width at the start of the time step, [:math:`m`]
1168
+ depth_t1 : npt.NDArray[np.float64]
1169
+ contrail depth at the start of the time step, [:math:`m`]
1170
+ sigma_yz_t1 : npt.NDArray[np.float64]
1171
+ sigma_yz governs the contrail plume's temporal evolution at the start of the time step
1172
+ dsn_dz_t1 : npt.NDArray[np.float64]
1173
+ vertical gradient of the horizontal velocity (wind shear) normal to the contrail axis
1174
+ at the start of the time step, [:math:`m s^{-1} / Pa`]::
1175
+
1176
+ X-----------------------X X
1177
+ ^ |
1178
+ | (dsn_dz) | <-- (dsn_dz)
1179
+ | |
1180
+ X
1181
+ diffuse_h_t1 : npt.NDArray[np.float64]
1182
+ horizontal diffusivity at the start of the time step, [:math:`m^{2} s^{-1}`]
1183
+ diffuse_v_t1 : npt.NDArray[np.float64]
1184
+ vertical diffusivity at the start of the time step, [:math:`m^{2} s^{-1}`]
1185
+ seg_ratio : npt.NDArray[np.float64] | float
1186
+ Segment length ratio before and after it is advected to the new location.
1187
+ See :func:`segment_length_ratio`.
1188
+ dt : npt.NDArray[np.timedelta64] | np.timedelta64
1189
+ integrate contrails with time steps of dt, [:math:`s`]
1190
+ max_depth: float | None
1191
+ Constrain maximum plume depth to prevent unrealistic values, [:math:`m`].
1192
+ If None is passed, the maximum plume depth is not constrained.
1193
+
1194
+ Returns
1195
+ -------
1196
+ sigma_yy_t2 : npt.NDArray[np.float64]
1197
+ The ``yy`` component of convariance matrix, [:math:`m^{2}`]
1198
+ sigma_zz_t2 : npt.NDArray[np.float64]
1199
+ The ``zz`` component of convariance matrix, [:math:`m^{2}`]
1200
+ sigma_yz_t2 : npt.NDArray[np.float64]
1201
+ The ``yz`` component of convariance matrix, [:math:`m^{2}`]
1202
+ """
1203
+ # Convert dt to seconds value and use dtype of other variables
1204
+ dtype = np.result_type(width_t1, depth_t1, sigma_yz_t1, dsn_dz_t1, diffuse_h_t1, diffuse_v_t1)
1205
+ dt_s = units.dt_to_seconds(dt, dtype)
1206
+
1207
+ sigma_yy = 0.125 * width_t1**2
1208
+ sigma_zz = 0.125 * depth_t1**2
1209
+
1210
+ # Convert from max_depth to an upper bound for diffuse_v_t1
1211
+ # All three terms involve the diffuse_v_t1 variable, so we need to
1212
+ # calculate the max value for diffuse_v_t1 and apply it to all three terms.
1213
+ # If we don't do this, we violate the some mathematical constraints of the
1214
+ # covariance matrix (positive definite). In particular, for downstream
1215
+ # calculations, we required that
1216
+ # sigma_yy_t2 * sigma_zz_t2 - sigma_yz_t2**2 >= 0
1217
+ if max_depth is not None:
1218
+ max_sigma_zz = 0.125 * max_depth**2
1219
+ max_diffuse_v = (max_sigma_zz - sigma_zz) / (2.0 * dt_s)
1220
+ diffuse_v_t1 = np.minimum(diffuse_v_t1, max_diffuse_v)
1221
+
1222
+ # Avoid some redundant calculations
1223
+ dsn_dz_t1_2 = dsn_dz_t1**2
1224
+ dt_s_2 = dt_s**2
1225
+ dt_s_3 = dt_s * dt_s_2
1226
+
1227
+ # Calculate the return arrays
1228
+ sigma_yy_t2 = (
1229
+ ((2 / 3) * dsn_dz_t1_2 * diffuse_v_t1 * dt_s_3)
1230
+ + (dsn_dz_t1_2 * sigma_zz * dt_s_2)
1231
+ + (2.0 * (diffuse_h_t1 + dsn_dz_t1 * sigma_yz_t1) * dt_s)
1232
+ + sigma_yy
1233
+ ) * (seg_ratio**2)
1234
+
1235
+ sigma_zz_t2 = (2.0 * diffuse_v_t1 * dt_s) + sigma_zz
1236
+
1237
+ sigma_yz_t2 = (
1238
+ (dsn_dz_t1 * diffuse_v_t1 * dt_s_2) + (dsn_dz_t1 * sigma_zz * dt_s) + sigma_yz_t1
1239
+ ) * seg_ratio
1240
+
1241
+ return sigma_yy_t2, sigma_zz_t2, sigma_yz_t2
1242
+
1243
+
1244
+ def new_contrail_dimensions(
1245
+ sigma_yy_t2: npt.NDArray[np.float64],
1246
+ sigma_zz_t2: npt.NDArray[np.float64],
1247
+ ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
1248
+ """
1249
+ Calculate the new contrail width and depth.
1250
+
1251
+ Parameters
1252
+ ----------
1253
+ sigma_yy_t2 : npt.NDArray[np.float64]
1254
+ element yy, covariance matrix of the Gaussian concentration
1255
+ field, Eq. (6) of Schumann (2012)
1256
+ sigma_zz_t2 : npt.NDArray[np.float64]
1257
+ element zz, covariance matrix of the Gaussian concentration
1258
+ field, Eq. (6) of Schumann (2012)
1259
+
1260
+ Returns
1261
+ -------
1262
+ width_t2 : npt.NDArray[np.float64]
1263
+ Contrail width at the end of the time step, [:math:`m`]
1264
+ depth_t2 : npt.NDArray[np.float64]
1265
+ Contrail depth at the end of the time step, [:math:`m`]
1266
+ """
1267
+ width_t2 = (8 * sigma_yy_t2) ** 0.5
1268
+ depth_t2 = (8 * sigma_zz_t2) ** 0.5
1269
+ return width_t2, depth_t2
1270
+
1271
+
1272
+ def new_effective_area_from_sigma(
1273
+ sigma_yy: npt.NDArray[np.float64],
1274
+ sigma_zz: npt.NDArray[np.float64],
1275
+ sigma_yz: npt.NDArray[np.float64] | float,
1276
+ ) -> npt.NDArray[np.float64]:
1277
+ """
1278
+ Calculate effective cross-sectional area of contrail plume (``area_eff``) from sigma parameters.
1279
+
1280
+ This method calculates the same output as :func`plume_effective_cross_sectional_area`, but
1281
+ calculated with different input parameters.
1282
+
1283
+ Parameters
1284
+ ----------
1285
+ sigma_yy : npt.NDArray[np.float64]
1286
+ element yy, covariance matrix of the Gaussian concentration
1287
+ field, Eq. (6) of Schumann (2012)
1288
+ sigma_zz : npt.NDArray[np.float64]
1289
+ element zz, covariance matrix of the Gaussian concentration
1290
+ field, Eq. (6) of Schumann (2012)
1291
+ sigma_yz : npt.NDArray[np.float64] | float
1292
+ element yz, covariance matrix of the Gaussian concentration
1293
+ field, Eq. (6) of Schumann (2012)
1294
+
1295
+ Returns
1296
+ -------
1297
+ npt.NDArray[np.float64]
1298
+ Effective cross-sectional area of the contrail plume (area_eff)
1299
+ """
1300
+ det_sigma = sigma_yy * sigma_zz - sigma_yz**2
1301
+ return 2.0 * np.pi * det_sigma**0.5
1302
+
1303
+
1304
+ def new_ice_water_content(
1305
+ iwc_t1: npt.NDArray[np.float64],
1306
+ q_t1: npt.NDArray[np.float64],
1307
+ q_t2: npt.NDArray[np.float64],
1308
+ q_sat_t1: npt.NDArray[np.float64],
1309
+ q_sat_t2: npt.NDArray[np.float64],
1310
+ mass_plume_t1: npt.NDArray[np.float64],
1311
+ mass_plume_t2: npt.NDArray[np.float64],
1312
+ ) -> npt.NDArray[np.float64]:
1313
+ """
1314
+ Calculate the new contrail ice water content after the time integration step (``iwc_t2``).
1315
+
1316
+ Parameters
1317
+ ----------
1318
+ iwc_t1 : npt.NDArray[np.float64]
1319
+ contrail ice water content, i.e., contrail ice mass per kg of air,
1320
+ at the start of the time step, [:math:`kg_{H_{2}O}/kg_{air}`]
1321
+ q_t1 : npt.NDArray[np.float64]
1322
+ specific humidity for each waypoint at the start of the
1323
+ time step, [:math:`kg_{H_{2}O}/kg_{air}`]
1324
+ q_t2 : npt.NDArray[np.float64]
1325
+ specific humidity for each waypoint at the end of the
1326
+ time step, [:math:`kg_{H_{2}O}/kg_{air}`]
1327
+ q_sat_t1 : npt.NDArray[np.float64]
1328
+ saturation humidity for each waypoint at the start of the
1329
+ time step, [:math:`kg_{H_{2}O}/kg_{air}`]
1330
+ q_sat_t2 : npt.NDArray[np.float64]
1331
+ saturation humidity for each waypoint at the end of the
1332
+ time step, [:math:`kg_{H_{2}O}/kg_{air}`]
1333
+ mass_plume_t1 : npt.NDArray[np.float64]
1334
+ contrail plume mass per unit length at the start of the
1335
+ time step, [:math:`kg_{air} m^{-1}`]
1336
+ mass_plume_t2 : npt.NDArray[np.float64]
1337
+ contrail plume mass per unit length at the end of the
1338
+ time step, [:math:`kg_{air} m^{-1}`]
1339
+
1340
+ Returns
1341
+ -------
1342
+ npt.NDArray[np.float64]
1343
+ Contrail ice water content at the end of the time step, [:math:`kg_{ice} kg_{air}^{-1}`]
1344
+
1345
+ Notes
1346
+ -----
1347
+ (1) The ice water content is fully conservative.
1348
+ (2) ``mass_h2o_t2``: the total H2O mass (ice + vapour) per unit of
1349
+ contrail plume [Units of kg-H2O/m]
1350
+ (3) ``q_sat`` is used to calculate mass_h2o because air inside the
1351
+ contrail is assumed to be ice saturated.
1352
+ (4) ``(mass_plume_t2 - mass_plume) * q_mean``: contrail absorbs
1353
+ (releases) H2O from (to) surrounding air.
1354
+ (5) ``iwc_t2 = mass_h2o_t2 / mass_plume_t2 - q_sat_t2``: H2O in the
1355
+ gas phase is removed (``- q_sat_t2``).
1356
+ """
1357
+ q_mean = 0.5 * (q_t1 + q_t2)
1358
+ mass_h2o_t1 = mass_plume_t1 * (iwc_t1 + q_sat_t1)
1359
+ mass_h2o_t2 = mass_h2o_t1 + (mass_plume_t2 - mass_plume_t1) * q_mean
1360
+ iwc_t2 = (mass_h2o_t2 / mass_plume_t2) - q_sat_t2
1361
+ iwc_t2.clip(min=0.0, out=iwc_t2)
1362
+ return iwc_t2
1363
+
1364
+
1365
+ def new_ice_particle_number(
1366
+ n_ice_per_m_t1: npt.NDArray[np.float64],
1367
+ dn_dt_agg: npt.NDArray[np.float64],
1368
+ dn_dt_turb: npt.NDArray[np.float64],
1369
+ seg_ratio: npt.NDArray[np.float64] | float,
1370
+ dt: npt.NDArray[np.timedelta64] | np.timedelta64,
1371
+ ) -> npt.NDArray[np.float64]:
1372
+ """Calculate the number of ice particles per distance at the end of the time step.
1373
+
1374
+ Parameters
1375
+ ----------
1376
+ n_ice_per_m_t1 : npt.NDArray[np.float64]
1377
+ number of contrail ice particles per distance at the start of
1378
+ the time step, [:math:`m^{-1}`]
1379
+ dn_dt_agg : npt.NDArray[np.float64]
1380
+ rate of ice particle losses due to sedimentation-induced aggregation, [:math:`# s^{-1}`]
1381
+ dn_dt_turb : npt.NDArray[np.float64]
1382
+ rate of contrail ice particle losses due to plume-internal turbulence, [:math:`# s^{-1}`]
1383
+ seg_ratio : npt.NDArray[np.float64] | float
1384
+ Segment length ratio before and after it is advected to the new location.
1385
+ dt : npt.NDArray[np.timedelta64] | np.timedelta64
1386
+ integrate contrails with time steps of dt, [:math:`s`]
1387
+
1388
+ Returns
1389
+ -------
1390
+ npt.NDArray[np.float64]
1391
+ number of ice particles per distance at the end of the time step, [:math:`m^{-1}`]
1392
+ """
1393
+ # Convert dt to seconds value and use dtype of other variables
1394
+ dtype = np.result_type(n_ice_per_m_t1, dn_dt_agg, dn_dt_turb, seg_ratio)
1395
+ dt_s = units.dt_to_seconds(dt, dtype)
1396
+
1397
+ n_ice_per_m_t1 = np.maximum(n_ice_per_m_t1, 0.0)
1398
+
1399
+ exp_term = np.where(dn_dt_turb * dt_s < 80.0, np.exp(-dn_dt_turb * dt_s), 0.0)
1400
+
1401
+ numerator = dn_dt_turb * n_ice_per_m_t1 * exp_term
1402
+ denominator = dn_dt_turb + (dn_dt_agg * n_ice_per_m_t1 * (1 - exp_term))
1403
+ n_ice_per_m_t2 = (numerator / denominator) * seg_ratio
1404
+
1405
+ small_loss = (dn_dt_turb * dt_s) < 1e-5 # For small ice particle losses
1406
+ denom = 1 + (dn_dt_agg * dt_s * n_ice_per_m_t1)
1407
+ n_ice_per_m_t2[small_loss] = n_ice_per_m_t1[small_loss] / denom[small_loss]
1408
+ n_ice_per_m_t2.clip(min=0.0, out=n_ice_per_m_t2)
1409
+ return n_ice_per_m_t2
1410
+
1411
+
1412
+ ########
1413
+ # Energy Forcing
1414
+ ########
1415
+ # TODO: This should be moved closer to the radiative forcing calculations
1416
+
1417
+
1418
+ def energy_forcing(
1419
+ rf_net_t1: npt.NDArray[np.float64],
1420
+ rf_net_t2: npt.NDArray[np.float64],
1421
+ width_t1: npt.NDArray[np.float64],
1422
+ width_t2: npt.NDArray[np.float64],
1423
+ seg_length_t2: npt.NDArray[np.float64] | float,
1424
+ dt: npt.NDArray[np.timedelta64] | np.timedelta64,
1425
+ ) -> npt.NDArray[np.float64]:
1426
+ """Calculate the contrail energy forcing over time step.
1427
+
1428
+ The contrail energy forcing is calculated as the local contrail net
1429
+ radiative forcing (RF', change in energy flux per contrail area) multiplied
1430
+ by its width and integrated over its length and lifetime.
1431
+
1432
+ Parameters
1433
+ ----------
1434
+ rf_net_t1 : npt.NDArray[np.float64]
1435
+ local contrail net radiative forcing at the start of the time step, [:math:`W m^{-2}`]
1436
+ rf_net_t2 : npt.NDArray[np.float64]
1437
+ local contrail net radiative forcing at the end of the time step, [:math:`W m^{-2}`]
1438
+ width_t1 : npt.NDArray[np.float64]
1439
+ contrail width at the start of the time step, [:math:`m`]
1440
+ width_t2 : npt.NDArray[np.float64]
1441
+ contrail width at the end of the time step, [:math:`m`]
1442
+ seg_length_t2 : npt.NDArray[np.float64] | float
1443
+ Segment length of contrail waypoint at the end of the time step, [:math:`m`]
1444
+ dt : npt.NDArray[np.timedelta64] | np.timedelta64
1445
+ integrate contrails with time steps of dt, [:math:`s`]
1446
+
1447
+ Returns
1448
+ -------
1449
+ npt.NDArray[np.float64]
1450
+ Contrail energy forcing over time step dt, [:math:`J`].
1451
+ """
1452
+ rad_flux_per_m = mean_radiative_flux_per_m(rf_net_t1, rf_net_t2, width_t1, width_t2)
1453
+ energy_flux_per_m = mean_energy_flux_per_m(rad_flux_per_m, dt)
1454
+ return energy_flux_per_m * seg_length_t2
1455
+
1456
+
1457
+ def mean_radiative_flux_per_m(
1458
+ rf_net_t1: npt.NDArray[np.float64],
1459
+ rf_net_t2: npt.NDArray[np.float64],
1460
+ width_t1: npt.NDArray[np.float64],
1461
+ width_t2: npt.NDArray[np.float64],
1462
+ ) -> npt.NDArray[np.float64]:
1463
+ """Calculate the mean radiative flux per length of contrail between two time steps.
1464
+
1465
+ Parameters
1466
+ ----------
1467
+ rf_net_t1 : npt.NDArray[np.float64]
1468
+ local contrail net radiative forcing at the start of the time step, [:math:`W m^{-2}`]
1469
+ rf_net_t2 : npt.NDArray[np.float64]
1470
+ local contrail net radiative forcing at the end of the time step, [:math:`W m^{-2}`]
1471
+ width_t1 : npt.NDArray[np.float64]
1472
+ contrail width at the start of the time step, [:math:`m`]
1473
+ width_t2 : npt.NDArray[np.float64]
1474
+ contrail width at the end of the time step, [:math:`m`]
1475
+
1476
+ Returns
1477
+ -------
1478
+ npt.NDArray[np.float64]
1479
+ Mean radiative flux between time steps, [:math:`W m^{-1}`]
1480
+ """
1481
+ rad_flux_per_m_t1 = width_t1 * rf_net_t1
1482
+ rad_flux_per_m_t2 = width_t2 * rf_net_t2
1483
+ return (rad_flux_per_m_t1 + rad_flux_per_m_t2) * 0.5
1484
+
1485
+
1486
+ def mean_energy_flux_per_m(
1487
+ rad_flux_per_m: npt.NDArray[np.float64], dt: npt.NDArray[np.timedelta64] | np.timedelta64
1488
+ ) -> npt.NDArray[np.float64]:
1489
+ """Calculate the mean energy flux per length of contrail on segment following waypoint.
1490
+
1491
+ Parameters
1492
+ ----------
1493
+ rad_flux_per_m : npt.NDArray[np.float64]
1494
+ Mean radiative flux between time steps for waypoint, [:math:`W m^{-1}`].
1495
+ See :func:`mean_radiative_flux_per_m`.
1496
+ dt : npt.NDArray[np.timedelta64]
1497
+ timedelta of integration timestep for each waypoint.
1498
+
1499
+ Returns
1500
+ -------
1501
+ npt.NDArray[np.float64]
1502
+ Mean energy flux per length of contrail after waypoint, [:math:`J m^{-1}`]
1503
+
1504
+ Notes
1505
+ -----
1506
+ Implementation differs from original fortran in two ways:
1507
+
1508
+ - Discontinuity is no longer set to 0 (this occurs directly in model :class:`Cocip`)
1509
+ - Instead of taking an average of the previous and following segments,
1510
+ energy flux is only calculated for the following segment.
1511
+
1512
+ See Also
1513
+ --------
1514
+ :func:`mean_radiative_flux_per_m`
1515
+ """
1516
+ dt_s = units.dt_to_seconds(dt, rad_flux_per_m.dtype)
1517
+ return rad_flux_per_m * dt_s