meteo-utils 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,454 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Mon Apr 2 16:48:56 2018
4
+
5
+ @author: hector
6
+ """
7
+
8
+ import glob
9
+ import os
10
+ import os.path as pth
11
+
12
+ import numpy as np
13
+ from osgeo import osr, gdal
14
+ import skimage.morphology as morph
15
+ from . import gdal_utils as gu
16
+
17
+ # Average conversion scale from degrees to meters
18
+ DEG_2_METERS = 111120
19
+
20
+ # Terrain occlusion threshold
21
+ OCCLUSION_THRES = 0
22
+
23
+ # Path to SAGA saga_cmd binary file
24
+ SAGA_CMD_PATH = "saga_cmd"
25
+
26
+ # Process SRTM tiles so that they are of the same resolution, extent and projection as HR dataset
27
+ # and derive slope and aspect.
28
+
29
+
30
+ def srtm_dem(data_dir_dem, template_file, output_dem, srtm_nodata=-32762):
31
+ # Find all SRTM tiles in the DEM directory, create virtual raster and the reproject,
32
+ # resample and subset it based of the HR template.
33
+ srtm_tiles = glob.glob(os.path.join(data_dir_dem, "srtm_??_??.tif"))
34
+ if srtm_tiles:
35
+ if len(srtm_tiles) > 1:
36
+ raw_dem = os.path.join(data_dir_dem, 'temp.tif')
37
+ raw_dem = gu.merge_raster_layers(srtm_tiles, raw_dem)
38
+ else:
39
+ raw_dem = srtm_tiles[0]
40
+ out_fid = gu.resample_with_gdalwarp(raw_dem, template_file,
41
+ resample_alg='cubic')
42
+ dem_data = out_fid.GetRasterBand(1).ReadAsArray().astype(np.float)
43
+
44
+ dem_data[dem_data == srtm_nodata] = np.nan
45
+ gu.save_image(dem_data,
46
+ out_fid.GetGeoTransform(),
47
+ out_fid.GetProjection(),
48
+ output_dem)
49
+
50
+ if os.path.exists(os.path.join(data_dir_dem, 'temp.tif')):
51
+ os.remove(os.path.join(data_dir_dem, 'temp.tif'))
52
+
53
+ def process_dem(data_dir_DEM, template_HR, output_dataset_name, overwrite_existing):
54
+
55
+ dem_file = os.path.join(data_dir_DEM, '%s_dem_hr.tif' % output_dataset_name)
56
+
57
+ if os.path.isfile(dem_file) and not overwrite_existing:
58
+ print('%s already processed' % dem_file)
59
+ else:
60
+ # Find all SRTM tiles in the DEM directory, create virtual raster and the reproject,
61
+ # resample and subset it based of the HR template. This assumes that the tiles are already
62
+ # present since they are downloaded by Sen2Cor.
63
+ srtm_dem(data_dir_DEM, template_HR, dem_file)
64
+
65
+ # Calculate slope and aspect using GDAL
66
+ slope_file = os.path.join(data_dir_DEM, '%s_slope_hr.tif' % output_dataset_name)
67
+ if not os.path.exists(slope_file) or overwrite_existing:
68
+ slope_from_dem(dem_file, slope_file)
69
+ aspect_file = os.path.join(data_dir_DEM, '%s_aspect_hr.tif' % output_dataset_name)
70
+ if not os.path.exists(aspect_file) or overwrite_existing:
71
+ aspect_from_dem(dem_file, aspect_file)
72
+
73
+ return dem_file, slope_file, aspect_file
74
+
75
+
76
+ def slope_from_dem(dem_file_path, output=None, scale=None):
77
+
78
+ if not output:
79
+ output = dem_file_path.replace('.tif', '_slope.tif')
80
+ if scale is None:
81
+ proj = gu.raster_info(dem_file_path)[0]
82
+ srs = osr.SpatialReference()
83
+ srs.ImportFromWkt(proj)
84
+ if srs.GetAttrValue('UNIT') == "degree":
85
+ # Covert degrees pixel size into meters for trigonometric computations
86
+ scale = DEG_2_METERS
87
+ else:
88
+ scale = 1
89
+
90
+ gdal.DEMProcessing(output, dem_file_path, "slope",
91
+ slopeFormat="degree",
92
+ computeEdges=True,
93
+ scale=scale)
94
+
95
+
96
+ def aspect_from_dem(dem_file_path, output=None):
97
+
98
+ if not output:
99
+ output = pth.splitext(dem_file_path)[0]+'_aspect.tif'
100
+
101
+ gdal.DEMProcessing(output, dem_file_path, "aspect",
102
+ computeEdges=True,
103
+ zeroForFlat=True)
104
+
105
+
106
+
107
+ def declination_angle(doy):
108
+ ''' Calculates the Earth declination angle
109
+
110
+ Parameters
111
+ ----------
112
+ doy : float or int
113
+ day of the year
114
+
115
+ Returns
116
+ -------
117
+ declination : float
118
+ Declination angle (radians)
119
+ '''
120
+ declination = np.radians(23.45) * np.sin((2.0 * np.pi * doy / 365.0) - 1.39)
121
+
122
+ return declination
123
+
124
+
125
+ def hour_angle(ftime, declination, lon, stdlon=0):
126
+ '''Calculates the hour angle
127
+
128
+ Parameters
129
+ ----------
130
+ ftime : float
131
+ Time of the day (decimal hours)
132
+ declination : float
133
+ Declination angle (radians)
134
+ lon : float
135
+ longitude of the site (degrees).
136
+ stdlon : float
137
+ Longitude of the standard meridian that represent the ftime time zone
138
+
139
+ Returns
140
+ w : float
141
+ hour angle (radians), negative before noon, positive after noon
142
+ '''
143
+
144
+ EOT = 0.258 * np.cos(declination) - 7.416 * np.sin(declination) - \
145
+ 3.648 * np.cos(2.0 * declination) - 9.228 * np.sin(2.0 * declination)
146
+ LC = (stdlon - lon) / 15.
147
+ time_corr = (-EOT / 60.) + LC
148
+ solar_time = ftime - time_corr
149
+ # Get the hour angle
150
+ w = np.radians((solar_time - 12.0) * 15.)
151
+
152
+ return w
153
+
154
+
155
+ def incidence_angle_tilted(lat, lon, doy, ftime, stdlon=0, aspect=0, slope=0):
156
+ ''' Calculates the incidence solar angle over a tilted flat surface
157
+
158
+ Parameters
159
+ ----------
160
+ lat : float or array
161
+ latitude (degrees)
162
+ lon : float or array
163
+ longitude (degrees)
164
+ doy : int
165
+ day of the year
166
+ ftime : float
167
+ Time of the day (decimal hours)
168
+ stdlon : float
169
+ Longitude of the standard meridian that represent the ftime time zone
170
+ aspect : float or array
171
+ surface azimuth angle, measured clockwise from north (degrees)
172
+ slope : float or array
173
+ slope angle (degrees)
174
+
175
+ Returns
176
+ -------
177
+ cos_theta_i : float or array
178
+ cosine of the incidence angle
179
+ '''
180
+
181
+ # Get the dclination and hour angle
182
+ delta = declination_angle(doy)
183
+ # Hour angle is considered negative before noon, positive after noon
184
+ omega = hour_angle(ftime, delta, lon, stdlon=stdlon)
185
+
186
+ f1, f2, f3 = inclination_factors(lat, slope, aspect)
187
+
188
+ cos_delta = np.cos(delta)
189
+ sin_delta = np.sin(delta)
190
+ cos_omega = np.cos(omega)
191
+ sin_omega = np.sin(omega)
192
+ cos_theta_i = sin_delta * f1 \
193
+ + cos_delta * cos_omega * f2 \
194
+ + cos_delta * sin_omega * f3
195
+
196
+ return cos_theta_i
197
+
198
+
199
+ def shading(dem_file, saa, sza, max_chunk_size=1e9):
200
+ """
201
+ Adapted from https://github.com/zoran-cuckovic/QGIS-terrain-shading
202
+ Parameters
203
+ ----------
204
+ dem_file
205
+ saa
206
+ sza
207
+ max_chunk_size
208
+
209
+ Returns
210
+ -------
211
+
212
+ """
213
+ def mean_angle(angle_array, degrees=True):
214
+ if degrees is True:
215
+ angle_array = np.radians(angle_array)
216
+ cos_i = np.nanmean(np.cos(angle_array))
217
+ sin_i = np.nanmean(np.sin(angle_array))
218
+ mean_angle = np.arctan2(sin_i, cos_i)
219
+ if degrees is True:
220
+ mean_angle = np.degrees(mean_angle)
221
+ return mean_angle
222
+
223
+ def window_loop(shape, chunk, axis=0, reverse=False, overlap=0):
224
+ """
225
+ Construct a frame to extract chunks of data from gdal
226
+ (and to insert them properly to a numpy matrix)
227
+ """
228
+ xsize, ysize = shape if axis == 0 else shape[::-1]
229
+
230
+ if reverse:
231
+ steps = np.arange(xsize // chunk, -1, -1)
232
+ begin = xsize
233
+ else:
234
+ steps = np.arange(1, xsize // chunk + 2)
235
+ begin = 0
236
+
237
+ x, y, x_off, y_off = 0, 0, xsize, ysize
238
+
239
+ for step in steps:
240
+
241
+ end = min(int(chunk * step), xsize)
242
+ if reverse:
243
+ x, x_off = end, begin - end
244
+ else:
245
+ x, x_off = begin, end - begin
246
+
247
+ if overlap and x >= overlap: x -= overlap * int(step)
248
+
249
+ begin = end
250
+
251
+ if x_off < chunk:
252
+ in_view = np.s_[:, : x_off] if not axis else np.s_[: x_off, :]
253
+ else:
254
+ in_view = np.s_[:]
255
+
256
+ if not axis:
257
+ gdal_take = (x, y, x_off, y_off)
258
+ else:
259
+ gdal_take = (y, x, y_off, x_off)
260
+
261
+ yield in_view, gdal_take
262
+ # We need to work with a single beam direction
263
+ direction = mean_angle(saa)
264
+ steep = (45 <= direction <= 135 or 225 <= direction <= 315)
265
+
266
+ s = direction % 90
267
+ if s > 45:
268
+ s = 90 - s
269
+
270
+ slope = np.tan(np.radians(s)) # matrix shear slope
271
+
272
+ # ! attention: x in gdal is y dimension un numpy (the first dimension)
273
+ dem_fid = gdal.Open(dem_file, gdal.GA_ReadOnly)
274
+ xsize, ysize = dem_fid.RasterXSize, dem_fid.RasterYSize
275
+ gt = dem_fid.GetGeoTransform()
276
+ proj = dem_fid.GetProjection()
277
+ # Compute the average pixel sixe
278
+ pixel_size = 0.5 * np.abs(gt[1]) + 0.5 * np.abs(gt[5])
279
+ # Check whether elevation projection units are degrees and convert to meters
280
+ srs = osr.SpatialReference()
281
+ srs.ImportFromWkt(proj)
282
+ if srs.GetAttrValue('UNIT') == "degree":
283
+ # Covert degrees pixel size into meters for trigonometric computations
284
+ pixel_size *= DEG_2_METERS
285
+ # adjust for pixel rotation (pixel diagonal = 45°)
286
+ # (diagonal is clipped by light path, under right angle,
287
+ # which means it becomes the adjacent, and the pixel size the hypothenuse.)
288
+ pixel_size *= np.cos(np.radians(s))
289
+
290
+ tilt = np.tan(np.radians(90. - sza))
291
+
292
+ chunk = min(int(max_chunk_size // (ysize if steep else xsize)),
293
+ (xsize if steep else ysize))
294
+
295
+ if s % 45 > 0:
296
+ # Determine the optimal chunk size (estimate!).
297
+ # The problem is to carry rasterized lines
298
+ # from one chunk to another.
299
+ # So, set chunk size according to minimum rasterisation error
300
+ c = (np.arange(1, chunk) * slope) % 1 # %1 to get decimals only
301
+ c[c > 0.5] -= 1
302
+ # this is not ideal : we cannot predict where it would stop
303
+ chunk -= np.argmin(np.round(abs(c), decimals=2)[::-1]) + 1
304
+
305
+ # 3) ------- SHEAR MATRIX (INDICES) -------------
306
+ chunk_slice = (ysize, chunk) if steep else (chunk, xsize)
307
+ indices_y, indices_x = np.indices(chunk_slice)
308
+ mx_z = np.zeros(chunk_slice)
309
+ mx_z[:] = -99999
310
+
311
+ # this is all upside down ...
312
+ rev_y = 90 <= direction <= 270
313
+ rev_x = not 180 <= direction <= 360
314
+
315
+ if rev_y:
316
+ indices_y = indices_y[::-1, :]
317
+ if not rev_x:
318
+ indices_x = indices_x[:, ::-1]
319
+
320
+ off_a = indices_x + indices_y * slope
321
+ off_b = indices_y + indices_x * slope
322
+
323
+ if steep:
324
+ axis = 0
325
+ # construct a slope to simulate sun angle
326
+ # elevations will be draped over this 2D slope
327
+ off = off_a[:, ::-1] # indices_y * slope + indices_x[:, ::-1]
328
+
329
+ src_y = indices_x[:, ::-1]
330
+ src_x = np.round(off_b).astype(int)
331
+
332
+ else:
333
+ axis = 1
334
+ off = off_b[:, ::-1] # indices_x[:,::-1] * slope + indices_y
335
+
336
+ src_x = indices_y
337
+ src_y = np.round(off_a).astype(int)
338
+
339
+ src = np.s_[src_y, src_x]
340
+
341
+ # Distance to slope's perpendicular is (x + y)/2 * root(2) for 45°
342
+ # because where x = y we have a square with a*root(2) diagonal
343
+ # However, in a rotated pixel grid, the slope is cutting through pixel diagonals
344
+ # so we take cosinus (pixel_size) to get this shortened distance
345
+ off *= pixel_size * np.nanmean(tilt)
346
+
347
+ # create a matrix to hold the sheared matrix
348
+ mx_temp = np.zeros(((np.max(src_y) + 1), np.max(src_x) + 1))
349
+
350
+ t_y, t_x = mx_temp.shape
351
+
352
+ # carrying lines from one chunk to the next (fussy...)
353
+ if steep:
354
+ l = np.s_[-1, : ysize]
355
+ f = np.s_[0, t_x - ysize:]
356
+ else:
357
+ l = np.s_[t_y - xsize:, -1]
358
+ f = np.s_[:xsize, 0]
359
+
360
+ last_line = np.zeros((ysize if steep else xsize))
361
+
362
+ # 4 ----- LOOP THOUGH DATA CHUNKS AND CALCULATE -----------------
363
+ counter = 0
364
+ out_array = np.zeros([ysize, xsize])
365
+ for dem_view, gdal_coords in window_loop(
366
+ shape=(xsize, ysize),
367
+ chunk=chunk,
368
+ axis=not steep,
369
+ reverse=rev_x if steep else rev_y,
370
+ overlap=1):
371
+
372
+ mx_z[dem_view] = dem_fid.ReadAsArray(*gdal_coords).astype(float)
373
+
374
+ # should handle better NoData !!
375
+ # nans will destroy the accumulation sequence
376
+ mask = np.isnan(mx_z)
377
+ mx_z[mask] = -9999
378
+
379
+ mx_temp[src] = mx_z + off
380
+
381
+ mx_temp[f] += -last_line # shadows have negative values, so *-1
382
+ # accumulate maximum shadow depths
383
+ mx_temp -= np.maximum.accumulate(mx_temp, axis=axis)
384
+ # first line has shadow od zero depth (nothing to accum), so copy from previous chunk
385
+ mx_temp[f] = last_line
386
+
387
+ last_line[:] = mx_temp[l] # save for later
388
+
389
+ out = mx_temp[src]
390
+ out[mask] = np.nan
391
+ counter += 1
392
+ out_array[gdal_coords[1]:gdal_coords[1] + gdal_coords[3],
393
+ gdal_coords[0]:gdal_coords[0] + gdal_coords[2]] = out[dem_view]
394
+
395
+
396
+ return out_array
397
+
398
+
399
+ def latlon_from_dem(dem_file_path, output=None):
400
+ if not output:
401
+ output = dem_file_path.replace('.tif', '_latlon.tif')
402
+
403
+ proj, gt, size_x, size_y = gu.raster_info(dem_file_path)[0:4]
404
+ dims = size_y, size_x
405
+ rows, cols = np.indices(dims)
406
+ lon = gt[0] + cols * gt[1] + rows * gt[2]
407
+ lat = gt[3] + cols * gt[4] + rows * gt[5]
408
+ srcSRS = osr.SpatialReference()
409
+ srcSRS.ImportFromWkt(proj)
410
+ dstSRS = osr.SpatialReference()
411
+ dstSRS.ImportFromEPSG(4326)
412
+ # GDAL decided to reverse the coordinate transformation axis order
413
+ # https://github.com/OSGeo/gdal/issues/1546
414
+ dstSRS.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
415
+ transform = osr.CoordinateTransformation(srcSRS, dstSRS)
416
+ array = np.stack([lon.reshape(-1), lat.reshape(-1)]).T
417
+ coords = np.array(transform.TransformPoints(array)).T
418
+
419
+ lon = coords[0].reshape(dims)
420
+ lat = coords[1].reshape(dims)
421
+ array = np.dstack([lat, lon])
422
+ gu.save_image(array, gt, proj, output, no_data_value=np.nan)
423
+ return lat, lon
424
+
425
+
426
+ def non_occluded_terrain(dem_file, saa, sza, smooth_radius=1):
427
+ # Get areas that are shaded by obstacles around
428
+ occlusion = shading(dem_file, saa, sza)
429
+ non_shaded = np.logical_and(np.isfinite(occlusion), occlusion >= OCCLUSION_THRES)
430
+ del occlusion
431
+ # Smooth the occlusion map
432
+ if smooth_radius > 0:
433
+ se = morph.disk(smooth_radius)
434
+ non_shaded = morph.binary_closing(non_shaded, footprint=se)
435
+ return non_shaded
436
+
437
+
438
+ def inclination_factors(lat, slope, aspect):
439
+ # Convert remaining angles into radians,
440
+ # aspect is considered the azimuth from the South being westward positive
441
+ lat, aspect, slope = map(np.radians, [lat, aspect - 180, slope])
442
+
443
+ sin_lat = np.sin(lat)
444
+ cos_lat = np.cos(lat)
445
+ cos_slope = np.cos(slope)
446
+ sin_slope = np.sin(slope)
447
+ cos_az = np.cos(aspect)
448
+ sin_az = np.sin(aspect)
449
+
450
+ f1 = sin_lat * cos_slope - cos_lat * sin_slope * cos_az
451
+ f2 = cos_lat * cos_slope + sin_lat * sin_slope * cos_az
452
+ f3 = sin_slope * sin_az
453
+ return f1, f2, f3
454
+