## Raport z części indywidualnej - podprojektu ## Tomasz Lech ### Omówienie projektu Celem projektu jest znalezienie najbardziej optymalnej drogi między zajętymi regałami a miejscami odbioru paczki. Projekt wykorzystuje wcześniej opracowany algorytm AStar, który jest opisany w pliku [route-planning](https://git.wmi.amu.edu.pl/s444399/AI/src/master/route-planning.md). Moduł podprojektu uruchamia się po uruchomieniu programu oraz naciśnięciu **g** na klawiaturze. Omawiany moduł genetyczny podprojektu w dalszej części raportu będzie się pojawiał w skrócie jako **mdg**. ### Opis składowych elementów wykorzystanych w **mdg** * Gen - jest to najmniejszy wykorzystywany obiekt, reprezentujący zajęty regał, kóry ma określony koszt do danego miejsca odbioru paczki. * Chromosom - jest to uporządkowany zbiór Genów, który reprezentuje kolejność odbioru paczek, końcowa długość wynika z ilości paczek na magazynie. * Populacja - jest to zbiór chromosomów. * Funkcja fitness - funkcja obliczająca całkowity koszt chromosomu. * Selekcja - składowa odpowiedzialna za wybór najlepszych chromosomów z pośród populacji. * Crossover - składowa odpowiedzialna za generowanie nowej populacji uwzględniając współczynnik mutacji, wielkość dziedziczonego fragmentu oraz otrzymane podczas selekcji chromosomy. ### Dane wejściowe Podane przez użytkownika przed uruchomieniem programu: * ileGeneracji - wartość, która definiuje ile generacji ma się wykonać po uruchomieniu modułu **mdg**, * ileWPopulacji - wartość, która definiuje ile chromosomów ma się znajdować w Populacji * fragment - wartość z zakresu (0,1), która zależnie do długości chromosomu określa część, która będzie dziedziczon. przy tworzeniu nowego chromosomu. * mutacja - wartość z zakresu (0,1), która określa jaka część nowo tworzonego chromosomu, po dziedziczeniu, ma zostać losowo zmieniona. * unbox - wartość określająca do jakiego miejsca odbioru ma się kierować wózek
0 - losowe miejsce odbioru
1 - miejsce odbioru tylko po lewej stronie mapy
2 - miejsce odbioru tylko po prawej stronie mapy
3 - miejsce odbioru wybierane korzystniej na podstawie kosztu

