pthelma 0.99.3.dev0__cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl

Sign up to get free protection for your applications and to get access to all the features.
evaporation/cli.py ADDED
@@ -0,0 +1,729 @@
1
+ import configparser
2
+ import datetime as dt
3
+ import logging
4
+ import os
5
+ import sys
6
+ import traceback
7
+ from glob import glob
8
+ from io import StringIO
9
+
10
+ import click
11
+ import iso8601
12
+ import numpy as np
13
+ from osgeo import gdal, ogr, osr
14
+
15
+ from evaporation import PenmanMonteith
16
+ from hspatial import NODATAVALUE
17
+ from htimeseries import HTimeseries, TzinfoFromString
18
+ from pthelma._version import __version__
19
+
20
+ gdal.UseExceptions()
21
+
22
+
23
+ class WrongValueError(configparser.Error):
24
+ pass
25
+
26
+
27
+ class App:
28
+ def __init__(self, configfilename):
29
+ self.configfilename = configfilename
30
+
31
+ def run(self):
32
+ self.config = AppConfig(self.configfilename)
33
+ self.config.read()
34
+ self._setup_logger()
35
+ self._execute_with_error_handling()
36
+
37
+ def _execute_with_error_handling(self):
38
+ self.logger.info("Starting evaporation, " + dt.datetime.today().isoformat())
39
+ try:
40
+ self._execute()
41
+ except Exception as e:
42
+ self.logger.error(str(e))
43
+ self.logger.debug(traceback.format_exc())
44
+ self.logger.info(
45
+ "evaporation terminated with error, " + dt.datetime.today().isoformat()
46
+ )
47
+ raise click.ClickException(str(e))
48
+ else:
49
+ self.logger.info("Finished evaporation, " + dt.datetime.today().isoformat())
50
+
51
+ def _setup_logger(self):
52
+ self.logger = logging.getLogger("evaporation")
53
+ self._set_logger_handler()
54
+ self.logger.setLevel(self.config.loglevel.upper())
55
+
56
+ def _set_logger_handler(self):
57
+ if getattr(self.config, "logfile", None):
58
+ self.logger.addHandler(logging.FileHandler(self.config.logfile))
59
+ else:
60
+ self.logger.addHandler(logging.StreamHandler())
61
+
62
+ def _execute(self):
63
+ if self.config.is_spatial:
64
+ ProcessSpatial(self.config).execute()
65
+ else:
66
+ ProcessAtPoint(self.config).execute()
67
+
68
+
69
+ class AppConfig:
70
+ config_file_options = {
71
+ "logfile": {"fallback": ""},
72
+ "loglevel": {"fallback": "warning"},
73
+ "base_dir": {"fallback": ""},
74
+ "time_step": {},
75
+ "elevation": {"fallback": ""},
76
+ "albedo": {},
77
+ "nighttime_solar_radiation_ratio": {"fallback": 0.6},
78
+ "unit_converter_temperature": {"fallback": "x"},
79
+ "unit_converter_humidity": {"fallback": "x"},
80
+ "unit_converter_wind_speed": {"fallback": "x"},
81
+ "unit_converter_pressure": {"fallback": "x"},
82
+ "unit_converter_solar_radiation": {"fallback": "x"},
83
+ "temperature_min_prefix": {"fallback": "temperature_min"},
84
+ "temperature_max_prefix": {"fallback": "temperature_max"},
85
+ "temperature_prefix": {"fallback": "temperature"},
86
+ "humidity_prefix": {"fallback": "humidity"},
87
+ "humidity_min_prefix": {"fallback": "humidity_min"},
88
+ "humidity_max_prefix": {"fallback": "humidity_max"},
89
+ "wind_speed_prefix": {"fallback": "wind_speed"},
90
+ "pressure_prefix": {"fallback": "pressure"},
91
+ "solar_radiation_prefix": {"fallback": "solar_radiation"},
92
+ "sunshine_duration_prefix": {"fallback": "sunshine_duration"},
93
+ "evaporation_prefix": {"fallback": "evaporation"},
94
+ }
95
+
96
+ def __init__(self, configfilename):
97
+ self.configfilename = configfilename
98
+
99
+ def read(self):
100
+ try:
101
+ self._parse_config()
102
+ except (OSError, configparser.Error) as e:
103
+ sys.stderr.write(str(e))
104
+ raise click.ClickException(str(e))
105
+
106
+ def _parse_config(self):
107
+ self._read_config_file()
108
+ self._get_config_options()
109
+ self._parse_config_options()
110
+ self._parse_unit_converters()
111
+ self._parse_elevation()
112
+
113
+ def _read_config_file(self):
114
+ self.config = configparser.ConfigParser(interpolation=None)
115
+ try:
116
+ self._read_config_file_assuming_it_has_section_headers()
117
+ except configparser.MissingSectionHeaderError:
118
+ self._read_config_file_without_sections()
119
+
120
+ def _read_config_file_assuming_it_has_section_headers(self):
121
+ with open(self.configfilename) as f:
122
+ self.config.read_file(f)
123
+
124
+ def _read_config_file_without_sections(self):
125
+ with open(self.configfilename) as f:
126
+ configuration = "[General]\n" + f.read()
127
+ self.config.read_file(StringIO(configuration))
128
+
129
+ def _get_config_options(self):
130
+ self.options = {
131
+ opt: self.config.get("General", opt, **kwargs)
132
+ for opt, kwargs in self.config_file_options.items()
133
+ }
134
+ for key, value in self.options.items():
135
+ setattr(self, key, value)
136
+
137
+ def _parse_config_options(self):
138
+ self._parse_log_level()
139
+ self._parse_time_step()
140
+ self._parse_unit_converters()
141
+ self._set_is_spatial()
142
+ self._parse_elevation()
143
+ self._parse_albedo()
144
+ self._parse_nighttime_solar_radiation_ratio()
145
+
146
+ def _parse_log_level(self):
147
+ log_levels = ("ERROR", "WARNING", "INFO", "DEBUG")
148
+ self.loglevel = self.options["loglevel"].upper()
149
+ if self.loglevel not in log_levels:
150
+ raise WrongValueError("loglevel must be one of " + ", ".join(log_levels))
151
+
152
+ def _parse_time_step(self):
153
+ s = self.options["time_step"]
154
+ self._check_time_step(s)
155
+ self.time_step = s
156
+
157
+ def _check_time_step(self, s):
158
+ if s not in ("D", "H"):
159
+ raise WrongValueError(
160
+ '"{}" is not an appropriate time step; in this version of '
161
+ "vaporize, the step must be either D or H.".format(s)
162
+ )
163
+
164
+ def _parse_unit_converters(self):
165
+ vars = {"temperature", "humidity", "wind_speed", "pressure", "solar_radiation"}
166
+ self.unit_converters = {}
167
+ for var in vars:
168
+ config_item = "unit_converter_" + var
169
+ config_value = self.options[config_item]
170
+ lambda_defn = "lambda x: " + config_value
171
+ self._set_unit_converter(var, config_item, config_value, lambda_defn)
172
+
173
+ def _set_unit_converter(self, var, config_item, config_value, lambda_defn):
174
+ try:
175
+ self.unit_converters[var] = eval(lambda_defn)
176
+ except Exception as e:
177
+ raise WrongValueError(
178
+ "{} while parsing {} ({}): {}".format(
179
+ e.__class__.__name__, config_item, config_value, str(e)
180
+ )
181
+ )
182
+
183
+ def _parse_elevation(self):
184
+ s = self.options["elevation"]
185
+ if s == "":
186
+ self.elevation = None
187
+ else:
188
+ self.elevation = self._get_number_or_grid(s)
189
+ self._check_elevation()
190
+
191
+ def _check_elevation(self):
192
+ if self.is_spatial:
193
+ self._check_elevation_for_spatial()
194
+ else:
195
+ self._check_elevation_for_point()
196
+
197
+ def _check_elevation_for_spatial(self):
198
+ if self.elevation is None:
199
+ raise WrongValueError(
200
+ "elevation needs to be specified in the configuration file"
201
+ )
202
+ elif np.any(self.elevation < -427) or np.any(self.elevation > 8848):
203
+ raise WrongValueError("The elevation must be between -427 and 8848")
204
+
205
+ def _check_elevation_for_point(self):
206
+ if self.elevation is not None:
207
+ raise WrongValueError(
208
+ "elevation should only be specified in the configuration file "
209
+ "in spatial calculation (otherwise we get it from the hts files)"
210
+ )
211
+
212
+ def _parse_albedo(self):
213
+ s = self.options["albedo"].split()
214
+ self._check_albedo_is_one_or_twelve_items(s)
215
+ albedo = []
216
+ for item in s:
217
+ albedo.append(self._get_number_or_grid(item))
218
+ self._check_albedo_domain(albedo[-1])
219
+ self.albedo = albedo[0] if len(s) == 1 else albedo
220
+
221
+ def _check_albedo_is_one_or_twelve_items(self, s):
222
+ if len(s) not in (1, 12):
223
+ raise ValueError(
224
+ "Albedo must be either one item or 12 space-separated items"
225
+ )
226
+
227
+ def _check_albedo_domain(self, albedo):
228
+ value_to_test = albedo if isinstance(albedo, float) else albedo.all()
229
+ if value_to_test < 0.0 or value_to_test > 1.0:
230
+ raise ValueError("Albedo must be between 0.0 and 1.0")
231
+
232
+ def _parse_nighttime_solar_radiation_ratio(self):
233
+ s = self.options["nighttime_solar_radiation_ratio"]
234
+ self.nighttime_solar_radiation_ratio = self._get_number_or_grid(s)
235
+ self._check_nighttime_solar_radiation_ratio()
236
+
237
+ def _check_nighttime_solar_radiation_ratio(self):
238
+ a = self.nighttime_solar_radiation_ratio
239
+ if a < 0.4 or a > 0.8:
240
+ raise WrongValueError(
241
+ "The nighttime solar radiation ratio must " "be between 0.4 and 0.8"
242
+ )
243
+
244
+ def _get_number_or_grid(self, s):
245
+ """Return either a number or a grid from s, depending on its contents.
246
+
247
+ If string s holds a valid number, return it; otherwise try to open the
248
+ geotiff file whose filename is s and read its first band into the
249
+ returned numpy array.
250
+ """
251
+ try:
252
+ return float(s)
253
+ except ValueError:
254
+ return self._get_grid(s)
255
+
256
+ def _get_grid(self, s):
257
+ input_file = gdal.Open(os.path.join(self.options["base_dir"], s))
258
+ result = input_file.GetRasterBand(1).ReadAsArray()
259
+ nodata = input_file.GetRasterBand(1).GetNoDataValue()
260
+ if nodata is not None:
261
+ result = np.ma.masked_values(result, nodata, copy=False)
262
+ return result
263
+
264
+ def _set_is_spatial(self):
265
+ base_dir = self.options["base_dir"]
266
+ base_dir_has_tif_files = bool(glob(os.path.join(base_dir, "*.tif")))
267
+ base_dir_has_hts_files = bool(glob(os.path.join(base_dir, "*.hts")))
268
+ self._check_tif_hts_consistency(base_dir_has_tif_files, base_dir_has_hts_files)
269
+ self.is_spatial = base_dir_has_tif_files
270
+
271
+ def _check_tif_hts_consistency(self, has_tif, has_hts):
272
+ if has_tif and has_hts:
273
+ raise WrongValueError(
274
+ "Base directory {} contains both tif files and hts files; "
275
+ "this is not allowed.".format(self.options["base_dir"])
276
+ )
277
+ elif not has_tif and not has_hts:
278
+ raise WrongValueError(
279
+ "Base directory {} contains neither tif files nor hts files.".format(
280
+ self.options["base_dir"]
281
+ )
282
+ )
283
+
284
+
285
+ class ProcessAtPoint:
286
+ def __init__(self, config):
287
+ self.config = config
288
+
289
+ def execute(self):
290
+ self._read_input_timeseries()
291
+ self._setup_attrs()
292
+ self._check_all_timeseries_are_in_same_location_and_timezone()
293
+ self._get_location_in_wgs84()
294
+ self._prepare_penman_monteith_parameters()
295
+ self._prepare_resulting_htimeseries_object()
296
+ self._determine_variables_to_use_in_calculation()
297
+ self._calculate_evaporation()
298
+ self._save_result()
299
+
300
+ def _setup_attrs(self):
301
+ atimeseries = self.input_timeseries["wind_speed"]
302
+ self.location = atimeseries.location
303
+ self.timezone = getattr(atimeseries, "timezone", None)
304
+
305
+ def _read_input_timeseries(self):
306
+ vars = {
307
+ "temperature_min",
308
+ "temperature_max",
309
+ "temperature",
310
+ "humidity",
311
+ "humidity_min",
312
+ "humidity_max",
313
+ "wind_speed",
314
+ "pressure",
315
+ "solar_radiation",
316
+ "sunshine_duration",
317
+ }
318
+ self.input_timeseries = {}
319
+ for var in vars:
320
+ self._get_input_timeseries_for_var(var)
321
+
322
+ def _get_input_timeseries_for_var(self, var):
323
+ filename = os.path.join(
324
+ self.config.base_dir, getattr(self.config, var + "_prefix") + ".hts"
325
+ )
326
+ if not os.path.exists(filename):
327
+ return
328
+ with open(filename, "r", newline="\n") as f:
329
+ self.input_timeseries[var] = HTimeseries(f)
330
+
331
+ def _check_all_timeseries_are_in_same_location_and_timezone(self):
332
+ for i, (name, hts) in enumerate(self.input_timeseries.items()):
333
+ if i == 0:
334
+ reference_hts = hts
335
+ else:
336
+ self._compare_location_and_timezone_of(hts, reference_hts)
337
+
338
+ def _compare_location_and_timezone_of(self, hts1, hts2):
339
+ self._compare_locations_of(hts1, hts2)
340
+ self._compare_altitudes_of(hts1, hts2)
341
+ self._compare_timezones_of(hts1, hts2)
342
+
343
+ def _compare_locations_of(self, hts1, hts2):
344
+ loc1 = hts1.location
345
+ loc2 = hts2.location
346
+
347
+ abscissas_differ = self._numbers_are_wrong_or_differ(
348
+ loc1.get("abscissa"), loc2.get("abscissa")
349
+ )
350
+ ordinates_differ = self._numbers_are_wrong_or_differ(
351
+ loc1.get("ordinate"), loc2.get("ordinate")
352
+ )
353
+ srids_differ = loc1.get("srid") != loc2.get("srid")
354
+
355
+ if abscissas_differ or ordinates_differ or srids_differ:
356
+ raise ValueError(
357
+ "Incorrect or unspecified or inconsistent locations in the time series "
358
+ "files."
359
+ )
360
+
361
+ def _numbers_are_wrong_or_differ(self, num1, num2, tolerance=1e7):
362
+ if num1 is None or num2 is None:
363
+ return True
364
+ return abs(num1 - num2) > tolerance
365
+
366
+ def _compare_altitudes_of(self, hts1, hts2):
367
+ altitude1 = hts1.location.get("altitude")
368
+ altitude2 = hts2.location.get("altitude")
369
+ if self._numbers_are_wrong_or_differ(altitude1, altitude2, 1e-2):
370
+ raise ValueError(
371
+ "Incorrect or unspecified or inconsistent altitudes in the time series "
372
+ "files."
373
+ )
374
+
375
+ def _compare_timezones_of(self, hts1, hts2):
376
+ timezone1 = getattr(hts1, "timezone", "")
377
+ timezone2 = getattr(hts2, "timezone", "")
378
+ if timezone1 != timezone2:
379
+ raise ValueError(
380
+ "Incorrect or unspecified or inconsistent time zones in the time "
381
+ "series files."
382
+ )
383
+
384
+ def _get_location_in_wgs84(self):
385
+ source_projection = osr.SpatialReference()
386
+ source_projection.ImportFromEPSG(self.location["srid"])
387
+ wgs84 = osr.SpatialReference()
388
+ wgs84.ImportFromEPSG(4326)
389
+ transform = osr.CoordinateTransformation(source_projection, wgs84)
390
+ apoint = ogr.Geometry(ogr.wkbPoint)
391
+ apoint.AddPoint(self.location["abscissa"], self.location["ordinate"])
392
+ apoint.Transform(transform)
393
+ self.location["latitude"] = apoint.GetY()
394
+ self.location["longitude"] = apoint.GetX()
395
+
396
+ def _prepare_penman_monteith_parameters(self):
397
+ nsrr = self.config.nighttime_solar_radiation_ratio
398
+ self.penman_monteith = PenmanMonteith(
399
+ albedo=self.config.albedo,
400
+ nighttime_solar_radiation_ratio=nsrr,
401
+ elevation=self.location["altitude"],
402
+ latitude=self.location["latitude"],
403
+ longitude=self.location["longitude"],
404
+ time_step=self.config.time_step,
405
+ unit_converters=self.config.unit_converters,
406
+ )
407
+
408
+ def _prepare_resulting_htimeseries_object(self):
409
+ tzinfo = self.input_timeseries["wind_speed"].data.index.tz
410
+ self.pet = HTimeseries(default_tzinfo=tzinfo)
411
+ self.pet.time_step = self.config.time_step
412
+ self.pet.unit = "mm"
413
+ self.pet.timezone = self.timezone
414
+ self.pet.variable = "Potential Evapotranspiration"
415
+ self.pet.precision = 2 if self.config.time_step == "H" else 1
416
+ self.pet.location = self.location
417
+
418
+ def _determine_variables_to_use_in_calculation(self):
419
+ if self.config.time_step == "H":
420
+ vars = ["temperature", "humidity", "wind_speed", "solar_radiation"]
421
+ if "pressure" in self.input_timeseries:
422
+ vars.append("pressure")
423
+ elif self.config.time_step == "D":
424
+ vars = (
425
+ "temperature_max",
426
+ "temperature_min",
427
+ "humidity_max",
428
+ "humidity_min",
429
+ "wind_speed",
430
+ (
431
+ "solar_radiation"
432
+ if "solar_radiation" in self.input_timeseries
433
+ else "sunshine_duration"
434
+ ),
435
+ )
436
+ self.input_vars = vars
437
+
438
+ def _calculate_evaporation(self):
439
+ for adatetime in self.input_timeseries["wind_speed"].data.index:
440
+ self._calculate_evaporation_for(adatetime)
441
+
442
+ def _calculate_evaporation_for(self, adatetime):
443
+ try:
444
+ kwargs = {
445
+ v: self.input_timeseries[v].data.loc[adatetime, "value"]
446
+ for v in self.input_vars
447
+ }
448
+ except (IndexError, KeyError):
449
+ return
450
+ kwargs["adatetime"] = self._datetime64_to_aware_datetime(adatetime)
451
+ self.pet.data.loc[adatetime, "value"] = self.penman_monteith.calculate(**kwargs)
452
+
453
+ def _datetime64_to_aware_datetime(self, adatetime):
454
+ result = adatetime.to_pydatetime()
455
+ if self.timezone:
456
+ result = result.replace(tzinfo=TzinfoFromString(self.timezone))
457
+ return result
458
+
459
+ def _save_result(self):
460
+ outfilename = self.config.evaporation_prefix + ".hts"
461
+ outpathname = os.path.join(self.config.base_dir, outfilename)
462
+ with open(outpathname, "w") as f:
463
+ self.pet.write(f, format=HTimeseries.FILE)
464
+
465
+
466
+ class ProcessSpatial:
467
+ def __init__(self, config):
468
+ self.config = config
469
+
470
+ def execute(self):
471
+ self._get_geographical_reference_file()
472
+ self._get_timestamps()
473
+ self._get_geodata()
474
+ self._prepare_penman_monteith_parameters()
475
+ self._calculate_evaporation_for_all_timestamps()
476
+ self._remove_stale_evaporation_files()
477
+
478
+ def _get_geographical_reference_file(self):
479
+ # Arbitrarily use the first temperature file to extract location and
480
+ # other geographical stuff. Elsewhere consistency of such data from all
481
+ # other files with this file will be checked.
482
+ pattern = os.path.join(
483
+ self.config.base_dir, self.config.wind_speed_prefix + "-*.tif"
484
+ )
485
+ wind_speed_files = glob(pattern)
486
+ self.geographical_reference_file = wind_speed_files[0]
487
+
488
+ def _get_timestamps(self):
489
+ pattern = self.config.wind_speed_prefix + "-*.tif"
490
+ saved_cwd = os.getcwd()
491
+ try:
492
+ os.chdir(self.config.base_dir)
493
+ wind_speed_files = glob(pattern)
494
+ finally:
495
+ os.chdir(saved_cwd)
496
+
497
+ # Remove the prefix from the start and the .tif from the end, leaving
498
+ # only the date.
499
+ prefix_len = len(self.config.wind_speed_prefix)
500
+ start = prefix_len + 1
501
+ self.timestamps = [item[start:-4] for item in wind_speed_files]
502
+
503
+ def _get_geodata(self):
504
+ """
505
+ Retrieve geographical stuff into self.latitude, self.longitude,
506
+ self.width, self.height, self.geo_transform, self.projection. We do
507
+ this by retrieving the data from self.geographical_reference_file.
508
+ """
509
+ # Read data from GeoTIFF file
510
+ fp = gdal.Open(self.geographical_reference_file)
511
+ self.width, self.height = fp.RasterXSize, fp.RasterYSize
512
+ self.geo_transform = fp.GetGeoTransform()
513
+ self.projection = osr.SpatialReference()
514
+ self.projection.ImportFromWkt(fp.GetProjection())
515
+
516
+ # Find (x_left, y_top), (x_right, y_bottom)
517
+ x_left, x_step, d1, y_top, d2, y_step = self.geo_transform
518
+ x_right = x_left + self.width * x_step
519
+ y_bottom = y_top + self.height * y_step
520
+
521
+ # Transform into (long_left, lat_top), (long_right, lat_bottom)
522
+ wgs84 = osr.SpatialReference()
523
+ wgs84.ImportFromEPSG(4326)
524
+ transform = osr.CoordinateTransformation(self.projection, wgs84)
525
+ top_left = ogr.Geometry(ogr.wkbPoint)
526
+ top_left.AddPoint(x_left, y_top)
527
+ bottom_right = ogr.Geometry(ogr.wkbPoint)
528
+ bottom_right.AddPoint(x_right, y_bottom)
529
+ top_left.Transform(transform)
530
+ bottom_right.Transform(transform)
531
+ long_left, lat_top = top_left.GetX(), top_left.GetY()
532
+ long_right, lat_bottom = bottom_right.GetX(), bottom_right.GetY()
533
+
534
+ # Calculate self.latitude and self.longitude
535
+ long_step = (long_right - long_left) / self.width
536
+ longitudes = np.arange(
537
+ long_left + long_step / 2.0, long_left + long_step * self.width, long_step
538
+ )
539
+ lat_step = (lat_top - lat_bottom) / self.height
540
+ latitudes = np.arange(
541
+ lat_top + lat_step / 2.0, lat_top + lat_step * self.height, lat_step
542
+ )
543
+ self.longitude, self.latitude = np.meshgrid(longitudes, latitudes)
544
+
545
+ def _prepare_penman_monteith_parameters(self):
546
+ nsrr = self.config.nighttime_solar_radiation_ratio
547
+ self.penman_monteith = PenmanMonteith(
548
+ albedo=self.config.albedo,
549
+ nighttime_solar_radiation_ratio=nsrr,
550
+ elevation=self.config.elevation,
551
+ latitude=self.latitude,
552
+ longitude=self.longitude,
553
+ time_step=self.config.time_step,
554
+ unit_converters=self.config.unit_converters,
555
+ )
556
+
557
+ def _calculate_evaporation_for_all_timestamps(self):
558
+ for timestamp in self.timestamps:
559
+ self._calculate_evaporation_for(timestamp)
560
+
561
+ def _calculate_evaporation_for(self, timestamp):
562
+ self._read_input_geofiles(timestamp)
563
+ self._verify_that_solar_radiation_or_sunshine_duration_was_present(timestamp)
564
+ adatetime = self._get_adatetime_arg(timestamp)
565
+ result = self.penman_monteith.calculate(adatetime=adatetime, **self.input_data)
566
+ self._write_result_to_file(result, timestamp, adatetime)
567
+
568
+ def _read_input_geofiles(self, timestamp):
569
+ self.input_data = {v: None for v in self._get_variables()}
570
+ for var in self.input_data:
571
+ self._read_input_geofile(var, timestamp)
572
+
573
+ def _get_variables(self):
574
+ if self.config.time_step == "H":
575
+ return {
576
+ "temperature",
577
+ "humidity",
578
+ "wind_speed",
579
+ "pressure",
580
+ "solar_radiation",
581
+ }
582
+ else:
583
+ return {
584
+ "temperature_max",
585
+ "temperature_min",
586
+ "humidity_max",
587
+ "humidity_min",
588
+ "wind_speed",
589
+ "solar_radiation",
590
+ "sunshine_duration",
591
+ }
592
+
593
+ def _read_input_geofile(self, variable, timestamp):
594
+ fp = self._open_geofile(variable, timestamp)
595
+ if fp is None:
596
+ return
597
+ self._verify_consistency_of_geodata(fp)
598
+ self.input_data[variable] = self._read_array_from_geofile(fp)
599
+ fp = None
600
+
601
+ def _open_geofile(self, variable, timestamp):
602
+ filename_prefix = getattr(self.config, variable + "_prefix")
603
+ filename = filename_prefix + "-" + timestamp + ".tif"
604
+ self._filename = os.path.join(self.config.base_dir, filename)
605
+ if variable in ("solar_radiation", "sunshine_duration"):
606
+ if not os.path.exists(self._filename):
607
+ # Either solar_radiation or sunshine_duration may be absent; here we
608
+ # allow both to be absent and elsewhere we will check that one was
609
+ # present
610
+ return
611
+ try:
612
+ return gdal.Open(self._filename)
613
+ except RuntimeError:
614
+ if variable == "pressure":
615
+ return
616
+ raise
617
+
618
+ def _verify_consistency_of_geodata(self, fp):
619
+ consistent = all(
620
+ (
621
+ self.width == fp.RasterXSize,
622
+ self.height == fp.RasterYSize,
623
+ self.geo_transform == fp.GetGeoTransform(),
624
+ self.projection.ExportToWkt() == fp.GetProjection(),
625
+ )
626
+ )
627
+ if not consistent:
628
+ raise Exception(
629
+ "Not all input files have the same "
630
+ "width, height, geo_transform and projection "
631
+ "(offending items: {} and {})".format(
632
+ self.geographical_reference_file, self._filename
633
+ )
634
+ )
635
+
636
+ def _read_array_from_geofile(self, fp):
637
+ array = fp.GetRasterBand(1).ReadAsArray()
638
+ nodata = fp.GetRasterBand(1).GetNoDataValue()
639
+ if nodata is not None:
640
+ array = np.ma.masked_values(array, nodata, copy=False)
641
+ return array
642
+
643
+ def _verify_that_solar_radiation_or_sunshine_duration_was_present(self, timestamp):
644
+ if self.input_data["solar_radiation"] is not None:
645
+ self.input_data.pop("sunshine_duration", None)
646
+ elif self.input_data.get("sunshine_duration", None) is not None:
647
+ self.input_data.pop("solar_radiation", None)
648
+ else:
649
+ raise RuntimeError(
650
+ "Neither sunshine_duration nor solar_radiation are available for "
651
+ + timestamp
652
+ )
653
+
654
+ def _get_adatetime_arg(self, timestamp):
655
+ adatetime = iso8601.parse_date(
656
+ self._timestamp_from_filename(timestamp), default_timezone=None
657
+ )
658
+ if self.config.time_step == "D":
659
+ adatetime = adatetime.date()
660
+ elif adatetime.tzinfo is None:
661
+ raise Exception(
662
+ "The time stamp in the input files does not have a time zone specified."
663
+ )
664
+ return adatetime
665
+
666
+ def _timestamp_from_filename(self, s):
667
+ """Convert a timestamp from its filename format to its iso format
668
+
669
+ E.g. from 2014-10-01-15-00-0100 to 2014-10-01 15:00-0100).
670
+ """
671
+ first_hyphen = s.find("-")
672
+ if first_hyphen < 0:
673
+ return s
674
+ second_hyphen = s.find("-", first_hyphen + 1)
675
+ if second_hyphen < 0:
676
+ return s
677
+ third_hyphen = s.find("-", second_hyphen + 1)
678
+ if third_hyphen < 0:
679
+ return s
680
+ fourth_hyphen = s.find("-", third_hyphen + 1)
681
+ chars = list(s)
682
+ chars[third_hyphen] = " "
683
+ if fourth_hyphen > 0:
684
+ chars[fourth_hyphen] = ":"
685
+ return "".join(chars)
686
+
687
+ def _write_result_to_file(self, result, timestamp, adatetime):
688
+ output_filename = self.config.evaporation_prefix + "-" + timestamp + ".tif"
689
+ output_pathname = os.path.join(self.config.base_dir, output_filename)
690
+ output = gdal.GetDriverByName("GTiff").Create(
691
+ output_pathname, self.width, self.height, 1, gdal.GDT_Float32
692
+ )
693
+ try:
694
+ output.SetMetadataItem("TIMESTAMP", adatetime.isoformat())
695
+ output.SetGeoTransform(self.geo_transform)
696
+ output.SetProjection(self.projection.ExportToWkt())
697
+ result[result.mask] = NODATAVALUE
698
+ output.GetRasterBand(1).SetNoDataValue(NODATAVALUE)
699
+ output.GetRasterBand(1).WriteArray(result)
700
+ finally:
701
+ # Close the dataset
702
+ output = None
703
+
704
+ def _remove_stale_evaporation_files(self):
705
+ """Remove evaporation files for which no input files exist."""
706
+ pattern = self.config.evaporation_prefix + "-*.tif"
707
+ saved_cwd = os.getcwd()
708
+ try:
709
+ os.chdir(self.config.base_dir)
710
+ evaporation_files = glob(pattern)
711
+ prefix_len = len(self.config.evaporation_prefix)
712
+ for filename in evaporation_files:
713
+ start = prefix_len + 1
714
+ if filename[start:-4] not in self.timestamps:
715
+ os.unlink(filename)
716
+ finally:
717
+ os.chdir(saved_cwd)
718
+
719
+
720
+ @click.command()
721
+ @click.argument("configfile")
722
+ @click.version_option(
723
+ version=__version__, message="%(prog)s from pthelma v.%(version)s"
724
+ )
725
+ def main(configfile):
726
+ """Calculation of evaporation and transpiration"""
727
+
728
+ app = App(configfile)
729
+ app.run()