Wasser Marsch! (Linux-Magazin, Mai 2021)

An ein traumatisches Ereignis als Grundschüler erinnere ich mich noch genau: Eine Wochenendzeitung hatte eine Knobelaufgabe für Kinder gestellt, deren Auflösung sie in der nächsten Ausgabe eine Woche später versprach. Es handelte sich um eine Badewanne mit zwei Wasserhähnen, von denen der eine die Wanne in 10, der andere in 15 Minuten füllte, und die Frage, wie lange es wohl dauern würde, wenn man bei Hähne voll aufdrehte, bis die Wanne voll wäre?

Als kleiner Pimpf war ich mir absolut sicher, 10 plus 15 ergibt 25, also 25 Minuten! Mein Vater lachte und gab zu bedenken, das könne nicht sein, denn zwei Hähne füllten die Badewanne schneller als einer allein. Am nächsten Wochenende triumphierte ich zunächst, denn Schwarz auf Weiß gedruckt kam in der folgenden Ausgabe tatsächlich die Bestätigung, 25 Minuten wäre die richtige Lösung! Wieder eine Woche später allerdings folgte die Ernüchterung: Nach erbosten Leserkommentaren musste die Zeitung eingestehen, dass ihr ein Irrtum unterlaufen war, denn es dauert nicht 25, sondern nur 6 Minuten, bis beide Hähne die Wanne füllen. Ich fiel aus allen Wolken und beschloss, ein berühmter Kolumnenschreiber für mathematische Rätsel zu werden. Wer zuletzt lacht, lacht am besten.

Abbildung 1: Zwei parallel geschaltete Widerstände R1 und R2 ...

Abbildung 2: ... und ihr Ersatzwiderstand R.

Wasserhähne und Widerstände

Erst viel später, im Studium in einer Vorlesung für Schaltungstechnik kam mir ein ähnliches Problem unter, das ebenfalls eine gute Erklärung der Lösung des Badewannenproblems bot, nämlich die parallele Anordnung von Widerständen in einem Schaltkreis. Der Elektrotechniker misst den Widerstand eines Leiters in Ohm, und je höher der Wert, desto mehr bremst er den Stromfluss.

Wie berechnet sich nun der Ersatzwiderstand einer Parallelschaltung zweier Widerstände R1 und R2 wie in Abbildung 1? Die Lösung führt über die Ströme I1 und I2, die durch die einzelnen Widerstände fließen, und die sich nach der Wiedervereinigung der beiden Kabel rechts zum Gesamtstrom I summieren. Die anliegende Spannung ist überall gleich, nämlich U, und nach dem Ohmschen Gesetz gilt U = R * I, also summieren sich die Einzelströme I1 und I2 zu U/R1 + U/R2. Ersetzt man nun die beiden parallel angeordneten Widerstände R1 und R2 durch einen Ersatzwiderstand R, ergibt sich für den Gesamtstrom ebenfalls I = U/R, also gilt:

     U/R = U/R1 + U/R2

und die Spannung U kürzt sich heraus, sodass sich

     1/R = 1/R1 + 1/R2

und nach Umformung

     R = (R1 * R2)/(R1 + R2)

ergibt. Heutzutage übernehmen Programme dem Ingenieur die Rechenarbeit ab, und Listing 1 zeigt die Entwicklung der Formel mit sympy, einem Paket für symbolische Algebra in Python.

Mit ihm lassen sich Symbole definieren, die das Paket später bei der Auswertung von Formeln intakt lässt, statt gleich Variablen durch Werte zu ersetzen und das numerische Ergebnis einer Formel zu ermitteln. Mit der Funktion simplify() beherrscht sie auch die Regeln der Algebra, und kann Ausdrücke vereinfachen, ganz so, wie das ein mathematisch talentierter Mensch täte.

Listing 1: parallel.py

    01 #!/usr/bin/env python3
    02 from sympy import simplify, symbols, pprint
    03 
    04 r, r1, r2, u, i1, i2 = \
    05   symbols("r r1 r2 u i1 i2")
    06 
    07 i1 = u / r1
    08 i2 = u / r2
    09 
    10 r = u / (i1 + i2)
    11 
    12 pprint(simplify(r))

So definieren die Zeile 4 und 5 in Listing 1 einen ganzen Schwung von Symbolen, r1, r2, r für die beiden Widerstände der Schaltung und ihren Ersatzwiderstand, u für die anliegende Spannung, sowie i1 und i2 für die beiden Teilströme. Die Formeln der Zeilen 7 bis 10 wenden das Ohmsche Gesetz an und summieren die Teilströme zum Gesamtstrom. Der Wert für den Ersatzwiderstand r kommt beim Aufruf des Skripts mit

    $ ./parallel.py 
     r₁⋅r₂ 
    ───────
    r₁ + r₂

