pycontrails 0.49.3__cp310-cp310-win_amd64.whl → 0.49.5__cp310-cp310-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.

Files changed (29) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/datalib.py +1 -1
  3. pycontrails/core/flight.py +11 -11
  4. pycontrails/core/interpolation.py +29 -19
  5. pycontrails/core/met.py +192 -104
  6. pycontrails/core/models.py +29 -15
  7. pycontrails/core/rgi_cython.cp310-win_amd64.pyd +0 -0
  8. pycontrails/core/vector.py +14 -15
  9. pycontrails/datalib/gfs/gfs.py +1 -1
  10. pycontrails/datalib/spire/spire.py +23 -19
  11. pycontrails/ext/synthetic_flight.py +3 -1
  12. pycontrails/models/accf.py +6 -4
  13. pycontrails/models/cocip/cocip.py +48 -18
  14. pycontrails/models/cocip/cocip_params.py +13 -10
  15. pycontrails/models/cocip/output_formats.py +62 -52
  16. pycontrails/models/cocipgrid/cocip_grid.py +459 -275
  17. pycontrails/models/cocipgrid/cocip_grid_params.py +12 -18
  18. pycontrails/models/emissions/ffm2.py +10 -8
  19. pycontrails/models/pcc.py +1 -1
  20. pycontrails/models/ps_model/ps_aircraft_params.py +1 -1
  21. pycontrails/models/ps_model/static/{ps-aircraft-params-20231117.csv → ps-aircraft-params-20240209.csv} +12 -3
  22. pycontrails/utils/json.py +12 -10
  23. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/METADATA +2 -2
  24. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/RECORD +28 -29
  25. pycontrails/models/cocipgrid/cocip_time_handling.py +0 -342
  26. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/LICENSE +0 -0
  27. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/NOTICE +0 -0
  28. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/WHEEL +0 -0
  29. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/top_level.txt +0 -0
