wiend 0.1.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.
- wiend/__init__.py +11 -0
- wiend/data_manager.py +267 -0
- wiend/download.py +242 -0
- wiend/downscale.py +515 -0
- wiend/utils.py +562 -0
- wiend/validation.py +92 -0
- wiend-0.1.0.dist-info/METADATA +148 -0
- wiend-0.1.0.dist-info/RECORD +11 -0
- wiend-0.1.0.dist-info/WHEEL +5 -0
- wiend-0.1.0.dist-info/licenses/LICENSE +21 -0
- wiend-0.1.0.dist-info/top_level.txt +1 -0
wiend/__init__.py
ADDED
wiend/data_manager.py
ADDED
|
@@ -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
|
wiend/download.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Download API for wiend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import xarray as xr
|
|
10
|
+
|
|
11
|
+
from wiend.data_manager import (
|
|
12
|
+
DEFAULT_GITHUB_REPOSITORY,
|
|
13
|
+
DEFAULT_WEIBULL_RELEASE_TAG,
|
|
14
|
+
EDH_DEFAULT_PATH,
|
|
15
|
+
build_edh_url,
|
|
16
|
+
load_edh_key,
|
|
17
|
+
)
|
|
18
|
+
from wiend.downscale import downscale
|
|
19
|
+
from wiend.validation import normalize_wind_da, parse_bbox
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_wind_da(
|
|
23
|
+
*,
|
|
24
|
+
bbox: tuple[float, float, float, float],
|
|
25
|
+
start: str | None = None,
|
|
26
|
+
end: str | None = None,
|
|
27
|
+
edh_key: str | None = None,
|
|
28
|
+
edh_path: str | None = None,
|
|
29
|
+
height: str = "10m",
|
|
30
|
+
u_var: str | None = None,
|
|
31
|
+
v_var: str | None = None,
|
|
32
|
+
bbox_order: str = "lonlat",
|
|
33
|
+
time_dim: str = "time",
|
|
34
|
+
chunks: dict[str, int] | None = None,
|
|
35
|
+
) -> xr.DataArray:
|
|
36
|
+
key = load_edh_key(edh_key)
|
|
37
|
+
path = edh_path or EDH_DEFAULT_PATH
|
|
38
|
+
url = build_edh_url(path, key)
|
|
39
|
+
|
|
40
|
+
ds_src = xr.open_dataset(
|
|
41
|
+
url,
|
|
42
|
+
engine="zarr",
|
|
43
|
+
chunks=chunks or {},
|
|
44
|
+
decode_coords="all",
|
|
45
|
+
mask_and_scale=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if u_var is None:
|
|
49
|
+
preferred_u = ["u10", "u10m", "u"] if height == "10m" else ["u100", "u100m", "u"]
|
|
50
|
+
for name in preferred_u:
|
|
51
|
+
if name in ds_src.data_vars:
|
|
52
|
+
u_var = name
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
if v_var is None:
|
|
56
|
+
preferred_v = ["v10", "v10m", "v"] if height == "10m" else ["v100", "v100m", "v"]
|
|
57
|
+
for name in preferred_v:
|
|
58
|
+
if name in ds_src.data_vars:
|
|
59
|
+
v_var = name
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
if u_var is None or v_var is None:
|
|
63
|
+
raise ValueError(f"Could not infer u/v variables. Available: {list(ds_src.data_vars)}")
|
|
64
|
+
|
|
65
|
+
u_da = normalize_wind_da(ds_src[u_var], time_dim=time_dim)
|
|
66
|
+
v_da = normalize_wind_da(ds_src[v_var], time_dim=time_dim)
|
|
67
|
+
|
|
68
|
+
lon_min, lat_min, lon_max, lat_max = parse_bbox(bbox, bbox_order=bbox_order)
|
|
69
|
+
u_da = u_da.sel(lat=slice(lat_min, lat_max), lon=slice(lon_min, lon_max))
|
|
70
|
+
v_da = v_da.sel(lat=slice(lat_min, lat_max), lon=slice(lon_min, lon_max))
|
|
71
|
+
|
|
72
|
+
if start is not None or end is not None:
|
|
73
|
+
u_da = u_da.sel(time=slice(start, end))
|
|
74
|
+
v_da = v_da.sel(time=slice(start, end))
|
|
75
|
+
|
|
76
|
+
u_da, v_da = xr.align(u_da, v_da, join="inner")
|
|
77
|
+
wind_da = (u_da**2 + v_da**2) ** 0.5
|
|
78
|
+
return wind_da.rename("wind").astype("float32")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def download(
|
|
82
|
+
*,
|
|
83
|
+
bbox: tuple[float, float, float, float],
|
|
84
|
+
start: str,
|
|
85
|
+
end: str,
|
|
86
|
+
out: str | Path | None = None,
|
|
87
|
+
edh_key: str | None = None,
|
|
88
|
+
edh_path: str | None = None,
|
|
89
|
+
height: str = "10m",
|
|
90
|
+
u_var: str | None = None,
|
|
91
|
+
v_var: str | None = None,
|
|
92
|
+
bbox_order: str = "lonlat",
|
|
93
|
+
time_dim: str = "time",
|
|
94
|
+
overwrite: bool = False,
|
|
95
|
+
edh_time_chunk: int = 168,
|
|
96
|
+
tile_lat: int = 50,
|
|
97
|
+
tile_lon: int = 50,
|
|
98
|
+
min_valid: int = 100,
|
|
99
|
+
compute: bool = True,
|
|
100
|
+
write_summary: bool = False,
|
|
101
|
+
) -> xr.DataArray:
|
|
102
|
+
"""Download ERA5 wind data and optionally write it to Zarr."""
|
|
103
|
+
wind_da = _get_wind_da(
|
|
104
|
+
bbox=bbox,
|
|
105
|
+
start=start,
|
|
106
|
+
end=end,
|
|
107
|
+
edh_key=edh_key,
|
|
108
|
+
edh_path=edh_path,
|
|
109
|
+
height=height,
|
|
110
|
+
u_var=u_var,
|
|
111
|
+
v_var=v_var,
|
|
112
|
+
bbox_order=bbox_order,
|
|
113
|
+
time_dim=time_dim,
|
|
114
|
+
chunks={"time": int(edh_time_chunk)},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if compute:
|
|
118
|
+
wind_da = wind_da.compute()
|
|
119
|
+
|
|
120
|
+
if out is None:
|
|
121
|
+
return wind_da
|
|
122
|
+
|
|
123
|
+
out_path = Path(out)
|
|
124
|
+
if out_path.exists():
|
|
125
|
+
if not overwrite:
|
|
126
|
+
raise ValueError(f"Output {out_path} exists; set overwrite=True to replace.")
|
|
127
|
+
if out_path.is_dir():
|
|
128
|
+
shutil.rmtree(out_path)
|
|
129
|
+
else:
|
|
130
|
+
out_path.unlink()
|
|
131
|
+
|
|
132
|
+
ds_out = wind_da.to_dataset()
|
|
133
|
+
ds_out = ds_out.assign_coords(
|
|
134
|
+
lat=ds_out["lat"].astype("float32"), lon=ds_out["lon"].astype("float32")
|
|
135
|
+
)
|
|
136
|
+
ds_out.attrs.update(
|
|
137
|
+
{
|
|
138
|
+
"start": start,
|
|
139
|
+
"end": end,
|
|
140
|
+
"bbox": list(map(float, bbox)),
|
|
141
|
+
"height": height,
|
|
142
|
+
"edh_path": edh_path or EDH_DEFAULT_PATH,
|
|
143
|
+
"time_chunk": int(edh_time_chunk),
|
|
144
|
+
"min_valid": int(min_valid),
|
|
145
|
+
"source": "era5_edh",
|
|
146
|
+
"tile_lat": int(tile_lat),
|
|
147
|
+
"tile_lon": int(tile_lon),
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
chunks_time = min(int(edh_time_chunk), int(ds_out.sizes.get("time", edh_time_chunk)))
|
|
152
|
+
chunks_lat = min(int(tile_lat), int(ds_out.sizes.get("lat", tile_lat)))
|
|
153
|
+
chunks_lon = min(int(tile_lon), int(ds_out.sizes.get("lon", tile_lon)))
|
|
154
|
+
encoding = {
|
|
155
|
+
"wind": {"dtype": "float32", "chunks": (chunks_time, chunks_lat, chunks_lon)},
|
|
156
|
+
"lat": {"dtype": "float32"},
|
|
157
|
+
"lon": {"dtype": "float32"},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for var_name in ds_out.data_vars:
|
|
161
|
+
ds_out[var_name].encoding = {}
|
|
162
|
+
for coord_name in ds_out.coords:
|
|
163
|
+
ds_out.coords[coord_name].encoding = {}
|
|
164
|
+
ds_out.encoding = {}
|
|
165
|
+
|
|
166
|
+
ds_out.to_zarr(str(out_path), mode="w", compute=True, encoding=encoding)
|
|
167
|
+
|
|
168
|
+
if write_summary:
|
|
169
|
+
summary = {
|
|
170
|
+
"bbox": list(map(float, bbox)),
|
|
171
|
+
"start": start,
|
|
172
|
+
"end": end,
|
|
173
|
+
"out": str(out_path),
|
|
174
|
+
"tile_lat": int(tile_lat),
|
|
175
|
+
"tile_lon": int(tile_lon),
|
|
176
|
+
"min_valid": int(min_valid),
|
|
177
|
+
"height": height,
|
|
178
|
+
"written_vars": list(ds_out.data_vars.keys()),
|
|
179
|
+
}
|
|
180
|
+
summary_path = Path(str(out_path) + ".summary.json")
|
|
181
|
+
summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
|
182
|
+
|
|
183
|
+
return wind_da
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def download_and_downscale(
|
|
187
|
+
*,
|
|
188
|
+
bbox: tuple[float, float, float, float],
|
|
189
|
+
start: str,
|
|
190
|
+
end: str,
|
|
191
|
+
out: str | Path,
|
|
192
|
+
mapping_method: str,
|
|
193
|
+
params_baseline_path: str | Path | None = None,
|
|
194
|
+
params_manipulator_path: str | Path | None = None,
|
|
195
|
+
spacing: float | None = None,
|
|
196
|
+
cache_dir: str | Path | None = None,
|
|
197
|
+
github_repository: str = DEFAULT_GITHUB_REPOSITORY,
|
|
198
|
+
release_tag: str = DEFAULT_WEIBULL_RELEASE_TAG,
|
|
199
|
+
github_token: str | None = None,
|
|
200
|
+
edh_key: str | None = None,
|
|
201
|
+
edh_path: str | None = None,
|
|
202
|
+
height: str = "10m",
|
|
203
|
+
u_var: str | None = None,
|
|
204
|
+
v_var: str | None = None,
|
|
205
|
+
bbox_order: str = "lonlat",
|
|
206
|
+
time_dim: str = "time",
|
|
207
|
+
overwrite: bool = False,
|
|
208
|
+
no_crop: bool = False,
|
|
209
|
+
edh_time_chunk: int = 168,
|
|
210
|
+
) -> Path:
|
|
211
|
+
"""Download ERA5 wind and run downscaling in one call."""
|
|
212
|
+
wind_da = download(
|
|
213
|
+
bbox=bbox,
|
|
214
|
+
start=start,
|
|
215
|
+
end=end,
|
|
216
|
+
out=None,
|
|
217
|
+
edh_key=edh_key,
|
|
218
|
+
edh_path=edh_path,
|
|
219
|
+
height=height,
|
|
220
|
+
u_var=u_var,
|
|
221
|
+
v_var=v_var,
|
|
222
|
+
bbox_order=bbox_order,
|
|
223
|
+
time_dim=time_dim,
|
|
224
|
+
edh_time_chunk=edh_time_chunk,
|
|
225
|
+
compute=True,
|
|
226
|
+
)
|
|
227
|
+
return downscale(
|
|
228
|
+
input_data=wind_da,
|
|
229
|
+
out=out,
|
|
230
|
+
bbox=bbox,
|
|
231
|
+
mapping_method=mapping_method,
|
|
232
|
+
params_baseline_path=params_baseline_path,
|
|
233
|
+
params_manipulator_path=params_manipulator_path,
|
|
234
|
+
spacing=spacing,
|
|
235
|
+
cache_dir=cache_dir,
|
|
236
|
+
github_repository=github_repository,
|
|
237
|
+
release_tag=release_tag,
|
|
238
|
+
github_token=github_token,
|
|
239
|
+
bbox_order=bbox_order,
|
|
240
|
+
no_crop=no_crop,
|
|
241
|
+
overwrite=overwrite,
|
|
242
|
+
)
|