pycontrails 0.49.5__cp311-cp311-macosx_11_0_arm64.whl → 0.50.1__cp311-cp311-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 (32) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/datalib.py +60 -38
  3. pycontrails/core/flight.py +11 -6
  4. pycontrails/core/interpolation.py +39 -1
  5. pycontrails/core/met.py +14 -16
  6. pycontrails/core/met_var.py +2 -2
  7. pycontrails/core/models.py +7 -3
  8. pycontrails/core/rgi_cython.cpython-311-darwin.so +0 -0
  9. pycontrails/core/vector.py +15 -13
  10. pycontrails/datalib/ecmwf/__init__.py +4 -0
  11. pycontrails/datalib/ecmwf/arco_era5.py +577 -0
  12. pycontrails/datalib/ecmwf/common.py +1 -1
  13. pycontrails/datalib/ecmwf/era5.py +2 -5
  14. pycontrails/datalib/ecmwf/variables.py +18 -0
  15. pycontrails/datalib/gfs/gfs.py +2 -2
  16. pycontrails/datalib/goes.py +14 -12
  17. pycontrails/models/cocip/cocip.py +48 -8
  18. pycontrails/models/cocip/cocip_params.py +20 -1
  19. pycontrails/models/cocip/contrail_properties.py +4 -9
  20. pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
  21. pycontrails/models/cocip/wake_vortex.py +22 -1
  22. pycontrails/models/cocipgrid/cocip_grid.py +103 -6
  23. pycontrails/models/cocipgrid/cocip_grid_params.py +25 -19
  24. pycontrails/models/issr.py +1 -1
  25. pycontrails/physics/constants.py +6 -0
  26. pycontrails/utils/dependencies.py +13 -11
  27. {pycontrails-0.49.5.dist-info → pycontrails-0.50.1.dist-info}/METADATA +4 -2
  28. {pycontrails-0.49.5.dist-info → pycontrails-0.50.1.dist-info}/RECORD +32 -30
  29. {pycontrails-0.49.5.dist-info → pycontrails-0.50.1.dist-info}/WHEEL +1 -1
  30. {pycontrails-0.49.5.dist-info → pycontrails-0.50.1.dist-info}/LICENSE +0 -0
  31. {pycontrails-0.49.5.dist-info → pycontrails-0.50.1.dist-info}/NOTICE +0 -0
  32. {pycontrails-0.49.5.dist-info → pycontrails-0.50.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,403 @@
1
+ """Wave-vortex downwash functions from Unterstrasser (2016).
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 was developed based on output from large eddy simulations, and improves agreement with
8
+ LES outputs relative to the default survival fraction parameterization used in CoCiP.
9
+
10
+ For comparison, CoCiP assumes that ``f_surv`` is equal to the change in the contrail ice water
11
+ content (by mass) before and after the wake vortex phase. However, for larger (smaller) ice
12
+ particles, their survival fraction by number could be smaller (larger) than their survival fraction
13
+ by mass. This is particularly important in the "soot-poor" scenario, for example, in cleaner
14
+ lean-burn engines where their soot emissions can be 3-4 orders of magnitude lower than conventional
15
+ RQL engines.
16
+
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import numpy as np
22
+ import numpy.typing as npt
23
+
24
+ from pycontrails.models.cocip.wake_vortex import wake_vortex_separation
25
+ from pycontrails.physics import constants, thermo
26
+
27
+
28
+ def ice_particle_number_survival_fraction(
29
+ air_temperature: npt.NDArray[np.float64],
30
+ rhi_0: npt.NDArray[np.float64],
31
+ ei_h2o: npt.NDArray[np.float64] | float,
32
+ wingspan: npt.NDArray[np.float64] | float,
33
+ true_airspeed: npt.NDArray[np.float64],
34
+ fuel_flow: npt.NDArray[np.float64],
35
+ aei_n: npt.NDArray[np.float64],
36
+ z_desc: npt.NDArray[np.float64],
37
+ ) -> npt.NDArray[np.float64]:
38
+ r"""
39
+ Calculate fraction of ice particle number surviving the wake vortex phase and required inputs.
40
+
41
+ This implementation is based on the work of :cite:`unterstrasserPropertiesYoungContrails2016`
42
+ and is an improved estimation compared with
43
+ :func:`contrail_properties.ice_particle_survival_fraction`.
44
+
45
+ Parameters
46
+ ----------
47
+ air_temperature : npt.NDArray[np.float64]
48
+ ambient temperature for each waypoint, [:math:`K`]
49
+ rhi_0: npt.NDArray[np.float64]
50
+ Relative humidity with respect to ice at the flight waypoint
51
+ ei_h2o : npt.NDArray[np.float64] | float
52
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
53
+ wingspan : npt.NDArray[np.float64] | float
54
+ aircraft wingspan, [:math:`m`]
55
+ true_airspeed : npt.NDArray[np.float64]
56
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
57
+ fuel_flow : npt.NDArray[np.float64]
58
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
59
+ aei_n : npt.NDArray[np.float64]
60
+ Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
61
+ z_desc : npt.NDArray[np.float64]
62
+ Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
63
+ [:math:`m`].
64
+
65
+ Returns
66
+ -------
67
+ npt.NDArray[np.float64]
68
+ Fraction of contrail ice particle number that survive the wake vortex phase.
69
+
70
+ References
71
+ ----------
72
+ - :cite:`unterstrasserPropertiesYoungContrails2016`
73
+
74
+ Notes
75
+ -----
76
+ - See eq. (3), (9), and (10) in :cite:`unterstrasserPropertiesYoungContrails2016`.
77
+ - For consistency in CoCiP, ``z_desc`` should be calculated using :func:`dz_max` instead of
78
+ using :func:`z_desc_length_scale`.
79
+ """
80
+ # Length scales
81
+ z_atm = z_atm_length_scale(air_temperature, rhi_0)
82
+ rho_emit = emitted_water_vapour_concentration(ei_h2o, wingspan, true_airspeed, fuel_flow)
83
+ z_emit = z_emit_length_scale(rho_emit, air_temperature)
84
+ z_total = z_total_length_scale(aei_n, z_atm, z_emit, z_desc)
85
+ return _survival_fraction_from_length_scale(z_total)
86
+
87
+
88
+ def z_total_length_scale(
89
+ aei_n: npt.NDArray[np.float64],
90
+ z_atm: npt.NDArray[np.float64],
91
+ z_emit: npt.NDArray[np.float64],
92
+ z_desc: npt.NDArray[np.float64],
93
+ ) -> npt.NDArray[np.float64]:
94
+ """
95
+ Calculate the total length-scale effect of the wake vortex downwash.
96
+
97
+ Parameters
98
+ ----------
99
+ aei_n : npt.NDArray[np.float64]
100
+ Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
101
+ z_atm : npt.NDArray[np.float64]
102
+ Length-scale effect of ambient supersaturation on the ice crystal mass budget, [:math:`m`]
103
+ z_emit : npt.NDArray[np.float64]
104
+ Length-scale effect of water vapour emissions on the ice crystal mass budget, [:math:`m`]
105
+ z_desc : npt.NDArray[np.float64]
106
+ Final vertical displacement of the wake vortex, `dz_max` in `wake_vortex.py`, [:math:`m`]
107
+
108
+ Returns
109
+ -------
110
+ npt.NDArray[np.float64]
111
+ Total length-scale effect of the wake vortex downwash, [:math:`m`]
112
+ """
113
+ alpha_base = (aei_n / 2.8e14) ** (-0.18)
114
+ alpha_atm = 1.7 * alpha_base
115
+ alpha_emit = 1.15 * alpha_base
116
+
117
+ z_total = alpha_atm * z_atm + alpha_emit * z_emit - 0.6 * z_desc
118
+
119
+ z_total.clip(min=0.0, out=z_total)
120
+ return z_total
121
+
122
+
123
+ def z_atm_length_scale(
124
+ air_temperature: npt.NDArray[np.float64],
125
+ rhi_0: npt.NDArray[np.float64],
126
+ *,
127
+ n_iter: int = 10,
128
+ ) -> npt.NDArray[np.float64]:
129
+ """Calculate the length-scale effect of ambient supersaturation on the ice crystal mass budget.
130
+
131
+ Parameters
132
+ ----------
133
+ air_temperature : npt.NDArray[np.float64]
134
+ Ambient temperature for each waypoint, [:math:`K`].
135
+ rhi_0 : npt.NDArray[np.float64]
136
+ Relative humidity with respect to ice at the flight waypoint.
137
+ n_iter : int
138
+ Number of iterations, set to 10 as default where ``z_atm`` is accurate to within +-1 m.
139
+
140
+ Returns
141
+ -------
142
+ npt.NDArray[np.float64]
143
+ The effect of the ambient supersaturation on the ice crystal mass budget,
144
+ provided as a length scale equivalent, [:math:`m`].
145
+
146
+ Notes
147
+ -----
148
+ - See eq. (5) in :cite:`unterstrasserPropertiesYoungContrails2016`.
149
+ """
150
+ # Only perform operation when the ambient condition is supersaturated w.r.t. ice
151
+ issr = rhi_0 > 1.0
152
+
153
+ rhi_issr = rhi_0[issr]
154
+ air_temperature_issr = air_temperature[issr]
155
+
156
+ # Solve non-linear equation numerically using the bisection method
157
+ # Did not use scipy functions because it is unstable when dealing with np.arrays
158
+ z_1 = np.zeros_like(rhi_issr)
159
+ z_2 = np.full_like(rhi_issr, 1000.0)
160
+ lhs = rhi_issr * thermo.e_sat_ice(air_temperature_issr) / air_temperature_issr
161
+
162
+ dry_adiabatic_lapse_rate = constants.g / constants.c_pd
163
+ for _ in range(n_iter):
164
+ z_est = 0.5 * (z_1 + z_2)
165
+ rhs = (thermo.e_sat_ice(air_temperature_issr + dry_adiabatic_lapse_rate * z_est)) / (
166
+ air_temperature_issr + dry_adiabatic_lapse_rate * z_est
167
+ )
168
+ z_1[lhs > rhs] = z_est[lhs > rhs]
169
+ z_2[lhs < rhs] = z_est[lhs < rhs]
170
+
171
+ out = np.zeros_like(rhi_0)
172
+ out[issr] = 0.5 * (z_1 + z_2)
173
+ return out
174
+
175
+
176
+ def emitted_water_vapour_concentration(
177
+ ei_h2o: npt.NDArray[np.float64] | float,
178
+ wingspan: npt.NDArray[np.float64] | float,
179
+ true_airspeed: npt.NDArray[np.float64],
180
+ fuel_flow: npt.NDArray[np.float64],
181
+ ) -> npt.NDArray[np.float64]:
182
+ r"""
183
+ Calculate aircraft-emitted water vapour concentration in the plume.
184
+
185
+ Parameters
186
+ ----------
187
+ ei_h2o : npt.NDArray[np.float64] | float
188
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
189
+ wingspan : npt.NDArray[np.float64] | float
190
+ aircraft wingspan, [:math:`m`]
191
+ true_airspeed : npt.NDArray[np.float64]
192
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
193
+ fuel_flow : npt.NDArray[np.float64]
194
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
195
+
196
+ Returns
197
+ -------
198
+ npt.NDArray[np.float64]
199
+ Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
200
+
201
+ Notes
202
+ -----
203
+ - See eq. (6) and (A8) in :cite:`unterstrasserPropertiesYoungContrails2016`.
204
+ """
205
+ h2o_per_dist = (ei_h2o * fuel_flow) / true_airspeed
206
+ area_p = plume_area(wingspan)
207
+ return h2o_per_dist / area_p
208
+
209
+
210
+ def z_emit_length_scale(
211
+ rho_emit: npt.NDArray[np.float64], air_temperature: npt.NDArray[np.float64], *, n_iter: int = 10
212
+ ) -> npt.NDArray[np.float64]:
213
+ """Calculate the length-scale effect of water vapour emissions on the ice crystal mass budget.
214
+
215
+ Parameters
216
+ ----------
217
+ rho_emit : npt.NDArray[np.float64] | float
218
+ Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
219
+ air_temperature : npt.NDArray[np.float64]
220
+ ambient temperature for each waypoint, [:math:`K`]
221
+ n_iter : int
222
+ Number of iterations, set to 10 as default where ``z_emit`` is accurate to within +-1 m.
223
+
224
+ Returns
225
+ -------
226
+ npt.NDArray[np.float64]
227
+ The effect of the aircraft water vapour emission on the ice crystal mass budget,
228
+ provided as a length scale equivalent, [:math:`m`]
229
+
230
+ Notes
231
+ -----
232
+ - See eq. (7) in :cite:`unterstrasserPropertiesYoungContrails2016`.
233
+ """
234
+ # Solve non-linear equation numerically using the bisection method
235
+ # Did not use scipy functions because it is unstable when dealing with np.arrays
236
+ z_1 = np.zeros_like(rho_emit)
237
+ z_2 = np.full_like(rho_emit, 1000.0)
238
+
239
+ lhs = (thermo.e_sat_ice(air_temperature) / (constants.R_v * air_temperature)) + rho_emit
240
+
241
+ dry_adiabatic_lapse_rate = constants.g / constants.c_pd
242
+ for _ in range(n_iter):
243
+ z_est = 0.5 * (z_1 + z_2)
244
+ rhs = thermo.e_sat_ice(air_temperature + dry_adiabatic_lapse_rate * z_est) / (
245
+ constants.R_v * (air_temperature + dry_adiabatic_lapse_rate * z_est)
246
+ )
247
+ z_1[lhs > rhs] = z_est[lhs > rhs]
248
+ z_2[lhs < rhs] = z_est[lhs < rhs]
249
+
250
+ return 0.5 * (z_1 + z_2)
251
+
252
+
253
+ def plume_area(wingspan: npt.NDArray[np.float64] | float) -> npt.NDArray[np.float64] | float:
254
+ """Calculate area of the wake-vortex plume.
255
+
256
+ Parameters
257
+ ----------
258
+ wingspan : npt.NDArray[np.float64] | float
259
+ aircraft wingspan, [:math:`m`]
260
+
261
+ Returns
262
+ -------
263
+ float
264
+ Area of two wake-vortex plumes, [:math:`m^{2}`]
265
+
266
+ Notes
267
+ -----
268
+ - See eq. (A6) and (A7) in :cite:`unterstrasserPropertiesYoungContrails2016`.
269
+ """
270
+ r_plume = 1.5 + 0.314 * wingspan
271
+ return 2.0 * 2.0 * np.pi * r_plume**2
272
+
273
+
274
+ def z_desc_length_scale(
275
+ wingspan: npt.NDArray[np.float64] | float,
276
+ air_temperature: npt.NDArray[np.float64],
277
+ air_pressure: npt.NDArray[np.float64],
278
+ true_airspeed: npt.NDArray[np.float64],
279
+ aircraft_mass: npt.NDArray[np.float64],
280
+ dT_dz: npt.NDArray[np.float64],
281
+ ) -> npt.NDArray[np.float64]:
282
+ """Calculate the final vertical displacement of the wake vortex.
283
+
284
+ Parameters
285
+ ----------
286
+ wingspan : npt.NDArray[np.float64] | float
287
+ aircraft wingspan, [:math:`m`]
288
+ air_temperature : npt.NDArray[np.float64]
289
+ ambient temperature for each waypoint, [:math:`K`]
290
+ air_pressure : npt.NDArray[np.float64]
291
+ pressure altitude at each waypoint, [:math:`Pa`]
292
+ true_airspeed : npt.NDArray[np.float64]
293
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
294
+ aircraft_mass : npt.NDArray[np.float64] | float
295
+ aircraft mass for each waypoint, [:math:`kg`]
296
+ dT_dz : npt.NDArray[np.float64]
297
+ potential temperature gradient, [:math:`K m^{-1}`]
298
+
299
+ Returns
300
+ -------
301
+ npt.NDArray[np.float64]
302
+ Final vertical displacement of the wake vortex, [:math:`m`]
303
+
304
+ Notes
305
+ -----
306
+ - See eq. (4) in :cite:`unterstrasserPropertiesYoungContrails2016`.
307
+ """
308
+ gamma_0 = _initial_wake_vortex_circulation(
309
+ wingspan, air_temperature, air_pressure, true_airspeed, aircraft_mass
310
+ )
311
+ n_bv = thermo.brunt_vaisala_frequency(air_pressure, air_temperature, dT_dz)
312
+ return ((8.0 * gamma_0) / (np.pi * n_bv)) ** 0.5
313
+
314
+
315
+ def _initial_wake_vortex_circulation(
316
+ wingspan: npt.NDArray[np.float64] | float,
317
+ air_temperature: npt.NDArray[np.float64],
318
+ air_pressure: npt.NDArray[np.float64],
319
+ true_airspeed: npt.NDArray[np.float64],
320
+ aircraft_mass: npt.NDArray[np.float64],
321
+ ) -> npt.NDArray[np.float64]:
322
+ """Calculate initial wake vortex circulation.
323
+
324
+ Parameters
325
+ ----------
326
+ wingspan : npt.NDArray[np.float64] | float
327
+ aircraft wingspan, [:math:`m`]
328
+ air_temperature : npt.NDArray[np.float64]
329
+ ambient temperature for each waypoint, [:math:`K`]
330
+ air_pressure : npt.NDArray[np.float64]
331
+ pressure altitude at each waypoint, [:math:`Pa`]
332
+ true_airspeed : npt.NDArray[np.float64]
333
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
334
+ aircraft_mass : npt.NDArray[np.float64] | float
335
+ aircraft mass for each waypoint, [:math:`kg`]
336
+
337
+ Returns
338
+ -------
339
+ npt.NDArray[np.float64]
340
+ Initial wake vortex circulation, [:math:`m^{2} s^{-1}`]
341
+
342
+ Notes
343
+ -----
344
+ - This is a measure of the strength/intensity of the wake vortex circulation.
345
+ - See eq. (A1) in :cite:`unterstrasserPropertiesYoungContrails2016`.
346
+ """
347
+ b_0 = wake_vortex_separation(wingspan)
348
+ rho_air = thermo.rho_d(air_temperature, air_pressure)
349
+ return (constants.g * aircraft_mass) / (rho_air * b_0 * true_airspeed)
350
+
351
+
352
+ def _survival_fraction_from_length_scale(
353
+ z_total: npt.NDArray[np.float64],
354
+ ) -> npt.NDArray[np.float64]:
355
+ """
356
+ Calculate fraction of ice particle number surviving the wake vortex phase.
357
+
358
+ Parameters
359
+ ----------
360
+ z_total : npt.NDArray[np.float64]
361
+ Total length-scale effect of the wake vortex downwash, [:math:`m`]
362
+
363
+ Returns
364
+ -------
365
+ npt.NDArray[np.float64]
366
+ Fraction of ice particle number surviving the wake vortex phase
367
+ """
368
+ f_surv = 0.45 + (1.19 / np.pi) * np.arctan(-1.35 + (z_total / 100.0))
369
+ np.clip(f_surv, 0.0, 1.0, out=f_surv)
370
+ return f_surv
371
+
372
+
373
+ def initial_contrail_depth(
374
+ z_desc: npt.NDArray[np.float64],
375
+ f_surv: npt.NDArray[np.float64],
376
+ ) -> npt.NDArray[np.float64]:
377
+ """Calculate initial contrail depth using :cite:`unterstrasserPropertiesYoungContrails2016`.
378
+
379
+ Parameters
380
+ ----------
381
+ z_desc : npt.NDArray[np.float64]
382
+ Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
383
+ [:math:`m`].
384
+ f_surv : npt.NDArray[np.float64]
385
+ Fraction of contrail ice particle number that survive the wake vortex phase.
386
+ See :func:`ice_particle_survival_fraction`.
387
+
388
+ Returns
389
+ -------
390
+ npt.NDArray[np.float64]
391
+ Initial contrail depth, [:math:`m`]
392
+
393
+ Notes
394
+ -----
395
+ - See eq. (12), and (13) in :cite:`unterstrasserPropertiesYoungContrails2016`.
396
+ - For consistency in CoCiP, `z_desc` should be calculated using :func:`dz_max` instead of
397
+ using :func:`z_desc_length_scale`.
398
+ """
399
+ return z_desc * np.where(
400
+ f_surv <= 0.2,
401
+ 6.0 * f_surv,
402
+ 0.15 * f_surv + (6.0 - 0.15) * 0.2,
403
+ )
@@ -1,4 +1,25 @@
1
- """Wave-vortex downwash functions."""
1
+ """Wave-vortex downwash functions.
2
+
3
+ This module includes equations from the original CoCiP model
4
+ :cite:`schumannContrailCirrusPrediction2012`. An alternative set of equations based on
5
+ :cite:`unterstrasserPropertiesYoungContrails2016` is available in
6
+ :py:mod:`unterstrasser_wake_vortex`.
7
+
8
+ Unterstrasser Notes
9
+ -------------------
10
+
11
+ Improved estimation of the survival fraction of the contrail ice crystal number ``f_surv``
12
+ during the wake-vortex phase. This is a parameterised model that is developed based on
13
+ outputs provided by large eddy simulations.
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
+ """
2
23
 
3
24
  from __future__ import annotations
4
25
 
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, NoReturn, TypeVar, overload
11
11
  import numpy as np
12
12
  import numpy.typing as npt
13
13
  import pandas as pd
14
+ import xarray as xr
14
15
 
15
16
  import pycontrails
16
17
  from pycontrails.core import models
@@ -20,7 +21,7 @@ from pycontrails.models import humidity_scaling, sac
20
21
  from pycontrails.models.cocip import cocip, contrail_properties, wake_vortex, wind_shear
21
22
  from pycontrails.models.cocipgrid.cocip_grid_params import CocipGridParams
22
23
  from pycontrails.models.emissions import Emissions
23
- from pycontrails.physics import geo, thermo, units
24
+ from pycontrails.physics import constants, geo, thermo, units
24
25
  from pycontrails.utils import dependencies
25
26
 
26
27
  if TYPE_CHECKING:
@@ -122,6 +123,13 @@ class CocipGrid(models.Model):
122
123
  msg = "Parameter 'radiative_heating_effects' is not yet implemented in CocipGrid"
123
124
  raise NotImplementedError(msg)
124
125
 
126
+ if self.params["unterstrasser_ice_survival_fraction"]:
127
+ msg = (
128
+ "Parameter 'unterstrasser_ice_survival_fraction' is not "
129
+ "yet implemented in CocipGrid"
130
+ )
131
+ raise NotImplementedError(msg)
132
+
125
133
  self._target_dtype = np.result_type(*self.met.data.values())
126
134
 
127
135
  @overload
@@ -304,23 +312,76 @@ class CocipGrid(models.Model):
304
312
  ``time_end``, new slices are selected from the larger ``self.met`` and
305
313
  ``self.rad`` data. The slicing only occurs in the time domain.
306
314
 
315
+ The end of currently-used ``met`` and ``rad`` will be used as the start
316
+ of newly-selected met slices when possible to avoid losing and re-loading
317
+ already-loaded met data.
318
+
307
319
  If ``self.params["downselect_met"]`` is True, :func:`_downselect_met` has
308
320
  already performed a spatial downselection of the met data.
309
321
  """
310
- if met is None or time_end > met.indexes["time"].to_numpy()[-1]:
322
+
323
+ if met is None:
311
324
  # idx is the first index at which self.met.variables["time"].to_numpy() >= time_end
312
325
  idx = np.searchsorted(self.met.indexes["time"].to_numpy(), time_end)
313
326
  sl = slice(max(0, idx - 1), idx + 1)
314
327
  logger.debug("Select met slice %s", sl)
315
328
  met = MetDataset(self.met.data.isel(time=sl), copy=False)
316
329
 
317
- if rad is None or time_end > rad.indexes["time"].to_numpy()[-1]:
330
+ elif time_end > met.indexes["time"].to_numpy()[-1]:
331
+ current_times = met.indexes["time"].to_numpy()
332
+ all_times = self.met.indexes["time"].to_numpy()
333
+ # idx is the first index at which all_times >= time_end
334
+ idx = np.searchsorted(all_times, time_end)
335
+ sl = slice(max(0, idx - 1), idx + 1)
336
+
337
+ # case 1: cannot re-use end of current met as start of new met
338
+ if current_times[-1] != all_times[sl.start]:
339
+ logger.debug("Select met slice %s", sl)
340
+ met = MetDataset(self.met.data.isel(time=sl), copy=False)
341
+ # case 2: can re-use end of current met plus one step of new met
342
+ elif sl.start < all_times.size - 1:
343
+ sl = slice(sl.start + 1, sl.stop)
344
+ logger.debug("Reuse end of met and select met slice %s", sl)
345
+ met = MetDataset(
346
+ xr.concat((met.data.isel(time=[-1]), self.met.data.isel(time=sl)), dim="time"),
347
+ copy=False,
348
+ )
349
+ # case 3: can re-use end of current met and nothing else
350
+ else:
351
+ logger.debug("Reuse end of met")
352
+ met = MetDataset(met.data.isel(time=[-1]), copy=False)
353
+
354
+ if rad is None:
318
355
  # idx is the first index at which self.rad.variables["time"].to_numpy() >= time_end
319
356
  idx = np.searchsorted(self.rad.indexes["time"].to_numpy(), time_end)
320
357
  sl = slice(max(0, idx - 1), idx + 1)
321
358
  logger.debug("Select rad slice %s", sl)
322
359
  rad = MetDataset(self.rad.data.isel(time=sl), copy=False)
323
360
 
361
+ elif time_end > rad.indexes["time"].to_numpy()[-1]:
362
+ current_times = rad.indexes["time"].to_numpy()
363
+ all_times = self.rad.indexes["time"].to_numpy()
364
+ # idx is the first index at which all_times >= time_end
365
+ idx = np.searchsorted(all_times, time_end)
366
+ sl = slice(max(0, idx - 1), idx + 1)
367
+
368
+ # case 1: cannot re-use end of current rad as start of new rad
369
+ if current_times[-1] != all_times[sl.start]:
370
+ logger.debug("Select rad slice %s", sl)
371
+ rad = MetDataset(self.rad.data.isel(time=sl), copy=False)
372
+ # case 2: can re-use end of current rad plus one step of new rad
373
+ elif sl.start < all_times.size - 1:
374
+ sl = slice(sl.start + 1, sl.stop)
375
+ logger.debug("Reuse end of rad and select rad slice %s", sl)
376
+ rad = MetDataset(
377
+ xr.concat((rad.data.isel(time=[-1]), self.rad.data.isel(time=sl)), dim="time"),
378
+ copy=False,
379
+ )
380
+ # case 3: can re-use end of current rad and nothing else
381
+ else:
382
+ logger.debug("Reuse end of rad")
383
+ rad = MetDataset(rad.data.isel(time=[-1]), copy=False)
384
+
324
385
  return met, rad
325
386
 
326
387
  def _attach_verbose_outputs_evolution(self, contrail_list: list[GeoVectorDataset]) -> None:
@@ -398,6 +459,17 @@ class CocipGrid(models.Model):
398
459
  nominal_segment_length=segment_length,
399
460
  attrs=attrs,
400
461
  )