über die Funktion pprint() ("Pretty Print") zutage und enthüllt, dass sympy erkannt hat, dass sich die Spannung u aus dem Bruch herauskürzen ließ und das Ergebnis nicht mehr von ihr abhängt.

Vom Widerstand zur Badewanne

Die Formel funktioniert auch für das eingangs erwähnte Badewannenproblem: Im Zähler steht das Produkt der Füllzeiten pro Wasserhahn (10 * 15) und unten im Nenner deren Summe (10 + 15), macht 150/25 oder 6 Minuten, genau wie in der Musterlösung des Sonntagsblatts.

Man kann sich die Lösung übrigens auch folgendermaßen überlegen: Nach einer Minute hat der erste Wasserhahn die Wanne zu einem Zehntel gefüllt, der zweite zu einem Fünfzehntel. Beide zusammen schaffen nach einer Minute 1/10 + 1/15 = 3/30 + 2/30 = 5/30 = 1/6 der Wanne, also ist letztere nach 6 Minuten voll.

Grenzwertig

Was passiert, wenn beide Widerstände in der Paralleschaltung gegen Null Ohm gehen, also den Stromfluss überhaupt nicht bremsen, oder im Fall einer Badewanne mit Niagara-artigen Füllhähnen, die die Wanne in Nullkommanichts füllen? Anschaulich ist klar, dass solche Spitzenarmaturen in Parallelschaltung die Wanne ebenso schnell füllen wie einzeln betrieben, aber wer in der Formel (R1*R2)/(R1 + R2) die Werte für R1 und R2 auf 0 setzt und einem Python-Programm zum Ausrechnen gibt, wird sein blaues Wunder erleben, denn Computer weigern sich standhaft, Divisionen durch Null auszuführen, da diese mathematisch undefiniert sind.

Im vorliegenden Fall steht aber sowohl im Zähler als auch im Nenner eine Null, was im Grenzfall manchmal interessante, weil endliche Ergebnisse liefert. Wohl gemerkt, definiert ist der Fall 0 nicht, aber den Grenzwert für R1 und R2 gegen Null kann man sehr wohl ausrechnen. Pythons sympy hält hierzu die Funktion limit() parat, die eine symbolische Formel entgegennimmt, ein Symbol (z.B. R1) und einen Grenzwert, in diesem Fall 0.

Listing 2: limit.py

    01 #!/usr/bin/env python3
    02 from sympy import limit, symbols
    03 
    04 r1, r2, r = symbols("r1 r2 r")
    05 
    06 r = (r1 * r2)/(r1 + r2)
    07 r1 = r2
    08 
    09   # r1/2->0
    10 print(limit(r, r1, 0))
    11 
    12   # 1/x with x->0
    13 x = symbols("x")
    14 print(limit(1/x, x, 0))

Listing 2 setzt vorher noch R1 = R2, und wenn in der limit()-Funktion R1 gegen Null geht, gehen beide Variablen, R1 und R2 gegen Null. Heraus kommt für den Grenzwert des Ersatzwiderstands erwartungsgemäß Null:

    $ ./limit.py
    0
    oo

Der zweite Testfall ab Zeile 13 in Listing 2 probiert, was mit der Formel 1/x passiert, wenn x gegen Null strebt: Die Ausgabe zeigt, dass das Ergebnis der Formal in diesem Fall gegen Unendlich strebt, was die Ascii-Ausgabe "oo" illustrieren soll.

Schön und in Farbe

Um zu illustrieren, wie die beiden Wasserhähne beim Füllen der Wanne zusammenspielen, stellen die drei Balkengrafiken in Abbildung 3 den jeweiligen Füllstand im Minutentakt dar. Im obersten Graph dreht der Bademeister nur den langsamen Hahn auf, der die Wanne in 15 Minuten füllt, im mittleren ist der 10-Minuten-Schnellhahn aktiv, und im unteren beide Hähne zusammen.

Abbildung 3: Mit Pyplot erzeugte Balkengrafik der WannenfĂĽllungen

Dabei nimmt die Funktion fill_tub() ab Zeile 5 die Wannenfüllrate pro Minute entgegen (per_min), sowie die Anzahl der dargestellten X-Werte, also den Minutenticker. Obwohl die dargestellen Szenarien jeweils unterschiedliche Mengen von Minutenwerten bedienen, bestehen die Graphfunktionen der matplotlib darauf, dass X- und Y-Werte als gleichgroße Arrays vorliegen, sonst hagelt's schwer verständliche Fehlermeldungen aus den Untiefen der Library. Die Funktion fill_tub gibt zwei Arrays lx und ly zurück, die Werte für die X- und Y-Achsen in der Balkengrafik enthalten. Beide Arrays sind gleich lang, und unbesetzte Y-Werte bei bereits voller Wanne werden wegen der Initialisierung in Zeile 7 einfach auf Null gesetzt.

