pyVPRM 3.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.
Files changed (42) hide show
  1. pyVPRM/VPRM.py +1118 -0
  2. pyVPRM/__init__.py +1 -0
  3. pyVPRM/lib/__init__.py +0 -0
  4. pyVPRM/lib/downmodis.py +1012 -0
  5. pyVPRM/lib/fancy_plot.py +75 -0
  6. pyVPRM/lib/flux_tower_class.py +444 -0
  7. pyVPRM/lib/fluxnet_info/fluxnet_sites.pkl +0 -0
  8. pyVPRM/lib/fluxnet_info/site_infos.txt +218 -0
  9. pyVPRM/lib/functions.py +471 -0
  10. pyVPRM/meteorologies/__init__.py +0 -0
  11. pyVPRM/meteorologies/era5_class_dkrz.py +279 -0
  12. pyVPRM/meteorologies/era5_class_draft.py +60 -0
  13. pyVPRM/meteorologies/era5_monthly_xr.py +149 -0
  14. pyVPRM/meteorologies/met_base_class.py +67 -0
  15. pyVPRM/meteorologies/met_local_measurement.py +56 -0
  16. pyVPRM/sat_managers/__init__.py +0 -0
  17. pyVPRM/sat_managers/base_manager.py +430 -0
  18. pyVPRM/sat_managers/city.py +21 -0
  19. pyVPRM/sat_managers/copernicus.py +29 -0
  20. pyVPRM/sat_managers/esa_world_cover.py +21 -0
  21. pyVPRM/sat_managers/mapbiomas.py +35 -0
  22. pyVPRM/sat_managers/modis.py +266 -0
  23. pyVPRM/sat_managers/proba_v.py +86 -0
  24. pyVPRM/sat_managers/sentinel2.py +192 -0
  25. pyVPRM/sat_managers/synmap.py +21 -0
  26. pyVPRM/sat_managers/viirs.py +232 -0
  27. pyVPRM/sat_managers/viirs09ga.py +173 -0
  28. pyVPRM/vprm_configs/__init__.py +0 -0
  29. pyVPRM/vprm_configs/copernicus_land_cover.yaml +94 -0
  30. pyVPRM/vprm_configs/esa_world_cover.yaml +71 -0
  31. pyVPRM/vprm_configs/synmap.yaml +106 -0
  32. pyVPRM/vprm_models/__init__.py +1 -0
  33. pyVPRM/vprm_models/model_params/__init__.py +0 -0
  34. pyVPRM/vprm_models/vprm_base.py +691 -0
  35. pyVPRM/vprm_models/vprm_base_no_xeric.py +585 -0
  36. pyVPRM/vprm_models/vprm_modified.py +530 -0
  37. pyVPRM/vprm_models/vprm_nn.py +130 -0
  38. pyVPRM-3.0.dist-info/LICENSE +21 -0
  39. pyVPRM-3.0.dist-info/METADATA +40 -0
  40. pyVPRM-3.0.dist-info/RECORD +42 -0
  41. pyVPRM-3.0.dist-info/WHEEL +5 -0
  42. pyVPRM-3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,430 @@
