weatherdb 1.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. docker/Dockerfile +30 -0
  2. docker/docker-compose.yaml +58 -0
  3. docker/docker-compose_test.yaml +24 -0
  4. docker/start-docker-test.sh +6 -0
  5. docs/requirements.txt +10 -0
  6. docs/source/Changelog.md +2 -0
  7. docs/source/License.rst +7 -0
  8. docs/source/Methode.md +161 -0
  9. docs/source/_static/custom.css +8 -0
  10. docs/source/_static/favicon.ico +0 -0
  11. docs/source/_static/logo.png +0 -0
  12. docs/source/api/api.rst +15 -0
  13. docs/source/api/cli.rst +8 -0
  14. docs/source/api/weatherDB.broker.rst +10 -0
  15. docs/source/api/weatherDB.config.rst +7 -0
  16. docs/source/api/weatherDB.db.rst +23 -0
  17. docs/source/api/weatherDB.rst +22 -0
  18. docs/source/api/weatherDB.station.rst +56 -0
  19. docs/source/api/weatherDB.stations.rst +46 -0
  20. docs/source/api/weatherDB.utils.rst +22 -0
  21. docs/source/conf.py +137 -0
  22. docs/source/index.rst +33 -0
  23. docs/source/setup/Configuration.md +127 -0
  24. docs/source/setup/Hosting.md +9 -0
  25. docs/source/setup/Install.md +49 -0
  26. docs/source/setup/Quickstart.md +183 -0
  27. docs/source/setup/setup.rst +12 -0
  28. weatherdb/__init__.py +24 -0
  29. weatherdb/_version.py +1 -0
  30. weatherdb/alembic/README.md +8 -0
  31. weatherdb/alembic/alembic.ini +80 -0
  32. weatherdb/alembic/config.py +9 -0
  33. weatherdb/alembic/env.py +100 -0
  34. weatherdb/alembic/script.py.mako +26 -0
  35. weatherdb/alembic/versions/V1.0.0_initial_database_creation.py +898 -0
  36. weatherdb/alembic/versions/V1.0.2_more_charachters_for_settings+term_station_ma_raster.py +88 -0
  37. weatherdb/alembic/versions/V1.0.5_fix-ma-raster-values.py +152 -0
  38. weatherdb/alembic/versions/V1.0.6_update-views.py +22 -0
  39. weatherdb/broker.py +667 -0
  40. weatherdb/cli.py +214 -0
  41. weatherdb/config/ConfigParser.py +663 -0
  42. weatherdb/config/__init__.py +5 -0
  43. weatherdb/config/config_default.ini +162 -0
  44. weatherdb/db/__init__.py +3 -0
  45. weatherdb/db/connections.py +374 -0
  46. weatherdb/db/fixtures/RichterParameters.json +34 -0
  47. weatherdb/db/models.py +402 -0
  48. weatherdb/db/queries/get_quotient.py +155 -0
  49. weatherdb/db/views.py +165 -0
  50. weatherdb/station/GroupStation.py +710 -0
  51. weatherdb/station/StationBases.py +3108 -0
  52. weatherdb/station/StationET.py +111 -0
  53. weatherdb/station/StationP.py +807 -0
  54. weatherdb/station/StationPD.py +98 -0
  55. weatherdb/station/StationT.py +164 -0
  56. weatherdb/station/__init__.py +13 -0
  57. weatherdb/station/constants.py +21 -0
  58. weatherdb/stations/GroupStations.py +519 -0
  59. weatherdb/stations/StationsBase.py +1021 -0
  60. weatherdb/stations/StationsBaseTET.py +30 -0
  61. weatherdb/stations/StationsET.py +17 -0
  62. weatherdb/stations/StationsP.py +128 -0
  63. weatherdb/stations/StationsPD.py +24 -0
  64. weatherdb/stations/StationsT.py +21 -0
  65. weatherdb/stations/__init__.py +11 -0
  66. weatherdb/utils/TimestampPeriod.py +369 -0
  67. weatherdb/utils/__init__.py +3 -0
  68. weatherdb/utils/dwd.py +350 -0
  69. weatherdb/utils/geometry.py +69 -0
  70. weatherdb/utils/get_data.py +285 -0
  71. weatherdb/utils/logging.py +126 -0
  72. weatherdb-1.1.0.dist-info/LICENSE +674 -0
  73. weatherdb-1.1.0.dist-info/METADATA +765 -0
  74. weatherdb-1.1.0.dist-info/RECORD +77 -0
  75. weatherdb-1.1.0.dist-info/WHEEL +5 -0
  76. weatherdb-1.1.0.dist-info/entry_points.txt +2 -0
  77. weatherdb-1.1.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,807 @@
