├── README.md ├── requirements.txt ├── screen.png └── src ├── __init__.py ├── app.py ├── display.py ├── entities ├── __init__.py ├── cell.py └── domain_events.py └── simulation.py /README.md: -------------------------------------------------------------------------------- 1 | # Genetic algorithms simulation 2 | 3 | Installation 4 | ------------ 5 | Dependencies: 6 | 7 | pip3 install -r requirements.txt 8 | Run: 9 | 10 | python3 sim.py 11 | 12 | Description 13 | ------------ 14 | This is a simulation of a genetic algorithm 15 | The population is represented as a group of cells 16 | The cell's dna has the following properties: 17 | 18 | - Radius (the size of the cell) 19 | - vision for food (distance from which a cell can detect food) 20 | - vision for poison (distance from which a cell can detect poison) 21 | - desire for food (attraction towards food (litterally)) 22 | - desire for poison (attraction towards poison) 23 | - move noise (distraction on the movement of the cell) 24 | - age (for recording purposes) 25 | 26 | Screenshot 27 | ------------ 28 | ![alt text](screen.png) 29 | 30 | The algorithm will converge to a solution that will be the most fit to stay alive: Eats enough food and avoids poison as possible. 31 | The attribute 'age' is used to detect how long a generation lasts. 32 | If a generation lives for too long, it is considered a solution. 33 | When a cell lives longer than 10000 generations, it is considered as a candidate solution, and it is printed to the console 34 | (You can redirect that to a file, and feed the value when starting the algorithm again fresh, which probably will yield better solutions) 35 | 36 | Note: 37 | ------------ 38 | The idea is not original. But the implementation is mine. 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.22.0 2 | pygame==1.9.6 3 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maa-ddy/genetic-algorithm-simulation/97e2646f3e998f85dea626bb1367cdfc52ef20ea/screen.png -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maa-ddy/genetic-algorithm-simulation/97e2646f3e998f85dea626bb1367cdfc52ef20ea/src/__init__.py -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | from simulation import Simulation 5 | from display import Gui 6 | 7 | 8 | 9 | 10 | class App(): 11 | def __init__(self): 12 | self.simulation = Simulation() 13 | self._init_pygame_attributes(self.simulation) 14 | self.gui = Gui(self.canvas, self.simulation) 15 | self.running = True 16 | 17 | def _init_pygame_attributes(self, simulation): 18 | dimensions = simulation.get_dimensions() 19 | self.fps = 60 20 | self.canvas = pygame.display.set_mode(dimensions) 21 | self.clock = pygame.time.Clock() 22 | 23 | def _tick(self): 24 | self.simulation.update() 25 | self.clock.tick(self.fps) 26 | for event in pygame.event.get(): 27 | if event.type == pygame.QUIT: 28 | running = False 29 | 30 | def _render(self): 31 | self.gui.clear() 32 | self.gui.render() 33 | 34 | def run(self): 35 | while self.running: 36 | self._render() 37 | self._tick() 38 | 39 | pygame.quit() 40 | quit() 41 | 42 | 43 | if __name__ == '__main__': 44 | App().run() 45 | -------------------------------------------------------------------------------- /src/display.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from entities.cell import Cell 3 | import numpy 4 | 5 | class Gui(): 6 | 7 | black = (0,0,0) 8 | 9 | def __init__(self, canvas, simulation): 10 | self.canvas = canvas 11 | self.simulation = simulation 12 | 13 | def _build_display_objs(self): 14 | cells = [DisplayCell(self.canvas, cell) for cell in self.simulation.population.values()] 15 | food = [DisplayFood(self.canvas).with_pos(food) for food in self.simulation.food] 16 | poison = [DisplayPoison(self.canvas).with_pos(poison) for poison in self.simulation.poison] 17 | return cells + food + poison 18 | 19 | def clear(self): 20 | self.canvas.fill(Gui.black) 21 | 22 | def render(self): 23 | self.clear() 24 | for display_obj in self._build_display_objs(): 25 | display_obj.render() 26 | pygame.display.update() 27 | 28 | 29 | class DisplayObj(): 30 | 31 | cyan = (66, 244, 176) 32 | green = (90, 240, 40) 33 | orange = (240, 110, 40) 34 | blue = (7, 90, 180) 35 | red = (220, 10, 40) 36 | 37 | def __init__(self, canvas): 38 | self.canvas = canvas 39 | 40 | def with_id(self, display_obj_id): 41 | self.id = display_obj_id 42 | 43 | def with_pos(self, pos): 44 | self.pos = pos 45 | return self 46 | 47 | def render(self): 48 | print("can't render abstract display obj") 49 | 50 | class DisplayCell(DisplayObj): 51 | 52 | def __init__(self, canvas, cell): 53 | self.cell = cell 54 | self.radius = cell.radius 55 | self.pos = cell.pos 56 | self.color = DisplayObj.cyan 57 | super().__init__(canvas) 58 | 59 | def _get_color(self): 60 | r, g, b = self.color 61 | saturation = self.cell.health / Cell.max_health 62 | return tuple(map(int, ( 63 | r * saturation, 64 | g * saturation, 65 | b * saturation 66 | ))) 67 | 68 | def render(self): 69 | pygame.draw.circle(self.canvas, self._get_color(), self.cell.pos, self.radius) 70 | pygame.draw.circle(self.canvas, DisplayObj.green, self.cell.pos, self.cell.vision_for_food, 2) 71 | pygame.draw.circle(self.canvas, DisplayObj.orange, self.cell.pos, self.cell.vision_for_poison, 2) 72 | pygame.draw.line(self.canvas, DisplayObj.blue, self.cell.pos, self.cell.pos + self.cell.speed * self.cell.desire_for_food, 2) 73 | pygame.draw.line(self.canvas, DisplayObj.red, self.cell.pos, self.cell.pos + self.cell.speed * self.cell.desire_for_poison, 2) 74 | 75 | class DisplayFood(DisplayObj): 76 | def __init__(self, canvas): 77 | self.radius = 5 78 | self.color = DisplayObj.green 79 | super().__init__(canvas) 80 | 81 | def render(self): 82 | pygame.draw.circle(self.canvas, self.color, self.pos, 5, 2) 83 | 84 | class DisplayPoison(DisplayObj): 85 | def __init__(self, canvas): 86 | self.color = DisplayObj.orange 87 | self.radius = 5 88 | super().__init__(canvas) 89 | 90 | def render(self): 91 | pygame.draw.circle(self.canvas, self.color, self.pos, 5) -------------------------------------------------------------------------------- /src/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maa-ddy/genetic-algorithm-simulation/97e2646f3e998f85dea626bb1367cdfc52ef20ea/src/entities/__init__.py -------------------------------------------------------------------------------- /src/entities/cell.py: -------------------------------------------------------------------------------- 1 | from .domain_events import DomainEventQueue, NewbornCellEvent, CellDeathEvent 2 | from random import random, uniform 3 | from math import sqrt 4 | import numpy 5 | 6 | default_noise = [0,0] 7 | default_vision_for_food = 100 8 | default_vision_for_poison = 50 9 | default_desire_for_food = 5 10 | default_desire_for_poison = -2 11 | default_dna = [10, default_noise[:], default_vision_for_food, 30, default_desire_for_food, 0, 1] 12 | 13 | def random_dna(): 14 | return [ 15 | int(random() * Cell.max_radius) + 4, 16 | [uniform(-2, 2), uniform(-2, 2)], 17 | int(random() * default_vision_for_food) + 5, 18 | int(random() * default_vision_for_poison) + 5, 19 | int(uniform(-default_desire_for_food, default_desire_for_food)) + 1, 20 | int(uniform(-default_desire_for_poison, default_desire_for_poison)) - 1, 21 | 1 22 | ] 23 | 24 | def sign(a): 25 | return 1 if a > 0 else -1 26 | 27 | class Cell(): 28 | 29 | max_health = 200 30 | max_radius = 40 31 | reproduction_rate = 0.001 32 | mutation_radius_range = 0.5 33 | mutation_vision_range = 2 34 | mutation_desire_range = 0.05 35 | 36 | INF = 1000 37 | 38 | def __init__(self, x, y, dna=default_dna): 39 | self.pos = numpy.array([x,y]) 40 | self.target = numpy.array([0,0]) 41 | self.escape = numpy.array([0,0]) 42 | self.random_target = [random() * Cell.INF, random() * Cell.INF] 43 | self.health = Cell.max_health 44 | self.speed = numpy.array([0,0]) 45 | self.dna = dna[:] 46 | self.radius, self.move_noise, self.vision_for_food, self.vision_for_poison, self.desire_for_food, self.desire_for_poison, self.age = dna 47 | 48 | DomainEventQueue.get_instance().push(NewbornCellEvent(self)) 49 | 50 | def selection(self): 51 | if random() < Cell.reproduction_rate: 52 | self.dna[-1] = self.age 53 | child_dna = self.crossover() 54 | child_dna = self.mutation(child_dna) 55 | child_pos = self.pos - self.move_noise 56 | return Cell(*child_pos, child_dna) 57 | 58 | 59 | def crossover(self): 60 | return self.dna[:] 61 | 62 | def mutation(self, propagated_dna): 63 | start = int(random()*6) 64 | end = int(random()*6) 65 | start, end = min(start, end) , max(start, end) 66 | dna = propagated_dna[:] 67 | for idx in range(start, end+1): 68 | if idx == 0: 69 | dna[0] = min(max(4, dna[0] + int(random() * Cell.mutation_radius_range)), Cell.max_radius) 70 | elif idx == 1: 71 | dna[1] = [dna[1][0]+uniform(-0.5,0.5), dna[1][1]*uniform(-0.5,0.5)] 72 | elif idx == 2: 73 | dna[2] = min(max(2, dna[2] + int(random() * Cell.mutation_vision_range)), default_vision_for_food * 2) 74 | elif idx == 3: 75 | dna[3] = min(max(2, dna[3] + int(random() * Cell.mutation_vision_range)), default_vision_for_poison * 2) 76 | elif idx == 4: 77 | dna[4] = min( 78 | max( 79 | -default_desire_for_food*2, 80 | dna[4] + int(uniform(-default_desire_for_food, default_desire_for_food) * Cell.mutation_desire_range) 81 | ), 82 | default_desire_for_food * 2 83 | ) 84 | elif idx == 5: 85 | dna[5] = min( 86 | max( 87 | -default_desire_for_poison * 2, 88 | dna[5] + int(uniform(-default_desire_for_poison, default_desire_for_poison) * Cell.mutation_desire_range) 89 | ), default_desire_for_poison * 2 90 | ) 91 | return dna 92 | 93 | def dist(self, point): 94 | return sqrt(sum(e*e for e in self.pos - point)) 95 | 96 | def seek_food(self, meals, poison): 97 | #target = numpy.array([max(width, height),max(width, height)]) 98 | #escape = numpy.array([max(width, height),max(width, height)]) 99 | target = numpy.array([Cell.INF, Cell.INF]) 100 | escape = numpy.array([Cell.INF, Cell.INF]) 101 | for meal in meals: 102 | if self.dist(meal) < self.radius + 5: 103 | meals.pop(meals.index(meal)) 104 | self.health += 20 105 | self.health = min(self.health, Cell.max_health) 106 | elif self.dist(meal) < self.dist(target): 107 | target = meal 108 | for p in poison: 109 | if self.dist(p) < self.radius + 5: 110 | poison.pop(poison.index(p)) 111 | self.health -= 80 112 | self.health = max(self.health, 0) 113 | elif self.dist(p) < self.dist(escape): 114 | escape = p 115 | if self.dist(target) > self.vision_for_food or self.dist(target) == self.dist(numpy.array([Cell.INF, Cell.INF])): 116 | self.target = None 117 | else: 118 | self.target = target 119 | if self.dist(escape) > self.vision_for_poison or self.dist(escape) == self.dist(numpy.array([Cell.INF, Cell.INF])): 120 | self.escape = None 121 | else: 122 | self.escape = escape 123 | 124 | 125 | 126 | def apply_force(self, intensity, pos): 127 | steering = pos - self.pos 128 | if abs(steering[0]) < abs(steering[1]) : 129 | steering = [ 130 | intensity * (sign(steering[0]) * abs(steering[0]/steering[1])), 131 | intensity*sign(steering[1]) 132 | ] 133 | else: 134 | steering = [ 135 | intensity*sign(steering[0]) + self.move_noise[0], 136 | intensity * sign(steering[1]) * abs(steering[1]/steering[0]) + self.move_noise[1] 137 | ] 138 | steering = list(map(int,steering)) 139 | self.speed = numpy.array(steering) 140 | 141 | def chase(self): 142 | if self.target is None: 143 | self.apply_force(self.desire_for_food, self.random_target) 144 | if self.dist(self.random_target) < 5: 145 | self.random_target = [10 + random()*(Cell.INF - 10), 10 + random() * (Cell.INF - 10)] 146 | else: 147 | self.random_target = [random() * Cell.INF, random() * Cell.INF] 148 | if self.dist(self.target) < self.radius and self.target in food: 149 | food.pop(food.index(self.target)) 150 | self.apply_force(self.desire_for_food,self.target) 151 | self.pos += self.speed 152 | if self.escape is not None: 153 | self.apply_force(self.desire_for_poison, self.escape) 154 | 155 | def move(self): 156 | if (self.speed & numpy.array([0,0])).all(): 157 | self.pos += self.move_noise 158 | self.pos += self.speed 159 | 160 | def live(self): 161 | self.age += 1 162 | self.health -= 1 163 | if self.health <= 0: 164 | self.die() 165 | 166 | def update(self, food, poison): 167 | self.seek_food(food, poison) 168 | self.chase() 169 | self.move() 170 | self.live() 171 | self.selection() 172 | if self.age > 10000: 173 | print("candidate :",self.dna) 174 | 175 | def die(self): 176 | DomainEventQueue.get_instance().push(CellDeathEvent(self)) 177 | -------------------------------------------------------------------------------- /src/entities/domain_events.py: -------------------------------------------------------------------------------- 1 | 2 | class DomainEventQueue(): 3 | 4 | singleton_instance = None 5 | 6 | def __init__(self): 7 | pass 8 | 9 | def get_instance(): 10 | if DomainEventQueue.singleton_instance is None: 11 | DomainEventQueue.singleton_instance = DomainEventQueue().__init_instance() 12 | return DomainEventQueue.singleton_instance 13 | 14 | def __init_instance(self): 15 | self.queue = [] 16 | self.subscribers = [] 17 | return self 18 | 19 | def subscribe(self, sub): 20 | self.subscribers.append(sub) 21 | 22 | def unsubscribe(self, sub): 23 | self.subscribers.remove(sub) 24 | 25 | def push(self, event): 26 | for sub in self.subscribers: 27 | sub.notify(event) 28 | self.queue.append(event) 29 | 30 | def clear(self): 31 | self.queue = [] 32 | 33 | def events_history(self): 34 | return self.queue[:] 35 | 36 | 37 | class NewbornCellEvent(): 38 | def __init__(self, cell): 39 | self.data = cell 40 | 41 | class CellDeathEvent(): 42 | def __init__(self, cell): 43 | self.data = cell -------------------------------------------------------------------------------- /src/simulation.py: -------------------------------------------------------------------------------- 1 | from entities.domain_events import DomainEventQueue, CellDeathEvent, NewbornCellEvent 2 | from entities.cell import Cell, random_dna 3 | from random import random, uniform 4 | 5 | class Simulation(): 6 | max_food = 100 7 | poison_generation_rate = 0.1 8 | max_population = 20 9 | 10 | default_width = 1500 11 | default_height = 1200 12 | 13 | def __init__(self): 14 | self.width = Simulation.default_width 15 | self.height = Simulation.default_height 16 | self.food = [] 17 | self.poison = [] 18 | self.population = {} 19 | self.event_dispatcher = EventDispatcher(self) 20 | self.init_food(count=30) 21 | self.init_population(count=10) 22 | 23 | def get_dimensions(self): 24 | return (self.width, self.height) 25 | 26 | def init_food(self, count=30): 27 | for k in range(count): 28 | self.generate_food() 29 | 30 | def init_population(self, count=10): 31 | for k in range(count): 32 | x, y = list(map(int,[20 + random() * (self.width - 20) , 20 + random() * (self.height - 20)])) 33 | Cell(x,y) 34 | 35 | def generate_food(self): 36 | if len(self.food) <= Simulation.max_food: 37 | random_pos = tuple(map(int, (10 + random() * (self.width - 10), 10 + random() * (self.height - 10)))) 38 | if random() < Simulation.poison_generation_rate: 39 | self.poison.append(random_pos) 40 | else: 41 | self.food.append(random_pos) 42 | 43 | def revive_population(self): 44 | if len(self.population) < 4 or random() < 0.001: 45 | random_pos = tuple(map(int, (10 + random() * (self.height - 10), 10 + random() * (self.width - 10)))) 46 | Cell(*random_pos, random_dna()) 47 | 48 | def update(self): 49 | population_copy = self.population.copy().items() 50 | for cell_id, cell in population_copy: 51 | cell.update(self.food, self.poison) 52 | if self.cell_is_out_of_map(cell): 53 | cell.die() 54 | self.generate_food() 55 | self.revive_population() 56 | 57 | def cell_is_out_of_map(self, cell): 58 | x, y = cell.pos 59 | return x < 0 or x > self.width or y < 0 or y > self.height 60 | 61 | def newborn(self, cell): 62 | if len(self.population) < Simulation.max_population: 63 | self.population[id(cell)] = cell 64 | 65 | def death(self, cell): 66 | self.population.pop(id(cell)) 67 | 68 | class EventDispatcher(): 69 | 70 | def __init__(self, simulation): 71 | DomainEventQueue.get_instance().subscribe(self) 72 | self.simulation = simulation 73 | 74 | def notify(self, event): 75 | if type(event) is NewbornCellEvent: 76 | self.simulation.newborn(event.data) 77 | elif type(event) is CellDeathEvent: 78 | self.simulation.death(event.data) 79 | --------------------------------------------------------------------------------