1
+ # from sentinelsat import SentinelAPI, read_geojson, geojson_to_wkt
2
+ import sys
3
+ import os
4
+ import time
5
+ from shapely.geometry import Point, Polygon, box
6
+ import rioxarray as rxr
7
+ import zipfile
8
+ import glob
9
+ from pyproj import Transformer
10
+ import geopandas as gpd
11
+ from pyVPRM.lib import downmodis
12
+ import math
13
+ from pyproj import Proj
14
+ from affine import Affine
15
+ from rioxarray.rioxarray import affine_to_coords
16
+ import requests
17
+ from requests.auth import HTTPDigestAuth
18
+ from rioxarray import merge
19
+ import yaml
20
+ import warnings
21
+
22
+ warnings.filterwarnings("ignore")
23
+ from matplotlib.colors import LinearSegmentedColormap
24
+ from lxml import etree
25
+ from datetime import datetime
26
+ from rasterio.warp import calculate_default_transform
27
+ import h5py
28
+ from dateutil import parser
29
+ import xarray as xr
30
+ from datetime import datetime, timedelta, date
31
+ import numpy as np
32
+ from loguru import logger
33
+
34
+ def geodesic_point_buffer(lat, lon, km):
35
+ buf = Point(0, 0).buffer(km * 1000) # distance in metres
36
+ t = Transformer.from_crs(
37
+ "+proj=aeqd +lat_0={lat} +lon_0={lon} +x_0=0 +y_0=0".format(lat=lat, lon=lon),
38
+ "+proj=longlat +datum=WGS84",
39
+ )
40
+ ret = [t.transform(i[0], i[1]) for i in buf.exterior.coords[:]]
41
+ return ret
42
+
43
+
44
+ def make_cmap(vmin, vmax, nbins, cmap_name="Reds"):
45
+ if isinstance(cmap_name, list):
46
+ cmap = LinearSegmentedColormap.from_list("some_name", cmap_name, N=nbins)
47
+ else:
48
+ cmap = mpl.cm.get_cmap(cmap_name) # define the colormap
49
+ # extract all colors from the .jet map
50
+ cmaplist = [cmap(i) for i in range(cmap.N)]
51
+
52
+ # create the new map
53
+ cmap = mpl.colors.LinearSegmentedColormap.from_list(
54
+ "Custom cmap", cmaplist, cmap.N
55
+ )
56
+ # cmap.set_under('blue') #'#d4ebf2')
57
+
58
+ # define the bins and normalize
59
+ bounds = np.linspace(vmin, vmax, nbins)
60
+ norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
61
+ return cmap, norm
62
+
63
+
64
+ class satellite_data_manager:
65
+ # Base class for all satellite images
66
+
67
+ def __init__(self, datapath=None, sat_image_path=None, sat_img=None):
68
+ self.bands = None
69
+ self.outpath = datapath
70
+ self.sat_image_path = sat_image_path
71
+ self.sat_img = sat_img
72
+ if isinstance(self.sat_img, str):
73
+ self.sat_img = xr.open_dataset(self.sat_img)
74
+ self.bands = list(self.sat_img.keys())
75
+ self.t = None
76
+
77
+ return
78
+
79
+ def value_at_lonlat(
80
+ self, lon, lat, as_array=True, key=None, isel={}, interp_method="nearest"
81
+ ):
82
+ """
83
+ Get the value at a specific latitude (lat) and longitude (lon)
84
+
85
+ Parameters:
86
+ lon(float or list of floats) : longitude
87
+ lat(float or list of floats) : latitude
88
+ as_array (bool): If true, return numpy array
89
+ key (str): Only get values for a specific key. All keys if None
90
+ interp_method (str): which method to use for interpolating. 'linear' by default.
91
+ For nearest neighbour set to 'nearest'.
92
+
93
+ Returns:
94
+ None
95
+ """
96
+
97
+ if self.t is None:
98
+ self.t = Transformer.from_crs(
99
+ "+proj=longlat +datum=WGS84", self.sat_img.rio.crs
100
+ )
101
+ x_a, y_a = self.t.transform(lon, lat)
102
+ # ret = self.sat_img.sel(x=x_a, y=y_a, method="nearest")
103
+ if key is not None:
104
+ if isinstance(x_a, list):
105
+ ret = (
106
+ self.sat_img[key]
107
+ .isel(isel)
108
+ .interp(x=("z", x_a), y=("z", y_a), method=interp_method)
109
+ )
110
+ else:
111
+ ret = (
112
+ self.sat_img[key]
113
+ .isel(isel)
114
+ .interp(x=x_a, y=y_a, method=interp_method)
115
+ )
116
+ else:
117
+ if isinstance(x_a, list) | isinstance(x_a, np.ndarray):
118
+ ret = self.sat_img.isel(isel).interp(
119
+ x=("z", x_a), y=("z", y_a), method=interp_method
120
+ )
121
+ else:
122
+ ret = self.sat_img.isel(isel).interp(x=x_a, y=y_a, method=interp_method)
123
+ if as_array:
124
+ return ret.to_array().values
125
+ else:
126
+ return ret
127
+
128
+ def drop_bands(self, band_names=None):
129
+ if band_names is not None:
130
+ self.sat_img = self.sat_img.drop(band_names)
131
+ else:
132
+ if self.bands is None:
133
+ self.set_band_names()
134
+ self.sat_img = self.sat_img.drop(self.bands)
135
+
136
+ def reduce_along_lon_lat(
137
+ self, lon, lat, interp_method="nearest", new_dim_name="z", inplace=True
138
+ ):
139
+ if self.t is None:
140
+ self.t = Transformer.from_crs(
141
+ "+proj=longlat +datum=WGS84", self.sat_img.rio.crs
142
+ )
143
+ x_a, y_a = self.t.transform(lon, lat)
144
+ ret = self.sat_img.interp(
145
+ x=(new_dim_name, x_a), y=(new_dim_name, y_a), method=interp_method
146
+ )
147
+ if inplace:
148
+ self.sat_img = ret
149
+ return
150
+ else:
151
+ return ret
152
+
153
+ def load(self, proj=None, **kwargs):
154
+ # loading using the individual loading functions
155
+ # of the derived classes
156
+
157
+ self.individual_loading(**kwargs)
158
+ if proj is not None:
159
+ self.reproject(proj=proj)
160
+ return
161
+
162
+ def get_band_names(self):
163
+ return self.keys
164
+
165
+ def reproject(self, proj, **kwargs):
166
+ # reproject using a pre-defined projection or by passing a
167
+ # projection string
168
+
169
+ if proj == "WGS84":
170
+ self.sat_img = self.sat_img.rio.reproject(
171
+ "+proj=longlat +datum=WGS84", **kwargs
172
+ ) #
173
+ elif proj == "CRS":
174
+ self.sat_img = self.sat_img.rio.reproject(
175
+ self.sat_img.rio.estimate_utm_crs(), **kwargs
176
+ )
177
+ else:
178
+ self.sat_img = self.sat_img.rio.reproject(proj, **kwargs)
179
+ try:
180
+ self.proj_dict = self.sat_img.rio.crs.to_dict()
181
+ except Exception as e:
182
+ self.proj_dict = {}
183
+ logger.info(e)
184
+ self.t = Transformer.from_crs(
185
+ "+proj=longlat +datum=WGS84", self.sat_img.rio.crs
186
+ )
187
+ return
188
+
189
+ def transform_to_grid(self, sat_manager_inst):
190
+
191
+ import xesmf as xe
192
+
193
+ src_x = self.sat_img.coords["x"].values
194
+ src_y = self.sat_img.coords["y"].values
195
+ X, Y = np.meshgrid(src_x, src_y)
196
+ t = Transformer.from_crs(self.sat_img.rio.crs, "+proj=longlat +datum=WGS84")
197
+ x_long, y_lat = t.transform(X, Y)
198
+ src_grid = xr.Dataset(
199
+ {
200
+ "lon": (["y", "x"], x_long, {"units": "degrees_east"}),
201
+ "lat": (["y", "x"], y_lat, {"units": "degrees_north"}),
202
+ }
203
+ )
204
+ src_grid = src_grid.set_coords(["lon", "lat"])
205
+
206
+ src_x = sat_manager_inst.sat_img.coords["x"].values
207
+ src_y = sat_manager_inst.sat_img.coords["y"].values
208
+ X, Y = np.meshgrid(src_x, src_y)
209
+ t = Transformer.from_crs(
210
+ sat_manager_inst.sat_img.rio.crs, "+proj=longlat +datum=WGS84"
211
+ )
212
+ x_long, y_lat = t.transform(X, Y)
213
+ dest_grid = xr.Dataset(
214
+ {
215
+ "lon": (["y", "x"], x_long, {"units": "degrees_east"}),
216
+ "lat": (["y", "x"], y_lat, {"units": "degrees_north"}),
217
+ }
218
+ )
219
+ dest_grid = dest_grid.set_coords(["lon", "lat"])
220
+
221
+ regridder = xe.Regridder(src_grid, dest_grid, "bilinear")
222
+ self.sat_img = regridder(self.sat_img)
223
+ self.sat_img = self.sat_img.assign_coords(
224
+ {
225
+ "x": sat_manager_inst.sat_img.coords["x"].values,
226
+ "y": sat_manager_inst.sat_img.coords["y"].values,
227
+ }
228
+ )
229
+ self.sat_img.rio.set_crs(sat_manager_inst.sat_img.rio.crs)
230
+ return
231
+
232
+ def plot_bands(self, cmaps="Reds", titles=None, save=None):
233
+ # plot the bands of the satellite image
234
+ pass
235
+
236
+ def plot_rgb(self, which_bands=[2, 1, 0], str_clip=2, figsize=0.9, save=None):
237
+ # plot an rgb image with the bands given in which_bands
238
+ pass
239
+
240
+ def plot_ndvi(
241
+ self, band1, band2, figsize=0.9, save=None, n_colors=9, vmin=None, vmax=1.0
242
+ ):
243
+ # plot the normalized difference vegetation index
244
+ pass
245
+
246
+ def add_tile(self, new_tiles, reproject=False):
247
+ # merge tiles together using the projection of the current satellite image
248
+
249
+ if not isinstance(new_tiles, list):
250
+ new_tiles = [new_tiles]
251
+ if not np.all([isinstance(i, satellite_data_manager) for i in new_tiles]):
252
+ logger.info("Can only merge with another instance of a satellite_data_manger")
253
+ if reproject:
254
+ logger.info("Do reprojections")
255
+ to_merge = [
256
+ i.sat_img.rio.reproject(self.sat_img.rio.crs) for i in new_tiles
257
+ ]
258
+ else:
259
+ to_merge = [i.sat_img for i in new_tiles]
260
+ to_merge.append(self.sat_img)
261
+ logger.info("Merge")
262
+ self.sat_img = merge.merge_datasets(to_merge)
263
+ return
264
+
265
+ def crop_box(self, box, from_disk=False):
266
+ if not box.crs == self.sat_img.rio.crs:
267
+ # If the crs is not equal reproject the data
268
+ box = box.to_crs(self.sat_img.rio.crs)
269
+ self.sat_img = self.sat_img.rio.clip(
270
+ box, all_touched=True, from_disk=from_disk
271
+ ).squeeze()
272
+
273
+ def crop(self, lonlat, radius, from_disk=False):
274
+ # crop the satellite images in place using a given radius around a given
275
+ # longitude and latitude
276
+
277
+ circle_poly = gpd.GeoSeries(
278
+ Polygon(geodesic_point_buffer(lonlat[1], lonlat[0], radius)), crs="WGS 84"
279
+ )
280
+ if not circle_poly.crs == self.sat_img.rio.crs:
281
+ # If the crs is not equal reproject the data
282
+ circle_poly = circle_poly.to_crs(self.sat_img.rio.crs)
283
+
284
+ crop_bound_box = [box(*circle_poly.total_bounds)]
285
+
286
+ self.sat_img = self.sat_img.rio.clip(
287
+ [box(*circle_poly.total_bounds)], all_touched=True, from_disk=from_disk
288
+ ).squeeze()
289
+ return
290
+
291
+ def crop_to_polygon(self, polygon, from_disk=False):
292
+ if not polygon.crs == self.sat_img.rio.crs:
293
+ polygon = polygon.to_crs(self.sat_img.rio.crs)
294
+
295
+ self.sat_img = self.sat_img.rio.clip(
296
+ polygon.geometry, all_touched=True, from_disk=from_disk
297
+ ).squeeze()
298
+ return
299
+
300
+ def crop_to_number_of_pixels(self, lonlat, num_pixels, key, reproject=False):
301
+ # crop the satellite images in place to a certain number of pixels around
302
+ # the given longitude and latitude
303
+
304
+ if (self.sat_img.rio.crs != self.sat_img.rio.estimate_utm_crs()) & reproject:
305
+ self.sat_img = self.sat_img.rio.reproject(
306
+ self_sat.img.rio.estimate_utm_crs()
307
+ )
308
+ t = Transformer.from_crs("+proj=longlat +datum=WGS84", self.sat_img.rio.crs)
309
+ x_a, y_a = t.transform(lonlat[0], lonlat[1])
310
+ x_ind = np.argmin(np.abs(self.sat_img.x.values - x_a))
311
+ y_ind = np.argmin(np.abs(self.sat_img.y.values - y_a))
312
+ shift = int(np.floor(num_pixels / 2.0))
313
+ return self.sat_img[key].values[
314
+ y_ind - shift : y_ind + shift + 1, x_ind - shift : x_ind + shift + 1
315
+ ]
316
+
317
+ def individual_loading(self):
318
+ return
319
+
320
+ def save(self, save_path):
321
+ # save satellite image to save_path
322
+
323
+ self.sat_img.to_netcdf(save_path)
324
+ return
325
+
326
+
327
+ class earthdata(satellite_data_manager):
328
+ # Classes for everything that can be downloaded from the NASA
329
+ # earthdata server, especially MODIS and VIIRS
330
+
331
+ def __init__(self, datapath=None, sat_image_path=None, sat_img=None):
332
+ super().__init__(datapath, sat_image_path, sat_img)
333
+ return
334
+
335
+ def _init_downloader(
336
+ self,
337
+ dest,
338
+ date,
339
+ delta,
340
+ username,
341
+ lonlat=None,
342
+ pwd=None,
343
+ token=None,
344
+ jpg=False,
345
+ enddate=None,
346
+ hv=None,
347
+ ):
348
+ # Downloader with interface to the Earthdata server
349
+
350
+ if hv is not None:
351
+ h = hv[0]
352
+ v = hv[1]
353
+ else:
354
+ h, v = self.lat_lon_to_modis(lonlat[1], lonlat[0])
355
+ day = "{}-{:02d}-{:02d}".format(date.year, date.month, date.day)
356
+ tiles = "h{:02d}v{:02d}".format(h, v)
357
+
358
+ downloader = downmodis.downModis(
359
+ destinationFolder=dest,
360
+ tiles=tiles,
361
+ today=day,
362
+ product=self.product,
363
+ path=self.path,
364
+ delta=delta,
365
+ user=username,
366
+ enddate=enddate,
367
+ password=pwd,
368
+ token=token,
369
+ jpg=jpg,
370
+ )
371
+
372
+ return downloader
373
+
374
+ def download(
375
+ self,
376
+ date,
377
+ savepath,
378
+ username,
379
+ lonlat=None,
380
+ pwd=None,
381
+ token=None,
382
+ delta=1,
383
+ jpg=False,
384
+ enddate=None,
385
+ hv=None,
386
+ rmv_downloads=False,
387
+ ):
388
+
389
+ modisDown = self._init_downloader(
390
+ savepath, date, delta, username, lonlat, pwd, token, jpg, enddate, hv
391
+ )
392
+
393
+ # try:
394
+ modisDown.connect()
395
+ ds = modisDown.getListDays()
396
+ for d in ds:
397
+ fs = modisDown.getFilesList(d)
398
+ cde = modisDown.checkDataExist(fs)
399
+ logger.info("Download {}: {}".format(d, cde))
400
+ for c in cde:
401
+ os.system(
402
+ "wget --user {} --password {} --directory-prefix {} {} ".format(
403
+ modisDown.user,
404
+ modisDown.password,
405
+ modisDown.writeFilePath,
406
+ os.path.join(modisDown.url, modisDown.path, d, c),
407
+ )
408
+ )
409
+ time.sleep(5)
410
+ # modisDown.dayDownload(d, cde)
411
+ return
412
+
413
+ def _to_standard_format(self):
414
+ return
415
+
416
+ def lat_lon_to_modis(self, lat, lon):
417
+ CELLS = 2400
418
+ VERTICAL_TILES = 18
419
+ HORIZONTAL_TILES = 36
420
+ EARTH_RADIUS = 6371007.181
421
+ EARTH_WIDTH = 2 * math.pi * EARTH_RADIUS
422
+
423
+ TILE_WIDTH = EARTH_WIDTH / HORIZONTAL_TILES
424
+ TILE_HEIGHT = TILE_WIDTH
425
+ CELL_SIZE = TILE_WIDTH / CELLS
426
+ MODIS_GRID = Proj(f"+proj=sinu +R={EARTH_RADIUS} +nadgrids=@null +wktext")
427
+ x, y = MODIS_GRID(lon, lat)
428
+ h = (EARTH_WIDTH * 0.5 + x) / TILE_WIDTH
429
+ v = -(EARTH_WIDTH * 0.25 + y - (VERTICAL_TILES - 0) * TILE_HEIGHT) / TILE_HEIGHT
430
+ return int(h), int(v)
@@ -0,0 +1,21 @@
1
+ from pyVPRM.sat_managers.base_manager import satellite_data_manager
2
+ import rioxarray as rxr
3
+ import numpy as np
4
+
5
+
6
+ class city_land_cover_map(satellite_data_manager):
7
+
8
+ def __init__(self, sat_image_path):
9
+ super().__init__()
10
+ self.load_kwargs = {}
11
+ self.sat_image_path = sat_image_path
12
+ self.resolution = 10.0
13
+
14
+ def get_resolution(self):
15
+ return self.resolution
16
+
17
+ def individual_loading(self):
18
+ self.sat_img = rxr.open_rasterio(
19
+ self.sat_image_path, band_as_variable=True
20
+ ).squeeze()
21
+ self.keys = np.array(list(self.sat_img.data_vars))
@@ -0,0 +1,29 @@
1
+ from pyVPRM.sat_managers.base_manager import satellite_data_manager
2
+ import rioxarray as rxr
3
+ import numpy as np
4
+
5
+
6
+ class copernicus_land_cover_map(satellite_data_manager):
7
+ # Class to load the copernicus land cover map
8
+ # To get the data, download for example from
9
+ # here: https://lcviewer.vito.be/download
10
+
11
+ def __init__(self, sat_image_path):
12
+ super().__init__()
13
+ self.load_kwargs = {}
14
+ self.sat_image_path = sat_image_path
15
+ self.resolution = 92.2256325412261
16
+
17
+ def get_resolution(self):
18
+ return self.resolution
19
+
20
+ def individual_loading(self):
21
+ try:
22
+ self.sat_img = rxr.open_rasterio(
23
+ self.sat_image_path, masked=True, band_as_variable=True, cache=False
24
+ ).squeeze()
25
+ except:
26
+ self.sat_img = rxr.open_rasterio(
27
+ self.sat_image_path, masked=True, cache=False
28
+ ).squeeze()
29
+ self.keys = np.array(list(self.sat_img.data_vars))
@@ -0,0 +1,21 @@
1
+ from pyVPRM.sat_managers.base_manager import satellite_data_manager
2
+ import rioxarray as rxr
3
+ import numpy as np
4
+
5
+
6
+ class esa_world_cover(satellite_data_manager):
7
+
8
+ def __init__(self, sat_image_path):
9
+ super().__init__()
10
+ self.load_kwargs = {}
11
+ self.sat_image_path = sat_image_path
12
+ self.resolution = 10.0
13
+
14
+ def get_resolution(self):
15
+ return self.resolution
16
+
17
+ def individual_loading(self):
18
+ self.sat_img = rxr.open_rasterio(
19
+ self.sat_image_path, band_as_variable=True
20
+ ).squeeze()
21
+ self.keys = np.array(list(self.sat_img.data_vars))
@@ -0,0 +1,35 @@
1
+ from pyVPRM.sat_managers.base_manager import satellite_data_manager
2
+ import rioxarray as rxr
3
+ import numpy as np
4
+ import xarray as xr
5
+
6
+
7
+ class mapbiomas(satellite_data_manager):
8
+ # Class to load the copernicus land cover map
9
+ # To get the data, download for example from
10
+ # here: https://lcviewer.vito.be/download
11
+
12
+ def __init__(self, sat_image_path):
13
+ super().__init__()
14
+ self.load_kwargs = {}
15
+ self.sat_image_path = sat_image_path
16
+ self.resolution = 30
17
+
18
+ def get_resolution(self):
19
+ return self.resolution
20
+
21
+ def individual_loading(self):
22
+ self.sat_img = xr.open_dataset(self.sat_image_path)
23
+ self.sat_img = self.sat_img.rio.write_crs("WGS84")
24
+ self.sat_img = self.sat_img.rename({"band_data": "band_1"})
25
+ self.sat_img["band_1"] = xr.where(
26
+ np.isnan(self.sat_img["band_1"]), 8, self.sat_img["band_1"]
27
+ )
28
+ # try:
29
+ # self.sat_img = rxr.open_rasterio(self.sat_image_path,
30
+ # masked=True, band_as_variable=True,
31
+ # cache=False).squeeze()
32
+ # except:
33
+ # self.sat_img = rxr.open_rasterio(self.sat_image_path,
34
+ # masked=True, cache=False).squeeze()
35
+ self.keys = np.array(list(self.sat_img.data_vars))