pycontrails 0.59.0__cp314-cp314-macosx_10_15_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.
- 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 +2936 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +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 +764 -0
- pycontrails/datalib/gruan.py +343 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +671 -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.59.0.dist-info/METADATA +179 -0
- pycontrails-0.59.0.dist-info/RECORD +123 -0
- pycontrails-0.59.0.dist-info/WHEEL +6 -0
- pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.59.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
"""Physical model data structures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import functools
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import warnings
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from dataclasses import dataclass, fields
|
|
14
|
+
from typing import Any, NoReturn, TypeVar, overload
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import numpy.typing as npt
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import scipy.interpolate
|
|
20
|
+
import xarray as xr
|
|
21
|
+
|
|
22
|
+
from pycontrails.core.fleet import Fleet
|
|
23
|
+
from pycontrails.core.flight import Flight
|
|
24
|
+
from pycontrails.core.met import MetDataArray, MetDataset, MetVariable, originates_from_ecmwf
|
|
25
|
+
from pycontrails.core.met_var import MET_VARIABLES, SpecificHumidity
|
|
26
|
+
from pycontrails.core.vector import GeoVectorDataset
|
|
27
|
+
from pycontrails.datalib.ecmwf import ECMWF_VARIABLES
|
|
28
|
+
from pycontrails.datalib.gfs import GFS_VARIABLES
|
|
29
|
+
from pycontrails.utils.json import NumpyEncoder
|
|
30
|
+
from pycontrails.utils.types import type_guard
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
#: Model input source types
|
|
35
|
+
ModelInput = MetDataset | GeoVectorDataset | Flight | Sequence[Flight] | None
|
|
36
|
+
|
|
37
|
+
#: Model output source types
|
|
38
|
+
ModelOutput = MetDataArray | MetDataset | GeoVectorDataset | Flight | list[Flight]
|
|
39
|
+
|
|
40
|
+
#: Model attribute source types
|
|
41
|
+
SourceType = MetDataset | GeoVectorDataset | Flight | Fleet
|
|
42
|
+
|
|
43
|
+
_Source = TypeVar("_Source")
|
|
44
|
+
|
|
45
|
+
# ------------
|
|
46
|
+
# Model Params
|
|
47
|
+
# ------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ModelParams:
|
|
52
|
+
"""Class for constructing model parameters.
|
|
53
|
+
|
|
54
|
+
Implementing classes must still use the ``@dataclass`` operator.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
#: Copy input ``source`` data on eval
|
|
58
|
+
copy_source: bool = True
|
|
59
|
+
|
|
60
|
+
# -----------
|
|
61
|
+
# Interpolate
|
|
62
|
+
# -----------
|
|
63
|
+
|
|
64
|
+
#: Interpolation method. Supported methods include "linear", "nearest", "slinear",
|
|
65
|
+
#: "cubic", and "quintic". See :class:`scipy.interpolate.RegularGridInterpolator`
|
|
66
|
+
#: for the description of each method. Not all methods are supported by all
|
|
67
|
+
#: met grids. For example, the "cubic" method requires at least 4 points per
|
|
68
|
+
#: dimension.
|
|
69
|
+
interpolation_method: str = "linear"
|
|
70
|
+
|
|
71
|
+
#: If True, points lying outside interpolation will raise an error
|
|
72
|
+
interpolation_bounds_error: bool = False
|
|
73
|
+
|
|
74
|
+
#: Used for outside interpolation value if :attr:`interpolation_bounds_error` is False
|
|
75
|
+
interpolation_fill_value: float = np.nan
|
|
76
|
+
|
|
77
|
+
#: Experimental. See :mod:`pycontrails.core.interpolation`.
|
|
78
|
+
interpolation_localize: bool = False
|
|
79
|
+
|
|
80
|
+
#: Experimental. See :mod:`pycontrails.core.interpolation`.
|
|
81
|
+
interpolation_use_indices: bool = False
|
|
82
|
+
|
|
83
|
+
#: Experimental. Alternative interpolation method to account for specific humidity
|
|
84
|
+
#: lapse rate bias. Must be one of ``None``, ``"cubic-spline"``, or ``"log-q-log-p"``.
|
|
85
|
+
#: If ``None``, no special interpolation is used for specific humidity.
|
|
86
|
+
#: The ``"cubic-spline"`` method applies a custom stretching of the met interpolation
|
|
87
|
+
#: table to account for the specific humidity lapse rate bias. The ``"log-q-log-p"``
|
|
88
|
+
#: method interpolates in the log of specific humidity and pressure, then converts
|
|
89
|
+
#: back to specific humidity.
|
|
90
|
+
#: Only used by models calling to :func:`interpolate_met`.
|
|
91
|
+
interpolation_q_method: str | None = None
|
|
92
|
+
|
|
93
|
+
# -----------
|
|
94
|
+
# Meteorology
|
|
95
|
+
# -----------
|
|
96
|
+
|
|
97
|
+
#: Call :meth:`_verify_met` on model instantiation.
|
|
98
|
+
verify_met: bool = True
|
|
99
|
+
|
|
100
|
+
#: Downselect input :class:`MetDataset`` to region around ``source``.
|
|
101
|
+
downselect_met: bool = True
|
|
102
|
+
|
|
103
|
+
#: Met longitude buffer for input to :meth:`Flight.downselect_met`,
|
|
104
|
+
#: in WGS84 coordinates.
|
|
105
|
+
#: Only applies when :attr:`downselect_met` is True.
|
|
106
|
+
met_longitude_buffer: tuple[float, float] = (0.0, 0.0)
|
|
107
|
+
|
|
108
|
+
#: Met latitude buffer for input to :meth:`Flight.downselect_met`,
|
|
109
|
+
#: in WGS84 coordinates.
|
|
110
|
+
#: Only applies when :attr:`downselect_met` is True.
|
|
111
|
+
met_latitude_buffer: tuple[float, float] = (0.0, 0.0)
|
|
112
|
+
|
|
113
|
+
#: Met level buffer for input to :meth:`Flight.downselect_met`,
|
|
114
|
+
#: in [:math:`hPa`].
|
|
115
|
+
#: Only applies when :attr:`downselect_met` is True.
|
|
116
|
+
met_level_buffer: tuple[float, float] = (0.0, 0.0)
|
|
117
|
+
|
|
118
|
+
#: Met time buffer for input to :meth:`Flight.downselect_met`
|
|
119
|
+
#: Only applies when :attr:`downselect_met` is True.
|
|
120
|
+
met_time_buffer: tuple[np.timedelta64, np.timedelta64] = (
|
|
121
|
+
np.timedelta64(0, "h"),
|
|
122
|
+
np.timedelta64(0, "h"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def as_dict(self) -> dict[str, Any]:
|
|
126
|
+
"""Convert object to dictionary.
|
|
127
|
+
|
|
128
|
+
We use this method instead of `dataclasses.asdict`
|
|
129
|
+
to use a shallow/unrecursive copy.
|
|
130
|
+
This will return values as Any instead of dict.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
dict[str, Any]
|
|
135
|
+
Dictionary version of self.
|
|
136
|
+
"""
|
|
137
|
+
return {(name := field.name): getattr(self, name) for field in fields(self)}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class AdvectionBuffers(ModelParams):
|
|
142
|
+
"""Override buffers in :class:`ModelParams` for advection models."""
|
|
143
|
+
|
|
144
|
+
#: Met longitude [WGS84] buffer for evolution by advection.
|
|
145
|
+
met_longitude_buffer: tuple[float, float] = (10.0, 10.0)
|
|
146
|
+
|
|
147
|
+
#: Met latitude buffer [WGS84] for evolution by advection.
|
|
148
|
+
met_latitude_buffer: tuple[float, float] = (10.0, 10.0)
|
|
149
|
+
|
|
150
|
+
#: Met level buffer [:math:`hPa`] for evolution by advection.
|
|
151
|
+
met_level_buffer: tuple[float, float] = (40.0, 40.0)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ------
|
|
155
|
+
# Models
|
|
156
|
+
# ------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Model(ABC):
|
|
160
|
+
"""Base class for physical models.
|
|
161
|
+
|
|
162
|
+
Implementing classes must implement the :meth:`eval` method
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
__slots__ = ("met", "params", "source")
|
|
166
|
+
|
|
167
|
+
#: Default model parameter dataclass
|
|
168
|
+
default_params: type[ModelParams] = ModelParams
|
|
169
|
+
|
|
170
|
+
#: Instantiated model parameters, in dictionary form
|
|
171
|
+
params: dict[str, Any]
|
|
172
|
+
|
|
173
|
+
#: Data evaluated in model
|
|
174
|
+
source: SourceType
|
|
175
|
+
|
|
176
|
+
#: Meteorology data
|
|
177
|
+
met: MetDataset | None
|
|
178
|
+
|
|
179
|
+
#: Require meteorology is not None on __init__()
|
|
180
|
+
met_required: bool = False
|
|
181
|
+
|
|
182
|
+
#: Required meteorology pressure level variables.
|
|
183
|
+
#: Each element in the list is a :class:`MetVariable` or a ``tuple[MetVariable]``.
|
|
184
|
+
#: If element is a ``tuple[MetVariable]``, the variable depends on the data source
|
|
185
|
+
#: and the tuple must include entries for a model-agnostic variable,
|
|
186
|
+
#: an ECMWF-specific variable, and a GFS-specific variable.
|
|
187
|
+
#: Only one of the three variable in the tuple is required for model evaluation.
|
|
188
|
+
met_variables: tuple[MetVariable | tuple[MetVariable, ...], ...]
|
|
189
|
+
|
|
190
|
+
#: Set of required parameters if processing already complete on ``met`` input.
|
|
191
|
+
processed_met_variables: tuple[MetVariable, ...]
|
|
192
|
+
|
|
193
|
+
#: Optional meteorology variables
|
|
194
|
+
optional_met_variables: tuple[MetVariable | tuple[MetVariable, ...], ...]
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
met: MetDataset | None = None,
|
|
199
|
+
params: ModelParams | dict[str, Any] | None = None,
|
|
200
|
+
**params_kwargs: Any,
|
|
201
|
+
) -> None:
|
|
202
|
+
# Load base params, override default and user params
|
|
203
|
+
self._load_params(params, **params_kwargs)
|
|
204
|
+
|
|
205
|
+
# Do *not* copy met on input
|
|
206
|
+
self.met = met
|
|
207
|
+
|
|
208
|
+
# require met inputs
|
|
209
|
+
if self.met_required:
|
|
210
|
+
self.require_met()
|
|
211
|
+
|
|
212
|
+
# verify met variables
|
|
213
|
+
if self.params["verify_met"]:
|
|
214
|
+
self._verify_met()
|
|
215
|
+
|
|
216
|
+
# Warn if humidity_scaling param is NOT present for ECMWF met data
|
|
217
|
+
humidity_scaling = self.params.get("humidity_scaling")
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
humidity_scaling is None
|
|
221
|
+
and self.met is not None
|
|
222
|
+
and SpecificHumidity in getattr(self, "met_variables", ())
|
|
223
|
+
and originates_from_ecmwf(self.met)
|
|
224
|
+
):
|
|
225
|
+
warnings.warn(
|
|
226
|
+
"\nMet data appears to have originated from ECMWF and no humidity "
|
|
227
|
+
"scaling is enabled. For ECMWF data, consider using one of: \n"
|
|
228
|
+
" - 'ConstantHumidityScaling'\n"
|
|
229
|
+
" - 'ExponentialBoostHumidityScaling'\n"
|
|
230
|
+
" - 'ExponentialBoostLatitudeCorrectionHumidityScaling'\n"
|
|
231
|
+
" - 'HistogramMatching'\n"
|
|
232
|
+
"For example: \n"
|
|
233
|
+
">>> from pycontrails.models.humidity_scaling import ConstantHumidityScaling\n"
|
|
234
|
+
f">>> {type(self).__name__}(met=met, ..., humidity_scaling=ConstantHumidityScaling(rhi_adj=0.99))" # noqa: E501
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Ensure humidity_scaling q_method matches parent model
|
|
238
|
+
elif humidity_scaling is not None:
|
|
239
|
+
# Some humidity scaling models use the interpolation_q_method parameter to determine
|
|
240
|
+
# which parameters to use for scaling. Ensure that both models are consistent.
|
|
241
|
+
parent_q = self.params["interpolation_q_method"]
|
|
242
|
+
if humidity_scaling.params["interpolation_q_method"] != parent_q:
|
|
243
|
+
warnings.warn(
|
|
244
|
+
f"Model {type(self).__name__} uses interpolation_q_method={parent_q} but "
|
|
245
|
+
f"humidity_scaling model {type(humidity_scaling).__name__} uses "
|
|
246
|
+
f"interpolation_q_method={humidity_scaling.params['interpolation_q_method']}. "
|
|
247
|
+
"Overriding humidity_scaling interpolation_q_method to match parent model."
|
|
248
|
+
)
|
|
249
|
+
humidity_scaling.params["interpolation_q_method"] = parent_q
|
|
250
|
+
|
|
251
|
+
def __repr__(self) -> str:
|
|
252
|
+
params = getattr(self, "params", {})
|
|
253
|
+
return f"{type(self).__name__} model\n\t{self.long_name}\n\tParams: {params}\n"
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
@abstractmethod
|
|
257
|
+
def name(self) -> str:
|
|
258
|
+
"""Get model name for use as a data key in :class:`xr.DataArray` or :class`Flight`."""
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
@abstractmethod
|
|
262
|
+
def long_name(self) -> str:
|
|
263
|
+
"""Get long name descriptor, annotated on :class:`xr.DataArray` outputs."""
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def hash(self) -> str:
|
|
267
|
+
"""Generate a unique hash for model instance.
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
str
|
|
272
|
+
Unique hash for model instance (sha1)
|
|
273
|
+
"""
|
|
274
|
+
params = json.dumps(self.params, sort_keys=True, cls=NumpyEncoder)
|
|
275
|
+
_hash = self.name + params
|
|
276
|
+
if self.met is not None:
|
|
277
|
+
_hash += self.met.hash
|
|
278
|
+
if hasattr(self, "source"):
|
|
279
|
+
_hash += self.source.hash
|
|
280
|
+
|
|
281
|
+
return hashlib.sha1(bytes(_hash, "utf-8")).hexdigest()
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def generic_met_variables(cls) -> tuple[MetVariable, ...]:
|
|
285
|
+
"""Return a model-agnostic list of required meteorology variables.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
tuple[MetVariable]
|
|
290
|
+
List of model-agnostic variants of required variables
|
|
291
|
+
"""
|
|
292
|
+
available = set(MET_VARIABLES)
|
|
293
|
+
return tuple(_find_match(required, available) for required in cls.met_variables)
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def ecmwf_met_variables(cls) -> tuple[MetVariable, ...]:
|
|
297
|
+
"""Return an ECMWF-specific list of required meteorology variables.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
tuple[MetVariable]
|
|
302
|
+
List of ECMWF-specific variants of required variables
|
|
303
|
+
"""
|
|
304
|
+
available = set(ECMWF_VARIABLES)
|
|
305
|
+
return tuple(_find_match(required, available) for required in cls.met_variables)
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def gfs_met_variables(cls) -> tuple[MetVariable, ...]:
|
|
309
|
+
"""Return a GFS-specific list of required meteorology variables.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
tuple[MetVariable]
|
|
314
|
+
List of GFS-specific variants of required variables
|
|
315
|
+
"""
|
|
316
|
+
available = set(GFS_VARIABLES)
|
|
317
|
+
return tuple(_find_match(required, available) for required in cls.met_variables)
|
|
318
|
+
|
|
319
|
+
def _verify_met(self) -> None:
|
|
320
|
+
"""Verify integrity of :attr:`met`.
|
|
321
|
+
|
|
322
|
+
This method confirms that :attr:`met` contains each variable in
|
|
323
|
+
:attr:`met_variables`. If this check fails, and :attr:`processed_met_variables`
|
|
324
|
+
is defined, confirm :attr:`met` contains each variable there.
|
|
325
|
+
|
|
326
|
+
Does not raise errors if :attr:`met` is None.
|
|
327
|
+
|
|
328
|
+
Raises
|
|
329
|
+
------
|
|
330
|
+
KeyError
|
|
331
|
+
Raises KeyError if data does not contain variables :attr:`met_variables`
|
|
332
|
+
"""
|
|
333
|
+
if self.met is None:
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
if not hasattr(self, "met_variables"):
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Try to verify met_variables
|
|
340
|
+
try:
|
|
341
|
+
self.met.ensure_vars(self.met_variables)
|
|
342
|
+
except KeyError as e1:
|
|
343
|
+
# If that fails, try to verify processed_met_variables
|
|
344
|
+
if hasattr(self, "processed_met_variables"):
|
|
345
|
+
try:
|
|
346
|
+
self.met.ensure_vars(self.processed_met_variables)
|
|
347
|
+
except KeyError as e2:
|
|
348
|
+
raise e2 from e1
|
|
349
|
+
else:
|
|
350
|
+
raise
|
|
351
|
+
|
|
352
|
+
def _load_params(
|
|
353
|
+
self, params: ModelParams | dict[str, Any] | None = None, **params_kwargs: Any
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Load parameters to model :attr:`params`.
|
|
356
|
+
|
|
357
|
+
Load order:
|
|
358
|
+
|
|
359
|
+
1. If ``params`` is a :attr:`default_params` instance, use as is. Otherwise
|
|
360
|
+
instantiate as :attr:`default_params`.
|
|
361
|
+
2. ``params`` input dict
|
|
362
|
+
3. ``params_kwargs`` override keys in params
|
|
363
|
+
|
|
364
|
+
Parameters
|
|
365
|
+
----------
|
|
366
|
+
params : dict[str, Any], optional
|
|
367
|
+
Model parameter dictionary or :attr:`default_params` instance.
|
|
368
|
+
Defaults to {}
|
|
369
|
+
**params_kwargs : Any
|
|
370
|
+
Override keys in ``params`` with keyword arguments.
|
|
371
|
+
|
|
372
|
+
Raises
|
|
373
|
+
------
|
|
374
|
+
KeyError
|
|
375
|
+
Unknown parameter passed into model
|
|
376
|
+
"""
|
|
377
|
+
if isinstance(params, self.default_params):
|
|
378
|
+
base_params = params
|
|
379
|
+
params = None
|
|
380
|
+
elif isinstance(params, ModelParams):
|
|
381
|
+
msg = f"Model parameters must be of type {self.default_params.__name__} or dict"
|
|
382
|
+
raise TypeError(msg)
|
|
383
|
+
else:
|
|
384
|
+
base_params = self.default_params()
|
|
385
|
+
|
|
386
|
+
self.params = base_params.as_dict()
|
|
387
|
+
self.update_params(params, **params_kwargs)
|
|
388
|
+
|
|
389
|
+
@abstractmethod
|
|
390
|
+
def eval(self, source: Any = None, **params: Any) -> ModelOutput:
|
|
391
|
+
"""Abstract method to handle evaluation.
|
|
392
|
+
|
|
393
|
+
Implementing classes should override call signature to overload ``source`` inputs
|
|
394
|
+
and model outputs.
|
|
395
|
+
|
|
396
|
+
Parameters
|
|
397
|
+
----------
|
|
398
|
+
source : ModelInput, optional
|
|
399
|
+
Dataset defining coordinates to evaluate model.
|
|
400
|
+
Defined by implementing class, but must be a subset of ModelInput.
|
|
401
|
+
If None, :attr:`met` is assumed to be evaluation points.
|
|
402
|
+
**params : Any
|
|
403
|
+
Overwrite model parameters before evaluation.
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
ModelOutput
|
|
408
|
+
Return type depends on implementing model
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
# ---------
|
|
412
|
+
# Utilities
|
|
413
|
+
# ---------
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def interp_kwargs(self) -> dict[str, Any]:
|
|
417
|
+
"""Shortcut to create interpolation arguments from :attr:`params`.
|
|
418
|
+
|
|
419
|
+
The output of this is useful for passing to :func:`interpolate_met`.
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
dict[str, Any]
|
|
424
|
+
Dictionary with keys
|
|
425
|
+
|
|
426
|
+
- "method"
|
|
427
|
+
- "bounds_error"
|
|
428
|
+
- "fill_value"
|
|
429
|
+
- "localize"
|
|
430
|
+
- "use_indices"
|
|
431
|
+
- "q_method"
|
|
432
|
+
|
|
433
|
+
as determined by :attr:`params`.
|
|
434
|
+
"""
|
|
435
|
+
params = self.params
|
|
436
|
+
return {
|
|
437
|
+
"method": params["interpolation_method"],
|
|
438
|
+
"bounds_error": params["interpolation_bounds_error"],
|
|
439
|
+
"fill_value": params["interpolation_fill_value"],
|
|
440
|
+
"localize": params["interpolation_localize"],
|
|
441
|
+
"use_indices": params["interpolation_use_indices"],
|
|
442
|
+
"q_method": params["interpolation_q_method"],
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
def require_met(self) -> MetDataset:
|
|
446
|
+
"""Ensure that :attr:`met` is a MetDataset.
|
|
447
|
+
|
|
448
|
+
Returns
|
|
449
|
+
-------
|
|
450
|
+
MetDataset
|
|
451
|
+
Returns reference to :attr:`met`.
|
|
452
|
+
This is helpful for type narrowing :attr:`met` when meteorology is required.
|
|
453
|
+
|
|
454
|
+
Raises
|
|
455
|
+
------
|
|
456
|
+
ValueError
|
|
457
|
+
Raises when :attr:`met` is None.
|
|
458
|
+
"""
|
|
459
|
+
return type_guard(
|
|
460
|
+
self.met,
|
|
461
|
+
MetDataset,
|
|
462
|
+
f"Meteorology is required for this model. Specify with {type(self).__name__}(met=...) ",
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def require_source_type(self, type_: type[_Source] | tuple[type[_Source], ...]) -> _Source:
|
|
466
|
+
"""Ensure that :attr:`source` is ``type_``.
|
|
467
|
+
|
|
468
|
+
Returns
|
|
469
|
+
-------
|
|
470
|
+
_Source
|
|
471
|
+
Returns reference to :attr:`source`.
|
|
472
|
+
This is helpful for type narrowing :attr:`source` to specific type(s).
|
|
473
|
+
|
|
474
|
+
Raises
|
|
475
|
+
------
|
|
476
|
+
ValueError
|
|
477
|
+
Raises when :attr:`source` is not ``_type_``.
|
|
478
|
+
"""
|
|
479
|
+
return type_guard(getattr(self, "source", None), type_, f"Source must be of type {type_}")
|
|
480
|
+
|
|
481
|
+
@overload
|
|
482
|
+
def _get_source(self, source: MetDataset | None) -> MetDataset: ...
|
|
483
|
+
|
|
484
|
+
@overload
|
|
485
|
+
def _get_source(self, source: GeoVectorDataset) -> GeoVectorDataset: ...
|
|
486
|
+
|
|
487
|
+
@overload
|
|
488
|
+
def _get_source(self, source: Sequence[Flight]) -> Fleet: ...
|
|
489
|
+
|
|
490
|
+
def _get_source(self, source: ModelInput) -> SourceType:
|
|
491
|
+
"""Construct :attr:`source` from ``source`` parameter."""
|
|
492
|
+
|
|
493
|
+
# Fallback to met coordinates if source is None
|
|
494
|
+
if source is None:
|
|
495
|
+
self.met = self.require_met()
|
|
496
|
+
|
|
497
|
+
# Return dataset with the same coords as self.met, but empty data_vars
|
|
498
|
+
return MetDataset._from_fastpath(xr.Dataset(coords=self.met.data.coords))
|
|
499
|
+
|
|
500
|
+
copy_source = self.params["copy_source"]
|
|
501
|
+
|
|
502
|
+
# Turn Sequence into Fleet
|
|
503
|
+
if isinstance(source, Sequence):
|
|
504
|
+
if not copy_source:
|
|
505
|
+
msg = "Parameter copy_source=False is not supported for Sequence[Flight] source"
|
|
506
|
+
raise ValueError(msg)
|
|
507
|
+
return Fleet.from_seq(source)
|
|
508
|
+
|
|
509
|
+
# Raise error if source is not a MetDataset or GeoVectorDataset
|
|
510
|
+
if not isinstance(source, MetDataset | GeoVectorDataset):
|
|
511
|
+
msg = f"Unknown source type: {type(source)}"
|
|
512
|
+
raise TypeError(msg)
|
|
513
|
+
|
|
514
|
+
if copy_source:
|
|
515
|
+
source = source.copy()
|
|
516
|
+
|
|
517
|
+
if not isinstance(source, Flight):
|
|
518
|
+
return source
|
|
519
|
+
|
|
520
|
+
# Ensure flight_id is present on Flight instances
|
|
521
|
+
# Either broadcast from attrs or add as 0
|
|
522
|
+
if "flight_id" not in source:
|
|
523
|
+
if "flight_id" in source.attrs:
|
|
524
|
+
source.broadcast_attrs("flight_id")
|
|
525
|
+
|
|
526
|
+
else:
|
|
527
|
+
warnings.warn(
|
|
528
|
+
"Source flight does not contain `flight_id` data or attr. "
|
|
529
|
+
"Adding `flight_id` of 0"
|
|
530
|
+
)
|
|
531
|
+
source["flight_id"] = np.zeros(len(source), dtype=int)
|
|
532
|
+
|
|
533
|
+
return source
|
|
534
|
+
|
|
535
|
+
def set_source(self, source: ModelInput = None) -> None:
|
|
536
|
+
"""Attach original or copy of input ``source`` to :attr:`source`.
|
|
537
|
+
|
|
538
|
+
Parameters
|
|
539
|
+
----------
|
|
540
|
+
source : MetDataset | GeoVectorDataset | Flight | Iterable[Flight] | None
|
|
541
|
+
Parameter ``source`` passed in :meth:`eval`.
|
|
542
|
+
If None, an empty MetDataset with coordinates like :attr:`met` is set to :attr:`source`.
|
|
543
|
+
|
|
544
|
+
See Also
|
|
545
|
+
--------
|
|
546
|
+
eval
|
|
547
|
+
"""
|
|
548
|
+
self.source = self._get_source(source)
|
|
549
|
+
|
|
550
|
+
def update_params(self, params: dict[str, Any] | None = None, **params_kwargs: Any) -> None:
|
|
551
|
+
"""Update model parameters on :attr:`params`.
|
|
552
|
+
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
params : dict[str, Any], optional
|
|
556
|
+
Model parameters to update, as dictionary.
|
|
557
|
+
Defaults to {}
|
|
558
|
+
**params_kwargs : Any
|
|
559
|
+
Override keys in ``params`` with keyword arguments.
|
|
560
|
+
"""
|
|
561
|
+
update_param_dict(self.params, params or {})
|
|
562
|
+
update_param_dict(self.params, params_kwargs)
|
|
563
|
+
|
|
564
|
+
def downselect_met(self) -> None:
|
|
565
|
+
"""Downselect :attr:`met` domain to the max/min bounds of :attr:`source`.
|
|
566
|
+
|
|
567
|
+
Override this method if special handling is needed in met down-selection.
|
|
568
|
+
|
|
569
|
+
- :attr:`source` must be defined before calling :meth:`downselect_met`.
|
|
570
|
+
- This method copies and re-assigns :attr:`met` using :meth:`met.copy()`
|
|
571
|
+
to avoid side-effects.
|
|
572
|
+
|
|
573
|
+
Raises
|
|
574
|
+
------
|
|
575
|
+
ValueError
|
|
576
|
+
Raised if :attr:`source` is not defined.
|
|
577
|
+
Raised if :attr:`source` is not a :class:`GeoVectorDataset`.
|
|
578
|
+
TypeError
|
|
579
|
+
Raised if :attr:`met` is not a :class:`MetDataset`.
|
|
580
|
+
"""
|
|
581
|
+
try:
|
|
582
|
+
source = self.source
|
|
583
|
+
except AttributeError as exc:
|
|
584
|
+
msg = "Attribute 'source' must be defined before calling 'downselect_met'."
|
|
585
|
+
raise AttributeError(msg) from exc
|
|
586
|
+
|
|
587
|
+
# TODO: This could be generalized for a MetDataset source
|
|
588
|
+
if not isinstance(source, GeoVectorDataset):
|
|
589
|
+
msg = "Attribute 'source' must be a GeoVectorDataset"
|
|
590
|
+
raise TypeError(msg)
|
|
591
|
+
|
|
592
|
+
if self.met is None:
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
# return if downselect_met is False
|
|
596
|
+
if not self.params["downselect_met"]:
|
|
597
|
+
logger.debug("Avoiding downselecting met because params['downselect_met'] is False")
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
logger.debug("Downselecting met in model %s", self.name)
|
|
601
|
+
|
|
602
|
+
# get buffers from params
|
|
603
|
+
buffers = {
|
|
604
|
+
"longitude_buffer": self.params.get("met_longitude_buffer"),
|
|
605
|
+
"latitude_buffer": self.params.get("met_latitude_buffer"),
|
|
606
|
+
"level_buffer": self.params.get("met_level_buffer"),
|
|
607
|
+
"time_buffer": self.params.get("met_time_buffer"),
|
|
608
|
+
}
|
|
609
|
+
kwargs = {k: v for k, v in buffers.items() if v is not None}
|
|
610
|
+
|
|
611
|
+
self.met = source.downselect_met(self.met, **kwargs)
|
|
612
|
+
|
|
613
|
+
def set_source_met(
|
|
614
|
+
self,
|
|
615
|
+
optional: bool = False,
|
|
616
|
+
variable: MetVariable | Sequence[MetVariable] | None = None,
|
|
617
|
+
) -> None:
|
|
618
|
+
"""Ensure or interpolate each required :attr:`met_variables` on :attr:`source` .
|
|
619
|
+
|
|
620
|
+
For each variable in :attr:`met_variables`, check :attr:`source` for data variable
|
|
621
|
+
with the same name.
|
|
622
|
+
|
|
623
|
+
For :class:`GeoVectorDataset` sources, try to interpolate :attr:`met`
|
|
624
|
+
if variable does not exist.
|
|
625
|
+
|
|
626
|
+
For :class:`MetDataset` sources, try to get data from :attr:`met`
|
|
627
|
+
if variable does not exist.
|
|
628
|
+
|
|
629
|
+
Parameters
|
|
630
|
+
----------
|
|
631
|
+
optional : bool, optional
|
|
632
|
+
Include :attr:`optional_met_variables`
|
|
633
|
+
variable : MetVariable | Sequence[MetVariable] | None, optional
|
|
634
|
+
MetVariable to set, from :attr:`met_variables`.
|
|
635
|
+
If None, set all variables in :attr:`met_variables`
|
|
636
|
+
and :attr:`optional_met_variables` if ``optional`` is True.
|
|
637
|
+
|
|
638
|
+
Raises
|
|
639
|
+
------
|
|
640
|
+
ValueError
|
|
641
|
+
Variable does not exist and :attr:`source` is a MetDataset.
|
|
642
|
+
KeyError
|
|
643
|
+
Variable not found in :attr:`source` or :attr:`met`.
|
|
644
|
+
"""
|
|
645
|
+
variables = self._determine_relevant_variables(optional, variable)
|
|
646
|
+
|
|
647
|
+
q_method = self.params["interpolation_q_method"]
|
|
648
|
+
|
|
649
|
+
for var in variables:
|
|
650
|
+
# If var is a tuple of options, check if at least one of them exists in source
|
|
651
|
+
if isinstance(var, tuple):
|
|
652
|
+
for v in var:
|
|
653
|
+
if v.standard_name in self.source:
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
# Check if var exists in source
|
|
657
|
+
elif var.standard_name in self.source:
|
|
658
|
+
continue
|
|
659
|
+
|
|
660
|
+
# Otherwise, interpolate / set from met
|
|
661
|
+
if not isinstance(self.met, MetDataset):
|
|
662
|
+
_raise_missing_met_var(var)
|
|
663
|
+
|
|
664
|
+
# take the first var name output from ensure_vars
|
|
665
|
+
met_key = self.met.ensure_vars(var)[0]
|
|
666
|
+
|
|
667
|
+
# interpolate GeoVectorDataset
|
|
668
|
+
if isinstance(self.source, GeoVectorDataset):
|
|
669
|
+
interpolate_met(self.met, self.source, met_key, **self.interp_kwargs)
|
|
670
|
+
continue
|
|
671
|
+
|
|
672
|
+
if not isinstance(self.source, MetDataset):
|
|
673
|
+
msg = f"Unknown source type: {type(self.source)}"
|
|
674
|
+
raise TypeError(msg)
|
|
675
|
+
|
|
676
|
+
da = self.met.data[met_key].reset_coords(drop=True)
|
|
677
|
+
try:
|
|
678
|
+
# This case is when self.source is a subgrid of self.met
|
|
679
|
+
# The call to .sel will raise a KeyError if this is not the case
|
|
680
|
+
|
|
681
|
+
# XXX: Sometimes this hangs when using dask!
|
|
682
|
+
# This issue is somewhat similar to
|
|
683
|
+
# https://github.com/pydata/xarray/issues/4406
|
|
684
|
+
self.source[met_key] = da.sel(self.source.coords)
|
|
685
|
+
|
|
686
|
+
except KeyError:
|
|
687
|
+
self.source[met_key] = _interp_grid_to_grid(
|
|
688
|
+
met_key, da, self.source, self.params, q_method
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
def _determine_relevant_variables(
|
|
692
|
+
self,
|
|
693
|
+
optional: bool,
|
|
694
|
+
variable: MetVariable | Sequence[MetVariable] | None,
|
|
695
|
+
) -> Sequence[MetVariable | tuple[MetVariable, ...]]:
|
|
696
|
+
"""Determine the relevant variables used in :meth:`set_source_met`."""
|
|
697
|
+
if variable is None:
|
|
698
|
+
if optional:
|
|
699
|
+
return (*self.met_variables, *self.optional_met_variables)
|
|
700
|
+
return self.met_variables
|
|
701
|
+
if isinstance(variable, MetVariable):
|
|
702
|
+
return (variable,)
|
|
703
|
+
return variable
|
|
704
|
+
|
|
705
|
+
# Following python implementation
|
|
706
|
+
# https://github.com/python/cpython/blob/618b7a8260bb40290d6551f24885931077309590/Lib/collections/__init__.py#L231
|
|
707
|
+
__marker = object()
|
|
708
|
+
|
|
709
|
+
def get_data_param(
|
|
710
|
+
self, other: SourceType, key: str, default: Any = __marker, *, set_attr: bool = True
|
|
711
|
+
) -> Any:
|
|
712
|
+
"""Get data from other source-compatible object with default set by model parameter key.
|
|
713
|
+
|
|
714
|
+
Retrieves data with the following hierarchy:
|
|
715
|
+
|
|
716
|
+
1. :attr:`other.data[key]`. Returns ``np.ndarray | xr.DataArray``.
|
|
717
|
+
2. :attr:`other.attrs[key]`
|
|
718
|
+
3. :attr:`params[key]`
|
|
719
|
+
4. ``default``
|
|
720
|
+
|
|
721
|
+
In case 3., the value of :attr:`params[key]` is attached to :attr:`other.attrs[key]`
|
|
722
|
+
unless ``set_attr`` is set to False.
|
|
723
|
+
|
|
724
|
+
Parameters
|
|
725
|
+
----------
|
|
726
|
+
key : str
|
|
727
|
+
Key to retrieve
|
|
728
|
+
default : Any, optional
|
|
729
|
+
Default value if key is not found.
|
|
730
|
+
set_attr : bool, optional
|
|
731
|
+
If True (default), set :attr:`source.attrs[key]` to :attr:`params[key]` if found.
|
|
732
|
+
This allows for better post model evaluation tracking.
|
|
733
|
+
|
|
734
|
+
Returns
|
|
735
|
+
-------
|
|
736
|
+
Any
|
|
737
|
+
Value(s) found for key in ``other`` data, ``other`` attrs, or model params
|
|
738
|
+
|
|
739
|
+
Raises
|
|
740
|
+
------
|
|
741
|
+
KeyError
|
|
742
|
+
Raises KeyError if key is not found in any location and ``default`` is not provided.
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
See Also
|
|
746
|
+
--------
|
|
747
|
+
get_source_param
|
|
748
|
+
pycontrails.core.vector.GeoVectorDataset.get_data_or_attr
|
|
749
|
+
"""
|
|
750
|
+
marker = self.__marker
|
|
751
|
+
|
|
752
|
+
out = other.data.get(key, marker)
|
|
753
|
+
if out is not marker:
|
|
754
|
+
return out
|
|
755
|
+
|
|
756
|
+
out = other.attrs.get(key, marker)
|
|
757
|
+
if out is not marker:
|
|
758
|
+
return out
|
|
759
|
+
|
|
760
|
+
out = self.params.get(key, marker)
|
|
761
|
+
if out is not marker:
|
|
762
|
+
if set_attr:
|
|
763
|
+
other.attrs[key] = out
|
|
764
|
+
|
|
765
|
+
return out
|
|
766
|
+
|
|
767
|
+
if default is not marker:
|
|
768
|
+
return default
|
|
769
|
+
|
|
770
|
+
msg = f"Key '{key}' not found in source data, attrs, or model params"
|
|
771
|
+
raise KeyError(msg)
|
|
772
|
+
|
|
773
|
+
def get_source_param(self, key: str, default: Any = __marker, *, set_attr: bool = True) -> Any:
|
|
774
|
+
"""Get source data with default set by parameter key.
|
|
775
|
+
|
|
776
|
+
Retrieves data with the following hierarchy:
|
|
777
|
+
|
|
778
|
+
1. :attr:`source.data[key]`. Returns ``np.ndarray | xr.DataArray``.
|
|
779
|
+
2. :attr:`source.attrs[key]`
|
|
780
|
+
3. :attr:`params[key]`
|
|
781
|
+
4. ``default``
|
|
782
|
+
|
|
783
|
+
In case 3., the value of :attr:`params[key]` is attached to :attr:`source.attrs[key]`
|
|
784
|
+
unless ``set_attr`` is set to False.
|
|
785
|
+
|
|
786
|
+
Parameters
|
|
787
|
+
----------
|
|
788
|
+
key : str
|
|
789
|
+
Key to retrieve
|
|
790
|
+
default : Any, optional
|
|
791
|
+
Default value if key is not found.
|
|
792
|
+
set_attr : bool, optional
|
|
793
|
+
If True (default), set :attr:`source.attrs[key]` to :attr:`params[key]` if found.
|
|
794
|
+
This allows for better post model evaluation tracking.
|
|
795
|
+
|
|
796
|
+
Returns
|
|
797
|
+
-------
|
|
798
|
+
Any
|
|
799
|
+
Value(s) found for key in source data, source attrs, or model params
|
|
800
|
+
|
|
801
|
+
Raises
|
|
802
|
+
------
|
|
803
|
+
KeyError
|
|
804
|
+
Raises KeyError if key is not found in any location and ``default`` is not provided.
|
|
805
|
+
|
|
806
|
+
See Also
|
|
807
|
+
--------
|
|
808
|
+
get_data_param
|
|
809
|
+
pycontrails.core.vector.GeoVectorDataset.get_data_or_attr
|
|
810
|
+
"""
|
|
811
|
+
return self.get_data_param(self.source, key, default, set_attr=set_attr)
|
|
812
|
+
|
|
813
|
+
def _cleanup_indices(self) -> None:
|
|
814
|
+
"""Cleanup indices artifacts if ``params["interpolation_use_indices"]`` is True."""
|
|
815
|
+
if self.params["interpolation_use_indices"] and isinstance(self.source, GeoVectorDataset):
|
|
816
|
+
self.source._invalidate_indices()
|
|
817
|
+
|
|
818
|
+
def transfer_met_source_attrs(self, source: SourceType | None = None) -> None:
|
|
819
|
+
"""Transfer met source metadata from :attr:`met` to ``source``."""
|
|
820
|
+
|
|
821
|
+
if self.met is None:
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
source = source or self.source
|
|
825
|
+
with contextlib.suppress(KeyError):
|
|
826
|
+
source.attrs["met_source_provider"] = self.met.provider_attr
|
|
827
|
+
|
|
828
|
+
with contextlib.suppress(KeyError):
|
|
829
|
+
source.attrs["met_source_dataset"] = self.met.dataset_attr
|
|
830
|
+
|
|
831
|
+
with contextlib.suppress(KeyError):
|
|
832
|
+
source.attrs["met_source_product"] = self.met.product_attr
|
|
833
|
+
|
|
834
|
+
with contextlib.suppress(KeyError):
|
|
835
|
+
source.attrs["met_source_forecast_time"] = self.met.attrs["forecast_time"]
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _interp_grid_to_grid(
|
|
839
|
+
met_key: str,
|
|
840
|
+
da: xr.DataArray,
|
|
841
|
+
source: MetDataset,
|
|
842
|
+
params: dict[str, Any],
|
|
843
|
+
q_method: str,
|
|
844
|
+
) -> xr.DataArray:
|
|
845
|
+
# This call to DataArray.interp was added in pycontrails 0.28.1
|
|
846
|
+
# For arbitrary grids, use xr.DataArray.interp
|
|
847
|
+
# Extract certain parameters to pass into interp
|
|
848
|
+
interp_kwargs = {
|
|
849
|
+
"method": params["interpolation_method"],
|
|
850
|
+
"kwargs": {
|
|
851
|
+
"bounds_error": params["interpolation_bounds_error"],
|
|
852
|
+
"fill_value": params["interpolation_fill_value"],
|
|
853
|
+
},
|
|
854
|
+
"assume_sorted": True,
|
|
855
|
+
}
|
|
856
|
+
# Correct dtype if promoted
|
|
857
|
+
# Somewhat of a pain: dask believes the dtype is float32, but
|
|
858
|
+
# when it is actually computed, it comes out as float64
|
|
859
|
+
# Call load() here to smooth over this issue
|
|
860
|
+
# https://github.com/pydata/xarray/issues/4770
|
|
861
|
+
# There is also an issue in which xarray assumes non-singleton
|
|
862
|
+
# dimensions. This causes issues when the ``da`` variable has
|
|
863
|
+
# a scalar dimension, or the ``self.source`` variable coincides
|
|
864
|
+
# with an edge of the ``da`` variable. For now, we try an additional
|
|
865
|
+
# sel over just the time dimension, which is the most common case.
|
|
866
|
+
# This stuff isn't so well unit tested in pycontrails, and the xarray
|
|
867
|
+
# and scipy interpolate conventions are always changing, so more
|
|
868
|
+
# issues may arise here in the future.
|
|
869
|
+
coords = source.coords
|
|
870
|
+
try:
|
|
871
|
+
da = da.sel(time=coords["time"])
|
|
872
|
+
except KeyError:
|
|
873
|
+
pass
|
|
874
|
+
else:
|
|
875
|
+
del coords["time"]
|
|
876
|
+
|
|
877
|
+
if q_method is None or met_key not in ("q", "specific_humidity"):
|
|
878
|
+
return da.interp(coords, **interp_kwargs).load().astype(da.dtype, copy=False)
|
|
879
|
+
|
|
880
|
+
if q_method == "cubic-spline":
|
|
881
|
+
ppoly = _load_spline()
|
|
882
|
+
|
|
883
|
+
da = da.assign_coords(level=ppoly(da["level"]))
|
|
884
|
+
level0 = coords.pop("level")
|
|
885
|
+
coords["level"] = ppoly(level0)
|
|
886
|
+
interped = da.interp(coords, **interp_kwargs).load().astype(da.dtype, copy=False)
|
|
887
|
+
return interped.assign_coords(level=level0)
|
|
888
|
+
|
|
889
|
+
msg = f"Unsupported q_method: {q_method}"
|
|
890
|
+
raise NotImplementedError(msg)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def _find_match(
|
|
894
|
+
required: MetVariable | Sequence[MetVariable], available: set[MetVariable]
|
|
895
|
+
) -> MetVariable:
|
|
896
|
+
"""Find match for required met variable in list of data-source-specific met variables.
|
|
897
|
+
|
|
898
|
+
Parameters
|
|
899
|
+
----------
|
|
900
|
+
required : MetVariable | Sequence[MetVariable]
|
|
901
|
+
Required met variable
|
|
902
|
+
|
|
903
|
+
available : Sequence[MetVariable]
|
|
904
|
+
Collection of data-source-specific met variables
|
|
905
|
+
|
|
906
|
+
Returns
|
|
907
|
+
-------
|
|
908
|
+
MetVariable
|
|
909
|
+
Match for required met variable in collection of data-source-specific met variables
|
|
910
|
+
|
|
911
|
+
Raises
|
|
912
|
+
------
|
|
913
|
+
KeyError
|
|
914
|
+
Raised if not match is found
|
|
915
|
+
"""
|
|
916
|
+
if isinstance(required, MetVariable):
|
|
917
|
+
return required
|
|
918
|
+
|
|
919
|
+
for var in required:
|
|
920
|
+
if var in available:
|
|
921
|
+
return var
|
|
922
|
+
|
|
923
|
+
required_keys = [v.standard_name for v in required]
|
|
924
|
+
available_keys = [v.standard_name for v in available]
|
|
925
|
+
msg = f"None of {required_keys} match variable in {available_keys}"
|
|
926
|
+
raise KeyError(msg)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def _raise_missing_met_var(var: MetVariable | Sequence[MetVariable]) -> NoReturn:
|
|
930
|
+
"""Raise KeyError on missing met variable.
|
|
931
|
+
|
|
932
|
+
Parameters
|
|
933
|
+
----------
|
|
934
|
+
var : MetVariable | list[MetVariable]
|
|
935
|
+
Met variable
|
|
936
|
+
|
|
937
|
+
Raises
|
|
938
|
+
------
|
|
939
|
+
KeyError
|
|
940
|
+
"""
|
|
941
|
+
if isinstance(var, MetVariable):
|
|
942
|
+
msg = (
|
|
943
|
+
f"Variable `{var.standard_name}` not found. Either pass parameter `met`"
|
|
944
|
+
f"in model constructor, or define `{var.standard_name}` data on input data."
|
|
945
|
+
)
|
|
946
|
+
raise KeyError(msg)
|
|
947
|
+
missing_keys = [v.standard_name for v in var]
|
|
948
|
+
msg = (
|
|
949
|
+
f"One of `{missing_keys}` is required. Either pass parameter `met`"
|
|
950
|
+
f"in model constructor, or define one of `{missing_keys}` data on input data."
|
|
951
|
+
)
|
|
952
|
+
raise KeyError(msg)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def interpolate_met(
|
|
956
|
+
met: MetDataset | None,
|
|
957
|
+
vector: GeoVectorDataset,
|
|
958
|
+
met_key: str,
|
|
959
|
+
vector_key: str | None = None,
|
|
960
|
+
*,
|
|
961
|
+
q_method: str | None = None,
|
|
962
|
+
**interp_kwargs: Any,
|
|
963
|
+
) -> npt.NDArray[np.floating]:
|
|
964
|
+
"""Interpolate ``vector`` against ``met`` gridded data.
|
|
965
|
+
|
|
966
|
+
If ``vector_key`` (=``met_key`` by default) already exists,
|
|
967
|
+
return values at ``vector_key``.
|
|
968
|
+
|
|
969
|
+
Mutates parameter ``vector`` in place by attaching new key
|
|
970
|
+
and returns values.
|
|
971
|
+
|
|
972
|
+
Parameters
|
|
973
|
+
----------
|
|
974
|
+
met : MetDataset | None
|
|
975
|
+
Met data to interpolate against
|
|
976
|
+
vector : GeoVectorDataset
|
|
977
|
+
Flight or GeoVectorDataset instance
|
|
978
|
+
met_key : str
|
|
979
|
+
Key of met variable in ``met``.
|
|
980
|
+
vector_key : str, optional
|
|
981
|
+
Key of variable to attach to ``vector``.
|
|
982
|
+
By default, use ``met_key``.
|
|
983
|
+
q_method : str, optional
|
|
984
|
+
Experimental method to use for interpolating specific humidity. See
|
|
985
|
+
:class:`ModelParams` for more information.
|
|
986
|
+
**interp_kwargs : Any,
|
|
987
|
+
Additional keyword only arguments passed to :meth:`GeoVectorDataset.intersect_met`.
|
|
988
|
+
For example, ``level=[...]``.
|
|
989
|
+
|
|
990
|
+
Returns
|
|
991
|
+
-------
|
|
992
|
+
npt.NDArray[np.floating]
|
|
993
|
+
Interpolated values.
|
|
994
|
+
|
|
995
|
+
Raises
|
|
996
|
+
------
|
|
997
|
+
KeyError
|
|
998
|
+
Parameter ``met_key`` not found in ``met``.
|
|
999
|
+
"""
|
|
1000
|
+
vector_key = vector_key or met_key
|
|
1001
|
+
|
|
1002
|
+
if (out := vector.get(vector_key, None)) is not None:
|
|
1003
|
+
return out
|
|
1004
|
+
|
|
1005
|
+
if met is None:
|
|
1006
|
+
msg = f"No variable key '{vector_key}' in 'vector' and 'met' is None"
|
|
1007
|
+
raise KeyError(msg)
|
|
1008
|
+
|
|
1009
|
+
if met_key in ("q", "specific_humidity") and q_method is not None:
|
|
1010
|
+
mda, log_applied = _extract_q(met, met_key, q_method)
|
|
1011
|
+
out = interpolate_gridded_specific_humidity(
|
|
1012
|
+
mda, vector, q_method, log_applied, **interp_kwargs
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
else:
|
|
1016
|
+
try:
|
|
1017
|
+
mda = met[met_key]
|
|
1018
|
+
except KeyError as exc:
|
|
1019
|
+
msg = f"No variable key '{met_key}' in 'met'."
|
|
1020
|
+
raise KeyError(msg) from exc
|
|
1021
|
+
|
|
1022
|
+
out = vector.intersect_met(mda, **interp_kwargs)
|
|
1023
|
+
|
|
1024
|
+
vector[vector_key] = out
|
|
1025
|
+
return out
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _extract_q(met: MetDataset, met_key: str, q_method: str) -> tuple[MetDataArray, bool]:
|
|
1029
|
+
"""Extract specific humidity from ``met`` :class:`MetDataset`.
|
|
1030
|
+
|
|
1031
|
+
Parameters
|
|
1032
|
+
----------
|
|
1033
|
+
met : MetDataset
|
|
1034
|
+
Met data
|
|
1035
|
+
met_key : str
|
|
1036
|
+
Key of specific humidity in ``met``. Typically either ``"q"`` or ``"specific_humidity"``.
|
|
1037
|
+
q_method : str
|
|
1038
|
+
Method to use for interpolating specific humidity.
|
|
1039
|
+
|
|
1040
|
+
Returns
|
|
1041
|
+
-------
|
|
1042
|
+
mda : MetDataArray
|
|
1043
|
+
Specific humidity data
|
|
1044
|
+
log_applied : bool
|
|
1045
|
+
Whether a log transform was applied to ``mda``.
|
|
1046
|
+
"""
|
|
1047
|
+
if q_method != "log-q-log-p":
|
|
1048
|
+
try:
|
|
1049
|
+
return met[met_key], False
|
|
1050
|
+
except KeyError as exc:
|
|
1051
|
+
msg = f"No variable key '{met_key}' in 'met'."
|
|
1052
|
+
raise KeyError(msg) from exc
|
|
1053
|
+
|
|
1054
|
+
try:
|
|
1055
|
+
return met["log_specific_humidity"], True
|
|
1056
|
+
except KeyError:
|
|
1057
|
+
warnings.warn(
|
|
1058
|
+
"No variable key 'log_specific_humidity' in 'met'. "
|
|
1059
|
+
"Falling back to 'specific_humidity'. "
|
|
1060
|
+
"Computation will be faster if 'log_specific_humidity' is provided."
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
try:
|
|
1064
|
+
return met[met_key], False
|
|
1065
|
+
except KeyError as exc:
|
|
1066
|
+
msg = f"No variable key '{met_key}' in 'met'."
|
|
1067
|
+
raise KeyError(msg) from exc
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _prepare_q(
|
|
1071
|
+
mda: MetDataArray, level: npt.NDArray[np.floating], q_method: str, log_applied: bool
|
|
1072
|
+
) -> tuple[MetDataArray, npt.NDArray[np.floating]]:
|
|
1073
|
+
"""Prepare specific humidity for interpolation with experimental ``q_method``.
|
|
1074
|
+
|
|
1075
|
+
Parameters
|
|
1076
|
+
----------
|
|
1077
|
+
mda : MetDataArray
|
|
1078
|
+
MetDataArray of specific humidity.
|
|
1079
|
+
level : npt.NDArray[np.floating]
|
|
1080
|
+
Levels to interpolate to, [:math:`hPa`].
|
|
1081
|
+
q_method : str
|
|
1082
|
+
One of ``"log-q-log-p"`` or ``"cubic-spline"``.
|
|
1083
|
+
log_applied : bool
|
|
1084
|
+
Whether a log transform was applied to ``mda``.
|
|
1085
|
+
|
|
1086
|
+
Returns
|
|
1087
|
+
-------
|
|
1088
|
+
mda : MetDataArray
|
|
1089
|
+
MetDataArray of specific humidity transformed for interpolation.
|
|
1090
|
+
level : npt.NDArray[np.floating]
|
|
1091
|
+
Transformed levels for interpolation.
|
|
1092
|
+
"""
|
|
1093
|
+
da = mda.data
|
|
1094
|
+
if not da._in_memory:
|
|
1095
|
+
# XXX: It's unclear where this should go. If we wait too long to load,
|
|
1096
|
+
# we may need to reload into memory on each call to intersect_met.
|
|
1097
|
+
# If we load here, we only load once, but we may load data that is
|
|
1098
|
+
# never used. For now, we load here.
|
|
1099
|
+
da.load()
|
|
1100
|
+
|
|
1101
|
+
if q_method == "log-q-log-p":
|
|
1102
|
+
return _prepare_q_log_q_log_p(da, level, log_applied)
|
|
1103
|
+
|
|
1104
|
+
assert not log_applied, "Log transform should not be applied for cubic spline interpolation"
|
|
1105
|
+
|
|
1106
|
+
if q_method == "cubic-spline":
|
|
1107
|
+
return _prepare_q_cubic_spline(da, level)
|
|
1108
|
+
|
|
1109
|
+
raise_invalid_q_method_error(q_method)
|
|
1110
|
+
return None
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def _prepare_q_log_q_log_p(
|
|
1114
|
+
da: xr.DataArray, level: npt.NDArray[np.floating], log_applied: bool
|
|
1115
|
+
) -> tuple[MetDataArray, npt.NDArray[np.floating]]:
|
|
1116
|
+
da = da.assign_coords(level=np.log(da["level"]))
|
|
1117
|
+
|
|
1118
|
+
if not log_applied:
|
|
1119
|
+
# ERA5 specific humidity can have negative values
|
|
1120
|
+
# These will get converted to NaNs
|
|
1121
|
+
# Ignore the xarray warning
|
|
1122
|
+
with warnings.catch_warnings():
|
|
1123
|
+
warnings.filterwarnings("ignore", message="invalid value encountered in log")
|
|
1124
|
+
da = np.log(da) # type: ignore[assignment]
|
|
1125
|
+
|
|
1126
|
+
mda = MetDataArray(da, copy=False)
|
|
1127
|
+
|
|
1128
|
+
level = np.log(level)
|
|
1129
|
+
return mda, level
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _prepare_q_cubic_spline(
|
|
1133
|
+
da: xr.DataArray, level: npt.NDArray[np.floating]
|
|
1134
|
+
) -> tuple[MetDataArray, npt.NDArray[np.floating]]:
|
|
1135
|
+
if da["level"][0] < 50.0 or da["level"][-1] > 1000.0:
|
|
1136
|
+
msg = "Cubic spline interpolation requires data to span 50-1000 hPa."
|
|
1137
|
+
raise ValueError(msg)
|
|
1138
|
+
ppoly = _load_spline()
|
|
1139
|
+
|
|
1140
|
+
da = da.assign_coords(level=ppoly(da["level"]))
|
|
1141
|
+
mda = MetDataArray(da, copy=False)
|
|
1142
|
+
|
|
1143
|
+
level = ppoly(level)
|
|
1144
|
+
|
|
1145
|
+
return mda, level
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def interpolate_gridded_specific_humidity(
|
|
1149
|
+
mda: MetDataArray,
|
|
1150
|
+
vector: GeoVectorDataset,
|
|
1151
|
+
q_method: str | None,
|
|
1152
|
+
log_applied: bool,
|
|
1153
|
+
**interp_kwargs: Any,
|
|
1154
|
+
) -> np.ndarray:
|
|
1155
|
+
"""Interpolate specific humidity against ``vector`` with experimental ``q_method``.
|
|
1156
|
+
|
|
1157
|
+
Parameters
|
|
1158
|
+
----------
|
|
1159
|
+
mda : MetDataArray
|
|
1160
|
+
MetDataArray of specific humidity.
|
|
1161
|
+
vector : GeoVectorDataset
|
|
1162
|
+
Flight or GeoVectorDataset instance
|
|
1163
|
+
q_method : {None, "cubic-spline", "log-q-log-p"}
|
|
1164
|
+
Experimental method to use for interpolating specific humidity.
|
|
1165
|
+
log_applied : bool
|
|
1166
|
+
Whether or not a log transform was applied to specific humidity.
|
|
1167
|
+
**interp_kwargs : Any,
|
|
1168
|
+
Additional keyword only arguments passed to `intersect_met`.
|
|
1169
|
+
|
|
1170
|
+
Returns
|
|
1171
|
+
-------
|
|
1172
|
+
np.ndarray
|
|
1173
|
+
Interpolated values.
|
|
1174
|
+
"""
|
|
1175
|
+
if q_method is None:
|
|
1176
|
+
return vector.intersect_met(mda, **interp_kwargs)
|
|
1177
|
+
|
|
1178
|
+
level = interp_kwargs.get("level", vector.level)
|
|
1179
|
+
mda, level = _prepare_q(mda, level, q_method, log_applied)
|
|
1180
|
+
interp_kwargs = {**interp_kwargs, "level": level}
|
|
1181
|
+
|
|
1182
|
+
out = vector.intersect_met(mda, **interp_kwargs)
|
|
1183
|
+
if q_method == "log-q-log-p":
|
|
1184
|
+
out = np.exp(out)
|
|
1185
|
+
|
|
1186
|
+
return out
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def raise_invalid_q_method_error(q_method: str) -> NoReturn:
|
|
1190
|
+
"""Raise error for invalid ``q_method``.
|
|
1191
|
+
|
|
1192
|
+
Parameters
|
|
1193
|
+
----------
|
|
1194
|
+
q_method : str
|
|
1195
|
+
``q_method`` to raise error for.
|
|
1196
|
+
|
|
1197
|
+
Raises
|
|
1198
|
+
------
|
|
1199
|
+
ValueError
|
|
1200
|
+
``q_method`` is not one of ``None``, ``"log-q-log-p"``, or ``"cubic-spline"``.
|
|
1201
|
+
"""
|
|
1202
|
+
available = None, "log-q-log-p", "cubic-spline"
|
|
1203
|
+
msg = f"Invalid 'q_method' value '{q_method}'. Must be one of {available}."
|
|
1204
|
+
raise ValueError(msg)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
@functools.cache
|
|
1208
|
+
def _load_spline() -> scipy.interpolate.PchipInterpolator:
|
|
1209
|
+
"""Load spline interpolator estimating the specific humidity vertical profile (ie, lapse rate).
|
|
1210
|
+
|
|
1211
|
+
Data computed from historic ERA5 reanalysis data for 2019.
|
|
1212
|
+
|
|
1213
|
+
The first data point ``(50.0, 1.8550577e-06)`` is added to the spline to
|
|
1214
|
+
ensure that the spline is monotonic for high altitudes. It was chosen
|
|
1215
|
+
so that the resulting spline has a continuous second derivative at 100 hPa.
|
|
1216
|
+
|
|
1217
|
+
Returns
|
|
1218
|
+
-------
|
|
1219
|
+
scipy.interpolate.PchipInterpolator
|
|
1220
|
+
Spline interpolator.
|
|
1221
|
+
"""
|
|
1222
|
+
|
|
1223
|
+
level = [
|
|
1224
|
+
50.0,
|
|
1225
|
+
100.0,
|
|
1226
|
+
125.0,
|
|
1227
|
+
150.0,
|
|
1228
|
+
175.0,
|
|
1229
|
+
200.0,
|
|
1230
|
+
225.0,
|
|
1231
|
+
250.0,
|
|
1232
|
+
300.0,
|
|
1233
|
+
350.0,
|
|
1234
|
+
400.0,
|
|
1235
|
+
450.0,
|
|
1236
|
+
500.0,
|
|
1237
|
+
550.0,
|
|
1238
|
+
600.0,
|
|
1239
|
+
650.0,
|
|
1240
|
+
700.0,
|
|
1241
|
+
750.0,
|
|
1242
|
+
775.0,
|
|
1243
|
+
800.0,
|
|
1244
|
+
825.0,
|
|
1245
|
+
850.0,
|
|
1246
|
+
875.0,
|
|
1247
|
+
900.0,
|
|
1248
|
+
925.0,
|
|
1249
|
+
950.0,
|
|
1250
|
+
975.0,
|
|
1251
|
+
1000.0,
|
|
1252
|
+
]
|
|
1253
|
+
q = [
|
|
1254
|
+
1.8550577e-06,
|
|
1255
|
+
2.6863474e-06,
|
|
1256
|
+
3.4371210e-06,
|
|
1257
|
+
5.6529648e-06,
|
|
1258
|
+
1.0849595e-05,
|
|
1259
|
+
2.0879523e-05,
|
|
1260
|
+
3.7430935e-05,
|
|
1261
|
+
6.1511033e-05,
|
|
1262
|
+
1.3460252e-04,
|
|
1263
|
+
2.4769874e-04,
|
|
1264
|
+
4.0938452e-04,
|
|
1265
|
+
6.2360929e-04,
|
|
1266
|
+
8.9822523e-04,
|
|
1267
|
+
1.2304801e-03,
|
|
1268
|
+
1.5927359e-03,
|
|
1269
|
+
2.0140875e-03,
|
|
1270
|
+
2.5222234e-03,
|
|
1271
|
+
3.1251940e-03,
|
|
1272
|
+
3.4660504e-03,
|
|
1273
|
+
3.8333545e-03,
|
|
1274
|
+
4.2424337e-03,
|
|
1275
|
+
4.7023618e-03,
|
|
1276
|
+
5.1869694e-03,
|
|
1277
|
+
5.6702676e-03,
|
|
1278
|
+
6.1630723e-03,
|
|
1279
|
+
6.6630659e-03,
|
|
1280
|
+
7.0036170e-03,
|
|
1281
|
+
7.1794386e-03,
|
|
1282
|
+
]
|
|
1283
|
+
|
|
1284
|
+
return scipy.interpolate.PchipInterpolator(level, q, extrapolate=False)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def update_param_dict(param_dict: dict[str, Any], new_params: dict[str, Any]) -> None:
|
|
1288
|
+
"""Update parameter dictionary in place.
|
|
1289
|
+
|
|
1290
|
+
Parameters
|
|
1291
|
+
----------
|
|
1292
|
+
param_dict : dict[str, Any]
|
|
1293
|
+
Active model parameter dictionary
|
|
1294
|
+
new_params : dict[str, Any]
|
|
1295
|
+
Model parameters to update, as a dictionary
|
|
1296
|
+
|
|
1297
|
+
Raises
|
|
1298
|
+
------
|
|
1299
|
+
KeyError
|
|
1300
|
+
Raises when ``new_params`` key is not found in ``param_dict``
|
|
1301
|
+
|
|
1302
|
+
"""
|
|
1303
|
+
for param, value in new_params.items():
|
|
1304
|
+
try:
|
|
1305
|
+
old_value = param_dict[param]
|
|
1306
|
+
except KeyError:
|
|
1307
|
+
msg = (
|
|
1308
|
+
f"Unknown parameter '{param}' passed into model. Possible "
|
|
1309
|
+
f"parameters include {', '.join(param_dict)}."
|
|
1310
|
+
)
|
|
1311
|
+
raise KeyError(msg) from None
|
|
1312
|
+
|
|
1313
|
+
# Convenience: convert timedelta64-like params
|
|
1314
|
+
if (
|
|
1315
|
+
isinstance(old_value, np.timedelta64)
|
|
1316
|
+
and not isinstance(value, np.timedelta64)
|
|
1317
|
+
and value is not None
|
|
1318
|
+
):
|
|
1319
|
+
value = pd.to_timedelta(value).to_numpy()
|
|
1320
|
+
|
|
1321
|
+
param_dict[param] = value
|