smos 0.3.1__py2.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.
- smos/__init__.py +16 -0
- smos/grid.py +57 -0
- smos/interface.py +518 -0
- smos/misc.py +91 -0
- smos/reshuffle.py +150 -0
- smos/smos_ic/__init__.py +16 -0
- smos/smos_ic/download.py +1 -0
- smos/smos_ic/interface_ic.py +190 -0
- smos/smos_ic/reshuffle_ic.py +120 -0
- smos/smos_l2/cli.py +225 -0
- smos/smos_l2/download.py +318 -0
- smos/smos_l2/reshuffle.py +155 -0
- smos/smos_l3/download.py +15 -0
- smos/smos_l4/__init__.py +16 -0
- smos/smos_l4/interface_l4.py +217 -0
- smos/smos_l4/reshuffle_l4.py +120 -0
- smos-0.3.1.dist-info/AUTHORS.rst +6 -0
- smos-0.3.1.dist-info/LICENSE.txt +21 -0
- smos-0.3.1.dist-info/METADATA +167 -0
- smos-0.3.1.dist-info/RECORD +23 -0
- smos-0.3.1.dist-info/WHEEL +6 -0
- smos-0.3.1.dist-info/entry_points.txt +2 -0
- smos-0.3.1.dist-info/top_level.txt +1 -0
smos/__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
|
smos/grid.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from pygeogrids.grids import CellGrid, BasicGrid, lonlat2cell
|
|
4
|
+
from ease_grid import EASE2_grid
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EASE25CellGrid(CellGrid):
|
|
9
|
+
""" CellGrid version of EASE25 Grid as used in SMOS IC """
|
|
10
|
+
|
|
11
|
+
def __init__(self, bbox=None):
|
|
12
|
+
"""
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
bbox: tuple, optional (default: None)
|
|
16
|
+
(min_lon, min_lat, max_lon, max_lat)
|
|
17
|
+
Bounding box to create subset for, if None is passed a global
|
|
18
|
+
grid is used.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
ease25 = EASE2_grid(25000)
|
|
22
|
+
lons, lats = ease25.londim, ease25.latdim
|
|
23
|
+
|
|
24
|
+
lons, lats = np.meshgrid(lons, lats)
|
|
25
|
+
assert lons.shape == lats.shape
|
|
26
|
+
shape = lons.shape
|
|
27
|
+
|
|
28
|
+
lats = np.flipud(lats) # flip lats, so that origin in bottom left
|
|
29
|
+
lons, lats = lons.flatten(), lats.flatten()
|
|
30
|
+
|
|
31
|
+
globgrid = BasicGrid(lons, lats, shape=shape)
|
|
32
|
+
sgpis = globgrid.activegpis
|
|
33
|
+
|
|
34
|
+
self.bbox = bbox
|
|
35
|
+
if self.bbox:
|
|
36
|
+
sgpis = globgrid.get_bbox_grid_points(
|
|
37
|
+
latmin=self.bbox[1], latmax=self.bbox[3],
|
|
38
|
+
lonmin=self.bbox[0], lonmax=self.bbox[2])
|
|
39
|
+
|
|
40
|
+
self.cellsize = 5.
|
|
41
|
+
|
|
42
|
+
super(EASE25CellGrid, self).__init__(lon=globgrid.arrlon,
|
|
43
|
+
lat=globgrid.arrlat,
|
|
44
|
+
subset=sgpis,
|
|
45
|
+
cells=lonlat2cell(globgrid.arrlon,
|
|
46
|
+
globgrid.arrlat,
|
|
47
|
+
self.cellsize),
|
|
48
|
+
shape=shape)
|
|
49
|
+
|
|
50
|
+
self.subset_shape = (len(np.unique(self.activearrlat)),
|
|
51
|
+
len(np.unique(self.activearrlon)))
|
|
52
|
+
|
|
53
|
+
def cut(self) -> CellGrid:
|
|
54
|
+
# create a new grid from the active subset
|
|
55
|
+
return BasicGrid(lon=self.activearrlon, lat=self.activearrlat,
|
|
56
|
+
gpis=self.activegpis, subset=None,
|
|
57
|
+
shape=self.subset_shape).to_cell_grid(self.cellsize)
|
smos/interface.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# The MIT License (MIT)
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2021, TU Wien
|
|
5
|
+
#
|
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
# furnished to do so, subject to the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
|
14
|
+
# all copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
# SOFTWARE.
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import warnings
|
|
26
|
+
import numpy as np
|
|
27
|
+
import pandas as pd
|
|
28
|
+
|
|
29
|
+
from pynetcf.time_series import GriddedNcOrthoMultiTs
|
|
30
|
+
from pygeogrids.netcdf import load_grid
|
|
31
|
+
|
|
32
|
+
from pygeobase.io_base import ImageBase, MultiTemporalImageBase
|
|
33
|
+
from pygeobase.object_base import Image
|
|
34
|
+
from datetime import timedelta, datetime
|
|
35
|
+
from netCDF4 import Dataset, date2num, num2date
|
|
36
|
+
from smos.grid import EASE25CellGrid
|
|
37
|
+
from smos import dist_name as subdist_name
|
|
38
|
+
from smos import dist_name, __version__
|
|
39
|
+
from collections import OrderedDict
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SMOSTs(GriddedNcOrthoMultiTs):
|
|
43
|
+
|
|
44
|
+
_t0_vars = {'sec': 'UTC_Seconds', 'days': 'Days'}
|
|
45
|
+
_t0_unit = 'days since 2000-01-01'
|
|
46
|
+
|
|
47
|
+
def __init__(self, ts_path, grid_path=None, index_add_time=False,
|
|
48
|
+
drop_missing=True, **kwargs):
|
|
49
|
+
"""
|
|
50
|
+
Class for reading SMOS time series after reshuffling images.
|
|
51
|
+
Missing images are represented in time series as lines where all
|
|
52
|
+
variables are NaN.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
ts_path : str
|
|
57
|
+
Directory where the netcdf time series files are stored
|
|
58
|
+
grid_path : str, optional (default: None)
|
|
59
|
+
Path to grid file, that is used to organize the location of time
|
|
60
|
+
series to read. If None is passed, grid.nc is searched for in the
|
|
61
|
+
ts_path.
|
|
62
|
+
index_add_time : bool, optional (default: False)
|
|
63
|
+
Add overpass time stamps to the data frame index. This needs the
|
|
64
|
+
'Days' and 'UTC_Seconds' variable available in the time series files.
|
|
65
|
+
drop_missing : bool, optional (default: True)
|
|
66
|
+
Drop Lines in TS where ALL variables are missing.
|
|
67
|
+
|
|
68
|
+
Optional keyword arguments that are passed to the Gridded Base:
|
|
69
|
+
------------------------------------------------------------------------
|
|
70
|
+
parameters : list, optional (default: None)
|
|
71
|
+
Specific variable names to read, if None are selected, all are read.
|
|
72
|
+
offsets : dict, optional (default:None)
|
|
73
|
+
Offsets (values) that are added to the parameters (keys)
|
|
74
|
+
scale_factors : dict, optional (default:None)
|
|
75
|
+
Offset (value) that the parameters (key) is multiplied with
|
|
76
|
+
ioclass_kws: dict
|
|
77
|
+
Optional keyword arguments to pass to OrthoMultiTs class:
|
|
78
|
+
----------------------------------------------------------------
|
|
79
|
+
read_bulk : boolean, optional (default:False)
|
|
80
|
+
if set to True the data of all locations is read into memory,
|
|
81
|
+
and subsequent calls to read_ts read from the cache and not from disk
|
|
82
|
+
this makes reading complete files faster#
|
|
83
|
+
read_dates : boolean, optional (default:False)
|
|
84
|
+
if false dates will not be read automatically but only on specific
|
|
85
|
+
request useable for bulk reading because currently the netCDF
|
|
86
|
+
num2date routine is very slow for big datasets
|
|
87
|
+
autofill : bool, (default: True)
|
|
88
|
+
Fill missing values with nans
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
if grid_path is None:
|
|
92
|
+
grid_path = os.path.join(ts_path, "grid.nc")
|
|
93
|
+
|
|
94
|
+
self.drop_missing = drop_missing
|
|
95
|
+
grid = load_grid(grid_path)
|
|
96
|
+
super(SMOSTs, self).__init__(ts_path, grid, **kwargs)
|
|
97
|
+
|
|
98
|
+
self.index_add_time = index_add_time
|
|
99
|
+
if (self.parameters is not None) and self.index_add_time:
|
|
100
|
+
for v in self._t0_vars.values():
|
|
101
|
+
self.parameters.append(v)
|
|
102
|
+
|
|
103
|
+
def _to_datetime(self, df:pd.DataFrame) -> pd.DataFrame:
|
|
104
|
+
# convert Days and UTC_Seconds to actual datetimes
|
|
105
|
+
units = self._t0_unit
|
|
106
|
+
|
|
107
|
+
df['_date'] = df.index.values
|
|
108
|
+
|
|
109
|
+
if (self._t0_vars['days'] not in df.columns) or \
|
|
110
|
+
(self._t0_vars['sec'] not in df.columns):
|
|
111
|
+
raise KeyError(f"Could not find {self._t0_vars['days']} or {self._t0_vars['sec']} "
|
|
112
|
+
f"in reshuffled data.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# days + (seconds / seconds per day)
|
|
116
|
+
num = df[self._t0_vars['days']].dropna() + (df[self._t0_vars['sec']].dropna() / 86400.)
|
|
117
|
+
if len(num) == 0:
|
|
118
|
+
df.loc[num.index, '_datetime_UTC'] = []
|
|
119
|
+
else:
|
|
120
|
+
df.loc[num.index, '_datetime_UTC'] = \
|
|
121
|
+
pd.DatetimeIndex(num2date(num.values, units=units,
|
|
122
|
+
calendar='standard', only_use_cftime_datetimes=False))
|
|
123
|
+
|
|
124
|
+
df = df.set_index('_datetime_UTC')
|
|
125
|
+
df = df[df.index.notnull()]
|
|
126
|
+
return df
|
|
127
|
+
|
|
128
|
+
def read(self, *args, **kwargs):
|
|
129
|
+
"""
|
|
130
|
+
Read time series by grid point index, or by lonlat. Convert columns to
|
|
131
|
+
ints if possible (if there are no Nans in it).
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
lon: float
|
|
136
|
+
Location longitude
|
|
137
|
+
lat : float
|
|
138
|
+
Location latitude
|
|
139
|
+
.. OR
|
|
140
|
+
gpi : int
|
|
141
|
+
Grid point Index
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
df : pd.DataFrame
|
|
146
|
+
Time Series data at the selected location
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
ts = super(SMOSTs, self).read(*args, **kwargs)
|
|
150
|
+
|
|
151
|
+
if self.drop_missing:
|
|
152
|
+
ts = ts.dropna(how='all')
|
|
153
|
+
|
|
154
|
+
for col in ts: # convert to ints, if possible
|
|
155
|
+
if (not np.isnan(ts[col]).any()) and \
|
|
156
|
+
(np.all(np.mod(ts[col].values, 1) == 0.)):
|
|
157
|
+
ts[col] = ts[col].astype(int)
|
|
158
|
+
|
|
159
|
+
if self.index_add_time:
|
|
160
|
+
ts = self._to_datetime(ts)
|
|
161
|
+
|
|
162
|
+
return ts
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class SMOSImg(ImageBase):
|
|
166
|
+
"""
|
|
167
|
+
Class for reading one SMOS netcdf image file.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
filename: str
|
|
172
|
+
filename of the SMOS nc image file
|
|
173
|
+
mode: str, optional (default: 'r')
|
|
174
|
+
mode of opening the file, only 'r' is implemented at the moment
|
|
175
|
+
parameters : str or list, optional (default: None)
|
|
176
|
+
one or list of parameters to read. We add 'Quality_Flags' if 'read_flags'
|
|
177
|
+
is not None. All parameters are described in docs/varnames.rst. If
|
|
178
|
+
None are passed, all parameters are read.
|
|
179
|
+
flatten: bool, optional (default: False)
|
|
180
|
+
If set then the data is read into 1D arrays. This is used to e.g
|
|
181
|
+
reshuffle the data.
|
|
182
|
+
grid : pygeogrids.CellGrid, optional (default: EASE25CellGrid)
|
|
183
|
+
Grid that the image data is organised on, by default the global EASE25
|
|
184
|
+
grid is used.
|
|
185
|
+
read_flags : tuple or None, optional (default: (0, 1))
|
|
186
|
+
Filter values to read based on the selected QUALITY_FLAGS.
|
|
187
|
+
Values for locations that are not assigned any of the here passed flags
|
|
188
|
+
are replaces with NaN (by default only the missing-data, i.e. flag=2,
|
|
189
|
+
locations are filtered out). If None is passed, no flags are considered.
|
|
190
|
+
float_fillval : float or None, optional (default: np.nan)
|
|
191
|
+
Fill Value for masked pixels, this is only applied to float variables.
|
|
192
|
+
Therefore e.g. mask variables are never filled but use the fill value
|
|
193
|
+
as in the data.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(self, filename, mode='r', parameters=None, flatten=False,
|
|
197
|
+
grid=EASE25CellGrid(bbox=None), read_flags=(0, 1), float_fillval=np.nan):
|
|
198
|
+
|
|
199
|
+
super(SMOSImg, self).__init__(filename, mode=mode)
|
|
200
|
+
|
|
201
|
+
if parameters is None:
|
|
202
|
+
parameters = []
|
|
203
|
+
if type(parameters) != list:
|
|
204
|
+
parameters = [parameters]
|
|
205
|
+
|
|
206
|
+
self.read_flags = read_flags
|
|
207
|
+
self.parameters = parameters
|
|
208
|
+
self.flatten = flatten
|
|
209
|
+
|
|
210
|
+
self.grid = grid
|
|
211
|
+
|
|
212
|
+
self.image_missing = False
|
|
213
|
+
self.img = None # to be loaded
|
|
214
|
+
self.glob_attrs = None
|
|
215
|
+
|
|
216
|
+
self.float_fillval = float_fillval
|
|
217
|
+
|
|
218
|
+
def get_global_attrs(self, exclude=('history', 'NCO', 'netcdf_version_id', 'contact',
|
|
219
|
+
'institution', 'creation_time')):
|
|
220
|
+
return {k: v for k, v in self.glob_attrs.items() if k not in exclude}
|
|
221
|
+
|
|
222
|
+
def _read_empty(self):
|
|
223
|
+
"""
|
|
224
|
+
Create an empty image for filling missing dates, this is necessary
|
|
225
|
+
for reshuffling as img2ts cannot handle missing days.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
empty_img : dict
|
|
230
|
+
Empty arrays of image size for each object parameter
|
|
231
|
+
"""
|
|
232
|
+
self.image_missing = True
|
|
233
|
+
|
|
234
|
+
return_img = {}
|
|
235
|
+
return_metadata = {}
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
rows, cols = self.grid.subset_shape
|
|
239
|
+
except AttributeError:
|
|
240
|
+
rows, cols = self.grid.shape
|
|
241
|
+
|
|
242
|
+
for param in self.parameters:
|
|
243
|
+
data = np.full((rows, cols), np.nan)
|
|
244
|
+
return_img[param] = data.flatten()
|
|
245
|
+
return_metadata[param] = {'image_missing': 1}
|
|
246
|
+
|
|
247
|
+
return return_img, return_metadata
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _read_img(self) -> (dict, dict):
|
|
251
|
+
raise NotImplementedError
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def read(self, timestamp):
|
|
255
|
+
"""
|
|
256
|
+
Read a single SMOS image, if it exists, otherwise fill an empty image
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
--------
|
|
260
|
+
timestamp : datetime
|
|
261
|
+
Time stamp for the image to read.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
if timestamp is None:
|
|
265
|
+
raise ValueError("No time stamp passed")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
return_img, return_metadata = self._read_img()
|
|
269
|
+
except IOError:
|
|
270
|
+
warnings.warn('Error loading image for {}, '
|
|
271
|
+
'generating empty image instead'.format(timestamp.date()))
|
|
272
|
+
return_img, return_metadata = self._read_empty()
|
|
273
|
+
|
|
274
|
+
if self.flatten:
|
|
275
|
+
self.img = Image(self.grid.activearrlon, self.grid.activearrlat,
|
|
276
|
+
return_img, return_metadata, timestamp)
|
|
277
|
+
|
|
278
|
+
else:
|
|
279
|
+
try:
|
|
280
|
+
shape = self.grid.subset_shape
|
|
281
|
+
except AttributeError:
|
|
282
|
+
shape = self.grid.shape
|
|
283
|
+
|
|
284
|
+
rows, cols = shape
|
|
285
|
+
for key in return_img:
|
|
286
|
+
return_img[key] = np.flipud(return_img[key].reshape(rows, cols))
|
|
287
|
+
|
|
288
|
+
self.img = Image(self.grid.activearrlon.reshape(rows, cols),
|
|
289
|
+
np.flipud(self.grid.activearrlat.reshape(rows, cols)),
|
|
290
|
+
return_img,
|
|
291
|
+
return_metadata,
|
|
292
|
+
timestamp)
|
|
293
|
+
return self.img
|
|
294
|
+
|
|
295
|
+
def write(self, image, **kwargs):
|
|
296
|
+
"""
|
|
297
|
+
Write the image to a separate output path. E.g. after reading only
|
|
298
|
+
a subset of the parameters, or when reading a spatial subset (with a
|
|
299
|
+
subgrid). If there is already a file, the new image is appended along
|
|
300
|
+
the time dimension.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
image : str
|
|
305
|
+
Path to netcdf file to create.
|
|
306
|
+
kwargs
|
|
307
|
+
Additional kwargs are given to netcdf4.Dataset
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
if self.img is None:
|
|
311
|
+
raise IOError("No data found for current image, load data first")
|
|
312
|
+
|
|
313
|
+
if self.img.timestamp is None:
|
|
314
|
+
raise IOError("No time stamp found for current image.")
|
|
315
|
+
|
|
316
|
+
lons = np.unique(self.img.lon.flatten())
|
|
317
|
+
lats = np.flipud(np.unique(self.img.lat.flatten()))
|
|
318
|
+
|
|
319
|
+
mode = 'w' if not os.path.isfile(image) else 'a'
|
|
320
|
+
ds = Dataset(image, mode=mode, **kwargs)
|
|
321
|
+
|
|
322
|
+
ds.set_auto_scale(True)
|
|
323
|
+
ds.set_auto_mask(True)
|
|
324
|
+
|
|
325
|
+
units = 'Days since 2000-01-01 00:00:00'
|
|
326
|
+
|
|
327
|
+
if mode == 'w':
|
|
328
|
+
ds.createDimension('timestamp', None) # stack dim
|
|
329
|
+
ds.createDimension('lat', len(lats))
|
|
330
|
+
ds.createDimension('lon', len(lons))
|
|
331
|
+
|
|
332
|
+
# this is not the obs time, but an image time stamp
|
|
333
|
+
ds.createVariable('timestamp', datatype=np.double, dimensions=('timestamp',),
|
|
334
|
+
zlib=True, chunksizes=None)
|
|
335
|
+
ds.createVariable('lat', datatype='float64', dimensions=('lat',), zlib=True)
|
|
336
|
+
ds.createVariable('lon', datatype='float64', dimensions=('lon',), zlib=True)
|
|
337
|
+
|
|
338
|
+
ds.variables['timestamp'].setncatts({'long_name': 'timestamp',
|
|
339
|
+
'units': units})
|
|
340
|
+
ds.variables['lat'].setncatts({'long_name': 'latitude', 'units': 'Degrees_North',
|
|
341
|
+
'valid_range': (-90, 90)})
|
|
342
|
+
ds.variables['lon'].setncatts({'long_name': 'longitude', 'units': 'Degrees_East',
|
|
343
|
+
'valid_range': (-180, 180)})
|
|
344
|
+
|
|
345
|
+
ds.variables['lon'][:] = lons
|
|
346
|
+
ds.variables['lat'][:] = lats
|
|
347
|
+
ds.variables['timestamp'][:] = np.array([])
|
|
348
|
+
|
|
349
|
+
this_global_attrs = \
|
|
350
|
+
OrderedDict([('subset_img_creation_time', str(datetime.now())),
|
|
351
|
+
('subset_img_bbox_corners_latlon', str(self.grid.bbox)),
|
|
352
|
+
('subset_software', f"{dist_name} | {subdist_name} | {__version__}")])
|
|
353
|
+
glob_attrs = self.glob_attrs
|
|
354
|
+
for k in ['ease_global', 'history', 'creation_time', 'NCO']:
|
|
355
|
+
try:
|
|
356
|
+
glob_attrs.pop(k)
|
|
357
|
+
except KeyError:
|
|
358
|
+
continue
|
|
359
|
+
glob_attrs.update(this_global_attrs)
|
|
360
|
+
ds.setncatts(glob_attrs)
|
|
361
|
+
|
|
362
|
+
idx = ds.variables['timestamp'].shape[0]
|
|
363
|
+
ds.variables['timestamp'][idx] = date2num(self.img.timestamp, units=units)
|
|
364
|
+
|
|
365
|
+
for var, vardata in self.img.data.items():
|
|
366
|
+
|
|
367
|
+
if var not in ds.variables.keys():
|
|
368
|
+
ds.createVariable(var, vardata.dtype, dimensions=('timestamp', 'lat', 'lon'),
|
|
369
|
+
zlib=True, complevel=6)
|
|
370
|
+
ds.variables[var].setncatts(self.img.metadata[var])
|
|
371
|
+
|
|
372
|
+
ds.variables[var][-1] = vardata
|
|
373
|
+
|
|
374
|
+
ds.close()
|
|
375
|
+
|
|
376
|
+
def read_masked_data(self, **kwargs):
|
|
377
|
+
raise NotImplementedError
|
|
378
|
+
|
|
379
|
+
def flush(self):
|
|
380
|
+
pass
|
|
381
|
+
|
|
382
|
+
def close(self):
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class SMOSDs(MultiTemporalImageBase):
|
|
387
|
+
"""
|
|
388
|
+
Class for reading SMOS images in nc format. Images are orgnaised in subdirs
|
|
389
|
+
for each year.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
data_path : str
|
|
394
|
+
Path to the nc files
|
|
395
|
+
parameter : str or list, optional (default: None)
|
|
396
|
+
one or list of parameters to read, see SMOS documentation
|
|
397
|
+
for more information (default: 'Soil_Moisture').
|
|
398
|
+
flatten: bool, optional (default: False)
|
|
399
|
+
If set then the data is read into 1D arrays. This is used to e.g
|
|
400
|
+
reshuffle the data.
|
|
401
|
+
grid : pygeogrids.CellGrid, optional (default: EASE25CellGrid)
|
|
402
|
+
Grid that the image data is organised on, by default the global EASE25
|
|
403
|
+
grid is used.
|
|
404
|
+
read_flags : tuple or None, optional (default: (0, 1))
|
|
405
|
+
Filter values to read based on the selected QUALITY_FLAGS.
|
|
406
|
+
Values for locations that are not assigned any of the here passed flags
|
|
407
|
+
are replaces with NaN (by default only the missing-data, i.e. flag=2,
|
|
408
|
+
locations are filtered out).
|
|
409
|
+
float_fillval : float or None, optional (default: np.nan)
|
|
410
|
+
Fill Value for masked pixels, this is only applied to float variables.
|
|
411
|
+
Therefore e.g. mask variables are never filled but use the fill value
|
|
412
|
+
as in the data.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def __init__(self, data_path, ioclass, parameters=None, flatten=False,
|
|
417
|
+
grid=EASE25CellGrid(bbox=None), filename_templ=None,
|
|
418
|
+
read_flags=(0, 1), float_fillval=np.nan, additional_kws=None):
|
|
419
|
+
|
|
420
|
+
ioclass_kws = {'parameters': parameters,
|
|
421
|
+
'flatten': flatten,
|
|
422
|
+
'grid': grid,
|
|
423
|
+
'read_flags': read_flags,
|
|
424
|
+
'float_fillval': float_fillval}
|
|
425
|
+
|
|
426
|
+
if additional_kws:
|
|
427
|
+
ioclass_kws.update(additional_kws)
|
|
428
|
+
|
|
429
|
+
sub_path = ['%Y']
|
|
430
|
+
|
|
431
|
+
super().__init__(data_path,
|
|
432
|
+
ioclass=ioclass,
|
|
433
|
+
fname_templ=filename_templ,
|
|
434
|
+
datetime_format="%Y%m%d",
|
|
435
|
+
subpath_templ=sub_path,
|
|
436
|
+
exact_templ=False,
|
|
437
|
+
ioclass_kws=ioclass_kws)
|
|
438
|
+
|
|
439
|
+
def _assemble_img(self, timestamp, mask=False, **kwargs):
|
|
440
|
+
img = None
|
|
441
|
+
try:
|
|
442
|
+
filepath = self._build_filename(timestamp)
|
|
443
|
+
except IOError:
|
|
444
|
+
filepath = None
|
|
445
|
+
|
|
446
|
+
if self._open(filepath):
|
|
447
|
+
kwargs['timestamp'] = timestamp
|
|
448
|
+
if mask is False:
|
|
449
|
+
img = self.fid.read(**kwargs)
|
|
450
|
+
else:
|
|
451
|
+
img = self.fid.read_masked_data(**kwargs)
|
|
452
|
+
|
|
453
|
+
return img
|
|
454
|
+
|
|
455
|
+
def read(self, timestamp, **kwargs):
|
|
456
|
+
return self._assemble_img(timestamp, **kwargs)
|
|
457
|
+
|
|
458
|
+
def write_multiple(self, root_path, start_date, end_date, stackfile='stack.nc',
|
|
459
|
+
**kwargs):
|
|
460
|
+
"""
|
|
461
|
+
Create multiple netcdf files or a netcdf stack in the passed directoy for
|
|
462
|
+
a range of time stamps. Note that stacking gets slower when the stack gets larger.
|
|
463
|
+
Empty images (if no original data can be loaded) are excluded here as
|
|
464
|
+
well.
|
|
465
|
+
|
|
466
|
+
Parameters
|
|
467
|
+
----------
|
|
468
|
+
root : str
|
|
469
|
+
Directory where the files / the stack are/is stored
|
|
470
|
+
start_date : datetime
|
|
471
|
+
Start date of images to write down
|
|
472
|
+
end_date
|
|
473
|
+
Last date of images to write down
|
|
474
|
+
stackfile : str, optional (default: 'stack.nc')
|
|
475
|
+
Name of the stack file to create in root_path. If no name is passed
|
|
476
|
+
we create single images instead of a stack with the same name as
|
|
477
|
+
the original images (faster).
|
|
478
|
+
kwargs:
|
|
479
|
+
kwargs that are passed to the image reading function
|
|
480
|
+
"""
|
|
481
|
+
timestamps = self.tstamps_for_daterange(start_date, end_date)
|
|
482
|
+
for t in timestamps:
|
|
483
|
+
self.read(t, **kwargs)
|
|
484
|
+
if self.fid.image_missing:
|
|
485
|
+
continue
|
|
486
|
+
if stackfile is None:
|
|
487
|
+
subdir = os.path.join(root_path, str(t.year))
|
|
488
|
+
if not os.path.exists(subdir): os.makedirs(subdir)
|
|
489
|
+
filepath = os.path.join(subdir, os.path.basename(self.fid.filename))
|
|
490
|
+
else:
|
|
491
|
+
filepath = os.path.join(root_path, stackfile)
|
|
492
|
+
print(f"{'Write' if not stackfile else 'Stack'} image for {str(t)}...")
|
|
493
|
+
self.fid.write(filepath)
|
|
494
|
+
|
|
495
|
+
def tstamps_for_daterange(self, start_date, end_date):
|
|
496
|
+
"""
|
|
497
|
+
return timestamps for daterange,
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
start_date: datetime.datetime
|
|
501
|
+
start of date range
|
|
502
|
+
end_date: datetime.datetime
|
|
503
|
+
end of date range
|
|
504
|
+
Returns
|
|
505
|
+
-------
|
|
506
|
+
timestamps : list
|
|
507
|
+
list of datetime objects of each available image between
|
|
508
|
+
start_date and end_date
|
|
509
|
+
"""
|
|
510
|
+
img_offsets = np.array([timedelta(hours=0)])
|
|
511
|
+
|
|
512
|
+
timestamps = []
|
|
513
|
+
diff = end_date - start_date
|
|
514
|
+
for i in range(diff.days + 1):
|
|
515
|
+
daily_dates = start_date + timedelta(days=i) + img_offsets
|
|
516
|
+
timestamps.extend(daily_dates.tolist())
|
|
517
|
+
|
|
518
|
+
return timestamps
|
smos/misc.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import os
|
|
3
|
+
from datetime import date
|
|
4
|
+
import typing as t
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
def _get_first_and_last_file(path: str):
|
|
8
|
+
# Get list of all years (folders) in the path
|
|
9
|
+
years = sorted([folder for folder in os.listdir(path) if folder.isdigit()], key=int)
|
|
10
|
+
|
|
11
|
+
if not years:
|
|
12
|
+
return None, None
|
|
13
|
+
|
|
14
|
+
# Get the first year and last year
|
|
15
|
+
first_year = years[0]
|
|
16
|
+
last_year = years[-1]
|
|
17
|
+
|
|
18
|
+
# Handle the first year
|
|
19
|
+
first_year_path = os.path.join(path, first_year)
|
|
20
|
+
first_months = sorted([folder for folder in os.listdir(first_year_path) if folder.isdigit()], key=int)
|
|
21
|
+
|
|
22
|
+
if first_months:
|
|
23
|
+
first_month = first_months[0]
|
|
24
|
+
first_month_path = os.path.join(first_year_path, first_month)
|
|
25
|
+
first_days = sorted([folder for folder in os.listdir(first_month_path) if folder.isdigit()], key=int)
|
|
26
|
+
|
|
27
|
+
if first_days:
|
|
28
|
+
first_day = first_days[0]
|
|
29
|
+
first_day_path = os.path.join(first_month_path, first_day)
|
|
30
|
+
first_files = sorted(os.listdir(first_day_path))
|
|
31
|
+
first_file = first_files[0] if first_files else None
|
|
32
|
+
else:
|
|
33
|
+
first_day_path = first_month_path
|
|
34
|
+
first_files = sorted(os.listdir(first_day_path))
|
|
35
|
+
first_file = first_files[0] if first_files else None
|
|
36
|
+
else:
|
|
37
|
+
first_month_path = first_year_path
|
|
38
|
+
first_files = sorted(os.listdir(first_month_path))
|
|
39
|
+
first_file = first_files[0] if first_files else None
|
|
40
|
+
|
|
41
|
+
# Handle the last year
|
|
42
|
+
last_year_path = os.path.join(path, last_year)
|
|
43
|
+
last_months = sorted([folder for folder in os.listdir(last_year_path) if folder.isdigit()], key=int, reverse=True)
|
|
44
|
+
|
|
45
|
+
if last_months:
|
|
46
|
+
last_month = last_months[0]
|
|
47
|
+
last_month_path = os.path.join(last_year_path, last_month)
|
|
48
|
+
last_days = sorted([folder for folder in os.listdir(last_month_path) if folder.isdigit()], key=int, reverse=True)
|
|
49
|
+
|
|
50
|
+
if last_days:
|
|
51
|
+
last_day = last_days[0]
|
|
52
|
+
last_day_path = os.path.join(last_month_path, last_day)
|
|
53
|
+
last_files = sorted(os.listdir(last_day_path))
|
|
54
|
+
else:
|
|
55
|
+
last_day_path = last_month_path
|
|
56
|
+
last_files = sorted(os.listdir(last_day_path))
|
|
57
|
+
else:
|
|
58
|
+
last_month_path = last_year_path
|
|
59
|
+
last_files = sorted(os.listdir(last_month_path))
|
|
60
|
+
|
|
61
|
+
return first_file, last_files[-1] if last_files else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_date(f: str) -> t.Union[date, None]:
|
|
65
|
+
for e in f.split('_'):
|
|
66
|
+
try:
|
|
67
|
+
dt = pd.to_datetime(e).to_pydatetime().date()
|
|
68
|
+
return dt
|
|
69
|
+
except Exception:
|
|
70
|
+
continue
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_first_last_day_images(img_path: str) -> \
|
|
75
|
+
(t.Union[date, None], t.Union[date, None]):
|
|
76
|
+
f, l = _get_first_and_last_file(img_path)
|
|
77
|
+
first_day = _get_date(f) if f is not None else f
|
|
78
|
+
last_day = _get_date(l) if l is not None else f
|
|
79
|
+
|
|
80
|
+
return first_day, last_day
|
|
81
|
+
|
|
82
|
+
def read_summary_yml(path: str) -> dict:
|
|
83
|
+
"""
|
|
84
|
+
Read image summary and return fields as dict.
|
|
85
|
+
"""
|
|
86
|
+
path = os.path.join(path, 'overview.yml')
|
|
87
|
+
|
|
88
|
+
with open(path, 'r') as stream:
|
|
89
|
+
props = yaml.safe_load(stream)
|
|
90
|
+
|
|
91
|
+
return props
|