Listing 3: bars.py

    01 #!/usr/bin/env python3
    02 from matplotlib import pyplot as plt
    03 from fractions import Fraction
    04 
    05 def fill_tub(per_min, xmax):
    06     lx=list(range(1, xmax+1))
    07     ly=[0] * xmax
    08     sum=0
    09     for i in range(xmax):
    10         sum+=per_min
    11         if sum > 1:
    12             break
    13         ly[i]=sum
    14 
    15     return lx,ly
    16 
    17 xmax=15
    18 
    19 plt.style.use('ggplot')
    20 fig,ax = plt.subplots(
    21   nrows=3,ncols=1,figsize=(5,10))
    22 fig.suptitle("Filling a bath tub")
    23 fig.subplots_adjust(hspace=.5)
    24 
    25 lx,ly = fill_tub(Fraction(1,15), xmax)
    26 ax[0].bar(lx, ly, color='tab:red')
    27 ax[0].set_xlabel("Minutes")
    28 ax[0].set_ylabel("Fill Level")
    29 ax[0].set_title("15min faucet")
    30 
    31 lx,ly = fill_tub(Fraction(1,10), xmax)
    32 ax[1].bar(lx, ly, color='tab:orange')
    33 ax[1].set_xlabel("Minutes")
    34 ax[1].set_ylabel("Fill Level")
    35 ax[1].set_title("10min faucet")
    36 
    37 lx,ly = fill_tub(Fraction(1,10) +
    38 	Fraction(1/15), xmax)
    39 ax[2].bar(lx, ly, color='tab:green')
    40 ax[2].set_xlabel("Minutes")
    41 ax[2].set_ylabel("Fill Level")
    42 ax[2].set_title(
    43   "Both 15min and 10min faucet")
    44 
    45 plt.savefig("bars.png")

Drei verschiedene Graphen untereinander in eine Bilddatei zu malen ist in matplotlib ein Kinderspiel, denn die Funktion subplots() erzeugt in Zeile 20 einfach ein Diagrammraster mit drei Reihen und einer Spalte, gibt in ax einen Array von drei Graph-Objekten zurück und fertig ist der Lack. Eigentlich sollte matplotlib dann alles geschmackvoll zu einem Gesamtgebinde arrangieren, doch im vorliegenden Fall sah die Darstellung ohne manuelle Intervention komischerweise etwas gequetscht aus, sodass Zeile 23 mit hspace noch zusätzlichen Spielraum zwischen den Einzelgraphen schaffen muss.

Zeile 45 schließlich schreibt die Bilddaten in eine Bilddatei bars.png, und Abbildung 3 zeigt das Ergebnis in einem Bildbetrachter.

Exaktes Bruchrechnen

Damit das Programm aus einem Bruch wie 1/15 nicht sofort eine Fließkommazahl macht, zieht Listing 3 das Paket fractions herein, das echte Bruchrechnung beherrscht. So kann Zeile 25 mit Fraction(1, 15) tatsächlich 1/15 und nicht etwa 0.0666666666666667 an fill_tub() übergeben. Letzteres würde beim Aufsummieren später böse Rundungsfehler einschleusen, und die Wanne eventuell sogar zum Überlaufen bringen. Das Paket fractions hingegen überlädt Operatoren wie "+" oder ">", sodass "sum += per_min" in Zeile 10 zur Wassermenge in der Wanne auch tatsächlich 1/15 hinzuzählt und sum in der Abfrage von Zeile 11 später bei voller Wanne auf exakt 1 steht und nicht etwa auf einem unrunden Float-Wert.

Wer Python 3 fährt, installiert Pakete wie matplotlib oder sympy einfach mit pip3 install ..., am besten in einer virtualenv-Umgebung, um andere Python-Skripte auf demselben Host nicht eventuell den Teppich wegzuziehen ([3]).

Wer sich für wissenschaftliche Anwendungen interessiert, und diese mit Python attackieren möchte, dem sei das exzellente Buch "Der Python-Kurs" [2] ans Herz gelegt. Es reißt typische Anwendungsfälle für Python-Programme in den Naturwissenschaften an, springt von Beispiel zu Beispiel, und ist so breitgefächert und tiefgründig, dass selbst gestandene Ingenieure dort noch das ein oder andere längst vergessene Schmankerl aus lang vergangenen Studienzeiten wiederentdecken.

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2021/04/snapshot/

[2]

"Der Python-Kurs für Ingenieure und Naturwissenschaftler", Veit Steinkamp, https://www.amazon.de/Python-Kurs-für-Ingenieure-Naturwissenschaftler-Praxislösungen/dp/3836273160

[3]

"Virtual Environments and Packages", https://docs.python.org/3/tutorial/venv.html

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc