Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc17c78440 |
BIN
Binary file not shown.
+138
-113
@@ -236,7 +236,6 @@ Ko vzorec ocenimo, si aplikacija zapomni, in ko ocenimo vse vzorce, se nam prika
|
|||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
= UPORABLJENE TEHNOLOGIJE
|
= UPORABLJENE TEHNOLOGIJE
|
||||||
V tem poglavju na kratko predstavimo tehnologije, ki smo jih uporabili, in razloge za njihovo izbiro.
|
|
||||||
|
|
||||||
== C++
|
== C++
|
||||||
C++ je visokonivojni programski jezik, ki je nastal kot razširitev jezika C. Razvil ga je Bjarne Stroustrup v zgodnjih 1980. letih z namenom, da združi moč in učinkovitost jezika C z objektno usmerjenimi koncepti, ki podpirajo boljšo organizacijo in ponovno uporabo kode @wiki_cpp.
|
C++ je visokonivojni programski jezik, ki je nastal kot razširitev jezika C. Razvil ga je Bjarne Stroustrup v zgodnjih 1980. letih z namenom, da združi moč in učinkovitost jezika C z objektno usmerjenimi koncepti, ki podpirajo boljšo organizacijo in ponovno uporabo kode @wiki_cpp.
|
||||||
@@ -391,9 +390,9 @@ SQLite je torej idealna rešitev, če potrebujemo preprosto, hitro in zanesljivo
|
|||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
= STRUKTURA APLIKACIJE
|
= STRUKTURA APLIKACIJE
|
||||||
Aplikacija ima tri glavne enote: DNA menedžer, DNA vizualizator in jedro. DNA menedžer hrani generacijo genomov, skrbi za vrstni red njihovega prikaza in za tvorjenje nove generacije, DNA vizualizator iz genoma izriše sliko, v jedru pa so preostale komponente, med njimi omrežni vmesnik in vmesnik za shranjevanje.
|
Aplikacija ima tri glavne enote: DNA menedžer, DNA vizualizator in jedro. DNA manedžer skrbi za shranjevanje trenutnega dna in mutacijo, DNA vizualizator pa skrbi za izris dna, njegovo delo je, da dobi dna sekvenco in jo pretvori v sliko. V jedru se nahajajo ostale komponente, ki so pomembne za delovanje aplikacije, omrežni vmesnik in vmesnik za shranjevanje.
|
||||||
|
|
||||||
Jedro je hrbtenica, ki povezuje vse komponente. Ob zagonu preračuna razmerja za prikaz besedila in slik dreves, med uporabo pa skrbi za premik in rotacijo slike, ko uporabnik s prstom povleče po zaslonu. Ko poteg doseže rob zaslona, jedro oceno posreduje DNA menedžerju in od njega dobi naslednji genom za prikaz, ga preda vizualizatorju in vrnjeno sliko prikaže uporabniku. Vsaka ocena se sproti shrani na disk, ob koncu generacije pa se shrani celotna generacija in se pošlje na strežnik.
|
Jedro je hrbtenica, ki povezuje vse komponente, ob zagonu preračuna razmerja za prikaz besedila in slik dreves, skrbi za premik in rotacijo slike, ko s prstom povlečemo po zaslonu, skrbi, da ko pridemo s prstom do roba in hočemo označiti sliko, se izbira pošlje v DNA manager, ki nam nato vrne naslednji DNA za prikaz. Jedro tukaj poskrbi da se prenese v DNA vizualizator in ko dobi sliko, da jo prikaže uporabniku, se po vsaki izbiri ta tudi shrani na disk, ko pridemo pa do konca generacije, se še cela generacija shrani na disk in nato se tudi pošlje na strežnik.
|
||||||
|
|
||||||
#figure(
|
#figure(
|
||||||
image("assets/potek_diagram/diagram.svg", height: 400pt),
|
image("assets/potek_diagram/diagram.svg", height: 400pt),
|
||||||
@@ -462,8 +461,8 @@ V @mat_rotacija za $Delta"mouseV"$ lahko vidimo na (@Prikaz_razdalje), da ko je
|
|||||||
|
|
||||||
$
|
$
|
||||||
"newAngleOfVectorMI" & = "rotationRad" + "oldAngleOfVectorM" \
|
"newAngleOfVectorMI" & = "rotationRad" + "oldAngleOfVectorM" \
|
||||||
"vectorMI"_x & = "sizeOfVectorMI" * cos("newAngleOfVectorMI") \
|
"vectorMI"_x & = "sizeOfVecorMI" * cos("newAngleOfVectorMI") \
|
||||||
"vectorMI"_y & = "sizeOfVectorMI" * sin("newAngleOfVectorMI") \
|
"vectorMI"_y & = "sizeOfVecorMI" * sin("newAngleOfVectorMI") \
|
||||||
"imageV"_x & = "vectorMI"_x + "mouseV"_x \
|
"imageV"_x & = "vectorMI"_x + "mouseV"_x \
|
||||||
"imageV"_y & = "vectorMI"_y + "mouseV"_y \
|
"imageV"_y & = "vectorMI"_y + "mouseV"_y \
|
||||||
"imageRotation" & = 90 + "rotation"
|
"imageRotation" & = 90 + "rotation"
|
||||||
@@ -488,7 +487,26 @@ $
|
|||||||
|
|
||||||
=== Uporabljene funkcije
|
=== Uporabljene funkcije
|
||||||
|
|
||||||
V zgornjih enačbah nastopata dve standardni funkciji. Funkcija $"atan2"$ vrne kot vektorja v pravem kvadrantu, pri čemer upošteva predznaka obeh koordinat @wiki_atan2. Argumenta ji podajamo v vrstnem redu $(Delta x, Delta y)$, zato je dobljeni kot merjen od navpične osi in v enačbah za razstavljanje vektorja nastopa sinus pri komponenti $x$ in kosinus pri komponenti $y$. Funkcija $"lerp"(v_0, v_1, t) = v_0 + t (v_1 - v_0)$ linearno interpolira med vrednostma $v_0$ in $v_1$ s parametrom $t in [0, 1]$ @wiki_lerp.
|
==== atan2
|
||||||
|
Funkcija atan2 je matematična funkcija, ki se uporablja za izračun tangente loka količnika, vendar namesto enega samega razmerja ($y/x$) vzame dva ločena argumenta ($y$ in $x$). Zaradi tega je še posebej uporabna za določanje kota v pravilnem kvadrantu, ki temelji na znakih obeh koordinat @wiki_atan2.
|
||||||
|
|
||||||
|
$
|
||||||
|
"atan2"(x, y) := cases(
|
||||||
|
arctan(frac(y, x)) & "if" x > 0,
|
||||||
|
arctan(frac(y, x))+pi & "if" x<0 "and" y >= 0,
|
||||||
|
arctan(frac(y, x))-pi & "if" x<0 "and" y < 0,
|
||||||
|
+ frac(pi, 2) & "if" x = 0 "and" y > 0,
|
||||||
|
- frac(pi, 2) & "if" x = 0 "and" y < 0,
|
||||||
|
"undefined" & "if" x = 0 "and" y = 0
|
||||||
|
)
|
||||||
|
$
|
||||||
|
|
||||||
|
==== LERP
|
||||||
|
Lerp (okrajšava za linearno interpolacijo) je matematična tehnika, ki se uporablja za iskanje vrednosti med dvema danima vrednostma na podlagi določenega interpolacijskega parametra, ki je pogosto označen kot $t$ @wiki_lerp.
|
||||||
|
|
||||||
|
$
|
||||||
|
"lerp"(v 0, v 1, t) = v 0 + t * (v 1 - v 0)
|
||||||
|
$
|
||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
@@ -497,12 +515,16 @@ V zgornjih enačbah nastopata dve standardni funkciji. Funkcija $"atan2"$ vrne k
|
|||||||
#let like = `like`
|
#let like = `like`
|
||||||
#let newGen = `newGen`
|
#let newGen = `newGen`
|
||||||
|
|
||||||
DNA menedžer hrani celotno populacijo tekoče generacije, skrbi za vrstni red prikaza osebkov in za prehod v naslednjo generacijo. Vmesnik sestavljajo tri funkcije (@dna_manager): #next vrne naslednji osebek za prikaz, #like sporoči uporabnikovo oceno, #newGen pa iz zbranih ocen zgradi naslednjo generacijo. Ko #next vrne `nullptr` in #like vrne `false`, so vsi osebki generacije prikazani in ocenjeni, zato jedro pokliče #newGen.
|
Namenjena uporaba je, najprej pokličemo #next, ki nam vrne objekt z DNA podatki in indeks DNA-ja, ko se uporabnik odloči, ali mu je všeč, to sporočimo managerju preko funkcije #like, ko nam #like vrne `false` in #next vrne `nullptr`, vemo, da smo prikazali in označili vse DNA-je v generaciji in moramo generirati novo generacijo s klicem #newGen (@dna_manager).
|
||||||
|
|
||||||
Menedžer dovoli, da naslednji osebek pridobimo, še preden ocenimo prejšnjega. Aplikacija to izkorišča tako, da hkrati hrani dva osebka: zgornjega, ki ga uporabnik premika in ocenjuje, in spodnjega, ki je že izrisan in čaka v ozadju. Po vsaki oceni spodnji osebek postane zgornji, na njegovo mesto pa vstopi naslednji.
|
Dna manager nam dovoli, da večkrat pridobimo naslednji DNA, ne da takoj označimo ali nam je všeč. V aplikaciji je to uporabljeno tako, da pridobimo dva DNA-ja za prikaz. Prvi je na vrhu in ga lahko premikamo in označimo, drugi je pa v ozadju. Po označitvi se zgornji in spodnji zamenjata in spodnji se zamenja z naslednjim.
|
||||||
|
|
||||||
|
Funkcija #next vrne DNA iz `vector[queued]` in ga nato inkrementira `queued`, če je `queued` večji od `vector`, vrne `nullptr`.
|
||||||
|
|
||||||
|
Funkcija #like sprejme indeks in liked in nato shrani index v vector liked ali disliked, nato inkrementira `seen` in če je večji od `vector`, vrne `false`.
|
||||||
|
|
||||||
|
Medtem funkcija #newGen vsebuje glavno logiko. Deluje na principu naravne evolucije. Naključno izbere dva DNA in ustvari otroka tako, da naključno kopira DNA od obeh staršev, da ima v povprečju 50 % DNA od enega starša in 50 % od drugega. Ko ustvari novo generacijo, celotno generacijo mutira tako, da naključno spremeni specificirano število genov (byte). Tukaj lahko opazimo robni primer, kaj, če imamo samo enega ali nič DNA-jev, ki so uporabniku všeč, v primeru, da kloniramo, dokler ne ustvarimo nove generacije in v primeru, ko uporabniku ni nobeden všeč, generiramo novo naključno generacijo.
|
||||||
|
|
||||||
Za sledenje stanju generacije menedžer uporablja dva indeksa v vektorju osebkov. Indeks `queued` kaže na naslednji osebek za prikaz, indeks `showed` pa na naslednji osebek za oceno. Funkcija #next vrne osebek na mestu `queued` in indeks poveča; ko `queued` doseže velikost generacije, vrne `nullptr`. Funkcija #like uvrsti indeks ocenjenega osebka med všečne ali nevšečne in poveča `showed`; ko sta oba indeksa na koncu vektorja, vrne `false`. Glavna logika evolucije je v funkciji #newGen in je opisana v
|
|
||||||
// @genetski_algoritem.
|
|
||||||
#figure(caption: [DnaManager])[
|
#figure(caption: [DnaManager])[
|
||||||
#grid(
|
#grid(
|
||||||
columns: 2,
|
columns: 2,
|
||||||
@@ -514,7 +536,7 @@ Za sledenje stanju generacije menedžer uporablja dva indeksa v vektorju osebkov
|
|||||||
uint128 randSeed;
|
uint128 randSeed;
|
||||||
int64_t id;
|
int64_t id;
|
||||||
int queued;
|
int queued;
|
||||||
int showed;
|
int seen;
|
||||||
std::vector<Dna> vector;
|
std::vector<Dna> vector;
|
||||||
std::vector<int> liked;
|
std::vector<int> liked;
|
||||||
std::vector<int> disliked;
|
std::vector<int> disliked;
|
||||||
@@ -551,7 +573,7 @@ Za sledenje stanju generacije menedžer uporablja dva indeksa v vektorju osebkov
|
|||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
== ZAPIS DNA
|
== DNA
|
||||||
Dna je sestavljen iz dveh struktur (@dna_objekt). Zanimliv del, ki ga opazimo, je, da je vsak gen točno en bite dolg in celoten objekt je v pomnilniku samo en velik list bitov (array of bites). To značinost uporabljamo v complementarnih funkcijah `newDna`, `makeChiled`, `clone` in `mutate`.
|
Dna je sestavljen iz dveh struktur (@dna_objekt). Zanimliv del, ki ga opazimo, je, da je vsak gen točno en bite dolg in celoten objekt je v pomnilniku samo en velik list bitov (array of bites). To značinost uporabljamo v complementarnih funkcijah `newDna`, `makeChiled`, `clone` in `mutate`.
|
||||||
|
|
||||||
#figure(caption: [DNA objekt])[
|
#figure(caption: [DNA objekt])[
|
||||||
@@ -704,11 +726,22 @@ Je sestavljena iz treh razredov: `Canvas`, `BackGround`, `Tree` (@razredi_vizual
|
|||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
=== Ozadje
|
=== Ozadje
|
||||||
Ozadje izrišemo v štirih korakih. Najprej narišemo nebo, in sicer podnevi kot navpičen barvni prehod s funkcijo `DrawRectangleGradientV` (@steps_nebo), ponoči pa kot enobarvno površino z zvezdami, katerih položaje določa naključno seme iz genoma (@steps_zvezde). Sledita sonce oziroma luna, sestavljena iz več koncentričnih krogov z nizko prosojnostjo, da dobimo mehak rob (@steps_sonce). Nazadnje izrišemo tri plasti gorovja, vsako s svojo barvo in višinskim razponom (@steps_gora_1 do @steps_gora_5).
|
Postopek izrisa ozadja je sestavljen iz 5 korakov
|
||||||
|
|
||||||
Gore so bile med vsemi elementi najzahtevnejše. Rešili smo jih s trakom trikotnikov (triangle strip) @wiki_triangle_strip, pri katerem se točke izmenjujejo med tlemi in naključno izbrano višino, tako da vsaka nova točka s prejšnjima dvema tvori trikotnik (@Triangle_Strip). Plast izrišemo iz 150 takih segmentov od levega do desnega roba slike.
|
+ Nebo je narisano s pomočjo raylib funkcije `DrawRectangleGradientV` (@steps_nebo).
|
||||||
|
|
||||||
Barvno shemo ozadja določata dva gena: `colorSet` izbere eno od štirih shem, tri dnevne in eno nočno, `moonY` pa znotraj dnevne sheme enega od štirih odtenkov, kar ustreza različnim delom dneva. Skupaj je torej na voljo 13 barvnih shem, znotraj vsake pa se zvezno spreminjajo oblika gorovja, razporeditev zvezd ter položaj in velikost sonca oziroma lune.
|
+ Če je noč, zvezde narišemo z naključnim semenom iz DNA, ki se uporabi za pozicijo, in funkcijo `DrawTriangle` (@steps_zvezde).
|
||||||
|
|
||||||
|
+ Sonce in luna sta narisana z raylib funkcijo `DrawCircle` (@steps_sonce).
|
||||||
|
|
||||||
|
+ Gore so bile težje za narisati, zato smo se odločili, da bomo uporabili triangle strip (@Triangle_Strip) @wiki_triangle_strip. Najprej smo se odločili, iz koliko segmentov jo bomo narisali v aplikaciji; uporabljamo 150 segmentov, s tem imamo širino segmenta. Sedaj sledimo tem korakom:
|
||||||
|
+ Prvo točko postavimo v spodnji levi kot (0,0).
|
||||||
|
+ Za drugo točko si izberemo naključno številko za višino gore (0,80).
|
||||||
|
+ Naslednja točka je spet na tleh, samo zamaknjena za širino segmenta na desno (20,0).
|
||||||
|
+ Za naslednjo točko si zberemo naključno število za višino gore in zamaknjeno na desno od prejšne višine (20, 90).
|
||||||
|
+ Sedaj ponavljamo točki 3 in 4, dokler ne pridemo do desnega roba slike.
|
||||||
|
|
||||||
|
+ Ponovimo korak 4 še dvakrat, da dobimo gorovje (@steps_gora_5).
|
||||||
|
|
||||||
#block(breakable: false)[
|
#block(breakable: false)[
|
||||||
#grid(
|
#grid(
|
||||||
@@ -717,19 +750,19 @@ Barvno shemo ozadja določata dva gena: `colorSet` izbere eno od štirih shem, t
|
|||||||
[
|
[
|
||||||
#figure(
|
#figure(
|
||||||
image("assets/draw_steps/steps_1_1.png", height: 100pt),
|
image("assets/draw_steps/steps_1_1.png", height: 100pt),
|
||||||
caption: [Korak dnevno nebo],
|
caption: [Korak nebo],
|
||||||
)<steps_nebo>
|
)<steps_nebo>
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
#figure(
|
#figure(
|
||||||
image("assets/draw_steps/steps_2_2.png", height: 100pt),
|
image("assets/draw_steps/steps_2_2.png", height: 100pt),
|
||||||
caption: [Korak nočno nebo z zvezdami],
|
caption: [Korak nebo],
|
||||||
)<steps_zvezde>
|
)<steps_zvezde>
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
#figure(
|
#figure(
|
||||||
image("assets/draw_steps/steps_1_2.png", height: 100pt),
|
image("assets/draw_steps/steps_1_2.png", height: 100pt),
|
||||||
caption: [Korak sonce oziroma luna],
|
caption: [Korak sonce],
|
||||||
)<steps_sonce>
|
)<steps_sonce>
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -809,10 +842,9 @@ Veja je struktura, ki vsebuje podatke o:
|
|||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
|
// TODO: Dodaj slike za lazjo predstavo
|
||||||
==== Algoritem
|
==== Algoritem
|
||||||
Algoritem drevo gradi po vejah in ne rekurzivno. V seznamu hrani opise vej, ki jih je še treba izrisati; iz seznama vzame prvi opis, izriše vejo in na njen konec doda opise njenih otrok, dokler seznam ni prazen. Vsaka veja pri tem prevzame nekaj lastnosti od starša (končno debelino in barvo), preostale pa prebere iz gena za svojo globino. Tako je celotno drevo določeno z osmimi geni, po enim za vsako globino, in s tem, kar veje podedujejo od svojih staršev.
|
Začnemo tako, da damo v seznam, kar prikazuje @prvi_drawArgs, in ponavljamo naslednje postopke, dokler imamo nekaj v seznamu.
|
||||||
|
|
||||||
Začnemo tako, da v seznam damo opis debla, ki ga prikazuje @prvi_drawArgs, in ponavljamo naslednje korake, dokler imamo nekaj v seznamu.
|
|
||||||
#figure(caption: [Prvi DrawArgs])[
|
#figure(caption: [Prvi DrawArgs])[
|
||||||
```cpp
|
```cpp
|
||||||
struct DrawArgs
|
struct DrawArgs
|
||||||
@@ -1006,11 +1038,10 @@ Sprva se je drevo izrisalo s pomočjo raylib funkcije `DrawLineEx`, drevo se je
|
|||||||
]
|
]
|
||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
=== Optimizacija DNA menedžerja
|
=== DNA manager optimizacija
|
||||||
|
|
||||||
Na začetku (@dna_manager_v1) je bil DnaManager struktura kazalcev na `Dna`, kar pomeni, da stanja ni bilo mogoče shraniti v datoteko, saj kazalci po ponovnem zagonu niso več veljavni. Zato smo kazalce zamenjali z indeksi (@dna_manager_v2), ki jih je mogoče zapisati; ob tem se je izkazalo še, da je za predpomnilnik ugodno, če so indeksi v seznamu `queued` urejeni naraščajoče.
|
Na začetku (@dna_manager_v1) je bil DnaManager samo struktura kazalcev na `DNA`, s tem imamo problem, da ne moremo shraniti kazalcev v datoteko. Spremenili smo ga na indekse (@dna_manager_v2), ki se dajo zapisati v datoteko, kasneje pa smo se spomnili, da bi bilo dobro za predpomnilnik, da so indeksi v queued sortirani. Med razhroščevanjem smo opazili, da queued in showed delujeta kot premikajoči indeksi za queued: pred njim so stvari, ki so že prikazane, za njim pa so stvari, ki jih še moramo prikazati. Zato smo spremenili queued in showed v en indeks namesto list (@dna_manager_v3).
|
||||||
|
|
||||||
Med razhroščevanjem smo opazili, da sta seznama `queued` in `showed` odveč. Ker osebke prikazujemo in ocenjujemo v enakem vrstnem redu, kot so shranjeni v vektorju, oba seznama vedno vsebujeta strnjen del vektorja, zato ju je mogoče nadomestiti z dvema premikajočima se indeksoma v vektorju osebkov (@dna_manager_v3): pred indeksom `showed` so ocenjeni osebki, med `showed` in `queued` so prikazani in še neocenjeni, za `queued` pa še neprikazani.
|
|
||||||
#align(center)[
|
#align(center)[
|
||||||
#grid(
|
#grid(
|
||||||
columns: 2,
|
columns: 2,
|
||||||
@@ -1065,88 +1096,104 @@ Med razhroščevanjem smo opazili, da sta seznama `queued` in `showed` odveč. K
|
|||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
= PODOBNOST
|
= PODOBNOST
|
||||||
Med sejo želimo uporabniku pokazati, kako daleč je evolucija napredovala, hkrati pa potrebujemo mero, s katero lahko delovanje genetskega postopka spremljamo tudi pri razvoju. Za oboje uporabljamo isto količino: *povprečno podobnost med vsemi pari osebkov v trenutni generaciji*. Pri 16 osebkih je to povprečje čez $binom(16, 2) = 120$ parov.
|
Da smo lahko prikazali, koliko so uporabniku slike všeč, smo se odločili, da bomo izračunali podobnost med slikami. Bolj ko so si slike podobne, bolj so všeč uporabniku in obratno. Zato smo preučili sedem algoritmov.
|
||||||
|
|
||||||
Poudariti je treba, kaj ta količina pove in kaj ne. Razdalja med genoma pove, kako podobni sta si sliki, torej kako zelo se je populacija zožila oziroma kam je izbira skonvergirala. Ne pove pa, ali so slike uporabniku dejansko všeč: populacija lahko skonvergira tudi k drevesom, s katerimi uporabnik ni zadovoljen, na primer kadar med prikazanimi osebki ni bilo nobenega, ki bi mu ustrezal. Podobnost je torej *mera konvergence*, ne mera všečnosti. V uporabniškem vmesniku je prikazana kot odstotek "všečnosti", kar je zaradi igrifikacije razumljivo, vsebinsko pa neposrečeno; primernejša oznaka bi bila "ujemanje z vašim izborom" ali "stopnja konvergence".
|
== Opis algoritmov
|
||||||
|
=== Evklidska razdalja
|
||||||
|
Evklidska razdalja je najkrajša razdalja med dvema točkama v evklidskem prostoru, ki je najpogosteje uporabljen koncept razdalje v matematiki in fiziki @wiki_euclidean_distance.
|
||||||
|
|
||||||
Ker se mera računa po vsaki generaciji in se prikaže uporabniku, mora biti izračunljiva neposredno nad 180-bajtnim genomom, imeti zalogo vrednosti, ki jo je mogoče prikazati kot odstotek, med konvergenco populacije naraščati in se izračunati dovolj hitro, da prehod med generacijama ni opazen. Da bi pokrili različne pristope k merjenju razdalje, smo preučili sedem mer iz treh družin: geometrijske (evklidska razdalja, skalarni produkt, kosinusna podobnost in njena varianta), množične (Jaccardov indeks) ter nizovne oziroma poravnalne (Hammingova razdalja, Levenshteinova razdalja in Needleman-Wunschev algoritem).
|
==== Formula
|
||||||
|
Za dve točki p in q v n-dimenzionalnem prostoru se izračuna:
|
||||||
|
$ d(p, q) = sqrt(sum_(i=1)^n (q_i - p_i)^2) $
|
||||||
|
|
||||||
== Opis mer
|
==== Normalizacija
|
||||||
Genom obravnavamo kot zaporedje $n = 180$ bajtov, torej kot vektor celih števil z zalogo vrednosti $[0, 255]$. Vse mere so definirane nad tem zapisom in vrnjeno vrednost normaliziramo na odstotke.
|
Da normaliziramo Evklidsko razdaljo, moramo deliti z najdaljšo možno razdaljo v prostoru:
|
||||||
|
$ "maxDistance" = 255 sqrt(n) $
|
||||||
|
Normalizacija je nato:
|
||||||
|
$ "normalizedDistance" = frac(d(p, q), "maxDistance") $
|
||||||
|
|
||||||
=== Geometrijske mere
|
=== Skalarni produkt
|
||||||
*Evklidska razdalja* je najkrajša razdalja med dvema točkama v evklidskem prostoru @wiki_euclidean_distance. Normaliziramo jo z največjo možno razdaljo v prostoru, podobnost pa je njen komplement:
|
Skalarni produkt (tudi dot produkt ali notranji produkt) je operacija med dvema vektorjema v vektorskem prostoru, ki kot rezultat vrne skalar (realno število) @wiki_dot_product.
|
||||||
|
|
||||||
|
==== Formula:
|
||||||
|
Za vektorja A in B v n-dimenzionalnem prostoru:
|
||||||
|
|
||||||
|
$ upright(bold(A)) op(dot) upright(bold(B)) = sum_(i=1)^n A_i B_i $
|
||||||
|
|
||||||
|
==== Normalizacija
|
||||||
|
Da noramliziramo skalarni produkt moramo deliti z največjim produktom v prostoru:
|
||||||
|
$ "maxDotProduct" = n*255*255 $
|
||||||
|
Normalizacija je nato:
|
||||||
$
|
$
|
||||||
d(p, q) & = sqrt(sum_(i=1)^n (q_i - p_i)^2) \
|
"normalizedDotProdut" = frac(
|
||||||
s & = 1 - frac(d(p, q), 255 sqrt(n))
|
upright(bold(A)) op(dot) upright(bold(B)),
|
||||||
|
"maxDotProduct"
|
||||||
|
)
|
||||||
$
|
$
|
||||||
|
|
||||||
*Skalarni produkt* dveh vektorjev vrne skalar @wiki_dot_product; normaliziramo ga z največjim možnim produktom $n dot 255^2$:
|
=== Kosinusna podobnost
|
||||||
|
Kosinusna podobnost je mera, ki se uporablja za določanje podobnosti dveh vektorjev ne glede na njuno velikost. Izračuna kosinus kota med dvema vektorjema v n-dimenzionalnem prostoru @wiki_cosine_similarity.
|
||||||
|
|
||||||
|
==== Formula:
|
||||||
$
|
$
|
||||||
upright(bold(A)) op(dot) upright(bold(B)) & = sum_(i=1)^n A_i B_i \
|
"similarity" = frac(upright(bold(A)) op(dot) upright(bold(B)), ||upright(bold(A))|| ||upright(bold(B))||)
|
||||||
s & = frac(upright(bold(A)) op(dot) upright(bold(B)), n dot 255^2)
|
|
||||||
$
|
$
|
||||||
|
|
||||||
*Kosinusna podobnost* meri kosinus kota med vektorjema in je neodvisna od njune velikosti @wiki_cosine_similarity:
|
==== Variacija
|
||||||
$ s = frac(upright(bold(A)) op(dot) upright(bold(B)), ||upright(bold(A))|| ||upright(bold(B))||) $
|
Variaciaja, ki jo bomo testirali, je, da najprej mapiramo vrednosti genov iz (0, 255) v (-127 to 128) in s tem razširimo izhodno območje.
|
||||||
Ker so vse komponente nenegativne, je kot med vektorjema vedno majhen in mera se že pri naključnih genomih giblje visoko. Zato smo preizkusili tudi *varianto*, pri kateri vrednosti genov najprej preslikamo iz $[0, 255]$ v $[-128, 127]$ in s tem razširimo izhodno območje.
|
|
||||||
|
|
||||||
=== Množične mere
|
=== Hammingova razdalja
|
||||||
*Jaccardov indeks* meri podobnost dveh množic kot razmerje med velikostjo preseka in velikostjo unije @wiki_jaccard_index:
|
Hammingova razdalja je koncept v matematiki in računalništvu, ki meri razliko med dvema zaporedjema enake dolžine. Najpogosteje se uporablja za nize znakov, binarne zaporedja (npr. bitne nize) ali DNA zaporedja @wiki_hamming_distance.
|
||||||
$ J(A, B) = frac(|A #sym.inter B|, |A #sym.union B|) $
|
|
||||||
|
|
||||||
=== Nizovne in poravnalne mere
|
|
||||||
*Hammingova razdalja* meri število mest, na katerih se zaporedji enake dolžine razlikujeta @wiki_hamming_distance. Ker je pri nas gen en bajt, primerjamo bajte in ne posameznih bitov:
|
=== Jaccardov indeks
|
||||||
|
Jaccardov indeks (ali Jaccardov koeficient podobnosti) je metrika, ki meri podobnost med dvema množicama. Izračuna se kot razmerje med velikostjo presečišča obeh množic in velikostjo njihove unije @wiki_jaccard_index.
|
||||||
|
|
||||||
|
==== Formula:
|
||||||
$
|
$
|
||||||
d(p, q) & = |{i : p_i != q_i}| \
|
J(A, B) = frac(|A #sym.inter B|, |A #sym.union B|)
|
||||||
s & = 1 - frac(d(p, q), n)
|
|
||||||
$
|
$
|
||||||
|
|
||||||
*Levenshteinova razdalja* je najmanjše število vstavitev, izbrisov in zamenjav, potrebnih za pretvorbo enega zaporedja v drugo @wiki_levenshtein_distance. Računa se z dinamičnim programiranjem in ima časovno zahtevnost $O(n^2)$, kar je pri $n = 180$ opazno počasneje od ostalih mer.
|
=== Levenshteinova razdalja
|
||||||
|
Levenshteinova razdalja (ali urejevalna razdalja) je metrika, ki meri razliko med dvema nizoma znakov. Določa najmanjše število urejevalnih operacij (vstavitev, izbris ali zamenjava znakov), potrebnih za pretvorbo enega niza v drugega @wiki_levenshtein_distance.
|
||||||
|
|
||||||
*Needleman-Wunschev algoritem* je postopek globalne poravnave zaporedij iz bioinformatike, prav tako zasnovan na dinamičnem programiranju @wiki_needlemanwunsch_algorithm. Vsaki možni poravnavi dodeli oceno glede na izbrane nagrade za ujemanje ter kazni za neujemanje in vrzeli, rezultat pa je poravnava z najvišjo oceno.
|
==== Formula:
|
||||||
|
#let head = "head"
|
||||||
|
#let tail = "tail"
|
||||||
|
#let lev = "lev"
|
||||||
|
#let sif = "if"
|
||||||
|
$
|
||||||
|
lev(a, b) := cases(
|
||||||
|
|a| & sif |b| = 0,
|
||||||
|
|b| & sif |a| = 0,
|
||||||
|
lev(tail(a), tail(b)) & sif head(a) = head(b),
|
||||||
|
1 + min
|
||||||
|
cases(
|
||||||
|
lev(tail(a), b),
|
||||||
|
lev(a, tail(b)),
|
||||||
|
lev(tail(a), tail(b)),
|
||||||
|
) & "otherwise"
|
||||||
|
)
|
||||||
|
$
|
||||||
|
|
||||||
#align(center)[
|
Where the
|
||||||
#figure(caption: [Pregled preučenih mer])[
|
|
||||||
#table(
|
|
||||||
columns: 4,
|
|
||||||
align: (left, left, left, left),
|
|
||||||
table.header([Mera], [Družina], [Časovna zahtevnost], [Testirana]),
|
|
||||||
table.hline(),
|
|
||||||
[Evklidska razdalja], [geometrijska], [$O(n)$], [da],
|
|
||||||
[Skalarni produkt], [geometrijska], [$O(n)$], [da],
|
|
||||||
[Kosinusna podobnost], [geometrijska], [$O(n)$], [da],
|
|
||||||
[Kosinusna varianta], [geometrijska], [$O(n)$], [da],
|
|
||||||
[Jaccardov indeks], [množična], [$O(n)$], [ne],
|
|
||||||
[Hammingova razdalja], [nizovna], [$O(n)$], [da],
|
|
||||||
[Levenshteinova razdalja], [poravnalna], [$O(n^2)$], [da],
|
|
||||||
[Needleman-Wunschev alg.], [poravnalna], [$O(n^2)$], [ne],
|
|
||||||
)
|
|
||||||
]<tabela_mere>
|
|
||||||
]
|
|
||||||
|
|
||||||
Razloga, zakaj Jaccardovega indeksa in Needleman-Wunschevega algoritma nismo testirali, sta navedena v @odlocitev_mera.
|
tail(x) is a string of all but the first character of x
|
||||||
|
|
||||||
|
head(x) is first character of x
|
||||||
|
|
||||||
|
|
||||||
|
=== Needleman-Wunschev algoritem
|
||||||
|
Needleman-Wunschev algoritem je algoritem, ki se uporablja v bioinformatiki za poravnavo beljakovinskih ali nukleotidnih zaporedij. Bil je ena prvih aplikacij dinamičnega programiranja za primerjavo bioloških zaporedij. Včasih se imenuje tudi algoritem optimalnega ujemanja in tehnika globalne poravnave. Needleman-Wunschov algoritem se še vedno široko uporablja za optimalno globalno poravnavo, zlasti kadar je kakovost globalne poravnave najpomembnejša. Algoritem dodeli oceno vsakemu možnemu poravnavanju, njegov namen pa je najti vsa možna poravnavanja z najvišjo oceno @wiki_needlemanwunsch_algorithm.
|
||||||
|
|
||||||
|
#pagebreak()
|
||||||
== Testiranje algoritmov
|
== Testiranje algoritmov
|
||||||
Mere smo primerjali na sejah evolucije, shranjenih v podatkovni bazi aplikacije. Na voljo smo imeli tri seje; vsaka je bila izpeljana do dvanajste generacije, torej obsega 13 generacij (0--12) in 208 uporabnikovih ocen, skupaj 624 ocen. Vse mere so bile izračunane na istih, že posnetih generacijah, tako da so med seboj neposredno primerljive.
|
Testiranje je potekalo na več primerih evolucije. Spodaj je prikazan en primer rezultatov podobnosti in časa računanja podobnosti v mikrosekundah. Slike generacij so v prilogi.
|
||||||
// TODO: dopolniti, kdo je izvedel te tri seje (avtor / testni uporabniki) -- mentorjevo vprašanje "Kdo je testiral?"
|
|
||||||
|
|
||||||
V nadaljevanju so prikazani rezultati seje, katere slike vseh generacij so v prilogi. Za to sejo @tabela_vsecki_gen prikazuje tudi, koliko osebkov je uporabnik v posamezni generaciji označil kot všečne, saj je od tega odvisno, katera veja postopka tvorjenja nove generacije se je izvedla (@genetski_algoritem).
|
|
||||||
|
|
||||||
#align(center)[
|
|
||||||
#figure(caption: [Število všečkanih osebkov po generacijah (od 16)])[
|
|
||||||
#table(
|
|
||||||
columns: 14,
|
|
||||||
table.header([Generacija], [0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12]),
|
|
||||||
table.hline(),
|
|
||||||
[Všečkani], [4], [3], [11], [14], [12], [15], [16], [1], [2], [4], [2], [7], [11],
|
|
||||||
)
|
|
||||||
]<tabela_vsecki_gen>
|
|
||||||
]
|
|
||||||
|
|
||||||
=== Testno okolje
|
=== Testno okolje
|
||||||
Algoritmi so bili testirani na računalniku z AMD Ryzen 5 5500U procesorjem in 16 GB rama. Vsak algoritem je bil zagnan petkrat in podatki so povprečje rezultatov.
|
Algoritmi so bili testirani na računalniku z AMD Ryzen 5 5500U procesorjem in 16 GB rama. Vsak algoritem je bil zagnan petkrat in podatki so povprečje rezultatov.
|
||||||
|
|
||||||
|
// TODO: Dodaj opis testnega okolja
|
||||||
=== Rezultati podobnosti
|
=== Rezultati podobnosti
|
||||||
|
|
||||||
#align(center)[
|
#align(center)[
|
||||||
@@ -1190,8 +1237,6 @@ Algoritmi so bili testirani na računalniku z AMD Ryzen 5 5500U procesorjem in 1
|
|||||||
#let sh = (0.449075, 22.8889, 44.9491, 46.75, 46.8519, 48.463, 46.6667, 44.9213, 27.3796, 59.213, 67.3981, 81.8981, 80.7778)
|
#let sh = (0.449075, 22.8889, 44.9491, 46.75, 46.8519, 48.463, 46.6667, 44.9213, 27.3796, 59.213, 67.3981, 81.8981, 80.7778)
|
||||||
#let sl = (50.3356, 61.4676, 72.4768, 73.375, 73.4259, 74.2315, 73.3333, 72.4606, 63.6898, 79.6065, 83.6991, 90.9491, 90.3889)
|
#let sl = (50.3356, 61.4676, 72.4768, 73.375, 73.4259, 74.2315, 73.3333, 72.4606, 63.6898, 79.6065, 83.6991, 90.9491, 90.3889)
|
||||||
|
|
||||||
#align(center)[
|
|
||||||
#figure(caption: [Podobnost znotraj generacije po posameznih merah])[
|
|
||||||
#lq.diagram(
|
#lq.diagram(
|
||||||
legend: (position: (100% + .5em, 0%)),
|
legend: (position: (100% + .5em, 0%)),
|
||||||
height: 200pt,
|
height: 200pt,
|
||||||
@@ -1205,8 +1250,7 @@ Algoritmi so bili testirani na računalniku z AMD Ryzen 5 5500U procesorjem in 1
|
|||||||
lq.plot(xs, sh, mark: none, label: [Hammingova]),
|
lq.plot(xs, sh, mark: none, label: [Hammingova]),
|
||||||
lq.plot(xs, sl, mark: none, label: [Levenshteinova]),
|
lq.plot(xs, sl, mark: none, label: [Levenshteinova]),
|
||||||
)
|
)
|
||||||
]<graf_podobnost>
|
|
||||||
]
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
=== Čas računanja podobnosti v μs
|
=== Čas računanja podobnosti v μs
|
||||||
#align(center)[
|
#align(center)[
|
||||||
@@ -1248,10 +1292,6 @@ Algoritmi so bili testirani na računalniku z AMD Ryzen 5 5500U procesorjem in 1
|
|||||||
#let th = (87, 250, 105, 106, 108, 104, 106, 103, 78, 103, 81, 58, 60)
|
#let th = (87, 250, 105, 106, 108, 104, 106, 103, 78, 103, 81, 58, 60)
|
||||||
#let tl = (105799, 100331, 97438, 97529, 96296, 96456, 96510, 97253, 97409, 99816, 98978, 98289, 99663)
|
#let tl = (105799, 100331, 97438, 97529, 96296, 96456, 96510, 97253, 97409, 99816, 98978, 98289, 99663)
|
||||||
|
|
||||||
#align(center)[
|
|
||||||
#figure(caption: [
|
|
||||||
Čas računanja podobnosti. Levenshteinova razdalja je zaradi merila izpuščena iz prikaza; njen čas se giblje med 96 296 μs in 105 799 μs, torej približno tisočkrat nad ostalimi merami.
|
|
||||||
])[
|
|
||||||
#lq.diagram(
|
#lq.diagram(
|
||||||
legend: (position: (100% + .5em, 0%)),
|
legend: (position: (100% + .5em, 0%)),
|
||||||
height: 200pt,
|
height: 200pt,
|
||||||
@@ -1266,29 +1306,15 @@ Algoritmi so bili testirani na računalniku z AMD Ryzen 5 5500U procesorjem in 1
|
|||||||
lq.plot(ts, th, mark: none, label: [Hammingova]),
|
lq.plot(ts, th, mark: none, label: [Hammingova]),
|
||||||
lq.plot(ts, tl, mark: none, label: [Levenshteinova]),
|
lq.plot(ts, tl, mark: none, label: [Levenshteinova]),
|
||||||
)
|
)
|
||||||
]<graf_cas>
|
|
||||||
]
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
== Interpretacija konvergence <interpretacija_konvergence>
|
== Odločitev
|
||||||
Krivulje na @graf_podobnost se med seboj razlikujejo po zalogi vrednosti, njihova oblika pa je enaka, saj vse merijo isti pojav: kako zelo se je populacija zožila. Če jih beremo skupaj s številom všečkov po generacijah (@tabela_vsecki_gen), postane razvidno, da oblike krivulje ne določa zaporedna številka generacije, temveč uporabnikove ocene, saj te izberejo vejo postopka tvorjenja nove generacije.
|
Odločili smo se, da bomo uporabili Hammingovo razdaljo, ker nam je všeč rezultat podobnosti, ker zače pri 0 % nadaljuje proti 100 % in implementacija je enostavna. Časa računanja podobnosti nismo upoštevali, ker so bili vsi razen Levenshteinove razdalje manj kot 0.5 milisekunde in je to za nas dovolj hitro.
|
||||||
|
|
||||||
*Naraščanje v generacijah 2--6.* Uporabnik je v teh generacijah označil med 11 in 16 všečkov, zato je bilo staršev veliko, na novo ustvarjenih osebkov pa malo. Populacija se zato hitro poenoti in Hammingova podobnost naraste s približno 45 % na 48 %, kosinusna pa čez 86 %.
|
Opaziti je, da Jaccarda indeks in Needleman-Wunschev algoritem manjakta v testiranju, in to je zato, ker:
|
||||||
|
|
||||||
*Zastoj med generacijama 6 in 7.* V generaciji 6 je bilo všečkanih vseh 16 osebkov. Ker otroci prepišejo samo mesta nevšečnih osebkov, otrok sploh ni bilo in je na populacijo delovala le mutacija treh bajtov na osebek. Podobnost se med tema dvema generacijama praktično ne spremeni (85,3 % proti 85,1 % pri kosinusni meri).
|
- Jaccardov indeks ni primeren za nas, ker deluje na skupini unikatnih vrednosti, česar mi nimamo, ker imamo lahko dve isti vrednosti, ki predstavljata dve različni lastnosti slike. Zato tudi če bi dobili 100 %, ujemanje ne bi pomenilo, da se sliki 100 % ujemata.
|
||||||
|
- Nam Needleman-Wunschev alogitem ne vrne nevtralne številke, ki bi jo lahko uporabili za rezultat podobnosti.
|
||||||
*Padec pri generaciji 8.* V generaciji 7 je bil všečen samo en osebek. Postopek je zato uporabil vejo `clone`, ki polovico bajtov otroka nadomesti z naključnimi vrednostmi, in 15 od 16 osebkov naslednje generacije je nastalo na ta način. Podobnost posledično strmo pade, pri Hammingovi meri s 44,9 % na 27,4 %. Padec torej ni napaka meritve, ampak pričakovana posledica robnega primera v postopku.
|
|
||||||
|
|
||||||
*Strop pri približno 82 %.* Po generaciji 9 podobnost spet naraste in se pri generacijah 11 in 12 ustali okoli 82 % (Hammingova mera). Popolnega ujemanja postopek ne more doseči, ker se mutacija izvede tudi na všečnih osebkih: vsak osebek v vsaki generaciji dobi tri naključne bajte, zato v populaciji vedno ostane nekaj razlik.
|
|
||||||
|
|
||||||
Iz tega je razvidno, da je mera razdalje uporabna tudi kot diagnostika postopka in ne le kot številka na zaslonu; iz njenega poteka je mogoče razbrati, katera veja postopka se je izvedla in kako močno je vplivala na populacijo. Naravno nadaljevanje tega opazovanja je merjenje hitrosti konvergence pri različnih stopnjah mutacije in različnih velikostih generacije, kar navajamo med predlogi za nadaljnje delo (@zakljucek).
|
|
||||||
|
|
||||||
== Odločitev <odlocitev_mera>
|
|
||||||
Izbrali smo Hammingovo razdaljo. Ustreza vsem štirim kriterijem iz uvoda tega poglavja: računa se neposredno nad bajti genoma, njena zaloga vrednosti je celoten razpon od 0 % do 100 %, med konvergenco narašča, izračun 120 parov pa traja približno 100 μs. Odločilna je bila zaloga vrednosti: pri naključni začetni populaciji se začne pri približno 0 % in se z zoževanjem izbire pomika proti 100 %, zato je uporabniku razumljiva brez dodatne razlage. Kosinusna podobnost je za prikaz manj primerna, ker se že pri naključni populaciji začne pri 75 %, evklidska pri 59 %, skalarni produkt pa se giblje le med 25 % in 32 %. Časov računanja pri odločitvi nismo upoštevali, saj so vse mere razen Levenshteinove pod 0,5 ms, kar je za enkraten izračun ob prehodu med generacijama zanemarljivo; Levenshteinova razdalja je s približno 100 ms na generacijo za prikaz v realnem času neuporabna.
|
|
||||||
|
|
||||||
Dveh mer nismo testirali:
|
|
||||||
|
|
||||||
- *Jaccardov indeks* deluje nad množicami unikatnih vrednosti, genom pa je zaporedje, v katerem ista vrednost na dveh različnih mestih pomeni dve različni lastnosti slike. Popolno ujemanje množic zato ne bi pomenilo, da sta sliki enaki.
|
|
||||||
- *Needleman-Wunschev algoritem* vrne oceno poravnave, ki je odvisna od izbire kazni in nagrad in nima nevtralne zgornje meje, zato je ni mogoče neposredno prikazati kot odstotek.
|
|
||||||
|
|
||||||
|
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
@@ -1457,10 +1483,9 @@ V tej diplomski nalogi smo razvili aplikacijo Treender, ki uporabnikom omogoča
|
|||||||
|
|
||||||
Za razvoj smo uporabljali tehnologije, kot so C++, Raylib in SQLite3, ki so omogočile učinkovito delovanje in vizualizacijo. Ključni izziv je bila optimizacija algoritmov za generiranje in risanje dreves, ki smo ga rešili z risanjem na teksturo in omejevanjem števila risalnih klicev. Ugotovili smo, da je pomembno, da je uporabniški vmesnik intuitiven, saj so uporabniki na začetku težko razumeli osnovne funkcije, kot je ocenjevanje z vlečenjem slike.
|
Za razvoj smo uporabljali tehnologije, kot so C++, Raylib in SQLite3, ki so omogočile učinkovito delovanje in vizualizacijo. Ključni izziv je bila optimizacija algoritmov za generiranje in risanje dreves, ki smo ga rešili z risanjem na teksturo in omejevanjem števila risalnih klicev. Ugotovili smo, da je pomembno, da je uporabniški vmesnik intuitiven, saj so uporabniki na začetku težko razumeli osnovne funkcije, kot je ocenjevanje z vlečenjem slike.
|
||||||
|
|
||||||
Za merjenje podobnosti med drevesi smo izbrali Hammingovo razdaljo, ker se računa neposredno nad genomom, pokrije celoten razpon od 0 % do 100 % in je dovolj hitra za izračun ob vsaki novi generaciji. Pri tem smo ugotovili, da ta količina ne meri všečnosti, ampak konvergenco populacije, torej to, kako zelo se je uporabnikova izbira zožila. Prav zato jo je mogoče uporabiti tudi kot diagnostiko postopka: iz poteka krivulje je razvidno, katera veja tvorjenja nove generacije se je izvedla in kako močno je vplivala na populacijo. Objava aplikacije na Google Play Store je zahtevala prilagoditve opisa in politike zasebnosti, da smo ustrezali zahtevam platforme.
|
Za izračun podobnosti med drevesi smo izbrali Hammingovo razdaljo, saj je nudila enostavno implementacijo in jasne rezultate. Objava aplikacije na Google Play Store je zahtevala prilagoditve opisa in politike zasebnosti, da smo ustrezali zahtevam platforme.
|
||||||
|
|
||||||
Projekt je dosegel zastavljene cilje: uporabnikom omogoča, da s 16 potezami na generacijo usmerjajo videz fraktalnih dreves, ne da bi morali svoj okus opisati z besedami ali nastavljati parametre. Hkrati je pokazal tudi omejitve izbrane zasnove. Za nadaljnje delo se kot najbolj obetavni kažejo štirje koraki:
|
|
||||||
|
|
||||||
|
Projekt je uspešno dosegel svoje cilje: uporabnikom je omogočil ustvarjanje in prilagajanje fraktalnih dreves, hkrati pa nam je prinesel izkušnje na področju razvoja, optimizacije in uporabniške izkušnje. V prihodnosti bi lahko aplikacijo izboljšali z dodajanjem zgodovine in komunikacije podobnosti.
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
|
|
||||||
#bibliography("citations.bib", title: [VIRI IN LITERATURA], style: "ieee.csl")
|
#bibliography("citations.bib", title: [VIRI IN LITERATURA], style: "ieee.csl")
|
||||||
|
|||||||
Reference in New Issue
Block a user