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.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cavapy
3
- Version: 2.0.4
3
+ Version: 2.0.6
4
4
  Summary: CAVA Python package. Retrive climate data.
5
- License: MIT
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/Risk-Team/cavapy
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
- __version__ = "2.0.3"
4
-
5
- from .cavapy import get_climate_data
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.sel(
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.sel(
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 distance to expand the region of interest (default: 0).
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.4"
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 = "MIT"
8
- repository = "https://github.com/Risk-Team/cavapy"
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