1
+ # libraries
2
+ import logging
3
+ from datetime import timedelta
4
+ from pathlib import Path
5
+ import warnings
6
+ import numpy as np
7
+ import pandas as pd
8
+ import sqlalchemy as sa
9
+ from sqlalchemy import text as sqltxt
10
+ from functools import cached_property
11
+ import rasterio as rio
12
+ import rasterio.mask
13
+ import pyproj
14
+ from shapely.ops import transform as shp_transform
15
+ from shapely import distance, Point
16
+
17
+ from ..db.connections import db_engine
18
+ from ..utils.dwd import dwd_id_to_str
19
+ from ..utils.TimestampPeriod import TimestampPeriod
20
+ from ..utils.geometry import polar_line, raster2points
21
+ from ..config import config
22
+ from ..db.models import MetaP
23
+ from .StationBases import StationPBase
24
+ from .StationPD import StationPD
25
+ from .StationT import StationT
26
+
27
+ # set settings
28
+ # ############
29
+ __all__ = ["StationP"]
30
+ log = logging.getLogger(__name__)
31
+
32
+ # variables
33
+ RICHTER_CLASSES = {
34
+ "no-protection": {
35
+ "min_horizon": 0,
36
+ "max_horizon": 3
37
+ },
38
+ "little-protection": {
39
+ "min_horizon": 3,
40
+ "max_horizon": 7
41
+ },
42
+ "protected": {
43
+ "min_horizon": 7,
44
+ "max_horizon": 12
45
+ },
46
+ "heavy-protection": {
47
+ "min_horizon": 12,
48
+ "max_horizon": np.inf
49
+ },
50
+ }
51
+
52
+ # class definition
53
+ ##################
54
+ class StationP(StationPBase):
55
+ """A class to work with and download 10 minutes precipitation data for one station."""
56
+ # common settings
57
+ _MetaModel = MetaP
58
+ _para = "p"
59
+ _para_base = _para
60
+ _para_long = "Precipitation"
61
+ _unit = "mm/10min"
62
+ _valid_kinds = {"raw", "qn", "qc", "corr", "filled", "filled_by"}
63
+ _best_kind = "corr"
64
+
65
+ # cdc dwd parameters
66
+ _ftp_folder_base = [
67
+ "climate_environment/CDC/observations_germany/climate/10_minutes/precipitation/"]
68
+ _cdc_col_names_imp = ["RWS_10", "QN"]
69
+ _db_col_names_imp = ["raw", "qn"]
70
+
71
+ # timestamp configurations
72
+ _tstp_format_db = "%Y%m%d %H:%M"
73
+ _tstp_dtype = "timestamp"
74
+ _interval = "10 min"
75
+
76
+ # aggregation
77
+ _min_agg_to = "10 min"
78
+
79
+ def __init__(self, id, **kwargs):
80
+ super().__init__(id, **kwargs)
81
+ self.id_str = dwd_id_to_str(id)
82
+
83
+ def _get_sql_new_qc(self, period):
84
+ # create sql_format_dict
85
+ sql_format_dict = dict(
86
+ para=self._para, stid=self.id, para_long=self._para_long,
87
+ decim=self._decimals,
88
+ **period.get_sql_format_dict(
89
+ format="'{}'".format(self._tstp_format_db)),
90
+ limit=0.1*self._decimals) # don't delete values below 0.1mm/10min if they are consecutive
91
+
92
+ # check if daily station is available
93
+ sql_check_d = """
94
+ SELECT EXISTS(
95
+ SELECT *
96
+ FROM information_schema.tables
97
+ WHERE table_schema = 'timeseries'
98
+ AND table_name = '{stid}_{para}_d'
99
+ );""".format(**sql_format_dict)
100
+ with db_engine.connect() as con:
101
+ daily_exists = con.execute(sqltxt(sql_check_d)).first()[0]
102
+
103
+ # create sql for dates where the aggregated 10 minutes measurements are 0
104
+ # althought the daily measurements are not 0
105
+ # or where the aggregated daily sum is more than the double of the daily measurement, when the daily measurement is more than 10 mm
106
+ if daily_exists:
107
+ sql_dates_failed = """
108
+ WITH ts_10min_d AS (
109
+ SELECT (ts.timestamp - INTERVAL '6h')::date as date, sum("raw") as raw
110
+ FROM timeseries."{stid}_{para}" ts
111
+ WHERE ts.timestamp BETWEEN {min_tstp} AND {max_tstp}
112
+ GROUP BY (ts.timestamp - INTERVAL '6h')::date)
113
+ SELECT date
114
+ FROM timeseries."{stid}_{para}_d" ts_d
115
+ LEFT JOIN ts_10min_d ON ts_d.timestamp::date=ts_10min_d.date
116
+ WHERE ts_d.timestamp BETWEEN {min_tstp}::date AND {max_tstp}::date
117
+ AND ((ts_10min_d.raw = 0 AND ts_d.raw <> 0) OR
118
+ (ts_10min_d.raw >= 10*{decim} AND ts_10min_d.raw >= (ts_d.raw*2)))
119
+ """.format(**sql_format_dict)
120
+ else:
121
+ log.error((
122
+ "For the {para_long} station with ID {stid} there is no timeserie with daily values. " +
123
+ "Therefor the quality check for daily values equal to 0 is not done.\n" +
124
+ "Please consider updating the daily stations with:\n" +
125
+ "stats = stations.StationsPD()\n" +
126
+ "stats.update_meta()\nstats.update_raw()"
127
+ ).format(**sql_format_dict))
128
+ sql_dates_failed = """
129
+ SELECT NULL::date as date
130
+ """
131
+
132
+ # remove single peaks above 5mm/10min
133
+ sql_single_peaks = """
134
+ SELECT ts.timestamp
135
+ FROM timeseries."{stid}_{para}" ts
136
+ INNER JOIN timeseries."{stid}_{para}" tsb
137
+ ON ts.timestamp = tsb.timestamp - INTERVAL '10 min'
138
+ INNER JOIN timeseries."{stid}_{para}" tsa
139
+ ON ts.timestamp = tsa.timestamp + INTERVAL '10 min'
140
+ WHERE ts.raw > (5*{decim}) AND tsb.raw = 0 AND tsa.raw = 0
141
+ AND ts.timestamp BETWEEN {min_tstp} AND {max_tstp}
142
+ """.format(**sql_format_dict)
143
+
144
+ # make sql for timestamps where 3 times same value in a row
145
+ sql_tstps_failed = """
146
+ WITH tstps_df as (
147
+ SELECT ts.timestamp as tstp_1,
148
+ ts2.timestamp as tstp_2,
149
+ ts3.timestamp as tstp_3
150
+ from timeseries."{stid}_{para}" ts
151
+ INNER JOIN timeseries."{stid}_{para}" ts2
152
+ on ts.timestamp = ts2.timestamp - INTERVAL '10 min'
153
+ INNER JOIN timeseries."{stid}_{para}" ts3
154
+ on ts.timestamp = ts3.timestamp - INTERVAL '20 min'
155
+ WHERE ts.qn != 3
156
+ AND ts.raw = ts2.raw AND ts2.raw = ts3.raw
157
+ AND ts.raw > {limit:n}
158
+ AND ts.raw is not NULL
159
+ AND ts.timestamp BETWEEN {min_tstp} AND {max_tstp}
160
+ )
161
+ SELECT tstp_1 AS timestamp FROM tstps_df
162
+ UNION SELECT tstp_2 FROM tstps_df
163
+ UNION SELECT tstp_3 FROM tstps_df
164
+ UNION ({sql_single_peaks})
165
+ """.format(sql_single_peaks=sql_single_peaks,
166
+ **sql_format_dict)
167
+
168
+ # create sql for new qc values
169
+ sql_new_qc = """
170
+ WITH tstps_failed as ({sql_tstps_failed}),
171
+ dates_failed AS ({sql_dates_failed})
172
+ SELECT ts.timestamp,
173
+ (CASE WHEN ((ts.timestamp IN (SELECT timestamp FROM tstps_failed))
174
+ OR ((ts.timestamp - INTERVAL '6h')::date IN (
175
+ SELECT date FROM dates_failed))
176
+ OR ts."raw" < 0
177
+ OR ts."raw" >= 50*{decim})
178
+ THEN NULL
179
+ ELSE ts."raw" END) as qc
180
+ FROM timeseries."{stid}_{para}" ts
181
+ WHERE ts.timestamp BETWEEN {min_tstp} AND {max_tstp}
182
+ """.format(
183
+ sql_tstps_failed=sql_tstps_failed,
184
+ sql_dates_failed=sql_dates_failed,
185
+ **sql_format_dict)
186
+
187
+ return sql_new_qc
188
+
189
+ def _check_df_raw(self, df):
190
+ """This function is used in the Base class on the single dataframe that is downloaded from the CDC Server before loading it in the database.
191
+
192
+ Here the function adapts the timezone relative to the date.
193
+ As the data on the CDC server is in MEZ before 200 and in UTC after 2000
194
+
195
+ Some precipitation stations on the DWD CDC server have also rows outside of the normal 10 Minute frequency, e.g. 2008-09-16 01:47 for Station 662.
196
+ Because those rows only have NAs for the measurement they are deleted."""
197
+ # correct Timezone before 2000 -> CEWT after 2000 -> UTC
198
+ if df.index.tzinfo is not None:
199
+ df.index = df.index.tz_localize(None) # some are already falsy localized
200
+ if df.index.min()>= pd.Timestamp(1999,12,31,23,0):
201
+ df.index = df.index.tz_localize("UTC")
202
+ elif df.index.max() < pd.Timestamp(2000,1,1,1,0):
203
+ df.index = df.index.tz_localize("Etc/GMT-1").tz_convert("UTC")
204
+ else:
205
+ raise ValueError("The timezone could not get defined for the given import." + str(df.head()))
206
+
207
+ # delete measurements outside of the 10 minutes frequency
208
+ df = df[df.index.minute%10==0].copy()
209
+
210
+ # check if duplicates and try to remove
211
+ if df.index.has_duplicates:
212
+ df = df.reset_index()\
213
+ .groupby(self._cdc_col_names_imp + [self._cdc_date_col])\
214
+ .first()\
215
+ .reset_index().set_index(self._cdc_date_col)
216
+ if df.index.has_duplicates:
217
+ raise ValueError("There are duplicates in the DWD data that couldn't get removed.")
218
+
219
+ # set frequency to 10 minutes
220
+ df = df.asfreq("10min")
221
+
222
+ # delete measurements below 0
223
+ n_col = self._cdc_col_names_imp[self._db_col_names_imp.index("raw")]
224
+ df.loc[df[n_col]<0, n_col] = np.nan
225
+
226
+ return df
227
+
228
+ @cached_property
229
+ def _table(self):
230
+ return sa.table(
231
+ f"{self.id}_{self._para}",
232
+ sa.column("timestamp", sa.DateTime),
233
+ sa.column("raw", sa.Integer),
234
+ sa.column("qc", sa.Integer),
235
+ sa.column("filled", sa.Integer),
236
+ sa.column("filled_by", sa.SmallInteger),
237
+ sa.column("corr", sa.Integer),
238
+ schema="timeseries")
239
+
240
+ @db_engine.deco_create_privilege
241
+ def _create_timeseries_table(self):
242
+ """Create the timeseries table in the DB if it is not yet existing."""
243
+ sql_add_table = '''
244
+ CREATE TABLE IF NOT EXISTS timeseries."{stid}_{para}" (
245
+ timestamp timestamp PRIMARY KEY,
246
+ raw int4,
247
+ qn smallint,
248
+ qc int4,
249
+ filled int4,
250
+ filled_by int2,
251
+ corr int4
252
+ );
253
+ '''.format(stid=self.id, para=self._para)
254
+ with db_engine.connect() as con:
255
+ con.execute(sqltxt(sql_add_table))
256
+ con.commit()
257
+
258
+ @staticmethod
259
+ def _check_period_extra(period):
260
+ """Additional checks on period used in StationBase class _check_period method."""
261
+ # add time to period if given as date
262
+ return period.expand_to_timestamp()
263
+
264
+ @staticmethod
265
+ def _richter_class_from_horizon(horizon):
266
+ richter_class = None
267
+ for key in RICHTER_CLASSES:
268
+ if (horizon >= RICHTER_CLASSES[key]["min_horizon"]) and \
269
+ (horizon < RICHTER_CLASSES[key]["max_horizon"]):
270
+ richter_class = key
271
+ return richter_class
272
+
273
+ @db_engine.deco_update_privilege
274
+ def update_horizon(self, skip_if_exist=True, **kwargs):
275
+ """Update the horizon angle (Horizontabschirmung) in the meta table.
276
+
277
+ Get new values from the raster and put in the table.
278
+
279
+ Parameters
280
+ ----------
281
+ skip_if_exist : bool, optional
282
+ Skip updating the value if there is already a value in the meta table.
283
+ The default is True.
284
+ **kwargs : dict, optional
285
+ Additional keyword arguments catch all, but unused here.
286
+
287
+ Returns
288
+ -------
289
+ float
290
+ The horizon angle in degrees (Horizontabschirmung).
291
+ """
292
+ if skip_if_exist:
293
+ horizon = self.get_horizon()
294
+ if horizon is not None:
295
+ return horizon
296
+
297
+ # check if files are available
298
+ dem_files = [Path(file) for file in config.get_list("data:rasters", "dems")]
299
+ for dem_file in dem_files:
300
+ if not dem_file.is_file():
301
+ raise ValueError(
302
+ f"The DEM file was not found in the data directory under: \n{dem_file}")
303
+
304
+ # get the horizon configurations
305
+ radius = int(config.get("weatherdb", "horizon_radius"))
306
+ proj_crs = pyproj.CRS.from_epsg(config.get("weatherdb", "horizon_crs"))
307
+ if not proj_crs.is_projected:
308
+ raise ValueError(
309
+ "The CRS for the horizon calculation is not projected. " +
310
+ "Please define a projected CRS in the config file under weatherdb.horizon_crs.")
311
+
312
+ # get raster basic information
313
+ dem_infos = {}
314
+ for dem_file in dem_files:
315
+ with rio.open(dem_file) as dem:
316
+ # get station position
317
+ stat_h, stat_tr = rasterio.mask.mask(
318
+ dem, [self.get_geom(crs=dem.crs)],
319
+ crop=True, all_touched=False,
320
+ nodata=np.nan)
321
+ stat_geom = raster2points(stat_h, stat_tr, crs=dem.crs)\
322
+ .to_crs(proj_crs)\
323
+ .iloc[0].geometry
324
+ xy = [stat_geom.x, stat_geom.y]
325
+ stat_h = stat_h.flatten()[0]
326
+
327
+ # get CRS transformer
328
+ tr_to_dem = pyproj.Transformer.from_crs(proj_crs, dem.crs, always_xy=True)
329
+ tr_from_dem = pyproj.Transformer.from_crs(dem.crs, proj_crs, always_xy=True)
330
+
331
+ # get typical distance of raster cell points
332
+ dem_tr = dem.transform
333
+ p_top_left = (dem_tr.c, dem_tr.f)
334
+ p_top_right = (dem_tr.c+dem_tr.a*dem.profile["width"], dem_tr.f)
335
+ p_bottom_left= (dem_tr.c, dem_tr.f+dem_tr.e*dem.profile["height"])
336
+ p_bottom_right = (p_top_right[0], p_bottom_left[1])
337
+ dists = []
338
+ for p in [p_top_left, p_top_right, p_bottom_left, p_bottom_right]:
339
+ p1 = Point(tr_from_dem.transform(*p))
340
+ dists.append(distance(
341
+ p1, Point(tr_from_dem.transform(p[0] + dem_tr.a, p[1] + dem_tr.e))
342
+ ))
343
+
344
+ dem_infos[dem_file] = dict(
345
+ stat_geom=stat_geom,
346
+ xy=xy,
347
+ stat_h=stat_h,
348
+ tr_to_dem=tr_to_dem,
349
+ tr_from_dem=tr_from_dem,
350
+ max_dist = max(dists)*1.2
351
+ )
352
+
353
+ # check if first dem has hight information
354
+ if np.isnan(dem_infos[dem_files[0]]["stat_h"]):
355
+ raise ValueError(
356
+ f"update_horizon(): No height was found in the first dem for {self._para_long} Station {self.id}. " +
357
+ "Therefor the horizon angle could not get updated.")
358
+
359
+ # get station center point
360
+ stat_geom = self.get_geom(crs=proj_crs)
361
+ xy = [stat_geom.x, stat_geom.y]
362
+
363
+ # compute the horizon angle for each western angle
364
+ hab = pd.Series(
365
+ index=pd.Index([], name="angle", dtype=int),
366
+ name="horizon",
367
+ dtype=float)
368
+ raise_hole_error = False
369
+ for angle in range(90, 271, 3):
370
+ missing_lines = [polar_line(xy, radius, angle)]
371
+ dem_lines = None
372
+ for dem_file, dem_info in dem_infos.items():
373
+ with rio.open(dem_file) as dem:
374
+ missing_lines_next = []
375
+ for line in missing_lines:
376
+ # get raster points
377
+ line_dem = shp_transform(
378
+ dem_info["tr_to_dem"].transform,
379
+ line)
380
+ dem_np, dem_tr = rasterio.mask.mask(
381
+ dem, [line_dem],
382
+ crop=True, all_touched=True,
383
+ nodata=np.nan)
384
+ dem_line = raster2points(dem_np, dem_tr, crs=dem.crs).to_crs(proj_crs)
385
+
386
+ # calculate the distance to the stations raster cell
387
+ dem_line["dist"] = dem_line.distance(dem_info["stat_geom"])
388
+ dem_line.drop(
389
+ dem_line.loc[dem_line["dist"]==0].index,
390
+ inplace=True)
391
+ dem_line = dem_line.sort_values("dist").reset_index(drop=True)
392
+
393
+
394
+ # calculate the horizon angle
395
+ dem_line["horizon"] = np.degrees(np.arctan(
396
+ (dem_line["data"]-dem_info["stat_h"]) / dem_line["dist"]))
397
+
398
+ # check if parts are missing and fill
399
+ #####################################
400
+
401
+ # look for holes inside the line
402
+ for i in dem_line[dem_line["dist"].diff() > dem_info["max_dist"]].index:
403
+ missing_lines_next.append(
404
+ polar_line(dem_line.loc[i-1, "geometry"].coords[0],
405
+ dem_line.loc[i, "dist"] - dem_line.loc[i-1, "dist"],
406
+ angle))
407
+
408
+ # merge the parts
409
+ dem_lines = pd.concat(
410
+ [dem_lines, dem_line], ignore_index=True)
411
+
412
+ # look for missing values at the end
413
+ dem_max_dist = dem_lines.iloc[-1]["dist"]
414
+ if dem_max_dist < (radius - dem_info["max_dist"]):
415
+ missing_lines_next.append(
416
+ polar_line(dem_lines.iloc[-1]["geometry"].coords[0],
417
+ radius - dem_max_dist,
418
+ angle))
419
+
420
+ # check if no parts are missing
421
+ if (len(missing_lines_next) == 0):
422
+ break
423
+ elif dem_file == dem_files[-1]:
424
+ raise_hole_error = True
425
+ else:
426
+ # create lines for next iteration
427
+ missing_lines = missing_lines_next
428
+
429
+ hab[angle] = dem_lines["horizon"].max()
430
+
431
+ if raise_hole_error:
432
+ log.warning(
433
+ f"Station{self._para}({self.id}).update_horizon(): There were holes in the DEM rasters providen when calculating the horizon angle. Therefor the calculated horizon angle could be faulty, but doesn't have to be, if the station is close to the sea for example.")
434
+
435
+ # calculate the mean "horizontabschimung"
436
+ # Richter: H’=0,15H(S-SW) +0,35H(SW-W) +0,35H(W-NW) +0, 15H(NW-N)
437
+ horizon = max(0,
438
+ 0.15*hab[(hab.index>225) & (hab.index<=270)].mean()
439
+ + 0.35*hab[(hab.index>=180) & (hab.index<=225)].mean()
440
+ + 0.35*hab[(hab.index>=135) & (hab.index<180)].mean()
441
+ + 0.15*hab[(hab.index>=90) & (hab.index<135)].mean())
442
+
443
+ # insert to meta table in db
444
+ self._update_meta(
445
+ cols=["horizon"],
446
+ values=[horizon])
447
+
448
+ return horizon
449
+
450
+ @db_engine.deco_update_privilege
451
+ def update_richter_class(self, skip_if_exist=True, **kwargs):
452
+ """Update the richter class in the meta table.
453
+
454
+ Get new values from the raster and put in the table.
455
+
456
+ Parameters
457
+ ----------
458
+ skip_if_exist : bool, optional
459
+ Skip updating the value if there is already a value in the meta table.
460
+ The default is True
461
+ **kwargs : dict, optional
462
+ Additional keyword arguments catch all, but unused here.
463
+
464
+ Returns
465
+ -------
466
+ str
467
+ The richter class name.
468
+ """
469
+ # check if already value in table
470
+ if skip_if_exist:
471
+ richter_class = self.get_richter_class()
472
+ if self.get_richter_class() is not None:
473
+ return richter_class
474
+
475
+ # get the richter class
476
+ richter_class = self._richter_class_from_horizon(
477
+ horizon=self.update_horizon(skip_if_exist=skip_if_exist))
478
+
479
+ # save to db
480
+ self._update_meta(
481
+ cols=["richter_class"],
482
+ values=[richter_class])
483
+
484
+ return richter_class
485
+
486
+ @db_engine.deco_update_privilege
487
+ def richter_correct(self, period=(None, None), **kwargs):
488
+ """Do the richter correction on the filled data for the given period.
489
+
490
+ Parameters
491
+ ----------
492
+ period : TimestampPeriod or (tuple or list of datetime.datetime or None), optional
493
+ The minimum and maximum Timestamp for which to get the timeseries.
494
+ If None is given, the maximum or minimal possible Timestamp is taken.
495
+ The default is (None, None).
496
+ **kwargs : dict, optional
497
+ Additional keyword arguments catch all, but unused here.
498
+
499
+ Raises
500
+ ------
501
+ Exception
502
+ If no richter class was found for this station.
503
+ """
504
+ # check if period is given
505
+ if not isinstance(period, TimestampPeriod):
506
+ period = TimestampPeriod(*period)
507
+ period_in = period.copy()
508
+ period = self._check_period(
509
+ period=period, kinds=["filled"])
510
+ if not period_in.is_empty():
511
+ sql_period_clause = \
512
+ "WHERE timestamp BETWEEN {min_tstp} AND {max_tstp}".format(
513
+ **period.get_sql_format_dict(f"'{self._tstp_format_db}'"))
514
+ else:
515
+ sql_period_clause = ""
516
+
517
+ # check if temperature station is filled
518
+ stat_t = StationT(self.id)
519
+ stat_t_period = stat_t.get_filled_period(kind="filled")
520
+ delta = timedelta(hours=5, minutes=50)
521
+ min_date = config.get_date("weatherdb", "min_date")
522
+ stat_t_min = stat_t_period[0].date()
523
+ stat_t_max = stat_t_period[1].date()
524
+ stat_p_min = (period[0] - delta).date()
525
+ stat_p_max = (period[1] - delta).date()
526
+ if stat_t_period.is_empty()\
527
+ or (stat_t_min > stat_p_min
528
+ and not (stat_p_min < min_date)
529
+ and (stat_t_min == min_date)) \
530
+ or (stat_t_max < stat_p_max)\
531
+ and not stat_t.is_last_imp_done(kind="filled"):
532
+ stat_t.fillup(period=period)
533
+
534
+ # get the richter exposition class
535
+ richter_class = self.update_richter_class(skip_if_exist=True)
536
+ if richter_class is None:
537
+ raise Exception("No richter class was found for the precipitation station {stid} and therefor no richter correction was possible."\
538
+ .format(stid=self.id))
539
+
540
+ # create the sql queries
541
+ sql_format_dict = dict(
542
+ stid=self.id,
543
+ para=self._para,
544
+ richter_class=richter_class,
545
+ period_clause=sql_period_clause,
546
+ n_decim=self._decimals,
547
+ t_decim=stat_t._decimals
548
+ )
549
+ # daily precipitation
550
+ sql_p_daily = """
551
+ SELECT timestamp::date AS date,
552
+ sum("filled") AS "filled",
553
+ count(*) FILTER (WHERE "filled" > 0) AS "count_n"
554
+ FROM timeseries."{stid}_{para}"
555
+ {period_clause}
556
+ GROUP BY timestamp::date
557
+ """.format(**sql_format_dict)
558
+
559
+ # add is_winter
560
+ sql_p_daily_winter = """
561
+ SELECT date,
562
+ CASE WHEN EXTRACT(MONTH FROM date) IN (1, 2, 3, 10, 11, 12)
563
+ THEN true::bool
564
+ ELSE false::bool
565
+ END AS is_winter,
566
+ "filled" AS "p_d",
567
+ "count_n"
568
+ FROM ({sql_p_daily}) tsp_d
569
+ """.format(sql_p_daily=sql_p_daily)
570
+
571
+ # add precipitation class
572
+ sql_p_daily_precip_class = """
573
+ SELECT
574
+ date, "count_n", "p_d",
575
+ CASE WHEN (tst."filled" >= (3 * {t_decim}) AND "is_winter") THEN 'precip_winter'
576
+ WHEN (tst."filled" >= (3 * {t_decim}) AND NOT "is_winter") THEN 'precip_summer'
577
+ WHEN (tst."filled" <= (-0.7 * {t_decim})::int) THEN 'snow'
578
+ WHEN (tst."filled" IS NULL) THEN NULL
579
+ ELSE 'mix'
580
+ END AS precipitation_typ
581
+ FROM ({sql_p_daily_winter}) tsp_d_wi
582
+ LEFT JOIN timeseries."{stid}_t" tst
583
+ ON tst.timestamp=tsp_d_wi.date
584
+ """.format(
585
+ sql_p_daily_winter=sql_p_daily_winter,
586
+ **sql_format_dict
587
+ )
588
+
589
+ # calculate the delta n
590
+ sql_delta_n = """
591
+ SELECT date,
592
+ CASE WHEN "count_n"> 0 THEN
593
+ round(("b_{richter_class}" * ("p_d"::float/{n_decim})^"e" * {n_decim})/"count_n")::int
594
+ ELSE 0
595
+ END AS "delta_10min"
596
+ FROM ({sql_p_daily_precip_class}) tsp_d2
597
+ LEFT JOIN richter_parameters r
598
+ ON r."precipitation_typ"=tsp_d2."precipitation_typ"
599
+ """.format(
600
+ sql_p_daily_precip_class=sql_p_daily_precip_class,
601
+ **sql_format_dict
602
+ )
603
+
604
+ # calculate the new corr
605
+ sql_new_corr = """
606
+ SELECT timestamp,
607
+ CASE WHEN "filled" > 0
608
+ THEN ts."filled" + ts_delta_n."delta_10min"
609
+ ELSE ts."filled"
610
+ END as corr
611
+ FROM timeseries."{stid}_{para}" ts
612
+ LEFT JOIN ({sql_delta_n}) ts_delta_n
613
+ ON (ts.timestamp)::date = ts_delta_n.date
614
+ {period_clause}
615
+ """.format(
616
+ sql_delta_n=sql_delta_n,
617
+ **sql_format_dict
618
+ )
619
+
620
+ # update the timeseries
621
+ sql_update = """
622
+ UPDATE timeseries."{stid}_{para}" ts
623
+ SET "corr" = new.corr
624
+ FROM ({sql_new_corr}) new
625
+ WHERE ts.timestamp = new.timestamp
626
+ AND ts.corr is distinct from new.corr;
627
+ """.format(
628
+ sql_new_corr=sql_new_corr,
629
+ **sql_format_dict
630
+ )
631
+
632
+ # run commands
633
+ if "return_sql" in kwargs and kwargs["return_sql"]:
634
+ return sql_update
635
+
636
+ self._execute_long_sql(
637
+ sql_update,
638
+ description="richter corrected for the period {min_tstp} - {max_tstp}".format(
639
+ **period.get_sql_format_dict(format=self._tstp_format_human)
640
+ ))
641
+
642
+ # mark last import as done, if previous are ok
643
+ if (self.is_last_imp_done(kind="qc") and self.is_last_imp_done(kind="filled")):
644
+ if (period_in.is_empty() or
645
+ period_in.contains(self.get_last_imp_period())):
646
+ self._mark_last_imp_done(kind="corr")
647
+
648
+ # update filled time in meta table
649
+ self.update_period_meta(kind="corr")
650
+
651
+ # update multi annual mean
652
+ self.update_ma_timeseries(kind="corr")
653
+
654
+ @db_engine.deco_update_privilege
655
+ def corr(self, *args, **kwargs):
656
+ return self.richter_correct(*args, **kwargs)
657
+
658
+ @db_engine.deco_update_privilege
659
+ def last_imp_richter_correct(self, _last_imp_period=None, **kwargs):
660
+ """Do the richter correction of the last import.
661
+
662
+ Parameters
663
+ ----------
664
+ _last_imp_period : weatherdb.utils.TimestampPeriod, optional
665
+ Give the overall period of the last import.
666
+ This is only for intern use of the stationsP method to not compute over and over again the period.
667
+ The default is None.
668
+ **kwargs : dict, optional
669
+ Additional keyword arguments passed to the richter_correct method.
670
+ """
671
+ if not self.is_last_imp_done(kind="corr"):
672
+ if _last_imp_period is None:
673
+ period = self.get_last_imp_period(all=True)
674
+ else:
675
+ period = _last_imp_period
676
+
677
+ self.richter_correct(
678
+ period=period,
679
+ **kwargs
680
+ )
681
+
682
+ else:
683
+ log.info("The last import of {para_long} Station {stid} was already richter corrected and is therefor skiped".format(
684
+ stid=self.id, para_long=self._para_long
685
+ ))
686
+
687
+ @db_engine.deco_update_privilege
688
+ def last_imp_corr(self, _last_imp_period=None, **kwargs):
689
+ """A wrapper for last_imp_richter_correct()."""
690
+ return self.last_imp_richter_correct(_last_imp_period=_last_imp_period, **kwargs)
691
+
692
+ @db_engine.deco_update_privilege
693
+ def _sql_fillup_extra_dict(self, **kwargs):
694
+ fillup_extra_dict = super()._sql_fillup_extra_dict(**kwargs)
695
+
696
+ stat_pd = StationPD(self.id)
697
+ if stat_pd.isin_db() and \
698
+ not stat_pd.get_filled_period(kind="filled", from_meta=True).is_empty():
699
+ # adjust 10 minutes sum to match measured daily value,
700
+ # but don't add more than 10mm/10min and don't create single peaks with more than 5mm/min
701
+ sql_extra = """
702
+ UPDATE new_filled_{stid}_{para} ts
703
+ SET filled = tsnew.filled
704
+ FROM (
705
+ SELECT ts.timestamp,
706
+ CASE WHEN tsb.filled = 0 AND tsa.filled = 0
707
+ THEN LEAST(ts.filled * coef, 5*{decim})
708
+ ELSE CASE WHEN ((ts.filled * coef) - ts.filled) <= (10 * {decim})
709
+ THEN LEAST(ts.filled * coef, 50*{decim})
710
+ ELSE LEAST(ts.filled + (10 * {decim}), 50*{decim})
711
+ END
712
+ END as filled
713
+ FROM new_filled_{stid}_{para} ts
714
+ INNER JOIN (
715
+ SELECT
716
+ date,
717
+ ts_d."raw"/ts_10."filled"::float AS coef
718
+ FROM (
719
+ SELECT
720
+ date(timestamp - '5h 50min'::INTERVAL),
721
+ sum(filled) AS filled
722
+ FROM new_filled_{stid}_{para}
723
+ GROUP BY date(timestamp - '5h 50min'::INTERVAL)
724
+ ) ts_10
725
+ LEFT JOIN timeseries."{stid}_p_d" ts_d
726
+ ON ts_10.date=ts_d.timestamp
727
+ WHERE ts_d."raw" IS NOT NULL
728
+ AND ts_10.filled > 0
729
+ ) df_coef
730
+ ON (ts.timestamp - '5h 50min'::INTERVAL)::date = df_coef.date
731
+ AND coef != 1
732
+ LEFT JOIN timeseries."{stid}_{para}" tsb
733
+ ON ts.timestamp = tsb.timestamp - INTERVAL '10 min'
734
+ LEFT JOIN timeseries."{stid}_{para}" tsa
735
+ ON ts.timestamp = tsa.timestamp + INTERVAL '10 min'
736
+ WHERE ts.filled IS NOT NULL
737
+ ) tsnew
738
+ WHERE tsnew.timestamp = ts.timestamp;
739
+ """.format(stid=self.id, para=self._para, decim=self._decimals)
740
+
741
+ fillup_extra_dict.update(dict(sql_extra_after_loop=sql_extra))
742
+ else:
743
+ log.warning(f"StationP({self.id}).fillup: There is no daily timeserie in the database, "+
744
+ "therefor the 10 minutes values are not getting adjusted to daily values")
745
+
746
+ return fillup_extra_dict
747
+
748
+ def get_corr(self, **kwargs):
749
+ return self.get_df(kinds=["corr"], **kwargs)
750
+
751
+ def get_qn(self, **kwargs):
752
+ return self.get_df(kinds=["qn"], **kwargs)
753
+
754
+ def get_richter_class(self, update_if_fails=True):
755
+ """Get the richter class for this station.
756
+
757
+ Provide the data from the meta table.
758
+
759
+ Parameters
760
+ ----------
761
+ update_if_fails: bool, optional
762
+ Should the richter class get updatet if no exposition class is found in the meta table?
763
+ If False and no exposition class was found None is returned.
764
+ The default is True.
765
+
766
+ Returns
767
+ -------
768
+ string
769
+ The corresponding richter exposition class.
770
+ """
771
+ sql = """
772
+ SELECT richter_class
773
+ FROM meta_{para}
774
+ WHERE station_id = {stid}
775
+ """.format(stid=self.id, para=self._para)
776
+
777
+ with db_engine.connect() as con:
778
+ res = con.execute(sqltxt(sql)).first()
779
+
780
+ # check result
781
+ if res is None:
782
+ if update_if_fails:
783
+ if db_engine.is_superuser:
784
+ self.update_richter_class()
785
+ # update_if_fails is False to not get an endless loop
786
+ return self.get_richter_class(update_if_fails=False)
787
+ else:
788
+ warnings.warn("You don't have the permissions to change something on the database.\nTherefor an update of the richter_class is not possible.")
789
+ return None
790
+ else:
791
+ return None
792
+ else:
793
+ return res[0]
794
+
795
+ def get_horizon(self):
796
+ """Get the value for the horizon angle. (Horizontabschirmung)
797
+
798
+ This value is defined by Richter (1995) as the mean horizon angle in the west direction as:
799
+ H’=0,15H(S-SW) +0,35H(SW-W) +0,35H(W-NW) +0, 15H(NW-N)
800
+
801
+ Returns
802
+ -------
803
+ float or None
804
+ The mean western horizon angle
805
+ """
806
+ return self.get_meta(infos="horizon")
807
+