462
+
463
+ if self.params["compute_atr20"]:
464
+ self.source["global_yearly_mean_rf_per_m"] = (
465
+ self.source["ef_per_m"].data
466
+ / constants.surface_area_earth
467
+ / constants.seconds_per_year
468
+ )
469
+ self.source["atr20_per_m"] = (
470
+ self.params["global_rf_to_atr20_factor"]
471
+ * self.source["global_yearly_mean_rf_per_m"].data
472
+ )
401
473
  else:
402
474
  self.source = result_merge_source(
403
475
  result=summary,
@@ -406,6 +478,18 @@ class CocipGrid(models.Model):
406
478
  nominal_segment_length=segment_length,
407
479
  attrs=attrs,
408
480
  )
481
+
482
+ if self.params["compute_atr20"]:
483
+ self.source["global_yearly_mean_rf_per_m"] = (
484
+ self.source["ef_per_m"]
485
+ / constants.surface_area_earth
486
+ / constants.seconds_per_year
487
+ )
488
+ self.source["atr20_per_m"] = (
489
+ self.params["global_rf_to_atr20_factor"]
490
+ * self.source["global_yearly_mean_rf_per_m"]
491
+ )
492
+
409
493
  return self.source
410
494
 
411
495
  # ---------------------------
@@ -1459,11 +1543,11 @@ def find_initial_persistent_contrails(
1459
1543
  air_pressure_1=air_pressure_1,
1460
1544
  )
