πŸ’Ύ Archived View for bacaliu.de β€Ί 20220727-regenradar.txt captured on 2023-07-10 at 13:51:47.

View Raw

More Information

-=-=-=-=-=-=-

                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                               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>