Computeralgebra mit SymPy#

In diesem Abschnitt wollen wir das Python-Modul SymPy vorstellen, welches uns ermöglicht symbolische Berechnungen durchzuführen. Insbesondere werden wir lernen, wie wir mit mathematischen Ausdrücken arbeiten können, wie wir Funktionen differenzieren und integrieren, und wie wir (nicht)lineare Gleichungssysteme und Differentialgleichungen lösen.

Wir müssen zunächst das Paket installieren, beispielsweise über unsere conda-Umgebung

conda install -c conda-forge sympy

und anschließend in unser Python-Skript einbinden:

import sympy as sy
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 1
----> 1 import sympy as sy

ModuleNotFoundError: No module named 'sympy'

Arbeiten mit Funktionen#

Beginnen wir zunächst damit eine mathematische Funktion zu definieren. Dazu müssen wir die veränderliche Variable kennzeichnen und anschließend die Funktionsvorschrift eingeben:

x = sy.symbols('x')
f = 1./3*x**3 - 2.*x**2 + 3.
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 x = sy.symbols('x')
      2 f = 1./3*x**3 - 2.*x**2 + 3.

NameError: name 'sy' is not defined

Die Variable x ist vom Typ

type(x)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 type(x)

NameError: name 'x' is not defined

und der Ausdruck f vom Typ

type(f)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 type(f)

NameError: name 'f' is not defined

Objekte vom Typ sympy.core.add.Add entstehen bei der Summation von, in unserem Fall,

f.args
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 f.args

NameError: name 'f' is not defined

Die 3 Argumente selbst sind symbolische Ausdrücke, welche wiederrum aus anderen Rechenoperationen hervorgegangen sind:

g1 = f.args[0]
print("Erster Summand", g1, "vom Typ", type(g1))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 g1 = f.args[0]
      2 print("Erster Summand", g1, "vom Typ", type(g1))

NameError: name 'f' is not defined
g2 = f.args[1]
print("Zweiter Summand", g2, "vom Typ", type(g2))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 g2 = f.args[1]
      2 print("Zweiter Summand", g2, "vom Typ", type(g2))

NameError: name 'f' is not defined
g3 = f.args[2]
print("Dritter Summand", g3, "vom Typ", type(g3))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 g3 = f.args[2]
      2 print("Dritter Summand", g3, "vom Typ", type(g3))

NameError: name 'f' is not defined

Der Ausdruck g1 ist nur die Zahl 3.0 und g2 und g3 sind vom Typ sympy.core.mul.Mult, sind also Ausdrücke die aus der Multiplikation weiterer Ausdrücke hervorgegangen sind. Ein mathematischer Ausdruck wird schließlich in Form eines Binärbaums dargestellt. Die genaue Darstellung bekommen wir wie folgt:

sy.srepr(f)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 sy.srepr(f)

NameError: name 'sy' is not defined

Vielleicht haben wir durch diese Betrachtung schon eine grobe Vorstellung wie symbolische Ausdrücke funktionieren. Über die genauen Details wollen wir uns aber hier keine weiteren Gedanken machen.

Schauen wir uns an, was wir mit der eben definierten Funktion anstellen können

Differenzieren#

Df = f.diff(x)
print("Ableitung:")
Df
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 1
----> 1 Df = f.diff(x)
      2 print("Ableitung:")
      3 Df

NameError: name 'f' is not defined

bzw. mit der freien Funktion sympy.diff

Df = sy.diff(f,x)
print("Ableitung:")
Df
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 1
----> 1 Df = sy.diff(f,x)
      2 print("Ableitung:")
      3 Df

NameError: name 'sy' is not defined

Natürlich kann SymPy auch kompliziertere Funktionen ableiten. Zu beachten ist hier, dass wir für alle mathematischen Funktionen die entsprechenden Funktionen aus dem SymPy-Modul verwenden:

g = sy.exp(-x**2)*sy.sin(x)
g.diff(x)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 g = sy.exp(-x**2)*sy.sin(x)
      2 g.diff(x)

NameError: name 'sy' is not defined

Integration#

Mit der Methode integrate können wir die Stammfunktion einer Funktion berechnen lassen

F = f.integrate(x)
print("Stammfunktion:")
F
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[13], line 1
----> 1 F = f.integrate(x)
      2 print("Stammfunktion:")
      3 F

NameError: name 'f' is not defined

Manchmal ist aber auch SymPy überfordert. Bei zu komplizierten Funktionen gibt die integrate-Methode lediglich

G = g.integrate(x)
print("Stammfunktion:")
G
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[14], line 1
----> 1 G = g.integrate(x)
      2 print("Stammfunktion:")
      3 G

NameError: name 'g' is not defined

zurück. Wir können auch bestimmte Integrale berechnen, indem wir als zweiten Parameter ein Tupel bestehend aus der Integrationsvariablen und den beiden Grenzen angeben:

v = sy.integrate(f, (x,0,1))
print("Integral über f mit Grenzen 0 und 1:")
v
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 1
----> 1 v = sy.integrate(f, (x,0,1))
      2 print("Integral über f mit Grenzen 0 und 1:")
      3 v

NameError: name 'sy' is not defined

Um uneigentliche Integrale, beispielsweise \(\int_0^\infty g(x)\,\mathrm dx\) zu berechnen nutzt man als obere Grenze einfach sy.oo (unendlich):

g = x**2*sy.exp(-x)
v = sy.integrate(g,(x,0,sy.oo))
print("Uneigentliches Inegral:")
v
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[16], line 1
----> 1 g = x**2*sy.exp(-x)
      2 v = sy.integrate(g,(x,0,sy.oo))
      3 print("Uneigentliches Inegral:")

NameError: name 'x' is not defined

Potenzreihen#

SymPy bietet viele weitere nützliche Funktionen, beispielsweise sympy.series(...). Diese Funktion berechnet eine Potenzreihe beliebiger Ordnung für eine gegebene Funktion:

G = g.series(x, 0, 4) # Taylorapproximation im Entwicklungsunkt 0, bis Ordnung 4
print("Potenzreihe:")
G
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[17], line 1
----> 1 G = g.series(x, 0, 4) # Taylorapproximation im Entwicklungsunkt 0, bis Ordnung 4
      2 print("Potenzreihe:")
      3 G

NameError: name 'g' is not defined
G = G.removeO()
print("Potenzreihe ohne Restglied:")
G
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[18], line 1
----> 1 G = G.removeO()
      2 print("Potenzreihe ohne Restglied:")
      3 G

NameError: name 'G' is not defined

Plotten#

SymPy stellt auch ein direktes Interface zu MatplotLib bereit. Der Unterschied zu den in visualisation gelernten Funktionen ist, dass wir hier nicht erst ein Gitter für die x-Werte anlegen müssen. Dies geschieht in den plot-Funktionen von SymPy automatisch. Wir müssen lediglich die SymPy-Expression und gegebenenfalls zusätzliche Funktionsparameter der sympy.plot-Funktion übergeben:

p1 = sy.plot(g, (x,-1,1), show=False, legend=True, title="Potenzreihenapproximation")
p2 = sy.plot(G, (x,-1,1), show=False)

p1.append(p2[0])
p1.show()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[19], line 1
----> 1 p1 = sy.plot(g, (x,-1,1), show=False, legend=True, title="Potenzreihenapproximation")
      2 p2 = sy.plot(G, (x,-1,1), show=False)
      4 p1.append(p2[0])

NameError: name 'sy' is not defined

Übungsaufgabe

Berechne die Ableitung, die Stammfunktion, und erstelle einen Plot der Funktionen

\[ f_1(x) = \arctan(x),\quad f_2(x) = \sinh(x),\quad f_3(x) = x\,\ln(x). \]

Nichtlineare Gleichungen#

Mit SymPy können wir auch nichtlineare Gleichungen bzw. Gleichungssysteme lösen. Dies geschieht algebraisch, sofern möglich.

Die Funktionen, welche wir dafür benötigen, ist sympy.solve(...). Wir konstruieren ein einfaches Beispiel in dem wir die stationären Punkte der Funktion \(f(x)=x\,\ln(x)\) berechnen:

f = x/(x**2+1)
Df = f.diff()
p3 = sy.plot(f,(x,-4,4))  # Plot function

eq = sy.Eq(Df, 0)         # Create equation with lhs DF and rhs 0
sols = sy.solve(eq, x)    # Solve the equation
print("Extrema at", sols) # Print solution
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[20], line 1
----> 1 f = x/(x**2+1)
      2 Df = f.diff()
      3 p3 = sy.plot(f,(x,-4,4))  # Plot function

NameError: name 'x' is not defined

Die stationären Punkte werden in einer Liste abgelegt. Über diese können wir anschließend iterieren. So wie in folgender Probe:

for sol in sols:
    print("Stationärer Punkt x =", sol)
    print("Ist vom Typ", type(sol))
    print("Erfüllt f'(x) =", Df.subs(x, sol))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[21], line 1
----> 1 for sol in sols:
      2     print("Stationärer Punkt x =", sol)
      3     print("Ist vom Typ", type(sol))

NameError: name 'sols' is not defined

Auch Systeme von Gleichungen können mit diesem Befehl gelöst werden wie folgendes Beispiel zeigt:

x1, x2 = sy.symbols("x1 x2")

equations = [
    sy.Eq(2*x1 + 1*x2, 10),
    sy.Eq(1*x1 - 2*x2, 11) 
]

sols = sy.solve(equations)
sols
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[22], line 1
----> 1 x1, x2 = sy.symbols("x1 x2")
      3 equations = [
      4     sy.Eq(2*x1 + 1*x2, 10),
      5     sy.Eq(1*x1 - 2*x2, 11) 
      6 ]
      8 sols = sy.solve(equations)

NameError: name 'sy' is not defined

Die zu lösenden Gleichungen wurden hier einfach in eine Liste gepackt und dem solve-Befehl übergeben. Das Ergebnis ist hier ein Objekt vom Typ

type(sols)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[23], line 1
----> 1 type(sols)

NameError: name 'sols' is not defined

Dictionary ist auch eine Containerklasse, welche Key-Value-Paare abspeichert. Der Key (Schlüssel) ist hier der Name der Variable und der Value deren Wert. Wir können auch mit einer for-Schleife über ein Dictionary iterieren. Die lokale Variable a in folgendem Beispiel ist dabei lediglich der Schlüssel. Den zugehörigen Wert bekommt man mit sols[a]:

print("Die Lösung unseres Gleichungssystems lautet")
for a in sols:
    print(a, "=", sols[a])
Die Lösung unseres Gleichungssystems lautet
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[24], line 2
      1 print("Die Lösung unseres Gleichungssystems lautet")
----> 2 for a in sols:
      3     print(a, "=", sols[a])

NameError: name 'sols' is not defined

Alternativ lassen sich lineare Gleichungssysteme auch mit den Befehl sympy.linsolve(...) lösen:

x1, x2, x3 = sy.symbols('x1, x2, x3')
sols = sy.linsolve([2*x1+3*x2-6*x3 - 3, 1*x1-3*x2+3*x3 - 2], (x1,x2,x3))
sols
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[25], line 1
----> 1 x1, x2, x3 = sy.symbols('x1, x2, x3')
      2 sols = sy.linsolve([2*x1+3*x2-6*x3 - 3, 1*x1-3*x2+3*x3 - 2], (x1,x2,x3))
      3 sols

NameError: name 'sy' is not defined

Für dieses System aus 2 Gleichungen und 3 Unbekannten hat der linsolve-Befehl eine Lösung ermittelt, die noch von \(x_3\) abhängt. Natürlich erwarten wir in diesem Beispiel unendlich viele Lösungen. Mit der subs-Methode können wir die freie Variable mit einem speziellen Wert substituieren:

for sol in sols:
    print("Allgemeine Lösung :", sol)
    print("Spezielle Lösung  :", sol.subs('x3', 0))
    print("Spezielle Lösung  :", sol.subs('x3', 2))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[26], line 1
----> 1 for sol in sols:
      2     print("Allgemeine Lösung :", sol)
      3     print("Spezielle Lösung  :", sol.subs('x3', 0))

NameError: name 'sols' is not defined

Gewöhnliche Differentialgleichungen#

Auch einfache Differentialgleichungen können mit SymPy gelöst werden. Zunächst müssen wir dafür eine Differentialgleichung definieren. Dazu legen wir zunächst ein Objekt vom Typ Function an und definieren die Differentialgleichung als mathematischen Ausdruck. Die Funktion Derivative erlaubt es die Ableitungen der gesuchten Funktion einzuarbeiten:

y = sy.Function('y')
ode = sy.Derivative(y(x),x,x) + 2*sy.Derivative(y(x),x) - 6*y(x) - sy.exp(3*x)
ode
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[27], line 1
----> 1 y = sy.Function('y')
      2 ode = sy.Derivative(y(x),x,x) + 2*sy.Derivative(y(x),x) - 6*y(x) - sy.exp(3*x)
      3 ode

NameError: name 'sy' is not defined

Die Funktion zum Lösen der Differentialgleichung ist sympy.dsolve (das “d” steht für differential equation). Wir übergeben den eben für die ODE definierten Ausdruck sowie die Funktion nach der wir diese lösen sollen:

sol = sy.dsolve(ode, y(x))
sol
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[28], line 1
----> 1 sol = sy.dsolve(ode, y(x))
      2 sol

NameError: name 'sy' is not defined
type(sol)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[29], line 1
----> 1 type(sol)

NameError: name 'sol' is not defined

