pycontrails 0.53.0__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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 +2312 -0
  10. pycontrails/core/flightplan.py +220 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +721 -0
  13. pycontrails/core/met.py +2833 -0
  14. pycontrails/core/met_var.py +307 -0
  15. pycontrails/core/models.py +1181 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-313-x86_64-linux-gnu.so +0 -0
  18. pycontrails/core/vector.py +2191 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_leo_utils/search.py +250 -0
  21. pycontrails/datalib/_leo_utils/static/bq_roi_query.sql +6 -0
  22. pycontrails/datalib/_leo_utils/vis.py +59 -0
  23. pycontrails/datalib/_met_utils/metsource.py +743 -0
  24. pycontrails/datalib/ecmwf/__init__.py +53 -0
  25. pycontrails/datalib/ecmwf/arco_era5.py +527 -0
  26. pycontrails/datalib/ecmwf/common.py +109 -0
  27. pycontrails/datalib/ecmwf/era5.py +538 -0
  28. pycontrails/datalib/ecmwf/era5_model_level.py +482 -0
  29. pycontrails/datalib/ecmwf/hres.py +782 -0
  30. pycontrails/datalib/ecmwf/hres_model_level.py +495 -0
  31. pycontrails/datalib/ecmwf/ifs.py +284 -0
  32. pycontrails/datalib/ecmwf/model_levels.py +79 -0
  33. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  34. pycontrails/datalib/ecmwf/variables.py +256 -0
  35. pycontrails/datalib/gfs/__init__.py +28 -0
  36. pycontrails/datalib/gfs/gfs.py +646 -0
  37. pycontrails/datalib/gfs/variables.py +100 -0
  38. pycontrails/datalib/goes.py +772 -0
  39. pycontrails/datalib/landsat.py +568 -0
  40. pycontrails/datalib/sentinel.py +512 -0
  41. pycontrails/datalib/spire.py +739 -0
  42. pycontrails/ext/bada.py +41 -0
  43. pycontrails/ext/cirium.py +14 -0
  44. pycontrails/ext/empirical_grid.py +140 -0
  45. pycontrails/ext/synthetic_flight.py +426 -0
  46. pycontrails/models/__init__.py +1 -0
  47. pycontrails/models/accf.py +406 -0
  48. pycontrails/models/apcemm/__init__.py +8 -0
  49. pycontrails/models/apcemm/apcemm.py +983 -0
  50. pycontrails/models/apcemm/inputs.py +226 -0
  51. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  52. pycontrails/models/apcemm/utils.py +437 -0
  53. pycontrails/models/cocip/__init__.py +29 -0
  54. pycontrails/models/cocip/cocip.py +2617 -0
  55. pycontrails/models/cocip/cocip_params.py +299 -0
  56. pycontrails/models/cocip/cocip_uncertainty.py +285 -0
  57. pycontrails/models/cocip/contrail_properties.py +1517 -0
  58. pycontrails/models/cocip/output_formats.py +2261 -0
  59. pycontrails/models/cocip/radiative_forcing.py +1262 -0
  60. pycontrails/models/cocip/radiative_heating.py +520 -0
  61. pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
  62. pycontrails/models/cocip/wake_vortex.py +396 -0
  63. pycontrails/models/cocip/wind_shear.py +120 -0
  64. pycontrails/models/cocipgrid/__init__.py +9 -0
  65. pycontrails/models/cocipgrid/cocip_grid.py +2573 -0
  66. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  67. pycontrails/models/dry_advection.py +486 -0
  68. pycontrails/models/emissions/__init__.py +21 -0
  69. pycontrails/models/emissions/black_carbon.py +594 -0
  70. pycontrails/models/emissions/emissions.py +1353 -0
  71. pycontrails/models/emissions/ffm2.py +336 -0
  72. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  73. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  74. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  75. pycontrails/models/humidity_scaling/__init__.py +37 -0
  76. pycontrails/models/humidity_scaling/humidity_scaling.py +1025 -0
  77. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  78. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  79. pycontrails/models/issr.py +210 -0
  80. pycontrails/models/pcc.py +327 -0
  81. pycontrails/models/pcr.py +154 -0
  82. pycontrails/models/ps_model/__init__.py +17 -0
  83. pycontrails/models/ps_model/ps_aircraft_params.py +376 -0
  84. pycontrails/models/ps_model/ps_grid.py +505 -0
  85. pycontrails/models/ps_model/ps_model.py +1017 -0
  86. pycontrails/models/ps_model/ps_operational_limits.py +540 -0
  87. pycontrails/models/ps_model/static/ps-aircraft-params-20240524.csv +68 -0
  88. pycontrails/models/ps_model/static/ps-synonym-list-20240524.csv +103 -0
  89. pycontrails/models/sac.py +459 -0
  90. pycontrails/models/tau_cirrus.py +168 -0
  91. pycontrails/physics/__init__.py +1 -0
  92. pycontrails/physics/constants.py +116 -0
  93. pycontrails/physics/geo.py +989 -0
  94. pycontrails/physics/jet.py +837 -0
  95. pycontrails/physics/thermo.py +451 -0
  96. pycontrails/physics/units.py +472 -0
  97. pycontrails/py.typed +0 -0
  98. pycontrails/utils/__init__.py +1 -0
  99. pycontrails/utils/dependencies.py +66 -0
  100. pycontrails/utils/iteration.py +13 -0
  101. pycontrails/utils/json.py +188 -0
  102. pycontrails/utils/temp.py +50 -0
  103. pycontrails/utils/types.py +165 -0
  104. pycontrails-0.53.0.dist-info/LICENSE +178 -0
  105. pycontrails-0.53.0.dist-info/METADATA +181 -0
  106. pycontrails-0.53.0.dist-info/NOTICE +43 -0
  107. pycontrails-0.53.0.dist-info/RECORD +109 -0
  108. pycontrails-0.53.0.dist-info/WHEEL +6 -0
  109. pycontrails-0.53.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,138 @@
