Radfahren durchgerechnet: 95 Prozent Grundumsatz und eine Route mit 0 km neuer Strecke

Angefangen hat es mit einem doppelten Punkt im Chart. Im Health Dashboard tauchte der heutige Tag zweimal auf, weil eine Endmarke auch dann angehängt wurde, wenn für heute schon ein Datenpunkt existierte. Aus dem Fix wurde ein Umbau, und aus dem Umbau die Frage, die mich dann zwei Wochen beschäftigt hat: Wie optimiere ich eigentlich mein Radfahren?

[[img:1]]

Datenbasis sind 23 GPX-Tracks aus 60 Tagen, sekundenweise durch ein Physikmodell gejagt statt durch Apples Puls-Schätzung.

Das Training ist nicht der Hebel

Mein Reflex war, am Training zu drehen. Die Zahlen sagen was anderes: In 60 Tagen kamen 130.500 kcal aus dem Grundumsatz und 6.595 aus dem gesamten Training. 95 zu 5. Ein Doppel-Cheeseburger mit 1.273 kcal entspricht 165 Minuten auf dem Rad.

Radfahren ist damit kein Kalorienhebel, sondern Muskelschutz und Herz-Kreislauf-Training. Die Bilanz entscheidet der Teller. Innerhalb der Fahrt gibt es trotzdem deutliche Unterschiede:

bergauf über 2 Prozent: 10,4 kcal/min, aber nur 14 Prozent der Fahrzeit

flach: 5,2 kcal/min, 78 Prozent der Zeit

bergab: 0,5 kcal/min

22 Prozent der Fahrzeit rolle ich unter 25 Watt, da läuft praktisch nur der Grundumsatz. Ein Höhenmeter kostet bei 134 kg Systemgewicht 1,31 kcal, unabhängig vom Tempo. Höhenmeter sind der stärkste Einzelhebel, schneller fahren der schwächste: Wer eine feste Runde schneller fährt, ist einfach früher zu Hause. Ich plane seitdem in Minuten, nicht in Kilometern.

Der Puls ist Symptom, nicht Regler. Die Korrelation zwischen Puls und Verbrauch liegt bei 0,82, rechnet man das Tempo heraus, bleiben 0,54. Der Rest ist Hitze, Müdigkeit und Trinkstand.

Apropos trinken: Ich hatte zuerst 1,0 bis 1,5 l/h Schweißrate im Plan, aus der Literatur zu Ausdauersportlern. Falsch übertragen, die Werte stammen von Rennradfahrern bei 250 bis 300 Watt. Verdunsten kann nur, was an Wärme anfällt, und ein Liter Schweiß entzieht 2.430 kJ. Bei meinen 120 bis 180 Watt sind es real 0,5 bis 0,8 l/h. Der Kopfschmerz-Hebel ist ohnehin nicht Wasser, sondern Natrium: Rund 800 mg gehen pro Liter Schweiß verloren, und wer die mit reinem Wasser auffüllt, verdünnt sein Blutnatrium noch zusätzlich.

Die verworfene Idee: Wasser bergab laufen lassen

Wenn Höhenmeter der Hebel sind, brauche ich eine Runde mit möglichst vielen davon im gleichen Zeitbudget. Also habe ich mir einen Routenplaner gebaut: OpenStreetMap für die Wege, ein Höhenmodell für die Höhen, das Leistungsmodell für die Geschwindigkeit.

Mein erster Ansatz war eine Wassersimulation. Vom höchsten befahrbaren Punkt Wasser bergab laufen lassen, immer die steilste abwärts führende Kante. Diese Fließpfade sind per Konstruktion die steilsten durchgehenden Linien im Netz, rückwärts gefahren also die höhenmeter-dichtesten Anstiege. Zählt man dann noch, wie viele Quellen über eine Kante laufen, hat man die Flow-Akkumulation, mit der Hydrologen Bachläufe finden. Nur eben für Rampen.

Als Routenplanung taugt das nicht, und das war schnell klar. Ein Fließpfad läuft von oben nach unten und hört unten auf. Er kennt kein Zeitbudget, keinen Rückweg und erst recht keine Runde. Ich wollte aber eine Feierabendrunde, die da wieder rauskommt wo sie angefangen hat. Die Simulation ist deshalb wieder rausgeflogen, die Route rechnet der Optimierer direkt auf dem Wegegraphen.

Übrig geblieben ist eine hübsche Karte, auf der man sieht, wo die Rampen überhaupt liegen.

[[img:2]]

Eine Route mit 0,0 km neuer Strecke

