Objektno programiranje, simbolno računanje#

Objektno programiranje#

Pri programiranju poznamo različne pristope, dokumentacija Python-a (docs.python.org) omenja npr:

  1. proceduralni: seznam navodil, kaj je treba izvesti (npr.: C, Pascal)

  2. deklerativni: opišemo kaj želimo, programski jezik pa izvede (npr., SQL)

  3. funkcijski: programiranje temelji na funkcijah (npr.: Haskell )

  4. objektni: program temelji na objektih, ki imajo lastnosti, funkcije … (npr.: Java, Smalltalk)

Python je objektno orientiran programski jezik, vendar pa nas ne sili v uporabo objektov v vseh primerih. Kot bomo videli pozneje, ima objektno programiranje veliko prednosti, vendar pa je lahko mnogokrat okorno in bi po nepotrebnem naredilo program kompleksen. Iz tega razloga se eksplicitnemu objektnemu programiranju izognemo, če se le da.

Objektno programiranje v Pythonu temelji na razredih (class), objekti so pa instance (instance) razreda. Pogledali si bomo zgolj nekatere osnove objektnega programiranja (da boste lažje razumeli kodo drugih avtorjev in jo prirejali svojim potrebam).

Razred definiramo z ukazom class (dokumentacija):

class ImeRazreda:
    '''docstring'''
    [izraz 1]
    [izraz 2]
    .
    .

kjer ime razreda (torej ImeRazreda) po PEP8 pišemo z veliko začetnico. Če je ime sestavljeno iz več besed, vsako pišemo z veliko (t. i. principi CamelCase).

Poglejmo si primer:

class Pravokotnik:
    """Razred za objekt pravokotnik"""

    def __init__(self, širina=1, višina=1): # to je konstruktor objekta. Se izvede, ko kličemo Pravokotnik(sirina=1, visina=4)
        self.širina = širina 
        self.višina = višina # višina je atribut objekta
        
    def površina(self):
        return self.širina * self.višina
    
    def set_širina(self, širina=1):
        self.širina = širina

Preden gremo v podrobnosti razumevanja kode, naredimo instanco razreda (torej objekt):

moj_pravokotnik = Pravokotnik()

Funkcije definirane znotraj razreda poimenujemo metode, ko jih kličemo na objektih.

V zgornjem primeru metodo površina uporabimo tako:

moj_pravokotnik.površina()
1

Ime self je referenca na instanco razreda (objekt, ki bo ustvarjen). Imena znotraj razreda postanejo atributi objekta.

