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.
@@ -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))
@@ -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)