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,42 @@
1
+ """
2
+ Interface to ``pycontrails-bada`` extension.
3
+
4
+ Requires data files obtained with a
5
+ `BADA License <https://www.eurocontrol.int/model/bada>`_ from Eurocontrol.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ try:
11
+ from pycontrails_bada import bada3, bada4, bada_model
12
+ from pycontrails_bada.bada3 import BADA3
13
+ from pycontrails_bada.bada4 import BADA4
14
+ from pycontrails_bada.bada_interface import BADA
15
+ from pycontrails_bada.bada_model import (
16
+ BADAFlight,
17
+ BADAFlightParams,
18
+ BADAGrid,
19
+ BADAGridParams,
20
+ BADAParams,
21
+ )
22
+
23
+ except ImportError as e:
24
+ raise ImportError(
25
+ "Failed to import the 'pycontrails-bada' package. Install with 'pip install "
26
+ "--index-url https://us-central1-python.pkg.dev/contrails-301217/pycontrails/simple "
27
+ "pycontrails-bada'."
28
+ ) from e
29
+ else:
30
+ __all__ = [
31
+ "BADA",
32
+ "BADA3",
33
+ "BADA4",
34
+ "BADAFlight",
35
+ "BADAFlightParams",
36
+ "BADAGrid",
37
+ "BADAGridParams",
38
+ "BADAParams",
39
+ "bada3",
40
+ "bada4",
41
+ "bada_model",
42
+ ]
@@ -0,0 +1,14 @@
1
+ """Interface to `pycontrails-cirium` extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ from pycontrails_cirium import Cirium
7
+
8
+ except ImportError as e:
9
+ raise ImportError(
10
+ "Failed to import the 'pycontrails-cirium' package. Install with 'pip install"
11
+ ' "pycontrails-cirium @ git+ssh://git@github.com/contrailcirrus/pycontrails-cirium.git"\'.'
12
+ ) from e
13
+ else:
14
+ __all__ = ["Cirium"]
@@ -0,0 +1,140 @@
1
+ """Simulate aircraft performance using empirical historical data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Any, NoReturn, overload
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import pandas as pd
11
+
12
+ from pycontrails.core.aircraft_performance import (
13
+ AircraftPerformanceGrid,
14
+ AircraftPerformanceGridParams,
15
+ )
16
+ from pycontrails.core.met import MetDataset
17
+ from pycontrails.core.vector import GeoVectorDataset
18
+
19
+
20
+ @dataclasses.dataclass
21
+ class EmpiricalGridParams(AircraftPerformanceGridParams):
22
+ """Parameters for :class:`EmpiricalGrid`."""
23
+
24
+ #: Random state to use for sampling
25
+ random_state: int | np.random.Generator | None = None
26
+
27
+ #: Empirical data to use for sampling. Must include columns:
28
+ #: - altitude_ft
29
+ #: - true_airspeed
30
+ #: - aircraft_mass
31
+ #: - fuel_flow
32
+ #: - engine_efficiency
33
+ #: - aircraft_type
34
+ #: - wingspan
35
+ #: If None, an error will be raised at runtime.
36
+ data: pd.DataFrame | None = None
37
+
38
+
39
+ class EmpiricalGrid(AircraftPerformanceGrid):
40
+ """Simulate aircraft performance using empirical historical data.
41
+
42
+ For each altitude, the model samples from the empirical data to obtain
43
+ hypothetical aircraft performance. The data is sampled with replacement,
44
+ so the same data may be used multiple times.
45
+
46
+ .. warning:: This model is experimental and will change in the future.
47
+
48
+ .. versionadded:: 0.47.0
49
+ """
50
+
51
+ name = "empirical_grid"
52
+ long_name = "Empirical Grid Aircraft Performance Model"
53
+
54
+ source: GeoVectorDataset
55
+ default_params = EmpiricalGridParams
56
+
57
+ variables = (
58
+ "true_airspeed",
59
+ "aircraft_mass",
60
+ "fuel_flow",
61
+ "engine_efficiency",
62
+ "aircraft_type",
63
+ "wingspan",
64
+ )
65
+
66
+ @overload
67
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
68
+
69
+ @overload
70
+ def eval(self, source: MetDataset | None = ..., **params: Any) -> NoReturn: ...
71
+
72
+ def eval(
73
+ self, source: GeoVectorDataset | MetDataset | None = None, **params: Any
74
+ ) -> GeoVectorDataset:
75
+ """Query the provided historical data and sample from it.
76
+
77
+ Parameters
78
+ ----------
79
+ source : GeoVectorDataset, optional
80
+ The source vector dataset to evaluate the model on. Presently, only
81
+ altitude is used to query the ``data`` parameter.
82
+ **params
83
+ Parameters to update the model with.
84
+
85
+ Returns
86
+ -------
87
+ GeoVectorDataset
88
+ The evaluated vector dataset.
89
+ """
90
+
91
+ self.update_params(**params)
92
+ self.set_source(source)
93
+ self.require_source_type(GeoVectorDataset)
94
+
95
+ altitude_ft = self.source.altitude_ft.copy()
96
+ altitude_ft.round(-3, out=altitude_ft) # round to flight levels
97
+
98
+ # Fill the source with sampled data at each flight level
99
+ self._sample(altitude_ft)
100
+
101
+ return self.source
102
+
103
+ def _query_data(self) -> pd.DataFrame:
104
+ """Query ``self.params["data"]`` for the source aircraft type."""
105
+
106
+ # Take only the columns that are not already in the source
107
+ columns = [v for v in self.variables if v not in self.source]
108
+ data = self.params["data"]
109
+ if data is None:
110
+ raise ValueError("No data provided")
111
+
112
+ aircraft_type = self.source.attrs.get("aircraft_type", self.params["aircraft_type"])
113
+ data = data.query(f"aircraft_type == '{aircraft_type}'")
114
+ assert not data.empty, f"No data for aircraft type: {aircraft_type}"
115
+
116
+ # Round to flight levels
117
+ data.loc[:, "altitude_ft"] = data["altitude_ft"].round(-3)
118
+
119
+ return data[["altitude_ft", *columns]].drop(columns=["aircraft_type"])
120
+
121
+ def _sample(self, altitude_ft: npt.NDArray[np.floating]) -> None:
122
+ """Sample the data and update the source."""
123
+
124
+ df = self._query_data()
125
+ grouped = df.groupby("altitude_ft")
126
+ rng = self.params["random_state"]
127
+
128
+ source = self.source
129
+ for k in df:
130
+ source[k] = np.full_like(altitude_ft, np.nan)
131
+
132
+ for altitude, group in grouped:
133
+ filt = altitude_ft == altitude
134
+ n = filt.sum()
135
+ if n == 0:
136
+ continue
137
+
138
+ sample = group.sample(n=n, replace=True, random_state=rng)
139
+ for k, v in sample.items():
140
+ source[k][filt] = v
@@ -0,0 +1,431 @@
1
+ """Tools for creating synthetic data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import pathlib
7
+ import warnings
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ from pyproj.geod import Geod
13
+
14
+ from pycontrails.core.flight import Flight
15
+ from pycontrails.core.met import MetDataArray
16
+ from pycontrails.physics import constants, geo, units
17
+ from pycontrails.utils.types import ArrayOrFloat
18
+
19
+ try:
20
+ from pycontrails.ext.bada import bada_model
21
+ except ImportError as e:
22
+ raise ImportError(
23
+ "Failed to import the 'pycontrails-bada' package. Install with 'pip install "
24
+ "--index-url https://us-central1-python.pkg.dev/contrails-301217/pycontrails/simple "
25
+ "pycontrails-bada'."
26
+ ) from e
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ SAMPLE_AIRCRAFT_TYPES = [
31
+ "A20N",
32
+ "A319",
33
+ "A320",
34
+ "A321",
35
+ "A332",
36
+ "A333",
37
+ "A359",
38
+ "A388",
39
+ "B38M",
40
+ "B737",
41
+ "B738",
42
+ "B739",
43
+ "B744",
44
+ "B752",
45
+ "B763",
46
+ "B772",
47
+ "B77W",
48
+ "B788",
49
+ "B789",
50
+ "CRJ2",
51
+ "CRJ7",
52
+ "CRJ9",
53
+ "E145",
54
+ "E190",
55
+ "E195",
56
+ "E75L",
57
+ "E75S",
58
+ ]
59
+
60
+
61
+ class SyntheticFlight:
62
+ """Create a synthetic flight."""
63
+
64
+ # Random number generator
65
+ rng = np.random.default_rng(None)
66
+
67
+ # Maximum queue size. When generating many thousands of flights, performance is slightly
68
+ # improved if this parameter is increased.
69
+ max_queue_size = 100
70
+
71
+ # Minimum number of waypoints in flight
72
+ min_n_waypoints = 10
73
+
74
+ # Type hint a few instance variables
75
+ bada: bada_model.BADA | None
76
+ aircraft_type: str
77
+
78
+ longitude_min: float
79
+ longitude_max: float
80
+ latitude_min: float
81
+ latitude_max: float
82
+ level_min: float
83
+ level_max: float
84
+ time_min: np.datetime64
85
+ time_max: np.datetime64
86
+
87
+ def __init__(
88
+ self,
89
+ bounds: dict[str, np.ndarray],
90
+ aircraft_type: str | None = None,
91
+ bada3_path: str | pathlib.Path | None = None,
92
+ bada4_path: str | pathlib.Path | None = None,
93
+ speed_m_per_s: float | None = None,
94
+ seed: int | None = None,
95
+ u_wind: MetDataArray | None = None,
96
+ v_wind: MetDataArray | None = None,
97
+ ) -> None:
98
+ """Create a synthetic flight generator.
99
+
100
+ Parameters
101
+ ----------
102
+ bounds : dict[str, np.ndarray]
103
+ A dictionary with keys "longitude", "latitude", "level", and "time". All synthetic
104
+ flights will have coordinates bounded by the extreme values in this dictionary.
105
+ aircraft_type : str
106
+ A flight type, assumed to exist in the BADA3 or BADA4 dataset.
107
+ If None provided, a random aircraft type will be chosen from
108
+ ``SAMPLE_AIRCRAFT`` on every call.
109
+ bada3_path : str | pathlib.Path, optional
110
+ A path to a local BADA3 data source.
111
+ Defaults to None.
112
+ bada4_path : str | pathlib.Path, optional
113
+ A path to a local BADA4 data source.
114
+ Defaults to None.
115
+ speed_m_per_s : float, optional
116
+ Directly define cruising air speed. Only used if `bada3_path` and `bada4_path` are
117
+ not specified. By default None.
118
+ seed : int, optional
119
+ Reseed the random generator. By default None.
120
+ u_wind, v_wind : MetDataArray, optional
121
+ Eastward and northward wind data. If provided, flight true airspeed is computed
122
+ with respect to wind.
123
+ max_queue_size : int, optional
124
+ Maximum queue size. When generating many thousands of flights, performance is slightly
125
+ improved if this parameter is increased.
126
+ min_n_waypoints : int, optional
127
+ Minimum number of waypoints in flight
128
+ """
129
+ if seed is not None:
130
+ self.rng = np.random.default_rng(seed)
131
+
132
+ self.bounds = bounds
133
+ self.constant_aircraft_type = aircraft_type
134
+ self.bada3_path = bada3_path
135
+ self.bada4_path = bada4_path
136
+ self.speed_m_per_s = speed_m_per_s
137
+ self.geod = Geod(a=constants.radius_earth)
138
+
139
+ def extremes(arr: np.ndarray) -> tuple[Any, Any]:
140
+ arr = np.asarray(arr)
141
+ return arr.min(), arr.max()
142
+
143
+ self.longitude_min, self.longitude_max = extremes(bounds["longitude"])
144
+ self.latitude_min, self.latitude_max = extremes(bounds["latitude"])
145
+ self.level_min, self.level_max = extremes(bounds["level"])
146
+ self.time_min, self.time_max = extremes(bounds["time"])
147
+
148
+ self.u_wind = u_wind
149
+ self.v_wind = v_wind
150
+
151
+ self._queue: list[Flight] = []
152
+ self._depth = 0
153
+
154
+ def __repr__(self) -> str:
155
+ """Get string representation."""
156
+ msg = "Synthetic flight generator with parameters:"
157
+ for key in ["longitude", "latitude", "level", "time"]:
158
+ msg += "\n"
159
+ for bound in ["min", "max"]:
160
+ attr = f"{key}_{bound}"
161
+ val = getattr(self, attr)
162
+ msg += f"{attr}: {val} "
163
+ msg += f"\nmax_queue_size: {self.max_queue_size} min_n_waypoints: {self.min_n_waypoints}"
164
+ return msg
165
+
166
+ def __call__(self, timestep: np.timedelta64 | None = None) -> Flight:
167
+ """Create random flight within `bounds` at a constant altitude.
168
+
169
+ BADA4 data is used to determine flight speed at a randomly chosen altitude within
170
+ the level constraints defined by `bounds`. The flight trajectory follows a great circle
171
+ from a uniformly random chosen source point to a uniformly randomly chosen destination.
172
+
173
+ For generating a large number of random flights, it's best to call this within a
174
+ generator expression, ie,
175
+ ``fls = (syn() for _ in range(100_000))``
176
+
177
+ Parameters
178
+ ----------
179
+ timestep : np.timedelta64, optional
180
+ Time interval between waypoints. By default, 1 minute
181
+
182
+ Returns
183
+ -------
184
+ Flight
185
+ Random `Flight` instance constrained by bounds.
186
+ """
187
+ if timestep is None:
188
+ timestep = np.timedelta64(1, "m")
189
+ # Building flights with `u_wind` and `v_wind` involved in the true airspeed calculation is
190
+ # slow. BUT, we can do it in a vectorized way. So we maintain a short queue that gets
191
+ # repeatedly replenished.
192
+ if self.u_wind is not None and self.v_wind is not None:
193
+ while not self._queue:
194
+ # Need to do some significant refactor to use generators seemlessly
195
+ # The parameter timestep can change between calls
196
+ new_batch = self._generate_with_wind(self.max_queue_size, timestep)
197
+ self._queue.extend(new_batch)
198
+ fl = self._queue.pop()
199
+ while np.any(np.diff(fl["time"]) != timestep):
200
+ logger.debug("Found flight in queue with bad timestep.")
201
+ fl = self._queue.pop()
202
+ return fl
203
+
204
+ self._depth = 0
205
+ self._define_aircraft()
206
+ return self._generate_single_flight_no_wind(timestep)
207
+
208
+ def _id(self) -> int:
209
+ """Get random flight ID."""
210
+ return self.rng.integers(100_000, 999_999).item()
211
+
212
+ def _define_aircraft(self) -> None:
213
+ """Define or update instance variables pertaining to flight aircrafts.
214
+
215
+ Specify
216
+ - aircraft_type
217
+ - bada_enabled
218
+ - fl_all
219
+ - cruise_tas
220
+
221
+ Raises
222
+ ------
223
+ FileNotFoundError
224
+ BADA files not found despite non-default BADA3 or BADA4 paths
225
+ ValueError
226
+ BADA files not found under default paths AND speed_m_per_s not defined
227
+ """
228
+ if self.constant_aircraft_type is None:
229
+ self.aircraft_type = self.rng.choice(SAMPLE_AIRCRAFT_TYPES)
230
+ else:
231
+ self.aircraft_type = self.constant_aircraft_type
232
+
233
+ try:
234
+ self.bada = bada_model.get_bada(
235
+ self.aircraft_type,
236
+ bada3_path=self.bada3_path,
237
+ bada4_path=self.bada4_path,
238
+ bada_priority=4,
239
+ )
240
+
241
+ except FileNotFoundError as err:
242
+ logger.warning("BADA files not found")
243
+
244
+ # If non-default bada paths were passed into __init__, we should raise an error
245
+ if self.bada3_path is not None or self.bada4_path is not None:
246
+ raise FileNotFoundError from err
247
+
248
+ # If bada paths were not passed into __init__, we expect to know speed_m_per_s
249
+ if self.speed_m_per_s is None:
250
+ msg = "Either specify 'bada3_path', 'bada4_path', or 'speed_m_per_s'."
251
+ raise ValueError(msg) from err
252
+ self.bada = None
253
+
254
+ def _calc_speed_m_per_s(self, level: ArrayOrFloat) -> ArrayOrFloat:
255
+ if self.bada is not None:
256
+ alt_ft = units.pl_to_ft(level)
257
+ return self.bada.nominal_cruising_speed(self.aircraft_type, alt_ft)
258
+
259
+ if self.speed_m_per_s is None:
260
+ raise ValueError("Either specify `bada3_path`, `bada4_path` or `speed_m_per_s`.")
261
+
262
+ if isinstance(level, np.ndarray):
263
+ return np.full_like(level, self.speed_m_per_s)
264
+ return self.speed_m_per_s
265
+
266
+ def _generate_single_flight_no_wind(self, timestep: np.timedelta64) -> Flight:
267
+ src_lon = self.rng.uniform(self.longitude_min, self.longitude_max)
268
+ src_lat = self.rng.uniform(self.latitude_min, self.latitude_max)
269
+ dest_lon = self.rng.uniform(self.longitude_min, self.longitude_max)
270
+ dest_lat = self.rng.uniform(self.latitude_min, self.latitude_max)
271
+ src = src_lon, src_lat
272
+ dest = dest_lon, dest_lat
273
+ az, _, dist = self.geod.inv(*src, *dest)
274
+
275
+ level = self.rng.uniform(self.level_min, self.level_max)
276
+ speed_m_per_s = self._calc_speed_m_per_s(level)
277
+
278
+ m_per_timestep = speed_m_per_s * (timestep / np.timedelta64(1, "s"))
279
+ npts = int(dist // m_per_timestep) # need to cast: dist is np.float64
280
+
281
+ # Dealing with situations of npts too small or too big
282
+ if npts > (self.time_max - self.time_min) / timestep:
283
+ msg = (
284
+ "Not enough available time in `bounds` to create good flight between "
285
+ f"source {src} and destination {dest}. Try enlarging the time dimension, "
286
+ "or reducing the longitude and latitude dimensions."
287
+ )
288
+
289
+ new_npts = int((self.time_max - self.time_min) / timestep)
290
+ logger.debug("Override npts from %s to %s", npts, new_npts)
291
+ npts = new_npts
292
+
293
+ if npts < self.min_n_waypoints:
294
+ raise ValueError(msg)
295
+ warnings.warn(msg)
296
+
297
+ if npts < self.min_n_waypoints:
298
+ # Try 10 times, then give up.
299
+ self._depth += 1
300
+ if self._depth > 10:
301
+ raise ValueError("Cannot create flight. Increase dimensions in `bounds`.")
302
+ return self._generate_single_flight_no_wind(timestep) # recursive
303
+
304
+ result = self.geod.fwd_intermediate(
305
+ *src,
306
+ az,
307
+ npts,
308
+ m_per_timestep,
309
+ return_back_azimuth=False,
310
+ )
311
+ longitude = np.asarray(result.lons)
312
+ latitude = np.asarray(result.lats)
313
+ if geo.haversine(longitude[-1], latitude[-1], *dest) > m_per_timestep:
314
+ logger.debug(
315
+ "Synthetic flight did not arrive at destination. "
316
+ "This is likely due to overriding npts."
317
+ )
318
+
319
+ rand_range = int((self.time_max - self.time_min - npts * timestep) / timestep) + 1
320
+ time0 = self.time_min + self.rng.integers(rand_range) * timestep
321
+ time: np.ndarray = np.arange(time0, time0 + npts * timestep, timestep)
322
+
323
+ df = {"longitude": longitude, "latitude": latitude, "level": level, "time": time}
324
+ return Flight(pd.DataFrame(df), flight_id=self._id(), aircraft_type=self.aircraft_type)
325
+
326
+ def _generate_with_wind(self, n: int, timestep: np.timedelta64) -> list[Flight]:
327
+ logger.debug("Generate %s new flights with wind", n)
328
+
329
+ self._define_aircraft()
330
+
331
+ # Step 1: Randomly select longitude, latitude, level
332
+ src_lon = self.rng.uniform(self.longitude_min, self.longitude_max, n)
333
+ src_lat = self.rng.uniform(self.latitude_min, self.latitude_max, n)
334
+ dest_lon = self.rng.uniform(self.longitude_min, self.longitude_max, n)
335
+ dest_lat = self.rng.uniform(self.latitude_min, self.latitude_max, n)
336
+ level = self.rng.uniform(self.level_min, self.level_max, n)
337
+ src = src_lon, src_lat
338
+ dest = dest_lon, dest_lat
339
+ az: np.ndarray
340
+ dist: np.ndarray
341
+ az, _, dist = self.geod.inv(*src, *dest)
342
+
343
+ # Step 2: Compute approximate flight times according to nominal (no wind) TAS
344
+ nom_speed_m_per_s = self._calc_speed_m_per_s(level)
345
+ # NOTE: Because of casting, the multiplication below is NOT associative!
346
+ # In other words, the parentheses are needed on the right-most term
347
+ nom_m_per_timestep = nom_speed_m_per_s * (timestep / np.timedelta64(1, "s"))
348
+ approx_flight_duration_s = dist / nom_speed_m_per_s * np.timedelta64(1, "s")
349
+
350
+ # Step 3: Randomly select start time -- use 0.9 for a small buffer
351
+ rand_float = 0.9 * self.rng.random(n)
352
+ time_windows = self.time_max - self.time_min - approx_flight_duration_s
353
+
354
+ # Here `time_windows` can have negative timedeltas, which is not good.
355
+ n_negative = np.sum(time_windows < np.timedelta64(0, "s"))
356
+ logger.debug(
357
+ "Found %s / %s src-dist pairs not fitting into the time dimension", n_negative, n
358
+ )
359
+ if n_negative >= 0.1 * n:
360
+ warnings.warn(
361
+ "Not enough available time in `bounds` to create reasonable random flights. Try "
362
+ "enlarging the time dimension, or reducing the longitude and latitude dimensions."
363
+ )
364
+ # Manually clip at 0
365
+ time_windows = np.maximum(time_windows, np.timedelta64(0, "s"))
366
+
367
+ rand_start = rand_float * time_windows
368
+ start_time = self.time_min + rand_start
369
+
370
+ # Step 4: Iterate and collect
371
+ lons = []
372
+ lats = []
373
+ times = []
374
+
375
+ def take_step(
376
+ cur_lon: np.ndarray,
377
+ cur_lat: np.ndarray,
378
+ cur_time: np.typing.NDArray[np.datetime64],
379
+ cur_az: np.ndarray,
380
+ cur_active: np.ndarray,
381
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
382
+ sin, cos = geo.azimuth_to_direction(cur_az, cur_lat)
383
+
384
+ if self.u_wind is None or self.v_wind is None:
385
+ raise TypeError("Define attributes `u_wind` and `v_wind`.")
386
+ u_wind = self.u_wind.interpolate(cur_lon, cur_lat, level, cur_time)
387
+ v_wind = self.v_wind.interpolate(cur_lon, cur_lat, level, cur_time)
388
+
389
+ nom_x = nom_m_per_timestep * cos
390
+ nom_y = nom_m_per_timestep * sin
391
+ # NOTE: Because of casting, the multiplication below is NOT associative.
392
+ # In other words, the parentheses are needed on the right-most term
393
+ tas_x = nom_x + u_wind * (timestep / np.timedelta64(1, "s"))
394
+ tas_y = nom_y + v_wind * (timestep / np.timedelta64(1, "s"))
395
+ tas = (tas_x**2 + tas_y**2) ** 0.5
396
+
397
+ next_lon, next_lat, _ = self.geod.fwd(cur_lon, cur_lat, cur_az, tas)
398
+ next_az: np.ndarray
399
+ next_dist: np.ndarray
400
+ next_az, _, next_dist = self.geod.inv(next_lon, next_lat, *dest)
401
+ next_time = cur_time + timestep
402
+ next_active = (
403
+ cur_active & (next_dist > nom_m_per_timestep) & (next_time < self.time_max)
404
+ )
405
+
406
+ return next_lon, next_lat, next_time, next_az, next_active
407
+
408
+ lon = src_lon
409
+ lat = src_lat
410
+ time = start_time
411
+ active = np.ones(lon.shape, dtype=bool)
412
+
413
+ while active.sum():
414
+ lons.append(np.where(active, lon, np.nan))
415
+ lats.append(np.where(active, lat, np.nan))
416
+ times.append(time)
417
+ lon, lat, time, az, active = take_step(lon, lat, time, az, active)
418
+
419
+ # Step 5: Assemble and return
420
+ lons_arr = np.asarray(lons).T
421
+ lats_arr = np.asarray(lats).T
422
+ times_arr = np.asarray(times).T
423
+ data = [
424
+ {"longitude": lon, "latitude": lat, "level": level, "time": time}
425
+ for lon, lat, level, time in zip(lons_arr, lats_arr, level, times_arr, strict=True)
426
+ ]
427
+ dfs = [pd.DataFrame(d).dropna() for d in data]
428
+ dfs = [df for df in dfs if len(df) >= self.min_n_waypoints]
429
+ dfs = [df.assign(altitude=units.pl_to_m(df["level"])).drop(columns="level") for df in dfs]
430
+
431
+ return [Flight(df, flight_id=self._id(), aircraft_type=self.aircraft_type) for df in dfs]
@@ -0,0 +1 @@
1
+ """Contrail modeling support."""