pycontrails 0.49.3__cp312-cp312-win_amd64.whl → 0.49.5__cp312-cp312-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/_version.py +2 -2
- pycontrails/core/datalib.py +1 -1
- pycontrails/core/flight.py +11 -11
- pycontrails/core/interpolation.py +29 -19
- pycontrails/core/met.py +192 -104
- pycontrails/core/models.py +29 -15
- pycontrails/core/rgi_cython.cp312-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +14 -15
- pycontrails/datalib/gfs/gfs.py +1 -1
- pycontrails/datalib/spire/spire.py +23 -19
- pycontrails/ext/synthetic_flight.py +3 -1
- pycontrails/models/accf.py +6 -4
- pycontrails/models/cocip/cocip.py +48 -18
- pycontrails/models/cocip/cocip_params.py +13 -10
- pycontrails/models/cocip/output_formats.py +62 -52
- pycontrails/models/cocipgrid/cocip_grid.py +459 -275
- pycontrails/models/cocipgrid/cocip_grid_params.py +12 -18
- pycontrails/models/emissions/ffm2.py +10 -8
- pycontrails/models/pcc.py +1 -1
- pycontrails/models/ps_model/ps_aircraft_params.py +1 -1
- pycontrails/models/ps_model/static/{ps-aircraft-params-20231117.csv → ps-aircraft-params-20240209.csv} +12 -3
- pycontrails/utils/json.py +12 -10
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/METADATA +2 -2
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/RECORD +28 -29
- pycontrails/models/cocipgrid/cocip_time_handling.py +0 -342
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/LICENSE +0 -0
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/NOTICE +0 -0
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/WHEEL +0 -0
- {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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|