pycontrails 0.58.0__cp314-cp314-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 (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,602 @@
1
+ """Simulate dry advection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import sys
7
+ from typing import Any, NoReturn, overload
8
+
9
+ if sys.version_info >= (3, 12):
10
+ from typing import override
11
+ else:
12
+ from typing_extensions import override
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+ import pandas as pd
17
+
18
+ from pycontrails.core import models
19
+ from pycontrails.core.met import MetDataset, maybe_downselect_mds
20
+ from pycontrails.core.met_var import (
21
+ AirTemperature,
22
+ EastwardWind,
23
+ MetVariable,
24
+ NorthwardWind,
25
+ VerticalVelocity,
26
+ )
27
+ from pycontrails.core.vector import GeoVectorDataset
28
+ from pycontrails.models.cocip import contrail_properties, wind_shear
29
+ from pycontrails.physics import geo, thermo
30
+
31
+
32
+ @dataclasses.dataclass
33
+ class DryAdvectionParams(models.AdvectionBuffers):
34
+ """Parameters for the :class:`DryAdvection` model."""
35
+
36
+ #: Apply Euler's method with a fixed step size of ``dt_integration``. Advected waypoints
37
+ #: are interpolated against met data once each ``dt_integration``.
38
+ dt_integration: np.timedelta64 = np.timedelta64(30, "m")
39
+
40
+ #: Max age of plume evolution. If set to ``None``, ``timesteps`` must not be None
41
+ #: and advection will continue until the final timestep for all plumes.
42
+ max_age: np.timedelta64 | None = np.timedelta64(20, "h")
43
+
44
+ #: Advection timesteps. If provided, ``dt_integration`` will be ignored.
45
+ #:
46
+ #: .. versionadded:: 0.54.11
47
+ timesteps: npt.NDArray[np.datetime64] | None = None
48
+
49
+ #: Rate of change of pressure due to sedimentation [:math:`Pa/s`]
50
+ sedimentation_rate: float = 0.0
51
+
52
+ #: Difference in altitude between top and bottom layer for stratification calculations,
53
+ #: [:math:`m`]. Used to approximate derivative of "lagrangian_tendency_of_air_pressure"
54
+ #: (upward component of air velocity) with respect to altitude.
55
+ dz_m: float = 200.0
56
+
57
+ #: Upper bound for evolved plume depth, constraining it to realistic values.
58
+ max_depth: float | None = 1500.0
59
+
60
+ #: Initial plume width, [:math:`m`]. Overridden by "width" key on :attr:`source`.
61
+ # If None, only pointwise advection is simulated without wind shear effects.
62
+ width: float | None = 100.0
63
+
64
+ #: Initial plume depth, [:math:`m`]. Overridden by "depth" key on :attr:`source`.
65
+ # If None, only pointwise advection is simulated without wind shear effects.
66
+ depth: float | None = 100.0
67
+
68
+ #: Initial plume direction, [:math:`m`]. Overridden by "azimuth" key on :attr:`source`.
69
+ # If None, only pointwise advection is simulated without wind shear effects.
70
+ azimuth: float | None = 0.0
71
+
72
+ #: Add additional intermediate variables to the output vector.
73
+ #: This includes interpolated met variables and wind-shear-derived geometry.
74
+ verbose_outputs: bool = False
75
+
76
+ #: Whether to include ``source`` points in the output vector. Enabling allows
77
+ #: the user to view additional data (e.g., interpolated met variables) for
78
+ #: source points as well as evolved points.
79
+ include_source_in_output: bool = False
80
+
81
+
82
+ class DryAdvection(models.Model):
83
+ """Simulate "dry advection" of an emissions plume with an elliptical cross section.
84
+
85
+ The model simulates both horizontal and vertical advection of a weightless
86
+ plume without any sedimentation effects. Unlike :class:`Cocip`, humidity is
87
+ not considered, and radiative forcing is not simulated. The model is
88
+ therefore useful simulating plume advection and dispersion itself.
89
+
90
+ .. versionadded:: 0.46.0
91
+
92
+ This model has two distinct modes of operation:
93
+
94
+ - **Pointwise only**: If ``azimuth`` is None, then the model will only advect
95
+ points without any wind shear effects. This mode is useful for testing
96
+ the advection algorithm itself, and for simulating the evolution of
97
+ a single point.
98
+ - **Wind shear effects**: If ``azimuth`` is not None, then the model will
99
+ advect points with wind shear effects. At each time step, the model
100
+ will evolve the plume geometry according to diffusion and wind shear
101
+ effects. This mode is also used in :class:`CocipGrid` and :class:`Cocip`.
102
+
103
+ Parameters
104
+ ----------
105
+ met : MetDataset
106
+ Meteorological data.
107
+ params : dict[str, Any]
108
+ Model parameters. See :class:`DryAdvectionParams` for details.
109
+ **kwargs : Any
110
+ Additional parameters passed as keyword arguments.
111
+ """
112
+
113
+ name = "dry_advection"
114
+ long_name = "Emission plume advection without sedimentation"
115
+ met_variables: tuple[MetVariable, ...] = (
116
+ AirTemperature,
117
+ EastwardWind,
118
+ NorthwardWind,
119
+ VerticalVelocity,
120
+ )
121
+ default_params = DryAdvectionParams
122
+
123
+ met: MetDataset
124
+ met_required = True
125
+ source: GeoVectorDataset
126
+
127
+ @overload
128
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
129
+
130
+ @overload
131
+ def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
132
+
133
+ def eval(self, source: GeoVectorDataset | None = None, **params: Any) -> GeoVectorDataset:
134
+ """Simulate dry advection (no sedimentation) of arbitrary points.
135
+
136
+ Like :class:`Cocip`, this model adds a "waypoint" column to the :attr:`source`.
137
+
138
+ Parameters
139
+ ----------
140
+ source : GeoVectorDataset | None
141
+ Arbitrary points to advect. A :class:`Flight` instance is not treated any
142
+ differently than a :class:`GeoVectorDataset`. In particular, the user must
143
+ explicitly set ``flight["azimuth"] = flight.segment_azimuth()`` if they
144
+ want to use wind shear effects for a flight.
145
+ In the current implementation, any existing meteorological variables in the ``source``
146
+ are ignored. The ``source`` will be interpolated against the :attr:`met` dataset.
147
+ **params : Any
148
+ Overwrite model parameters defined in ``__init__``.
149
+
150
+ Returns
151
+ -------
152
+ GeoVectorDataset
153
+ Advected points.
154
+ """
155
+ self.update_params(params)
156
+
157
+ max_age = self.params["max_age"]
158
+ timesteps = self.params["timesteps"]
159
+ if max_age is None and timesteps is None:
160
+ msg = "Timesteps must be set using the timesteps parameter when max_age is None"
161
+ raise ValueError(msg)
162
+
163
+ self.set_source(source)
164
+ self.source = self.require_source_type(GeoVectorDataset)
165
+ self.downselect_met()
166
+ if not self.source.coords_intersect_met(self.met).any():
167
+ msg = "No source coordinates intersect met data."
168
+ raise ValueError(msg)
169
+
170
+ self.source = self._prepare_source()
171
+
172
+ interp_kwargs = self.interp_kwargs
173
+
174
+ sedimentation_rate = self.params["sedimentation_rate"]
175
+ dz_m = self.params["dz_m"]
176
+ max_depth = self.params["max_depth"]
177
+ verbose_outputs = self.params["verbose_outputs"]
178
+ source_time = self.source["time"]
179
+
180
+ if timesteps is None:
181
+ dt_integration = self.params["dt_integration"]
182
+ t0 = pd.Timestamp(source_time.min()).floor(pd.Timedelta(dt_integration)).to_numpy()
183
+ t1 = source_time.max()
184
+ timesteps = np.arange(
185
+ t0 + dt_integration, t1 + dt_integration + max_age, dt_integration
186
+ )
187
+
188
+ vector2 = GeoVectorDataset()
189
+ met = None
190
+
191
+ evolved = []
192
+ tmin = source_time.min()
193
+ for t in timesteps:
194
+ filt = (source_time < t) & (source_time >= tmin)
195
+ tmin = t
196
+
197
+ vector1 = vector2 + self.source.filter(filt, copy=False)
198
+ if vector1.size == 0:
199
+ vector2 = GeoVectorDataset()
200
+ continue
201
+ evolved.append(vector1) # NOTE: vector1 is mutated below (geometry and weather added)
202
+
203
+ t0 = vector1["time"].min()
204
+ t1 = vector1["time"].max()
205
+ met = maybe_downselect_mds(self.met, met, t0, t1)
206
+
207
+ vector2 = _evolve_one_step(
208
+ met,
209
+ vector1,
210
+ t,
211
+ sedimentation_rate=sedimentation_rate,
212
+ dz_m=dz_m,
213
+ max_depth=max_depth,
214
+ verbose_outputs=verbose_outputs,
215
+ **interp_kwargs,
216
+ )
217
+
218
+ filt = vector2.coords_intersect_met(self.met)
219
+ if max_age is not None:
220
+ filt &= vector2["age"] <= max_age
221
+ vector2 = vector2.filter(filt)
222
+
223
+ if not vector2 and np.all(source_time < t):
224
+ break
225
+
226
+ evolved.append(vector2)
227
+ out = GeoVectorDataset.sum(evolved, fill_value=np.nan)
228
+
229
+ if self.params["include_source_in_output"]:
230
+ return out
231
+
232
+ filt = out["age"] > np.timedelta64(0, "ns")
233
+ return out.filter(filt)
234
+
235
+ def _prepare_source(self) -> GeoVectorDataset:
236
+ r"""Prepare :attr:`source` vector for advection by wind-shear-derived variables.
237
+
238
+ The following variables are always guaranteed to be present in :attr:`source`:
239
+
240
+ - ``age``: Age of plume.
241
+ - ``waypoint``: Identifier for each waypoint.
242
+
243
+ If ``flight_id`` is present in :attr:`source`, it is retained.
244
+
245
+ If `"azimuth"` is present in :attr:`source`, `source.attrs`, or :attr:`params`,
246
+ the following variables will also be added:
247
+
248
+ - ``azimuth``: Initial plume direction, measured in clockwise direction from
249
+ true north, [:math:`\deg`].
250
+ - ``width``: Initial plume width, [:math:`m`].
251
+ - ``depth``: Initial plume depth, [:math:`m`].
252
+ - ``sigma_yz``: All zeros for cross-term term in covariance matrix of plume.
253
+
254
+ Returns
255
+ -------
256
+ GeoVectorDataset
257
+ A filtered version of the source with only the required columns.
258
+ """
259
+ self.source.setdefault("level", self.source.level)
260
+ self.source["age"] = np.full(self.source.size, np.timedelta64(0, "ns"))
261
+ self.source.setdefault("waypoint", np.arange(self.source.size))
262
+
263
+ columns = ["longitude", "latitude", "level", "time", "age", "waypoint"]
264
+ if "flight_id" in self.source:
265
+ columns.append("flight_id")
266
+
267
+ azimuth = self.get_source_param("azimuth", set_attr=False)
268
+ if azimuth is None:
269
+ # Early exit for pointwise only simulation
270
+ if self.params["width"] is not None or self.params["depth"] is not None:
271
+ raise ValueError(
272
+ "If 'azimuth' is None, then 'width' and 'depth' must also be None."
273
+ )
274
+ return GeoVectorDataset._from_fastpath(self.source.select(columns, copy=False).data)
275
+
276
+ if "azimuth" not in self.source:
277
+ self.source["azimuth"] = np.full_like(self.source["longitude"], azimuth)
278
+
279
+ for key in ("width", "depth"):
280
+ if key in self.source:
281
+ continue
282
+ if key in self.source.attrs:
283
+ self.source.broadcast_attrs(key)
284
+ continue
285
+
286
+ val = self.params[key]
287
+ if val is None:
288
+ raise ValueError(f"If '{key}' is None, then 'azimuth' must also be None.")
289
+
290
+ self.source[key] = np.full_like(self.source["longitude"], val)
291
+
292
+ columns.extend(["azimuth", "width", "depth", "sigma_yz", "area_eff"])
293
+ self.source["sigma_yz"] = np.zeros_like(self.source["longitude"])
294
+ width = self.source["width"]
295
+ depth = self.source["depth"]
296
+ self.source["area_eff"] = contrail_properties.plume_effective_cross_sectional_area(
297
+ width, depth, sigma_yz=0.0
298
+ )
299
+
300
+ return GeoVectorDataset._from_fastpath(self.source.select(columns, copy=False).data)
301
+
302
+ @override
303
+ def downselect_met(self) -> None:
304
+ if not self.params["downselect_met"]:
305
+ return
306
+
307
+ buffers = {
308
+ f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
309
+ for coord in ("longitude", "latitude", "level")
310
+ }
311
+
312
+ max_age = self.params["max_age"]
313
+ if max_age is None:
314
+ max_age = max(
315
+ np.timedelta64(0), self.params["timesteps"].max() - self.source["time"].max()
316
+ )
317
+ buffers["time_buffer"] = (np.timedelta64(0, "ns"), max_age)
318
+
319
+ self.met = self.source.downselect_met(self.met, **buffers)
320
+
321
+
322
+ def _perform_interp_for_step(
323
+ met: MetDataset,
324
+ vector: GeoVectorDataset,
325
+ dz_m: float,
326
+ **interp_kwargs: Any,
327
+ ) -> None:
328
+ """Perform all interpolation required for one step of advection."""
329
+
330
+ vector.setdefault("level", vector.level)
331
+ air_pressure = vector.setdefault("air_pressure", vector.air_pressure)
332
+
333
+ models.interpolate_met(met, vector, "northward_wind", "v_wind", **interp_kwargs)
334
+ models.interpolate_met(met, vector, "eastward_wind", "u_wind", **interp_kwargs)
335
+ models.interpolate_met(
336
+ met,
337
+ vector,
338
+ "lagrangian_tendency_of_air_pressure",
339
+ "vertical_velocity",
340
+ **interp_kwargs,
341
+ )
342
+
343
+ az = vector.get("azimuth")
344
+ if az is None:
345
+ # Early exit for pointwise only simulation
346
+ return
347
+
348
+ air_temperature = models.interpolate_met(met, vector, "air_temperature", **interp_kwargs)
349
+ air_pressure_lower = thermo.pressure_dz(air_temperature, air_pressure, dz_m)
350
+ vector["air_pressure_lower"] = air_pressure_lower
351
+ level_lower = air_pressure_lower / 100.0
352
+
353
+ models.interpolate_met(
354
+ met,
355
+ vector,
356
+ "eastward_wind",
357
+ "u_wind_lower",
358
+ level=level_lower,
359
+ **interp_kwargs,
360
+ )
361
+ models.interpolate_met(
362
+ met,
363
+ vector,
364
+ "northward_wind",
365
+ "v_wind_lower",
366
+ level=level_lower,
367
+ **interp_kwargs,
368
+ )
369
+ models.interpolate_met(
370
+ met,
371
+ vector,
372
+ "air_temperature",
373
+ "air_temperature_lower",
374
+ level=level_lower,
375
+ **interp_kwargs,
376
+ )
377
+
378
+ lons = vector["longitude"]
379
+ lats = vector["latitude"]
380
+ dist = 1000.0
381
+
382
+ # These should probably not be included in model input ... so
383
+ # we'll get a warning if they get overwritten
384
+ longitude_head, latitude_head = geo.forward_azimuth(lons=lons, lats=lats, az=az, dist=dist)
385
+ longitude_tail, latitude_tail = geo.forward_azimuth(lons=lons, lats=lats, az=az, dist=-dist)
386
+ vector["longitude_head"] = longitude_head
387
+ vector["latitude_head"] = latitude_head
388
+ vector["longitude_tail"] = longitude_tail
389
+ vector["latitude_tail"] = latitude_tail
390
+
391
+ for met_key in ("eastward_wind", "northward_wind"):
392
+ vector_key = f"{met_key}_head"
393
+ models.interpolate_met(
394
+ met,
395
+ vector,
396
+ met_key,
397
+ vector_key,
398
+ **interp_kwargs,
399
+ longitude=longitude_head,
400
+ latitude=latitude_head,
401
+ )
402
+
403
+ vector_key = f"{met_key}_tail"
404
+ models.interpolate_met(
405
+ met,
406
+ vector,
407
+ met_key,
408
+ vector_key,
409
+ **interp_kwargs,
410
+ longitude=longitude_tail,
411
+ latitude=latitude_tail,
412
+ )
413
+
414
+
415
+ def _calc_geometry(
416
+ vector: GeoVectorDataset,
417
+ dz_m: float,
418
+ dt: npt.NDArray[np.timedelta64] | np.timedelta64,
419
+ max_depth: float | None,
420
+ verbose_outputs: bool,
421
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
422
+ """Calculate wind-shear-derived geometry of evolved plume.
423
+
424
+ This method mutates the input ``vector`` in place.
425
+ """
426
+
427
+ u_wind = vector["u_wind"]
428
+ v_wind = vector["v_wind"]
429
+ u_wind_lower = vector.data.pop("u_wind_lower")
430
+ v_wind_lower = vector.data.pop("v_wind_lower")
431
+
432
+ air_temperature = vector["air_temperature"]
433
+ air_temperature_lower = vector.data.pop("air_temperature_lower")
434
+ air_pressure = vector["air_pressure"]
435
+ air_pressure_lower = vector.data.pop("air_pressure_lower")
436
+
437
+ ds_dz = wind_shear.wind_shear(u_wind, u_wind_lower, v_wind, v_wind_lower, dz_m)
438
+
439
+ azimuth = vector["azimuth"]
440
+ latitude = vector["latitude"]
441
+ cos_a, sin_a = geo.azimuth_to_direction(azimuth, latitude)
442
+
443
+ width = vector["width"]
444
+ depth = vector["depth"]
445
+ sigma_yz = vector["sigma_yz"]
446
+ area_eff = vector["area_eff"]
447
+
448
+ dsn_dz = wind_shear.wind_shear_normal(
449
+ u_wind_top=u_wind,
450
+ u_wind_btm=u_wind_lower,
451
+ v_wind_top=v_wind,
452
+ v_wind_btm=v_wind_lower,
453
+ cos_a=cos_a,
454
+ sin_a=sin_a,
455
+ dz=dz_m,
456
+ )
457
+
458
+ dT_dz = thermo.T_potential_gradient(
459
+ air_temperature,
460
+ air_pressure,
461
+ air_temperature_lower,
462
+ air_pressure_lower,
463
+ dz_m,
464
+ )
465
+
466
+ depth_eff = contrail_properties.plume_effective_depth(width, area_eff)
467
+
468
+ diffuse_h = contrail_properties.horizontal_diffusivity(ds_dz, depth)
469
+ diffuse_v = contrail_properties.vertical_diffusivity(
470
+ air_pressure,
471
+ air_temperature,
472
+ dT_dz,
473
+ depth_eff,
474
+ terminal_fall_speed=0.0,
475
+ sedimentation_impact_factor=0.0,
476
+ eff_heat_rate=None,
477
+ )
478
+
479
+ if verbose_outputs:
480
+ vector["ds_dz"] = ds_dz
481
+ vector["dsn_dz"] = dsn_dz
482
+ vector["dT_dz"] = dT_dz
483
+
484
+ sigma_yy_2, sigma_zz_2, sigma_yz_2 = contrail_properties.plume_temporal_evolution(
485
+ width,
486
+ depth,
487
+ sigma_yz,
488
+ dsn_dz,
489
+ diffuse_h,
490
+ diffuse_v,
491
+ seg_ratio=1.0,
492
+ dt=dt,
493
+ max_depth=max_depth,
494
+ )
495
+ width_2, depth_2 = contrail_properties.new_contrail_dimensions(sigma_yy_2, sigma_zz_2)
496
+ area_eff_2 = contrail_properties.plume_effective_cross_sectional_area(
497
+ width_2, depth_2, sigma_yz_2
498
+ )
499
+
500
+ longitude_head = vector.data.pop("longitude_head")
501
+ latitude_head = vector.data.pop("latitude_head")
502
+ longitude_tail = vector.data.pop("longitude_tail")
503
+ latitude_tail = vector.data.pop("latitude_tail")
504
+ u_wind_head = vector.data.pop("eastward_wind_head")
505
+ v_wind_head = vector.data.pop("northward_wind_head")
506
+ u_wind_tail = vector.data.pop("eastward_wind_tail")
507
+ v_wind_tail = vector.data.pop("northward_wind_tail")
508
+
509
+ longitude_head_t2, latitude_head_t2 = geo.advect_horizontal(
510
+ longitude=longitude_head,
511
+ latitude=latitude_head,
512
+ u_wind=u_wind_head,
513
+ v_wind=v_wind_head,
514
+ dt=dt,
515
+ )
516
+ longitude_tail_t2, latitude_tail_t2 = geo.advect_horizontal(
517
+ longitude=longitude_tail,
518
+ latitude=latitude_tail,
519
+ u_wind=u_wind_tail,
520
+ v_wind=v_wind_tail,
521
+ dt=dt,
522
+ )
523
+
524
+ azimuth_2 = geo.azimuth(
525
+ lons0=longitude_tail_t2,
526
+ lats0=latitude_tail_t2,
527
+ lons1=longitude_head_t2,
528
+ lats1=latitude_head_t2,
529
+ )
530
+
531
+ return azimuth_2, width_2, depth_2, sigma_yz_2, area_eff_2
532
+
533
+
534
+ def _evolve_one_step(
535
+ met: MetDataset,
536
+ vector: GeoVectorDataset,
537
+ t: np.datetime64,
538
+ *,
539
+ sedimentation_rate: float,
540
+ dz_m: float,
541
+ max_depth: float | None,
542
+ verbose_outputs: bool,
543
+ **interp_kwargs: Any,
544
+ ) -> GeoVectorDataset:
545
+ """Evolve plume geometry by one step.
546
+
547
+ This method mutates the input ``vector`` in place.
548
+ """
549
+
550
+ _perform_interp_for_step(met, vector, dz_m, **interp_kwargs)
551
+ u_wind = vector["u_wind"]
552
+ v_wind = vector["v_wind"]
553
+ vertical_velocity = vector["vertical_velocity"] + sedimentation_rate
554
+
555
+ latitude = vector["latitude"]
556
+ longitude = vector["longitude"]
557
+
558
+ dt = t - vector["time"]
559
+ longitude_2, latitude_2 = geo.advect_horizontal(longitude, latitude, u_wind, v_wind, dt) # type: ignore[arg-type]
560
+ level_2 = geo.advect_level(
561
+ vector.level,
562
+ vertical_velocity,
563
+ rho_air=0.0,
564
+ terminal_fall_speed=0.0,
565
+ dt=dt, # type: ignore[arg-type]
566
+ )
567
+
568
+ out = GeoVectorDataset._from_fastpath(
569
+ {
570
+ "longitude": longitude_2,
571
+ "latitude": latitude_2,
572
+ "level": level_2,
573
+ "time": np.full(longitude_2.shape, t),
574
+ "age": vector["age"] + dt,
575
+ "waypoint": vector["waypoint"],
576
+ }
577
+ )
578
+
579
+ flight_id = vector.get("flight_id")
580
+ if flight_id is not None:
581
+ out["flight_id"] = flight_id
582
+
583
+ azimuth = vector.get("azimuth")
584
+ if azimuth is None:
585
+ # Early exit for "pointwise only" simulation
586
+ return out
587
+
588
+ # Attach wind-shear-derived geometry to output vector
589
+ azimuth_2, width_2, depth_2, sigma_yz_2, area_eff_2 = _calc_geometry(
590
+ vector,
591
+ dz_m=dz_m,
592
+ dt=dt, # type: ignore[arg-type]
593
+ max_depth=max_depth,
594
+ verbose_outputs=verbose_outputs,
595
+ )
596
+ out["azimuth"] = azimuth_2
597
+ out["width"] = width_2
598
+ out["depth"] = depth_2
599
+ out["sigma_yz"] = sigma_yz_2
600
+ out["area_eff"] = area_eff_2
601
+
602
+ 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
+ "EDBGaseous",
15
+ "EDBnvpm",
16
+ "Emissions",
17
+ "EmissionsParams",
18
+ "load_default_aircraft_engine_mapping",
19
+ "load_engine_nvpm_profile_from_edb",
20
+ "load_engine_params_from_edb",
21
+ ]