pyTSEB 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.
pyTSEB/PyTSEB.py ADDED
@@ -0,0 +1,1666 @@
1
+ # This file is part PyTSEB, consisting of of high level pyTSEB scripting
2
+ # Copyright 2016 Hector Nieto and contributors listed in the README.md file.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Lesser General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ """
18
+ Created on Thu Jan 7 16:37:45 2016
19
+ @author: Hector Nieto (hector.nieto@ica.csic.es)
20
+
21
+ DESCRIPTION
22
+ ===========
23
+ This package contains the class object for configuring and running TSEB for both
24
+ an image with constant meteorology forcing and a time-series of tabulated data.
25
+
26
+ EXAMPLES
27
+ ========
28
+ The easiest way to get a feeling of TSEB and its configuration is throuh the ipython/jupyter
29
+ notebooks.
30
+
31
+ Jupyter notebook pyTSEB GUI
32
+ ---------------------------
33
+ To configure TSEB for processing a time series of tabulated data, type in a ipython terminal or a
34
+ jupyter notebook.
35
+
36
+ .. code-block:: ipython
37
+
38
+ from TSEB_IPython_Interface import TSEB_IPython_Interface # Import IPython TSEB interface
39
+ setup=TSEB_IPython_Interface() # Create the setup instance from the interface class object
40
+ setup.PointTimeSeriesWidget() # Launches the GUI
41
+
42
+ then to run pyTSEB.
43
+
44
+ .. code-block:: ipython
45
+
46
+ setup.GetDataTSEBWidgets(isImage = False) # Get the data from the widgets
47
+ setup.RunTSEB(isImage = False) # Run TSEB
48
+
49
+ Similarly, to configure and run TSEB for an image.
50
+
51
+ .. code-block:: ipython
52
+
53
+ from TSEB_IPython_Interface import TSEB_IPython_Interface # Import IPython TSEB interface
54
+ setup=TSEB_IPython_Interface() # Create the setup instance from the interface class object
55
+ setup.LocalImageWidget() # Launches the GUI
56
+ setup.GetDataTSEBWidgets(isImage = True) # Get the data from the widgets
57
+ setup.RunTSEB(isImage = True) # Run TSEB
58
+
59
+ Parsing directly a configuration file
60
+ -------------------------------------
61
+ You can also parse direcly into TSEB a configuration file previouly created.
62
+
63
+ >>> from TSEB_ConfigFile_Interface import TSEB_ConfigFile_Interface # Import Configuration File TSEB interface
64
+ >>> tseb=TSEB_ConfigFile_Interface()
65
+ >>> configData=tseb.parseInputConfig(configFile,isImage=True) # Read the data from the configuration file into a python dictionary
66
+ >>> tseb.GetDataTSEB(configData,isImage=True) # Parse the data from the dictionary to TSEB
67
+ >>> tseb.RunTSEB(isImage=True)
68
+
69
+ see the guidelines for input and configuration file preparation in :doc:`README_Notebooks`.
70
+
71
+ """
72
+
73
+ from os.path import join, splitext, dirname, basename, exists
74
+ from os import mkdir
75
+ from collections import OrderedDict
76
+ import math
77
+
78
+ from osgeo import gdal, ogr, osr
79
+ import numpy as np
80
+ import pandas as pd
81
+ from netCDF4 import Dataset
82
+
83
+ from . import TSEB
84
+ from . import meteo_utils as met
85
+ from . import net_radiation as rad
86
+ from . import resistances as res
87
+ from . import clumping_index as CI
88
+ from . import energy_combination_ET as pet
89
+ from . import dis_TSEB
90
+
91
+
92
+ # Constants for indicating whether model output field should be saved to file
93
+ S_N = 0 # Save Not
94
+ S_P = 1 # Save as Primary output
95
+ S_A = 2 # Save as Ancillary output
96
+
97
+
98
+ class PyTSEB(object):
99
+
100
+ def __init__(self, parameters):
101
+ self.p = parameters
102
+
103
+ # Model description parameters
104
+ self.model_type = self.p['model']
105
+ self.resistance_form = self.p['resistance_form']
106
+ self.res_params = {}
107
+ self.G_form = self.p['G_form']
108
+ self.water_stress = self.p['water_stress']
109
+ self.calc_daily_ET = False
110
+
111
+ def process_local_image(self):
112
+ ''' Prepare input data and calculate energy fluxes for all the pixel in an image.
113
+
114
+ Parameters
115
+ ----------
116
+ None
117
+
118
+ Returns
119
+ -------
120
+ in_data : dict
121
+ All the input data coming into the model.
122
+ out_data : dict
123
+ All the output data coming out of the model.
124
+ '''
125
+
126
+ # ======================================
127
+ # Process the input
128
+
129
+ # Create an input dictionary
130
+ in_data = dict()
131
+ temp_data = dict()
132
+ res_params = dict()
133
+ input_fields = self._get_input_structure()
134
+
135
+ # Get projection, geo transform of the input dataset, or its subset if specified.
136
+ # It is assumed that all the input rasters have exactly the same projection, dimensions and
137
+ # resolution.
138
+ try:
139
+ field = list(input_fields)[0]
140
+ fid = gdal.Open(self.p[field], gdal.GA_ReadOnly)
141
+ self.prj = fid.GetProjection()
142
+ self.geo = fid.GetGeoTransform()
143
+ dims = (fid.RasterYSize, fid.RasterXSize)
144
+ fid = None
145
+ self.subset = []
146
+ if "subset" in self.p:
147
+ self.subset, self.geo = self._get_subset(self.p["subset"], self.prj, self.geo)
148
+ if self.subset[3] <= 0 or self.subset[2] <= 0 or\
149
+ self.subset[0] >= dims[1] or self.subset[1] >= dims[0]:
150
+ print("ERROR: Requested subset does not intersect the data extent.")
151
+ return
152
+ if self.subset[1] + self.subset[3] > dims[0] or\
153
+ self.subset[0] + self.subset[2] > dims[1]:
154
+ print("WARNING: Requested subset extends beyond the data extent.")
155
+ self.subset[3] = min(self.subset[3], dims[0] - self.subset[1])
156
+ self.subset[2] = min(self.subset[2], dims[1] - self.subset[0])
157
+ dims = (self.subset[3], self.subset[2])
158
+ except KeyError:
159
+ print('Error reading ' + input_fields[field])
160
+ fid = None
161
+ return
162
+
163
+ # Process all input fields
164
+ for field in list(input_fields):
165
+ # Some fields might need special treatment
166
+ if field in ["lat", "lon", "stdlon", "DOY", "time"]:
167
+ success, temp_data[field] = self._set_param_array(field, dims)
168
+ elif field == "input_mask":
169
+ if self.p['input_mask'] == '0':
170
+ # Create mask from landcover array
171
+ mask = np.ones(dims, np.int32)
172
+ mask[np.logical_or.reduce((in_data['landcover'] == res.WATER,
173
+ in_data['landcover'] == res.URBAN,
174
+ in_data['landcover'] == res.SNOW))] = 0
175
+ success = True
176
+ else:
177
+ success, mask = self._set_param_array(field, dims)
178
+ elif field in ['KN_b', 'KN_c', 'KN_c_dash']:
179
+ success, res_params[field] = self._set_param_array(field, dims)
180
+ elif field == "G":
181
+ # Get the Soil Heat flux if G_form includes the option of
182
+ # Constant G or constant ratio of soil reaching radiation
183
+ if self.G_form[0][0] == TSEB.G_CONSTANT or self.G_form[0][0] == TSEB.G_RATIO:
184
+ success, self.G_form[1] = self._set_param_array(self.G_form[1], dims)
185
+ # Santanello and Friedls G
186
+ elif self.G_form[0][0] == TSEB.G_TIME_DIFF:
187
+ # Set the time in the G_form flag to compute the Santanello and
188
+ # Friedl G
189
+ self.G_form[1] = self._set_param_array("time", dims)[1]
190
+ elif field == 'S_dn_24':
191
+ success, in_data[field] = self._set_param_array(field, dims)
192
+ if success:
193
+ self.calc_daily_ET = True
194
+ else:
195
+ # Model specific fields which might need special treatment
196
+ success, inputs = self._set_special_model_input(field, dims)
197
+ if success:
198
+ in_data.update(inputs)
199
+ else:
200
+ success, in_data[field] = self._set_param_array(field, dims)
201
+
202
+ if not success:
203
+ # Some fields are optional is some circumstances or can be calculated if missing.
204
+ if field in ["SZA", "SAA"]:
205
+ print("Estimating missing %s parameter" % field)
206
+ try:
207
+ in_data['SZA'], in_data['SAA'] = met.calc_sun_angles(temp_data["lat"],
208
+ temp_data["lon"],
209
+ temp_data["stdlon"],
210
+ temp_data["DOY"],
211
+ temp_data["time"])
212
+ except KeyError as e:
213
+ print("ERROR: Cannot calculate or read {}. {} or parameter {} are missing."
214
+ .format(input_fields[field], field, e))
215
+ return
216
+ elif field == "p":
217
+ print("Estimating missing %s parameter" % field)
218
+ try:
219
+ in_data["p"] = met.calc_pressure(in_data["alt"])
220
+ except KeyError as e:
221
+ print("ERROR: Cannot calculate or read {}. {} or parameter {} are missing."
222
+ .format(input_fields[field], field, e))
223
+ return
224
+ elif field == "L_dn":
225
+ print("Estimating missing %s parameter" % field)
226
+ try:
227
+ in_data['L_dn'] = rad.calc_longwave_irradiance(in_data['ea'],
228
+ in_data['T_A1'],
229
+ in_data['p'],
230
+ in_data['z_T'])
231
+ except KeyError as e:
232
+ print("ERROR: Cannot calculate or read {}. {} or parameter {} are missing."
233
+ .format(input_fields[field], field, e))
234
+ return
235
+ elif (field in ['KN_b', 'KN_c', 'KN_c_dash']
236
+ and self.resistance_form != TSEB.KUSTAS_NORMAN_1999):
237
+ print("ERROR: Cannot read {}.".format(input_fields[field]))
238
+ return
239
+ elif field == "input_mask":
240
+ print("Please set input_mask=0 for processing the whole image.")
241
+ return
242
+ elif field == "S_dn_24":
243
+ print("Provide a valid S_dn_24 (Daily shortwave irradiance) "
244
+ "value if you want to estimate daily ET")
245
+ elif field in ["alt", "lat", "lon", "stdlon", "DOY", "time"]:
246
+ print("WARNING!: Non-critical parameter %s "
247
+ "is invalid or missing..."%field)
248
+ pass
249
+ else:
250
+ print('ERROR: file read {}'.format(field))
251
+ print('Please type a valid filename or a numeric value for '
252
+ .format(input_fields[field]))
253
+ return
254
+
255
+ temp_data = None
256
+
257
+ # ======================================
258
+ # Run the chosen model
259
+
260
+ out_data = self.run(in_data, mask)
261
+
262
+ # ======================================
263
+ # Save output files
264
+
265
+ # Output variables to be saved in images
266
+ all_fields = self._get_output_structure()
267
+ primary_fields = [field for field, save in all_fields.items() if save == S_P]
268
+ ancillary_fields = [field for field, save in all_fields.items() if save == S_A]
269
+ print(primary_fields)
270
+ outdir = dirname(self.p['output_file'])
271
+ if not exists(outdir):
272
+ mkdir(outdir)
273
+ self.write_raster_output(self.p['output_file'], out_data, primary_fields)
274
+ outputfile = splitext(self.p['output_file'])[0] + '_ancillary' + \
275
+ splitext(self.p['output_file'])[1]
276
+ self.write_raster_output(outputfile, out_data, ancillary_fields)
277
+ print('Saved Files')
278
+
279
+ return in_data, out_data
280
+
281
+ def process_point_series_array(self):
282
+ ''' Prepare input data and calculate energy fluxes for all the dates in point time-series.
283
+
284
+ Parameters
285
+ ----------
286
+ None
287
+
288
+ Returns
289
+ -------
290
+ in_data : dict
291
+ All the input data coming into the model.
292
+ out_data : dict
293
+ All the output data coming out of the model.
294
+ '''
295
+
296
+ def compose_date(
297
+ years,
298
+ months=1,
299
+ days=1,
300
+ weeks=None,
301
+ hours=None,
302
+ minutes=None,
303
+ seconds=None,
304
+ milliseconds=None,
305
+ microseconds=None,
306
+ nanoseconds=None):
307
+ ''' Taken from http://stackoverflow.com/questions/34258892/converting-year-and-day-of-year-into-datetime-index-in-pandas'''
308
+ years = np.asarray(years) - 1970
309
+ months = np.asarray(months) - 1
310
+ days = np.asarray(days) - 1
311
+ types = ('<M8[Y]', '<m8[M]', '<m8[D]', '<m8[W]', '<m8[h]',
312
+ '<m8[m]', '<m8[s]', '<m8[ms]', '<m8[us]', '<m8[ns]')
313
+ vals = (years, months, days, weeks, hours, minutes, seconds,
314
+ milliseconds, microseconds, nanoseconds)
315
+ return sum(np.asarray(v, dtype=t) for t, v in zip(types, vals)
316
+ if v is not None)
317
+
318
+ # ======================================
319
+ # Process the input
320
+
321
+ # Read input data from CSV file
322
+ in_data = pd.read_csv(self.p['input_file'],
323
+ sep="\s+",
324
+ index_col=False)
325
+ in_data.index = compose_date(
326
+ years=in_data['year'],
327
+ days=in_data['DOY'],
328
+ hours=in_data['time'],
329
+ minutes=in_data['time'] % 1 * 60)
330
+
331
+ # Check if all the required columns are present
332
+ required_columns = self._get_required_data_columns()
333
+ missing = set(required_columns) - (set(in_data.columns))
334
+ if missing:
335
+ print('ERROR: ' + str(list(missing)) + ' not found in file ' + self.p['input_file'])
336
+ return None, None
337
+
338
+ # Fill in data fields which might not be in the input file
339
+ if 'SZA' not in in_data.columns:
340
+ sza, _ = met.calc_sun_angles(self.p['lat'], self.p['lon'],
341
+ self.p['stdlon'], in_data['DOY'], in_data['time'])
342
+ in_data['SZA'] = sza
343
+ if 'SAA' not in in_data.columns:
344
+ _, saa = met.calc_sun_angles(self.p['lat'], self.p['lon'],
345
+ self.p['stdlon'], in_data['DOY'], in_data['time'])
346
+ in_data['SAA'] = saa
347
+ if 'p' not in in_data.columns:
348
+ # Estimate barometric pressure from the altitude if not included in the table
349
+ in_data['p'] = met.calc_pressure(self.p['alt'])
350
+ if 'f_c' not in in_data.columns: # Fractional cover
351
+ in_data['f_c'] = self.p['f_c'] # Use default value
352
+ if 'w_C' not in in_data.columns: # Canopy width to height ratio
353
+ in_data['w_C'] = self.p['w_C'] # Use default value
354
+ if 'f_g' not in in_data.columns: # Green fraction
355
+ in_data['f_g'] = self.p['f_g'] # Use default value
356
+ if 'rho_vis_C' not in in_data.columns:
357
+ in_data['rho_vis_C'] = self.p['rho_vis_C']
358
+ if 'tau_vis_C' not in in_data.columns:
359
+ in_data['tau_vis_C'] = self.p['tau_vis_C']
360
+ if 'rho_nir_C' not in in_data.columns:
361
+ in_data['rho_nir_C'] = self.p['rho_nir_C']
362
+ if 'tau_nir_C' not in in_data.columns:
363
+ in_data['tau_nir_C'] = self.p['tau_nir_C']
364
+ if 'rho_vis_S' not in in_data.columns:
365
+ in_data['rho_vis_S'] = self.p['rho_vis_S']
366
+ if 'rho_nir_S' not in in_data.columns:
367
+ in_data['rho_nir_S'] = self.p['rho_nir_S']
368
+ if 'emis_C' not in in_data.columns:
369
+ in_data['emis_C'] = self.p['emis_C']
370
+ if 'emis_S' not in in_data.columns:
371
+ in_data['emis_S'] = self.p['emis_S']
372
+
373
+ # Fill in other data fields from the parameter file
374
+ in_data['landcover'] = self.p['landcover']
375
+ in_data['z_u'] = self.p['z_u']
376
+ in_data['z_T'] = self.p['z_T']
377
+ in_data['leaf_width'] = self.p['leaf_width']
378
+ in_data['z0_soil'] = self.p['z0_soil']
379
+ in_data['alpha_PT'] = self.p['alpha_PT']
380
+ in_data['x_LAD'] = self.p['x_LAD']
381
+
382
+ # Incoming long wave radiation
383
+ # If longwave irradiance was not provided then estimate it based on air
384
+ # temperature and humidity
385
+ if 'L_dn' not in in_data.columns:
386
+ in_data['L_dn'] = rad.calc_longwave_irradiance(in_data['ea'], in_data['T_A1'],
387
+ in_data['p'], in_data['z_T'])
388
+
389
+ # Get the Soil Heat flux if G_form includes the option of measured G
390
+ dims = in_data['LAI'].shape
391
+ if self.G_form[0][0] == TSEB.G_CONSTANT:
392
+ if 'G' in in_data.columns:
393
+ self.G_form[1] = in_data['G']
394
+ else:
395
+ self.G_form[1] = np.ones(dims) * self.G_form[1]
396
+ elif self.G_form[0][0] == TSEB.G_RATIO:
397
+ self.G_form[1] = np.ones(dims) * self.G_form[1]
398
+ elif self.G_form[0][0] == TSEB.G_TIME_DIFF:
399
+ # Set the time in the G_form flag to compute the Santanello and
400
+ # Friedl G
401
+ self.G_form[1] = in_data['time']
402
+
403
+ # Set the Kustas and Norman resistance parameters
404
+ if self.resistance_form == 0:
405
+ self.res_params['KN_b'] = np.ones(dims) * self.p['KN_b']
406
+ self.res_params['KN_c'] = np.ones(dims) * self.p['KN_c']
407
+ self.res_params['KN_C_dash'] = np.ones(dims) * self.p['KN_C_dash']
408
+
409
+ # ======================================
410
+ # Run the chosen model
411
+
412
+ out_data = self.run(in_data.to_records(index=False))
413
+ out_data = pd.DataFrame(data=out_data,
414
+ index=in_data.index)
415
+
416
+ # ======================================
417
+ # Save output file
418
+
419
+ # Output Headers
420
+ outputTxtFieldNames = [
421
+ 'Year',
422
+ 'DOY',
423
+ 'Time',
424
+ 'LAI',
425
+ 'f_g',
426
+ 'VZA',
427
+ 'SZA',
428
+ 'SAA',
429
+ 'L_dn',
430
+ 'Rn_model',
431
+ 'Rn_sw_veg',
432
+ 'Rn_sw_soil',
433
+ 'Rn_lw_veg',
434
+ 'Rn_lw_soil',
435
+ 'T_C',
436
+ 'T_S',
437
+ 'T_AC',
438
+ 'LE_model',
439
+ 'H_model',
440
+ 'LE_C',
441
+ 'H_C',
442
+ 'LE_S',
443
+ 'H_S',
444
+ 'G_model',
445
+ 'R_S',
446
+ 'R_x',
447
+ 'R_A',
448
+ 'u_friction',
449
+ 'L',
450
+ 'Skyl',
451
+ 'z_0M',
452
+ 'd_0',
453
+ 'flag']
454
+
455
+ # Create the ouput directory if it doesn't exist
456
+ outdir = dirname(self.p['output_file'])
457
+ if not exists(outdir):
458
+ mkdir(outdir)
459
+
460
+ # Write the data
461
+ csvData = pd.concat([in_data[['year',
462
+ 'DOY',
463
+ 'time',
464
+ 'LAI',
465
+ 'f_g',
466
+ 'VZA',
467
+ 'SZA',
468
+ 'SAA',
469
+ 'L_dn']],
470
+ out_data[['R_n1',
471
+ 'Sn_C1',
472
+ 'Sn_S1',
473
+ 'Ln_C1',
474
+ 'Ln_S1',
475
+ 'T_C1',
476
+ 'T_S1',
477
+ 'T_AC1',
478
+ 'LE1',
479
+ 'H1',
480
+ 'LE_C1',
481
+ 'H_C1',
482
+ 'LE_S1',
483
+ 'H_S1',
484
+ 'G1',
485
+ 'R_S1',
486
+ 'R_x1',
487
+ 'R_A1',
488
+ 'u_friction',
489
+ 'L',
490
+ 'Skyl',
491
+ 'z_0M',
492
+ 'd_0',
493
+ 'flag']]],
494
+ axis=1)
495
+ csvData.to_csv(
496
+ self.p['output_file'],
497
+ sep='\t',
498
+ index=False,
499
+ header=outputTxtFieldNames)
500
+
501
+ print('Done')
502
+
503
+ return in_data, out_data
504
+
505
+ def run(self, in_data, mask=None):
506
+ ''' Execute the routines to calculate energy fluxes.
507
+
508
+ Parameters
509
+ ----------
510
+ in_data : dict
511
+ The input data for the model.
512
+ mask : int array or None
513
+ If None then fluxes will be calculated for all input points. Otherwise, fluxes will be
514
+ calculated only for points for which mask is 1.
515
+
516
+ Returns
517
+ -------
518
+ out_data : dict
519
+ The output data from the model.
520
+ '''
521
+
522
+ print("Processing...")
523
+
524
+ model_params = {"calcG_params": [self.G_form[0], self.G_form[1]],
525
+ "resistance_form": [self.resistance_form, self.res_params]}
526
+
527
+ if mask is None:
528
+ mask = np.ones(in_data['LAI'].shape, np.int32)
529
+
530
+ # Create the output dictionary
531
+ out_data = dict()
532
+ for field in self._get_output_structure():
533
+ out_data[field] = np.zeros(in_data['LAI'].shape, np.float32) + np.nan
534
+
535
+ # Esimate diffuse and direct irradiance
536
+ difvis, difnir, fvis, fnir = rad.calc_difuse_ratio(
537
+ in_data['S_dn'], in_data['SZA'], press=in_data['p'])
538
+ out_data['fvis'] = fvis
539
+ out_data['fnir'] = fnir
540
+ out_data['Skyl'] = difvis * fvis + difnir * fnir
541
+ out_data['S_dn_dir'] = in_data['S_dn'] * (1.0 - out_data['Skyl'])
542
+ out_data['S_dn_dif'] = in_data['S_dn'] * out_data['Skyl']
543
+
544
+ # ======================================
545
+ # First process bare soil cases
546
+
547
+ noVegPixels = in_data['LAI'] <= 0
548
+ noVegPixels = np.logical_or.reduce(
549
+ (in_data['f_c'] <= 0.01,
550
+ in_data['LAI'] <= 0,
551
+ np.isnan(in_data['LAI'])))
552
+ # in_data['LAI'][noVegPixels] = 0
553
+ # in_data['f_c'][noVegPixels] = 0
554
+ i = np.array(np.logical_and(noVegPixels, mask == 1))
555
+
556
+ # Calculate roughness
557
+ out_data['z_0M'][i] = in_data['z0_soil'][i]
558
+ out_data['d_0'][i] = 0
559
+
560
+ # Net shortwave radition for bare soil
561
+ spectraGrdOSEB = out_data['fvis'] * \
562
+ in_data['rho_vis_S'] + out_data['fnir'] * in_data['rho_nir_S']
563
+ out_data['Sn_S1'][i] = (1. - spectraGrdOSEB[i]) * \
564
+ (out_data['S_dn_dir'][i] + out_data['S_dn_dif'][i])
565
+
566
+ # Other fluxes for bare soil
567
+ self._call_flux_model_soil(in_data, out_data, model_params, i)
568
+
569
+ # Set canopy fluxes to 0
570
+ out_data['Sn_C1'][i] = 0.0
571
+ out_data['Ln_C1'][i] = 0.0
572
+ out_data['LE_C1'][i] = 0.0
573
+ out_data['H_C1'][i] = 0.0
574
+
575
+ # ======================================
576
+ # Then process vegetated cases
577
+
578
+ i = np.array(np.logical_and(~noVegPixels, mask == 1))
579
+
580
+ # Calculate roughness
581
+ out_data['z_0M'][i], out_data['d_0'][i] = \
582
+ res.calc_roughness(in_data['LAI'][i],
583
+ in_data['h_C'][i],
584
+ w_C=in_data['w_C'][i],
585
+ landcover=in_data['landcover'][i],
586
+ f_c=in_data['f_c'][i])
587
+
588
+ # Net shortwave radiation for vegetation
589
+ F = np.zeros(in_data['LAI'].shape, np.float32)
590
+ F[i] = in_data['LAI'][i] / in_data['f_c'][i]
591
+ # Clumping index
592
+ omega0 = np.zeros(in_data['LAI'].shape, np.float32)
593
+ Omega = np.zeros(in_data['LAI'].shape, np.float32)
594
+ omega0[i] = CI.calc_omega0_Kustas(
595
+ in_data['LAI'][i],
596
+ in_data['f_c'][i],
597
+ x_LAD=in_data['x_LAD'][i],
598
+ isLAIeff=True)
599
+ if self.p['calc_row'][0] == 0: # randomly placed canopies
600
+ Omega[i] = CI.calc_omega_Kustas(
601
+ omega0[i], in_data['SZA'][i], w_C=in_data['w_C'][i])
602
+ elif self.p['calc_row'][0] == 1: # row crop canopies
603
+ Omega[i] = CI.calc_omega_rows(in_data['LAI'][i],
604
+ in_data['f_c'][i],
605
+ theta=in_data['SZA'][i],
606
+ psi=self.p['calc_row'][1] - in_data['SAA'][i],
607
+ w_c=in_data['w_C'][i],
608
+ x_lad=in_data['x_LAD'][i],
609
+ is_lai_eff=True)
610
+
611
+ else:
612
+ Omega[i] = CI.calc_omega_Kustas(
613
+ omega0[i], in_data['SZA'][i], w_C=in_data['w_C'][i])
614
+ LAI_eff = F * Omega
615
+ [out_data['Sn_C1'][i],
616
+ out_data['Sn_S1'][i]] = rad.calc_Sn_Campbell(in_data['LAI'][i],
617
+ in_data['SZA'][i],
618
+ out_data['S_dn_dir'][i],
619
+ out_data['S_dn_dif'][i],
620
+ out_data['fvis'][i],
621
+ out_data['fnir'][i],
622
+ in_data['rho_vis_C'][i],
623
+ in_data['tau_vis_C'][i],
624
+ in_data['rho_nir_C'][i],
625
+ in_data['tau_nir_C'][i],
626
+ in_data['rho_vis_S'][i],
627
+ in_data['rho_nir_S'][i],
628
+ x_LAD=in_data['x_LAD'][i],
629
+ LAI_eff=LAI_eff[i])
630
+
631
+ # Other fluxes for vegetation
632
+ self._call_flux_model_veg(in_data, out_data, model_params, i)
633
+
634
+ # Calculate the bulk fluxes
635
+ out_data['LE1'] = out_data['LE_C1'] + out_data['LE_S1']
636
+ out_data['LE_partition'] = out_data['LE_C1'] / out_data['LE1']
637
+ out_data['H1'] = out_data['H_C1'] + out_data['H_S1']
638
+ out_data['R_ns1'] = out_data['Sn_C1'] + out_data['Sn_S1']
639
+ out_data['R_nl1'] = out_data['Ln_C1'] + out_data['Ln_S1']
640
+ out_data['R_n1'] = out_data['R_ns1'] + out_data['R_nl1']
641
+ out_data['delta_R_n1'] = out_data['Sn_C1'] + out_data['Ln_C1']
642
+
643
+ if self.water_stress:
644
+ i = mask == 1
645
+ [_, _, _, _, _, _, out_data['LE_0'][i], _,
646
+ out_data['LE_C_0'][i], _, _, _, _, _, _, _, _, _, _] = \
647
+ pet.shuttleworth_wallace(
648
+ in_data['T_A1'][i],
649
+ in_data['u'][i],
650
+ in_data['ea'][i],
651
+ in_data['p'][i],
652
+ out_data['Sn_C1'][i],
653
+ out_data['Sn_S1'][i],
654
+ in_data['L_dn'][i],
655
+ np.maximum(in_data['LAI'][i], 0.01),
656
+ in_data['h_C'][i],
657
+ in_data['emis_C'][i],
658
+ in_data['emis_S'][i],
659
+ out_data['z_0M'][i],
660
+ out_data['d_0'][i],
661
+ in_data['z_u'][i],
662
+ in_data['z_T'][i],
663
+ f_c=in_data['f_c'][i],
664
+ w_C=in_data['w_C'][i],
665
+ leaf_width=in_data['leaf_width'][i],
666
+ z0_soil=in_data['z0_soil'][i],
667
+ x_LAD=in_data['x_LAD'][i],
668
+ Rst_min=self.p['Rst_min'],
669
+ R_ss=self.p['R_ss'],
670
+ calcG_params=[model_params["calcG_params"][0],
671
+ model_params["calcG_params"][1][i]],
672
+ resistance_form=[model_params["resistance_form"][0],
673
+ {k: model_params["resistance_form"][1][k][i]
674
+ for k in model_params["resistance_form"][1]}])
675
+
676
+ out_data['CWSI'][i] = 1.0 - (out_data['LE_C1'][i] / out_data['LE_C_0'][i])
677
+
678
+ if self.calc_daily_ET:
679
+ out_data['ET_day'] = met.flux_2_evaporation(in_data['S_dn_24'] * out_data['LE1'] / in_data['S_dn'],
680
+ t_k=20+273.15,
681
+ time_domain=24)
682
+
683
+ print("Finished processing!")
684
+ return out_data
685
+
686
+ def _call_flux_model_veg(self, in_data, out_data, model_params, i):
687
+ ''' Call a TSEB_PT model to calculate fluxes for data points containing vegetation.
688
+
689
+ Parameters
690
+ ----------
691
+ in_data : dict
692
+ The input data for the model.
693
+ out_data : dict
694
+ Dict containing the output data from the model which will be updated. It also contains
695
+ previusly calculated shortwave radiation and roughness values which are used as input
696
+ data.
697
+
698
+ Returns
699
+ -------
700
+ None
701
+ '''
702
+
703
+ [out_data['flag'][i], out_data['T_S1'][i], out_data['T_C1'][i],
704
+ out_data['T_AC1'][i], out_data['Ln_S1'][i], out_data['Ln_C1'][i],
705
+ out_data['LE_C1'][i], out_data['H_C1'][i], out_data['LE_S1'][i],
706
+ out_data['H_S1'][i], out_data['G1'][i], out_data['R_S1'][i],
707
+ out_data['R_x1'][i], out_data['R_A1'][i], out_data['u_friction'][i],
708
+ out_data['L'][i], out_data['n_iterations'][i]] = TSEB.TSEB_PT(
709
+ in_data['T_R1'][i],
710
+ in_data['VZA'][i],
711
+ in_data['T_A1'][i],
712
+ in_data['u'][i],
713
+ in_data['ea'][i],
714
+ in_data['p'][i],
715
+ out_data['Sn_C1'][i],
716
+ out_data['Sn_S1'][i],
717
+ in_data['L_dn'][i],
718
+ in_data['LAI'][i],
719
+ in_data['h_C'][i],
720
+ in_data['emis_C'][i],
721
+ in_data['emis_S'][i],
722
+ out_data['z_0M'][i],
723
+ out_data['d_0'][i],
724
+ in_data['z_u'][i],
725
+ in_data['z_T'][i],
726
+ f_c=in_data['f_c'][i],
727
+ f_g=in_data['f_g'][i],
728
+ w_C=in_data['w_C'][i],
729
+ leaf_width=in_data['leaf_width'][i],
730
+ z0_soil=in_data['z0_soil'][i],
731
+ alpha_PT=in_data['alpha_PT'][i],
732
+ x_LAD=in_data['x_LAD'][i],
733
+ calcG_params=[model_params["calcG_params"][0],
734
+ model_params["calcG_params"][1][i]],
735
+ resistance_form=[model_params["resistance_form"][0],
736
+ {k: model_params["resistance_form"][1][k][i]
737
+ for k in model_params["resistance_form"][1]}])
738
+
739
+ def _call_flux_model_soil(self, in_data, out_data, model_params, i):
740
+ ''' Call a OSEB model to calculate soil fluxes for data points containing no vegetation.
741
+
742
+ Parameters
743
+ ----------
744
+ in_data : dict
745
+ The input data for the model.
746
+ out_data : dict
747
+ Dict containing the output data from the model which will be updated. It also contains
748
+ previusly calculated shortwave radiation and roughness values which are used as input
749
+ data.
750
+
751
+ Returns
752
+ -------
753
+ None
754
+ '''
755
+
756
+ [out_data['flag'][i],
757
+ out_data['Ln_S1'][i],
758
+ out_data['LE_S1'][i],
759
+ out_data['H_S1'][i],
760
+ out_data['G1'][i],
761
+ out_data['R_A1'][i],
762
+ out_data['u_friction'][i],
763
+ out_data['L'][i],
764
+ out_data['n_iterations'][i]] = TSEB.OSEB(in_data['T_R1'][i],
765
+ in_data['T_A1'][i],
766
+ in_data['u'][i],
767
+ in_data['ea'][i],
768
+ in_data['p'][i],
769
+ out_data['Sn_S1'][i],
770
+ in_data['L_dn'][i],
771
+ in_data['emis_S'][i],
772
+ out_data['z_0M'][i],
773
+ out_data['d_0'][i],
774
+ in_data['z_u'][i],
775
+ in_data['z_T'][i],
776
+ calcG_params=[model_params["calcG_params"][0],
777
+ model_params["calcG_params"][1][i]])
778
+
779
+ def _set_param_array(self, parameter, dims, band=1):
780
+ '''Set model input parameter as an array.
781
+
782
+ Parameters
783
+ ----------
784
+ parameter : float or string
785
+ If float it should be the value of the parameter. If string of a number it is also
786
+ the value of the parameter. Otherwise it is the name of the parameter and the value, or
787
+ path to raster file which contains the values, is read from the parameters dictionary.
788
+ dims : int list
789
+ The dimensions of the output parameter array.
790
+ band : int (default = 1)
791
+ Band (in GDAL convention) of raster file to be read, if parameter is to be read from a
792
+ raster file.
793
+
794
+ Returns
795
+ -------
796
+ success : boolean
797
+ True is the parameter was succefully set, false otherwise.
798
+ array : float array
799
+ The set parameter array.
800
+ '''
801
+
802
+ success = True
803
+ array = None
804
+
805
+ # See if the parameter is a number
806
+ try:
807
+ array = np.zeros(dims, np.float32) + float(parameter)
808
+ return success, array
809
+ except ValueError:
810
+ pass
811
+
812
+ # Otherwise see if the parameter is a parameter name
813
+ try:
814
+ inputString = self.p[parameter]
815
+ except KeyError:
816
+ success = False
817
+ return success, array
818
+ # If it is then get the value of that parameter
819
+ try:
820
+ array = np.zeros(dims, np.float32) + float(inputString)
821
+ except ValueError:
822
+ try:
823
+ fid = gdal.Open(inputString, gdal.GA_ReadOnly)
824
+ if self.subset:
825
+ array = fid.GetRasterBand(band).ReadAsArray(self.subset[0],
826
+ self.subset[1],
827
+ self.subset[2],
828
+ self.subset[3]).astype(np.float32)
829
+ else:
830
+ array = fid.GetRasterBand(band).ReadAsArray().astype(np.float32)
831
+ except AttributeError:
832
+ print("%s image not present for parameter %s" % (inputString, parameter))
833
+ success = False
834
+ finally:
835
+ fid = None
836
+
837
+ return success, array
838
+
839
+ def write_raster_output(self, outfile, output, fields):
840
+ '''Write the specified arrays of a dictionary to a raster file.
841
+
842
+ Parameters
843
+ ----------
844
+ outfile : string
845
+ Path to the output raster. If the path ends in ".nc" the output will be saved in a
846
+ netCDF file. If the path ends in ".vrt" then the outputs will be saved in a GDAL
847
+ virtual raster with the actual data saved as GeoTIFFs (one per field) in .data
848
+ sub-folder. Otherwise, the output will be saved as one GeoTIFF.
849
+ output : dict
850
+ The dictionary containing the output data arrays.
851
+ fields : string list
852
+ The list of output fields from the output dictionary to save to file.
853
+
854
+ Returns
855
+ -------
856
+ None
857
+ '''
858
+
859
+ # If the output file has .nc extension then save it as netCDF,
860
+ # otherwise assume that the output should be a GeoTIFF
861
+ ext = splitext(outfile)[1]
862
+ if ext.lower() == ".nc":
863
+ driver_name = "netCDF"
864
+ opt = ["FORMAT=NC4"]
865
+ opt = []
866
+ elif ext.lower() == ".vrt":
867
+ driver_name = "VRT"
868
+ opt = []
869
+ else:
870
+ driver_name = "COG"
871
+ opt = ['COMPRESS=DEFLATE', 'PREDICTOR=YES', 'BIGTIFF=IF_SAFER']
872
+ if driver_name in ["COG", "netCDF"]:
873
+ # Save the data using GDAL by first creating a MEM layer and later using Translate
874
+ rows, cols = np.shape(output['H1'])
875
+ driver = gdal.GetDriverByName("MEM")
876
+ nbands = len(fields)
877
+ ds = driver.Create("MEM", cols, rows, nbands, gdal.GDT_Float32)
878
+ ds.SetGeoTransform(self.geo)
879
+ ds.SetProjection(self.prj)
880
+ for i, field in enumerate(fields):
881
+ band = ds.GetRasterBand(i + 1)
882
+ band.WriteArray(output[field])
883
+ band.SetStatistics(*band.ComputeStatistics(0))
884
+ out_ds = gdal.Translate(outfile, ds, format=driver_name, creationOptions=opt,
885
+ noData=None)
886
+ # If GDAL drivers for other formats do not exist then default to GeoTiff
887
+ if out_ds is None:
888
+ print("Warning: Selected GDAL driver is not supported! Saving as GeoTiff!")
889
+ driver_name = "GTiff"
890
+ opt = ['COMPRESS=DEFLATE', 'PREDICTOR=1', 'BIGTIFF=IF_SAFER']
891
+ gdal.Translate(outfile, ds, format=driver_name, creationOptions=opt, noData=None)
892
+ out_ds = None
893
+ ds = None
894
+ # In case of netCDF format use netCDF4 module to assign proper names
895
+ # to variables (GDAL can't do this). Also it seems that GDAL has
896
+ # problems assigning projection to all the bands so fix that.
897
+ if driver_name == "netCDF":
898
+ ds = Dataset(outfile, 'a')
899
+ grid_mapping = ds["Band1"].grid_mapping
900
+ for i, field in enumerate(fields):
901
+ ds.renameVariable('Band{}'.format(i + 1), field)
902
+ ds[field].grid_mapping = grid_mapping
903
+ ds.close()
904
+
905
+ else:
906
+ # Save each individual oputput in a GeoTIFF file in .data directory using GDAL
907
+ out_dir = join(dirname(outfile),
908
+ splitext(basename(outfile))[0] + ".data")
909
+ if not exists(out_dir):
910
+ mkdir(out_dir)
911
+ out_files = []
912
+ rows, cols = np.shape(output['H1'])
913
+ outfile_tif = (splitext(basename(outfile))[0]).replace("_ancillary", "")
914
+ for i, field in enumerate(fields):
915
+ driver = gdal.GetDriverByName("MEM")
916
+ out_path = join(out_dir, f"{outfile_tif}_{field}.tif")
917
+ ds = driver.Create("MEM", cols, rows, 1, gdal.GDT_Float32)
918
+ ds.SetGeoTransform(self.geo)
919
+ ds.SetProjection(self.prj)
920
+ band = ds.GetRasterBand(1)
921
+ band.WriteArray(output[field])
922
+ opt = ['COMPRESS=DEFLATE', 'PREDICTOR=YES', 'BIGTIFF=IF_SAFER']
923
+ out_ds = gdal.Translate(out_path, ds, format="COG", creationOptions=opt,
924
+ noData=None, stats=True)
925
+ # If GDAL drivers for other formats do not exist then default to GeoTiff
926
+ if out_ds is None:
927
+ opt = ['COMPRESS=DEFLATE', 'PREDICTOR=1', 'BIGTIFF=IF_SAFER']
928
+ gdal.Translate(out_path, ds, format="GTiff", creationOptions=opt, noData=None,
929
+ stats=True)
930
+ out_ds = None
931
+ ds = None
932
+ out_files.extend([out_path])
933
+
934
+ # Create the Virtual Raster Table
935
+ out_vrt = out_dir.replace('.data', '.vrt')
936
+ print(out_files)
937
+ gdal.BuildVRT(out_vrt, out_files, separate=True)
938
+
939
+ def _get_output_structure(self):
940
+ ''' Output fields' names for TSEB model.
941
+
942
+ Parameters
943
+ ----------
944
+ None
945
+
946
+ Returns
947
+ -------
948
+ output_structure: ordered dict
949
+ Names of the output fields as keys and instructions on whether the output
950
+ should be saved to file as values.
951
+ '''
952
+
953
+ output_structure = OrderedDict([
954
+ # Energy fluxes
955
+ ('R_n1', S_P), # net radiation reaching the surface at time t1
956
+ ('R_ns1', S_A), # net shortwave radiation reaching the surface at time t1
957
+ ('R_nl1', S_A), # net longwave radiation reaching the surface at time t1
958
+ ('delta_R_n1', S_A), # net radiation divergence in the canopy at time t1
959
+ ('Sn_S1', S_N), # Shortwave radiation reaching the soil at time t1
960
+ ('Sn_C1', S_N), # Shortwave radiation intercepted by the canopy at time t1
961
+ ('Ln_S1', S_N), # Longwave radiation reaching the soil at time t1
962
+ ('Ln_C1', S_N), # Longwave radiation intercepted by the canopy at time t1
963
+ ('H_C1', S_A), # canopy sensible heat flux (W/m^2) at time t1
964
+ ('H_S1', S_N), # soil sensible heat flux (W/m^2) at time t1
965
+ ('H1', S_P), # total sensible heat flux (W/m^2) at time t1
966
+ ('LE_C1', S_A), # canopy latent heat flux (W/m^2) at time t1
967
+ ('LE_S1', S_N), # soil latent heat flux (W/m^2) at time t1
968
+ ('LE1', S_P), # total latent heat flux (W/m^2) at time t1
969
+ ('LE_partition', S_A), # Latent Heat Flux Partition (LEc/LE) at time t1
970
+ ('G1', S_P), # ground heat flux (W/m^2) at time t1
971
+ # temperatures (might not be accurate)
972
+ ('T_C1', S_A), # canopy temperature at time t1 (deg C)
973
+ ('T_S1', S_A), # soil temperature at time t1 (deg C)
974
+ ('T_AC1', S_N), # air temperature at the canopy interface at time t1 (deg C)
975
+ # resistances
976
+ # resistance to heat transport in the surface layer (s/m) at time t1
977
+ ('R_A1', S_A),
978
+ # resistance to heat transport in the canopy surface layer (s/m) at time t1
979
+ ('R_x1', S_A),
980
+ # resistance to heat transport from the soil surface (s/m) at time t1 fluxes
981
+ ('R_S1', S_A),
982
+ # miscaleneous
983
+ ('albedo1', S_N), # surface albedo (Rs_out/Rs_in)
984
+ ('omega0', S_N), # nadir view vegetation clumping factor
985
+ ('alpha', S_N), # the priestly Taylor factor
986
+ ('Ri', S_N), # Richardson number at time t1
987
+ ('L', S_A), # Monin Obukhov Length at time t1
988
+ ('u_friction', S_A), # Friction velocity
989
+ ('theta_s1', S_N), # Sun zenith angle at time t1
990
+ ('F', S_N), # Leaf Area Index
991
+ ('z_0M', S_N), # Aerodynamic roughness length for momentum trasport (m)
992
+ ('d_0', S_N), # Zero-plane displacement height (m)
993
+ ('Skyl', S_N),
994
+ ('flag', S_A), # Quality flag
995
+ ('n_iterations', S_N)]) # Number of iterations before model converged to stable value
996
+
997
+
998
+ if self.calc_daily_ET:
999
+ output_structure['ET_day'] = S_P
1000
+
1001
+ if self.water_stress:
1002
+ output_structure['LE_0'] = S_A
1003
+ output_structure['LE_C_0'] = S_A
1004
+ output_structure['CWSI'] = S_P
1005
+
1006
+ return output_structure
1007
+
1008
+ def _get_input_structure(self):
1009
+ ''' Input fields' names for TSEB_PT model. Only relevant for image processing mode.
1010
+
1011
+ Parameters
1012
+ ----------
1013
+ None
1014
+
1015
+ Returns
1016
+ -------
1017
+ input_fields: string ordered dict
1018
+ Names (keys) and descriptions (values) of TSEB_PT input fields.
1019
+ '''
1020
+
1021
+ input_fields = OrderedDict([
1022
+ # General parameters
1023
+ ("T_R1", "Land Surface Temperature"),
1024
+ ("LAI", "Leaf Area Index"),
1025
+ ("VZA", "View Zenith Angle for LST"),
1026
+ ("landcover", "Landcover"),
1027
+ ("input_mask", "Input Mask"),
1028
+ # Vegetation parameters
1029
+ ("f_c", "Fractional Cover"),
1030
+ ("h_C", "Canopy Height"),
1031
+ ("w_C", "Canopy Width Ratio"),
1032
+ ("f_g", "Green Vegetation Fraction"),
1033
+ ("leaf_width", "Leaf Width"),
1034
+ ("x_LAD", "Leaf Angle Distribution"),
1035
+ ("alpha_PT", "Initial Priestley-Taylor Alpha Value"),
1036
+ # Spectral Properties
1037
+ ("rho_vis_C", "Leaf PAR Reflectance"),
1038
+ ("tau_vis_C", "Leaf PAR Transmitance"),
1039
+ ("rho_nir_C", "Leaf NIR Reflectance"),
1040
+ ("tau_nir_C", "Leaf NIR Transmitance"),
1041
+ ("rho_vis_S", "Soil PAR Reflectance"),
1042
+ ("rho_nir_S", "Soil NIR Reflectance"),
1043
+ ("emis_C", "Leaf Emissivity"),
1044
+ ("emis_S", "Soil Emissivity"),
1045
+ # Illumination conditions
1046
+ ("lat", "Latitude"),
1047
+ ("lon", "Longitude"),
1048
+ ("stdlon", "Standard Longitude"),
1049
+ ("time", "Observation Time for LST"),
1050
+ ("DOY", "Observation Day Of Year for LST"),
1051
+ ("SZA", "Sun Zenith Angle"),
1052
+ ("SAA", "Sun Azimuth Angle"),
1053
+ # Meteorological parameters
1054
+ ("T_A1", "Air temperature"),
1055
+ ("u", "Wind Speed"),
1056
+ ("ea", "Vapour Pressure"),
1057
+ ("alt", "Altitude"),
1058
+ ("p", "Pressure"),
1059
+ ("S_dn", "Shortwave Irradiance"),
1060
+ ("z_T", "Air Temperature Height"),
1061
+ ("z_u", "Wind Speed Height"),
1062
+ ("z0_soil", "Soil Roughness"),
1063
+ ("L_dn", "Longwave Irradiance"),
1064
+ # Resistance parameters
1065
+ ("KN_b", "Kustas and Norman Resistance Parameter b"),
1066
+ ("KN_c", "Kustas and Norman Resistance Parameter c"),
1067
+ ("KN_C_dash", "Kustas and Norman Resistance Parameter c-dash"),
1068
+ # Soil heat flux parameter
1069
+ ("G", "Soil Heat Flux Parameter"),
1070
+ ('S_dn_24', 'Daily shortwave irradiance')])
1071
+
1072
+ return input_fields
1073
+
1074
+ def _set_special_model_input(self, field, dims):
1075
+ ''' Special processing for setting certain input fields. Only relevant for image processing
1076
+ mode.
1077
+
1078
+ Parameters
1079
+ ----------
1080
+ field : string
1081
+ The name of the input field for which the special processing is needed.
1082
+ dims : int list
1083
+ The dimensions of the output parameter array.
1084
+
1085
+ Returns
1086
+ -------
1087
+ success : boolean
1088
+ True is the parameter was succefully set, false otherwise.
1089
+ inputs : dict
1090
+ Dictionary with keys holding input field name and value holding the input value.
1091
+ '''
1092
+ return False, None
1093
+
1094
+ def _get_required_data_columns(self):
1095
+ ''' Input columns' names required in an input asci table for TSEB_PT model. Only relevant
1096
+ for point time-series processing mode.
1097
+
1098
+ Parameters
1099
+ ----------
1100
+ None
1101
+
1102
+ Returns
1103
+ -------
1104
+ required_columns : string tuple
1105
+ Names of the required input columns.
1106
+ '''
1107
+
1108
+ required_columns = ('year',
1109
+ 'DOY',
1110
+ 'time',
1111
+ 'T_R1',
1112
+ 'VZA',
1113
+ 'T_A1',
1114
+ 'u',
1115
+ 'ea',
1116
+ 'S_dn',
1117
+ 'LAI',
1118
+ 'h_C')
1119
+ return required_columns
1120
+
1121
+ def _get_subset(self, roi_shape, raster_proj_wkt, raster_geo_transform):
1122
+
1123
+ # Find extent of ROI in roiShape projection
1124
+ roi = ogr.Open(roi_shape)
1125
+ roi_layer = roi.GetLayer()
1126
+ roi_extent = roi_layer.GetExtent()
1127
+
1128
+ # Convert the extent to raster projection
1129
+ roi_proj = roi_layer.GetSpatialRef()
1130
+ raster_proj = osr.SpatialReference()
1131
+ raster_proj.ImportFromWkt(raster_proj_wkt)
1132
+ try:
1133
+ # For GDAL 3 indicate the "legacy" axis order
1134
+ # https://gdal.org/tutorials/osr_api_tut.html#crs-and-axis-order
1135
+ # https://github.com/OSGeo/gdal/issues/1546
1136
+ roi_proj = roi_proj.Clone()
1137
+ roi_proj.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
1138
+ raster_proj = raster_proj.Clone()
1139
+ raster_proj.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
1140
+ except AttributeError:
1141
+ # For GDAL 2 do nothing
1142
+ pass
1143
+ transform = osr.CoordinateTransformation(roi_proj, raster_proj)
1144
+ point_UL = ogr.CreateGeometryFromWkt("POINT ({} {})"
1145
+ .format(min(roi_extent[0], roi_extent[1]),
1146
+ max(roi_extent[2], roi_extent[3])))
1147
+ point_UL.Transform(transform)
1148
+ point_UL = point_UL.GetPoint()
1149
+ point_LR = ogr.CreateGeometryFromWkt("POINT ({} {})"
1150
+ .format(max(roi_extent[0], roi_extent[1]),
1151
+ min(roi_extent[2], roi_extent[3])))
1152
+ point_LR.Transform(transform)
1153
+ point_LR = point_LR.GetPoint()
1154
+
1155
+ # Get pixel location of this extent
1156
+ ulX = raster_geo_transform[0]
1157
+ ulY = raster_geo_transform[3]
1158
+ pixel_size = raster_geo_transform[1]
1159
+ pixel_UL = [max(int(math.floor((ulY - point_UL[1]) / pixel_size)), 0),
1160
+ max(int(math.floor((point_UL[0] - ulX) / pixel_size)), 0)]
1161
+ pixel_LR = [int(round((ulY - point_LR[1]) / pixel_size)),
1162
+ int(round((point_LR[0] - ulX) / pixel_size))]
1163
+
1164
+ # Get projected extent
1165
+ point_proj_UL = (ulX + pixel_UL[1] * pixel_size, ulY - pixel_UL[0] * pixel_size)
1166
+
1167
+ # Convert to xoff, yoff, xcount, ycount as required by GDAL ReadAsArray()
1168
+ subset_pix = [pixel_UL[1], pixel_UL[0],
1169
+ pixel_LR[1] - pixel_UL[1], pixel_LR[0] - pixel_UL[0]]
1170
+
1171
+ # Get the geo transform of the subset
1172
+ subset_geo_transform = [point_proj_UL[0], pixel_size, raster_geo_transform[2],
1173
+ point_proj_UL[1], raster_geo_transform[4], -pixel_size]
1174
+
1175
+ return subset_pix, subset_geo_transform
1176
+
1177
+
1178
+ class PyDTD(PyTSEB):
1179
+
1180
+ def __init__(self, parameters):
1181
+ super().__init__(parameters)
1182
+
1183
+ def _get_input_structure(self):
1184
+ ''' Input fields' names for DTD model. Only relevant for image processing mode.
1185
+
1186
+ Parameters
1187
+ ----------
1188
+ None
1189
+
1190
+ Returns
1191
+ -------
1192
+ outputStructure: string ordered dict
1193
+ Names (keys) and descriptions (values) of DTD input fields.
1194
+ '''
1195
+
1196
+ input_fields = super()._get_input_structure()
1197
+ input_fields["T_R0"] = "Early Morning Land Surface Temperature"
1198
+ input_fields["T_A0"] = "Early Morning Air Temperature"
1199
+ return input_fields
1200
+
1201
+ def _get_required_data_columns(self):
1202
+ ''' Input columns' names required in an input asci table for TSEB_PT model. Only relevant
1203
+ for point time-series processing mode.
1204
+
1205
+ Parameters
1206
+ ----------
1207
+ None
1208
+
1209
+ Returns
1210
+ -------
1211
+ required_columns : string tuple
1212
+ Names of the required input columns.
1213
+ '''
1214
+
1215
+ required_columns = ('year',
1216
+ 'DOY',
1217
+ 'time',
1218
+ 'T_R0',
1219
+ 'T_R1',
1220
+ 'VZA',
1221
+ 'T_A0',
1222
+ 'T_A1',
1223
+ 'u',
1224
+ 'ea',
1225
+ 'S_dn',
1226
+ 'LAI',
1227
+ 'h_C')
1228
+
1229
+ return required_columns
1230
+
1231
+ def _call_flux_model_veg(self, in_data, out_data, model_params, i):
1232
+ ''' Call a DTD model to calculate fluxes for data points containing vegetation.
1233
+
1234
+ Parameters
1235
+ ----------
1236
+ in_data : dict
1237
+ The input data for the model.
1238
+ out_data : dict
1239
+ Dict containing the output data from the model which will be updated. It also contains
1240
+ previusly calculated shortwave radiation and roughness values which are used as input
1241
+ data.
1242
+
1243
+ Returns
1244
+ -------
1245
+ None
1246
+ '''
1247
+
1248
+ [out_data['flag'][i], out_data['T_S1'][i], out_data['T_C1'][i],
1249
+ out_data['T_AC1'][i], out_data['Ln_S1'][i], out_data['Ln_C1'][i],
1250
+ out_data['LE_C1'][i], out_data['H_C1'][i], out_data['LE_S1'][i],
1251
+ out_data['H_S1'][i], out_data['G1'][i], out_data['R_S1'][i],
1252
+ out_data['R_x1'][i], out_data['R_A1'][i], out_data['u_friction'][i],
1253
+ out_data['L'][i], out_data['Ri'], out_data['n_iterations'][i]] = TSEB.DTD(
1254
+ in_data['T_R0'][i],
1255
+ in_data['T_R1'][i],
1256
+ in_data['VZA'][i],
1257
+ in_data['T_A0'][i],
1258
+ in_data['T_A1'][i],
1259
+ in_data['u'][i],
1260
+ in_data['ea'][i],
1261
+ in_data['p'][i],
1262
+ out_data['Sn_C1'][i],
1263
+ out_data['Sn_S1'][i],
1264
+ in_data['L_dn'][i],
1265
+ in_data['LAI'][i],
1266
+ in_data['h_C'][i],
1267
+ in_data['emis_C'][i],
1268
+ in_data['emis_S'][i],
1269
+ out_data['z_0M'][i],
1270
+ out_data['d_0'][i],
1271
+ in_data['z_u'][i],
1272
+ in_data['z_T'][i],
1273
+ f_c=in_data['f_c'][i],
1274
+ w_C=in_data['w_C'][i],
1275
+ f_g=in_data['f_g'][i],
1276
+ leaf_width=in_data['leaf_width'][i],
1277
+ z0_soil=in_data['z0_soil'][i],
1278
+ alpha_PT=in_data['alpha_PT'][i],
1279
+ x_LAD=in_data['x_LAD'][i],
1280
+ calcG_params=[model_params["calcG_params"][0],
1281
+ model_params["calcG_params"][1][i]],
1282
+ resistance_form=[model_params["resistance_form"][0],
1283
+ {k: model_params["resistance_form"][1][k][i]
1284
+ for k in model_params["resistance_form"][1]}])
1285
+
1286
+ def _call_flux_model_soil(self, in_data, out_data, model_params, i):
1287
+ ''' Call a OSEB model to calculate soil fluxes for data points containing no vegetation.
1288
+
1289
+ Parameters
1290
+ ----------
1291
+ in_data : dict
1292
+ The input data for the model.
1293
+ out_data : dict
1294
+ Dict containing the output data from the model which will be updated. It also contains
1295
+ previusly calculated shortwave radiation and roughness values which are used as input
1296
+ data.
1297
+
1298
+ Returns
1299
+ -------
1300
+ None
1301
+ '''
1302
+
1303
+ [out_data['flag'][i],
1304
+ out_data['Ln_S1'][i],
1305
+ out_data['LE_S1'][i],
1306
+ out_data['H_S1'][i],
1307
+ out_data['G1'][i],
1308
+ out_data['R_A1'][i],
1309
+ out_data['u_friction'][i],
1310
+ out_data['L'][i],
1311
+ out_data['n_iterations'][i]] = TSEB.OSEB(in_data['T_R1'][i],
1312
+ in_data['T_A1'][i],
1313
+ in_data['u'][i],
1314
+ in_data['ea'][i],
1315
+ in_data['p'][i],
1316
+ out_data['Sn_S1'][i],
1317
+ in_data['L_dn'][i],
1318
+ in_data['emis_S'][i],
1319
+ out_data['z_0M'][i],
1320
+ out_data['d_0'][i],
1321
+ in_data['z_u'][i],
1322
+ in_data['z_T'][i],
1323
+ calcG_params=[model_params["calcG_params"][0],
1324
+ model_params["calcG_params"][1][i]],
1325
+ T0_K=(in_data['T_R0'][i], in_data['T_A0'][i]))
1326
+
1327
+
1328
+ class PyTSEB2T(PyTSEB):
1329
+
1330
+ def __init__(self, parameters):
1331
+ super().__init__(parameters)
1332
+
1333
+ def _get_input_structure(self):
1334
+ ''' Input fields' names for TSEB_2T model. Only relevant for image processing mode.
1335
+
1336
+ Parameters
1337
+ ----------
1338
+ None
1339
+
1340
+ Returns
1341
+ -------
1342
+ outputStructure: string ordered dict
1343
+ Names (keys) and descriptions (values) of TSEB_2T input fields.
1344
+ '''
1345
+
1346
+ input_fields = super()._get_input_structure()
1347
+ del input_fields["T_R1"]
1348
+ input_fields["T_C"] = "Canopy Temperature"
1349
+ input_fields["T_S"] = "Soil Temperature"
1350
+ return input_fields
1351
+
1352
+ def _set_special_model_input(self, field, dims):
1353
+ ''' Special processing for setting certain input fields. Only relevant for image processing
1354
+ mode.
1355
+
1356
+ Parameters
1357
+ ----------
1358
+ field : string
1359
+ The name of the input field for which the special processing is needed.
1360
+ dims : int list
1361
+ The dimensions of the output parameter array.
1362
+
1363
+ Returns
1364
+ -------
1365
+ success : boolean
1366
+ True is the parameter was succefully set, false otherwise.
1367
+ array : float array
1368
+ The set parameter array.
1369
+ '''
1370
+
1371
+ if field == "T_C":
1372
+ success, val = self._set_param_array("T_R1", dims)
1373
+ inputs = {field: val}
1374
+ elif field == "T_S":
1375
+ success, val = self._set_param_array("T_R1", dims, band=2)
1376
+ inputs = {field: val}
1377
+ else:
1378
+ success = False
1379
+ inputs = None
1380
+ return success, inputs
1381
+
1382
+ def _get_required_data_columns(self):
1383
+ ''' Input columns' names required in an input asci table for TSEB_PT model. Only relevant
1384
+ for point time-series processing mode.
1385
+
1386
+ Parameters
1387
+ ----------
1388
+ None
1389
+
1390
+ Returns
1391
+ -------
1392
+ required_columns : string tuple
1393
+ Names of the required input columns.
1394
+ '''
1395
+
1396
+ required_columns = ('year',
1397
+ 'DOY',
1398
+ 'time',
1399
+ 'T_C',
1400
+ 'T_S',
1401
+ 'T_A1',
1402
+ 'u',
1403
+ 'ea',
1404
+ 'S_dn',
1405
+ 'LAI',
1406
+ 'h_C')
1407
+ return required_columns
1408
+
1409
+ def _call_flux_model_veg(self, in_data, out_data, model_params, i):
1410
+ ''' Call a TSEB_2T model to calculate fluxes for data points containing vegetation.
1411
+
1412
+ Parameters
1413
+ ----------
1414
+ in_data : dict
1415
+ The input data for the model.
1416
+ out_data : dict
1417
+ Dict containing the output data from the model which will be updated. It also contains
1418
+ previusly calculated shortwave radiation and roughness values which are used as input
1419
+ data.
1420
+
1421
+ Returns
1422
+ -------
1423
+ None
1424
+ '''
1425
+
1426
+ [out_data['flag'][i], out_data['T_AC1'][i], out_data['Ln_S1'][i],
1427
+ out_data['Ln_C1'][i], out_data['LE_C1'][i], out_data['H_C1'][i],
1428
+ out_data['LE_S1'][i], out_data['H_S1'][i], out_data['G1'][i],
1429
+ out_data['R_S1'][i], out_data['R_x1'][i], out_data['R_A1'][i],
1430
+ out_data['u_friction'][i], out_data['L'][i], out_data['n_iterations'][i]] = TSEB.TSEB_2T(
1431
+ in_data['T_C'][i],
1432
+ in_data['T_S'][i],
1433
+ in_data['T_A1'][i],
1434
+ in_data['u'][i],
1435
+ in_data['ea'][i],
1436
+ in_data['p'][i],
1437
+ out_data['Sn_C1'][i],
1438
+ out_data['Sn_S1'][i],
1439
+ in_data['L_dn'][i],
1440
+ in_data['LAI'][i],
1441
+ in_data['h_C'][i],
1442
+ in_data['emis_C'][i],
1443
+ in_data['emis_S'][i],
1444
+ out_data['z_0M'][i],
1445
+ out_data['d_0'][i],
1446
+ in_data['z_u'][i],
1447
+ in_data['z_T'][i],
1448
+ f_c=in_data['f_c'][i],
1449
+ f_g=in_data['f_g'][i],
1450
+ w_C=in_data['w_C'][i],
1451
+ leaf_width=in_data['leaf_width'][i],
1452
+ z0_soil=in_data['z0_soil'][i],
1453
+ alpha_PT=in_data['alpha_PT'][i],
1454
+ x_LAD=in_data['x_LAD'][i],
1455
+ calcG_params=[model_params["calcG_params"][0],
1456
+ model_params["calcG_params"][1][i]],
1457
+ resistance_form=[model_params["resistance_form"][0],
1458
+ {k: model_params["resistance_form"][1][k][i]
1459
+ for k in model_params["resistance_form"][1]}])
1460
+
1461
+ def _call_flux_model_soil(self, in_data, out_data, model_params, i):
1462
+ ''' Call a OSEB model to calculate soil fluxes for data points containing no vegetation.
1463
+
1464
+ Parameters
1465
+ ----------
1466
+ in_data : dict
1467
+ The input data for the model.
1468
+ out_data : dict
1469
+ Dict containing the output data from the model which will be updated. It also contains
1470
+ previusly calculated shortwave radiation and roughness values which are used as input
1471
+ data.
1472
+
1473
+ Returns
1474
+ -------
1475
+ None
1476
+ '''
1477
+
1478
+ [out_data['flag'][i],
1479
+ out_data['Ln_S1'][i],
1480
+ out_data['LE_S1'][i],
1481
+ out_data['H_S1'][i],
1482
+ out_data['G1'][i],
1483
+ out_data['R_A1'][i],
1484
+ out_data['u_friction'][i],
1485
+ out_data['L'][i],
1486
+ out_data['n_iterations'][i]] = TSEB.OSEB(in_data['T_S'][i],
1487
+ in_data['T_A1'][i],
1488
+ in_data['u'][i],
1489
+ in_data['ea'][i],
1490
+ in_data['p'][i],
1491
+ out_data['Sn_S1'][i],
1492
+ in_data['L_dn'][i],
1493
+ in_data['emis_S'][i],
1494
+ out_data['z_0M'][i],
1495
+ out_data['d_0'][i],
1496
+ in_data['z_u'][i],
1497
+ in_data['z_T'][i],
1498
+ calcG_params=[model_params["calcG_params"][0],
1499
+ model_params["calcG_params"][1][i]])
1500
+
1501
+
1502
+ class PydisTSEB(PyTSEB):
1503
+
1504
+ def __init__(self, parameters):
1505
+
1506
+ super().__init__(parameters)
1507
+ # Method for ensuring consistency between low- and high-resolution fluxes
1508
+ self.flux_LR_method = self.p["flux_LR_method"]
1509
+ # Correct LST (if True) or air temperature (if false) during disaggregation.
1510
+ self.correct_LST = self.p["correct_LST"]
1511
+
1512
+ def _get_input_structure(self):
1513
+ ''' Input fields' names for disTSEB model. Only relevant for image processing mode.
1514
+
1515
+ Parameters
1516
+ ----------
1517
+ None
1518
+
1519
+ Returns
1520
+ -------
1521
+ outputStructure: string ordered dict
1522
+ Names (keys) and descriptions (values) of disTSEB input fields.
1523
+ '''
1524
+
1525
+ input_fields = super()._get_input_structure()
1526
+ input_fields["flux_LR"] = "pyTSEB Low Resolution Flux date"
1527
+ input_fields["flux_LR_ancillary"] = "pyTSEB Low Resolution ancillary Flux data"
1528
+
1529
+ return input_fields
1530
+
1531
+ def _get_output_structure(self):
1532
+ ''' Input fields' names for disTSEB model. Only relevant for image processing mode.
1533
+
1534
+ Parameters
1535
+ ----------
1536
+ None
1537
+
1538
+ Returns
1539
+ -------
1540
+ outputStructure: string ordered dict
1541
+ Names (keys) and descriptions (values) of disTSEB input fields.
1542
+ '''
1543
+
1544
+ output_structure = super()._get_output_structure()
1545
+ output_structure["T_offset"] = S_A
1546
+ output_structure["T_offset_orig"] = S_A
1547
+ output_structure["counter"] = S_A
1548
+ return output_structure
1549
+
1550
+ def _set_special_model_input(self, field, dims):
1551
+ ''' Special processing for setting certain input fields. Only relevant for image processing
1552
+ mode.
1553
+
1554
+ Parameters
1555
+ ----------
1556
+ field : string
1557
+ The name of the input field for which the special processing is needed.
1558
+ dims : int list
1559
+ The dimensions of the output parameter array.
1560
+
1561
+ Returns
1562
+ -------
1563
+ success : boolean
1564
+ True is the parameter was succefully set, false otherwise.
1565
+ array : float array
1566
+ The set parameter array.
1567
+ '''
1568
+ if field in ["flux_LR", "flux_LR_ancillary"]:
1569
+ # Low resolution data in case disaggregation is to be used.
1570
+ inputs = {}
1571
+ fid = gdal.Open(self.p[field], gdal.GA_ReadOnly)
1572
+ if fid is None:
1573
+ print("ERROR: Low resolution data for disaggregation is not avaiable.")
1574
+ return False, None
1575
+ prj_LR = fid.GetProjection()
1576
+ geo_LR = fid.GetGeoTransform()
1577
+ subset = []
1578
+ if "subset" in self.p:
1579
+ subset, geo_LR = self._get_subset(self.p["subset"], prj_LR, geo_LR)
1580
+ inputs[field] = fid.GetRasterBand(1).ReadAsArray(subset[0],
1581
+ subset[1],
1582
+ subset[2],
1583
+ subset[3]).astype(np.float32)
1584
+ else:
1585
+ inputs[field] = fid.GetRasterBand(1).ReadAsArray().astype(np.float32)
1586
+ inputs['scale'] = [geo_LR, prj_LR, self.geo, self.prj]
1587
+ success = True
1588
+ else:
1589
+ success = False
1590
+ inputs = None
1591
+
1592
+ return success, inputs
1593
+
1594
+ def _call_flux_model_soil(self, in_data, out_data, model_params, i):
1595
+ return
1596
+
1597
+ def _call_flux_model_veg(self, in_data, out_data, model_params, i):
1598
+ ''' Call a dis_TSEB model to calculate fluxes
1599
+
1600
+ Parameters
1601
+ ----------
1602
+ in_data : dict
1603
+ The input data for the model.
1604
+ out_data : dict
1605
+ Dict containing the output data from the model which will be updated. It also contains
1606
+ previusly calculated shortwave radiation and roughness values which are used as input
1607
+ data.
1608
+
1609
+ Returns
1610
+ -------
1611
+ None
1612
+ '''
1613
+
1614
+ print('Running dis TSEB for the whole image')
1615
+
1616
+ [out_data['flag'],
1617
+ out_data['T_S1'],
1618
+ out_data['T_C1'],
1619
+ out_data['T_AC1'],
1620
+ out_data['Ln_S1'],
1621
+ out_data['Ln_C1'],
1622
+ out_data['LE_C1'],
1623
+ out_data['H_C1'],
1624
+ out_data['LE_S1'],
1625
+ out_data['H_S1'],
1626
+ out_data['G1'],
1627
+ out_data['R_S1'],
1628
+ out_data['R_x1'],
1629
+ out_data['R_A1'],
1630
+ out_data['u_friction'],
1631
+ out_data['L'],
1632
+ out_data['n_iterations'],
1633
+ out_data['T_offset'],
1634
+ out_data['counter'],
1635
+ out_data['T_offset_orig']] = dis_TSEB.dis_TSEB(
1636
+ in_data['flux_LR'],
1637
+ in_data['scale'],
1638
+ in_data['T_R1'],
1639
+ in_data['VZA'],
1640
+ in_data['T_A1'],
1641
+ in_data['u'],
1642
+ in_data['ea'],
1643
+ in_data['p'],
1644
+ out_data['Sn_C1'],
1645
+ out_data['Sn_S1'],
1646
+ in_data['L_dn'],
1647
+ in_data['LAI'],
1648
+ in_data['h_C'],
1649
+ in_data['emis_C'],
1650
+ in_data['emis_S'],
1651
+ out_data['z_0M'],
1652
+ out_data['d_0'],
1653
+ in_data['z_u'],
1654
+ in_data['z_T'],
1655
+ UseL=in_data['flux_LR_ancillary'],
1656
+ f_c=in_data['f_c'],
1657
+ f_g=in_data['f_g'],
1658
+ w_C=in_data['w_C'],
1659
+ leaf_width=in_data['leaf_width'],
1660
+ z0_soil=in_data['z0_soil'],
1661
+ alpha_PT=in_data['alpha_PT'],
1662
+ x_LAD=in_data['x_LAD'],
1663
+ calcG_params=model_params["calcG_params"],
1664
+ resistance_form=model_params["resistance_form"],
1665
+ flux_LR_method=self.flux_LR_method,
1666
+ correct_LST=self.correct_LST)