5.3. Ligninger#

Første trin for at løse ligninger er at få skrevet ligninger op i et sprog, Python kan forstå. Det afgørende er her at indse at et lighedstegn kan have flere fundamentalt forskellige betydninger. Tidligere har vi tildelt variable bestemte værdier ved f.eks. at skrive k = 4, mens vi her vil bruge lighedstegnet til at beskrive et udsagn om sammenhængen mellem to udtryk. I SymPy-sprog er dette en equality og syntaksen er (når vi har importeret SymPy som sp som vi plejer) givet ved sp.Eq(venstre side, højre side). Pythagoras’ læresætning kan f.eks. opskrives som:

import sympy as sp
from sympy.abc import a, b, c             # Vi definerer a, b og c som symbolske variable

Pytha = sp.Eq(a ** 2 + b ** 2, c ** 2)    # Syntaks: sp.Eq(venstre side, højre side)
display(Pytha)
\[\displaystyle a^{2} + b^{2} = c^{2}\]

Man kan ligeledes danne ligninger ved at sammensætte allerede definerede udtryk, her eksemplificeret ved cosinusrelationen:

from sympy.abc import theta

expr = a**2 + b**2 - 2*a*b*sp.cos(theta) # Vi laver nu blot den ene side af ligningen som et udtryk (expr for "expression")
display(expr)

cos_relation = sp.Eq(c**2, expr)         # Og vi kan nu sammensætte højre- og venstresiderne
display(cos_relation)
\[\displaystyle a^{2} - 2 a b \cos{\left(\theta \right)} + b^{2}\]
\[\displaystyle c^{2} = a^{2} - 2 a b \cos{\left(\theta \right)} + b^{2}\]

Vi kunne selvfølgelig også have gjort det hele i ét hug:

cos_relation = sp.Eq(c**2, a**2 + b**2 - 2*a*b*sp.cos(theta))
display(cos_relation)
\[\displaystyle c^{2} = a^{2} - 2 a b \cos{\left(\theta \right)} + b^{2}\]

Nu når vi har lighederne på plads, er det blevet tid til at lade SymPy regne for os. SymPy giver os to forskellige værktøjer til at løse ligninger, og det er lidt forskelligt, hvad de hver især er gode til.

5.3.1. Solveset#

Den første metode hedder solveset(), som kan oversættes til “løsningsmængde”. Dette er den relativ ny metode, og SymPy-teamet arbejder på at denne skal være den primære løsningsmetode i fremtiden. Den løser dog ikke alle opgaver godt endnu, hvorfor vi nedenfor vil præsentere et alternativ.

Lad os prøve at løse en af overstående ligninger. Lad os tage helt standard Pythagoras \(a^2 + b^2 = c^2\) (ligningen defineret som Pytha ovenfor). Hvis vi nu kender \(c=5\) og \(a=3\) og ønsker at finde \(b\), indsætter vi først værdierne i ligningen:

display(Pytha)
Pytha_indsat = Pytha.subs([(a, 3), (c, 5)])
display(Pytha_indsat)
\[\displaystyle a^{2} + b^{2} = c^{2}\]
\[\displaystyle b^{2} + 9 = 25\]

Vi benytter nu solveset() til at løse denne. For en ligning virker denne funktion ved, at man angiver ligningen og den variabel, som man ønsker at løse for:

solution = sp.solveset(Pytha_indsat, b)
display(solution)
\[\displaystyle \left\{-4, 4\right\}\]

Vi har altså nu fundet løsningerne til ligningen. I tilfældet med Pythagoras’ sætning leder vi efter en sidelængde, så vores løsning skal være et positivt tal. Vi kan indskrænke løsningsdomænet ved at give solveset et “domain”-keyword. Dette skal være et såkaldt SymPy-set, som er en lidt indviklet størrelse, men i langt de fleste tilfælde kan man slippe afsted med at bede om reelle tal ved at skrive sp.Reals eller ved at give et interval med sp.Interval(fra, til), hvor man kan bruge oo for uendelig (forudsat at oo er blevet importeret). For mere avancerede løsningsdomæner henvises til dokumentationen her. I dette tilfælde angiver vi et interval:

from sympy import oo           # Vi importerer uendelig
pos_solution = sp.solveset(Pytha_indsat, b, sp.Interval(0, oo))
display(pos_solution)
\[\displaystyle \left\{4\right\}\]

solveset virker også hvis vi istedet for en ligning angiver et udtryk (altså uden lighedstegn). Da sætter SymPy udtrykket lig med 0 og løser den derved fremkomne ligning (i dette eksempel \(b^2-16 = 0\)):

pos_solution = sp.solveset(b**2 - 16, b, sp.Interval(0, oo))
display(pos_solution)
\[\displaystyle \left\{4\right\}\]

Vi vil bruge den samme syntaks her, hvor vi vil finde rødder i et fjerdegrads polynomium.

from sympy.abc import x
expr = x ** 4 - 1
solutions = sp.solveset(expr, x) # Da vi har angivet et udtryk, sætter SymPy udtrykket lig med 0 og løser.
display(solutions)
\[\displaystyle \left\{-1, 1, - i, i\right\}\]

Er vi kun interesserede i reelle løsninger, kan vi indskrænke domænet til de reelle tal:

solutions_real = sp.solveset(expr, x, sp.Reals)
display(solutions_real)
\[\displaystyle \left\{-1, 1\right\}\]

Solveset virker også, hvis vi ikke har en numerisk værdi for alle symboler, men ønsker den generelle løsning. Hvis vi f.eks. vil finde \(b\) i cosinusrelationen som defineret tidligere, kan vi skrive følgende:

display(cos_relation)
sol_b = sp.solveset(cos_relation, b)
display(sol_b)
\[\displaystyle c^{2} = a^{2} - 2 a b \cos{\left(\theta \right)} + b^{2}\]
\[\displaystyle \left\{a \cos{\left(\theta \right)} - \sqrt{a^{2} \cos^{2}{\left(\theta \right)} - a^{2} + c^{2}}, a \cos{\left(\theta \right)} + \sqrt{a^{2} \cos^{2}{\left(\theta \right)} - a^{2} + c^{2}}\right\}\]

Solveset() giver os løsninger skrevet op i mængdenotation for at give os de generelle fuldstændige løsninger. Selvom dette kan være meget fint i nogle tilfælde, ender vi dog i andre tilfælde med en generel og ganske ubrugelig løsning. Dette sker eksempelvis, hvis vi prøver at løse for \(\theta\) direkte:

sp.solveset(cos_relation, theta)
\[\displaystyle \left\{\theta \mid \theta \in \mathbb{C} \wedge - a^{2} + 2 a b \cos{\left(\theta \right)} - b^{2} + c^{2} = 0 \right\}\]

Hvilket kan læses som “De værdier \(\theta\) for hvilket det gælder at \(\theta\) er et komplekst tal og at cosinusrelationen er opfyldt”. Det er jo rigtig nok (sammenlign med selve cosinusrelationen!), men fortæller os ikke så meget, vi ikke vidste i forvejen. Det leder os videre til den anden metode til ligningsløsning:

5.3.2. Solve#

Solve() er den ældre funktion, som på trods af at være mindre generel oftest giver os en brugbar løsning. Syntaksen for input til Solve() er den samme som for Solveset()

sp.solve(cos_relation, theta)
[-acos((a**2 + b**2 - c**2)/(2*a*b)) + 2*pi,
 acos((a**2 + b**2 - c**2)/(2*a*b))]

Når der som her er flere løsninger, giver Solve()løsningerne som elementer i en liste, som display ikke kan konvertere til LaTeX. Vi må derfor nøjes med output i tekstformat eller vi kan bruge en løkke til at vise løsningerne en af gangen med display:

sols = sp.solve(cos_relation, theta)
display(sols)           # Viser listen med løsninger som tekst ... OK, men ikke så kønt
for løsning in sols:    # Løkke, der viser løsningerne en af gangen med LaTeX-formattering
    display(løsning)
[-acos((a**2 + b**2 - c**2)/(2*a*b)) + 2*pi,
 acos((a**2 + b**2 - c**2)/(2*a*b))]
\[\displaystyle - \operatorname{acos}{\left(\frac{a^{2} + b^{2} - c^{2}}{2 a b} \right)} + 2 \pi\]
\[\displaystyle \operatorname{acos}{\left(\frac{a^{2} + b^{2} - c^{2}}{2 a b} \right)}\]

Når man bruger solve kan man angive en liste af ligheder (eller uligheder), som afgrænser den variable. Hvis vi vil bestemme sidelængen \(b\) (som er nul eller positiv) ved hjælp af Pythagoras’ sætning, kan opgaven i solve-sprog lyde:

display(Pytha_indsat)
sol_b = sp.solve([Pytha_indsat, b >= 0], b)
display(sol_b)
\[\displaystyle b^{2} + 9 = 25\]
\[\displaystyle b = 4\]

Overordnet set er solve rigtig god til at give én løsning. Det kan altså ofte bruges i sammenhænge, hvor man vil tjekke et resultat, eller hvis man blot skal bruge en vilkårlig løsning og ikke den fuldstændige løsning. Eksempel: Vi løser \(\sin(\theta) = 1\):

sp.solve(sp.Eq(sp.sin(theta), 1), theta)
[pi/2]

Her får vi altså en løsning, og det vil ofte være den løsning, vi leder efter. Men da \(\sin(x)\) er periodisk, ved vi, at der er flere løsninger. Så svaret er ikke udtømmende.

I modsætning hertil har solveset den fordel, at den giver et matematisk stringent svar, og den vil altså returnere hele løsningen:

sp.solveset(sp.Eq(sp.sin(theta), 1), theta)
\[\displaystyle \left\{2 n \pi + \frac{\pi}{2}\; |\; n \in \mathbb{Z}\right\}\]

5.3.3. Numerisk løsning#

Nogle gange kan vi komme ud for en situation, hvor en opgave ikke har en brugbar eksakt, symbolsk løsning, eller at hverken solve eller solveset giver et svar, vi kan bruge. Vi kan så benytte sp.nsolve til numerisk løsning af ligninger. Vi bruger således SymPy (som er designet til at være et symbolsk værktøj) til et formål, der er på kanten af dets anvendelsesområde, og vi skal derfor bruge værktøjet med forsigtighed. Det er derfor en god idé i disse tilfælde at tegne grafer for at illustrere opgaven. Derved kan vi checke at svaret rent faktisk giver mening i forhold til opgaven, og det tillader os også at give et ret godt gæt på en løsning.

Kaldesekvensen for at bestemme en numeriske løsning er:

sp.nsolve(ligning, variabel, startgæt)

Hvis nu vi eksempelvis vil finde en løsning til \(x\log(x+1) = \sin(x)\), starter vi med at lave et plot (se notesbogen om plotning for en mere grundig forklaring af syntaksen).

from sympy.abc import x
from sympy.plotting import plot

figur = plot(x * sp.log(x + 1), sp.sin(x), (x, 0, 2))
../../_images/Notebook3_ligninger_36_0.png

Heraf ser vi altså, at et godt startgæt vil være omkring \(x=1.2\). Dette vil vi nu bruge.

lign = sp.Eq(x * sp.log(x + 1), sp.sin(x))
display(lign)
sp.nsolve(lign, x, 1.2)
\[\displaystyle x \log{\left(x + 1 \right)} = \sin{\left(x \right)}\]
\[\displaystyle 1.1852999437201\]

Sympy leder med nsolve blot efter en løsning. Vi kan altså ende med at få forskellige svar alt efter, hvad vores startgæt er. Gætter vi \(x = 0.1\) får vi:

sp.nsolve(lign, x, 0.1)
\[\displaystyle 1.64437672593563 \cdot 10^{-51}\]

som er en numerisk værdi meget tæt på 0, der også er en løsning til ligningen. Det er derfor vigtigt at være opmærksom på, hvilken løsning, der er relevant i en konkret sammenhæng, og lave et godt startgæt inspireret af dette.