wiend 0.1.0__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.
wiend-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 florianscheiber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
wiend-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: wiend
3
+ Version: 0.1.0
4
+ Summary: ERA5 wind download, downscaling and bias correction using Weibull parameter maps.
5
+ Author: Florian Scheiber
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/florianscheiber/wiend
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: dask>=2024.0
12
+ Requires-Dist: scipy>=1.11
13
+ Requires-Dist: xarray>=2022.0
14
+ Requires-Dist: numpy>=1.21
15
+ Requires-Dist: fsspec>=2023.0
16
+ Requires-Dist: aiohttp>=3.9
17
+ Requires-Dist: requests>=2.31
18
+ Requires-Dist: zarr>=2.10
19
+ Requires-Dist: tqdm>=4.50
20
+ Dynamic: license-file
21
+
22
+ wiend
23
+
24
+ Python library for downloading ERA5 wind speed time series from Earth Data Hub and applying Weibull-based downscaling and bias correction.
25
+
26
+ Features
27
+ Download ERA5 wind speed data from Earth Data Hub
28
+ Automatic spatial validation for Europe
29
+ Weibull-based downscaling
30
+ Weibull-based bias correction
31
+ Zarr output support
32
+ Xarray-native workflow
33
+ Installation
34
+ pip install wiend
35
+
36
+ Requirements
37
+
38
+ - Python 3.11 or newer
39
+ - EDH access token via `EDH_TOKEN` or `EDH_KEY`
40
+ - Internet access for the first Weibull data download
41
+ - The shell examples below use a POSIX-style terminal; Windows users can set environment variables with PowerShell or cmd equivalents
42
+
43
+ Quick start
44
+
45
+ 1. Install the package:
46
+ ```bash
47
+ pip install wiend
48
+ ```
49
+ 2. Set your EDH token:
50
+ ```bash
51
+ export EDH_TOKEN="your-token"
52
+ ```
53
+ 3. Run the combined download + downscale workflow:
54
+ ```python
55
+ import wiend as wd
56
+
57
+ out_path = wd.download_and_downscale(
58
+ bbox=[5, 45, 15, 55],
59
+ start="2020-01-01",
60
+ end="2020-12-31",
61
+ out="wind_downscaled.zarr",
62
+ mapping_method="qm",
63
+ spacing=0.0025,
64
+ )
65
+
66
+ print(out_path)
67
+ ```
68
+
69
+ Development note
70
+
71
+ The temporary-clone smoke test is a development-only check for package installation. End users should install `wiend` from PyPI and do not need a local clone.
72
+
73
+ Data
74
+
75
+ The package code is distributed through PyPI.
76
+
77
+ The Weibull parameter maps are distributed separately as GitHub Release assets and are downloaded automatically when required due to the file size of the corrected parameters (~950 MB).
78
+
79
+ GitHub Release tag: weibull-parameters-v1
80
+
81
+ Release assets:
82
+
83
+ - `weibull-parameters-baseline-v1.zarr.zip`
84
+ - `weibull-parameters-corrected-v1.zarr.zip`
85
+
86
+ Local cache location: ~/.wiend/
87
+
88
+ The library downloads the release assets, extracts the Zarr archives, and reuses the cached data on later runs.
89
+
90
+ Example
91
+ import wiend as wd
92
+
93
+ ds = wd.download(
94
+ bbox=[5, 45, 15, 55],
95
+ start="2020-01-01",
96
+ end="2020-12-31",
97
+ )
98
+
99
+ out_path = wd.downscale(
100
+ input_data=ds,
101
+ out="wind_downscaled.zarr",
102
+ bbox=[5, 45, 15, 55],
103
+ mapping_method="qm",
104
+ spacing=0.0025,
105
+ )
106
+
107
+ print(out_path)
108
+
109
+
110
+ Or:
111
+
112
+ import wiend as wd
113
+
114
+ out_path = wd.download_and_downscale(
115
+ bbox=[5, 45, 15, 55], # lon_min, lat_min, lon_max, lat_max
116
+ start="2020-01-01",
117
+ end="2020-12-31",
118
+ out="wind_downscaled.zarr",
119
+ mapping_method="qm",
120
+ spacing=0.0025,
121
+ )
122
+
123
+ print(out_path)
124
+
125
+ Design Decisions
126
+ Python >= 3.11
127
+ MIT License
128
+ Distribution via PyPI
129
+ Weibull maps distributed via GitHub Releases
130
+ Europe-only support
131
+ Outputs:
132
+ xarray.Dataset
133
+ Zarr
134
+ Development
135
+
136
+ The project uses:
137
+
138
+ Pixi
139
+ Ruff
140
+ pre-commit
141
+ pytest
142
+
143
+ ## Testing
144
+
145
+ Run tests with:
146
+ bash
147
+ pixi run pytest tests/ -v
148
+ All tests and linting run automatically on GitHub via CI when you push.
wiend-0.1.0/README.md ADDED
@@ -0,0 +1,127 @@
1
+ wiend
2
+
3
+ Python library for downloading ERA5 wind speed time series from Earth Data Hub and applying Weibull-based downscaling and bias correction.
4
+
5
+ Features
6
+ Download ERA5 wind speed data from Earth Data Hub
7
+ Automatic spatial validation for Europe
8
+ Weibull-based downscaling
9
+ Weibull-based bias correction
10
+ Zarr output support
11
+ Xarray-native workflow
12
+ Installation
13
+ pip install wiend
14
+
15
+ Requirements
16
+
17
+ - Python 3.11 or newer
18
+ - EDH access token via `EDH_TOKEN` or `EDH_KEY`
19
+ - Internet access for the first Weibull data download
20
+ - The shell examples below use a POSIX-style terminal; Windows users can set environment variables with PowerShell or cmd equivalents
21
+
22
+ Quick start
23
+
24
+ 1. Install the package:
25
+ ```bash
26
+ pip install wiend
27
+ ```
28
+ 2. Set your EDH token:
29
+ ```bash
30
+ export EDH_TOKEN="your-token"
31
+ ```
32
+ 3. Run the combined download + downscale workflow:
33
+ ```python
34
+ import wiend as wd
35
+
36
+ out_path = wd.download_and_downscale(
37
+ bbox=[5, 45, 15, 55],
38
+ start="2020-01-01",
39
+ end="2020-12-31",
40
+ out="wind_downscaled.zarr",
41
+ mapping_method="qm",
42
+ spacing=0.0025,
43
+ )
44
+
45
+ print(out_path)
46
+ ```
47
+
48
+ Development note
49
+
50
+ The temporary-clone smoke test is a development-only check for package installation. End users should install `wiend` from PyPI and do not need a local clone.
51
+
52
+ Data
53
+
54
+ The package code is distributed through PyPI.
55
+
56
+ The Weibull parameter maps are distributed separately as GitHub Release assets and are downloaded automatically when required due to the file size of the corrected parameters (~950 MB).
57
+
58
+ GitHub Release tag: weibull-parameters-v1
59
+
60
+ Release assets:
61
+
62
+ - `weibull-parameters-baseline-v1.zarr.zip`
63
+ - `weibull-parameters-corrected-v1.zarr.zip`
64
+
65
+ Local cache location: ~/.wiend/
66
+
67
+ The library downloads the release assets, extracts the Zarr archives, and reuses the cached data on later runs.
68
+
69
+ Example
70
+ import wiend as wd
71
+
72
+ ds = wd.download(
73
+ bbox=[5, 45, 15, 55],
74
+ start="2020-01-01",
75
+ end="2020-12-31",
76
+ )
77
+
78
+ out_path = wd.downscale(
79
+ input_data=ds,
80
+ out="wind_downscaled.zarr",
81
+ bbox=[5, 45, 15, 55],
82
+ mapping_method="qm",
83
+ spacing=0.0025,
84
+ )
85
+
86
+ print(out_path)
87
+
88
+
89
+ Or:
90
+
91
+ import wiend as wd
92
+
93
+ out_path = wd.download_and_downscale(
94
+ bbox=[5, 45, 15, 55], # lon_min, lat_min, lon_max, lat_max
95
+ start="2020-01-01",
96
+ end="2020-12-31",
97
+ out="wind_downscaled.zarr",
98
+ mapping_method="qm",
99
+ spacing=0.0025,
100
+ )
101
+
102
+ print(out_path)
103
+
104
+ Design Decisions
105
+ Python >= 3.11
106
+ MIT License
107
+ Distribution via PyPI
108
+ Weibull maps distributed via GitHub Releases
109
+ Europe-only support
110
+ Outputs:
111
+ xarray.Dataset
112
+ Zarr
113
+ Development
114
+
115
+ The project uses:
116
+
117
+ Pixi
118
+ Ruff
119
+ pre-commit
120
+ pytest
121
+
122
+ ## Testing
123
+
124
+ Run tests with:
125
+ bash
126
+ pixi run pytest tests/ -v
127
+ All tests and linting run automatically on GitHub via CI when you push.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wiend"
7
+ version = "0.1.0"
8
+ authors = [{name="Florian Scheiber"}]
9
+ description = "ERA5 wind download, downscaling and bias correction using Weibull parameter maps."
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ dependencies = [
15
+ "dask>=2024.0",
16
+ "scipy>=1.11",
17
+ "xarray>=2022.0",
18
+ "numpy>=1.21",
19
+ "fsspec>=2023.0",
20
+ "aiohttp>=3.9",
21
+ "requests>=2.31",
22
+ "zarr>=2.10",
23
+ "tqdm>=4.50",
24
+ ]
25
+ urls = { "Repository" = "https://github.com/florianscheiber/wiend" }
26
+
27
+ [tool.setuptools]
28
+ package-dir = { "" = "src" }
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.ruff]
34
+ line-length = 100
35
+
36
+ [tool.ruff.lint]
37
+ select = [
38
+ "E",
39
+ "F",
40
+ "I",
41
+ ]
wiend-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from wiend.download import download, download_and_downscale
4
+ from wiend.downscale import downscale
5
+
6
+ __all__ = [
7
+ "__version__",
8
+ "download",
9
+ "downscale",
10
+ "download_and_downscale",
11
+ ]
@@ -0,0 +1,267 @@
1
+ """Data access and cache helpers for wiend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import xarray as xr
15
+
16
+ from wiend.validation import (
17
+ check_dims_and_coords,
18
+ check_lat_lon_1d_ascending,
19
+ )
20
+
21
+ EDH_DEFAULT_PATH = "era5/reanalysis-era5-single-levels-v0.zarr"
22
+ DEFAULT_GITHUB_REPOSITORY = "florianscheiber/wiend"
23
+ DEFAULT_WEIBULL_RELEASE_TAG = "latest"
24
+
25
+
26
+ def get_cache_dir(cache_dir: str | Path | None = None) -> Path:
27
+ """Return the local cache directory, defaulting to ~/.wiend."""
28
+ if cache_dir is not None:
29
+ return Path(cache_dir).expanduser().resolve()
30
+ return (Path.home() / ".wiend").resolve()
31
+
32
+
33
+ def load_edh_key(edh_key: str | None = None) -> str:
34
+ """Load EDH token from argument or environment."""
35
+ if edh_key is not None:
36
+ return edh_key
37
+
38
+ try:
39
+ from dotenv import load_dotenv
40
+ except ImportError:
41
+ load_dotenv = None
42
+
43
+ if load_dotenv is not None:
44
+ load_dotenv()
45
+
46
+ key = os.getenv("EDH_TOKEN") or os.getenv("EDH_KEY")
47
+ if not key:
48
+ raise ValueError("No EDH token found. Pass edh_key or set EDH_TOKEN/EDH_KEY.")
49
+ return key
50
+
51
+
52
+ def build_edh_url(edh_path: str, edh_key: str) -> str:
53
+ """Build full EDH URL from logical path and token."""
54
+ if edh_path.startswith("http://") or edh_path.startswith("https://"):
55
+ return edh_path
56
+ normalized_path = edh_path.lstrip("/")
57
+ return f"https://edh:{edh_key}@api.earthdatahub.destine.eu/{normalized_path}"
58
+
59
+
60
+ def load_wind_from_zarr(path_input: str | Path) -> xr.DataArray:
61
+ """Load wind data from Zarr and validate expected structure."""
62
+ ds = xr.open_zarr(str(path_input))
63
+
64
+ if "wind" not in ds:
65
+ raise KeyError(f"Zarr missing variable 'wind'. Found: {list(ds.data_vars)}")
66
+
67
+ wind = ds["wind"].astype("float32")
68
+ required_dims = ("time", "lat", "lon")
69
+ required_coords = ("time", "lat", "lon")
70
+ check_dims_and_coords(wind, required_dims, required_coords)
71
+ check_lat_lon_1d_ascending(wind["lat"], wind["lon"])
72
+ return wind
73
+
74
+
75
+ def load_weibull_params_from_zarr(
76
+ path_input: str | Path,
77
+ *,
78
+ bbox: tuple[float, float, float, float] | None = None,
79
+ crop: bool = True,
80
+ ) -> tuple[xr.DataArray, xr.DataArray]:
81
+ """Load Weibull parameters k and a with optional bbox crop."""
82
+ ds = xr.open_zarr(str(path_input))
83
+
84
+ if crop:
85
+ if bbox is None:
86
+ raise ValueError("bbox must be provided when crop=True.")
87
+ lat_min, lat_max, lon_min, lon_max = bbox
88
+ ds = ds.sel(lat=slice(lat_min, lat_max), lon=slice(lon_min, lon_max))
89
+
90
+ if "k" not in ds or "a" not in ds:
91
+ raise KeyError(f"Params Zarr {path_input} missing 'k' or 'a'.")
92
+
93
+ k = ds["k"]
94
+ a = ds["a"]
95
+
96
+ if k.sizes["lat"] == 0 or k.sizes["lon"] == 0:
97
+ raise ValueError(f"Cropping produced an empty grid for {path_input} and bbox={bbox}.")
98
+
99
+ required_dims = ("lat", "lon")
100
+ required_coords = ("lat", "lon")
101
+ check_dims_and_coords(k, required_dims, required_coords)
102
+ check_dims_and_coords(a, required_dims, required_coords)
103
+ check_lat_lon_1d_ascending(k["lat"], k["lon"])
104
+ check_lat_lon_1d_ascending(a["lat"], a["lon"])
105
+ return k, a
106
+
107
+
108
+ def _spacing_token(spacing: float | None) -> str | None:
109
+ if spacing is None:
110
+ return None
111
+ return f"spacing-{str(spacing).replace('.', '-')}"
112
+
113
+
114
+ def _github_api_release_url(repository: str, release_tag: str) -> str:
115
+ owner, repo = repository.split("/", 1)
116
+ owner_q = urllib.parse.quote(owner, safe="")
117
+ repo_q = urllib.parse.quote(repo, safe="")
118
+ if release_tag == "latest":
119
+ return f"https://api.github.com/repos/{owner_q}/{repo_q}/releases/latest"
120
+ tag_q = urllib.parse.quote(release_tag, safe="")
121
+ return f"https://api.github.com/repos/{owner_q}/{repo_q}/releases/tags/{tag_q}"
122
+
123
+
124
+ def _github_release_payload(
125
+ repository: str,
126
+ release_tag: str,
127
+ github_token: str | None = None,
128
+ ) -> dict[str, Any]:
129
+ url = _github_api_release_url(repository, release_tag)
130
+ headers = {"Accept": "application/vnd.github+json"}
131
+ if github_token:
132
+ headers["Authorization"] = f"Bearer {github_token}"
133
+ req = urllib.request.Request(url, headers=headers)
134
+ try:
135
+ with urllib.request.urlopen(req) as response:
136
+ body = response.read().decode("utf-8")
137
+ except urllib.error.HTTPError as exc:
138
+ raise RuntimeError(f"Failed to fetch GitHub release metadata from {url}: {exc}") from exc
139
+ return json.loads(body)
140
+
141
+
142
+ def _select_asset(
143
+ assets: list[dict[str, Any]],
144
+ *,
145
+ role_key: str,
146
+ spacing: float | None,
147
+ ) -> dict[str, Any]:
148
+ matches: list[dict[str, Any]] = []
149
+ for asset in assets:
150
+ name = str(asset.get("name", "")).lower()
151
+ if role_key not in name:
152
+ continue
153
+ matches.append(asset)
154
+
155
+ if not matches:
156
+ raise RuntimeError(f"No release asset found for role={role_key!r}.")
157
+ if len(matches) > 1:
158
+ names = ", ".join(str(a.get("name", "<unknown>")) for a in matches)
159
+ raise RuntimeError(f"Multiple release assets matched role={role_key!r}: {names}")
160
+ return matches[0]
161
+
162
+
163
+ def _download_file(url: str, target: Path, github_token: str | None = None) -> None:
164
+ target.parent.mkdir(parents=True, exist_ok=True)
165
+ headers: dict[str, str] = {}
166
+ if github_token:
167
+ headers["Authorization"] = f"Bearer {github_token}"
168
+
169
+ req = urllib.request.Request(url, headers=headers)
170
+ try:
171
+ with urllib.request.urlopen(req) as response, target.open("wb") as file_obj:
172
+ shutil.copyfileobj(response, file_obj)
173
+ except urllib.error.HTTPError as exc:
174
+ raise RuntimeError(f"Failed to download asset from {url}: {exc}") from exc
175
+
176
+
177
+ def _materialize_release_asset(
178
+ asset: dict[str, Any],
179
+ *,
180
+ cache_dir: Path,
181
+ github_token: str | None = None,
182
+ ) -> Path:
183
+ name = str(asset["name"])
184
+ asset_dir = cache_dir / "assets"
185
+ archive_path = asset_dir / name
186
+ extract_dir = cache_dir / "datasets" / Path(name).stem
187
+
188
+ if extract_dir.exists():
189
+ return _resolve_dataset_path(extract_dir)
190
+
191
+ if not archive_path.exists():
192
+ download_url = str(asset["browser_download_url"])
193
+ _download_file(download_url, archive_path, github_token=github_token)
194
+
195
+ suffix = archive_path.suffix.lower()
196
+ if suffix == ".zip":
197
+ shutil.unpack_archive(str(archive_path), str(extract_dir))
198
+ return _resolve_dataset_path(extract_dir)
199
+
200
+ # Fallback: non-archive asset, expose directly.
201
+ return archive_path
202
+
203
+
204
+ def _resolve_dataset_path(extract_dir: Path) -> Path:
205
+ """
206
+ Resolve the actual dataset root from an extracted release asset directory.
207
+ """
208
+ zarr_dirs = list(extract_dir.rglob("*.zarr"))
209
+ if len(zarr_dirs) == 1:
210
+ return zarr_dirs[0]
211
+ if len(zarr_dirs) > 1:
212
+ raise RuntimeError(
213
+ f"Ambiguous extracted asset {extract_dir}: multiple .zarr directories found."
214
+ )
215
+
216
+ children = [p for p in extract_dir.iterdir() if p.name != "__MACOSX"]
217
+ if len(children) == 1:
218
+ return children[0]
219
+ return extract_dir
220
+
221
+
222
+ def resolve_weibull_reference_paths(
223
+ *,
224
+ spacing: float | None,
225
+ cache_dir: str | Path | None = None,
226
+ github_repository: str = DEFAULT_GITHUB_REPOSITORY,
227
+ release_tag: str = DEFAULT_WEIBULL_RELEASE_TAG,
228
+ github_token: str | None = None,
229
+ ) -> tuple[Path, Path]:
230
+ """
231
+ Resolve baseline/manipulator Weibull parameter paths from GitHub release assets.
232
+ """
233
+ cache_path = get_cache_dir(cache_dir)
234
+ cache_path.mkdir(parents=True, exist_ok=True)
235
+
236
+ payload = _github_release_payload(
237
+ github_repository,
238
+ release_tag,
239
+ github_token=github_token,
240
+ )
241
+ assets = payload.get("assets")
242
+ if not isinstance(assets, list):
243
+ raise RuntimeError("GitHub release payload does not include an assets list.")
244
+
245
+ baseline_asset = _select_asset(
246
+ assets,
247
+ role_key="baseline",
248
+ spacing=spacing,
249
+ )
250
+ manipulator_asset = _select_asset(
251
+ assets,
252
+ role_key="corrected",
253
+ spacing=spacing,
254
+ )
255
+
256
+ baseline_path = _materialize_release_asset(
257
+ baseline_asset,
258
+ cache_dir=cache_path,
259
+ github_token=github_token,
260
+ )
261
+ manipulator_path = _materialize_release_asset(
262
+ manipulator_asset,
263
+ cache_dir=cache_path,
264
+ github_token=github_token,
265
+ )
266
+
267
+ return baseline_path, manipulator_path