4.1. Simbolički izrazi¶
Za manipulacije simboličkim izrazima u Pythonu, na raspolaganju nam
je paket sympy
>>> from sympy import *
Kao prvo, potrebno je na slijedeći način deklarirati varijable koje namjeravamo koristiti u simboličkim izrazima
>>> a, b, c, x, y, z, t = symbols('a, b, c, x, y, z, t')
>>> alpha, beta = symbols('alpha, beta')
(Simboli s lijeve i desne strane ovih deklaracija se ne moraju nužno slagati, što može biti korisno za npr. skraćeni unos grčkih slova, ali općenito je to recept za probleme.)
Pomoću ovih varijabli sad izgrađujemo simboličke izraze:
>>> i1 = (beta+alpha)**3; i1
(alpha + beta)**3
Ukoliko želimo ljepši ispis možemo ga uključiti s
>>> init_printing()
>>> i1
3
(alpha + beta)
(U Jupyter notebooku bi ovo bilo prikazano još ljepše, s pravim grčkim slovima.) U ostatku ovog dokumenta nećemo koristiti ovu mogućnost.
Sympy ne provodi skoro nikakve operacije na izrazima dok to eksplicitno ne
zatražimo. Recimo da želimo razviti gornji izraz koristeći
binomni teorem. Za to služi funkcija expand()
>>> expand(i1)
alpha**3 + 3*alpha**2*beta + 3*alpha*beta**2 + beta**3
Funkcija expand()
, kao i mnoge druge, se može alternativno upotrijebiti
i kao metoda simboličkog izraza.
>>> i1.expand()
alpha**3 + 3*alpha**2*beta + 3*alpha*beta**2 + beta**3
U prvom pristupu expand
doživljavamo kao funkciju ili operaciju, dok je
izraz i1
njen argument odnosno operand. To je način razmišljanja
svojstven standardnom proceduralnom ili pak
tzv. funkcionalnom programiranju.
U drugom pristupu izraz i1
treba pak doživljavati kao objekt , u smislu tzv.
objektno-orijentiranog (OO) programiranja, a expand()
je tzv. metoda
što je naziv za funkciju koja je pridružena tipu objekta na koji djeluje [1].
Da bi saznali što pojedina metoda radi, upišemo je nakon odgovarajućeg
objekta i operatora točkice .
, dodamo upitnik i stisnemo TAB.
Pri upotrebi metode ne smije se zaboraviti na zagrade,
koje su često prazne, ali nekad sadrže opcionalne argumente kojima modificiramo
ponašanje metode. Ukoliko zaboravimo zagrade Python ne poziva funkciju već samo
ispisuje njeno ime poput "<metoda expand pridružena objektu tom i tom>"
:
>>> i1.expand
<bound method Expr.expand of (alpha + beta)**3>
Tek zagrade daju zahtjev interpreteru da dotičnu metodu i pozove tj. izvrši.
Naravno ovakve jednostavne izraze možemo razviti i na ruke, dok računalo blista kad radi s velikim izrazima (sve dok stanu u memoriju računala)
>>> i2 = (a +2*b + 3*c)**3 * (x+y)**3
>>> i2.expand()
a**3*x**3 + 3*a**3*x**2*y + 3*a**3*x*y**2 + a**3*y**3 + 6*a**2*b*x**3 + 18*a**2*b*x**2*y + 18*a**2*b*x*y**2 + 6*a**2*b*y**3 + 9*a**2*c*x**3 + 27*a**2*c*x**2*y + 27*a**2*c*x*y**2 + 9*a**2*c*y**3 + 12*a*b**2*x**3 + 36*a*b**2*x**2*y + 36*a*b**2*x*y**2 + 12*a*b**2*y**3 + 36*a*b*c*x**3 + 108*a*b*c*x**2*y + 108*a*b*c*x*y**2 + 36*a*b*c*y**3 + 27*a*c**2*x**3 + 81*a*c**2*x**2*y + 81*a*c**2*x*y**2 + 27*a*c**2*y**3 + 8*b**3*x**3 + 24*b**3*x**2*y + 24*b**3*x*y**2 + 8*b**3*y**3 + 36*b**2*c*x**3 + 108*b**2*c*x**2*y + 108*b**2*c*x*y**2 + 36*b**2*c*y**3 + 54*b*c**2*x**3 + 162*b*c**2*x**2*y + 162*b*c**2*x*y**2 + 54*b*c**2*y**3 + 27*c**3*x**3 + 81*c**3*x**2*y + 81*c**3*x*y**2 + 27*c**3*y**3
Potenciranjem i razvijanjem gornjeg izraza dobivamo izraz od 550 članova ...
>>> i3 = expand(i2**3)
>>> len(i3.args)
550
... kojeg sympy s lakoćom faktorizira:
>>> factor(i3)
(x + y)**9*(a + 2*b + 3*c)**9
Napomena
Svaki simbolički izraz ima strukturu funkcija(arg1, arg2, ...)
gdje
argumenti mogu opet biti druge funkcije sa svojim argumentima, tvoreći
tako drvoliku strukturu. Funkcija i argumenti nekog izraza pohranjeni
su u atributima func
i args
, što smo gore iskoristili za brojanje
članova izraza i3
gdje je funkcija naprosto operacija zbrajanja
>>> i3.func
<class 'sympy.core.add.Add'>
Cijelu strukturu izraza možemo ispisati pomoću funkcije srepr
.
Vještom upotrebom
func
i args
možemo proizvoljno manipulirati simboličkim
izrazima.
Često je korisno izraz organizirati kao polinom u nekoj varijabli. Za to služi
funkcija collect()
:
>>> i4=i2.expand().collect(y)
>>> i4
a**3*x**3 + 6*a**2*b*x**3 + 9*a**2*c*x**3 + 12*a*b**2*x**3 + 36*a*b*c*x**3 + 27*a*c**2*x**3 + 8*b**3*x**3 + 36*b**2*c*x**3 + 54*b*c**2*x**3 + 27*c**3*x**3 + y**3*(a**3 + 6*a**2*b + 9*a**2*c + 12*a*b**2 + 36*a*b*c + 27*a*c**2 + 8*b**3 + 36*b**2*c + 54*b*c**2 + 27*c**3) + y**2*(3*a**3*x + 18*a**2*b*x + 27*a**2*c*x + 36*a*b**2*x + 108*a*b*c*x + 81*a*c**2*x + 24*b**3*x + 108*b**2*c*x + 162*b*c**2*x + 81*c**3*x) + y*(3*a**3*x**2 + 18*a**2*b*x**2 + 27*a**2*c*x**2 + 36*a*b**2*x**2 + 108*a*b*c*x**2 + 81*a*c**2*x**2 + 24*b**3*x**2 + 108*b**2*c*x**2 + 162*b*c**2*x**2 + 81*c**3*x**2)
>>> len(i4.args)
13
(Uočite da collect()
ne pojednostavljuje koeficijente [2].)
Za dobiti koeficijent uz neku potenciju neke varijable koristi se
funkcija coeff()
. Npr, koeficijent uz \(a^9\) jest
>>> i3.coeff(a, 9)
x**9 + 9*x**8*y + 36*x**7*y**2 + 84*x**6*y**3 + 126*x**5*y**4 + 126*x**4*y**5 + 84*x**3*y**6 + 36*x**2*y**7 + 9*x*y**8 + y**9
Najsveobuhvatnija funkcija za pojednostavljivanje simboličkih izraza
je simplify()
:
>>> i5 = a/(1-a) + a/(1+a); i5
a/(a + 1) + a/(-a + 1)
>>> i5.simplify()
-2*a/(a**2 - 1)
Funkcija simplify()
je kompozicija elementarnijih funkcija za
pojednostavljivanje izraza. Jedna od tih elementarnijih funkcija je
trigsimp()
koja pri pojednostavljivanju rabi samo trigonometrijske
identitete.
>>> (sin(x)**4 + 2*sin(x)**2*cos(x)**2 + cos(x)**4).trigsimp()
1
Još neke funkcije za pojednostavljivanje simboličkih izraza
su expand_trig
, powsimp
, expand_log
, logcombine
.
Uočite da sympy neće naivno “pojednostaviti” izraze koji
uključuju multifunkcije,
kao na slijedećem primjeru, u
kojem upoznajemo i važnu metodu subs()
koja služi za uvrštavanje vrijednosti
varijabli i druge supstitucije u izrazima
>>> i6 = log(a) + log(b)
>>> i6.simplify()
log(a) + log(b)
>>> i6.subs({a:-1, b:-1})
2*I*pi
>>> i7 = log(a*b)
>>> i7.subs([(a,-1),(b,-1)])
0
(Vidimo da argument od subs
može biti rječnik, ali i lista koja definira
zamjene.)
Inače, simboličke varijable se mogu definirati i s dodatnim svojstvima koja
onda mogu omogućiti željena pojednostavljenja izraza:
>>> m, n = symbols('m, n', positive=True)
>>> (log(m) + log(n)).simplify()
log(m*n)
Napomena
Za bolju kontrolu sympy koristi svoje vlastite klase brojeva, a ne one Pythonove. To omogućuje npr. upotrebu racionalnih brojeva bez gubitka točnosti i njihov prirodni ispis:
>>> i1 = (x/7).subs(x, 3); i1
3/7
>>> srepr(i1)
'Rational(3, 7)'
>>> srepr(7*i1)
'Integer(3)'
Da bi očuvali ta svojstva trebamo pripaziti da ne unosimo u izraze Pythonove brojeve s pomičnom točkom (float). Problem obično nastaje kad unesemo omjer dva cijela broja koja Python pretvori u float prije nego sympy napravi konverziju u svoje tipove.
>>> 2**(1/2)
1.4142135623730951
Da bi to spriječili dovoljno je napraviti eksplicitnu konverziju
(“sympyfikaciju”) jednog od brojeva funkcijom S
>>> 2**(S(1)/2)
sqrt(2)
Napomena
Slično, sympy ima svoj skup simboličkih matematičkih konstanti:
bazu prirodnog logaritma E
, Ludolfov broj pi
(sic!), imaginarnu
jedinicu I
, beskonačnost oo
itd.
Usporedimo ponašanje broja \(\pi\) iz scipy i
simpy paketa:
>>> import scipy
>>> scipy.exp(1j*scipy.pi)
(-1+1.2246467991473532e-16j)
>>> exp(I*pi)
-1
>>> pi.is_irrational
True
Vidimo da je čuvena Eulerova relacija sa scipy konstantama zadovoljena
samo na konačnu točnost standardnih brojeva s pomičnom točkom.
(Ovdje je bilo nužno ponovno učitati scipy paket i to na način da
ne dođe do kolizije pi
i exp
sa istoimenim objektima iz
sympy paketa.)
Napomena
Ako na kraju poželimo vidjeti rezultat u obliku broja s pomičnom točkom
koristimo funkciju N
:
>>> N(sqrt(2))
1.41421356237310
>>> N(pi, n=50)
3.1415926535897932384626433832795028841971693993751
Alternativno ime ove funkcije je evalf
i u tom obliku se ona obično
koristi kao metoda
>>> E.evalf()
2.71828182845905
Zadatak 1
Uzmite izraz \((a+b)((c+y x) x + t x^2)\) i algebarskim manipulacijama postignite prikaz u slijedećim ekvivalentnim oblicima
- \(x (a + b) (c + t x + x y)\)
- \(a c x + a t x^2 + a x^2 y + b c x + b t x^2 + b x^2 y\)
Zadatak 2
Koristeći algebarske manipulacije pokažite da vrijedi
Pazite na sintaksu: \(\sin^3 x\) se unosi kao sin(x)**3!
Zadatak 3
Za izraz \(f(x)\) kažemo da je paran, odnosno neparan u varijabli
\(x\) ako vrijedi \(f(x) = f(-x)\), odnosno \(f(x) = - f(-x)\).
Isprogramirajte funkciju parnost(izraz, varijabla)
koja će testirati
to svojstvo i vraćati 1 ako je izraz paran u varijabli, -1 ako je
neparan i 0 ako nije ni paran ni neparan.
Zadatak 4
Isprogramirajte funkciju leg(n, x)
koja vraća Legendreov polinom
\(P_n(x)\) u simboličkoj varijabli x, koristeći rekurzijsku relaciju
Funkcija treba davati identične rezultate kao sympy funkcija legendre(n, x).
Bilješke
[1] | To onda omogućuje da metode istog imena rade različite stvari s različitim objektima (tzv. polimorfizam ). Kako je python OO jezik, takva sintaksa se obilato koristi i brojne funkcije se ni ne mogu koristiti na prvi način. Jedna od prednosti takvog pristupa je da elegantno možemo saznati popis svih funkcija koje rade nešto smisleno sa zadanim objektom, i to tako da nakon što stavimo točkicu ”.” poslije objekta stisnemo TAB tipku. Dobit ćemo popis svih metoda tog objekta. Ovo međutim ne funkcionira s netom upisanim izrazom u trenutnoj ćeliji, već samo s ranije definiranim izrazima (objektima) kojima smo pridjelili ime. Pridjeljivanje imena objektima se izvodi znakom jednakosti i korisno je ne samo zbog navedenog razloga već i inače radi lakšeg baratanja izrazima i kasnijeg referiranja na iste. |
[2] | To se može postići npr. ovako: |
>>> sum(i4.coeff(y, a).factor()*y**a for a in range(4))
x**3*(a + 2*b + 3*c)**3 + 3*x**2*y*(a + 2*b + 3*c)**3 + 3*x*y**2*(a + 2*b + 3*c)**3 + y**3*(a + 2*b + 3*c)**3