Der erste brauchbare Lauf gewann mit 259 Höhenmetern. Ich habe nachgefragt, wo ich da eigentlich langfahre. Antwort: nach Hopferbach, dieselbe Rampe zweimal, gleicher Weg zurück. 0,0 km einmalig gefahrene Strecke.

Der Optimierer hatte recht. Reine Höhenmeter-Maximierung führt zwingend zur Wiederholung, weil jede neue Strecke Zeit auf flachem Verbindungsstück kostet und dieselbe Rampe nochmal zu fahren nicht. Mir fehlte keine Rechenleistung, mir fehlte eine Nebenbedingung. --min-new setzt jetzt den Mindestanteil einmalig gefahrener Strecke.

Eine andere Rückfrage hat davor schon einen echten Bug freigelegt. Ich wollte nur wissen, wie ich von Fünfhäuser heimkomme, und bekam 9,8 km für eine Verbindung von 1,4 km Luftlinie. Das war kein Umweg, das war ein zerschnittener Graph.

Der teuerste Fehler war die Auflösung

Der Steigungsfilter warf alles aus dem Netz, was steiler als die fahrbare Grenze war, und prüfte dabei auch Kanten von 20 bis 50 Metern Länge. Bei einem Höhenraster mit 300 Metern Rasterweite stammt deren Höhendifferenz aber aus Interpolationsrauschen, nicht aus echter Steigung. So flogen flache Straßen raus. Die Regel steht jetzt fest im Code: Steigungsfilter erst ab Kantenlängen oberhalb der halben Rasterweite. Nach dem Fix stiegen alle Routenvarianten um 18 bis 42 Höhenmeter.

Die eigentliche Lösung war, das grobe Raster loszuwerden. Baden-Württemberg gibt sein amtliches Höhenmodell DGM1 kostenlos raus, 1 Meter Rasterweite, 2x2-km-Kacheln. Das URL-Schema ist nirgends dokumentiert, steht aber im Quelltext des offiziellen QGIS-Plugins:

https://opengeodata.lgl-bw.de/data/dgm/dgm1_32_{easting}_{northing}_2_bw.zip

Kachelraster in UTM32, easting ungerade, northing gerade, jeweils in Kilometern. Wer das nicht weiß, bekommt nur 404er.

Mit 1 Meter zu rechnen wäre Unsinn: Mein 9-km-Radius wären über 10 GB Höhenpunkte, und was zählt, ist die Steigung über Kantenlängen von 20 bis 200 Metern. Also aggregiere ich per gdalwarp -r average auf 10 Meter runter. Aus 12 GB Rohdaten wird ein GeoTIFF von 9 MB, dreißigmal feiner als vorher, und es passt komplett in den RAM. Die Aggregation ist dabei kein Kompromiss, sondern der Punkt: Sie beseitigt genau das Rauschen, das vorher den Steigungsfilter sabotiert hat.

Das läuft in einem Container auf ghcr.io/osgeo/gdal, weil ich keine GDAL-Toolchain auf dem Mac haben will:

docker run --rm -v "$PWD:/work" healthgeo \
python3 scripts/geodata/build_dem.py --radius 9 --target-res 10 --out var/route-cache/dem.tif

Gespart habe ich mir dabei pyosmium, das sich ohne Build-Toolchain nicht installieren ließ. Stellt sich raus: GDAL bringt den OSM-PBF-Treiber sowieso mit, ogr2ogr liest den Geofabrik-Auszug direkt.

[[img:3]]

Was rausgekommen ist

[[img:4]]

231 Höhenmeter, 17,9 km, 59 Minuten, 98 Prozent neue Strecke. Meine bisherigen Runden lagen bei 82 Höhenmetern in 41 Minuten. Macht +149 Höhenmeter und rund 250 kcal für 18 Minuten mehr Zeit.

Die genaue Route liefere ich hier natürlich nicht mit. Karte und Höhenprofil sind an Anfang und Ende um je 2,5 Kilometer gekürzt und der Startpunkt ist raus, sonst würde ich meinen Wohnort gleich mit veröffentlichen. Wer sowas selbst will, nimmt den Planer und seinen eigenen Startpunkt.

Beweisbar optimal ist die Route nicht. Maximale Höhenmeter auf einer Rundtour im Zeitbudget ist ein Orienteering Problem und damit NP-schwer. Deterministisch ist sie: gleicher Input, bit-identische Route.

Was ich aus den zwei Wochen mitnehme: Beide echten Bugs kamen nicht aus Tests, sondern aus der Frage "das verstehe ich nicht". Und der teuerste Fehler steckte nicht in der Physik, sondern in einem zu groben Raster.