grid-process 1.0.0__py3-none-any.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.
- grid_process/__init__.py +14 -0
- grid_process/aggreg.py +447 -0
- grid_process/blending.py +206 -0
- grid_process/config/__init__.py +5 -0
- grid_process/config/grid_process.yaml +20 -0
- grid_process/config/library_settings.py +68 -0
- grid_process/da_utils.py +104 -0
- grid_process/data_model.py +531 -0
- grid_process/ds_utils.py +61 -0
- grid_process/ens_stats.py +396 -0
- grid_process/grid_interp.py +2016 -0
- grid_process/grid_interp_utils.py +208 -0
- grid_process/helpers.py +174 -0
- grid_process/optimization.py +50 -0
- grid_process/sun.py +502 -0
- grid_process/time_utils.py +110 -0
- grid_process/timeseries.py +547 -0
- grid_process/units_conv.py +209 -0
- grid_process-1.0.0.dist-info/METADATA +155 -0
- grid_process-1.0.0.dist-info/RECORD +23 -0
- grid_process-1.0.0.dist-info/WHEEL +4 -0
- grid_process-1.0.0.dist-info/licenses/AUTHORS +11 -0
- grid_process-1.0.0.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Copyright (c) 2026 MeteoSwiss, contributors listed in AUTHORS
|
|
2
|
+
#
|
|
3
|
+
# Distributed under the terms of the BSD 3-Clause License.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Module with utilities for spatial interpolation of gridded datasets
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from pyproj import CRS
|
|
15
|
+
import xarray as xr
|
|
16
|
+
|
|
17
|
+
from grid_process.helpers import reproject
|
|
18
|
+
from grid_process import data_model as dm
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Y_DIM, X_DIM = dm.YX[0]
|
|
22
|
+
LAT, LON = dm.LATLON[0]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_dst_grid( # pylint: disable=too-many-locals
|
|
26
|
+
dst_grid_crs: Any,
|
|
27
|
+
bottomleft: tuple[float, float],
|
|
28
|
+
topright: tuple[float, float] | None = None,
|
|
29
|
+
nrowcol: tuple[int, int] | None = None,
|
|
30
|
+
resolution: float | None = None,
|
|
31
|
+
add_latlon: bool = False,
|
|
32
|
+
dtype: np.dtype = np.dtype("float32"),
|
|
33
|
+
) -> xr.Dataset:
|
|
34
|
+
"""
|
|
35
|
+
Create a destination grid on which data can be interpolated. The output
|
|
36
|
+
can be directly used with :func:`interp2grid`. Two out of the three optional
|
|
37
|
+
input arguments ``topright``, ``nrowcol``, ``resolution`` must be provided.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
dst_grid_crs : string, int, dict or a CRS object
|
|
42
|
+
Anything accepted by pyproj.crs.CRS.from_user_input:
|
|
43
|
+
https://pyproj4.github.io/pyproj/dev/api/crs/crs.html#pyproj.crs.CRS.from_user_input
|
|
44
|
+
(e.g. EPSG:4326)
|
|
45
|
+
bottomleft : tuple of floats
|
|
46
|
+
(y, x) coordinates of the center of the bottom left corner pixel
|
|
47
|
+
topright : tuple of floats, optional
|
|
48
|
+
(y, x) coordinates of the center of the top right corner pixel.
|
|
49
|
+
If provided with ``nrowcol``, this is the exact coordinates. If provided
|
|
50
|
+
with ``resolution`` this is a closed upper bound. The exact coordinates of
|
|
51
|
+
the top right corner pixel will then be a multiple of the resolution with
|
|
52
|
+
respect to the bottom left corner.
|
|
53
|
+
nrowcol : tuple of int, optional
|
|
54
|
+
number of rows (y) and columns (x) respectively
|
|
55
|
+
resolution : float, optional
|
|
56
|
+
resolution of the destination grid in the units of the ``grid_mapping``
|
|
57
|
+
add_latlon : bool
|
|
58
|
+
If True, add latitude and longitude 2d arrays as coordinates
|
|
59
|
+
dtype : numpy.dtype
|
|
60
|
+
The dtype in which spatial coordinates are returned
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
dst_grid : xarray.Dataset
|
|
65
|
+
dataset with spatial horizontal coordinates and an associated crs attribute
|
|
66
|
+
|
|
67
|
+
See also
|
|
68
|
+
--------
|
|
69
|
+
:func:`interp2grid`
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
start_y, start_x = bottomleft
|
|
73
|
+
|
|
74
|
+
if topright is not None and nrowcol is not None:
|
|
75
|
+
end_y, end_x = topright
|
|
76
|
+
nrows, ncols = nrowcol
|
|
77
|
+
y_coords = np.linspace(start_y, end_y, nrows, dtype=dtype)
|
|
78
|
+
x_coords = np.linspace(start_x, end_x, ncols, dtype=dtype)
|
|
79
|
+
elif topright is not None and resolution is not None:
|
|
80
|
+
end_y, end_x = topright
|
|
81
|
+
y_coords = np.linspace(start_y, end_y, num=int((end_y - start_y) / resolution) + 1, dtype=dtype)
|
|
82
|
+
x_coords = np.linspace(start_x, end_x, num=int((end_x - start_x) / resolution) + 1, dtype=dtype)
|
|
83
|
+
elif nrowcol is not None and resolution is not None:
|
|
84
|
+
nrows, ncols = nrowcol
|
|
85
|
+
end_y = start_y + (nrows - 1) * resolution
|
|
86
|
+
end_x = start_x + (ncols - 1) * resolution
|
|
87
|
+
y_coords = np.linspace(start_y, end_y, nrows, dtype=dtype)
|
|
88
|
+
x_coords = np.linspace(start_x, end_x, ncols, dtype=dtype)
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError("At least two of 'topright', 'nrowcol', 'resolution' arguments must be provided")
|
|
91
|
+
|
|
92
|
+
dst_grid = xr.Dataset(
|
|
93
|
+
coords={X_DIM: x_coords, Y_DIM: y_coords},
|
|
94
|
+
attrs={"crs": dst_grid_crs},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if add_latlon:
|
|
98
|
+
x_2d, y_2d = np.meshgrid(x_coords, y_coords)
|
|
99
|
+
dst_crs = CRS.from_user_input(dst_grid_crs)
|
|
100
|
+
longitude, latitude = reproject(x_2d, y_2d, dst_crs, "EPSG:4326")
|
|
101
|
+
dst_grid = dst_grid.assign_coords(
|
|
102
|
+
{
|
|
103
|
+
LON: ((Y_DIM, X_DIM), longitude.astype(dtype, copy=False)),
|
|
104
|
+
LAT: ((Y_DIM, X_DIM), latitude.astype(dtype, copy=False)),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return dst_grid
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_dst_grid_from_src( # pylint: disable=too-many-locals
|
|
112
|
+
src_grid: xr.Dataset,
|
|
113
|
+
dst_grid_crs: Any,
|
|
114
|
+
add_latlon: bool = False,
|
|
115
|
+
) -> xr.Dataset:
|
|
116
|
+
"""
|
|
117
|
+
Creates a destination grid on which data can be interpolated from a source grid
|
|
118
|
+
in a different Coordinate Reference System. Domain and sizes are preserved.
|
|
119
|
+
The output can be directly used with :func:`interp2grid`.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
src_grid : xarray Dataset
|
|
124
|
+
The source grid from which a new destination grid with same sizes and domain
|
|
125
|
+
is derived. Supported data models include:
|
|
126
|
+
|
|
127
|
+
- CF-convention grids (see https://cfconventions.org/)
|
|
128
|
+
- internal MCH models
|
|
129
|
+
- Unstructured grids
|
|
130
|
+
|
|
131
|
+
Typical spatial dimension and coordinate names are supported, and can be extended via environment variables:
|
|
132
|
+
|
|
133
|
+
- Y coordinate: export GRID_PROCESS__data_model__grid__y (e.g., '["y", "latitude", "lat", "dummy_y"]')
|
|
134
|
+
- X coordinate: export GRID_PROCESS__data_model__grid__x (e.g., '["x", "longitude", "lon", "dummy_x"]')
|
|
135
|
+
- Unstructured dimension: export GRID_PROCESS__data_model__grid__unstructured_dim (e.g., '["dummy_cell"]')
|
|
136
|
+
- When the spatial coordinates variables have a CF compliant `standard_name` attribute, arbitrary names can be
|
|
137
|
+
used directly.
|
|
138
|
+
|
|
139
|
+
Spatial CRS must be referenced via one of:
|
|
140
|
+
|
|
141
|
+
- Global attribute 'crs' (e.g., "EPSG:2056" or anything accepted by :meth:`pyproj.CRS.from_user_input`)
|
|
142
|
+
- Global attribute 'grid_mapping' (internal grid database convention)
|
|
143
|
+
- CF-convention grid mapping variable
|
|
144
|
+
- If coordinates are named 'latitude'/'longitude' or 'lat'/'lon', WGS84 (EPSG:4326) is assumed.
|
|
145
|
+
dst_grid_crs : string, int, dict or a CRS object
|
|
146
|
+
Anything accepted by pyproj.crs.CRS.from_user_input:
|
|
147
|
+
https://pyproj4.github.io/pyproj/dev/api/crs/crs.html#pyproj.crs.CRS.from_user_input
|
|
148
|
+
(e.g. EPSG:4326)
|
|
149
|
+
add_latlon : bool
|
|
150
|
+
If True, add latitude and longitude 2d arrays as coordinates
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
dst_grid : xarray.Dataset
|
|
155
|
+
dataset with spatial horizontal coordinates and an associated crs attribute
|
|
156
|
+
|
|
157
|
+
See also
|
|
158
|
+
--------
|
|
159
|
+
:func:`interp2grid`
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
spatial_dims = dm.get_spatial_dims(src_grid)
|
|
163
|
+
src_grid = dm.set_grid_mapping(src_grid)
|
|
164
|
+
src_y, src_x, src_crs = dm.get_coords_name_crs(src_grid, spatial_dims)
|
|
165
|
+
dst_crs = CRS.from_user_input(dst_grid_crs)
|
|
166
|
+
x_min, x_max = src_grid[src_x].values.min(), src_grid[src_x].values.max()
|
|
167
|
+
y_min, y_max = src_grid[src_y].values.min(), src_grid[src_y].values.max()
|
|
168
|
+
x_bnds = (x_min, x_min, x_max, x_max)
|
|
169
|
+
y_bnds = (y_min, y_max, y_min, y_max)
|
|
170
|
+
x_bnds, y_bnds = reproject(x_bnds, y_bnds, src_crs, dst_crs)
|
|
171
|
+
x_bnds, y_bnds = (min(x_bnds), max(x_bnds)), (min(y_bnds), max(y_bnds))
|
|
172
|
+
|
|
173
|
+
if len(spatial_dims) == 1: # need to infer expected sizes for x and y dims
|
|
174
|
+
xy_ratio = (x_bnds[1] - x_bnds[0]) / (y_bnds[1] - y_bnds[0])
|
|
175
|
+
dst_x_size = round(np.sqrt(src_grid.sizes[spatial_dims[0]] * xy_ratio))
|
|
176
|
+
dst_y_size = round(np.sqrt(src_grid.sizes[spatial_dims[0]] / xy_ratio))
|
|
177
|
+
bool_decrease = False
|
|
178
|
+
else:
|
|
179
|
+
dst_x_size, dst_y_size = (
|
|
180
|
+
src_grid.sizes[spatial_dims[1]],
|
|
181
|
+
src_grid.sizes[spatial_dims[0]],
|
|
182
|
+
)
|
|
183
|
+
bool_decrease = dm.is_decreasing(src_grid[src_y], spatial_dims[0])
|
|
184
|
+
|
|
185
|
+
src_x_dtype, src_y_dtype = src_grid[src_x].dtype, src_grid[src_y].dtype
|
|
186
|
+
dst_x_dtype = src_x_dtype if src_x_dtype == np.float64 else np.float32
|
|
187
|
+
dst_y_dtype = src_y_dtype if src_y_dtype == np.float64 else np.float32
|
|
188
|
+
dst_x_coords = np.linspace(x_bnds[0], x_bnds[1], dst_x_size, dtype=dst_x_dtype)
|
|
189
|
+
dst_y_coords = np.linspace(y_bnds[0], y_bnds[1], dst_y_size, dtype=dst_y_dtype)
|
|
190
|
+
if bool_decrease:
|
|
191
|
+
dst_y_coords = dst_y_coords[::-1]
|
|
192
|
+
|
|
193
|
+
dst_grid = xr.Dataset(
|
|
194
|
+
coords={X_DIM: dst_x_coords, Y_DIM: dst_y_coords},
|
|
195
|
+
attrs={"crs": dst_grid_crs},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if add_latlon:
|
|
199
|
+
x_2d, y_2d = np.meshgrid(dst_x_coords, dst_y_coords)
|
|
200
|
+
longitude, latitude = reproject(x_2d, y_2d, dst_crs, "EPSG:4326")
|
|
201
|
+
dst_grid = dst_grid.assign_coords(
|
|
202
|
+
{
|
|
203
|
+
LON: ((Y_DIM, X_DIM), longitude.astype(dst_x_dtype, copy=False)),
|
|
204
|
+
LAT: ((Y_DIM, X_DIM), latitude.astype(dst_y_dtype, copy=False)),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return dst_grid
|
grid_process/helpers.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright (c) 2026 MeteoSwiss, contributors listed in AUTHORS
|
|
2
|
+
#
|
|
3
|
+
# Distributed under the terms of the BSD 3-Clause License.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Module for general helper functions and decorators
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from functools import wraps
|
|
14
|
+
from typing import Any, overload
|
|
15
|
+
from collections.abc import Callable, Hashable
|
|
16
|
+
|
|
17
|
+
import xarray as xr
|
|
18
|
+
import numpy as np
|
|
19
|
+
from numpy.typing import NDArray
|
|
20
|
+
from pyproj import Transformer, CRS
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def timefn(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
26
|
+
"""
|
|
27
|
+
Generic decorator to log function processing times
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@wraps(func)
|
|
31
|
+
def measure_time(*args: Any, **kwargs: Any) -> Any:
|
|
32
|
+
t0 = time.monotonic()
|
|
33
|
+
result = func(*args, **kwargs)
|
|
34
|
+
logger.debug("@timefn: %s took %.3f sec", func.__name__, time.monotonic() - t0)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
return measure_time
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
def make_accessor(func: Callable[..., xr.Dataset], accessor_class: type) -> Callable[..., xr.Dataset]: ...
|
|
42
|
+
@overload
|
|
43
|
+
def make_accessor(func: Callable[..., xr.DataArray], accessor_class: type) -> Callable[..., xr.DataArray]: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def make_accessor(
|
|
47
|
+
func: Callable[..., xr.Dataset | xr.DataArray], accessor_class: type
|
|
48
|
+
) -> Callable[..., xr.Dataset | xr.DataArray]:
|
|
49
|
+
"""
|
|
50
|
+
Generic decorator to turn functions operating on xarray objects into
|
|
51
|
+
accessors based on a registered dataset or dataarray ``accessor_class``
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@wraps(func)
|
|
55
|
+
def method(accessor: xr.Dataset | xr.DataArray, *args: Any, **kwargs: Any) -> xr.Dataset | xr.DataArray:
|
|
56
|
+
return func(accessor._data, *args, **kwargs) # pylint: disable=protected-access # own accessor classes
|
|
57
|
+
|
|
58
|
+
setattr(accessor_class, func.__name__, method)
|
|
59
|
+
return func
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def keepattrs(func: Callable[..., xr.Dataset]) -> Callable[..., xr.Dataset]:
|
|
63
|
+
"""
|
|
64
|
+
Decorator to keep dataset and data variables attributes for functions
|
|
65
|
+
operating on xarray.Datasets
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@wraps(func)
|
|
69
|
+
def wrapper(*args: xr.Dataset, **kwargs: Any) -> xr.Dataset:
|
|
70
|
+
vars_attrs = {name: da.attrs for name, da in args[0].data_vars.items()}
|
|
71
|
+
ds_attrs = args[0].attrs
|
|
72
|
+
ds_out = func(*args, **kwargs)
|
|
73
|
+
update_vars_attrs(ds_out, vars_attrs)
|
|
74
|
+
ds_out.attrs.update(ds_attrs)
|
|
75
|
+
return ds_out
|
|
76
|
+
|
|
77
|
+
return wrapper
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def update_vars_attrs(ds: xr.Dataset, vars_attrs: dict[Hashable, dict[Hashable, Any]]) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Updates data variables attributes in a xarray.Dataset. The operation is
|
|
83
|
+
done "in-place".
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
ds : xarray.Dataset
|
|
88
|
+
can contain one or several data variables
|
|
89
|
+
vars_attrs : dict
|
|
90
|
+
contains the attributes of each data variables in the input dataset
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
for varname, attributes in vars_attrs.items():
|
|
94
|
+
ds[varname].attrs.update(attributes)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def replace_vars_attrs(ds: xr.Dataset, vars_attrs: dict[str, dict]) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Replaces data variables attributes in a xarray.Dataset. The operation is
|
|
100
|
+
done "in-place".
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
ds : xarray.Dataset
|
|
105
|
+
can contain one or several data variables
|
|
106
|
+
vars_attrs : dict
|
|
107
|
+
contains the new attributes of each data variables in the input dataset
|
|
108
|
+
to be replaced
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
for varname, attributes in vars_attrs.items():
|
|
112
|
+
ds[varname].attrs = attributes
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@overload
|
|
116
|
+
def reproject(
|
|
117
|
+
x_coords: NDArray[np.floating],
|
|
118
|
+
y_coords: NDArray[np.floating],
|
|
119
|
+
src_crs: CRS | str,
|
|
120
|
+
dst_crs: CRS | str,
|
|
121
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]: ...
|
|
122
|
+
@overload
|
|
123
|
+
def reproject(
|
|
124
|
+
x_coords: list[float], y_coords: list[float], src_crs: CRS | str, dst_crs: CRS | str
|
|
125
|
+
) -> tuple[list[float], list[float]]: ...
|
|
126
|
+
@overload
|
|
127
|
+
def reproject(
|
|
128
|
+
x_coords: tuple[float, ...],
|
|
129
|
+
y_coords: tuple[float, ...],
|
|
130
|
+
src_crs: CRS | str,
|
|
131
|
+
dst_crs: CRS | str,
|
|
132
|
+
) -> tuple[tuple[float, ...], tuple[float, ...]]: ...
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def reproject(
|
|
136
|
+
x_coords: NDArray[np.floating] | list[float] | tuple[float, ...],
|
|
137
|
+
y_coords: NDArray[np.floating] | list[float] | tuple[float, ...],
|
|
138
|
+
src_crs: CRS | str,
|
|
139
|
+
dst_crs: CRS | str,
|
|
140
|
+
) -> (
|
|
141
|
+
tuple[NDArray[np.float64], NDArray[np.float64]]
|
|
142
|
+
| tuple[list[float], list[float]]
|
|
143
|
+
| tuple[tuple[float, ...], tuple[float, ...]]
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Reprojects `x` and `y` coordinates from a source CRS to a new specified
|
|
147
|
+
one.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
x_coords : numpy.ndarray, list or tuple
|
|
152
|
+
contains `x` coordinates.
|
|
153
|
+
y_coords : numpy.ndarray, list or tuple
|
|
154
|
+
contains `y` coordinates
|
|
155
|
+
src_crs : pyproj.CRS of the source or equivalent string
|
|
156
|
+
The value can be anything accepted by
|
|
157
|
+
pyproj.CRS.from_user_input, such as an authority string
|
|
158
|
+
(e.g. "EPSG:4326") or a WKT string
|
|
159
|
+
dst_crs : pyproj.CRS of the destination or equivalent string
|
|
160
|
+
The value can be anything accepted by
|
|
161
|
+
pyproj.CRS.from_user_input, such as an authority string
|
|
162
|
+
(eg "EPSG:4326") or a WKT string
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
x_coords_new : numpy.ndarray, list or tuple of float64
|
|
167
|
+
same as input ``x_coords`` in the new CRS and cast as float64
|
|
168
|
+
y_coords_new : numpy.ndarray, list or tuple of float64
|
|
169
|
+
same as input ``y_coords`` in the new CRS and cast as float64
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
transformer = Transformer.from_crs(src_crs, dst_crs, always_xy=True)
|
|
173
|
+
|
|
174
|
+
return transformer.transform(x_coords, y_coords)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2026 MeteoSwiss, contributors listed in AUTHORS
|
|
2
|
+
#
|
|
3
|
+
# Distributed under the terms of the BSD 3-Clause License.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Module for optimization of computations
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from typing import Any, TypeVar, cast
|
|
14
|
+
from collections.abc import Callable, Hashable
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def memoize(maxsize: int = 10) -> Callable[[F], F]:
|
|
22
|
+
"""
|
|
23
|
+
LRU cache decorator implementation for any arbitrary input since the
|
|
24
|
+
caching is purely based on the optional keyword argument ``hkey``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def real_memoize(func: F) -> F:
|
|
28
|
+
cache: dict[Hashable, Any] = {}
|
|
29
|
+
hkeys = []
|
|
30
|
+
|
|
31
|
+
@wraps(func)
|
|
32
|
+
def _memoize(*args: Any, **kwargs: Any) -> Any:
|
|
33
|
+
hkey = kwargs.pop("hkey", None)
|
|
34
|
+
if hkey in cache:
|
|
35
|
+
logger.debug("@memoize: %s cached!", func.__name__)
|
|
36
|
+
return cache[hkey]
|
|
37
|
+
logger.debug("@memoize: %s computing ...", func.__name__)
|
|
38
|
+
result = func(*args, **kwargs)
|
|
39
|
+
if hkey is not None:
|
|
40
|
+
cache[hkey] = result
|
|
41
|
+
hkeys.append(hkey)
|
|
42
|
+
if len(hkeys) > maxsize:
|
|
43
|
+
cache.pop(hkeys.pop(0))
|
|
44
|
+
logger.debug("@memoize: %s cache size is %s}", func.__name__, len(cache))
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
return cast(F, _memoize)
|
|
49
|
+
|
|
50
|
+
return real_memoize
|