ngiab-data-preprocess 3.3.1__py3-none-any.whl → 4.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.
- data_processing/create_realization.py +1 -1
- data_processing/dataset_utils.py +212 -0
- data_processing/datasets.py +87 -0
- data_processing/forcings.py +230 -75
- data_processing/gpkg_utils.py +2 -2
- data_processing/subset.py +2 -2
- data_sources/source_validation.py +4 -1
- map_app/static/css/toggle.css +82 -0
- map_app/static/js/data_processing.js +10 -1
- map_app/static/js/main.js +17 -0
- map_app/static/resources/screenshot.jpg +0 -0
- map_app/templates/index.html +15 -3
- map_app/views.py +18 -2
- ngiab_data_cli/__main__.py +19 -15
- ngiab_data_cli/arguments.py +7 -0
- ngiab_data_cli/forcing_cli.py +36 -7
- {ngiab_data_preprocess-3.3.1.dist-info → ngiab_data_preprocess-4.0.0.dist-info}/METADATA +4 -3
- {ngiab_data_preprocess-3.3.1.dist-info → ngiab_data_preprocess-4.0.0.dist-info}/RECORD +22 -20
- data_processing/zarr_utils.py +0 -162
- map_app/static/resources/screenshot.png +0 -0
- {ngiab_data_preprocess-3.3.1.dist-info → ngiab_data_preprocess-4.0.0.dist-info}/LICENSE +0 -0
- {ngiab_data_preprocess-3.3.1.dist-info → ngiab_data_preprocess-4.0.0.dist-info}/WHEEL +0 -0
- {ngiab_data_preprocess-3.3.1.dist-info → ngiab_data_preprocess-4.0.0.dist-info}/entry_points.txt +0 -0
- {ngiab_data_preprocess-3.3.1.dist-info → ngiab_data_preprocess-4.0.0.dist-info}/top_level.txt +0 -0
|
@@ -202,7 +202,7 @@ def get_model_attributes(hydrofabric: Path):
|
|
|
202
202
|
(SELECT crs_string FROM source_crs), 'EPSG:4326')) AS latitude FROM 'divide-attributes';""",
|
|
203
203
|
conn,
|
|
204
204
|
)
|
|
205
|
-
except
|
|
205
|
+
except sqlite3.OperationalError:
|
|
206
206
|
with sqlite3.connect(hydrofabric) as conn:
|
|
207
207
|
conf_df = pandas.read_sql_query("SELECT* FROM 'divide-attributes';", conn,)
|
|
208
208
|
source_crs = get_table_crs_short(hydrofabric, "divides")
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Tuple, Union
|
|
5
|
+
|
|
6
|
+
import geopandas as gpd
|
|
7
|
+
import numpy as np
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from dask.distributed import Client, progress
|
|
10
|
+
import datetime
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# known ngen variable names
|
|
15
|
+
# https://github.com/CIROH-UA/ngen/blob/4fb5bb68dc397298bca470dfec94db2c1dcb42fe/include/forcing/AorcForcing.hpp#L77
|
|
16
|
+
|
|
17
|
+
def validate_dataset_format(dataset: xr.Dataset) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Validate the format of the dataset.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
dataset : xr.Dataset
|
|
24
|
+
Dataset to be validated.
|
|
25
|
+
|
|
26
|
+
Raises
|
|
27
|
+
------
|
|
28
|
+
ValueError
|
|
29
|
+
If the dataset is not in the correct format.
|
|
30
|
+
"""
|
|
31
|
+
if "time" not in dataset.coords:
|
|
32
|
+
raise ValueError("Dataset must have a 'time' coordinate")
|
|
33
|
+
if not np.issubdtype(dataset.time.dtype, np.datetime64):
|
|
34
|
+
raise ValueError("Time coordinate must be a numpy datetime64 type")
|
|
35
|
+
if "x" not in dataset.coords:
|
|
36
|
+
raise ValueError("Dataset must have an 'x' coordinate")
|
|
37
|
+
if "y" not in dataset.coords:
|
|
38
|
+
raise ValueError("Dataset must have a 'y' coordinate")
|
|
39
|
+
if "crs" not in dataset.attrs:
|
|
40
|
+
raise ValueError("Dataset must have a 'crs' attribute")
|
|
41
|
+
if "name" not in dataset.attrs:
|
|
42
|
+
raise ValueError("Dataset must have a name attribute to identify it")
|
|
43
|
+
|
|
44
|
+
def validate_time_range(dataset: xr.Dataset, start_time: str, end_time: str) -> Tuple[str, str]:
|
|
45
|
+
'''
|
|
46
|
+
Ensure that all selected times are in the passed dataset.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
dataset : xr.Dataset
|
|
51
|
+
Dataset with a time coordinate.
|
|
52
|
+
start_time : str
|
|
53
|
+
Desired start time in YYYY/MM/DD HH:MM:SS format.
|
|
54
|
+
end_time : str
|
|
55
|
+
Desired end time in YYYY/MM/DD HH:MM:SS format.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
str
|
|
60
|
+
start_time, or if not available, earliest available timestep in dataset.
|
|
61
|
+
str
|
|
62
|
+
end_time, or if not available, latest available timestep in dataset.
|
|
63
|
+
'''
|
|
64
|
+
end_time_in_dataset = dataset.time.isel(time=-1).values
|
|
65
|
+
start_time_in_dataset = dataset.time.isel(time=0).values
|
|
66
|
+
if np.datetime64(start_time) < start_time_in_dataset:
|
|
67
|
+
logger.warning(
|
|
68
|
+
f"provided start {start_time} is before the start of the dataset {start_time_in_dataset}, selecting from {start_time_in_dataset}"
|
|
69
|
+
)
|
|
70
|
+
start_time = start_time_in_dataset
|
|
71
|
+
if np.datetime64(end_time) > end_time_in_dataset:
|
|
72
|
+
logger.warning(
|
|
73
|
+
f"provided end {end_time} is after the end of the dataset {end_time_in_dataset}, selecting until {end_time_in_dataset}"
|
|
74
|
+
)
|
|
75
|
+
end_time = end_time_in_dataset
|
|
76
|
+
return start_time, end_time
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def clip_dataset_to_bounds(
|
|
80
|
+
dataset: xr.Dataset, bounds: Tuple[float, float, float, float], start_time: str, end_time: str
|
|
81
|
+
) -> xr.Dataset:
|
|
82
|
+
"""
|
|
83
|
+
Clip the dataset to specified geographical bounds.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
dataset : xr.Dataset
|
|
88
|
+
Dataset to be clipped.
|
|
89
|
+
bounds : tuple[float, float, float, float]
|
|
90
|
+
Corners of bounding box. bounds[0] is x_min, bounds[1] is y_min,
|
|
91
|
+
bounds[2] is x_max, bounds[3] is y_max.
|
|
92
|
+
start_time : str
|
|
93
|
+
Desired start time in YYYY/MM/DD HH:MM:SS format.
|
|
94
|
+
end_time : str
|
|
95
|
+
Desired end time in YYYY/MM/DD HH:MM:SS format.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
xr.Dataset
|
|
100
|
+
Clipped dataset.
|
|
101
|
+
"""
|
|
102
|
+
# check time range here in case just this function is imported and not the whole module
|
|
103
|
+
start_time, end_time = validate_time_range(dataset, start_time, end_time)
|
|
104
|
+
dataset = dataset.sel(
|
|
105
|
+
x=slice(bounds[0], bounds[2]),
|
|
106
|
+
y=slice(bounds[1], bounds[3]),
|
|
107
|
+
time=slice(start_time, end_time),
|
|
108
|
+
)
|
|
109
|
+
logger.info("Selected time range and clipped to bounds")
|
|
110
|
+
return dataset
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_to_cache(stores: xr.Dataset, cached_nc_path: Path) -> xr.Dataset:
|
|
114
|
+
"""Compute the store and save it to a cached netCDF file. This is not required but will save time and bandwidth."""
|
|
115
|
+
logger.info("Downloading and caching forcing data, this may take a while")
|
|
116
|
+
|
|
117
|
+
if not cached_nc_path.parent.exists():
|
|
118
|
+
cached_nc_path.parent.mkdir(parents=True)
|
|
119
|
+
|
|
120
|
+
# sort of terrible work around for half downloaded files
|
|
121
|
+
temp_path = cached_nc_path.with_suffix(".downloading.nc")
|
|
122
|
+
if os.path.exists(temp_path):
|
|
123
|
+
os.remove(temp_path)
|
|
124
|
+
|
|
125
|
+
## Cast every single variable to float32 to save space to save a lot of memory issues later
|
|
126
|
+
## easier to do it now in this slow download step than later in the steps without dask
|
|
127
|
+
for var in stores.data_vars:
|
|
128
|
+
stores[var] = stores[var].astype("float32")
|
|
129
|
+
|
|
130
|
+
client = Client.current()
|
|
131
|
+
future = client.compute(stores.to_netcdf(temp_path, compute=False))
|
|
132
|
+
# Display progress bar
|
|
133
|
+
progress(future)
|
|
134
|
+
future.result()
|
|
135
|
+
|
|
136
|
+
os.rename(temp_path, cached_nc_path)
|
|
137
|
+
|
|
138
|
+
data = xr.open_mfdataset(cached_nc_path, parallel=True, engine="h5netcdf")
|
|
139
|
+
return data
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_local_cache(
|
|
143
|
+
cached_nc_path: Path,
|
|
144
|
+
start_time: str,
|
|
145
|
+
end_time: str,
|
|
146
|
+
gdf: gpd.GeoDataFrame,
|
|
147
|
+
remote_dataset: xr.Dataset
|
|
148
|
+
) -> Union[xr.Dataset, None]:
|
|
149
|
+
|
|
150
|
+
merged_data = None
|
|
151
|
+
|
|
152
|
+
if not os.path.exists(cached_nc_path):
|
|
153
|
+
logger.info("No cache found")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
logger.info("Found cached nc file")
|
|
157
|
+
# open the cached file and check that the time range is correct
|
|
158
|
+
cached_data = xr.open_mfdataset(
|
|
159
|
+
cached_nc_path, parallel=True, engine="h5netcdf"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if "name" not in cached_data.attrs or "name" not in remote_dataset.attrs:
|
|
163
|
+
logger.warning("No name attribute found to compare datasets")
|
|
164
|
+
return
|
|
165
|
+
if cached_data.name != remote_dataset.name:
|
|
166
|
+
logger.warning("Cached data from different source, .name attr doesn't match")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
range_in_cache = cached_data.time[0].values <= np.datetime64(
|
|
170
|
+
start_time
|
|
171
|
+
) and cached_data.time[-1].values >= np.datetime64(end_time)
|
|
172
|
+
|
|
173
|
+
if not range_in_cache:
|
|
174
|
+
# the cache does not contain the desired time range
|
|
175
|
+
logger.warning("Requested time range not in cache")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
cached_vars = cached_data.data_vars.keys()
|
|
179
|
+
forcing_vars = remote_dataset.data_vars.keys()
|
|
180
|
+
# replace rainrate with precip
|
|
181
|
+
missing_vars = set(forcing_vars) - set(cached_vars)
|
|
182
|
+
if len(missing_vars) > 0:
|
|
183
|
+
logger.warning(f"Missing forcing vars in cache: {missing_vars}")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
if range_in_cache:
|
|
187
|
+
logger.info("Time range is within cached data")
|
|
188
|
+
logger.debug(f"Opened cached nc file: [{cached_nc_path}]")
|
|
189
|
+
merged_data = clip_dataset_to_bounds(
|
|
190
|
+
cached_data, gdf.total_bounds, start_time, end_time
|
|
191
|
+
)
|
|
192
|
+
logger.debug("Clipped stores")
|
|
193
|
+
|
|
194
|
+
return merged_data
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def save_and_clip_dataset(
|
|
198
|
+
dataset: xr.Dataset,
|
|
199
|
+
gdf: gpd.GeoDataFrame,
|
|
200
|
+
start_time: datetime.datetime,
|
|
201
|
+
end_time: datetime.datetime,
|
|
202
|
+
cache_location: Path,
|
|
203
|
+
) -> xr.Dataset:
|
|
204
|
+
"""convenience function clip the remote dataset, and either load from cache or save to cache if it's not present"""
|
|
205
|
+
gdf = gdf.to_crs(dataset.crs)
|
|
206
|
+
|
|
207
|
+
cached_data = check_local_cache(cache_location, start_time, end_time, gdf, dataset)
|
|
208
|
+
|
|
209
|
+
if not cached_data:
|
|
210
|
+
clipped_data = clip_dataset_to_bounds(dataset, gdf.total_bounds, start_time, end_time)
|
|
211
|
+
cached_data = save_to_cache(clipped_data, cache_location)
|
|
212
|
+
return cached_data
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import s3fs
|
|
4
|
+
from data_processing.s3fs_utils import S3ParallelFileSystem
|
|
5
|
+
import xarray as xr
|
|
6
|
+
from dask.distributed import Client, LocalCluster
|
|
7
|
+
from data_processing.dataset_utils import validate_dataset_format
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_v3_retrospective_zarr(forcing_vars: list[str] = None) -> xr.Dataset:
|
|
14
|
+
"""Load zarr datasets from S3 within the specified time range."""
|
|
15
|
+
# if a LocalCluster is not already running, start one
|
|
16
|
+
if not forcing_vars:
|
|
17
|
+
forcing_vars = ["lwdown", "precip", "psfc", "q2d", "swdown", "t2d", "u2d", "v2d"]
|
|
18
|
+
try:
|
|
19
|
+
client = Client.current()
|
|
20
|
+
except ValueError:
|
|
21
|
+
cluster = LocalCluster()
|
|
22
|
+
client = Client(cluster)
|
|
23
|
+
s3_urls = [
|
|
24
|
+
f"s3://noaa-nwm-retrospective-3-0-pds/CONUS/zarr/forcing/{var}.zarr"
|
|
25
|
+
for var in forcing_vars
|
|
26
|
+
]
|
|
27
|
+
# default cache is readahead which is detrimental to performance in this case
|
|
28
|
+
fs = S3ParallelFileSystem(anon=True, default_cache_type="none") # default_block_size
|
|
29
|
+
s3_stores = [s3fs.S3Map(url, s3=fs) for url in s3_urls]
|
|
30
|
+
# the cache option here just holds accessed data in memory to prevent s3 being queried multiple times
|
|
31
|
+
# most of the data is read once and written to disk but some of the coordinate data is read multiple times
|
|
32
|
+
dataset = xr.open_mfdataset(s3_stores, parallel=True, engine="zarr", cache=True)
|
|
33
|
+
|
|
34
|
+
# set the crs attribute to conform with the format
|
|
35
|
+
esri_pe_string = dataset.crs.esri_pe_string
|
|
36
|
+
dataset = dataset.drop_vars(["crs"])
|
|
37
|
+
dataset.attrs["crs"] = esri_pe_string
|
|
38
|
+
dataset.attrs["name"] = "v3_retrospective_zarr"
|
|
39
|
+
|
|
40
|
+
# rename the data vars to work with ngen
|
|
41
|
+
variables = {
|
|
42
|
+
"LWDOWN": "DLWRF_surface",
|
|
43
|
+
"PSFC": "PRES_surface",
|
|
44
|
+
"Q2D": "SPFH_2maboveground",
|
|
45
|
+
"RAINRATE": "precip_rate",
|
|
46
|
+
"SWDOWN": "DSWRF_surface",
|
|
47
|
+
"T2D": "TMP_2maboveground",
|
|
48
|
+
"U2D": "UGRD_10maboveground",
|
|
49
|
+
"V2D": "VGRD_10maboveground",
|
|
50
|
+
}
|
|
51
|
+
dataset = dataset.rename_vars(variables)
|
|
52
|
+
|
|
53
|
+
validate_dataset_format(dataset)
|
|
54
|
+
return dataset
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_aorc_zarr(start_year: int = None, end_year: int = None) -> xr.Dataset:
|
|
58
|
+
"""Load the aorc zarr dataset from S3."""
|
|
59
|
+
if not start_year or not end_year:
|
|
60
|
+
logger.warning("No start or end year provided, defaulting to 1979-2023")
|
|
61
|
+
logger.warning("To reduce the time taken to load the data, provide a smaller range")
|
|
62
|
+
if not start_year:
|
|
63
|
+
start_year = 1979
|
|
64
|
+
if not end_year:
|
|
65
|
+
end_year = 2023
|
|
66
|
+
try:
|
|
67
|
+
client = Client.current()
|
|
68
|
+
except ValueError:
|
|
69
|
+
cluster = LocalCluster()
|
|
70
|
+
client = Client(cluster)
|
|
71
|
+
|
|
72
|
+
logger.info(f"Loading AORC zarr datasets from {start_year} to {end_year}")
|
|
73
|
+
estimated_time_s = ((end_year - start_year) * 2.5) + 3.5
|
|
74
|
+
# from testing, it's about 2.1s per year + 3.5s overhead
|
|
75
|
+
logger.info(f"This should take roughly {estimated_time_s} seconds")
|
|
76
|
+
fs = S3ParallelFileSystem(anon=True, default_cache_type="none")
|
|
77
|
+
s3_url = "s3://noaa-nws-aorc-v1-1-1km/"
|
|
78
|
+
urls = [f"{s3_url}{i}.zarr" for i in range(start_year, end_year+1)]
|
|
79
|
+
filestores = [s3fs.S3Map(url, s3=fs) for url in urls]
|
|
80
|
+
dataset = xr.open_mfdataset(filestores, parallel=True, engine="zarr", cache=True)
|
|
81
|
+
dataset.attrs["crs"] = "+proj=longlat +datum=WGS84 +no_defs"
|
|
82
|
+
dataset.attrs["name"] = "aorc_1km_zarr"
|
|
83
|
+
# rename latitude and longitude to x and y
|
|
84
|
+
dataset = dataset.rename({"latitude": "y", "longitude": "x"})
|
|
85
|
+
|
|
86
|
+
validate_dataset_format(dataset)
|
|
87
|
+
return dataset
|