@@ -1,342 +0,0 @@
1
- """Utilities for :class:`Cocip` and :class:`CocipGrid`."""
2
-
3
- from __future__ import annotations
4
-
5
- import dataclasses
6
- import logging
7
- import warnings
8
- from typing import TYPE_CHECKING, Any
9
-
10
- import numpy as np
11
- import numpy.typing as npt
12
- import pandas as pd
13
-
14
- from pycontrails.core import coordinates
15
- from pycontrails.core.met import MetDataset
16
- from pycontrails.core.vector import GeoVectorDataset
17
- from pycontrails.models.cocip import cocip
18
- from pycontrails.utils import dependencies
19
-
20
- if TYPE_CHECKING:
21
- import tqdm
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
- # Crude constants used to estimate model runtime
26
- # If we seek more accuracy, these should be seasonally adjusted
27
- # The first constant is the proportion of points generating persistent contrails
28
- # that survive to the end of the hour
29
- # The second constants it the probability of a contrail surviving an hour of
30
- # ongoing model evolution
31
- HEURISTIC_INITIAL_SURVIVAL_RATE = 0.1
32
- HEURISTIC_EVOLUTION_SURVIVAL_RATE = 0.8
33
-
34
-
35
- class CocipTimeHandlingMixin:
36
- """Support :class:`Cocip` and :class:`CocipGrid` time handling."""
37
-
38
- params: dict[str, Any]
39
- source: GeoVectorDataset | MetDataset
40
- met: MetDataset
41
- rad: MetDataset
42
-
43
- #: Convenience container to hold time filters
44
- #: See :meth:`CocipGrid.eval` for usage.
45
- timedict: dict[np.datetime64, np.ndarray]
46
-
47
- def validate_time_params(self) -> None:
48
- """Raise a `ValueError` if `met_slice_dt` is not a multiple of `dt_integration`."""
49
- met_slice_dt: np.timedelta64 | None = self.params.get("met_slice_dt")
50
-
51
- if met_slice_dt is None:
52
- # TODO: Reset to something big enough to cover the entire met dataset
53
- raise NotImplementedError
54
- if met_slice_dt == np.timedelta64(0, "s"):
55
- raise ValueError("met_slice_dt must be a positive timedelta")
56
-
57
- dt_integration: np.timedelta64 = self.params["dt_integration"]
58
- ratio = met_slice_dt / dt_integration
59
- if not ratio.is_integer():
60
- raise ValueError(
61
- f"met_slice_dt ({met_slice_dt}) must be a multiple "
62
- f"of dt_integration ({dt_integration})"
63
- )
64
-
65
- # I'm not sure how necessary this is ...
66
- met_time_diff = np.diff(self.met.data["time"].values)
67
- ratios = met_slice_dt / met_time_diff
68
- if np.any(ratios != ratios.astype(int)):
69
- raise ValueError(
70
- f"met_slice_dt ({met_slice_dt}) must be a multiple "
71
- "of the time difference between met time steps "
72
- f"({met_time_diff})"
73
- )
74
-
75
- # Validate met_slice_dt
76
- met_time_diff = np.diff(self.met.data["time"].values).astype("timedelta64[h]")
77
- if np.unique(met_time_diff).size > 1:
78
- raise NotImplementedError("CocipGrid only supports met with constant time diff.")
79
- met_time_res = met_time_diff[0]
80
-
81
- n_hours = met_slice_dt / met_time_res
82
- if n_hours < 1 or not n_hours.is_integer():
83
- raise ValueError(
84
- f"Parameter `met_slice_dt` must be a positive multiple of {met_time_res}."
85
- )
86
-
87
- @property
88
- def source_time(self) -> npt.NDArray[np.datetime64]:
89
- """Return the time array of the :attr:`source` data."""
90
- if not hasattr(self, "source"):
91
- raise AttributeError("source not set")
92
- if isinstance(self.source, GeoVectorDataset):
93
- return self.source["time"]
94
- if isinstance(self.source, MetDataset):
95
- return self.source.variables["time"].values
96
- raise TypeError(f"Cannot calculate timesteps for {self.source}")
97
-
98
- def attach_timedict(self) -> None:
99
- """Attach or update :attr:`timedict`.
100
-
101
- This attribute is a dictionary of the form::
102
-
103
- {t: filt}
104
-
105
- where the key ``t`` is the time of the start of the met slice and the value
106
- ``filt`` is a :class:`numpy.ndarray` of the same shape as the
107
- the time source. Specifically, ``filt`` is a boolean array that can
108
- be used to filter the time source.
109
-
110
- Presently, keys are hard-coded with :attr:`dtype` ``datetime64[h]``.
111
-
112
- The keys of this dictionary can be used to slice the ``met`` dataset
113
- in the ``time`` dimension. However, this must be done with care in case
114
- the met dataset is not aligned with the time source.
115
- """
116
- met_slice_dt = self.params.get("met_slice_dt")
117
- if met_slice_dt is None:
118
- raise ValueError("met_slice_dt must be set")
119
- self.validate_time_params()
120
-
121
- # Cast to pandas to use ceil and floor methods below
122
- met_slice_dt = pd.to_timedelta(met_slice_dt)
123
- source_time = self.source_time
124
- tmin = pd.to_datetime(source_time.min())
125
- tmax = pd.to_datetime(source_time.max() + self.params["max_age"])
126
- self._check_met_rad_time(tmin, tmax)
127
-
128
- # Ideally we'd use the keys to index into the met dataset
129
- # with met.data.sel(time=slice(t1, t2)), but this is not even
130
- # possible the the rad data because of time shifting.
131
- # So, at the very least, we'll want to use np.searchsorted
132
- # to find the indices of the met dataset that correspond to
133
- # the time slice. See _load_met_slices
134
- t_start = tmin.floor(met_slice_dt)
135
- t_end = tmax.ceil(met_slice_dt)
136
-
137
- met_times = np.arange(t_start, t_end + met_slice_dt, met_slice_dt).astype("datetime64[h]")
138
- zipped = zip(met_times, met_times[1:])
139
- self.timedict = {t1: (source_time >= t1) & (source_time < t2) for t1, t2 in zipped}
140
-
141
- def _check_met_rad_time(self, tmin: pd.Timestamp, tmax: pd.Timestamp) -> None:
142
- if self.met.data["time"].min() > tmin or self.met.data["time"].max() < tmax:
143
- warnings.warn(
144
- "Parameter 'met' is too short in the time dimension. "
145
- "Include additional time in 'met' or reduce 'max_age' parameter. "
146
- f"Model start time: {tmin} Model end time: {tmax}"
147
- )
148
- if self.rad.data["time"].min() > tmin or self.rad.data["time"].max() < tmax:
149
- warnings.warn(
150
- "Parameter 'rad' is too short in the time dimension. "
151
- "Include additional time in 'rad' or reduce 'max_age' parameter."
152
- f"Model start time: {tmin} Model end time: {tmax}"
153
- )
154
-
155
- def init_pbar(self) -> tqdm.tqdm | None:
156
- """Initialize a progress bar for model evaluation."""
157
-
158
- if not self.params["show_progress"]:
159
- return None
160
-
161
- try:
162
- from tqdm.auto import tqdm
163
- except ModuleNotFoundError as exc:
164
- dependencies.raise_module_not_found_error(
165
- name="CocipGrid.init_pbar method",
166
- package_name="tqdm",
167
- module_not_found_error=exc,
168
- extra="Alternatively, set model parameter 'show_progress=False'.",
169
- )
170
-
171
- estimate = self._estimate_runtime()
172
-
173
- # We call update on contrail initialization and at each evolution step
174
- total = sum(e.n_steps_new_vectors + e.n_steps_old_vectors for e in estimate.values())
175
- # We also call update on each met loading step
176
- total += len(estimate)
177
-
178
- return tqdm(total=total, desc=f"{type(self).__name__} eval")
179
-
180
- def _estimate_runtime(self) -> dict[np.datetime64, CocipRuntimeStats]:
181
- """Calculate number of new meshes and predict number of persistent meshes by met slice.
182
-
183
- Returns
184
- -------
185
- dict[np.datetime64, CocipRuntimeStats]
186
- Estimate of the runtime for each met slice.
187
- """
188
-
189
- met_slice_dt = self.params["met_slice_dt"]
190
- if met_slice_dt is None:
191
- raise ValueError("met_slice_dt must be set")
192
- if not hasattr(self, "timedict"):
193
- raise AttributeError("First call attach_met_slice_timedict")
194
-
195
- dt_integration = self.params["dt_integration"]
196
- met_slice_dt = self.params["met_slice_dt"]
197
- if isinstance(self.source, MetDataset):
198
- n_splits = self._grid_spatial_n_splits()
199
- else:
200
- split_size = (
201
- self.params["target_split_size_pre_SAC_boost"] * self.params["target_split_size"]
202
- )
203
-
204
- # Adjust scaling factors when met_slice_dt is more than a single hour
205
- met_slice_scale = met_slice_dt / np.timedelta64(1, "h")
206
-
207
- # Mirror the structure of timedict to estimate the runtime
208
- estimate: dict[np.datetime64, CocipRuntimeStats] = {}
209
- for t_start, filt in self.timedict.items():
210
- t_next = t_start + met_slice_dt
211
- t_prev = t_start - met_slice_dt
212
-
213
- times_in_filt = self.source_time[filt]
214
- n_steps_by_time = np.ceil((t_next - times_in_filt) / dt_integration)
215
-
216
- # Tricky logic for different sources
217
- if isinstance(self.source, MetDataset):
218
- n_new_vectors = len(times_in_filt) * n_splits
219
- n_steps_new_vectors = int(n_steps_by_time.sum() + 1) * n_splits
220
- elif times_in_filt.size:
221
- n_new_vectors = max(int(times_in_filt.size / split_size), 1)
222
- n_steps_new_vectors = int(n_steps_by_time.max() + 1) * n_new_vectors
223
- else:
224
- n_new_vectors = 0
225
- n_steps_new_vectors = 0
226
-
227
- prev_stats = estimate.get(t_prev)
228
- if prev_stats is None:
229
- prev_stats = CocipRuntimeStats(t_prev, 0, 0, 0, 0)
230
-
231
- n_old_vectors_float = int(
232
- HEURISTIC_INITIAL_SURVIVAL_RATE * prev_stats.n_new_vectors
233
- + HEURISTIC_EVOLUTION_SURVIVAL_RATE**met_slice_scale * prev_stats.n_old_vectors
234
- )
235
- n_old_vectors = max(round(n_old_vectors_float), 1)
236
- n_steps_old_vectors = n_old_vectors * (met_slice_dt // dt_integration).item()
237
- estimate[t_start] = CocipRuntimeStats(
238
- t_start=t_start,
239
- n_new_vectors=n_new_vectors,
240
- n_steps_new_vectors=n_steps_new_vectors,
241
- n_old_vectors=n_old_vectors,
242
- n_steps_old_vectors=n_steps_old_vectors,
243
- )
244
-
245
- return estimate
246
-
247
- def _grid_spatial_n_splits(self) -> int:
248
- """Compute the number of vector "spatial" splits at a single time.
249
-
250
- Helper method used in :meth:`_estimate_runtime` and :meth:`_generate_new_grid_vectors`.
251
-
252
- This method assumes :attr:`source` is a :class:`MetDataset`.
253
-
254
- Returns
255
- -------
256
- int
257
- The number of spatial splits.
258
- """
259
- grid_size = (
260
- self.source.data["longitude"].size
261
- * self.source.data["latitude"].size
262
- * self.source.data["level"].size
263
- )
264
- split_size = int(
265
- self.params["target_split_size_pre_SAC_boost"] * self.params["target_split_size"]
266
- )
267
- return max(grid_size // split_size, 1)
268
-
269
- def _load_met_slices(
270
- self, start: np.datetime64, pbar: tqdm.tqdm | None = None
271
- ) -> tuple[MetDataset, MetDataset]:
272
- """Load met and rad slices for interpolation.
273
-
274
- :attr:`met` and :attr:`rad` are sliced by `slice(start, start + .met_slice_dt)`
275
-
276
- Parameters
277
- ----------
278
- start : np.datetime64
279
- Start of time domain of interest. Does not need to have hour resolution,
280
- but we will get runtime errors in other methods if not.
281
- pbar : tqdm.tqdm | None, optional
282
- Progress bar. The :meth:`pbar.update` method is called after both slices
283
- are loaded.
284
-
285
- Returns
286
- -------
287
- met : MetDataset
288
- Met data sliced to the time domain of interest.
289
- rad : MetDataset
290
- Rad data sliced to the time domain of interest.
291
-
292
- Raises
293
- ------
294
- NotImplementedError
295
- If :attr:`met` data has finer than hourly "time" resolution
296
- RuntimeError
297
- If ``met`` or ``rad`` time slices do not not contain at least two time values.
298
- """
299
-
300
- # Only support met with hourly timestamps
301
- time = self.met.variables["time"].values
302
- remainder = time - time.astype("datetime64[h]")
303
- if np.any(remainder.astype(float)):
304
- raise NotImplementedError("Only support met data with hourly time coordinates.")
305
-
306
- request = start, start + self.params["met_slice_dt"]
307
- buffer = np.timedelta64(0, "h"), np.timedelta64(0, "h")
308
- met_sl = coordinates.slice_domain(time, request, buffer)
309
- rad_sl = coordinates.slice_domain(self.rad.variables["time"].values, request, buffer)
310
-
311
- logger.debug("Update met slices. Start: %s, Stop: %s", met_sl.start, met_sl.stop)
312
- logger.debug("Update rad slices. Start: %s, Stop: %s", rad_sl.start, rad_sl.stop)
313
- xr_met_slice = self.met.data.isel(time=met_sl)
314
- xr_rad_slice = self.rad.data.isel(time=rad_sl)
315
-
316
- # We no longer require time two slices for linear interpolation, but for the
317
- # sake of contrail evolution, we expect to always use two. See method
318
- # _evolve_vector, where we explicitly unpack the time domain into two variables.
319
- if len(xr_met_slice["time"]) < 2:
320
- raise RuntimeError("Malformed met slice.")
321
- if len(xr_rad_slice["time"]) < 2:
322
- raise RuntimeError("Malformed rad slice.")
323
-
324
- # If data is already loaded into memory, calling load will not waste memory
325
- met_slice = MetDataset(xr_met_slice, copy=False)
326
- met_slice = cocip.add_tau_cirrus(met_slice)
327
- rad_slice = MetDataset(xr_rad_slice, copy=False)
328
-
329
- if pbar is not None:
330
- pbar.update()
331
- return met_slice, rad_slice
332
-
333
-
334
- @dataclasses.dataclass
335
- class CocipRuntimeStats:
336
- """Support for estimating runtime and progress bar."""
337
-
338
- t_start: np.datetime64
339
- n_new_vectors: int
340
- n_old_vectors: int
341
- n_steps_new_vectors: int
342
- n_steps_old_vectors: int