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

  1. \(x (a + b) (c + t x + x y)\)
  2. \(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

\[\frac{\sin^3 x + \cos^3 x}{\sin x + \cos x } = 1 - \sin x \cos x\]

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

\[P_n(x) = \frac{2n-1}{n} x P_{n-1}(x) - \frac{n-1}{n} P_{n}(x)\]

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