ascat 2.4.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.
- ascat/__init__.py +16 -0
- ascat/aggregate/__init__.py +0 -0
- ascat/aggregate/aggregators.py +366 -0
- ascat/aggregate/interface.py +146 -0
- ascat/cgls.py +164 -0
- ascat/download/connectors.py +632 -0
- ascat/download/interface.py +272 -0
- ascat/eumetsat/__init__.py +0 -0
- ascat/eumetsat/level1.py +548 -0
- ascat/eumetsat/level2.py +404 -0
- ascat/file_handling.py +1003 -0
- ascat/h_saf.py +322 -0
- ascat/read_native/__init__.py +0 -0
- ascat/read_native/bufr.py +603 -0
- ascat/read_native/cdr.py +399 -0
- ascat/read_native/eps_native.py +2051 -0
- ascat/read_native/formats/ccsds.xml +26 -0
- ascat/read_native/formats/eps.dtd +170 -0
- ascat/read_native/formats/eps.xsl +2303 -0
- ascat/read_native/formats/eps_ascatl1bszf_13.1.xml +354 -0
- ascat/read_native/formats/eps_ascatl1bszf_6.4.xml +965 -0
- ascat/read_native/formats/eps_ascatl1bszf_6.4m.xml +965 -0
- ascat/read_native/formats/eps_ascatl1bszf_6.5.xml +1023 -0
- ascat/read_native/formats/eps_ascatl1bszf_6.6.xml +1021 -0
- ascat/read_native/formats/eps_ascatl1bszf_7.0.xml +557 -0
- ascat/read_native/formats/eps_ascatl1bszf_9.0.xml +503 -0
- ascat/read_native/formats/eps_ascatl1bszo_13.1.xml +394 -0
- ascat/read_native/formats/eps_ascatl1bszo_6.4.xml +1125 -0
- ascat/read_native/formats/eps_ascatl1bszo_6.4m.xml +1125 -0
- ascat/read_native/formats/eps_ascatl1bszo_6.5.xml +1183 -0
- ascat/read_native/formats/eps_ascatl1bszo_6.6.xml +1171 -0
- ascat/read_native/formats/eps_ascatl1bszo_7.0.xml +709 -0
- ascat/read_native/formats/eps_ascatl1bszo_9.0.xml +700 -0
- ascat/read_native/formats/eps_ascatl1bszr_13.1.xml +394 -0
- ascat/read_native/formats/eps_ascatl1bszr_6.4.xml +1125 -0
- ascat/read_native/formats/eps_ascatl1bszr_6.4m.xml +1125 -0
- ascat/read_native/formats/eps_ascatl1bszr_6.5.xml +1183 -0
- ascat/read_native/formats/eps_ascatl1bszr_6.6.xml +1171 -0
- ascat/read_native/formats/eps_ascatl1bszr_7.0.xml +707 -0
- ascat/read_native/formats/eps_ascatl1bszr_9.0.xml +698 -0
- ascat/read_native/formats/eps_ascatl2smo_0.1.xml +354 -0
- ascat/read_native/formats/eps_ascatl2smo_3b.xml +416 -0
- ascat/read_native/formats/eps_ascatl2smo_4.xml +414 -0
- ascat/read_native/formats/eps_ascatl2smr_0.1.xml +355 -0
- ascat/read_native/formats/eps_ascatl2smr_3b.xml +410 -0
- ascat/read_native/formats/eps_ascatl2smr_4.xml +408 -0
- ascat/read_native/hdf5.py +270 -0
- ascat/read_native/nc.py +580 -0
- ascat/read_native/ragged_array_ts.py +2572 -0
- ascat/read_native/xarray_io.py +1769 -0
- ascat/regrid/__init__.py +0 -0
- ascat/regrid/interface.py +138 -0
- ascat/regrid/regrid.py +158 -0
- ascat/utils.py +374 -0
- ascat-2.4.0.dist-info/LICENSE.txt +21 -0
- ascat-2.4.0.dist-info/METADATA +120 -0
- ascat-2.4.0.dist-info/RECORD +60 -0
- ascat-2.4.0.dist-info/WHEEL +5 -0
- ascat-2.4.0.dist-info/entry_points.txt +5 -0
- ascat-2.4.0.dist-info/top_level.txt +1 -0
ascat/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info[:2] >= (3, 8):
|
|
4
|
+
# TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version # pragma: no cover
|
|
6
|
+
else:
|
|
7
|
+
from importlib_metadata import PackageNotFoundError, version # pragma: no cover
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
# Change here if project is renamed and does not equal the package name
|
|
11
|
+
dist_name = __name__
|
|
12
|
+
__version__ = version(dist_name)
|
|
13
|
+
except PackageNotFoundError: # pragma: no cover
|
|
14
|
+
__version__ = "unknown"
|
|
15
|
+
finally:
|
|
16
|
+
del version, PackageNotFoundError
|
|
File without changes
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Copyright (c) 2024, TU Wien
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
|
6
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
7
|
+
# this list of conditions and the following disclaimer.
|
|
8
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
9
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
10
|
+
# documentation and/or other materials provided with the distribution.
|
|
11
|
+
# * Neither the name of TU Wien, Department of Geodesy and Geoinformation
|
|
12
|
+
# nor the names of its contributors may be used to endorse or promote
|
|
13
|
+
# products derived from this software without specific prior written
|
|
14
|
+
# permission.
|
|
15
|
+
|
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
19
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL TU WIEN DEPARTMENT OF GEODESY AND
|
|
20
|
+
# GEOINFORMATION BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
21
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
22
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
|
23
|
+
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
24
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
25
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
26
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
27
|
+
|
|
28
|
+
import datetime
|
|
29
|
+
import tempfile
|
|
30
|
+
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
import pandas as pd
|
|
35
|
+
import xarray as xr
|
|
36
|
+
|
|
37
|
+
from flox.xarray import xarray_reduce
|
|
38
|
+
from dask.array import unique as da_unique
|
|
39
|
+
|
|
40
|
+
import ascat.read_native.ragged_array_ts as rat
|
|
41
|
+
from ascat.read_native.xarray_io import get_swath_product_id
|
|
42
|
+
from ascat.read_native.xarray_io import dtype_to_nan
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TemporalSwathAggregator:
|
|
46
|
+
"""Class to aggregate ASCAT data its location ids over time."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
filepath,
|
|
51
|
+
start_dt,
|
|
52
|
+
end_dt,
|
|
53
|
+
t_delta,
|
|
54
|
+
agg,
|
|
55
|
+
snow_cover_mask=80,
|
|
56
|
+
frozen_soil_mask=80,
|
|
57
|
+
subsurface_scattering_mask=5,
|
|
58
|
+
ssm_sensitivity_mask=1,
|
|
59
|
+
no_masking=False,
|
|
60
|
+
):
|
|
61
|
+
"""Initialize the class.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
filepath : str
|
|
66
|
+
Path to the data.
|
|
67
|
+
start_dt : str
|
|
68
|
+
Start date and time (formatted e.g. 2020-01-01T00:00:00).
|
|
69
|
+
end_dt : str
|
|
70
|
+
End date and time (formatted e.g. 2020-02-01T00:00:00).
|
|
71
|
+
t_delta : str
|
|
72
|
+
Time period for aggregation (e.g. 1D, 1W, 1M, 1Y, 3M, 4Y, etc.).
|
|
73
|
+
agg : str
|
|
74
|
+
Aggregation method (e.g. mean, median, std).
|
|
75
|
+
snow_cover_mask : int, optional
|
|
76
|
+
Snow cover probability value above which to mask the source data.
|
|
77
|
+
frozen_soil_mask : int, optional
|
|
78
|
+
Frozen soil probability value above which to mask the source data.
|
|
79
|
+
subsurface_scattering_mask : int, optional
|
|
80
|
+
Subsurface scattering probability value above which to mask
|
|
81
|
+
the source data.
|
|
82
|
+
ssm_sensitivity_mask : float, optional
|
|
83
|
+
Soil moisture sensitivity value above which to mask
|
|
84
|
+
the source data.
|
|
85
|
+
no_masking : boolean, optional
|
|
86
|
+
Ignore all masks (default: False).
|
|
87
|
+
"""
|
|
88
|
+
self.filepath = filepath
|
|
89
|
+
|
|
90
|
+
fmt = "%Y-%m-%dT%H:%M:%S"
|
|
91
|
+
self.start_dt = datetime.datetime.strptime(start_dt, fmt)
|
|
92
|
+
self.end_dt = datetime.datetime.strptime(end_dt, fmt)
|
|
93
|
+
self.timedelta = pd.Timedelta(t_delta)
|
|
94
|
+
self.no_masking = no_masking
|
|
95
|
+
|
|
96
|
+
agg_methods = [
|
|
97
|
+
"mean", "median", "mode", "std", "min", "max", "argmin", "argmax",
|
|
98
|
+
"quantile", "first", "last"
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
if agg in agg_methods:
|
|
102
|
+
agg = "nan" + agg
|
|
103
|
+
|
|
104
|
+
self.agg = agg
|
|
105
|
+
|
|
106
|
+
# assumes ONLY swath files are in the folder
|
|
107
|
+
first_fname = str(next(Path(filepath).rglob("*.nc")).name)
|
|
108
|
+
product = get_swath_product_id(first_fname)
|
|
109
|
+
self.product = product
|
|
110
|
+
|
|
111
|
+
self.collection = rat.SwathFileCollection.from_product_id(
|
|
112
|
+
Path(filepath), product)
|
|
113
|
+
|
|
114
|
+
self.grid = self.collection.grid
|
|
115
|
+
self.data = None
|
|
116
|
+
self.agg_vars = {
|
|
117
|
+
"surface_soil_moisture": {
|
|
118
|
+
"dtype": np.dtype("int16"),
|
|
119
|
+
"scale_factor": 1e-2,
|
|
120
|
+
},
|
|
121
|
+
"backscatter40": {
|
|
122
|
+
"dtype": np.dtype("int32"),
|
|
123
|
+
"scale_factor": 1e-7,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
self.mask_probs = {
|
|
127
|
+
"snow_cover_probability": snow_cover_mask,
|
|
128
|
+
"frozen_soil_probability": frozen_soil_mask,
|
|
129
|
+
"subsurface_scattering_probability": subsurface_scattering_mask,
|
|
130
|
+
"surface_soil_moisture_sensitivity": ssm_sensitivity_mask,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def _read_data(self):
|
|
134
|
+
"""Read data."""
|
|
135
|
+
print("constructing dataset, this may take some time...")
|
|
136
|
+
|
|
137
|
+
self.data = self.collection.read(
|
|
138
|
+
date_range=(self.start_dt, self.end_dt),)
|
|
139
|
+
|
|
140
|
+
print("done constructing dataset")
|
|
141
|
+
|
|
142
|
+
def _set_metadata(self, ds):
|
|
143
|
+
"""Add appropriate metadata to datasets."""
|
|
144
|
+
return ds
|
|
145
|
+
|
|
146
|
+
def _create_output_encoding(self):
|
|
147
|
+
"""Create NetCDF encoding."""
|
|
148
|
+
|
|
149
|
+
output_encoding = {
|
|
150
|
+
"latitude": {
|
|
151
|
+
"dtype": np.dtype("int32"),
|
|
152
|
+
"scale_factor": 1e-6,
|
|
153
|
+
"zlib": True,
|
|
154
|
+
"complevel": 4,
|
|
155
|
+
"_FillValue": dtype_to_nan[np.dtype("int32")],
|
|
156
|
+
"missing_value": dtype_to_nan[np.dtype("int32")],
|
|
157
|
+
},
|
|
158
|
+
"longitude": {
|
|
159
|
+
"dtype": np.dtype("int32"),
|
|
160
|
+
"scale_factor": 1e-6,
|
|
161
|
+
"zlib": True,
|
|
162
|
+
"complevel": 4,
|
|
163
|
+
"_FillValue": dtype_to_nan[np.dtype("int32")],
|
|
164
|
+
"missing_value": dtype_to_nan[np.dtype("int32")],
|
|
165
|
+
},
|
|
166
|
+
"time": {
|
|
167
|
+
"dtype": np.dtype("float64"),
|
|
168
|
+
"zlib": True,
|
|
169
|
+
"complevel": 4,
|
|
170
|
+
"_FillValue": 0,
|
|
171
|
+
"missing_value": 0,
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for var in self.agg_vars:
|
|
176
|
+
if var in output_encoding:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
output_encoding[var] = {
|
|
180
|
+
"dtype": self.agg_vars[var]["dtype"],
|
|
181
|
+
"scale_factor": self.agg_vars[var]["scale_factor"],
|
|
182
|
+
"zlib": True,
|
|
183
|
+
"complevel": 4,
|
|
184
|
+
"_FillValue": dtype_to_nan[self.agg_vars[var]["dtype"]],
|
|
185
|
+
"missing_value": dtype_to_nan[self.agg_vars[var]["dtype"]],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return output_encoding
|
|
189
|
+
|
|
190
|
+
def write_time_steps(self, outpath):
|
|
191
|
+
"""
|
|
192
|
+
Loop through time steps and write them to file.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
outpath : str
|
|
197
|
+
Output path.
|
|
198
|
+
"""
|
|
199
|
+
product_id = self.product.lower().replace("_", "-")
|
|
200
|
+
grid_sampling = str(self.collection.ioclass.grid_sampling_km) + "km"
|
|
201
|
+
|
|
202
|
+
if self.agg is not None:
|
|
203
|
+
datasets = self.get_aggregated_time_steps()
|
|
204
|
+
agg_str = f"_{self.agg}"
|
|
205
|
+
else:
|
|
206
|
+
datasets = self.get_time_steps()
|
|
207
|
+
agg_str = "_data"
|
|
208
|
+
|
|
209
|
+
fmt = "%Y%m%d%H%M%S"
|
|
210
|
+
|
|
211
|
+
paths = []
|
|
212
|
+
for ds in datasets:
|
|
213
|
+
step_start_str = (
|
|
214
|
+
np.datetime64(ds.attrs["start_time"]).astype(
|
|
215
|
+
datetime.datetime).strftime(fmt))
|
|
216
|
+
step_end_str = (
|
|
217
|
+
np.datetime64(ds.attrs["end_time"]).astype(
|
|
218
|
+
datetime.datetime).strftime(fmt))
|
|
219
|
+
|
|
220
|
+
out_name = (f"ascat"
|
|
221
|
+
f"_{product_id}"
|
|
222
|
+
f"_{grid_sampling}"
|
|
223
|
+
f"{agg_str}"
|
|
224
|
+
f"_{step_start_str}"
|
|
225
|
+
f"_{step_end_str}.nc")
|
|
226
|
+
|
|
227
|
+
paths.append(Path(outpath) / out_name)
|
|
228
|
+
|
|
229
|
+
print("saving datasets...", end="\r")
|
|
230
|
+
|
|
231
|
+
output_encoding = self._create_output_encoding()
|
|
232
|
+
xr.save_mfdataset(datasets, paths, encoding=output_encoding)
|
|
233
|
+
print("complete ")
|
|
234
|
+
|
|
235
|
+
return paths
|
|
236
|
+
|
|
237
|
+
def get_time_steps(self):
|
|
238
|
+
"""
|
|
239
|
+
Loop through time steps of the range, return the merged data
|
|
240
|
+
for each unmodified.
|
|
241
|
+
"""
|
|
242
|
+
time_steps = pd.date_range(
|
|
243
|
+
start=self.start_dt, end=self.end_dt, freq=self.timedelta)
|
|
244
|
+
datasets = []
|
|
245
|
+
|
|
246
|
+
if self.data is not None:
|
|
247
|
+
# I don't know why this case would exist, but if it does...
|
|
248
|
+
ds = self.data
|
|
249
|
+
for timestep in time_steps:
|
|
250
|
+
step_start = timestep
|
|
251
|
+
step_end = timestep + self.timedelta - pd.Timedelta("1s")
|
|
252
|
+
ds_step = ds.sel(time=slice(step_start, step_end))
|
|
253
|
+
ds_step.attrs["start_time"] = np.datetime64(step_start).astype(
|
|
254
|
+
str)
|
|
255
|
+
ds_step.attrs["end_time"] = np.datetime64(step_end).astype(str)
|
|
256
|
+
datasets.append(ds_step)
|
|
257
|
+
|
|
258
|
+
if self.data is None:
|
|
259
|
+
for timestep in time_steps:
|
|
260
|
+
step_start = timestep
|
|
261
|
+
step_end = timestep + self.timedelta
|
|
262
|
+
ds_step = self.collection.read(
|
|
263
|
+
date_range=(step_start, step_end))
|
|
264
|
+
step_end = step_end - pd.Timedelta("1s")
|
|
265
|
+
ds_step.attrs["start_time"] = np.datetime64(step_start).astype(
|
|
266
|
+
str)
|
|
267
|
+
ds_step.attrs["end_time"] = np.datetime64(step_end).astype(str)
|
|
268
|
+
datasets.append(ds_step)
|
|
269
|
+
|
|
270
|
+
return datasets
|
|
271
|
+
|
|
272
|
+
def get_aggregated_time_steps(self):
|
|
273
|
+
"""Loop through data in time steps, aggregating it over time."""
|
|
274
|
+
if self.data is None:
|
|
275
|
+
self._read_data()
|
|
276
|
+
|
|
277
|
+
ds = self.data
|
|
278
|
+
|
|
279
|
+
present_agg_vars = [
|
|
280
|
+
var for var in self.agg_vars if var in ds.variables
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
print("masking data...", end="\r")
|
|
284
|
+
|
|
285
|
+
global_mask = (ds.surface_flag != 0)
|
|
286
|
+
|
|
287
|
+
ds = ds.where(~global_mask, drop=False)
|
|
288
|
+
|
|
289
|
+
if not self.no_masking:
|
|
290
|
+
variable_masks = {
|
|
291
|
+
"surface_soil_moisture": (
|
|
292
|
+
(ds["frozen_soil_probability"]
|
|
293
|
+
> self.mask_probs["frozen_soil_probability"])
|
|
294
|
+
| (ds["snow_cover_probability"]
|
|
295
|
+
> self.mask_probs["snow_cover_probability"])
|
|
296
|
+
| (ds["subsurface_scattering_probability"]
|
|
297
|
+
> self.mask_probs["subsurface_scattering_probability"])
|
|
298
|
+
| (ds["surface_soil_moisture_sensitivity"]
|
|
299
|
+
< self.mask_probs["surface_soil_moisture_sensitivity"])),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for var, var_mask in variable_masks.items():
|
|
303
|
+
ds[var] = ds[var].where(~var_mask, drop=False)
|
|
304
|
+
|
|
305
|
+
print("grouping data... ")
|
|
306
|
+
|
|
307
|
+
# discretize time into integer-labeled steps according to our desired frequency
|
|
308
|
+
ds["time_steps"] = (
|
|
309
|
+
ds.time - np.datetime64(self.start_dt, "ns")) // self.timedelta
|
|
310
|
+
|
|
311
|
+
# get unique time_step and location_id values so we can tell xarray_reduce
|
|
312
|
+
# what to expect.
|
|
313
|
+
expected_time_steps = da_unique(ds["time_steps"].data).compute()
|
|
314
|
+
expected_location_ids = da_unique(ds["location_id"].data).compute()
|
|
315
|
+
|
|
316
|
+
# remove NaN from the expected location ids (this was introduced by the masking)
|
|
317
|
+
expected_location_ids = expected_location_ids[
|
|
318
|
+
~np.isnan(expected_location_ids)]
|
|
319
|
+
|
|
320
|
+
# group the data by time_steps and location_id and aggregate it
|
|
321
|
+
grouped_ds = xarray_reduce(
|
|
322
|
+
ds[present_agg_vars],
|
|
323
|
+
ds["time_steps"],
|
|
324
|
+
ds["location_id"],
|
|
325
|
+
expected_groups=(expected_time_steps, expected_location_ids),
|
|
326
|
+
func=self.agg)
|
|
327
|
+
|
|
328
|
+
# convert the location_id back to an integer
|
|
329
|
+
grouped_ds["location_id"] = grouped_ds["location_id"].astype(int)
|
|
330
|
+
|
|
331
|
+
lons, lats = self.grid.gpi2lonlat(grouped_ds.location_id.values)
|
|
332
|
+
grouped_ds["longitude"] = ("location_id", lons)
|
|
333
|
+
grouped_ds["latitude"] = ("location_id", lats)
|
|
334
|
+
grouped_ds = grouped_ds.set_coords(["longitude", "latitude"])
|
|
335
|
+
grouped_ds = grouped_ds.chunk({"time_steps": 1})
|
|
336
|
+
|
|
337
|
+
# Compute the results, write to a temporary directory, and then read it back in.
|
|
338
|
+
# If we don't do this, dask will try to compute the entire dataset
|
|
339
|
+
# for each step of time before writing it to disk later.
|
|
340
|
+
|
|
341
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
342
|
+
temp_path = Path(temp_dir) / "ascat.nc"
|
|
343
|
+
print("computing results... ")
|
|
344
|
+
grouped_ds.to_netcdf(temp_path)
|
|
345
|
+
grouped_ds.close()
|
|
346
|
+
grouped_ds = xr.open_dataset(temp_path)
|
|
347
|
+
|
|
348
|
+
groups = []
|
|
349
|
+
for timestep, group in grouped_ds.groupby("time_steps", squeeze=False):
|
|
350
|
+
group = group.squeeze("time_steps")
|
|
351
|
+
print(
|
|
352
|
+
f"writing time step {timestep + 1}/{len(grouped_ds['time_steps'])}... ",
|
|
353
|
+
end="\r",
|
|
354
|
+
)
|
|
355
|
+
step_start = self.start_dt + self.timedelta * timestep
|
|
356
|
+
step_end = (
|
|
357
|
+
self.start_dt + self.timedelta * (timestep + 1) -
|
|
358
|
+
pd.Timedelta("1s"))
|
|
359
|
+
group.attrs["start_time"] = np.datetime64(step_start).astype(str)
|
|
360
|
+
group.attrs["end_time"] = np.datetime64(step_end).astype(str)
|
|
361
|
+
group["time_steps"] = np.datetime64(step_start, "ns")
|
|
362
|
+
group = group.rename({"time_steps": "time"})
|
|
363
|
+
group = self._set_metadata(group)
|
|
364
|
+
groups.append(group)
|
|
365
|
+
|
|
366
|
+
return groups
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Copyright (c) 2024, TU Wien
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
|
6
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
7
|
+
# this list of conditions and the following disclaimer.
|
|
8
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
9
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
10
|
+
# documentation and/or other materials provided with the distribution.
|
|
11
|
+
# * Neither the name of TU Wien, Department of Geodesy and Geoinformation
|
|
12
|
+
# nor the names of its contributors may be used to endorse or promote
|
|
13
|
+
# products derived from this software without specific prior written
|
|
14
|
+
# permission.
|
|
15
|
+
|
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
19
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL TU WIEN DEPARTMENT OF GEODESY AND
|
|
20
|
+
# GEOINFORMATION BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
21
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
22
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
|
23
|
+
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
24
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
25
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
26
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
import argparse
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import ascat.aggregate.aggregators as aggs
|
|
33
|
+
from ascat.regrid.interface import swath_regrid_main
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_args_temporal_swath_agg(args):
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
description=("Generate aggregates of ASCAT swath data over "
|
|
39
|
+
"a given time period"))
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"filepath", metavar="FILEPATH", help="Path to the data")
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"outpath", metavar="OUTPATH", help="Path to the output data")
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--start_dt",
|
|
46
|
+
metavar="START_DT",
|
|
47
|
+
help="Start datetime (formatted e.g. 2020-01-01T00:00:00)")
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--end_dt",
|
|
50
|
+
metavar="END_DT",
|
|
51
|
+
help="End datetime (formatted e.g. 2020-02-01T00:00:00)")
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--t_delta",
|
|
54
|
+
metavar="T_DELTA",
|
|
55
|
+
help="Time period for aggregation (e.g. 1D, 1W, 1M, 1Y, 2D, etc.)")
|
|
56
|
+
parser.add_argument("--agg", metavar="AGG", help="Aggregation")
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--snow_cover_mask",
|
|
59
|
+
metavar="SNOW_COVER_MASK",
|
|
60
|
+
type=int,
|
|
61
|
+
default=90,
|
|
62
|
+
help=("Snow cover probability (0-100 %) value above which "
|
|
63
|
+
"to mask the source data (default: 90 %)"))
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--frozen_soil_mask",
|
|
66
|
+
metavar="FROZEN_SOIL_MASK",
|
|
67
|
+
type=int,
|
|
68
|
+
default=90,
|
|
69
|
+
help=("Frozen soil probability (0-100 %) value above which "
|
|
70
|
+
"to mask the source data (default: 90 %)"))
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--subsurface_scattering_mask",
|
|
73
|
+
metavar="SUBSURFACE_SCATTERING_MASK",
|
|
74
|
+
type=int,
|
|
75
|
+
default=10,
|
|
76
|
+
help=("Subsurface scattering probability (0-100 %) value above which "
|
|
77
|
+
"to mask the source data (default: 10 %)"))
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--ssm_sensitivity_mask",
|
|
80
|
+
metavar="SSM_SENSITIVITY_MASK",
|
|
81
|
+
type=float,
|
|
82
|
+
default=1.0,
|
|
83
|
+
help=("Surface soil moisture sensitivity (in dB) value below which "
|
|
84
|
+
"to mask the source data (default: 1 dB)"))
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--no_masking",
|
|
87
|
+
action='store_const',
|
|
88
|
+
const=True,
|
|
89
|
+
default=False,
|
|
90
|
+
help="Ignore all masks")
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--regrid",
|
|
93
|
+
metavar="REGRID_DEG",
|
|
94
|
+
type=float,
|
|
95
|
+
help=("Regrid the data to a regular grid with the given "
|
|
96
|
+
" spacing in degrees"))
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--grid_store",
|
|
99
|
+
metavar="GRID_STORE",
|
|
100
|
+
help=("Path to a directory for storing grids and "
|
|
101
|
+
"lookup tables between them"))
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--suffix",
|
|
104
|
+
metavar="SUFFIX",
|
|
105
|
+
help="File suffix (default: _REGRID_DEGdeg)")
|
|
106
|
+
|
|
107
|
+
return parser.parse_args(args)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def temporal_swath_agg_main(cli_args):
|
|
111
|
+
"""
|
|
112
|
+
Command line interface routine for temporal aggregation of ASCAT data.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
cli_args : list
|
|
117
|
+
Command line arguments.
|
|
118
|
+
"""
|
|
119
|
+
args = parse_args_temporal_swath_agg(cli_args)
|
|
120
|
+
|
|
121
|
+
transf = aggs.TemporalSwathAggregator(
|
|
122
|
+
args.filepath, args.start_dt, args.end_dt, args.t_delta, args.agg,
|
|
123
|
+
args.snow_cover_mask, args.frozen_soil_mask,
|
|
124
|
+
args.subsurface_scattering_mask, args.ssm_sensitivity_mask,
|
|
125
|
+
args.no_masking)
|
|
126
|
+
|
|
127
|
+
outpath = Path(args.outpath)
|
|
128
|
+
outpath.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
filenames = transf.write_time_steps(outpath)
|
|
131
|
+
|
|
132
|
+
if args.regrid is not None:
|
|
133
|
+
for filename in filenames:
|
|
134
|
+
regrid_args = [str(filename), str(outpath), str(args.regrid)]
|
|
135
|
+
if args.grid_store is not None:
|
|
136
|
+
regrid_args.extend(["--grid_store", args.grid_store])
|
|
137
|
+
if args.suffix is not None:
|
|
138
|
+
regrid_args.extend(["--suffix", args.suffix])
|
|
139
|
+
swath_regrid_main(regrid_args)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run_temporal_swath_agg():
|
|
143
|
+
"""
|
|
144
|
+
Run command line interface for temporal aggregation of ASCAT data.
|
|
145
|
+
"""
|
|
146
|
+
temporal_swath_agg_main(sys.argv[1:])
|
ascat/cgls.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Copyright (c) 2024, TU Wien, Department of Geodesy and Geoinformation
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
|
6
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
7
|
+
# this list of conditions and the following disclaimer.
|
|
8
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
9
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
10
|
+
# documentation and/or other materials provided with the distribution.
|
|
11
|
+
# * Neither the name of TU Wien, Department of Geodesy and Geoinformation
|
|
12
|
+
# nor the names of its contributors may be used to endorse or promote
|
|
13
|
+
# products derived from this software without specific prior written
|
|
14
|
+
# permission.
|
|
15
|
+
|
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
19
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL TU WIEN DEPARTMENT OF GEODESY AND
|
|
20
|
+
# GEOINFORMATION BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
21
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
22
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
|
23
|
+
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
24
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
25
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
26
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
CGLS SWI interface.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import glob
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
import pynetcf.time_series as netcdf_dataset
|
|
37
|
+
import pygeogrids.netcdf as netcdf
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SWI_TS(netcdf_dataset.GriddedNcOrthoMultiTs):
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
SWI TS reader for timeseries data from CGLOPS
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
data_path : string
|
|
48
|
+
path to the netCDF files
|
|
49
|
+
parameters : list
|
|
50
|
+
list of parameters to read from netCDF file
|
|
51
|
+
dt : string, optional
|
|
52
|
+
datetime in the filenames of the cells.
|
|
53
|
+
If not given it is detected from the files in the data_path.
|
|
54
|
+
Automatic detection only works if the files follow the CGLS
|
|
55
|
+
naming convention.
|
|
56
|
+
version : string, optional
|
|
57
|
+
version number of the files
|
|
58
|
+
If not given it is detected from the files in the data_path.
|
|
59
|
+
Automatic detection only works if the files follow the CGLS
|
|
60
|
+
naming convention.
|
|
61
|
+
grid_fname : string, optional
|
|
62
|
+
filename + path of the grid netCDF file,
|
|
63
|
+
default is the standard grid file
|
|
64
|
+
(c_gls_SWI-STATIC-DGG_201501010000_GLOBE_ASCAT_V3.0.1.nc)
|
|
65
|
+
in the same folder as the data
|
|
66
|
+
read_bulk : boolean, optional
|
|
67
|
+
if set to true then a complete 5x5 degree cell will be read at once
|
|
68
|
+
providing speedup if the complete data is needed.
|
|
69
|
+
fname_template : string, optional
|
|
70
|
+
Filename template. Has to have three slots for {dt}, {version} and a
|
|
71
|
+
slot for the {cell} number that is available for further formatting.
|
|
72
|
+
The has to be without the .nc ending since this is added during reading.
|
|
73
|
+
cell_fn : string, optional
|
|
74
|
+
cell number in the fname_template.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, data_path, parameters=['SWI_001', 'SWI_005', 'SWI_010',
|
|
78
|
+
'SWI_015', 'SWI_020', 'SWI_040',
|
|
79
|
+
'SWI_060', 'SWI_100', 'SSF'],
|
|
80
|
+
dt=None, version=None,
|
|
81
|
+
grid_fname=None, read_bulk=True,
|
|
82
|
+
fname_template='c_gls_SWI-TS_{dt}_C{cell}_ASCAT_V{version}',
|
|
83
|
+
cell_fn='{:04d}'):
|
|
84
|
+
|
|
85
|
+
if grid_fname is None:
|
|
86
|
+
grid_fname = os.path.join(
|
|
87
|
+
data_path, 'c_gls_SWI-STATIC-DGG_201501010000_GLOBE_ASCAT_V3.0.1.nc')
|
|
88
|
+
grid = netcdf.load_grid(grid_fname, location_var_name='location_id',
|
|
89
|
+
subset_flag='land_flag')
|
|
90
|
+
|
|
91
|
+
# detect datetime and version if not given
|
|
92
|
+
if dt is None or version is None:
|
|
93
|
+
globstring = fname_template.format(dt="*",
|
|
94
|
+
cell="*",
|
|
95
|
+
version="*")
|
|
96
|
+
found_files = glob.glob(os.path.join(data_path, globstring))
|
|
97
|
+
if len(found_files) == 0:
|
|
98
|
+
raise IOError("No data found in {}".format(data_path))
|
|
99
|
+
fn = found_files[0]
|
|
100
|
+
fn = os.path.splitext(os.path.basename(fn))[0]
|
|
101
|
+
parts = fn.split('_')
|
|
102
|
+
if dt is None:
|
|
103
|
+
# this only works if the files follow the CGLS naming convention
|
|
104
|
+
# for everything else dt should be given as a keyword
|
|
105
|
+
dt = parts[3]
|
|
106
|
+
if version is None:
|
|
107
|
+
version = parts[-1][1:]
|
|
108
|
+
|
|
109
|
+
scale_factors = {'SWI_001': 0.5,
|
|
110
|
+
'SWI_005': 0.5,
|
|
111
|
+
'SWI_010': 0.5,
|
|
112
|
+
'SWI_015': 0.5,
|
|
113
|
+
'SWI_020': 0.5,
|
|
114
|
+
'SWI_040': 0.5,
|
|
115
|
+
'SWI_060': 0.5,
|
|
116
|
+
'SWI_100': 0.5,
|
|
117
|
+
'QFLAG_001': 0.5,
|
|
118
|
+
'QFLAG_005': 0.5,
|
|
119
|
+
'QFLAG_010': 0.5,
|
|
120
|
+
'QFLAG_015': 0.5,
|
|
121
|
+
'QFLAG_020': 0.5,
|
|
122
|
+
'QFLAG_040': 0.5,
|
|
123
|
+
'QFLAG_060': 0.5,
|
|
124
|
+
'QFLAG_100': 0.5,
|
|
125
|
+
'SSF': 1}
|
|
126
|
+
|
|
127
|
+
dtypes = {'SWI_001': np.uint8,
|
|
128
|
+
'SWI_005': np.uint8,
|
|
129
|
+
'SWI_010': np.uint8,
|
|
130
|
+
'SWI_015': np.uint8,
|
|
131
|
+
'SWI_020': np.uint8,
|
|
132
|
+
'SWI_040': np.uint8,
|
|
133
|
+
'SWI_060': np.uint8,
|
|
134
|
+
'SWI_100': np.uint8,
|
|
135
|
+
'QFLAG_001': np.uint8,
|
|
136
|
+
'QFLAG_005': np.uint8,
|
|
137
|
+
'QFLAG_010': np.uint8,
|
|
138
|
+
'QFLAG_015': np.uint8,
|
|
139
|
+
'QFLAG_020': np.uint8,
|
|
140
|
+
'QFLAG_040': np.uint8,
|
|
141
|
+
'QFLAG_060': np.uint8,
|
|
142
|
+
'QFLAG_100': np.uint8,
|
|
143
|
+
'SSF': np.uint8}
|
|
144
|
+
|
|
145
|
+
super(SWI_TS, self).__init__(
|
|
146
|
+
data_path, grid,
|
|
147
|
+
fn_format=fname_template.format(dt=dt, version=version,
|
|
148
|
+
cell=cell_fn),
|
|
149
|
+
parameters=parameters, scale_factors=scale_factors,
|
|
150
|
+
dtypes=dtypes, autoscale=False,
|
|
151
|
+
automask=False, ioclass_kws={'read_bulk': read_bulk,
|
|
152
|
+
'loc_ids_name': 'locations'})
|
|
153
|
+
|
|
154
|
+
def _read_gp(self, gpi, period=None, mask_frozen=True):
|
|
155
|
+
data = super(SWI_TS, self)._read_gp(gpi, period=period)
|
|
156
|
+
|
|
157
|
+
if mask_frozen is True:
|
|
158
|
+
unfrozen = data['SSF'].values <= 1
|
|
159
|
+
data = data[unfrozen]
|
|
160
|
+
|
|
161
|
+
for column in data:
|
|
162
|
+
data.loc[data[column] > 100, column] = np.nan
|
|
163
|
+
|
|
164
|
+
return data
|