cavapy 2.0.4__tar.gz → 2.0.6__tar.gz
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.
- {cavapy-2.0.4 → cavapy-2.0.6}/PKG-INFO +4 -3
- {cavapy-2.0.4 → cavapy-2.0.6}/cavapy/__init__.py +10 -5
- {cavapy-2.0.4 → cavapy-2.0.6}/cavapy/cava_download.py +97 -8
- {cavapy-2.0.4 → cavapy-2.0.6}/cavapy/cavapy.py +2 -1
- {cavapy-2.0.4 → cavapy-2.0.6}/pyproject.toml +3 -3
- {cavapy-2.0.4 → cavapy-2.0.6}/LICENSE +0 -0
- {cavapy-2.0.4 → cavapy-2.0.6}/README.md +0 -0
- {cavapy-2.0.4 → cavapy-2.0.6}/cavapy/cava_bias.py +0 -0
- {cavapy-2.0.4 → cavapy-2.0.6}/cavapy/cava_config.py +0 -0
- {cavapy-2.0.4 → cavapy-2.0.6}/cavapy/cava_plot.py +0 -0
- {cavapy-2.0.4 → cavapy-2.0.6}/cavapy/cava_validation.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cavapy
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.6
|
|
4
4
|
Summary: CAVA Python package. Retrive climate data.
|
|
5
|
-
License:
|
|
5
|
+
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Author: Riccardo Soldan
|
|
8
8
|
Author-email: riccardosoldan@hotmail.it
|
|
9
9
|
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
12
13
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -25,7 +26,7 @@ Requires-Dist: seaborn
|
|
|
25
26
|
Requires-Dist: tqdm
|
|
26
27
|
Requires-Dist: xclim
|
|
27
28
|
Requires-Dist: xsdba
|
|
28
|
-
Project-URL: Repository, https://github.com/
|
|
29
|
+
Project-URL: Repository, https://github.com/un-fao/cavapy
|
|
29
30
|
Description-Content-Type: text/markdown
|
|
30
31
|
|
|
31
32
|
<p align="center">
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
"""CAVA Python package for retrieving and visualizing climate data."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
"""CAVA Python package for retrieving and visualizing climate data."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("cavapy")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0+unknown"
|
|
9
|
+
|
|
10
|
+
from .cavapy import get_climate_data
|
|
6
11
|
from .cava_plot import plot_spatial_map, plot_time_series
|
|
7
12
|
from .cava_config import (
|
|
8
13
|
VALID_DOMAINS,
|
|
@@ -26,6 +26,12 @@ from .cava_config import (
|
|
|
26
26
|
from .cava_validation import _ensure_inventory_not_empty
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
SPATIAL_COORDS = {
|
|
30
|
+
"longitude": "xlim",
|
|
31
|
+
"latitude": "ylim",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
29
35
|
@contextmanager
|
|
30
36
|
def _suppress_stderr_fd():
|
|
31
37
|
"""Temporarily redirect stderr to /dev/null (also silences C-level warnings)."""
|
|
@@ -46,6 +52,95 @@ def _suppress_stderr_fd():
|
|
|
46
52
|
os.close(saved_fd)
|
|
47
53
|
|
|
48
54
|
|
|
55
|
+
def _coordinate_slice(coord: xr.DataArray, bounds: tuple[float, float]) -> slice:
|
|
56
|
+
"""Build a label slice matching the coordinate's native order."""
|
|
57
|
+
values = np.asarray(coord.values, dtype=float)
|
|
58
|
+
if values.ndim != 1:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Coordinate '{coord.name}' must be one-dimensional for spatial subsetting"
|
|
61
|
+
)
|
|
62
|
+
if values.size == 0:
|
|
63
|
+
raise ValueError(f"Coordinate '{coord.name}' is empty")
|
|
64
|
+
|
|
65
|
+
lower, upper = min(bounds), max(bounds)
|
|
66
|
+
if values.size == 1:
|
|
67
|
+
return slice(lower, upper)
|
|
68
|
+
diffs = np.diff(values)
|
|
69
|
+
if np.all(diffs >= 0):
|
|
70
|
+
return slice(lower, upper)
|
|
71
|
+
if not np.all(diffs <= 0):
|
|
72
|
+
raise ValueError(f"Coordinate '{coord.name}' must be monotonic")
|
|
73
|
+
return slice(upper, lower)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _nearest_coordinate_value(coord: xr.DataArray, bounds: tuple[float, float]) -> float:
|
|
77
|
+
"""Return the coordinate value closest to the center of the requested bounds."""
|
|
78
|
+
values = np.asarray(coord.values, dtype=float)
|
|
79
|
+
if values.ndim != 1:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Coordinate '{coord.name}' must be one-dimensional for nearest selection"
|
|
82
|
+
)
|
|
83
|
+
if values.size == 0:
|
|
84
|
+
raise ValueError(f"Coordinate '{coord.name}' is empty")
|
|
85
|
+
|
|
86
|
+
center = (bounds[0] + bounds[1]) / 2
|
|
87
|
+
return float(values[np.abs(values - center).argmin()])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _empty_spatial_coords(data: xr.DataArray) -> list[str]:
|
|
91
|
+
"""Return spatial coordinates whose selected dimension is empty."""
|
|
92
|
+
empty = []
|
|
93
|
+
for coord_name in SPATIAL_COORDS:
|
|
94
|
+
if coord_name not in data.coords:
|
|
95
|
+
raise ValueError(f"Missing coordinate '{coord_name}' after subsetting")
|
|
96
|
+
if coord_name in data.sizes:
|
|
97
|
+
size = data.sizes[coord_name]
|
|
98
|
+
else:
|
|
99
|
+
size = data.coords[coord_name].size
|
|
100
|
+
if size == 0:
|
|
101
|
+
empty.append(coord_name)
|
|
102
|
+
return empty
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _select_spatial_subset(
|
|
106
|
+
data: xr.DataArray,
|
|
107
|
+
bbox: dict[str, tuple[float, float]],
|
|
108
|
+
log: logging.Logger,
|
|
109
|
+
) -> xr.DataArray:
|
|
110
|
+
"""Select bbox cells, falling back to nearest cells if the bbox is sub-grid."""
|
|
111
|
+
selectors = {
|
|
112
|
+
coord_name: _coordinate_slice(data[coord_name], bbox[bbox_key])
|
|
113
|
+
for coord_name, bbox_key in SPATIAL_COORDS.items()
|
|
114
|
+
}
|
|
115
|
+
subset = data.sel(selectors)
|
|
116
|
+
empty_coords = _empty_spatial_coords(subset)
|
|
117
|
+
if not empty_coords:
|
|
118
|
+
return subset
|
|
119
|
+
|
|
120
|
+
fallback_selectors = dict(selectors)
|
|
121
|
+
nearest_values = {}
|
|
122
|
+
for coord_name in empty_coords:
|
|
123
|
+
bbox_key = SPATIAL_COORDS[coord_name]
|
|
124
|
+
nearest_value = _nearest_coordinate_value(data[coord_name], bbox[bbox_key])
|
|
125
|
+
fallback_selectors[coord_name] = [nearest_value]
|
|
126
|
+
nearest_values[coord_name] = nearest_value
|
|
127
|
+
|
|
128
|
+
subset = data.sel(fallback_selectors)
|
|
129
|
+
still_empty = _empty_spatial_coords(subset)
|
|
130
|
+
if still_empty:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"Spatial subset is empty after nearest-neighbour fallback for "
|
|
133
|
+
f"{', '.join(still_empty)}. Requested bbox: {bbox}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
log.warning(
|
|
137
|
+
"Spatial bbox selected no cells for %s; using nearest coordinate value(s): %s",
|
|
138
|
+
", ".join(empty_coords),
|
|
139
|
+
nearest_values,
|
|
140
|
+
)
|
|
141
|
+
return subset
|
|
142
|
+
|
|
143
|
+
|
|
49
144
|
def process_worker(num_threads, **kwargs) -> xr.DataArray:
|
|
50
145
|
"""Run per-variable processing inside a thread pool and return the result."""
|
|
51
146
|
variable = kwargs["variable"]
|
|
@@ -341,10 +436,7 @@ def _download_data(
|
|
|
341
436
|
|
|
342
437
|
ds_var.coords["longitude"] = (ds_var.coords["longitude"] + 180) % 360 - 180
|
|
343
438
|
ds_var = ds_var.sortby(ds_var.longitude)
|
|
344
|
-
ds_cropped = ds_var
|
|
345
|
-
longitude=slice(bbox["xlim"][0], bbox["xlim"][1]),
|
|
346
|
-
latitude=slice(bbox["ylim"][1], bbox["ylim"][0]),
|
|
347
|
-
)
|
|
439
|
+
ds_cropped = _select_spatial_subset(ds_var, bbox, log)
|
|
348
440
|
|
|
349
441
|
# Unit conversion
|
|
350
442
|
if var in ["t2mx", "t2mn", "t2m"]:
|
|
@@ -388,10 +480,7 @@ def _download_data(
|
|
|
388
480
|
raise ValueError(msg)
|
|
389
481
|
|
|
390
482
|
log.info("Connection established")
|
|
391
|
-
ds_cropped = ds_var
|
|
392
|
-
longitude=slice(bbox["xlim"][0], bbox["xlim"][1]),
|
|
393
|
-
latitude=slice(bbox["ylim"][1], bbox["ylim"][0]),
|
|
394
|
-
)
|
|
483
|
+
ds_cropped = _select_spatial_subset(ds_var, bbox, log)
|
|
395
484
|
|
|
396
485
|
# Unit conversion
|
|
397
486
|
if variable in ["tas", "tasmax", "tasmin"]:
|
|
@@ -386,7 +386,8 @@ def get_climate_data(
|
|
|
386
386
|
historical (bool): Flag to indicate if processing historical data (default: False).
|
|
387
387
|
If True, historical data is provided together with projections.
|
|
388
388
|
Historical simulation runs for CORDEX-CORE initiative are provided for the 1980-2005 time period.
|
|
389
|
-
buffer (int): Buffer
|
|
389
|
+
buffer (int): Buffer in coordinate degrees added to each side of the bbox,
|
|
390
|
+
whether the bbox comes from country bounds or from xlim/ylim (default: 0).
|
|
390
391
|
xlim (tuple or None): Longitudinal bounds of the region of interest. Use only when country is None (default: None).
|
|
391
392
|
ylim (tuple or None): Latitudinal bounds of the region of interest. Use only when country is None (default: None).
|
|
392
393
|
remote (bool): Flag to work with remote data or not (default: True).
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cavapy"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.6"
|
|
4
4
|
description = "CAVA Python package. Retrive climate data."
|
|
5
5
|
authors = ["Riccardo Soldan <riccardosoldan@hotmail.it>", "Oleh Lokshyn <olokshyn@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
|
-
license = "
|
|
8
|
-
repository = "https://github.com/
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
repository = "https://github.com/un-fao/cavapy"
|
|
9
9
|
classifiers = [
|
|
10
10
|
"Programming Language :: Python :: 3",
|
|
11
11
|
"License :: OSI Approved :: MIT License",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|