pycontrails 0.58.0__cp314-cp314-win_amd64.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.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cp314-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +5 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- 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
|
+
]
|