1461
1545
  iwc_1 = contrail_properties.iwc_post_wake_vortex(iwc, iwc_ad)
1546
+ f_surv = contrail_properties.ice_particle_survival_fraction(iwc, iwc_1)
1462
1547
  n_ice_per_m = contrail_properties.ice_particle_number(
1463
1548
  nvpm_ei_n=nvpm_ei_n,
1464
1549
  fuel_dist=fuel_dist,
1465
- iwc=iwc,
1466
- iwc_1=iwc_1,
1550
+ f_surv=f_surv,
1467
1551
  air_temperature=air_temperature,
1468
1552
  T_crit_sac=T_crit_sac,
1469
1553
  min_ice_particle_number_nvpm_ei_n=params["min_ice_particle_number_nvpm_ei_n"],
@@ -2241,6 +2325,14 @@ def _contrail_grid_variable_attrs() -> dict[str, dict[str, str]]:
2241
2325
  "long_name": "Ice water content after the wake vortex phase",
2242
2326
  "units": "kg_h2o / kg_air",
2243
2327
  },
2328
+ "global_yearly_mean_rf_per_m": {
2329
+ "long_name": "Global yearly mean RF per meter of flight trajectory",
2330
+ "units": "W / m**2 / m",
2331
+ },
2332
+ "atr20_per_m": {
2333
+ "long_name": "Average Temperature Response over a 20 year horizon",
2334
+ "units": "K / m",
2335
+ },
2244
2336
  }
2245
2337
 
2246
2338
 
@@ -2249,7 +2341,12 @@ def _supported_verbose_outputs_formation() -> set[str]:
2249
2341
 
2250
2342
  Uses output of :func:`_contrail_grid_variable_attrs` as a source of truth.
2251
2343
  """
2252
- return set(_contrail_grid_variable_attrs()) - {"contrail_age", "ef_per_m"}
2344
+ return set(_contrail_grid_variable_attrs()) - {
2345
+ "contrail_age",
2346
+ "ef_per_m",
2347
+ "global_yearly_mean_rf_per_m",
2348
+ "atr20_per_m",
2349
+ }
2253
2350
 
2254
2351
 
2255
2352
  def _warn_not_wrap(met: MetDataset) -> None: