Mathe für Muffel (Linux-Magazin, August 2004)

Das Modul Math::Algebra::Symbols löst Gleichungen mittels klassischer Mathematik mit Symbolen. Imager::Plot erzeugt professionell aussehende Graphen.

Millionen von Schülern graust's vor dem realitätsfernen Algebraunterricht. Dabei zieht sich das Rechnen mit der Variablen x durch alle Lebenslagen: Ob Benzinverbrauch oder eine Fuchs- und Hasenjagd, überall kommen die Grundsätze der Algebra gut zur Geltung. Heute übernimmt einmal Perl das Lösen und Umformen von Gleichungen.

Nehmen wir als Beispiel eine einfache Formel: In Amerika geben Autohersteller nicht an, wieviel Liter Benzin ihre Produkte auf 100 Kilometer verbrauchen, sondern wieviele Meilen diese mit einer Gallone Benzin fahren. Ein hoher Wert indiziert also niedrigen Benzinverbrauch.

Benzinschleudern auf amerikanisch

Wie lässt sich nun ein Miles/Gallon-Wert in Liter pro 100 km umrechnen? Listing mpgal definiert mit Math::Algebra::Symbols zwei Symbole $gallons und $miles und baut die Formel Schritt für Schritt auf. Der Ausdruck

    my $liters = $gallons * 37854118/10000000;

gibt an, dass eine Gallone 3.7854118 Litern entspricht. Zwei Dinge sind zu beachten: Erstens mag Math::Algebra::Symbols in der bei Drucklegung vorhandenen Version 1.16 (noch) keine langen Fließkommawerte, also gibt man sie als Brüche an. Und da $liters die Anzahl der Liter im Beispiel angibt, muss man die Anzahl der Gallonen mit 3.7854118 multiplizieren: Zwei Gallonen (eine 2 für $gallons) entspricht so 7.5708236 Litern in $liters. Ähnliches gilt für Kilometer und Meilen, eine Meile entspricht 1.609344 Kilometern.

Den Benzinverbrauch pro 100km gibt die Formel

    my $usage = $liters / $kilometers * 100;

an. Da vorher schon Formeln für $liters und $kilometers angegeben wurden, ersetzt Math::Algebra::Symbols die Symbole und generiert eine Gleichung, die nur noch von $gallons und $miles abhängt. Zeile 18 in mpgal gibt sie aus:

    Formula: 94635295/402336*$gallons/$miles

Math::Algebra::Symbols hat dazu mit ein wenig Bruchrechnung die Konstanten gekürzt. Um konkrete Werte in die Formel einzusetzen, weist mpgal den Variablen $gallons und $miles Werte zu und ruft in Zeile 25 jeweils eval $usage auf, um die Formel anzuwenden:

    20 m/gal: 11.8 l/100km
    30 m/gal:  7.8 l/100km
    40 m/gal:  5.9 l/100km

Das war nun trivial, eine einfache Funktion in Perl hätte es auch getan. Math::Algebra::Symbols kann aber noch mehr: Gleichungen, auch gerne höheren Grades, nach Variablen auflösen, zum Beispiel.

Listing 1: mpgal

    01 #!/usr/bin/perl
    02 ###########################################
    03 # mpgal - miles/gallon => liters/100km
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Math::Algebra::Symbols;
    10 
    11 my ($gallons, $miles) = 
    12    symbols(qw(gallons miles));
    13 
    14 my $liters = $gallons * 37854118/10000000;
    15 my $kilometers = $miles * 1609344/1000000;
    16 my $usage = $liters / $kilometers * 100;
    17 
    18 print "Formula: $usage\n";
    19 
    20 for $miles (qw(20 30 40)) {
    21 
    22     $gallons = 1;
    23 
    24     printf "$miles m/gal: " .
    25            "%4.1f l/100km\n", eval $usage;
    26 }

Der Fuchs jagt den Hasen

Wie wär's mit folgender Textaufgabe: Ein Fuchs sieht einen in 10 Meter Entfernung und konstanten 5 Metern pro Sekunde davonzischenden Hasen und beginnt die Hatz, während der er mit 7 Metern pro Sekundenquadrat beschleunigt. Wie lange dauert es, bis er den Hasen schnappt?

Listing race definiert hierzu ein Symbol $t für die verstrichene Zeit in Sekunden und gibt die von Hase und Fuchs gelaufene Strecke, abhängig von der Zeit an:

    my $rabbit = 10 + 5 * $t;
    my $fox    =      7 * $t * $t;

Der Hase hat mit seinen 10 Metern Vorsprung und gemäß der Formel für konstante Geschwindigkeit (s = v * t), zum Zeitpunkt t die Strecke 10 + 5 * t zurückgelegt, während der Fuchs gemäß der Formel für konstante Beschleunigung (s = a * t**2) genau 7 * $t**2 Meter aufholt.

Er schnappt den Hasen, wenn beide Streckenangaben übereinstimmen, also die Gleichung

    my $schnapp = ($rabbit - $fox);

den Wert 0 liefert. Von Hand ausgerechnet liefe dies auf eine quadratische Gleichung hinaus, und ich müsste mein Formelbuch aus der 7. Klasse aus dem Keller holen, aber dank Math::Algebra::Symbols löst man $schnapp einfach mittels

    $schnapp->solve("t")

nach $t auf und erhält (wegen der quadratischen Gleichung) eine Liste mit zwei symbolischen Gleichungslösungen zurück:

    Solution: 1/14*sqrt(305)+5/14
    Solution: -1/14*sqrt(305)+5/14

Da negative Zeiten für praktische Belange wie das Leben des Hasen irrelevant sind, wird die zweite Lösung ab Zeile 23 verworfen. Den Lösungswert in Sekunden erhält man durch Einsetzen, was wie im vorigen Beispiel Perls eval erledigt:

    my $val = eval $solution;

Nach etwa 1.60 Sekunden ist's also aus für den Hasen, und nachdem Zeile 27 die symbolische Variable $t auf dieses Ergebnis gesetzt hat, steht auch die vom Fuchs zurückgelegte Strecke fest: eval $fox gibt sie mit etwa 18.02 Metern an.

Listing 2: race

    01 #!/usr/bin/perl
    02 ###########################################
    03 # race - Fox chasing a Rabbit
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Math::Algebra::Symbols;
    10 
    11 my ($t) = symbols(qw(t));
    12 
    13 my $rabbit = 10 + 5 * $t;
    14 my $fox    =      14/2 * $t * $t;
    15 
    16 my $schnapp = ($rabbit - $fox);
    17 
    18 for my $solution 
    19     (@{$schnapp->solve("t")}) {
    20     print "Solution: $solution\n";
    21     my $val = eval $solution;
    22     if($val < 0) {
    23         print "Discarded\n";
    24         next;
    25     } else {
    26         printf "%.2f seconds\n", $val;
    27         $t = $val;
    28         printf "%.2f meters\n", eval $fox;
    29     }
    30 }

Grafisch aufpoliert

Listing graph illustriert das Ganze graphisch, wie Abbildung 1 zeigt. Mit dem Modul Imager::Plot vom lassen sich mittels ein paar Zeilen Perlcode professionelle Plots in verschiedenen Bildformaten zeichnen.

Abbildung 1: Nach etwa 1.6 Sekunden schnappt der konstant beschleunigende Fuchs den mit 10 Meter Vorsprung und konstanter Geschwindigkeit rennenden Hasen.

Zeile 12 erzeugt ein neues Imager::Plot-Objekt und nutzt als Font für die Graphenbeschriftung den unter dem angegebenen Pfad stehenden Truetype-Font tahoma.ttf.

Die for-Schleife ab Zeile 21 iteriert in Hundertstel-Schritten über X-Werte von 0.0 bis 2.0 und legt drei Arrays an: @t für die X-Achsenwerte und @rabbit bzw. @fox für die den jeweiligen Zeitwerten gemäß den Bewegungsformeln zugeordneten Ortswerte von Hase und Fuchs.

Zeile 28 fügt den Hasenplot in Grün in das Koordinatensystem ein, Zeile 37 und folgende fügt den Graphen des Fuchses in rot hinzu. Die Render-Funktion in Zeile 55 übernimmt das Zeichnen und die write-Methode in Zeile 58 schreibt das Ganze in eine PNG-Datei.

Kurvendiskussion

Abbildung 2 zeigt den Kurvenverlauf des Polynoms y = t^3 - 3t^2 - 3t + 1, dessen zwei Bäuche jeweils ein lokales Maximum und Minimum darstellen. In der Schule lernt man, dass die Steigung der Kurve an diesen Stellen gleich Null ist. Um sie zu bestimmen, differenziert man die Funktion, setzt das Ergebnis gleich Null und bestimmt die Lösungen der entstehenden Gleichung.

Glücklicherweise kann Math::Algebra::Symbols einfache Funktionen differenzieren:

    my ($x) = symbols('x');
    my $y = $x**3 - 3*$x**2 - 3*$x + 1;
    my $diff = $y->d('x');
    my $extrema = $diff->solve('x');
    print eval($_), "\n" for @$extrema;

Die Methode $y->d('x') differenziert die in $y definierte Funktion nach $x, was das angegebene Polynom dritten Grades in eines zweiten Grades überführt. Die nachfolgend aufgerufene solve()-Methode löst letzteres und gibt wie vorher eine Referenz auf ein Array mit zwei Elementen zurück:

   -1/6*sqrt(72)+1
   1/6*sqrt(72)+1

Diese rationalen Zahlen konvertiert die eval-Funktion in die Fließkommawerte

    -0.414213562373095
    2.41421356237309

Das sind die x-Werte der Bäuche. Die y-Werte erhält man, wenn man die Variable $x gleich dem evaluierten Wert für das jeweilige Extremum setzt und anschließend $y auswertet:

    for(@$extrema) {
        $x = eval $_;
        print eval($y), "\n";
    }

Das Ergebnis:

    1.65685424949238
    -9.65685424949238

Oder wie wär's mit dem Wendepunkt des Graphen zwischen den zwei Bäuchen? An dieser Stelle ist die Grenze zwischen abnehmender und zunehmender Steigung. Wie ich von meinem genialen Mathelehrer Hauptner am Gymnasium Neusäß auch nach zwanzig Jahren noch weiss, ist die zweite Ableitung dort gleich Null:

    $y->d('x')->d('x')->solve('x');

gibt exakt den Wert 1 zurück -- da staunt der Fachmann und der Laie wundert sich!

Math::Algebra::Symbols kann auch mit allerlei trigonomischen Funktionen umgehen, allerdings kommt es bei komplizierteren Strukturen noch ins Schleudern.

Abbildung 2: Der Funktionsverlauf des Polynoms t^3 - 3t^2 - 3t + 1

Vorsicht, Baustelle!

Math::Algebra::Symbols und Imager::Plot sind vom CPAN erhältlich und eignen sich hervorragend zum Herumspielen mit allerlei mathematischen Rätseln. Math::Algebra::Symbols ist allerdings noch Alpha-Qualität, wird aber von seinem Autor, Philip Brenan, stetig weiterentwickelt und könnte im Laufe der Zeit immer mehr Funktionen, ähnlich wie Mathematica, anbieten. Paukt fleißig Mathe, lernt für's Leben!

Listing 3: graph

    01 #!/usr/bin/perl
    02 ###########################################
    03 # graph -- Graph of fox/rabbit chase
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use strict;
    07 use warnings;
    08 
    09 use Imager;
    10 use Imager::Plot;
    11 
    12 my $plot = Imager::Plot->new(
    13   Width  => 550,
    14   Height => 350,
    15   GlobalFont => 
    16   '/usr/share/fonts/truetype/tahoma.ttf');
    17 
    18 my (@t, @rabbit, @fox);
    19 
    20     # Generate function data
    21 for(my $i = 0.0; $i < 2.0; $i += 0.01) {
    22     push @t, $i;
    23     push @rabbit, 10 + 5 * $i;
    24     push @fox, 7 * $i * $i;
    25 }
    26 
    27     # Add rabbit plot
    28 $plot->AddDataSet(X => \@t, Y => \@rabbit,
    29   style => { 
    30     marker => { 
    31       size   => 2,
    32       symbol => 'circle',
    33       color => Imager::Color->new('green')
    34 }});
    35 
    36     # Add fox plot
    37 $plot->AddDataSet(X => \@t, Y => \@fox,
    38   style => { 
    39     marker => {
    40       size   => 2,
    41       symbol => 'circle',
    42       color  => Imager::Color->new('red')
    43     }});
    44 
    45 my $img = Imager->new(xsize => 600, 
    46                       ysize => 400);
    47 
    48 $img->box(filled => 1, color => 'white');
    49 
    50     # Add text
    51 $plot->{'Ylabel'} = 'Distance';
    52 $plot->{'Xlabel'} = 'Time';
    53 $plot->{'Title'}  = 'Fox vs. Rabbit';
    54 
    55 $plot->Render(Image => $img, 
    56     Xoff => 40, Yoff => 370);
    57 
    58 $img->write(file => "graph.png");

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2004/08/Perl

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.