Po uruchomieniu programu: * generowanie losowo rozmieszczonych paczek na regałach - po naciśnięciu **r** na klawiaturze. ### Integracja *W pliku program.py*
Uruchomienie **mdg**: if event.key == pygame.K_g: start(self.data,self.wheel) Po zakończeniu algorytmu, uruchamiamy moduł, który rozwiezie paczki do miejsca odbioru: for gen in self.data.best[0]: if(gen.unboxWczesniejszegoGenu == None): kordStartowy = (self.wheel.ns, self.wheel.we) else: kordStartowy = self.data.unbox[gen.unboxWczesniejszegoGenu] zbierzBox(gen,self.data, self.moves, kordStartowy) *W pliku genetyczne.py* def start(data, wheel): ileGeneracji = 20 ileWPopulacji = 16 fragment = 0.5 mutacja = 0.05 unbox = 3 data.kordyWozka = (wheel.ns, wheel.we) data.jakLiczycKoszt = unbox randomPopulation = genRandomPopulation(data, ileWPopulacji) for i in range(ileGeneracji): if i == 0: best2 = dwieNajlepsze(randomPopulation, data) else: x = genPopulacje(data,best2[0], best2[1], ileWPopulacji, fragment, mutacja) best2 = dwieNajlepsze(x, data) del x data.histZmian.append(data.best[1]) rysujWykres(data, ileGeneracji, 0, 2000) W celu modyfikacji danych wejściowych należy zmienić wartości zmiennych, pamiętając o podanych powyżej ograniczeniach. Powyżej fragment kodu reprezentujący działanie pętli, której iteracje odpowiadają tworzeniom nowych generacji. ### Sposób działania algorytmu: ```mermaid graph TD A[
Generowanie
losowego
chromosomu
] B[
Generowanie
losowej
populacji
] C[
Selekcyjny
wybór najlepszych chromosomów
z pośród populacji
] D[Generowanie nowej populacji z podanych chromosomów] E[Ilość generacji] A --> B B --> C C --> D D --> E E --> C ``` ### Implementacja #### Generowanie losowego chromosomu *W pliku Gene.py* Klasa Gene: class Gene: def __init__(self): self.kordy = None self.unbox1 = None self.unbox2 = None self.unboxWczesniejszegoGenu = None self.kordyUnboxa = None Odpowiednio: * kordy - krotka z koordynatami regału * unbox1 - koszt potrzebny do przejazdu z miejsca regału do miejsca oddania paczki po lewej stronie mapy * unbox2 - koszty potrzebny do przejazdu z miejsca regału do miejsca oddania paczki po prawej stronie mapy * unboxWczesniejszegoGenu - wartość (0 lub 1) która definiuje z jakiego miejsca oddania paczki jechał wózek do regału reprezentowanego przez ten gen * kordyUnboxa - koordynaty miejsca oddania paczki do którego będzie jechać wózek Od tego momentu miejsce oddania paczki będzie określane jako **unbox** *W pliku genetyczne.py* def generateGeny(data): geny = [] zajeteRegaly = data.zajeteRegaly[:] for r in zajeteRegaly: g = Gene() g.kordy = r g.unbox1 = policzCost(data.astarMap,r,data.unbox[0]) if(len(data.unbox) > 1): g.unbox2 = policzCost(data.astarMap,r,data.unbox[1]) geny.append(g) return geny def genRandomChromosome(data): chromosome = generateGeny(data) random.shuffle(chromosome) unboxLastGen = None for gen in chromosome: gen.unboxWczesniejszegoGenu = unboxLastGen krotkaKosztJakiUnbox = wybierzUnbox(gen, data.jakLiczycKoszt) unboxLastGen = krotkaKosztJakiUnbox[1] gen.kordyUnboxa = data.unbox[krotkaKosztJakiUnbox[1]] return chromosome Odpowiednio: * Funkcja *generateGeny* generuje oraz oblicza wartości unboxów dla danego regału oraz zwraca je jako listę genów * Funkcja *genRandomChromosome* losowo miesza wygenerowane geny oraz dla podanej wartości **unbox** (podanej przy uruchomieniu programu) zapisuje w genach wartości odpowiadające koodrynatom unboxa oraz z jakiego unboxa wózek przyjedzie. W przypadku pierwszego genu, do którego wózek będzie jechać z określonego miejsca ta wartość pozostaje *None*. Funkcja zwraca spójny chromosom. #### Generowanie Losowej populacji *W pliku genetyczne.py* def genRandomPopulation(data, ileWPopulacji): populacja = [] for i in range(ileWPopulacji): populacja.append(genRandomChromosome(data)) return populacja Odpowiednio: * Dla podanej wartości *ileWPopulacji* funkcja generuje losową populację, wykonując tyle iteracji ile wynosi wartość. #### Selekcyjny wybór najlepszych chromosomów z pośród populacji na podstawie funkcji fitness *W pliku genetyczne.py* def fitness(chromosome, data): koszt = 0 unboxPoprzedniegoGenu = None for item, gen in enumerate(chromosome): if(item == 0): koszt += policzCost(data.astarMap, data.kordyWozka, gen.kordy) krotkaKosztJakiUnbox = wybierzUnbox(gen, data.jakLiczycKoszt) koszt += krotkaKosztJakiUnbox[0] unboxPoprzedniegoGenu = krotkaKosztJakiUnbox[1] else: if unboxPoprzedniegoGenu == 0: koszt += gen.unbox1 elif unboxPoprzedniegoGenu == 1: koszt += gen.unbox2 krotkaKosztJakiUnbox = wybierzUnbox(gen, data.jakLiczycKoszt) koszt += krotkaKosztJakiUnbox[0] unboxPoprzedniegoGenu = krotkaKosztJakiUnbox[1] return koszt Odpowiednio: * Zmienna *koszt* jest sumą całkowitą kosztów przejechania trasy. * Pętla *for* iteruje się tyle razy ile jest genów w chromosomie. * W pierwszej iteracji koszt jest liczony dla pierwszego genu w chromosomie wywołując AStar, z pozycji początkowej wózka, do miejsca regału. * Dla reszty iteracji jest sprawdzane do którego unboxa będzie jechać wózek, i taka wartość kosztu jest dodawana co całkowitej sumy oraz koszt przejechania od unboxa poprzedniego genu do regału (zmienna *unboxPoprzedniegoGenu*) def dwieNajlepsze(populacja, data): tmpPopulacja = populacja[:] chromFitness = [] for chrom in populacja: chromFitness.append(fitness(chrom,data)) bestValue = min(chromFitness) bestChromIndex = chromFitness.index(bestValue) pierwsza = tmpPopulacja[bestChromIndex] if (data.best == None): data.best = (pierwsza[:],bestValue) elif(data.best[1] > bestValue): data.best = (pierwsza[:],bestValue) data.doWykresu.append(bestValue) tmpPopulacja.pop(bestChromIndex) chromFitness.pop(bestChromIndex) bestValue = min(chromFitness) bestChromIndex = chromFitness.index(bestValue) druga = tmpPopulacja[bestChromIndex] tmpPopulacja.pop(bestChromIndex) chromFitness.pop(bestChromIndex) return (pierwsza, druga) Funkcja selekcji dla której odpowiednio: * W pierwszej pętli *for* tworzy się lista *chromFitness* przetrzymująca wartości kosztów dla danego chromosomu. Wartości w *chromFitness* odpowiadają chromosomom na tych samych indeksach w liście populacja. * Zmienna *bestValue* reprezentuje najlepszy koszt z danej populacji * Zmienna *pierwsza* reprezentuje chromosom o najkorzystniejszym koszcie. * Zmienna *druga* reprezentuje chromosom o drugim co do wartości najkorzystniejszym koszcie. * W zmiennej *best* klasy obiektu *data* zapisywana jest krotka odpowiednio (chromosom,koszt) najlepszego chromosomu. * Funkcja zwraca krotkę z dwoma najlepszymi chromosomami w populacji. #### Generowanie nowej populacji - Crossover *W pliku genetyczne.py* def crossover(data,pierwszy, drugi, fragmentLiczba, wspMutacji): ileWChrom = len(pierwszy) tmp = random.randint(0, ileWChrom-fragmentLiczba) kordyFragment = (tmp,tmp+fragmentLiczba) nowyChrom = [Gene() for q in range(ileWChrom)] iterator = kordyFragment[1] pomIterator = kordyFragment[1] usedKordy = [] for i in range(kordyFragment[0],kordyFragment[1]): nowyChrom[i].kordy = pierwszy[i].kordy nowyChrom[i].unbox1 = pierwszy[i].unbox1 nowyChrom[i].unbox2 = pierwszy[i].unbox2 usedKordy.append(pierwszy[i].kordy) for x in range(ileWChrom): if(iterator > ileWChrom - 1): iterator = 0 if(pomIterator > ileWChrom - 1): pomIterator = 0 if(nowyChrom[iterator].kordy == None and drugi[pomIterator].kordy not in usedKordy): nowyChrom[iterator].kordy = drugi[pomIterator].kordy nowyChrom[iterator].kordy = drugi[pomIterator].kordy nowyChrom[iterator].unbox1 = drugi[pomIterator].unbox1 nowyChrom[iterator].unbox2 = drugi[pomIterator].unbox2 iterator += 1 pomIterator += 1 else: pomIterator +=1 nowyChrom = mutate(wspMutacji, nowyChrom) unboxLastGen = None for gen in nowyChrom: gen.unboxWczesniejszegoGenu = unboxLastGen krotkaKosztJakiUnbox = wybierzUnbox(gen, data.jakLiczycKoszt) unboxLastGen = krotkaKosztJakiUnbox[1] gen.kordyUnboxa = data.unbox[krotkaKosztJakiUnbox[1]] return nowyChrom Odpowiednio: * Dane wejściowe są to: * *pierwszy*, *drugi* - wybrane najkorzystniejsze chromosomy, z których ma powstać nowy chromosom * *fragmentLiczba* - jest to liczba reprezentująca jaki fragment z **pierwszego** chromosomu zostanie bezpośrednio skopiowany do nowego chromosomu, ten fragment jest wybierany losowo spośród chromosomu natomiast jego długość jest określona procentowo i zależy od podanej wartości (oraz ilości genów w chromosomoie) * *wspMutacji* - jest to liczba reprezentująca jak wiele par w chromosomie zostanie zamienionych miejscami. * Zmienne pomocnicze: * *iterator*, *pomIterator* - w pierwszych dwóch instrukcjach warunkowych jest pilnowane aby iterując się nie przekroczyły dopuszczalnej wartości (odpowiadają one indeksom w kolejce). *Iterator* jest indeksem w nowym, tworzonym chromosomie. *pomIterator* jest indeksem który przechodzi przez **drugi** podany chromosom. * lista *usedKordy* - do niej są dodawane koordynaty genów, które zostały skopiowane z **pierwszego** chromosomu, aby geny o tych samych koordynatach z **drugiego** chromosomu nie zostały zapisane w nowym chromosomie. * Następuje skopiowanie fagmentu z **pierwszego** chromosomu, w pierwszej pętli *for* wykonuje się przepisanie wartości do powstającego chromosomu. W drugiej pętli *for* następuje przepisanie pozostałych wartości z **drugiego** chromosomu do powstającego chromosomu. * Po przepisaniu wartości według wspMutacji jest dokonywana zamiana genów w nowym chromosomie. * Ostatnia pętla **for** łączy geny ze sobą (zapisując unbox poprzedniego genu) *W pliku genetyczne.py* def genPopulacje(data,pierwszy, drugi, ileWPopulacji, fragmentLiczba, wspMutacji): ileWChrom = len(pierwszy) fragment = round(fragmentLiczba*ileWChrom) if(fragment == 1): fragment +=1 nowaPopulacja = [] for i in range(ileWPopulacji): nowaPopulacja.append(crossover(data,pierwszy,drugi,fragment, wspMutacji)) return nowaPopulacja Odpowiednio: * W pętli *for* tworzone są nowe chromosomy z **pierwszego** oraz **drugiego** najlepszego chromosomu z poprzedniej generacji. * Nowe chromosomy zapisywane są do *nowaPopulacja* * Z powstałej populacji na nowo selekcjonowane są dwa najlepsze, z których będą powstawać nowe populacje w zależności od wartości podanych generacji. ### Dalsze działanie programu Po zakończeniu **mdg** uruchamia się okienko pokazujące wykres najlepszych kosztów otrzymywanych w danej populacji. Po zamknięciu okienka wózek zaczyna rozwozić paczki do miejsc oddania paczki.