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.
- docker/Dockerfile +30 -0
- docker/docker-compose.yaml +58 -0
- docker/docker-compose_test.yaml +24 -0
- docker/start-docker-test.sh +6 -0
- docs/requirements.txt +10 -0
- docs/source/Changelog.md +2 -0
- docs/source/License.rst +7 -0
- docs/source/Methode.md +161 -0
- docs/source/_static/custom.css +8 -0
- docs/source/_static/favicon.ico +0 -0
- docs/source/_static/logo.png +0 -0
- docs/source/api/api.rst +15 -0
- docs/source/api/cli.rst +8 -0
- docs/source/api/weatherDB.broker.rst +10 -0
- docs/source/api/weatherDB.config.rst +7 -0
- docs/source/api/weatherDB.db.rst +23 -0
- docs/source/api/weatherDB.rst +22 -0
- docs/source/api/weatherDB.station.rst +56 -0
- docs/source/api/weatherDB.stations.rst +46 -0
- docs/source/api/weatherDB.utils.rst +22 -0
- docs/source/conf.py +137 -0
- docs/source/index.rst +33 -0
- docs/source/setup/Configuration.md +127 -0
- docs/source/setup/Hosting.md +9 -0
- docs/source/setup/Install.md +49 -0
- docs/source/setup/Quickstart.md +183 -0
- docs/source/setup/setup.rst +12 -0
- weatherdb/__init__.py +24 -0
- weatherdb/_version.py +1 -0
- weatherdb/alembic/README.md +8 -0
- weatherdb/alembic/alembic.ini +80 -0
- weatherdb/alembic/config.py +9 -0
- weatherdb/alembic/env.py +100 -0
- weatherdb/alembic/script.py.mako +26 -0
- weatherdb/alembic/versions/V1.0.0_initial_database_creation.py +898 -0
- weatherdb/alembic/versions/V1.0.2_more_charachters_for_settings+term_station_ma_raster.py +88 -0
- weatherdb/alembic/versions/V1.0.5_fix-ma-raster-values.py +152 -0
- weatherdb/alembic/versions/V1.0.6_update-views.py +22 -0
- weatherdb/broker.py +667 -0
- weatherdb/cli.py +214 -0
- weatherdb/config/ConfigParser.py +663 -0
- weatherdb/config/__init__.py +5 -0
- weatherdb/config/config_default.ini +162 -0
- weatherdb/db/__init__.py +3 -0
- weatherdb/db/connections.py +374 -0
- weatherdb/db/fixtures/RichterParameters.json +34 -0
- weatherdb/db/models.py +402 -0
- weatherdb/db/queries/get_quotient.py +155 -0
- weatherdb/db/views.py +165 -0
- weatherdb/station/GroupStation.py +710 -0
- weatherdb/station/StationBases.py +3108 -0
- weatherdb/station/StationET.py +111 -0
- weatherdb/station/StationP.py +807 -0
- weatherdb/station/StationPD.py +98 -0
- weatherdb/station/StationT.py +164 -0
- weatherdb/station/__init__.py +13 -0
- weatherdb/station/constants.py +21 -0
- weatherdb/stations/GroupStations.py +519 -0
- weatherdb/stations/StationsBase.py +1021 -0
- weatherdb/stations/StationsBaseTET.py +30 -0
- weatherdb/stations/StationsET.py +17 -0
- weatherdb/stations/StationsP.py +128 -0
- weatherdb/stations/StationsPD.py +24 -0
- weatherdb/stations/StationsT.py +21 -0
- weatherdb/stations/__init__.py +11 -0
- weatherdb/utils/TimestampPeriod.py +369 -0
- weatherdb/utils/__init__.py +3 -0
- weatherdb/utils/dwd.py +350 -0
- weatherdb/utils/geometry.py +69 -0
- weatherdb/utils/get_data.py +285 -0
- weatherdb/utils/logging.py +126 -0
- weatherdb-1.1.0.dist-info/LICENSE +674 -0
- weatherdb-1.1.0.dist-info/METADATA +765 -0
- weatherdb-1.1.0.dist-info/RECORD +77 -0
- weatherdb-1.1.0.dist-info/WHEEL +5 -0
- weatherdb-1.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
|