Das Ergebnis ist ein Objekt vom Tyo Equality, bestehend aus einer linken und einer rechten Seite einer Gleichung. Um die rechte Seite zu extrahieren und daraus eine Funktion zu definieren nutzen wir

f = sol.rhs
type(f)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[30], line 1
----> 1 f = sol.rhs
      2 type(f)

NameError: name 'sol' is not defined

Unsere eben berechnete Funktion hängt von folgenden Variablen ab:

f.free_symbols
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[31], line 1
----> 1 f.free_symbols

NameError: name 'f' is not defined

Dass unsere Lösung von 2 beliebigen Konstanten C1 und C2 abhängt ist nicht verwunderlich, da wir keine Anfangsbedingungen für unsere Differentialgleichung zweiter Art definiert haben. Wir können nun auch feste Werte für die Konstanten wählen. Dazu definieren wir ein Dictionary und nutzen die Funktion subs (Substituiere):

constants = {'C1': 1, 'C2': 3} # Dictionary anlegen
my_f = f.subs(constants)       # Konstanten durch Werte ersetzen
my_f
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[32], line 2
      1 constants = {'C1': 1, 'C2': 3} # Dictionary anlegen
----> 2 my_f = f.subs(constants)       # Konstanten durch Werte ersetzen
      3 my_f

NameError: name 'f' is not defined

Unsere Funktion my_f hängt nun nur noch von x ab und wir können wie gewohnt weiter arbeiten:

sy.plot(my_f, (x,0,2))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[33], line 1
----> 1 sy.plot(my_f, (x,0,2))

NameError: name 'sy' is not defined

Schauen wir uns nun noch an, wie wir Anfangswertprobleme, also Differentialgleichungen zusammen mit entsprechenden Anfangsbedingungen, lösen können. Ruft man mit sp.dsolve? den Hilfetext zur Funktion dsolve auf, stößt man schnell auf den Parameter ics. Dieser soll ein Dictionary sein, welches alle Anfangs- und Randbedingungen beinhaltet. Dafür soll folgende Syntax genutzt werden:

initial_conditions = {y(0)                    : 0, # y(0) = 0
                      y(x).diff(x).subs(x, 0) : 1} # y'(0)= 1
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[34], line 1
----> 1 initial_conditions = {y(0)                    : 0, # y(0) = 0
      2                       y(x).diff(x).subs(x, 0) : 1} # y'(0)= 1

NameError: name 'y' is not defined

Wir haben hier die Anfangsbedingungen \(y(0)=0\) und \(y'(0)=1\) vorgegeben. Der Aufruf von dsolve ergibt nun:

sol = sy.dsolve(ode, y(x), ics=initial_conditions)
f = sol.rhs
f
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[35], line 1
----> 1 sol = sy.dsolve(ode, y(x), ics=initial_conditions)
      2 f = sol.rhs
      3 f

NameError: name 'sy' is not defined

Offensichtlich hängt die Lösung nur noch von der Variablen \(x\) ab. Klar, da die Konstanten in der allgemeinen Lösung durch die Anfangsbedingungen eliminiert wurden.

f aus oberem Block ist wieder ein mathematischer Ausdruck, mit dem wir wie gewohnt weiter arbeiten können:

print("f ist vom Typ", type(f))
sy.plot(f, (x,0,1))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[36], line 1
----> 1 print("f ist vom Typ", type(f))
      2 sy.plot(f, (x,0,1))

NameError: name 'f' is not defined

In folgendem Beispiel wird die Riccati-Differentialgleichung

\[ y'(x) + y(x)^2+\frac{2}{x}\,y(x) = -\frac{2}{x^2} \]

gelöst. Interessant ist auch die Ausgabe der Funktion sympy.classify_ode(...). SymPy analysiert die Differentialgleichung und wendet, abhängig von der Klassifikation der Gleichng, eine entsprechende Lösungsstrategie an:

ode = sy.Derivative(y(x),x) + y(x)**2 + 2/x*y(x) + 2/(x**2)
sy.classify_ode(ode)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[37], line 1
----> 1 ode = sy.Derivative(y(x),x) + y(x)**2 + 2/x*y(x) + 2/(x**2)
      2 sy.classify_ode(ode)

NameError: name 'sy' is not defined
sol = sy.dsolve(ode, y(x))
sol
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[38], line 1
----> 1 sol = sy.dsolve(ode, y(x))
      2 sol

NameError: name 'sy' is not defined
f = sol.rhs
my_f = f.subs('C1', 1)
my_f
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[39], line 1
----> 1 f = sol.rhs
      2 my_f = f.subs('C1', 1)
      3 my_f

NameError: name 'sol' is not defined