Primer atributa `višina˙:

moj_pravokotnik.višina
1

Od kje pride rezultat 1? Ko ustvarimo objekt, se najprej izvede inicializacijska funkcija __init__(), pri tem se kot argumenti funkcije __init__ uporabijo argumenti, ki jih posredujemo v razred.

Primer:

tvoj_pravokotnik = Pravokotnik(višina=5, širina=3)
tvoj_pravokotnik.površina()
15

Pripravili smo tudi metodo, ki spremeni atribut širina:

moj_pravokotnik.set_širina(širina=100)
moj_pravokotnik.površina()
100

Atribute lahko spreminjamo tudi neposredno, vendar se temu (zaradi možnosti napake in napačne uporabe) ponavadi izogibamo.

Primer:

moj_pravokotnik.višina = 3
moj_pravokotnik.površina()
300

Dedovanje#

Pomembna lastnost razredov je dedovanje; samo ime pove bistvo: tako kot ljudje dedujemo od svojih staršev, podobno velja tudi za razrede. Vsak razred (class) tako lahko deduje lastnosti kakega drugega razreda (dokumentacija). Lahko ima celo več staršev (v te podrobnosti tukaj ne bomo šli).

Sintaksa razreda, ki deduje, je:

class Otrok(Starš):
    [izraz]
    .
    .

Opomba: tudi, če razredu ne definiramo starša, deduje razred class.

Primer, ko novi razred Kvadrat podeduje obstoječega (Pravokotnik):

class Kvadrat(Pravokotnik):
    "Razred kvadrat"
    
    def __init__(self, širina=1):
        # kličimo iniciacijo razreda Pravokotnik
        super().__init__(širina=širina, višina=širina)
        
    def set_širina(self, širina):
        self.širina = širina
        self.višina = širina

Poglejmo sedaj uporabo:

moj_kvadrat = Kvadrat(širina=4)

Razred Kvadrat nima definicije metode za izračun površine, vendar pa jo je podedoval od razreda Pravokotnik in zato ima metodo za izračun površine:

moj_kvadrat.površina()
16

V kolikor spremenimo širino, se ustrezno spremeni površina:

moj_kvadrat.set_širina(5)
moj_kvadrat.površina()
25

Primer dedovanja razreda list (seznam)#

Najprej pripravimo seznam:

seznam = list([1,2,3])
seznam
[1, 2, 3]

Če želimo seznamu dodati vrednost, uporabimo metodo append (to je metoda, ki jo imajo objekti tipa list):

seznam.append(1)

Nato seznam prikažemo (najprej uvozimo matplotlib):

import matplotlib.pyplot as plt
plt.plot(seznam)
plt.show()
../_images/9aa465499a8dec5394f8bcb2ad09f5e16ce1f6fa116bdaba0436475fafe2ab57.png

Če nekaj takega izvajamo pogosto, potem je bolje, da si pripravimo svoj razred Seznam, ki deduje od list in dodamo metodo za prikaz nariši:

class Seznam(list):
        
    def nariši(self):
        plt.plot(self, 'r.', label='Dolgo besedilo')
        plt.legend()
        plt.ylim(-5, 5)
        plt.show()

Instanca objekta je:

moj_seznam = Seznam([1, 2, 3])
type(moj_seznam)
__main__.Seznam
moj_seznam
[1, 2, 3]

Čeprav nismo definirali metode append, jo je nov razred podedoval po razredu list:

moj_seznam.append(1)
moj_seznam
[1, 2, 3, 1]

Ima tudi metodo za izris:

moj_seznam.nariši()
../_images/8e21bd2eada211d0373dee2b9cce77daa7cddb1e25815ece589fba8d5c651dcd.png

Simbolno računanje s SymPy#

Termin simbolno računanje pomeni, da matematične izraze rešujemo strojno v obliki abstraktnih simbolov (in ne numerično). Strojno simbolno računanje nam pomaga kadar nas zanima rezultat v simbolni obliki in so izrazi preobsežni za klasično reševanje na list in papir. K strojnemu reševanju se zatečemo tudi zaradi zmanjšanja možnosti napake (pri obsežnih izračunih se ljudje lahko zmotimo).

Simbolno računanje nikakor ni nadomestek numeričnih metod!

Pogledali si bomo nekatere osnove, nekateri priporočeni dodatni viri pa so:

SymPy je eden od sistemov za strojno algebro (CAS - Computer Algebra Systems), ki pa ima poleg zmogljivosti tudi to prednost, da je v celoti napisan v Pythonu (alternativni paket v Pythonu Sage na primer ni v celoti napisan v Pythonu).

Nekatera namenska komercialna orodja:

Najprej uvozimo modul SymPy; tipično paket uvozimo kot sym:

import sympy as sym

Opazimo lahko, da se SymPy uvaža tudi from sympy import *. Temu se praviloma izogibamo, saj tako s SymPy imeni po nepotrebnem zapolnimo osnovni imenski prostor programa. V slednjem primeru do funkcij paketa (npr. sympy.Sum) dostopamo neposredno (npr. Sum), kar je lahko privlačno, vendar nas začne motiti, ko dodamo še druge pakete (npr. numpy), kar lahko poleg zmede privede do tega, da se funkcije z enakimi imeni „povozijo“.

Zato da dobimo lepo oblikovan LaTeX izpis, uporabimo:

sym.init_printing()

Definiranje spremenljivk in numerični izračun#

Spremenljivke definiramo takole:

x, y, k = sym.symbols('x, y, k')

Preverimo lahko tip:

type(x)
sympy.core.symbol.Symbol

Opazimo, da je spremenljivka x sedaj Symbol iz paketa sympy.

Sedaj lahko naredimo preprost izračun:

x**y + k
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[24], line 1
----> 1 x**y + k

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\IPython\core\displayhook.py:268, in DisplayHook.__call__(self, result)
    266 self.start_displayhook()
    267 self.write_output_prompt()
--> 268 format_dict, md_dict = self.compute_format_data(result)
    269 self.update_user_ns(result)
    270 self.fill_exec_result(result)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\IPython\core\displayhook.py:157, in DisplayHook.compute_format_data(self, result)
    127 def compute_format_data(self, result):
    128     """Compute format data of the object to be displayed.
    129 
    130     The format data is a generalization of the :func:`repr` of an object.
   (...)
    155 
    156     """
--> 157     return self.shell.display_formatter.format(result)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\IPython\core\formatters.py:182, in DisplayFormatter.format(self, obj, include, exclude)
    180 md = None
    181 try:
--> 182     data = formatter(obj)
    183 except:
    184     # FIXME: log the exception
    185     raise

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\decorator.py:232, in decorate.<locals>.fun(*args, **kw)
    230 if not kwsyntax:
    231     args, kw = fix(args, kw, sig)
--> 232 return caller(func, *(extras + args), **kw)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\IPython\core\formatters.py:226, in catch_format_error(method, self, *args, **kwargs)
    224 """show traceback on failed format call"""
    225 try:
--> 226     r = method(self, *args, **kwargs)
    227 except NotImplementedError:
    228     # don't warn on NotImplementedErrors
    229     return self._check_return(None, args[0])

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\IPython\core\formatters.py:347, in BaseFormatter.__call__(self, obj)
    345     method = get_real_method(obj, self.print_method)
    346     if method is not None:
--> 347         return method()
    348     return None
    349 else:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\sympy\interactive\printing.py:178, in _init_ipython_printing.<locals>._print_latex_png(o)
    176     s = '$\\displaystyle %s$' % s
    177 try:
--> 178     return _preview_wrapper(s)
    179 except RuntimeError as e:
    180     debug('preview failed with:', repr(e),
    181           ' Falling back to matplotlib backend')

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\sympy\interactive\printing.py:89, in _init_ipython_printing.<locals>._preview_wrapper(o)
     87 exprbuffer = BytesIO()
     88 try:
---> 89     preview(o, output='png', viewer='BytesIO', euler=euler,
     90             outputbuffer=exprbuffer, extra_preamble=extra_preamble,
     91             dvioptions=dvioptions, fontsize=fontsize)
     92 except Exception as e:
     93     # IPython swallows exceptions
     94     debug("png printing:", "_preview_wrapper exception raised:",
     95           repr(e))

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\sympy\printing\preview.py:369, in preview(expr, output, viewer, euler, packages, filename, outputbuffer, preamble, dvioptions, outputTexFile, extra_preamble, fontsize, **latex_settings)
    366 cmd.extend(commandend[cmd_variant])
    368 try:
--> 369     _check_output_no_window(cmd, cwd=workdir, stderr=STDOUT)
    370 except CalledProcessError as e:
    371     raise RuntimeError(
    372         "'%s' exited abnormally with the following output:\n%s" %
    373         (' '.join(cmd), e.output))

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\sympy\printing\preview.py:25, in _check_output_no_window(*args, **kwargs)
     23 else:
     24     creation_flag = 0 # Default value
---> 25 return check_output(*args, creationflags=creation_flag, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\subprocess.py:466, in check_output(timeout, *popenargs, **kwargs)
    463         empty = b''
    464     kwargs['input'] = empty
--> 466 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    467            **kwargs).stdout

File ~\AppData\Local\Programs\Python\Python312\Lib\subprocess.py:550, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    548 with Popen(*popenargs, **kwargs) as process:
    549     try:
--> 550         stdout, stderr = process.communicate(input, timeout=timeout)
    551     except TimeoutExpired as exc:
    552         process.kill()

File ~\AppData\Local\Programs\Python\Python312\Lib\subprocess.py:1196, in Popen.communicate(self, input, timeout)
   1194     self._stdin_write(input)
   1195 elif self.stdout:
-> 1196     stdout = self.stdout.read()
   1197     self.stdout.close()
   1198 elif self.stderr:

KeyboardInterrupt: 

Funkcijo lahko tudi poimenujemo; tukaj je primer, kjer uporabimo funkcijo sinus in konstanto \(\pi\):

f = sym.sin(1.2*sym.pi + x)**2
f
../_images/cfcc51e0412854e8546efdf4bb8cd925db42e52b8047decee6e23824ae68aa62.png

Bralec se morebiti sprašuje, zakaj potrebujemo novo funkcijo sympy.sin(), saj imamo vendar že tisto iz paketa numpy! Razlog je v tem, da simbolni izračun potrebuje popolnoma drugačno obravnavo kakor numerični in zato je koda zadaj povsem drugačna.

Če želimo zapisati enačbo, torej da enačimo en izraz z drugim, to naredimo takole:

enačba = sym.Eq(sym.sin(k*x),0.5)
enačba
../_images/777022286609fdca993fe6da9d1b72dda54856d502a4362bad39de429a43bbed.png

Nedefinirane matematične funkcije zapišemo kot (dokumentacija):

g = sym.Function('g')

Sedaj lahko, na primer, definiramo differencialno enačbo:

sym.Eq(g(x).diff(x), x)
../_images/46ab96b323b3800edfe8945835e11774775d2aecb673078d1da11f27c953b53a.png

Pri definiranju spremenljivk lahko dodajamo predpostavke:

x = sym.Symbol('x', positive=True)
x.is_positive
True

Predpostavke se potem upoštevajo pri izračunu. V splošnem vemo, da \(\sqrt{(x^2)}\ne x\), če pa je \(x\) pozitiven, pa velja \(\sqrt{(x^2)}= x\) in sympy glede na predpostavke izračuna pravilen rezultat:

sym.sqrt(x**2)
../_images/a62f1bf0f037de5a14e602497a42670c302da5db132010539cd91d2d25add5c6.png

Z metodo assumptions0 pogledamo predpostavke objekta:

x.assumptions0
{'positive': True,
 'extended_real': True,
 'hermitian': True,
 'extended_nonzero': True,
 'infinite': False,
 'nonzero': True,
 'imaginary': False,
 'commutative': True,
 'nonpositive': False,
 'zero': False,
 'complex': True,
 'negative': False,
 'extended_nonnegative': True,
 'finite': True,
 'extended_positive': True,
 'extended_negative': False,
 'nonnegative': True,
 'real': True,
 'extended_nonpositive': False}

SymPy pozna tipe števil (dokumentacija):

  • Real realna števila,

  • Rational racionalna števila,

  • Complex kompleksna števila,

  • Integer cela števila.

Ti tipi so pomembni, saj lahko vplivajo na način reševanja in na rešitev.

Racionalna števila#

Zgoraj smo že definirali realna števila. Poglejmo na primeru sedaj racionalna števila:

r1 = sym.Rational(4, 5)
r2 = sym.Rational(5, 4)
r1
../_images/2ca5a7b1da0abcb8a66ef64f5a9e3514537ecc465bab19847940ee9d78c1e2e3.png

Nekateri izračuni:

r1+r2
../_images/3e760233670809c9a0cf4c0a69ab2c3370ef48b0881970894778a077d95dcb6f.png
r1/r2
../_images/fd756d14a146a98aa8b0ae85e116f62254d37bd387ff9cbe78351c3370315ad8.png

Kompleksna števila#

Imaginarno število se zapiše z I (to je drugače kot pri numpy, kjer je imaginarni del definiran z j):

1+1*sym.I
../_images/7f73cfcbe4fc082bfddd0a93d02f113cfa9740ac742f3b14fc31c9deade4ad18.png
sym.I**2
../_images/d42ce757c38d93f08ca9ba5ba51bc1dbb529ac27fbdf9ae82e39d2572c183f67.png

Numerični izračun#

Pri simbolnem izračunu najprej analitične izraze rešimo, poenostavimo itd., nato pa pogosto želimo tudi izračunati konkreten rezultat.

Poglejmo primer:

x = sym.symbols('x')
f = sym.exp(x**x**x + sym.pi)
f
../_images/a2626477b6f1da44add829001c3a0c9b0cf48b39fef1f16124a2089287d944ae.png

Če želimo sedaj namesto \(x\) uporabiti vrednost, npr 0.5, to naredimo z metodo subs() (dokumentacija):

f.subs(x, 0.5)
../_images/1452e1e4d1e82556721f6d08e442268ffb64fb788cc0c270671aa8ee09383c96.png

Zgoraj smo uporabili konstanto \(\pi\) (dokumentacija); nekatere tipično uporabljene konstante so:

  • sympy.pi za število \(\pi\),

  • sympy.E za naravno število \(e\),

  • sympy.oo za neskončnost.

Kot smo videli zgoraj, subs() naredi zamenjavo in potem poenostavitve, ki so očitne; števila \(\pi\) ni izračunal v racionalni obliki. To moramo eksplicitno zahtevati z metodo:

kateri imata obe argument n (število števk).

Poglejmo primer:

f.subs(x, 2).evalf(n=80)
../_images/859af3bf52cefb83321b9b1092e7e8bf8151cc3bc8bc532e26d9fbe1815908ce.png

Podobno je z N:

sym.N(f.subs(x, 2), n=80)
../_images/859af3bf52cefb83321b9b1092e7e8bf8151cc3bc8bc532e26d9fbe1815908ce.png

Mimogrede smo pokazali, da pod pogojem, da v izrazu nimamo števil s plavajočo vejico, lahko rezultat prikažemo poljubno natančno (dokumentacija).

V subs funkciji lahko uporabimo tudi slovar. Primer:

x, y = sym.symbols('x, y')
parametri = {x: 4, y: 10}
(x + y).subs(parametri)
../_images/9166a76e4c7188a60c99c6c89cc0834bef1c30309bc070a9db6f09d3cf3f7823.png

ali seznam terk:

(x + y).subs([(x, 4), (y, 10)])
../_images/9166a76e4c7188a60c99c6c89cc0834bef1c30309bc070a9db6f09d3cf3f7823.png

Podobno ima metoda sympy.evalf() argument subs, ki sprejme slovar zamenjav, primer:

(y**x).evalf(subs=parametri)
../_images/b51127934d3ba17a39febdbc570fbb6a30142bcf157b4c479e692177012146ee.png

Metoda sympy.subs pa lahko zamenja simbol (ali izraz) tudi z drugim izrazom:

(x + y).subs(x, y + sym.oo)
../_images/8a4e4235f4eeb3ac8d36895aed005f6e891473586120516a4b052364d8e22920.png

SymPy in NumPy#

Pogosto sympy povežemo z numpy. Za primer si poglejmo, kako bi izraz:

x = sym.symbols('x')
f = sym.sin(x) + sym.exp(x)
f
../_images/d9a678613784e35364d07be8a03846a09e7f062d51612e636b2d8dfaf26a1fb8.png

numerično učinkovito izračunali pri tisoč vrednostih \(x\).

Najprej uvozimo paket numpy:

import numpy as np

Pripravimo numerično polje vrednosti:

x_vec = np.linspace(0, 10, 1000, endpoint=False)
x_vec[:10]
array([0.  , 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09])

Glede na zapisano zgoraj in predhodno znanje uporabimo izpeljevanje seznamov:

y_vec = np.array([f.evalf(subs={x: vrednost}) for vrednost in x_vec])

Opazimo, da je to dolgotrajno, zato izmerimo potreben čas:

%%timeit -n1
y_vec = np.array([f.evalf(subs={x: vrednost}) for vrednost in x_vec])
377 ms ± 31.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Uporaba funkcije lambdify#

Bistveno hitrejši način je uporaba pristopa lambdify, kjer se pripravi prevedena funkcija, optimirana za numerično izvajanje. Sintaksa funkcije sympy.lambdify() je (dokumentacija):

sympy.lambdify(simboli, funkcija, modules=None)

kjer so argumenti:

  • simboli simboli uporabljeni v funkcija, ki se zamenjajo z numeričnimi vrednostmi,

  • funkcija predstavlja sympy funkcijo,

  • modules predstavlja, za kateri paket je prevedena oblika pripravljena. Če je numpy nameščen, je privzeto za ta modul.

Primer uporabe:

f_hitra = sym.lambdify(x, f, modules='numpy')
y_vec_hitra = f_hitra(x_vec)

Preverimo hitrost:

%%timeit -n100
f_hitra(x_vec)
The slowest run took 5.98 times longer than the fastest. This could mean that an intermediate result is being cached.
53.6 µs ± 52 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Opazimo približno 10.000-kratno pohitritev!

Poglejmo še primer uporabe funkcije več spremenljivk:

f_hitra2 = sym.lambdify((x, y), (x + y + sym.pi)**2, 'numpy')
x = np.linspace(0, 10, 10)
y = x
f_hitra2(x, y)
array([  9.8696044 ,  28.77051002,  57.54795885,  96.20195089,
       144.73248614, 203.1395646 , 271.42318627, 349.58335115,
       437.62005924, 535.53331054])

Grafični prikaz#

SymPy ima na matplotlib temelječ prikaz podatkov. Prikaz je sicer glede na matplotlib bolj omejen in ga uporabljamo za preproste prikaze. (dokumentacija).

Pogledali si bomo preproste primere, ki se navezujejo na funkcijo sympy.plotting.plot; najprej uvozimo funkcijo:

from sympy.plotting import plot

Sintaksa uporabe funkcije sympy.plotting.plot() (dokumentacija) je:

plot(izraz, razpon, **kwargs)

kjer so argumenti:

  • izraz je matematični izraz ali več izrazov,

  • razpon je razpon prikaza (privzeti razpon je (-10, 10)),

  • **kwargs so keyword arguments, torej slovar različnih možnosti.

Funkcija vrne instanco objekta sympy.Plot().

Minimalni primer ene funkcije:

x = sym.symbols('x')
plot(x**2)
../_images/0f4a29f8ff3634ceacadd402b2a15013e5d34601d83502669839c1701a786f6b.png
<sympy.plotting.plot.Plot at 0x17308af6510>

Opomba: zadnja vrstica opozori na rezultat v obliki instance Plot; izpis objekta <sympy.plotting.plot.Plot at 0x...> skrijemo z uporabo podpičja:

plot(x**2);

slika pa se vseeno prikaže.

Nekateri pogosti argumenti so:

  • show prikaže sliko (privzeto True),

  • line_color barva izrisa,

  • xscale in yscale način prikaza (možnosti: linear ali log),

  • xlim in ylim omejitev prikaza za osi (terka dveh (min, max) vrednosti).

Pripravimo dve sliki, kjer bo \(y\) os logaritemska in bo razpon izrisa od 1 do 5:

izris1 = plot(x**2, (x, 1, 5), show=False, legend=True, yscale='log')
izris2 = plot(30*sym.log(x), (x, 1, 5), show=False, line_color='C2', legend=True, yscale='log')

Sedaj prvo sliko razširimo z drugo in prikažemo rezultat:

izris1.extend(izris2)
izris1.show()
../_images/06e74443ff4f5a3cdab9d046aeb80c7908f4b5ef90d335ff6081f23442ad65e1.png

Parametrični izris#

Podobno uporabljamo funkcijo sympy.plotting.plot_parametric za parametrični izris (dokumentacija):

plot_parametric(izraz_x, izraz_y, range, **kwargs)

kjer sta nova argumenta:

  • izraz_x in izraz_y definicije lege koordinate \(x\) in \(y\),

  • **kwargs je slovar možnosti.

Uvozimo funkcijo:

from sympy.plotting import plot_parametric

Prikažimo uporabo na primeru:

plot_parametric(sym.sin(x), sym.sin(2*x), (x, 0, 2*sym.pi));
../_images/3a17ff3a87cca9c70c8e04fccb994005c5053a00aaaed0d6ceed9b1692d6b8f3.png

Izris v prostoru#

Funkcija sympy.plotting.plot3D (dokumentacija) za izris v prostoru ima sintakso:

plot3d(izraz, razpon_x, razpon_y, **kwargs)

kjer so argumenti:

  • izraz definicija površine,

  • razpon_x in razpon_y razpon koordinate x in y,

  • **kwargs slovar možnosti.

Uvozimo funkcijo:

from sympy.plotting import plot3d

Prikažimo uporabo na primeru:

x, y = sym.symbols('x y')
plot3d(x**2 + y**2, (x, -5, 5), (y, -5, 5));
../_images/593b8255f5a71422e100a2013a6e90f861926ed6f29bc36fa95950bc34591aa4.png

Za ostale prikaze glejte dokumentacijo.

Algebra#

V tem poglavju si bomo pogledali nekatere osnove uporabe SymPy za algebrajske operacije.

Uporaba expand in factor#

Definirajmo matematični izraz:

x = sym.symbols('x')
f = (x+1)*(x+2)*(x+3)
f
../_images/3cc11bc42bdefbfd1fbe06ea901bc6cecd22b035958ec803d54e477ed2065229.png

in ga sedaj razčlenimo (angl. expand, glejte dokumentacijo):

aa = sym.expand(f)
aa
../_images/589ca0fc7877657d05318af33172a1af97a738abd2e8abaaca2f241bce3d9011.png

Če želimo pogledati koeficiente pred x, to naredimo z metodo coeff():

aa.coeff(x)
../_images/a28165f21132b3ecda9391d48dd23cbf7651e859ca1d98f2e94c43afe484c0db.png

Argumenti funkcije definirajo, kakšno razširitev želimo (dokumentacija). Če želimo npr. trigonometrično razširitev, potem uporabimo trig=True:

a, b = sym.symbols('a, b')
sym.expand(sym.sin(a+b))
../_images/8b34ba99a4f7048425077d6ac4c1c293ee1ebc08b7ea1575a02d5fafe89c99c4.png
sym.expand(sym.sin(a+b), trig=True)
../_images/fb42666eb43c1bb178478c20f131d31217baccdc140b5d178e396db42a33ebcb.png

Obratna operacija od razčlenitve je razcepitev ali razstavljanje ali faktorizacija (angl. factor, dokumentacija):

sym.factor(x**3 + 6 * x**2 + 11*x + 6)
../_images/3cc11bc42bdefbfd1fbe06ea901bc6cecd22b035958ec803d54e477ed2065229.png

Če nas zanimajo posamezni členi, potem to naredimo s funkcijo sympy.factor_list:

sym.factor_list(x**3 + 6 * x**2 + 11*x + 6)
../_images/ea8ae5130c19f84527a047c18aea43263991bebe71dbff0d037e41efaff76e07.png

Poenostavljanje izrazov s simplify#

Funkcija sympy.simplify() (dokumentacija) poskuša poenostaviti izraze v bolj preproste (npr. s krajšanjem spremenljivk).

Za posebne namene lahko poenostavimo tudi z:

Za več glejte dokumentacijo.

Primeri poenostavljanja:

sym.simplify((x+1)*(x+1)*(x+3))
../_images/783d038914bb2cb41e6724f4af16e608e3e346428912d00891dd1a93bab41f01.png
sym.simplify(sym.sin(a)**2 + sym.cos(a)**2)
../_images/d81096050bd44ce57b5d5fe4208dfd494a47c011aecee7a2c422785cd87b6ef4.png
sym.simplify(sym.cos(x)/sym.sin(x))
../_images/563d6094e9255fa98dbf43b31d020ace326b4c3d38a2225d6c37125c8a7d4901.png

Uporaba apart in together#

Funkciji uporabljamo za delo z ulomki:

f1 = 1/((1 + x) * (5 + x))
f1
../_images/407e11504bce7437320ce4d1c6155e1c8708d783b74d2606cc2b350e4d347bdc.png

Razcep na parcialne ulomke (angl. partial fraction decomposition) izvedemo s funkcijo sympy.apart() (dokumentacija):

f2 = sym.apart(f1, x)
f2
../_images/0032336f88627f88a1ef3e16d428b97d5584c3b2dc314c41def286a1b6154535.png

in potem ponovno v obratni smeri s funkcijo sympy.together():

sym.together(f2)
../_images/407e11504bce7437320ce4d1c6155e1c8708d783b74d2606cc2b350e4d347bdc.png

V slednjem primeru pridemo do podobnega rezultata s sympy.simplify():

sym.simplify(f2)
../_images/407e11504bce7437320ce4d1c6155e1c8708d783b74d2606cc2b350e4d347bdc.png

Odvajanje#

Odvajanje je načeloma relativno preprosta matematična operacija, ki jo izvedemo s funkcijo sympy.diff() (dokumentacija):

Pripravimo primer:

x, y, z = sym.symbols('x, y, z')
f = sym.sin(x*y) + sym.cos(y*z)
f
../_images/5ff7249ddff2b921d4836494423a55b5390f3604939c5488ed676c7cb003b511.png

Odvajajmo ga po \(x\):

sym.diff(f, x)
../_images/396c7f6d437daf0a8f06b229731fa6e27509d443ce5e651f334b6c56b1d7d1c3.png

ali tudi

f.diff(x)
../_images/396c7f6d437daf0a8f06b229731fa6e27509d443ce5e651f334b6c56b1d7d1c3.png

Odvode višjega reda definiramo tako:

sym.diff(f, x, x, x)
../_images/3e75788866ec8f130111685bdadeb806093bff676ebdadcb9d66fe19083ad52c.png

ali (isti rezultat malo drugače):

sym.diff(f, x, 3)
../_images/3e75788866ec8f130111685bdadeb806093bff676ebdadcb9d66fe19083ad52c.png

Odvod po več spremenljivkah \(\frac{d^3f}{dx\,dy^2}\) izvedemo takole:

sym.diff(f, x, 1, y, 2)
../_images/4a1fb2f486edb296169ae8d717c1232a5be8055c2d18238bfdbbf0d4b91981a8.png

Integriranje#

Funkcijo integrate lahko uporabimo za nedoločeno integriranje (dokumentacija):

integrate(f, x)

ali za določeno integriranje:

integrate(f, (x, a, b))

kjer so argumenti:

  • f funkcija, ki jo integriramo,

  • x spremenljivka, po kateri integriramo,

  • a in b meje integriranja.

Primer nedoločenega integriranja:

x = sym.symbols('x')
f = sym.sin(x*y) + sym.cos(y*z)
sym.integrate(f, x)
../_images/5659145c23aeb9a34f4a2f81a12f1fce627f01c74acef6ebc8ea5e1f6e2ef166.png

Opazimo, da sympy pravilno upošteva možnost, da je \(y=0\).

Še primer določenega integriranja:

sym.integrate(f, (x, -1, 1))
../_images/b7111734a58ceccf79603e820280d7fc1612dc1b22c061d32eb5a553a649f9c9.png

Primer, ko so meje v neskončnosti (uporabimo konstanto za neskončnost sympy.oo):

sym.integrate(sym.exp(-x**2), (x, -sym.oo, sym.oo))
../_images/ccef02a8d83a3c43a6ef7751e0c9d76bac908e2bb2411907f1c600908ecb5eff.png

Vsota in produkt vrste#

Vsoto vrste definiramo s pomočju funkcije sympy.Sum() (dokumentacija):

sympy.Sum(izraz, (spr, start, end))

kjer so argumenti:

  • izraz izraz, katerega seštevamo,

  • spr, start in end spremenljivka, ki naračša od start do end (end je vključen).

Primer vsote vrste:

n = sym.Symbol('n')
f = sym.Sum(1/x**n, (n, 1, sym.oo))
f
../_images/294dce18e8c07e6fdb70e7f5c58d0b5add61b77dea190a4b5f065f8cc748c4cb.png

Šele ko uporabimo metodo doit_(), se izračun izvede:

f.doit()
../_images/ebe6feee72f542ebd434c62a7d53ae25e602a21454cd9539ca783630cfd423a5.png

Poglejmo še številčni rezultat:

f.subs({x: 5}).evalf()
../_images/c93686befe153074e678fb4b337ce6c7b80083932202eeb9002b915a963d425a.png

Produkt vrste definiramo podobno s funkcijo sympy.Product (dokumentacija):

sympy.Product(izraz, (spr, start, end))

kjer so argumenti:

  • izraz izraz, katerega množimo,

  • spr, start in end spremenljivka, ki naračša od start do end (end je vključen).

Primer:

f = sym.Product(1/n, (n, 1, 5))
f
../_images/8207d5e77928e425180f9d0a5d27e000de3732fb3b10a844e1110064a8dba45e.png
f.doit()
../_images/3fed1b161ded0a4bfb92a7120f15189ad891707a9875d6f46569cd738d7dc0e6.png

Limitni račun#

Limite računamo s pomočjo funkcije sympy.limit() (dokumentacija):

sympy.limit(f, x, x0)

kjer so argumenti:

  • f izraz, katerega limito iščemo,

  • x spremenljivka, ki limitira proti x0,

  • x0 limita.

Primer:

x = sym.symbols('x')
f = sym.sin(x)/x
f
../_images/765b1df3002a67d91142c2d544525bcf3ffdd0e71b84c22aeddf3d2249911bee.png
sym.limit(f, x, 0)
../_images/d81096050bd44ce57b5d5fe4208dfd494a47c011aecee7a2c422785cd87b6ef4.png

Za primer si poglejmo uporabo limite na definiciji odvoda:

\[ \frac{\mathrm{d}f}{\mathrm{d}x} = \lim_{h\rightarrow 0}\frac{f(x+h,y)-f(x,y)}{h}. \]

Pripravimo funkcijo f in njen odvod:

x, y, z, h = sym.symbols('x, y, z, h')
f = sym.sin(x*y) + sym.cos(y*z)

Odvod funkcije je:

sym.diff(f, x)
../_images/396c7f6d437daf0a8f06b229731fa6e27509d443ce5e651f334b6c56b1d7d1c3.png

Enak rezultat izračunamo tudi z uporabo limite:

sym.limit((f.subs(x, x+h) - f)/h, h, 0)
../_images/396c7f6d437daf0a8f06b229731fa6e27509d443ce5e651f334b6c56b1d7d1c3.png

Taylorjeve vrste#

Taylorjeve vrste izračunamo s pomočjo funkcijo sympy.series() (dokumentacija):

sympy.series(izraz, x=None, x0=0, n=6, dir='+')

kjer so argumenti:

  • izraz izraz, katerega vrsto določamo,

  • x neodvisna spremenljivka,

  • x0 vrednost, okoli katere določamo vrsto (privzeto 0),

  • n red vrste (privzeto 6),

  • dir smer razvoja vrste (+ ali -).

Primer:

x = sym.symbols('x')
sym.series(sym.exp(x), x) # privzete vrednosti x0=0, in n=6
../_images/6086771a78e1d58325f476fdb7530df32cd6bec3980477b49838a1d42e8a2734.png

Če želimo definirati drugo izhodišče (x0=2) in z več členi (n=8), to izvedemo takole:

s1 = sym.series(sym.exp(x), x, x0=2, n=8)
s1
../_images/ccb74c260fb2439ed8791276eb4cc1345a27e03fc3e563289b53d65f1426c0d9.png

Rezultat vključuje tudi red veljavnosti; na ta način lahko kontroliramo veljavnosti izvajanja (\(\mathcal{O}\)).

Primer:

s1 = sym.cos(x).series(x, 0, 5)
s1
../_images/a0fdaf198fbda3d65519dbf747af74d4b3503616fab85006f4f3de8d170e5461.png
s2 = sym.sin(x).series(x, 0, 2)
s2
../_images/33ee20909f81aa49fad6303bc8185685896d8c21895128cdd0a5cd99ff7dc3af.png

Izračuna s1 in s2 imata različna reda veljavnosti, posledično je produkt:

s1 * s2
../_images/d1fb8f5094a70485cf6bf923c26dffc6ea062a78624f8d2d0db5cd261857fb8e.png

natančen samo do reda \(\mathcal{O}(x^2)\), kar sympy ustrezno obravnava:

s3 = sym.simplify(s1 * s2)
s3
../_images/33ee20909f81aa49fad6303bc8185685896d8c21895128cdd0a5cd99ff7dc3af.png

Podatek o stopnji veljavnosti lahko odstranimo:

s3.removeO()
../_images/a62f1bf0f037de5a14e602497a42670c302da5db132010539cd91d2d25add5c6.png

Linearna algebra#

Matrike in vektorji#

Matrike in vektorje definiramo s funkcjo Matrix. Če se pri numpy.array ni treba dosledno držati matematičnega zapisa vektorjev in matrik, je pri sympy to nujno.

Poglejmo si primer; najprej pripravimo spremenljivke:

m11, m12, m21, m22 = sym.symbols('m11, m12, m21, m22')
b1, b2 = sym.symbols('b1, b2')

Nato matriko in stolpični vektor:

A = sym.Matrix([[m11, m12],[m21, m22]])
A
../_images/fabbff8489dae2de9d637c573cb2e58a71e2a56533eae66813e92544d1998f10.png
b = sym.Matrix([[b1], [b2]])
b
../_images/628aca1b0bd366381d226be6630f54c7add0c34f8bdadadbc166ba2f532a49e4.png

Sedaj si poglejmo nekatere tipične operacije; najprej množenje matrike in vektorja:

A * b
../_images/4a33616a9cd466c20d65bb4cc8c401604f1bf0360bdeb5d3a23f5601a2cd6ce7.png

Nato skalarni produkt dveh vektorjev (paziti moramo na transponiranje enega od vektorjev):

b.T*b
../_images/a4a13a20e65c0166d76e6ec0c8c4be9437a04a239ad6f3fd007d5840ff469eee.png

Determinanta in inverzna matrika:

A.det()
../_images/cf8bfc95bdea0f0958b5b25357197d51b6855b256fad827706f1d63d5310209e.png
A.inv()
../_images/dc4b4e738f3014f76c8991edccbe7cc1d7988734a224eff3f4e36a21248ed856.png

Množenje in potenca matrike:

A*A
../_images/2d69eb351594020673abcebf0e7778d508a6ac4f154ef62ce2c471bd59d5f875.png
A**2
../_images/2d69eb351594020673abcebf0e7778d508a6ac4f154ef62ce2c471bd59d5f875.png

Reševanje enačb#

Enačbe in sistem enačb rešujemo s funkcijo sympy.solve() (dokumentacija). Podprto je reševanje sledečih enačb:

  • polinomske enačbe,

  • transcendentne enačbe,

  • odsekovno definirane enačbe kot kombinacija zgornjih dveh tipov,

  • sistem linearnih in polinomskih enačb,

  • sistem enačb z neenakostmi.

Sintaksa je:

sympy.solve(f, *symbols, **flags)

kjer so argumenti:

  • f izraz ali seznam izrazov,

  • *symbols simbol ali seznam simbolov, katere želimo določiti,

  • **flags slovar možnosti.

Poglejmo primer:

x = sym.symbols('x')
f = sym.sin(x)
en = sym.Eq(f, 1/2)
en
../_images/aefb226d64133b4090ef5e4e32d7ce419454cb2b3af84b01502006d9700ca680.png
sym.solve(en, x)
../_images/996f6c8b63435e031989a119dcafc65e0a4be15d89e8caa4ced84f09ae430573.png

Prikažimo rešitev (opazimo, da smo našli samo dve od neskončno rešitev):

p1 = sym.plotting.plot(sym.sin(x), (x, -2*sym.pi, 2*sym.pi), line_color='C0', show=False, legend=True)
p2 = sym.plotting.plot(0.5, (x, -2*sym.pi, 2*sym.pi), line_color='C1', show=False, legend=True)
p1.extend(p2)
p1.show()
../_images/b4a4cc308747a6593bf263ee9dca5404bd4eba8f75d56717882163499bcd91eb.png

Kvadratna enačba:

a, b, c, x = sym.symbols('a, b, c, x')
sym.solve(a*x**2 + b*x + c, x)
../_images/de690edcf14d63955807697e02e7df87fa230038145155159e732a4027064bff.png

Sistem enačb:

x, y = sym.symbols('x y')
sym.solve([x + y - 1, x - y - 1], [x, y])
../_images/c40a008cc79d11ec047f7e2976b517d14620f442831024cde2ce9330e1f0cdb2.png

Za nelinearne sisteme pa lahko uporabimo tudi numerično reševanje s funkcijo sympy.nsolve() (dokumentacija):

sympy.nsolve(f, [args,] x0, modules=['mpmath'], **kwargs)

kjer so argumenti:

  • f enačba ali sistem enačb, ki ga rešujemo,

  • args spremenljivke (opcijsko),

  • x0 začetni približek (skalar ali vektor),

  • modules paket, ki se uporabi za izračun numerične vrednosti (enaka logika kot pri funkciji lambdify, privzet je paket mpmath),

  • **kwargs slovar opcij.

Poglejmo primer od zgoraj:

x = sym.symbols('x')
eq = sym.Eq(sym.sin(x), 0.5)
sol = sym.nsolve(en, x, 3)
sol
../_images/947d82f9c72d06572fc73015eea31fa7697d8c85513435f3524df1e693961c6a.png

Reševanje diferencialnih enačb#

Nedefinirane funkcije#

Preden si ogledamo diferencialne enačbe, moramo spoznati nedefinirane funkcije. Take funkcije definiramo sympy.Function dokumentacija.

Kot primer si poglejmo kako definiramo enačbo \(\ddot x(t)=g\). Najprej definirajmo nedoločeno funkcijo:

x = sym.Function('x')

Nato simbole:

t, g = sym.symbols('t, g')

In še enačbo:

sym.Eq(x(t).diff(t,2), g)
../_images/50bdc56afd8d4bb6b9647984fea28a3f81a8d360908606061f35028370f5ba1e.png

Diferencialne enačbe#

Diferencialne enačbe in sisteme diferencialnih enačb rešujemo s funkcijo sympy.dsolve() (dokumentacija):

sympy.dsolve(eq, func=None, hint='default', simplify=True, 
             ics=None, xi=None, eta=None, x0=0, n=6, **kwargs)

kjer so izbrani argumenti:

  • eq differencialna enačba ali sistem diferencialnih enačb,

  • func rešitev, ki jo iščemo,

  • ics začetni in robni pogoji diferencialne enačbe.

Poglejmo si primer mase \(m\), ki drsi po površini s koeficientom trenja \(\mu\); začetna hitrost je \(v_0\), pomik \(x_0=0\).

Definirajmo simbole:

x = sym.Function('x')
t, m, mu, g, v0, x0 = sym.symbols('t m mu g v0 x0', real=True, positive=True)

Definirajmo diferencialno enačbo:

eq = sym.Eq(m*x(t).diff(t,2), -mu*g*m)
eq
../_images/0027f162394cf7b608f00e65d67715045d1a55737aa57fe987c8bd9d0b1d53bd.png

Poglejmo lastnosti diferencialne enačbe:

sym.ode_order(eq, x(t))
../_images/d09c16c60c1bec6d87b5a6ea36a28d98601be002bd4b459592b55e0896ad9754.png
sym.classify_ode(eq)
('factorable',
 'nth_algebraic',
 'nth_linear_constant_coeff_undetermined_coefficients',
 'nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients',
 'nth_linear_constant_coeff_variation_of_parameters',
 'nth_linear_euler_eq_nonhomogeneous_variation_of_parameters',
 'nth_algebraic_Integral',
 'nth_linear_constant_coeff_variation_of_parameters_Integral',
 'nth_linear_euler_eq_nonhomogeneous_variation_of_parameters_Integral',
 '2nd_nonlinear_autonomous_conserved',
 '2nd_nonlinear_autonomous_conserved_Integral')

Rešimo jo:

rešitev = sym.dsolve(eq, x(t), 
                     ics={
                         x(0):0,                     #pri času 0s je pomik nič
                         x(t).diff(t).subs(t, 0): v0 #pri času 0s je hitrost enaka v0
                     })
rešitev
../_images/8e869e77442b73494e310bc3572115707e2b3ef9523fd8ab8638f48b49ff29bb.png

Desno stran enačbe prikličemo takole (rhs - right-hand side):

rešitev.rhs
../_images/41356b2e05b013a527154169a141e22f72de9a830c85863c698c2908b1a0cbd1.png

Nekaj vprašanj za razmislek!#

  1. Pojasnite na primeru proceduralno in funkcijsko programiranje.

  • Definirajte preprost objekt, naredite nekaj funkcij temu objektu.

  • Definirajte objekt, ki pri kreiranju instance zahteva zgolj celoštevilsko vrednost( npr.: dolžino seznama, ki jo bomo uporabili pri naslednji točki).

  • Objektu iz prejšnje točke naj pri inicializaciji argumentu data priredi naključni seznam ustrezne dolžine (glejte funkcijo np.random.rand).

  • Objektu iz prejšnje točke dodajte metodo za zapis vrednosti v datoteko s pomočjo funkcije np.savetxt.

  • Enako kot pri prejšnji točki, vendar naj se podatki shranijo v binarni obliki s pomočjo modula pickle.

  • Dodajte metodo za branje iz datoteke (s pomočjo np.genfromtxt).

  • Uvozite ves naslovni prostor iz SymPy. Nastavite lep izpis rezultatov.

  • Za trikotnik na sliki definirajte funkcijo za izračun površine in volumna.

Trikotnik

  • Izračunajte številčne vrednosti (podatki naj bodo definirani v slovarju in si jih izmislite).

  • Izračunajte statični moment ploskve \(S_{xx}=\int_A y\,dA=\int_{0}^{b} y\,x(y)\,dy\), kjer je \(x(y)=a-a\,y/b\).

  • Izračunajte vztrajnostni moment ploskve \(I_{xx}=\int_A y^2\,dA\), \(dA = x(y) \cdot dy\).

  • Prikažite \(I_{xx}\) v odvisnosti od parametra \(b\) (\(a\) definirajte poljubno).

  • Nedoločeno in določeno (v mejah od 0 do \(\tau\)) integrirajte izraz: \(\sin(5+t)+e^t\).

  • Z odvajanjem pokažite pravilnost nedoločenega integrala iz predhodnega koraka.

  • Za kotaleči valj (polmer \(r\), masa \(m\)) povežite translatorno \(x\) prostost z rotacijsko \(\varphi\). Pozneje boste vse izrazili s slednjo. Namig: Dolžina loka kroga ustreza zmnožku polmera \(r\) in kota \(\varphi\) [rad].

  • Določite translatorno kinetično energijo težišča (definirajte s hitrostjo \(\dot x\), zaradi predhodne povezave pa bi naj bil rezultat s \(\dot{\varphi}\)). \(E_k = \frac{1}{2} \, m \, v^2\).

  • Določite še masni vztrajnostni moment valja in rotacijsko kinetično energijo. Obe kinetični energiji seštejte in izraz poenostavite (če je potrebno). \(J_v = \frac{1}{2} \, m \, r^2\) \(E_{k, r} = \frac{1}{2} \, J_v \, \left[\frac{d}{dt} \varphi(t)\right]^2\)

  • Če na valj deluje moment \(-M\), definirajte mehansko energijo: \(E_m=-M\,\varphi\) in določite gibalno enačbo iz spremembe mehanske energije: \(\frac{d E_m}{d t}=\frac{d E_k}{d t}\).

  • Nadaljujete na predhodni enačbi: poiščite sympy funkcijo replace in ugotovite razliko s subs. Poskusite s pomočjo replace \(\dot{\varphi}\) na obeh straneh enačbe spremeniti v 1.

  • Najdite rešitev za predhodno pridobljeno diferencialno enačbo.

  • Izmislite si začetne pogoje in jih uporabite na predhodno rešeni diferencialni enačbi. Izmislite si še preostale podatke ter prikažite rezultat.

  • Določite čas, ko je zasuk \(\varphi\) spet enak začetnemu (če ste predpostavili začetni zasuk nič, potem torej iščete \(\varphi=0\). Določite tudi čas, ko je kotna hitrost \(\dot{\varphi}\) enaka nič.

Dodatno#

sympy.mechanics#

sympy ima vgrajeno podporo za klasično mehaniko (dokumentacija). Celovit tutorial je bil prikazan na znanstveni konferenci SciPy 2016.