pycontrails 0.58.0__cp314-cp314-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pycontrails might be problematic. Click here for more details.

Files changed (122) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2931 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,508 @@
1
+ """Wave-vortex downwash functions from Lottermoser & Unterstrasser (2025).
2
+
3
+ Notes
4
+ -----
5
+ :cite:`unterstrasserPropertiesYoungContrails2016` provides a parameterized model of the
6
+ survival fraction of the contrail ice crystal number ``f_surv`` during the wake-vortex phase.
7
+ The model has since been updated in :cite:`lottermoserHighResolutionEarlyContrails2025`. This update
8
+ improves the goodness-of-fit between the parameterised model and LES, and expands the parameter
9
+ space and can now be used for very low and very high soot inputs, different fuel types (where the
10
+ EI H2Os are different), and higher ambient temperatures (up to 235 K) to accomodate for contrails
11
+ formed by liquid hydrogen aircraft. The model was developed based on output from large eddy
12
+ simulations, and improves agreement with LES outputs relative to the default survival fraction
13
+ parameterization used in CoCiP.
14
+
15
+ For comparison, CoCiP assumes that ``f_surv`` is equal to the change in the contrail ice water
16
+ content (by mass) before and after the wake vortex phase. However, for larger (smaller) ice
17
+ particles, their survival fraction by number could be smaller (larger) than their survival fraction
18
+ by mass. This is particularly important in the "soot-poor" scenario, for example, in cleaner
19
+ lean-burn engines where their soot emissions can be 3-4 orders of magnitude lower than conventional
20
+ RQL engines.
21
+
22
+ ADD CITATION TO BIBTEX: :cite:`lottermoserHighResolutionEarlyContrails2025`
23
+ Lottermoser, A. and Unterstraßer, S.: High-resolution modelling of early contrail evolution from
24
+ hydrogen-powered aircraft, EGUsphere [preprint], https://doi.org/10.5194/egusphere-2024-3859, 2025.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import numpy as np
30
+ import numpy.typing as npt
31
+
32
+ from pycontrails.models.cocip.wake_vortex import wake_vortex_separation
33
+ from pycontrails.physics import constants, thermo
34
+
35
+
36
+ def ice_particle_number_survival_fraction(
37
+ air_temperature: npt.NDArray[np.floating],
38
+ rhi_0: npt.NDArray[np.floating],
39
+ ei_h2o: npt.NDArray[np.floating] | float,
40
+ wingspan: npt.NDArray[np.floating] | float,
41
+ true_airspeed: npt.NDArray[np.floating],
42
+ fuel_flow: npt.NDArray[np.floating],
43
+ aei_n: npt.NDArray[np.floating],
44
+ z_desc: npt.NDArray[np.floating],
45
+ *,
46
+ analytical_solution: bool = True,
47
+ ) -> npt.NDArray[np.floating]:
48
+ r"""
49
+ Calculate fraction of ice particle number surviving the wake vortex phase and required inputs.
50
+
51
+ This implementation is based on the work of :cite:`unterstrasserPropertiesYoungContrails2016`
52
+ and is an improved estimation compared with
53
+ :func:`contrail_properties.ice_particle_survival_fraction`.
54
+
55
+ Parameters
56
+ ----------
57
+ air_temperature : npt.NDArray[np.floating]
58
+ ambient temperature for each waypoint, [:math:`K`]
59
+ rhi_0: npt.NDArray[np.floating]
60
+ Relative humidity with respect to ice at the flight waypoint
61
+ ei_h2o : npt.NDArray[np.floating] | float
62
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
63
+ wingspan : npt.NDArray[np.floating] | float
64
+ aircraft wingspan, [:math:`m`]
65
+ true_airspeed : npt.NDArray[np.floating]
66
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
67
+ fuel_flow : npt.NDArray[np.floating]
68
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
69
+ aei_n : npt.NDArray[np.floating]
70
+ Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
71
+ z_desc : npt.NDArray[np.floating]
72
+ Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
73
+ [:math:`m`].
74
+ analytical_solution : bool
75
+ Use analytical solution to calculate ``z_atm`` and ``z_emit`` instead of numerical solution.
76
+
77
+ Returns
78
+ -------
79
+ npt.NDArray[np.floating]
80
+ Fraction of contrail ice particle number that survive the wake vortex phase.
81
+
82
+ References
83
+ ----------
84
+ - :cite:`unterstrasserPropertiesYoungContrails2016`
85
+ - :cite:`lottermoserHighResolutionEarlyContrails2025`
86
+
87
+ Notes
88
+ -----
89
+ - For consistency in CoCiP, ``z_desc`` should be calculated using :func:`dz_max` instead of
90
+ using :func:`z_desc_length_scale`.
91
+ """
92
+ rho_emit = emitted_water_vapour_concentration(ei_h2o, wingspan, true_airspeed, fuel_flow)
93
+
94
+ # Length scales
95
+ if analytical_solution:
96
+ z_atm = z_atm_length_scale_analytical(air_temperature, rhi_0)
97
+ z_emit = z_emit_length_scale_analytical(rho_emit, air_temperature)
98
+
99
+ else:
100
+ z_atm = z_atm_length_scale_numerical(air_temperature, rhi_0)
101
+ z_emit = z_emit_length_scale_numerical(rho_emit, air_temperature)
102
+
103
+ z_total = z_total_length_scale(z_atm, z_emit, z_desc, true_airspeed, fuel_flow, aei_n, wingspan)
104
+ return _survival_fraction_from_length_scale(z_total)
105
+
106
+
107
+ def z_total_length_scale(
108
+ z_atm: npt.NDArray[np.floating],
109
+ z_emit: npt.NDArray[np.floating],
110
+ z_desc: npt.NDArray[np.floating],
111
+ true_airspeed: npt.NDArray[np.floating],
112
+ fuel_flow: npt.NDArray[np.floating],
113
+ aei_n: npt.NDArray[np.floating],
114
+ wingspan: npt.NDArray[np.floating] | float,
115
+ ) -> npt.NDArray[np.floating]:
116
+ """
117
+ Calculate the total length-scale effect of the wake vortex downwash.
118
+
119
+ Parameters
120
+ ----------
121
+ z_atm : npt.NDArray[np.floating]
122
+ Length-scale effect of ambient supersaturation on the ice crystal mass budget, [:math:`m`]
123
+ z_emit : npt.NDArray[np.floating]
124
+ Length-scale effect of water vapour emissions on the ice crystal mass budget, [:math:`m`]
125
+ z_desc : npt.NDArray[np.floating]
126
+ Final vertical displacement of the wake vortex, `dz_max` in `wake_vortex.py`, [:math:`m`]
127
+ true_airspeed : npt.NDArray[np.floating]
128
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
129
+ fuel_flow : npt.NDArray[np.floating]
130
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
131
+ aei_n : npt.NDArray[np.floating]
132
+ Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
133
+ wingspan : npt.NDArray[np.floating] | float
134
+ aircraft wingspan, [:math:`m`]
135
+
136
+ Returns
137
+ -------
138
+ npt.NDArray[np.floating]
139
+ Total length-scale effect of the wake vortex downwash, [:math:`m`]
140
+
141
+ Notes
142
+ -----
143
+ - For `psi`, see Appendix A1 in :cite:`lottermoserHighResolutionEarlyContrails2025`.
144
+ - For `z_total`, see Eq. (9) and (10) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
145
+ """
146
+ # Calculate psi term
147
+ fuel_dist = fuel_flow / true_airspeed # Units: [:math:`kg m^{-1}`]
148
+ n_ice_dist = fuel_dist * aei_n # Units: [:math:`m^{-1}`]
149
+
150
+ n_ice_per_vol = n_ice_dist / plume_area(wingspan) # Units: [:math:`m^{-3}`]
151
+ n_ice_per_vol_ref = 3.38e12 / plume_area(60.3)
152
+
153
+ psi = (n_ice_per_vol_ref / n_ice_per_vol) ** 0.16
154
+
155
+ # Calculate total length-scale effect
156
+ return psi * (1.27 * z_atm + 0.42 * z_emit) - 0.49 * z_desc
157
+
158
+
159
+ def z_atm_length_scale_analytical(
160
+ air_temperature: npt.NDArray[np.floating],
161
+ rhi_0: npt.NDArray[np.floating],
162
+ ) -> npt.NDArray[np.floating]:
163
+ """Calculate the length-scale effect of ambient supersaturation on the ice crystal mass budget.
164
+
165
+ Parameters
166
+ ----------
167
+ air_temperature : npt.NDArray[np.floating]
168
+ Ambient temperature for each waypoint, [:math:`K`].
169
+ rhi_0 : npt.NDArray[np.floating]
170
+ Relative humidity with respect to ice at the flight waypoint.
171
+
172
+ Returns
173
+ -------
174
+ npt.NDArray[np.floating]
175
+ The effect of the ambient supersaturation on the ice crystal mass budget,
176
+ provided as a length scale equivalent, estimated with analytical fit [:math:`m`].
177
+
178
+ Notes
179
+ -----
180
+ - See Eq. (A2) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
181
+ """
182
+ z_atm = np.zeros_like(rhi_0)
183
+
184
+ # Only perform operation when the ambient condition is supersaturated w.r.t. ice
185
+ issr = rhi_0 > 1.0
186
+
187
+ s_i = rhi_0 - 1.0
188
+ z_atm[issr] = 607.46 * s_i[issr] ** 0.897 * (air_temperature[issr] / 205.0) ** 2.225
189
+ return z_atm
190
+
191
+
192
+ def z_atm_length_scale_numerical(
193
+ air_temperature: npt.NDArray[np.floating],
194
+ rhi_0: npt.NDArray[np.floating],
195
+ *,
196
+ n_iter: int = 10,
197
+ ) -> npt.NDArray[np.floating]:
198
+ """Calculate the length-scale effect of ambient supersaturation on the ice crystal mass budget.
199
+
200
+ Parameters
201
+ ----------
202
+ air_temperature : npt.NDArray[np.floating]
203
+ Ambient temperature for each waypoint, [:math:`K`].
204
+ rhi_0 : npt.NDArray[np.floating]
205
+ Relative humidity with respect to ice at the flight waypoint.
206
+ n_iter : int
207
+ Number of iterations, set to 10 as default where ``z_atm`` is accurate to within +-1 m.
208
+
209
+ Returns
210
+ -------
211
+ npt.NDArray[np.floating]
212
+ The effect of the ambient supersaturation on the ice crystal mass budget,
213
+ provided as a length scale equivalent, estimated with numerical methods [:math:`m`].
214
+
215
+ Notes
216
+ -----
217
+ - See Eq. (6) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
218
+ """
219
+ # Only perform operation when the ambient condition is supersaturated w.r.t. ice
220
+ issr = rhi_0 > 1.0
221
+
222
+ rhi_issr = rhi_0[issr]
223
+ air_temperature_issr = air_temperature[issr]
224
+
225
+ # Solve non-linear equation numerically using the bisection method
226
+ # Did not use scipy functions because it is unstable when dealing with np.arrays
227
+ z_1 = np.zeros_like(rhi_issr)
228
+ z_2 = np.full_like(rhi_issr, 1000.0)
229
+ lhs = rhi_issr * thermo.e_sat_ice(air_temperature_issr) / (air_temperature_issr**3.5)
230
+
231
+ dry_adiabatic_lapse_rate = constants.g / constants.c_pd
232
+ for _ in range(n_iter):
233
+ z_est = 0.5 * (z_1 + z_2)
234
+ rhs = (thermo.e_sat_ice(air_temperature_issr + dry_adiabatic_lapse_rate * z_est)) / (
235
+ air_temperature_issr + dry_adiabatic_lapse_rate * z_est
236
+ ) ** 3.5
237
+ z_1[lhs > rhs] = z_est[lhs > rhs]
238
+ z_2[lhs < rhs] = z_est[lhs < rhs]
239
+
240
+ out = np.zeros_like(rhi_0)
241
+ out[issr] = 0.5 * (z_1 + z_2)
242
+ return out
243
+
244
+
245
+ def emitted_water_vapour_concentration(
246
+ ei_h2o: npt.NDArray[np.floating] | float,
247
+ wingspan: npt.NDArray[np.floating] | float,
248
+ true_airspeed: npt.NDArray[np.floating],
249
+ fuel_flow: npt.NDArray[np.floating],
250
+ ) -> npt.NDArray[np.floating]:
251
+ r"""
252
+ Calculate aircraft-emitted water vapour concentration in the plume.
253
+
254
+ Parameters
255
+ ----------
256
+ ei_h2o : npt.NDArray[np.floating] | float
257
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
258
+ wingspan : npt.NDArray[np.floating] | float
259
+ aircraft wingspan, [:math:`m`]
260
+ true_airspeed : npt.NDArray[np.floating]
261
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
262
+ fuel_flow : npt.NDArray[np.floating]
263
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
264
+
265
+ Returns
266
+ -------
267
+ npt.NDArray[np.floating]
268
+ Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
269
+
270
+ Notes
271
+ -----
272
+ - See eq. (6) and (A8) in :cite:`unterstrasserPropertiesYoungContrails2016`.
273
+ """
274
+ h2o_per_dist = (ei_h2o * fuel_flow) / true_airspeed
275
+ area_p = plume_area(wingspan)
276
+ return h2o_per_dist / area_p
277
+
278
+
279
+ def z_emit_length_scale_analytical(
280
+ rho_emit: npt.NDArray[np.floating],
281
+ air_temperature: npt.NDArray[np.floating],
282
+ ) -> npt.NDArray[np.floating]:
283
+ """Calculate the length-scale effect of water vapour emissions on the ice crystal mass budget.
284
+
285
+ Parameters
286
+ ----------
287
+ rho_emit : npt.NDArray[np.floating]
288
+ Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
289
+ air_temperature : npt.NDArray[np.floating]
290
+ ambient temperature for each waypoint, [:math:`K`]
291
+
292
+ Returns
293
+ -------
294
+ npt.NDArray[np.floating]
295
+ The effect of the aircraft water vapour emission on the ice crystal mass budget,
296
+ provided as a length scale equivalent, estimated with analytical fit [:math:`m`]
297
+
298
+ Notes
299
+ -----
300
+ - See Eq. (A3) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
301
+ """
302
+ t_205 = air_temperature - 205.0
303
+ return (
304
+ 1106.6
305
+ * ((rho_emit * 1e5) ** (0.678 + 0.0116 * t_205))
306
+ * np.exp(-(0.0807 + 0.000428 * t_205) * t_205)
307
+ )
308
+
309
+
310
+ def z_emit_length_scale_numerical(
311
+ rho_emit: npt.NDArray[np.floating],
312
+ air_temperature: npt.NDArray[np.floating],
313
+ *,
314
+ n_iter: int = 10,
315
+ ) -> npt.NDArray[np.floating]:
316
+ """Calculate the length-scale effect of water vapour emissions on the ice crystal mass budget.
317
+
318
+ Parameters
319
+ ----------
320
+ rho_emit : npt.NDArray[np.floating]
321
+ Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
322
+ air_temperature : npt.NDArray[np.floating]
323
+ ambient temperature for each waypoint, [:math:`K`]
324
+ n_iter : int
325
+ Number of iterations, set to 10 as default where ``z_emit`` is accurate to within +-1 m.
326
+
327
+ Returns
328
+ -------
329
+ npt.NDArray[np.floating]
330
+ The effect of the aircraft water vapour emission on the ice crystal mass budget,
331
+ provided as a length scale equivalent, estimated with numerical methods [:math:`m`]
332
+
333
+ Notes
334
+ -----
335
+ - See Eq. (7) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
336
+ """
337
+ # Solve non-linear equation numerically using the bisection method
338
+ # Did not use scipy functions because it is unstable when dealing with np.arrays
339
+ z_1 = np.zeros_like(rho_emit)
340
+ z_2 = np.full_like(rho_emit, 1000.0)
341
+
342
+ lhs = (thermo.e_sat_ice(air_temperature) / (constants.R_v * air_temperature**3.5)) + (
343
+ rho_emit / (air_temperature**2.5)
344
+ )
345
+
346
+ dry_adiabatic_lapse_rate = constants.g / constants.c_pd
347
+ for _ in range(n_iter):
348
+ z_est = 0.5 * (z_1 + z_2)
349
+ rhs = thermo.e_sat_ice(air_temperature + dry_adiabatic_lapse_rate * z_est) / (
350
+ constants.R_v * (air_temperature + dry_adiabatic_lapse_rate * z_est) ** 3.5
351
+ )
352
+ z_1[lhs > rhs] = z_est[lhs > rhs]
353
+ z_2[lhs < rhs] = z_est[lhs < rhs]
354
+
355
+ return 0.5 * (z_1 + z_2)
356
+
357
+
358
+ def plume_area(wingspan: npt.NDArray[np.floating] | float) -> npt.NDArray[np.floating] | float:
359
+ """Calculate area of the wake-vortex plume.
360
+
361
+ Parameters
362
+ ----------
363
+ wingspan : npt.NDArray[np.floating] | float
364
+ aircraft wingspan, [:math:`m`]
365
+
366
+ Returns
367
+ -------
368
+ npt.NDArray[np.floating] | float
369
+ Area of two wake-vortex plumes, [:math:`m^{2}`]
370
+
371
+ Notes
372
+ -----
373
+ - See Appendix A2 in eq. (A6) and (A7) in :cite:`lottermoserHighResolutionEarlyContrails2025`.
374
+ """
375
+ r_plume = 1.5 + 0.314 * wingspan
376
+ return 2.0 * np.pi * r_plume**2
377
+
378
+
379
+ def z_desc_length_scale(
380
+ wingspan: npt.NDArray[np.floating] | float,
381
+ air_temperature: npt.NDArray[np.floating],
382
+ air_pressure: npt.NDArray[np.floating],
383
+ true_airspeed: npt.NDArray[np.floating],
384
+ aircraft_mass: npt.NDArray[np.floating],
385
+ dT_dz: npt.NDArray[np.floating],
386
+ ) -> npt.NDArray[np.floating]:
387
+ """Calculate the final vertical displacement of the wake vortex.
388
+
389
+ Parameters
390
+ ----------
391
+ wingspan : npt.NDArray[np.floating] | float
392
+ aircraft wingspan, [:math:`m`]
393
+ air_temperature : npt.NDArray[np.floating]
394
+ ambient temperature for each waypoint, [:math:`K`]
395
+ air_pressure : npt.NDArray[np.floating]
396
+ pressure altitude at each waypoint, [:math:`Pa`]
397
+ true_airspeed : npt.NDArray[np.floating]
398
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
399
+ aircraft_mass : npt.NDArray[np.floating]
400
+ aircraft mass for each waypoint, [:math:`kg`]
401
+ dT_dz : npt.NDArray[np.floating]
402
+ potential temperature gradient, [:math:`K m^{-1}`]
403
+
404
+ Returns
405
+ -------
406
+ npt.NDArray[np.floating]
407
+ Final vertical displacement of the wake vortex, [:math:`m`]
408
+
409
+ Notes
410
+ -----
411
+ - See eq. (4) in :cite:`unterstrasserPropertiesYoungContrails2016`.
412
+ """
413
+ gamma_0 = _initial_wake_vortex_circulation(
414
+ wingspan, air_temperature, air_pressure, true_airspeed, aircraft_mass
415
+ )
416
+ n_bv = thermo.brunt_vaisala_frequency(air_pressure, air_temperature, dT_dz)
417
+ return ((8.0 * gamma_0) / (np.pi * n_bv)) ** 0.5
418
+
419
+
420
+ def _initial_wake_vortex_circulation(
421
+ wingspan: npt.NDArray[np.floating] | float,
422
+ air_temperature: npt.NDArray[np.floating],
423
+ air_pressure: npt.NDArray[np.floating],
424
+ true_airspeed: npt.NDArray[np.floating],
425
+ aircraft_mass: npt.NDArray[np.floating],
426
+ ) -> npt.NDArray[np.floating]:
427
+ """Calculate initial wake vortex circulation.
428
+
429
+ Parameters
430
+ ----------
431
+ wingspan : npt.NDArray[np.floating] | float
432
+ aircraft wingspan, [:math:`m`]
433
+ air_temperature : npt.NDArray[np.floating]
434
+ ambient temperature for each waypoint, [:math:`K`]
435
+ air_pressure : npt.NDArray[np.floating]
436
+ pressure altitude at each waypoint, [:math:`Pa`]
437
+ true_airspeed : npt.NDArray[np.floating]
438
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
439
+ aircraft_mass : npt.NDArray[np.floating]
440
+ aircraft mass for each waypoint, [:math:`kg`]
441
+
442
+ Returns
443
+ -------
444
+ npt.NDArray[np.floating]
445
+ Initial wake vortex circulation, [:math:`m^{2} s^{-1}`]
446
+
447
+ Notes
448
+ -----
449
+ - This is a measure of the strength/intensity of the wake vortex circulation.
450
+ - See eq. (A1) in :cite:`unterstrasserPropertiesYoungContrails2016`.
451
+ """
452
+ b_0 = wake_vortex_separation(wingspan)
453
+ rho_air = thermo.rho_d(air_temperature, air_pressure)
454
+ return (constants.g * aircraft_mass) / (rho_air * b_0 * true_airspeed)
455
+
456
+
457
+ def _survival_fraction_from_length_scale(
458
+ z_total: npt.NDArray[np.floating],
459
+ ) -> npt.NDArray[np.floating]:
460
+ """
461
+ Calculate fraction of ice particle number surviving the wake vortex phase.
462
+
463
+ Parameters
464
+ ----------
465
+ z_total : npt.NDArray[np.floating]
466
+ Total length-scale effect of the wake vortex downwash, [:math:`m`]
467
+
468
+ Returns
469
+ -------
470
+ npt.NDArray[np.floating]
471
+ Fraction of ice particle number surviving the wake vortex phase
472
+ """
473
+ f_surv = 0.42 + (1.31 / np.pi) * np.arctan(-1.00 + (z_total / 100.0))
474
+ np.clip(f_surv, 0.0, 1.0, out=f_surv)
475
+ return f_surv
476
+
477
+
478
+ def initial_contrail_depth(
479
+ z_desc: npt.NDArray[np.floating],
480
+ f_surv: npt.NDArray[np.floating],
481
+ ) -> npt.NDArray[np.floating]:
482
+ """Calculate initial contrail depth using :cite:`unterstrasserPropertiesYoungContrails2016`.
483
+
484
+ Parameters
485
+ ----------
486
+ z_desc : npt.NDArray[np.floating]
487
+ Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
488
+ [:math:`m`].
489
+ f_surv : npt.NDArray[np.floating]
490
+ Fraction of contrail ice particle number that survive the wake vortex phase.
491
+ See :func:`ice_particle_survival_fraction`.
492
+
493
+ Returns
494
+ -------
495
+ npt.NDArray[np.floating]
496
+ Initial contrail depth, [:math:`m`]
497
+
498
+ Notes
499
+ -----
500
+ - See eq. (12), and (13) in :cite:`unterstrasserPropertiesYoungContrails2016`.
501
+ - For consistency in CoCiP, `z_desc` should be calculated using :func:`dz_max` instead of
502
+ using :func:`z_desc_length_scale`.
503
+ """
504
+ return z_desc * np.where(
505
+ f_surv <= 0.2,
506
+ 6.0 * f_surv,
507
+ 0.15 * f_surv + (6.0 - 0.15) * 0.2,
508
+ )