πΎ Archived View for bacaliu.de βΊ 20220727-regenradar.txt captured on 2023-07-10 at 13:51:47.
-=-=-=-=-=-=-
ββββββββββββββββββββββββββββββββββββββββ REGENRADAR DWD-Daten zu Diagramm mit Python-Bokeh ββββββββββββββββββββββββββββββββββββββββ 2022-07-27 Inhaltsverzeichnis ββββββββββββββββββ 1. Der Plan 2. Das Vorgehen .. 1. Laden der Daten .. 2. Speichern als .pickle .. 3. Laden der Arrays und Timestamps .. 4. Stereographische Umwandlung .. 5. FehlerprΓΌfung: richtige Koordinaten? .. 6. Endlich: das Plotten 3. Bibliography Nav Footer 1 Der Plan ββββββββββ Der [DWD] stellt auf <https://opendata.dwd.de> auch einige Radar-Daten zum Download bereit. Diese mΓΆchte ich nutzen, um eine kurzfristige Niederschlagsprognose zu erstelen. Insbesondere fΓΌr das Planen von Radtouren oder Wanderungen ist es von Vorteil 5-minΓΌtige Daten zu haben, statt nur 1-StΓΌndige des MOSMIX, die zudem nur alle 6 Stunden aktualisiert werden. Bei der Recherche stoΓe ich auf Bokeh (Van de Ven). Vorteile gegenΓΌber Matplotlib sehe ich mehrere: β Geschwindigkeit: Um einige Matplotlib-Figuren abzuspeichern werden mehrere Sekunden benΓΆtigt, Bokeh schafft das Erstellen in einem Bruchteil der Zeit, weil erst am EndgerΓ€t gerendert wird β’ daher habe ich auch `mosmix.py' auf Bokeh umgestellt β InteraktivitΓ€t: Der User kann zoomen, verschieben etc. und die Achsbeschriftungen βwandern mitβ, im gegensatz dazu ein langes `.png' in einem x-scrollable-DIV zu stecken. [DWD] <https://www.dwd.de> 2 Das Vorgehen ββββββββββββββ Zwecks dessen schreibe ich ein Skript, welches dieses darstellt. FolgendermaΓen stelle ich *schematisch* dar, wie das funktioniert. 2.1 Laden der Daten βββββββββββββββββββ βββββ β def reload_data(): β link = "https://opendata.dwd.de/weather/radar/composit/rv/DE1200_RV_LATEST.tar.bz2" β β os.system(f"rm {PATH}/DE1200*") β os.system(f"wget {link} -O {os.path.join(PATH, 'data.bz2')}") β os.system(f"tar -xf {os.path.join(PATH, 'data.bz2')} -C {PATH}") βββββ Programmlisting 1: Herunterladen Die Daten liegen hinter dem angegebenen link. Man lΓΆsche zunΓ€chst vorhandene Daten, die alle mit "DE1200" beginnen, nicht aber die spΓ€ter im gleichen Ordner erzeugten `.pkl'-Dateien, damit auch /wΓ€hrend/ des neuladens Diagramme erstellt werden kΓΆnnen. Dann lade man den link herunter und extrahiere die `.tar.gz'. Es entstehen fΓΌr jede 5 Minuten eine Datei, die einfach `DE1200{TIMESTAMP}' heiΓt. 2.2 Speichern als .pickle βββββββββββββββββββββββββ βββββ β def bin_to_array(f, shape): β start = len(f) - (shape[0]*shape[1])*2 β array = np.zeros((shape[0], shape[1])) β index = start β for i in reversed(range(shape[0])): β for j in range(shape[1]): β num = (f[index+1] % 16)*256+f[index] β des = f[index]//16 β if des >= 8: β array[i, j] = None β else: β num = num/100*12 β if des >= 4 and num: β num *= -1 β array[i, j] = num β index += 2 β return array β β β def load_arrays(): β files = os.listdir(PATH) β files = filter(lambda f: 'DE1200' in f, files) β files = list(files) β files.sort() β β arrays = [] β timestamps = [] β for n, file in enumerate(tqdm(files, desc="lese BinΓ€rdaten")): β num = int(file.split("_")[2]) β tstr = re.findall("[0-9]{10}", file)[0] β dt = datetime.strptime("20"+tstr, "%Y%m%d%H%M") β dt += timedelta(minutes=num) β timestamps.append(dt) β with open(os.path.join(PATH, file), "rb") as f: β f = f.read() β array = bin_to_array(f, (1200, 1100)) β arrays.append(array) β β arrays = np.array(arrays) β β with open(os.path.join(PATH, "timestamps.pkl"), "wb") as f: β pickle.dump(timestamps, f) β β with open(os.path.join(PATH, "arrays.pkl"), "wb") as f: β pickle.dump(arrays, f) βββββ Programmlisting 2: Umwandeln und Abspeichern `bin_to_array' stellt hier eine Hifsfunktion dar um die Radardaten aufzubereiten. Diese liest die BinΓ€rdaten ein und fΓΌhrt einfache Kalkulationen durch um z.B. das Vorzeichen umzukehren wenn ein bestimmtes Bit gesetzt ist. NΓ€heres dazu findet man im [PDF zu den Radarprodukten des DWD]. Diese Funktion wird von `load_arrays' fΓΌr jede der Dateien aufgerufen wird um ein Array zu erhalten. Es werden dann jeweils Array und Zeitstempel (aus dem Dateinamen) in Listen geschrieben und letztlich in `timestamp.pkl' und `arrays.pkl' gepickelt. [PDF zu den Radarprodukten des DWD] <https://www.dwd.de/DE/leistungen/radolan/radolan_info/radolan_radvor_op_komposit_format_pdf.pdf?__blob=publicationFile&v=5> 2.3 Laden der Arrays und Timestamps βββββββββββββββββββββββββββββββββββ Wie es sich fΓΌr ein gutes Programm gehΓΆrt, werden getter und setter genutzt. βββββ β def get_arrays(): β with open(os.path.join(PATH, "arrays.pkl"), "rb") as f: β return pickle.load(f) βββββ Programmlisting 3: get_arrays βββββ β def get_timestamps(): β with open(os.path.join(PATH, "timestamps.pkl"), "rb") as f: β timestamps = pickle.load(f) β timestamps = [UTC.localize(t).astimezone(CET).replace(tzinfo=None) for t in timestamps] β return timestamps βββββ Programmlisting 4: get_timestamps In `get_timestamps' wandle ich zwar die Zeiten in CET um, aber ersetze die `tzinfo' des `datetime'-Objekts jeweils durch `None'. Grund dafΓΌr ist, dass `Bokeh' nicht besonders gut mit Zeitzonen umgeht, und ich mir den Stress erspare. Das Radar ist nur fΓΌr Abdeckung in Deutschland, also reicht es auch nur dessen Zeitzone zu verwenden. 2.4 Stereographische Umwandlung βββββββββββββββββββββββββββββββ Der Benutzer wird seine Koordinaten in lat/lon angeben oder durch eingabe eines Suchtermes ΓΌber (βNominatimβ 2022) welche generieren. Diese mΓΌssen nun aber umgewandelt werden in die Pixel, die das Rader bietet. Auch hier half o.g. PDF-Dokument des DWD, um eine Funktion zu definieren. βββββ β def latlon2xy(lat, lon): β R = 6370.04 β M = lambda p: (1+np.sin(60/180*np.pi))/(1+np.sin(p/180*np.pi)) β β p = lat β l = lon β β x = R*M(p)*np.cos(p/180*np.pi)*np.sin((l-10)/180*np.pi) β y = -R*M(p)*np.cos(p/180*np.pi)*np.cos((l-10)/180*np.pi) β return x - (-543.4628665604781), 1200 - y -4808.644645330335 βββββ Programmlisting 5: latlon2xy 2.5 FehlerprΓΌfung: richtige Koordinaten? ββββββββββββββββββββββββββββββββββββββββ Um Fehler aufgrund unerlaubter Koordinaten zu vermeiden rufe ich bei jeder Nutzereingabe `is_valid_koordinate' auf. βββββ β def is_valid_koordinate(lat, lon): β x, y = latlon2xy(lat, lon) β arrays = get_arrays() β x_max, y_max = arrays[0].shape β β return all([x>=0, x<x_max, y>=0, y<y_max]) βββββ Programmlisting 6: valide Koordinaten? Die Arrays werden geladen und deren Shape gibt Auskunft darΓΌber ob die gewΓ€hlten Koordinaten im Bereich liegen. 2.6 Endlich: das Plotten ββββββββββββββββββββββββ βββββ β def bok_rain(lat=51.85, lon=7.49): β arrays = get_arrays() β timestamps = get_timestamps() β β x, y = latlon2xy(lat, lon) β radius=[1, 2, 4] β β max_intense = [ β a[ β max(int(y-max(radius)), 0):min(int(y+max(radius)), 1200), β max(int(x-max(radius)), 0):min(int(x+max(radius)), 1100) β ].max() β for a in arrays β ] β β plot = figure( β title=f"Niederschlagsradar fΓΌr {lat}ΒΊN/{lon}ΒΊE", β x_range=( β timestamps[0], β timestamps[-1] β ), β x_axis_type="datetime", β tools=["save", "xpan", "xwheel_zoom", "reset", "xzoom_in", "xzoom_out"], β active_scroll="xwheel_zoom", β y_range=[0, max(max(max_intense)*1.01, 1)], β ) β β try: β for radius, alpha in zip( β [1, 2, 4], β [1, 0.7, 0.4] β ): β intense = [ β a[ β max(int(y-radius), 0):min(int(y+radius), 1200), β max(int(x-radius), 0):min(int(x+radius), 1100) β ].max() β for a in arrays β ] β β plot.line( β np.array(timestamps), β intense, β legend_label=f"Radius von {radius}km", β line_color="#458588", β line_alpha=alpha β ) β β plot.line( β np.array(timestamps), β [ β sum(intense[:i])/12 β for i, _ in enumerate(intense) β ], β line_dash=[4, 4], β line_color="#458588", β line_alpha=alpha/2 β ) β except (): β print("AAAAAAAAAAAAAAAAAAAA no Error-Handling") β β plot.width = 1024 β plot.height = 384 β plot.background_fill_alpha = 0 β plot.border_fill_alpha = 0 β plot.sizing_mode = "stretch_width" β β plot.outline_line_color = "gray" β plot.title.text_color = "gray" β β plot.legend.label_text_color = "gray" β plot.legend.background_fill_color = "gray" β plot.legend.background_fill_alpha = 0.125 β β for axis in [plot.xaxis, plot.yaxis]: β axis.axis_label_text_color = "gray" β axis.axis_line_color = "gray" β axis.major_label_text_color = "gray" β axis.major_tick_line_color = "gray" β axis.minor_tick_line_color = "gray" β β for grid in [plot.xgrid, plot.ygrid]: β grid.grid_line_color = "gray" β grid.grid_line_alpha = 0.5 β β script, divs = components(plot, CDN) β return script, divs βββββ Programmlisting 7: plotten Die tolle Funktion mit dem Namen `bok_rain' (im gegensatz zu `plot_rain' eine Implementation mit `Bokeh', daher der Name) ruft nun die Daten auf und berechnet zunΓ€chst die maximale intensitΓ€t im maximalen Radius ΓΌber die gesamte Zeit. Die Radien dienen dazu, auch Regen in der Umgebung berΓΌcksichtigen zu kΓΆnnen; zieht etwa in 80 Minuten ein sehr Starker und kleiner Schauer in 2km vorbei, so sollte man dennoch mit Regen rechnen, denn er kΓΆnnte wachsen. Dieser Radius wird dann als obere Schranke fΓΌr das Diagramm eingestellt, minimal aber 1 (in Liter pro Quadratmeter und Stunde). Bokeh geben wir als Tools nur die x-zoom varianten, denn ein Zoom in alle Richtungen lΓ€sst einen schnell den Plot verlieren. Dann wird versucht (und bei Fehlern passiert nichts) fΓΌr jeden Radius mit unterschiedlichen Transparenzwerten eine Linie zu plotten. Nach einigen Einstellungen bezΓΌglich der Farben, viel "gray", damit das Diagramm im hellen und dunklen Modus gleich gut funktioniert, wird es mit `components' als skript und div exportiert und auf der Seite eingebettet. 3 Bibliography ββββββββββββββ βNominatim,β. 2022. July 12, 2022, URL: <https://nominatim.openstreetmap.org/ui/search.html>, retrieved on July 27, 2022. Van de Ven, B. 2022. βBokeh,β July 7, 2022, URL: <https://bokeh.org>, retrieved on July 27, 2022. Nav βββ β Tags: [Meta] - [Wetter] - [Python] β Formats: [md] - [txt] - [html] - [gmi] [Meta] <./tags/Meta.org> [Wetter] <./tags/Wetter.org> [Python] <./tags/Python.org> [md] <./20220727-regenradar.md> [txt] <./20220727-regenradar.txt> [html] <./20220727-regenradar.html> [gmi] <./20220727-regenradar.gmi> Footer ββββββ License: CC BY-4.0 [Impressum und Datenschutz] [Impressum und Datenschutz] <./impressum-datenschutz.gmi>