1
+ """Default :class:`CocipGrid` parameters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+
7
+ from pycontrails.core.aircraft_performance import AircraftPerformanceGrid
8
+ from pycontrails.core.fuel import Fuel, JetA
9
+ from pycontrails.models.cocip.cocip_params import CocipParams
10
+
11
+
12
+ @dataclasses.dataclass
13
+ class CocipGridParams(CocipParams):
14
+ """Default parameters for :class:`CocipGrid`."""
15
+
16
+ # ---------
17
+ # Algorithm
18
+ # ---------
19
+
20
+ #: Approximate size of a typical :class:`numpy.ndarray` used with in CoCiP calculations.
21
+ #: The 4-dimensional array defining the waypoint is raveled and split into
22
+ #: batches of this size.
23
+ #: A smaller number for this parameter will reduce memory footprint at the
24
+ #: expense of a longer compute time.
25
+ target_split_size: int = 100_000
26
+
27
+ #: Additional boost to target split size before SAC is computed. For typical meshes, only
28
+ #: 10% of waypoints will survive SAC and initial downwash filtering. Accordingly, this parameter
29
+ #: magnifies mesh split size before SAC is computed. See :attr:`target_split_size`.
30
+ target_split_size_pre_SAC_boost: float = 3.0
31
+
32
+ #: Display ``tqdm`` progress bar showing batch evaluation progress.
33
+ show_progress: bool = True
34
+
35
+ # ------------------
36
+ # Simulated Aircraft
37
+ # ------------------
38
+
39
+ #: Nominal segment length to place at each grid point [:math:`m`]. Round-off error
40
+ #: can be problematic with a small nominal segment length and a large
41
+ #: :attr:`dt_integration` parameter. On the other hand,
42
+ #: too large of a nominal segment length diminishes the "locality" of the grid point.
43
+ #:
44
+ #: .. versionadded:: 0.32.2
45
+ #:
46
+ #: EXPERIMENTAL: If None, run CoCiP in "segment-free"
47
+ #: mode. This mode does not include any terms involving segments (wind shear,
48
+ #: segment length, any derived terms). See :attr:`azimuth`
49
+ #: and :attr:`dsn_dz_factor` for more details.
50
+ segment_length: float | None = 1000.0
51
+
52
+ #: Fuel type
53
+ fuel: Fuel = dataclasses.field(default_factory=JetA)
54
+
55
+ #: ICAO code designating simulated aircraft type. Needed for the
56
+ #: :attr:`aircraft_performance` and :class:`Emissions` models.
57
+ aircraft_type: str = "B737"
58
+
59
+ #: Engine unique identification number for the ICAO Aircraft Emissions Databank (EDB)
60
+ #: If None, an assumed engine_uid is used in :class:`Emissions`.
61
+ engine_uid: str | None = None
62
+
63
+ #: Navigation bearing [:math:`\deg`] measured in clockwise direction from
64
+ #: true north, by default 0.0.
65
+ #:
66
+ #: .. versionadded:: 0.32.2
67
+ #:
68
+ #: EXPERIMENTAL: If None, run CoCiP in "segment-free"
69
+ #: mode. This mode does not include any terms involving segments (wind shear,
70
+ #: segment_length, any derived terms), unless :attr:`dsn_dz_factor`
71
+ #: is non-zero.
72
+ azimuth: float | None = 0.0
73
+
74
+ #: Experimental parameter used to approximate ``dsn_dz`` from ``ds_dz`` via
75
+ #: ``dsn_dz = ds_dz * dsn_dz_factor``.
76
+ #: A value of 0.0 disables any normal wind shear effects.
77
+ #: An initial unpublished experiment suggests that
78
+ #: ``dsn_dz_factor = 0.665`` adequately approximates the mean EF predictions
79
+ #: of :class:`CocipGrid` over all azimuths.
80
+ #:
81
+ #: .. versionadded:: 0.32.2
82
+ dsn_dz_factor: float = 0.0
83
+
84
+ #: --------------------
85
+ #: Aircraft Performance
86
+ #: --------------------
87
+
88
+ #: Aircraft wingspan, [:math:`m`]. If included in :attr:`CocipGrid.source`,
89
+ #: this parameter is unused. Otherwise, if this parameter is None, the
90
+ #: :attr:`aircraft_performance` model is used to estimate the wingspan.
91
+ wingspan: float | None = None
92
+
93
+ #: Nominal aircraft mass, [:math:`kg`]. If included in :attr:`CocipGrid.source`,
94
+ #: this parameter is unused. Otherwise, if this parameter is None, the
95
+ #: :attr:`aircraft_performance` model is used to estimate the aircraft mass.
96
+ aircraft_mass: float | None = None
97
+
98
+ #: Cruising true airspeed, [:math:`m \ s^{-1}`]. If included in :attr:`CocipGrid.source`,
99
+ #: this parameter is unused. Otherwise, if this parameter is None, the
100
+ #: :attr:`aircraft_performance` model is used to estimate the true airspeed.
101
+ true_airspeed: float | None = None
102
+
103
+ #: Nominal engine efficiency, [:math:`0 - 1`]. If included in :attr:`CocipGrid.source`,
104
+ #: this parameter is unused. Otherwise, if this parameter is None, the
105
+ #: :attr:`aircraft_performance` model is used to estimate the engine efficiency.
106
+ engine_efficiency: float | None = None
107
+
108
+ #: Nominal fuel flow, [:math:`kg \ s^{-1}`]. If included in :attr:`CocipGrid.source`,
109
+ #: this parameter is unused. Otherwise, if this parameter is None, the
110
+ #: :attr:`aircraft_performance` model is used to estimate the fuel flow.
111
+ fuel_flow: float | None = None
112
+
113
+ #: Aircraft performance model. Required unless ``source`` or ``params``
114
+ #: provide all of the following variables:
115
+ #:
116
+ #: - wingspan
117
+ #: - true_airspeed (or mach_number)
118
+ #: - fuel_flow
119
+ #: - engine_efficiency
120
+ #: - aircraft_mass
121
+ #:
122
+ #: If None and :attr:`CocipGrid.source` or :class:`CocipGridParams` do not provide
123
+ #: the above variables, a ValueError is raised. See :class:`PSGrid` for an open-source
124
+ #: implementation of a :class:`AircraftPerformanceGrid` model.
125
+ aircraft_performance: AircraftPerformanceGrid | None = None
126
+
127
+ # ------------
128
+ # Model output
129
+ # ------------
130
+
131
+ #: Attach additional formation specific data to the output. If True, attach
132
+ #: all possible formation data. See :mod:`pycontrails.models.cocipgrid.cocip_grid`
133
+ #: for a list of supported formation data.
134
+ verbose_outputs_formation: bool | set[str] = False
135
+
136
+ #: Attach contrail evolution data to :attr:`CocipGrid.contrail_list`. Requires
137
+ #: substantial memory overhead.
138
+ verbose_outputs_evolution: bool = False
@@ -0,0 +1,486 @@
1
+ """Simulate dry advection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Any, NoReturn, overload
7
+
8
+ import numpy as np
9
+
10
+ from pycontrails.core import models
11
+ from pycontrails.core.flight import Flight
12
+ from pycontrails.core.met import MetDataset
13
+ from pycontrails.core.met_var import AirTemperature, EastwardWind, NorthwardWind, VerticalVelocity
14
+ from pycontrails.core.vector import GeoVectorDataset
15
+ from pycontrails.models.cocip import contrail_properties, wind_shear
16
+ from pycontrails.physics import geo, thermo
17
+
18
+
19
+ @dataclasses.dataclass
20
+ class DryAdvectionParams(models.ModelParams):
21
+ """Parameters for the :class:`DryAdvection` model."""
22
+
23
+ #: Apply Euler's method with a fixed step size of ``dt_integration``. Advected waypoints
24
+ #: are interpolated against met data once each ``dt_integration``.
25
+ dt_integration: np.timedelta64 = np.timedelta64(30, "m")
26
+
27
+ #: Max age of plume evolution.
28
+ max_age: np.timedelta64 = np.timedelta64(20, "h")
29
+
30
+ #: Rate of change of pressure due to sedimentation [:math:`Pa/s`]
31
+ sedimentation_rate: float = 0.0
32
+
33
+ #: Difference in altitude between top and bottom layer for stratification calculations,
34
+ #: [:math:`m`]. Used to approximate derivative of "lagrangian_tendency_of_air_pressure"
35
+ #: (upward component of air velocity) with respect to altitude.
36
+ dz_m: float = 200.0
37
+
38
+ #: Upper bound for evolved plume depth, constraining it to realistic values.
39
+ max_depth: float | None = 1500.0
40
+
41
+ #: Initial plume width, [:math:`m`]. Overridden by "width" key on :attr:`source`.
42
+ # If None, only pointwise advection is simulated without wind shear effects.
43
+ width: float | None = 100.0
44
+
45
+ #: Initial plume depth, [:math:`m`]. Overridden by "depth" key on :attr:`source`.
46
+ # If None, only pointwise advection is simulated without wind shear effects.
47
+ depth: float | None = 100.0
48
+
49
+ #: Initial plume direction, [:math:`m`]. Overridden by "azimuth" key on :attr:`source`.
50
+ # If None, only pointwise advection is simulated without wind shear effects.
51
+ azimuth: float | None = 0.0
52
+
53
+
54
+ class DryAdvection(models.Model):
55
+ """Simulate "dry advection" of an emissions plume with an elliptical cross section.
56
+
57
+ The model simulates both horizontal and vertical advection of a weightless
58
+ plume without any sedimentation effects. Unlike :class:`Cocip`, humidity is
59
+ not considered, and radiative forcing is not simulated. The model is
60
+ therefore useful simulating plume advection and dispersion itself.
61
+
62
+ .. versionadded:: 0.46.0
63
+
64
+ This model has two distinct modes of operation:
65
+
66
+ - **Pointwise only**: If ``azimuth`` is None, then the model will only advect
67
+ points without any wind shear effects. This mode is useful for testing
68
+ the advection algorithm itself, and for simulating the evolution of
69
+ a single point.
70
+ - **Wind shear effects**: If ``azimuth`` is not None, then the model will
71
+ advect points with wind shear effects. At each time step, the model
72
+ will evolve the plume geometry according to diffusion and wind shear
73
+ effects. This mode is also used in :class:`CocipGrid` and :class:`Cocip`.
74
+
75
+ Parameters
76
+ ----------
77
+ met : MetDataset
78
+ Meteorological data.
79
+ params : dict[str, Any]
80
+ Model parameters. See :class:`DryAdvectionParams` for details.
81
+ **kwargs : Any
82
+ Additional parameters passed as keyword arguments.
83
+ """
84
+
85
+ name = "dry_advection"
86
+ long_name = "Emission plume advection without sedimentation"
87
+ met_variables = AirTemperature, EastwardWind, NorthwardWind, VerticalVelocity
88
+ default_params = DryAdvectionParams
89
+
90
+ met: MetDataset
91
+ met_required = True
92
+ source: GeoVectorDataset
93
+
94
+ @overload
95
+ def eval(self, source: Flight, **params: Any) -> Flight: ...
96
+
97
+ @overload
98
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
99
+
100
+ @overload
101
+ def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
102
+
103
+ def eval(self, source: GeoVectorDataset | None = None, **params: Any) -> GeoVectorDataset:
104
+ """Simulate dry advection (no sedimentation) of arbitrary points.
105
+
106
+ Like :class:`Cocip`, this model adds a "waypoint" column to the :attr:`source`.
107
+
108
+ Parameters
109
+ ----------
110
+ source : GeoVectorDataset
111
+ Arbitrary points to advect.
112
+ params : Any
113
+ Overwrite model parameters defined in ``__init__``.
114
+
115
+ Returns
116
+ -------
117
+ GeoVectorDataset
118
+ Advected points.
119
+ """
120
+ self.update_params(params)
121
+ self.set_source(source)
122
+ self.source = self.require_source_type(GeoVectorDataset)
123
+
124
+ self._prepare_source()
125
+
126
+ interp_kwargs = self.interp_kwargs
127
+
128
+ dt_integration = self.params["dt_integration"]
129
+ max_age = self.params["max_age"]
130
+ sedimentation_rate = self.params["sedimentation_rate"]
131
+ dz_m = self.params["dz_m"]
132
+ max_depth = self.params["max_depth"]
133
+
134
+ source_time = self.source["time"]
135
+ t0 = source_time.min()
136
+ t1 = source_time.max()
137
+ timesteps = np.arange(t0 + dt_integration, t1 + dt_integration + max_age, dt_integration)
138
+
139
+ vector = None
140
+
141
+ evolved = []
142
+ for t in timesteps:
143
+ filt = (source_time < t) & (source_time >= t - dt_integration)
144
+ vector = self.source.filter(filt) + vector
145
+ vector = _evolve_one_step(
146
+ self.met,
147
+ vector,
148
+ t,
149
+ sedimentation_rate=sedimentation_rate,
150
+ dz_m=dz_m,
151
+ max_depth=max_depth,
152
+ **interp_kwargs,
153
+ )
154
+
155
+ filt = (vector["age"] <= max_age) & vector.coords_intersect_met(self.met)
156
+ vector = vector.filter(filt)
157
+
158
+ evolved.append(vector)
159
+ if not vector and np.all(source_time < t):
160
+ break
161
+
162
+ return GeoVectorDataset.sum(evolved, fill_value=np.nan)
163
+
164
+ def _prepare_source(self) -> None:
165
+ r"""Prepare :attr:`source` vector for advection by wind-shear-derived variables.
166
+
167
+ This method adds the following variables to :attr:`source` if the `"azimuth"`
168
+ parameter is not None:
169
+
170
+ - ``age``: Age of plume.
171
+ - ``azimuth``: Initial plume direction, measured in clockwise direction from
172
+ true north, [:math:`\deg`].
173
+ - ``width``: Initial plume width, [:math:`m`].
174
+ - ``depth``: Initial plume depth, [:math:`m`].
175
+ - ``sigma_yz``: All zeros for cross-term term in covariance matrix of plume.
176
+ """
177
+
178
+ self.source.setdefault("level", self.source.level)
179
+
180
+ columns: tuple[str, ...] = ("longitude", "latitude", "level", "time")
181
+ if "azimuth" in self.source:
182
+ columns += ("azimuth",)
183
+ self.source = GeoVectorDataset(self.source.select(columns, copy=False))
184
+
185
+ # Get waypoint index if not already set
186
+ self.source.setdefault("waypoint", np.arange(self.source.size))
187
+
188
+ self.source["age"] = np.full(self.source.size, np.timedelta64(0, "ns"))
189
+
190
+ if "azimuth" not in self.source:
191
+ if isinstance(self.source, Flight):
192
+ pointwise_only = False
193
+ self.source["azimuth"] = self.source.segment_azimuth()
194
+ else:
195
+ try:
196
+ self.source.broadcast_attrs("azimuth")
197
+ except KeyError:
198
+ if (azimuth := self.params["azimuth"]) is not None:
199
+ pointwise_only = False
200
+ self.source["azimuth"] = np.full_like(self.source["longitude"], azimuth)
201
+ else:
202
+ pointwise_only = True
203
+ else:
204
+ pointwise_only = False
205
+ else:
206
+ pointwise_only = False
207
+
208
+ for key in ("width", "depth"):
209
+ if key in self.source:
210
+ continue
211
+ if key in self.source.attrs:
212
+ self.source.broadcast_attrs(key)
213
+ continue
214
+
215
+ val = self.params[key]
216
+ if val is None and not pointwise_only:
217
+ raise ValueError(f"If '{key}' is None, then 'azimuth' must also be None.")
218
+
219
+ if val is not None and pointwise_only:
220
+ raise ValueError(f"Cannot specify '{key}' without specifying 'azimuth'.")
221
+
222
+ if not pointwise_only:
223
+ self.source[key] = np.full_like(self.source["longitude"], val)
224
+
225
+ if pointwise_only:
226
+ return
227
+
228
+ self.source["sigma_yz"] = np.zeros_like(self.source["longitude"])
229
+ width = self.source["width"]
230
+ depth = self.source["depth"]
231
+ self.source["area_eff"] = contrail_properties.plume_effective_cross_sectional_area(
232
+ width, depth, sigma_yz=0.0
233
+ )
234
+
235
+
236
+ def _perform_interp_for_step(
237
+ met: MetDataset,
238
+ vector: GeoVectorDataset,
239
+ dz_m: float,
240
+ **interp_kwargs: Any,
241
+ ) -> None:
242
+ """Perform all interpolation required for one step of advection."""
243
+
244
+ vector.setdefault("level", vector.level)
245
+ air_pressure = vector.setdefault("air_pressure", vector.air_pressure)
246
+
247
+ air_temperature = models.interpolate_met(met, vector, "air_temperature", **interp_kwargs)
248
+ models.interpolate_met(met, vector, "northward_wind", "v_wind", **interp_kwargs)
249
+ models.interpolate_met(met, vector, "eastward_wind", "u_wind", **interp_kwargs)
250
+ models.interpolate_met(
251
+ met,
252
+ vector,
253
+ "lagrangian_tendency_of_air_pressure",
254
+ "vertical_velocity",
255
+ **interp_kwargs,
256
+ )
257
+
258
+ az = vector.get("azimuth")
259
+ if az is None:
260
+ # Early exit for pointwise only simulation
261
+ return
262
+
263
+ air_pressure_lower = thermo.pressure_dz(air_temperature, air_pressure, dz_m)
264
+ vector["air_pressure_lower"] = air_pressure_lower
265
+ level_lower = air_pressure_lower / 100.0
266
+
267
+ models.interpolate_met(
268
+ met,
269
+ vector,
270
+ "eastward_wind",
271
+ "u_wind_lower",
272
+ level=level_lower,
273
+ **interp_kwargs,
274
+ )
275
+ models.interpolate_met(
276
+ met,
277
+ vector,
278
+ "northward_wind",
279
+ "v_wind_lower",
280
+ level=level_lower,
281
+ **interp_kwargs,
282
+ )
283
+ models.interpolate_met(
284
+ met,
285
+ vector,
286
+ "air_temperature",
287
+ "air_temperature_lower",
288
+ level=level_lower,
289
+ **interp_kwargs,
290
+ )
291
+
292
+ lons = vector["longitude"]
293
+ lats = vector["latitude"]
294
+ dist = 1000.0
295
+
296
+ # These should probably not be included in model input ... so
297
+ # we'll get a warning if they get overwritten
298
+ longitude_head, latitude_head = geo.forward_azimuth(lons=lons, lats=lats, az=az, dist=dist)
299
+ longitude_tail, latitude_tail = geo.forward_azimuth(lons=lons, lats=lats, az=az, dist=-dist)
300
+ vector["longitude_head"] = longitude_head
301
+ vector["latitude_head"] = latitude_head
302
+ vector["longitude_tail"] = longitude_tail
303
+ vector["latitude_tail"] = latitude_tail
304
+
305
+ for met_key in ("eastward_wind", "northward_wind"):
306
+ vector_key = f"{met_key}_head"
307
+ models.interpolate_met(
308
+ met,
309
+ vector,
310
+ met_key,
311
+ vector_key,
312
+ **interp_kwargs,
313
+ longitude=longitude_head,
314
+ latitude=latitude_head,
315
+ )
316
+
317
+ vector_key = f"{met_key}_tail"
318
+ models.interpolate_met(
319
+ met,
320
+ vector,
321
+ met_key,
322
+ vector_key,
323
+ **interp_kwargs,
324
+ longitude=longitude_tail,
325
+ latitude=latitude_tail,
326
+ )
327
+
328
+
329
+ def _calc_geometry(
330
+ vector: GeoVectorDataset,
331
+ dz_m: float,
332
+ dt: np.timedelta64,
333
+ max_depth: float | None,
334
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
335
+ """Calculate wind-shear-derived geometry of evolved plume."""
336
+
337
+ u_wind = vector["u_wind"]
338
+ v_wind = vector["v_wind"]
339
+ u_wind_lower = vector.data.pop("u_wind_lower")
340
+ v_wind_lower = vector.data.pop("v_wind_lower")
341
+
342
+ air_temperature = vector["air_temperature"]
343
+ air_temperature_lower = vector.data.pop("air_temperature_lower")
344
+ air_pressure = vector["air_pressure"]
345
+ air_pressure_lower = vector.data.pop("air_pressure_lower")
346
+
347
+ ds_dz = wind_shear.wind_shear(u_wind, u_wind_lower, v_wind, v_wind_lower, dz_m)
348
+
349
+ azimuth = vector["azimuth"]
350
+ latitude = vector["latitude"]
351
+ cos_a, sin_a = geo.azimuth_to_direction(azimuth, latitude)
352
+
353
+ width = vector["width"]
354
+ depth = vector["depth"]
355
+ sigma_yz = vector["sigma_yz"]
356
+ area_eff = vector["area_eff"]
357
+
358
+ dsn_dz = wind_shear.wind_shear_normal(
359
+ u_wind_top=u_wind,
360
+ u_wind_btm=u_wind_lower,
361
+ v_wind_top=v_wind,
362
+ v_wind_btm=v_wind_lower,
363
+ cos_a=cos_a,
364
+ sin_a=sin_a,
365
+ dz=dz_m,
366
+ )
367
+
368
+ dT_dz = thermo.T_potential_gradient(
369
+ air_temperature,
370
+ air_pressure,
371
+ air_temperature_lower,
372
+ air_pressure_lower,
373
+ dz_m,
374
+ )
375
+
376
+ depth_eff = contrail_properties.plume_effective_depth(width, area_eff)
377
+
378
+ diffuse_h = contrail_properties.horizontal_diffusivity(ds_dz, depth)
379
+ diffuse_v = contrail_properties.vertical_diffusivity(
380
+ air_pressure,
381
+ air_temperature,
382
+ dT_dz,
383
+ depth_eff,
384
+ terminal_fall_speed=0.0,
385
+ sedimentation_impact_factor=0.0,
386
+ eff_heat_rate=None,
387
+ )
388
+
389
+ sigma_yy_2, sigma_zz_2, sigma_yz_2 = contrail_properties.plume_temporal_evolution(
390
+ width,
391
+ depth,
392
+ sigma_yz,
393
+ dsn_dz,
394
+ diffuse_h,
395
+ diffuse_v,
396
+ seg_ratio=1.0,
397
+ dt=dt,
398
+ max_depth=max_depth,
399
+ )
400
+ width_2, depth_2 = contrail_properties.new_contrail_dimensions(sigma_yy_2, sigma_zz_2)
401
+ area_eff_2 = contrail_properties.plume_effective_cross_sectional_area(
402
+ width_2, depth_2, sigma_yz_2
403
+ )
404
+
405
+ longitude_head = vector.data.pop("longitude_head")
406
+ latitude_head = vector.data.pop("latitude_head")
407
+ longitude_tail = vector.data.pop("longitude_tail")
408
+ latitude_tail = vector.data.pop("latitude_tail")
409
+ u_wind_head = vector.data.pop("eastward_wind_head")
410
+ v_wind_head = vector.data.pop("northward_wind_head")
411
+ u_wind_tail = vector.data.pop("eastward_wind_tail")
412
+ v_wind_tail = vector.data.pop("northward_wind_tail")
413
+
414
+ longitude_head_t2 = geo.advect_longitude(
415
+ longitude=longitude_head, latitude=latitude_head, u_wind=u_wind_head, dt=dt
416
+ )
417
+ latitude_head_t2 = geo.advect_latitude(latitude=latitude_head, v_wind=v_wind_head, dt=dt)
418
+
419
+ longitude_tail_t2 = geo.advect_longitude(
420
+ longitude=longitude_tail, latitude=latitude_tail, u_wind=u_wind_tail, dt=dt
421
+ )
422
+ latitude_tail_t2 = geo.advect_latitude(latitude=latitude_tail, v_wind=v_wind_tail, dt=dt)
423
+
424
+ azimuth_2 = geo.azimuth(
425
+ lons0=longitude_tail_t2,
426
+ lats0=latitude_tail_t2,
427
+ lons1=longitude_head_t2,
428
+ lats1=latitude_head_t2,
429
+ )
430
+
431
+ return azimuth_2, width_2, depth_2, sigma_yz_2, area_eff_2
432
+
433
+
434
+ def _evolve_one_step(
435
+ met: MetDataset,
436
+ vector: GeoVectorDataset,
437
+ t: np.datetime64,
438
+ *,
439
+ sedimentation_rate: float,
440
+ dz_m: float,
441
+ max_depth: float | None,
442
+ **interp_kwargs: Any,
443
+ ) -> GeoVectorDataset:
444
+ """Evolve plume geometry by one step."""
445
+
446
+ _perform_interp_for_step(met, vector, dz_m, **interp_kwargs)
447
+ u_wind = vector["u_wind"]
448
+ v_wind = vector["v_wind"]
449
+ vertical_velocity = vector["vertical_velocity"] + sedimentation_rate
450
+
451
+ latitude = vector["latitude"]
452
+ longitude = vector["longitude"]
453
+
454
+ dt = t - vector["time"]
455
+ longitude_2 = geo.advect_longitude(longitude, latitude, u_wind, dt) # type: ignore[arg-type]
456
+ latitude_2 = geo.advect_latitude(latitude, v_wind, dt) # type: ignore[arg-type]
457
+ level_2 = geo.advect_level(
458
+ vector.level, vertical_velocity, 0.0, 0.0, dt # type: ignore[arg-type]
459
+ )
460
+
461
+ out = GeoVectorDataset(
462
+ longitude=longitude_2,
463
+ latitude=latitude_2,
464
+ level=level_2,
465
+ time=np.full(longitude_2.shape, t),
466
+ copy=False,
467
+ )
468
+ out["age"] = vector["age"] + dt
469
+ out["waypoint"] = vector["waypoint"]
470
+
471
+ azimuth = vector.get("azimuth")
472
+ if azimuth is None:
473
+ # Early exit for "pointwise only" simulation
474
+ return out
475
+
476
+ # Attach wind-shear-derived geometry to output vector
477
+ azimuth_2, width_2, depth_2, sigma_yz_2, area_eff_2 = _calc_geometry(
478
+ vector, dz_m, dt, max_depth # type: ignore[arg-type]
479
+ )
480
+ out["azimuth"] = azimuth_2
481
+ out["width"] = width_2
482
+ out["depth"] = depth_2
483
+ out["sigma_yz"] = sigma_yz_2
484
+ out["area_eff"] = area_eff_2
485
+
486
+ return out
@@ -0,0 +1,21 @@
1
+ """Aircraft Emissions modeling."""
2
+
3
+ from pycontrails.models.emissions.emissions import (
4
+ EDBGaseous,
5
+ EDBnvpm,
6
+ Emissions,
7
+ EmissionsParams,
8
+ load_default_aircraft_engine_mapping,
9
+ load_engine_nvpm_profile_from_edb,
10
+ load_engine_params_from_edb,
11
+ )
12
+
13
+ __all__ = [
14
+ "Emissions",
15
+ "EmissionsParams",
16
+ "EDBGaseous",
17
+ "EDBnvpm",
18
+ "load_default_aircraft_engine_mapping",
19
+ "load_engine_nvpm_profile_from_edb",
20
+ "load_engine_params_from_edb",
21
+ ]