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,68 @@
|
|
|
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
|
+
import os
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from pydantic_settings import (
|
|
12
|
+
BaseSettings,
|
|
13
|
+
PydanticBaseSettingsSource,
|
|
14
|
+
SettingsConfigDict,
|
|
15
|
+
YamlConfigSettingsSource,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
DataModelOptions = Annotated[list[str], Field(..., min_length=1)]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CacheSizeSettings(BaseModel):
|
|
22
|
+
nn_lookup: int
|
|
23
|
+
nn_lookup_regular: int
|
|
24
|
+
reproject_poi: int
|
|
25
|
+
reproject_grid: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GridSettings(BaseModel):
|
|
29
|
+
unstructured_dim: DataModelOptions
|
|
30
|
+
y: DataModelOptions
|
|
31
|
+
x: DataModelOptions
|
|
32
|
+
surf_alt: DataModelOptions
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PoiSettings(BaseModel):
|
|
36
|
+
dim: str
|
|
37
|
+
coord_from: str
|
|
38
|
+
alt_from: DataModelOptions
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DataModelSettings(BaseModel):
|
|
42
|
+
grid: GridSettings
|
|
43
|
+
poi: PoiSettings
|
|
44
|
+
validtime: DataModelOptions
|
|
45
|
+
leadtime: DataModelOptions
|
|
46
|
+
reftime: DataModelOptions
|
|
47
|
+
ens_dim: DataModelOptions
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LibrarySettings(BaseSettings):
|
|
51
|
+
model_config = SettingsConfigDict(
|
|
52
|
+
yaml_file=os.path.join(os.path.dirname(__file__), "grid_process.yaml"),
|
|
53
|
+
env_prefix="GRID_PROCESS__",
|
|
54
|
+
env_nested_delimiter="__",
|
|
55
|
+
)
|
|
56
|
+
cache_size: CacheSizeSettings
|
|
57
|
+
data_model: DataModelSettings
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def settings_customise_sources(
|
|
61
|
+
cls,
|
|
62
|
+
settings_cls: type[BaseSettings],
|
|
63
|
+
init_settings: PydanticBaseSettingsSource,
|
|
64
|
+
env_settings: PydanticBaseSettingsSource,
|
|
65
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
66
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
67
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
68
|
+
return (env_settings, YamlConfigSettingsSource(settings_cls))
|
grid_process/da_utils.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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 general utility functions that operate on xarray.DataArray
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
from collections.abc import Sequence, Hashable
|
|
14
|
+
from functools import partial
|
|
15
|
+
|
|
16
|
+
import xarray as xr
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from grid_process.helpers import make_accessor, update_vars_attrs
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Register xarray accessor so module functions can also be called as methods
|
|
25
|
+
# on xr.DataArray objects
|
|
26
|
+
@xr.register_dataarray_accessor("da_utils")
|
|
27
|
+
class DaUtils:
|
|
28
|
+
"""
|
|
29
|
+
Xarray accessors class for general utility functions on DataArray
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, da: xr.DataArray):
|
|
33
|
+
self._data = da
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
dataarray_accessor = partial(make_accessor, accessor_class=DaUtils)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataarray_accessor
|
|
40
|
+
def da_to_ds(da: xr.DataArray, vars_attrs: dict[Hashable, dict[Hashable, Any]]) -> xr.Dataset:
|
|
41
|
+
"""
|
|
42
|
+
Transform a dataarray, containing model data and one or several
|
|
43
|
+
variables specified by the `variable` dimension, to a dataset. The function
|
|
44
|
+
also reassign the attributes of the data variables saved in ``vars_attrs``.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
da : xarray.DataArray
|
|
49
|
+
contains one or more variables specified in the `variable` dimension
|
|
50
|
+
vars_attrs : dict
|
|
51
|
+
contains the attributes of each variables found along the `variable`
|
|
52
|
+
dimension
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
ds : xarray.Dataset
|
|
57
|
+
the dataset is composed of the data variables of the input DataArray
|
|
58
|
+
|
|
59
|
+
See also
|
|
60
|
+
--------
|
|
61
|
+
:func:`~grid_process.ds_utils.ds_to_da` which is the inverse function
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
ds = da.to_dataset("variable")
|
|
65
|
+
update_vars_attrs(ds, vars_attrs)
|
|
66
|
+
|
|
67
|
+
return ds
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataarray_accessor
|
|
71
|
+
def bin_to_class(
|
|
72
|
+
da: xr.DataArray,
|
|
73
|
+
bins: Sequence[float],
|
|
74
|
+
bins_label: Sequence[str | int],
|
|
75
|
+
da_name: str | None = None,
|
|
76
|
+
) -> xr.DataArray:
|
|
77
|
+
"""
|
|
78
|
+
Convert numerical values into classes as defined by ``bins`` and
|
|
79
|
+
``bins_label``.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
-----------
|
|
83
|
+
da : xarray.DataArray
|
|
84
|
+
array with numerical values
|
|
85
|
+
bins : sequence of floats
|
|
86
|
+
defines the boudaries for the classes
|
|
87
|
+
bins_label : sequence of str or int
|
|
88
|
+
class labels. len(``bin_labels``) = len(``bins``) + 1
|
|
89
|
+
da_name : str, optional
|
|
90
|
+
used to rename the output DataArray. By default no renaming is done.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
class_da : xarray.DataArray
|
|
95
|
+
same as input array but with data converted from numerical values to
|
|
96
|
+
classes
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
bins_label_arr = np.asarray(bins_label)
|
|
100
|
+
class_da = xr.apply_ufunc(lambda x: bins_label_arr[np.digitize(x, bins=bins, right=True)], da)
|
|
101
|
+
|
|
102
|
+
if da_name is not None:
|
|
103
|
+
class_da = class_da.rename(da_name)
|
|
104
|
+
return class_da
|
|
@@ -0,0 +1,531 @@
|
|
|
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 utility functions to deal with the numerous supported data models
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, TypeVar, Literal, overload
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from numpy.typing import NDArray
|
|
18
|
+
from pyproj import CRS
|
|
19
|
+
import geopandas as gpd
|
|
20
|
+
import xarray as xr
|
|
21
|
+
|
|
22
|
+
from grid_process import CONFIG
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
27
|
+
|
|
28
|
+
DATA_MODEL = CONFIG.data_model
|
|
29
|
+
FLAT_DIM: list[str] = DATA_MODEL.grid.unstructured_dim
|
|
30
|
+
YX: tuple[tuple[str, str], ...] = tuple(zip(DATA_MODEL.grid.y, DATA_MODEL.grid.x))
|
|
31
|
+
LATLON: tuple[tuple[str, str], ...] = tuple(yx for yx in YX if "lat" in yx[0] and "lon" in yx[1])
|
|
32
|
+
SURF_ALT: list[str] = DATA_MODEL.grid.surf_alt
|
|
33
|
+
REFTIME: list[str] = DATA_MODEL.reftime
|
|
34
|
+
VALIDTIME: list[str] = DATA_MODEL.validtime
|
|
35
|
+
LEADTIME: list[str] = DATA_MODEL.leadtime
|
|
36
|
+
ENS_DIM: list[str] = DATA_MODEL.ens_dim
|
|
37
|
+
POI_ALT_COL: list[str] = DATA_MODEL.poi.alt_from
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_t_dim(ds: xr.Dataset) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Returns name of temporal dimension according to the possible options defined
|
|
43
|
+
in the config (could be overwritten by setting appropriate env variable).
|
|
44
|
+
Temporal dimension could be expressed as valid time or lead time.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
validtime_dim = get_validtime(ds)
|
|
48
|
+
if validtime_dim is not None:
|
|
49
|
+
return validtime_dim
|
|
50
|
+
|
|
51
|
+
leadtime_dim = get_leadtime(ds)
|
|
52
|
+
if leadtime_dim is not None:
|
|
53
|
+
return leadtime_dim
|
|
54
|
+
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Temporal dimension must either be one of {VALIDTIME} (valid time) or "
|
|
57
|
+
f"{LEADTIME} (lead time) or have an adequate CF standard_name attribute"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_temporal_dims(
|
|
62
|
+
data: xr.Dataset | xr.DataArray,
|
|
63
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
64
|
+
"""
|
|
65
|
+
Returns names of temporal dimensions according to the possible options defined
|
|
66
|
+
in the config (could be overwritten by setting appropriate env variable) or
|
|
67
|
+
according to CF standard name. The returned tuple contains dimension names in
|
|
68
|
+
the following order: (reftime, leadtime, validtime).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
return _get_temporal(data, "dim")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_temporal_coords(
|
|
75
|
+
data: xr.Dataset | xr.DataArray,
|
|
76
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
77
|
+
"""
|
|
78
|
+
Returns names of temporal coordinates according to the possible options defined
|
|
79
|
+
in the config (could be overwritten by setting appropriate env variable) or
|
|
80
|
+
according to CF standard name. The returned tuple contains coordinate names in
|
|
81
|
+
the following order: (reftime, leadtime, validtime).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
return _get_temporal(data, "coord")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_temporal(
|
|
88
|
+
data: xr.Dataset | xr.DataArray, kind: Literal["dim", "coord"]
|
|
89
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
90
|
+
"""
|
|
91
|
+
Returns names of temporal dimensions or coordinates.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
reftime = get_reftime(data, kind)
|
|
95
|
+
leadtime = get_leadtime(data, kind)
|
|
96
|
+
validtime = get_validtime(data, kind)
|
|
97
|
+
|
|
98
|
+
return reftime, leadtime, validtime
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_reftime(data: xr.Dataset | xr.DataArray, kind: Literal["dim", "coord"] = "dim") -> str | None:
|
|
102
|
+
"""
|
|
103
|
+
Returns name of forecast reference time dimension or coordinate according to
|
|
104
|
+
the possible options defined in the config (could be overwritten by setting
|
|
105
|
+
appropriate env variable) or according to CF standard name.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
return _get_supported_dimcoord(data, kind, REFTIME, "forecast_reference_time")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@overload
|
|
112
|
+
def get_validtime(
|
|
113
|
+
data: xr.Dataset | xr.DataArray,
|
|
114
|
+
kind: Literal["dim", "coord"] = "dim",
|
|
115
|
+
raise_notfound: Literal[True] = True,
|
|
116
|
+
) -> str: ...
|
|
117
|
+
@overload
|
|
118
|
+
def get_validtime(
|
|
119
|
+
data: xr.Dataset | xr.DataArray,
|
|
120
|
+
kind: Literal["dim", "coord"] = "dim",
|
|
121
|
+
raise_notfound: Literal[False] = False,
|
|
122
|
+
) -> str | None: ...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_validtime(
|
|
126
|
+
data: xr.Dataset | xr.DataArray,
|
|
127
|
+
kind: Literal["dim", "coord"] = "dim",
|
|
128
|
+
raise_notfound: bool = False,
|
|
129
|
+
) -> str | None:
|
|
130
|
+
"""
|
|
131
|
+
Returns name of valid time dimension or coordinate according to the possible
|
|
132
|
+
options defined in the config (could be overwritten by setting appropriate
|
|
133
|
+
env variable) or according to CF standard name. When raise_notfound is set
|
|
134
|
+
to True, raise a ValueError if a valid time dimension/coordinate can not be
|
|
135
|
+
found.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
validtime = _get_supported_dimcoord(data, kind, VALIDTIME, "time")
|
|
139
|
+
if raise_notfound and validtime is None:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Could not find a valid time dimension. Must be one of {VALIDTIME}"
|
|
142
|
+
" or have the CF standard_name attribute 'time'"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return validtime
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_leadtime(data: xr.Dataset | xr.DataArray, kind: Literal["dim", "coord"] = "dim") -> str | None:
|
|
149
|
+
"""
|
|
150
|
+
Returns name of lead time dimension or coordinate according to the possible
|
|
151
|
+
options defined in the config (could be overwritten by setting appropriate
|
|
152
|
+
env variable) or according to CF standard name.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
return _get_supported_dimcoord(data, kind, LEADTIME, "forecast_period")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_supported_dimcoord(
|
|
159
|
+
data: xr.Dataset | xr.DataArray,
|
|
160
|
+
kind: Literal["dim", "coord"],
|
|
161
|
+
config_options: list[str],
|
|
162
|
+
cf_standard: str,
|
|
163
|
+
) -> str | None:
|
|
164
|
+
"""
|
|
165
|
+
Returns name of existing dimension or coordinate in ``data`` from the
|
|
166
|
+
``config_options`` list or with a CF conventions standard_name attribute
|
|
167
|
+
``cf_standard``.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
dim_coord: tuple[str, ...] = tuple(data.coords) if kind == "coord" else tuple(data.dims) # type: ignore[arg-type]
|
|
171
|
+
for name in config_options:
|
|
172
|
+
if name in dim_coord:
|
|
173
|
+
return name
|
|
174
|
+
|
|
175
|
+
for name in dim_coord:
|
|
176
|
+
if data[name].attrs.get("standard_name", "").lower() == cf_standard:
|
|
177
|
+
return name
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_ensemble_dim(data: xr.Dataset | xr.DataArray) -> str:
|
|
183
|
+
"""
|
|
184
|
+
Returns name of ensemble members dimension according to the possible options
|
|
185
|
+
defined in the config (could be overwritten by setting appropriate env variable)
|
|
186
|
+
or according to CF standard name.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
ens_dim = _get_supported_dimcoord(data, "dim", ENS_DIM, "realization")
|
|
190
|
+
if ens_dim is None:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"Could not find an ensemble members dimension. Must be one of "
|
|
193
|
+
f"{ENS_DIM} or have the CF standard_name attribute 'realization'"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return ens_dim
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def set_grid_mapping(ds: xr.Dataset) -> xr.Dataset:
|
|
200
|
+
"""
|
|
201
|
+
Set grid_mapping attribute according to internal conventions when the input
|
|
202
|
+
grid follow CF conventions (grid mapping encoded as a data variable). The
|
|
203
|
+
data variable encoding the grid mapping is removed.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def serializable_mapping() -> dict[str, Any]:
|
|
207
|
+
return {
|
|
208
|
+
key: float(value) if isinstance(value, np.floating) else value
|
|
209
|
+
for key, value in ds_out[mapname].attrs.items()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ds_out = ds.copy(deep=False)
|
|
213
|
+
grid_map = set()
|
|
214
|
+
for varname, da in ds_out.items():
|
|
215
|
+
if "grid_mapping" in da.attrs:
|
|
216
|
+
grid_map.add(da.attrs.pop("grid_mapping"))
|
|
217
|
+
elif "grid_mapping_name" in da.attrs:
|
|
218
|
+
grid_map.add(varname)
|
|
219
|
+
|
|
220
|
+
if len(grid_map) == 0:
|
|
221
|
+
return ds_out
|
|
222
|
+
if len(grid_map) > 1:
|
|
223
|
+
raise ValueError("Several grid mappings in the same dataset is not supported")
|
|
224
|
+
|
|
225
|
+
mapname = grid_map.pop()
|
|
226
|
+
if {"grid_mapping", "crs"}.isdisjoint(ds_out.attrs) and mapname in ds_out:
|
|
227
|
+
ds_out.attrs["grid_mapping"] = json.dumps(serializable_mapping())
|
|
228
|
+
|
|
229
|
+
return ds_out.drop_vars(mapname, errors="ignore")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_spatial_dims(grid: xr.Dataset | xr.DataArray) -> tuple[str, ...]:
|
|
233
|
+
"""
|
|
234
|
+
Returns names of spatial dimensions according to the possible options defined
|
|
235
|
+
in the config (could be overwritten by setting appropriate env variable)
|
|
236
|
+
or according to CF conventions. The returned tuple has size one in case of
|
|
237
|
+
unstructured grid otherwise contains dimension names in the order (y, x).
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
flat_dim = get_flat_dim(grid)
|
|
241
|
+
if flat_dim is not None:
|
|
242
|
+
return (flat_dim,)
|
|
243
|
+
|
|
244
|
+
spatial_dims = _get_spatial(grid, tuple(grid.dims)) # type: ignore[arg-type]
|
|
245
|
+
|
|
246
|
+
if None in spatial_dims:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"Spatial dimensions must either be one of {YX}, have CF conventions"
|
|
249
|
+
f" attributes for arbitrary names or be one of {FLAT_DIM} in case of"
|
|
250
|
+
" unstructured grid"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return spatial_dims # type: ignore[return-value]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_spatial_coords(grid: xr.Dataset | xr.DataArray) -> tuple[str, str]:
|
|
257
|
+
"""
|
|
258
|
+
Returns names of non-dimension spatial coordinates according to the
|
|
259
|
+
possible options defined in the config (could be overwritten by setting
|
|
260
|
+
appropriate env variable) or according to CF conventions. The returned
|
|
261
|
+
tuple contains coordinate names in the order (y, x).
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
notdim_coords = tuple(set(grid.coords) - set(grid.dims))
|
|
265
|
+
spatial_coords = _get_spatial(grid, notdim_coords) # type: ignore[arg-type]
|
|
266
|
+
|
|
267
|
+
if None in spatial_coords:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
"Could not find non-dimension spatial coordinates. Must be one of "
|
|
270
|
+
f"{YX} or have CF conventions attributes for arbitrary names"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return spatial_coords # type: ignore[return-value]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _get_spatial(grid: xr.Dataset | xr.DataArray, dim_or_coord: tuple[str, ...]) -> tuple[str | None, str | None]:
|
|
277
|
+
"""
|
|
278
|
+
Returns names of spatial dimensions or coordinates in ``dim_or_coord``.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
for y, x in YX:
|
|
282
|
+
if y in dim_or_coord and x in dim_or_coord:
|
|
283
|
+
return y, x
|
|
284
|
+
|
|
285
|
+
# look for coordinates with CF attributes in coords
|
|
286
|
+
spatial: list[str | None] = [None, None]
|
|
287
|
+
for name in dim_or_coord:
|
|
288
|
+
attrs = grid[name].attrs
|
|
289
|
+
if (attrs.get("axis", "").upper() == "Y") or (
|
|
290
|
+
attrs.get("standard_name", "").lower() in ("latitude", "grid_latitude", "projection_y_coordinate")
|
|
291
|
+
):
|
|
292
|
+
spatial[0] = name
|
|
293
|
+
elif (attrs.get("axis", "").upper() == "X") or (
|
|
294
|
+
attrs.get("standard_name", "").lower() in ("longitude", "grid_longitude", "projection_x_coordinate")
|
|
295
|
+
):
|
|
296
|
+
spatial[1] = name
|
|
297
|
+
|
|
298
|
+
if None not in spatial:
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
return (spatial[0], spatial[1])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def get_flat_dim(grid: xr.Dataset | xr.DataArray) -> str | None:
|
|
305
|
+
"""
|
|
306
|
+
Returns spatial dimension name for an unstructured grid according to the
|
|
307
|
+
possible options defined in the config (could be overwritten by setting
|
|
308
|
+
appropriate env variable).
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
for dim in FLAT_DIM:
|
|
312
|
+
if dim in grid.dims:
|
|
313
|
+
return dim
|
|
314
|
+
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_latlon_coords(grid: xr.Dataset | xr.DataArray, nondim: bool = True) -> tuple[str, str] | None:
|
|
319
|
+
"""
|
|
320
|
+
Returns names of latitude and longitude coordinates if they exist according
|
|
321
|
+
to the possible options defined in the config (could be overwritten by setting
|
|
322
|
+
appropriate env variable) or according to CF conventions. The returned tuple
|
|
323
|
+
contains coordinate names in the order (latitude, longitude). When ``nondim``
|
|
324
|
+
is True (default), only checks coordinates that are non-dimensional.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
coords = tuple(grid.coords)
|
|
328
|
+
if nondim:
|
|
329
|
+
coords = tuple(set(coords) - set(grid.dims))
|
|
330
|
+
return _get_latlon(grid, coords) # type: ignore[arg-type]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def get_latlon_dims(grid: xr.Dataset | xr.DataArray) -> tuple[str, str] | None:
|
|
334
|
+
"""
|
|
335
|
+
Returns names of latitude and longitude dimensions if they exist according
|
|
336
|
+
to the possible options defined in the config (could be overwritten by setting
|
|
337
|
+
appropriate env variable) or according to CF conventions. The returned tuple
|
|
338
|
+
contains coordinate names in the order (latitude, longitude).
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
return _get_latlon(grid, tuple(grid.dims)) # type: ignore[arg-type]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _get_latlon(grid: xr.Dataset | xr.DataArray, dim_or_coord: tuple[str, ...]) -> tuple[str, str] | None:
|
|
345
|
+
"""
|
|
346
|
+
Returns names of latitude and longitude dimensions or coordinates in
|
|
347
|
+
``dim_or_coord``.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
for lat, lon in LATLON:
|
|
351
|
+
if lat in dim_or_coord and lon in dim_or_coord:
|
|
352
|
+
return lat, lon
|
|
353
|
+
|
|
354
|
+
# look for coordinates with CF attributes in coords
|
|
355
|
+
latlon: list[str | None] = [None, None]
|
|
356
|
+
for name in dim_or_coord:
|
|
357
|
+
coord_attrs = grid[name].attrs
|
|
358
|
+
if coord_attrs.get("standard_name", "") == "latitude" or coord_attrs.get("long_name", "") == "latitude":
|
|
359
|
+
latlon[0] = name
|
|
360
|
+
elif coord_attrs.get("standard_name", "") == "longitude" or coord_attrs.get("long_name", "") == "longitude":
|
|
361
|
+
latlon[1] = name
|
|
362
|
+
|
|
363
|
+
if None not in latlon:
|
|
364
|
+
break
|
|
365
|
+
|
|
366
|
+
if None in latlon:
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
return (latlon[0], latlon[1]) # type: ignore[return-value]
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def get_surf_alt(grid: xr.Dataset | xr.DataArray) -> str:
|
|
373
|
+
"""
|
|
374
|
+
Returns name of the surface altitude coordinate variable according to the
|
|
375
|
+
possible options defined in the config (could be overwritten by setting
|
|
376
|
+
appropriate env variable).
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
for name in SURF_ALT:
|
|
380
|
+
if name in grid.coords:
|
|
381
|
+
return name
|
|
382
|
+
|
|
383
|
+
raise ValueError(f"Missing one of {SURF_ALT} coordinate variable")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def get_poi_alt(poi_info: gpd.GeoDataFrame) -> NDArray[np.float32]:
|
|
387
|
+
"""
|
|
388
|
+
Returns POIs altitudes from ``poi_info`` according to the column names defined
|
|
389
|
+
in the config (could be overwritten by setting appropriate env variable) or
|
|
390
|
+
from a "z" geometry. Altitude data is expected to be in meters above sea level.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
for name in POI_ALT_COL:
|
|
394
|
+
if name in poi_info.columns:
|
|
395
|
+
return poi_info[name].values.astype(np.float32)
|
|
396
|
+
|
|
397
|
+
if poi_info.iloc[0].geometry.has_z:
|
|
398
|
+
return poi_info.geometry.z.values.astype(np.float32)
|
|
399
|
+
|
|
400
|
+
raise ValueError(
|
|
401
|
+
"GeoDataFrame must have a column with one of the following names "
|
|
402
|
+
f"{POI_ALT_COL} or a z coordinate. Data is assumed to be in meters"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def get_grid_crs(grid: xr.Dataset | xr.DataArray) -> CRS:
|
|
407
|
+
"""
|
|
408
|
+
Get correct CRS according to supported conventions.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
grid : xarray Dataset or DataArray
|
|
413
|
+
contains spatial horizontal coordinates. Must have a "grid_mapping" or "crs"
|
|
414
|
+
attribute referencing the spatial dimensions unless `longitude` and
|
|
415
|
+
`latitude` (or configured equivalents) are dimensions.
|
|
416
|
+
|
|
417
|
+
Returns
|
|
418
|
+
-------
|
|
419
|
+
pyproj CRS :
|
|
420
|
+
pyproj representation of input grid coordinate reference system
|
|
421
|
+
|
|
422
|
+
See also
|
|
423
|
+
--------
|
|
424
|
+
:func:`~grid_process.helpers.set_grid_mapping`
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
if "crs" in grid.attrs:
|
|
428
|
+
return CRS.from_user_input(grid.crs)
|
|
429
|
+
if "grid_mapping" in grid.attrs:
|
|
430
|
+
return parse_grid_mapping(grid.grid_mapping)
|
|
431
|
+
if get_latlon_dims(grid) is not None:
|
|
432
|
+
return CRS.from_string("EPSG:4326")
|
|
433
|
+
|
|
434
|
+
raise ValueError("The dataset must have a grid_mapping or crs attribute or have latitude and longitude dimensions")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def parse_grid_mapping(grid_mapping: str | dict[str, Any]) -> CRS:
|
|
438
|
+
"""
|
|
439
|
+
Parse internal grid database style "grid_mapping" attribute and decode it with Pyproj.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
grid_mapping : dict or dict encapsulated in a string
|
|
444
|
+
contains CF conventions attributes parsable by pyproj.CRS.from_cf() or
|
|
445
|
+
the key "epsg_code" with value being an authority string (e.g. EPSG:4326)
|
|
446
|
+
|
|
447
|
+
Returns
|
|
448
|
+
-------
|
|
449
|
+
pyproj CRS :
|
|
450
|
+
pyproj representation of input grid_mapping
|
|
451
|
+
|
|
452
|
+
See also
|
|
453
|
+
--------
|
|
454
|
+
:func:`get_grid_crs`
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
if isinstance(grid_mapping, str):
|
|
458
|
+
grid_mapping = json.loads(grid_mapping)
|
|
459
|
+
if "epsg_code" in grid_mapping:
|
|
460
|
+
return CRS.from_string(grid_mapping["epsg_code"]) # type: ignore[index]
|
|
461
|
+
|
|
462
|
+
return CRS.from_cf(grid_mapping) # type: ignore[arg-type]
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def get_coords_name_crs(
|
|
466
|
+
grid: xr.Dataset | xr.DataArray, spatial_dims: tuple[str, ...] | None = None
|
|
467
|
+
) -> tuple[str, str, CRS]:
|
|
468
|
+
"""
|
|
469
|
+
Get correct coordinate names and CRS according to supported conventions. When
|
|
470
|
+
non-dimensional longitude and latitude coordinates arrays are present, these
|
|
471
|
+
are used instead of the native spatial dimension coordinates.
|
|
472
|
+
|
|
473
|
+
Parameters
|
|
474
|
+
----------
|
|
475
|
+
grid : xarray Dataset or DataArray
|
|
476
|
+
contains spatial horizontal coordinates. Must have a "grid_mapping" or "crs"
|
|
477
|
+
attribute referencing the spatial dimensions unless longitude and
|
|
478
|
+
latitude coordinates are present.
|
|
479
|
+
spatial_dims : tuple of string(s), optional
|
|
480
|
+
name of the ``grid`` spatial dimension(s). Has size one in case of
|
|
481
|
+
unstructured grid otherwise contains dimension names in the order (y, x).
|
|
482
|
+
If not provided, calls :func:`~grid_process.helpers.get_spatial_dims`
|
|
483
|
+
|
|
484
|
+
Returns
|
|
485
|
+
-------
|
|
486
|
+
grid_y : str
|
|
487
|
+
name of the northward coordinate. Usually 'x' or 'longitude'
|
|
488
|
+
grid_x : str
|
|
489
|
+
name of the eastward coordinate. Usually 'y' or 'latitude''
|
|
490
|
+
grid_crs : pyproj CRS
|
|
491
|
+
pyproj representation of input grid coordinate reference system
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
latlon = get_latlon_coords(grid)
|
|
495
|
+
if latlon is not None:
|
|
496
|
+
grid_y, grid_x = latlon
|
|
497
|
+
return grid_y, grid_x, CRS.from_string("EPSG:4326")
|
|
498
|
+
|
|
499
|
+
if spatial_dims is None:
|
|
500
|
+
spatial_dims = get_spatial_dims(grid)
|
|
501
|
+
|
|
502
|
+
if len(spatial_dims) == 1:
|
|
503
|
+
grid_y, grid_x = get_spatial_coords(grid)
|
|
504
|
+
return grid_y, grid_x, get_grid_crs(grid)
|
|
505
|
+
|
|
506
|
+
grid_y, grid_x = spatial_dims
|
|
507
|
+
if grid_y in grid.coords and grid_x in grid.coords:
|
|
508
|
+
return grid_y, grid_x, get_grid_crs(grid)
|
|
509
|
+
|
|
510
|
+
raise ValueError("Missing spatial coordinates")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def is_decreasing1d(coords: xr.DataArray) -> bool:
|
|
514
|
+
"""
|
|
515
|
+
Whether 1 dimensional ``coords`` are in decreasing order.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
return bool(np.ediff1d(coords.values[:2])[0] < 0)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def is_decreasing(coords: xr.DataArray, dim: str) -> bool:
|
|
522
|
+
"""
|
|
523
|
+
Checks whether ``coords`` are in decreasing order along dimension ``dim``.
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
if coords.ndim > 1:
|
|
527
|
+
other_dim = next(d for d in coords.dims if d != dim)
|
|
528
|
+
mid_index = coords.sizes[other_dim] // 2
|
|
529
|
+
return is_decreasing1d(coords.isel({other_dim: mid_index}))
|
|
530
|
+
|
|
531
|
+
return is_decreasing1d(coords)
|