From 3db4b9be9b394f1ef297da54bcdec60c12adc464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20St=C3=B8ren?= Date: Thu, 24 Oct 2024 18:37:43 +0200 Subject: [PATCH 1/9] feat: add scrollable_item_list --- gui_with_pygame.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/gui_with_pygame.py b/gui_with_pygame.py index 17148cf..ccd7ebf 100644 --- a/gui_with_pygame.py +++ b/gui_with_pygame.py @@ -143,6 +143,7 @@ def __init__(self): self.input_field_active_bg = (58, 58, 58) pygame.font.init() self.font = pygame.font.Font(None, 36) + class SelectableListItem: """Represents a genome item that can be selected, with a checkbox.""" def __init__(self, x, y, width, height, genome_id, fitness, font, text_color, bg_color, selected_color, checkbox_size=20): @@ -179,6 +180,69 @@ def handle_event(self, event): self.selected = not self.selected +class ScrollableList: + def __init__(self, x, y, width, height, items, font, text_color, bg_color, selected_color, visible_count=10, padding=10): + self.x = x + self.y = y + self.width = width + self.height = height + self.items = items + self.font = font + self.text_color = text_color + self.bg_color = bg_color + self.selected_color = selected_color + self.visible_count = visible_count + self.scroll_offset = 0 # This tracks where we are in the list + self.padding = padding # Distance between items + # Create a base list of items without positioning them + print(items[0]) + self.list_items = [ + SelectableListItem( + x, y, width, (height - (visible_count - 1) * padding) // visible_count, # Adjust height to account for padding + + genome_id=item[1], fitness=item[0].fitness_value, font=font, text_color=text_color, + bg_color=bg_color, selected_color=selected_color + ) for item in items + ] + + def draw(self, screen): + # Draw only the visible items, adjusting their position based on scroll_offset and adding padding + for i in range(self.scroll_offset, min(self.scroll_offset + self.visible_count, len(self.list_items))): + list_item = self.list_items[i] + # Adjust y position with padding + list_item.rect.y = self.y + (i - self.scroll_offset) * (list_item.rect.height + self.padding) + list_item.checkbox_rect.y = list_item.rect.y + (list_item.rect.height - list_item.checkbox_size) // 2 + list_item.draw(screen) + + def scroll(self, direction): + # Scroll up (scroll_offset decreases) + if direction == "up" and self.scroll_offset > 0: + self.scroll_offset -= 1 + # Scroll down (scroll_offset increases) + elif direction == "down" and self.scroll_offset < len(self.list_items) - self.visible_count: + self.scroll_offset += 1 + + def handle_event(self, event): + # Pass the event to all visible list items + for i in range(self.scroll_offset, min(self.scroll_offset + self.visible_count, len(self.list_items))): + list_item = self.list_items[i] + list_item.handle_event(event) + + # Handle scrolling with arrow keys + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_UP: + self.scroll("up") + elif event.key == pygame.K_DOWN: + self.scroll("down") + + # Handle scrolling with mouse wheel + if event.type == pygame.MOUSEWHEEL: + if event.y > 0: + self.scroll("up") + elif event.y < 0: + self.scroll("down") + + class GenomeViewer: def __init__(self, genomes, font, text_color, bg_color, selected_color): self.genomes = genomes @@ -462,6 +526,8 @@ def __init__(self): self.show_visualization_button = Button(100, 350, 200, 50, "Show Visualization", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.visualize_genomes) self.get_which_frames_to_show_input = InputField(400, 350, 150, 50, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, initial_text="0") + self.scrollable_list = ScrollableList(50, 50, 500, 500, self.genomes, st.font, (255, 255, 255), (50, 50, 50), (0, 100, 255), visible_count=10, padding=10) + def event_handler(self): for event in pygame.event.get(): @@ -476,6 +542,7 @@ def event_handler(self): input_field.handle_event(event) if st.sc_selector == 3: self.genome_viewer.handle_event(event) + self.scrollable_list.handle_event(event) if st.sc_selector == 4: self.get_which_frames_to_show_input.handle_event(event) @@ -515,6 +582,7 @@ def watch_genome_scene(self): # Draw the genome viewer list self.genome_viewer.draw(self.screen) + self.scrollable_list.draw(self.screen) # Draw the buttons self.run_button.draw(self.screen) From 8ede068ccd308d185e8145dc1e30104e08a39bbd Mon Sep 17 00:00:00 2001 From: Christian Fredrik Johnsen Date: Thu, 24 Oct 2024 18:50:29 +0200 Subject: [PATCH 2/9] refactor: general improvements in main.py --- main.py | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/main.py b/main.py index 35ca2ea..56c84fb 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ -from genericpath import exists from src.environments.debug_env import env_debug_init, run_game_debug from src.utils.config import Config from src.genetics.NEAT import NEAT @@ -19,10 +18,7 @@ def play_genome(args): from_gen = 0 test_genome(from_gen, to_gen) - if args.generation is not None: - generation_num = args.generation - else: - generation_num = -1 + generation_num = args.generation if args.generation is not None else -1 genome = load_best_genome(generation_num) env, state = env_debug_init() run_game_debug(env, state, genome, 0, visualize=False) @@ -59,21 +55,18 @@ def main(args): neat_name = "latest" neat = load_neat(neat_name) - config_instance = Config() - if neat is None: - exists = False + if neat is not None: # TODO: Add option to insert new config into NEAT object. generation_nums, best_fitnesses, avg_fitnesses, min_fitnesses = get_fitnesses_from_file("fitness_values") print(f"Generation numbs: {generation_nums}") from_generation = generation_nums[-1] + 1 print(f"From generation: {from_generation}") #config_instance = neat.config else: - exists = True - neat = NEAT(config_instance) + neat = NEAT(Config()) neat.initiate_genomes() from_generation = 0 - generations = (config_instance.generations) if args.n_generations == -1 else args.n_generations + generations = (neat.config.generations) if args.n_generations == -1 else args.n_generations print(f"Training from generation {from_generation} to generation {from_generation + generations}") @@ -89,12 +82,7 @@ def main(args): save_fitness(best_fitnesses, avg_fitnesses, min_fitnesses) neat.calculate_number_of_children_of_species() - new_genomes_list = [] - for specie in neat.species: - new_genomes_list.append(neat.generate_offspring(specie)) - - flattened_genomes = [genome for sublist in new_genomes_list for genome in sublist] - neat.genomes = flattened_genomes + neat.genomes = [genome for specie in neat.species for genome in neat.generate_offspring(specie)] # Generate offspring in each specie print(f"new generation size: {len(neat.genomes)}" ) @@ -104,16 +92,13 @@ def main(args): save_fitness_data() except KeyboardInterrupt: print("\nProcess interrupted! Saving fitness data...") - finally: - # Always save fitness data before exiting, whether interrupted or completed - save_fitness(best_fitnesses, avg_fitnesses, min_fitnesses, exists) - save_neat(neat, "latest") + finally: # Always save fitness data before exiting, whether interrupted or completed + save_fitness(best_fitnesses, avg_fitnesses, min_fitnesses) + save_neat(neat, args.neat_name) print("Fitness data saved.") profiler.disable() - - # Create a stats object to print out profiling results - stats = pstats.Stats(profiler).sort_stats('cumtime') + stats = pstats.Stats(profiler).sort_stats('cumtime') # Create a stats object to print out profiling results stats.print_stats() return neat.genomes @@ -147,5 +132,4 @@ def command_line_interface(): parser.print_help() if __name__ == "__main__": - # main() - command_line_interface() \ No newline at end of file + command_line_interface() From 416f9dad022492286a42d88fdb201b8a2e1ce653 Mon Sep 17 00:00:00 2001 From: Christian Fredrik Johnsen Date: Thu, 24 Oct 2024 18:50:53 +0200 Subject: [PATCH 3/9] docs: add documentation to load_best_genome. --- src/utils/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.py b/src/utils/utils.py index f1cf860..9e1e901 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -58,7 +58,8 @@ def save_best_genome(genome: Genome, generation: int): pickle.dump(genome, f) # type: ignore def load_best_genome(generation: int): - if generation == -1: + """Loads the best genome from the given generation. If -1 is passed as argument, the latest generation is displayed.""" + if generation == -1: # Find the genome from the latest generation. files = os.listdir('data/good_genomes') pattern = re.compile(r'best_genome_(\d+).obj') generations = [] From 3f5852957c7284b04c539e668cdf8b8e7e1a8883 Mon Sep 17 00:00:00 2001 From: BrageNTNU Date: Thu, 24 Oct 2024 18:53:32 +0200 Subject: [PATCH 4/9] fix: fixed filenames --- main.py | 25 +++++++++++---------- src/genetics/NEAT.py | 19 +++++++++++++--- src/utils/config.py | 6 ++--- src/utils/utils.py | 52 ++++++++++++++++++++++---------------------- 4 files changed, 58 insertions(+), 44 deletions(-) diff --git a/main.py b/main.py index 35ca2ea..f24a833 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ from src.environments.debug_env import env_debug_init, run_game_debug from src.utils.config import Config from src.genetics.NEAT import NEAT -from src.utils.utils import save_fitness, save_best_genome, load_best_genome, save_neat, load_neat, save_fitness_data, get_fitnesses_from_file +from src.utils.utils import read_fitness_file, save_fitness, save_best_genome, load_best_genome, save_neat, load_neat, save_fitness_data import warnings import cProfile import pstats @@ -35,11 +35,11 @@ def test_genome(from_gen: int, to_gen: int): fitness = run_game_debug(env, state, genome, i) print(fitness) -def collect_fitnesses(genomes, generation, min_fitnesses, avg_fitnesses, best_fitnesses): +def collect_fitnesses(genomes, generation, min_fitnesses, avg_fitnesses, best_fitnesses, neat_name): fitnesses = [genome.fitness_value for genome in genomes] best_genome = max(genomes, key=lambda genome: genome.fitness_value) - save_best_genome(best_genome, generation) + save_best_genome(best_genome, generation, neat_name) min_fitness, avg_fitness, max_fitness = min(fitnesses), sum(fitnesses) / len(fitnesses), max(fitnesses) best_fitnesses.append(max_fitness) @@ -60,9 +60,9 @@ def main(args): neat = load_neat(neat_name) config_instance = Config() - if neat is None: + if neat is not None: exists = False - generation_nums, best_fitnesses, avg_fitnesses, min_fitnesses = get_fitnesses_from_file("fitness_values") + generation_nums, best_fitnesses, avg_fitnesses, min_fitnesses = read_fitness_file(neat_name) print(f"Generation numbs: {generation_nums}") from_generation = generation_nums[-1] + 1 print(f"From generation: {from_generation}") @@ -80,15 +80,16 @@ def main(args): try: for generation in range(from_generation, from_generation + generations): neat.train_genomes() - collect_fitnesses(neat.genomes, generation, min_fitnesses, avg_fitnesses, best_fitnesses) + collect_fitnesses(neat.genomes, generation, min_fitnesses, avg_fitnesses, best_fitnesses, neat_name) neat.sort_species(neat.genomes) neat.check_population_improvements() neat.check_individual_impovements() # Check if the species are improving, remove the ones that are not after 15 generations neat.adjust_fitness() - save_fitness(best_fitnesses, avg_fitnesses, min_fitnesses) + save_fitness(best_fitnesses, avg_fitnesses, min_fitnesses, neat_name) neat.calculate_number_of_children_of_species() + new_genomes_list = [] for specie in neat.species: new_genomes_list.append(neat.generate_offspring(specie)) @@ -101,20 +102,20 @@ def main(args): for genome in neat.genomes: if not genome.elite: neat.add_mutation(genome) - save_fitness_data() + save_fitness_data(neat_name) except KeyboardInterrupt: print("\nProcess interrupted! Saving fitness data...") finally: # Always save fitness data before exiting, whether interrupted or completed - save_fitness(best_fitnesses, avg_fitnesses, min_fitnesses, exists) - save_neat(neat, "latest") + save_fitness(best_fitnesses, avg_fitnesses, min_fitnesses, neat_name) + save_neat(neat, neat_name) print("Fitness data saved.") profiler.disable() # Create a stats object to print out profiling results - stats = pstats.Stats(profiler).sort_stats('cumtime') - stats.print_stats() + #stats = pstats.Stats(profiler).sort_stats('cumtime') + #stats.print_stats() return neat.genomes diff --git a/src/genetics/NEAT.py b/src/genetics/NEAT.py index 95f4128..bf45ec6 100644 --- a/src/genetics/NEAT.py +++ b/src/genetics/NEAT.py @@ -182,7 +182,7 @@ def check_population_improvements(self): return self.improvement_counter += 1 print(f"Improvement counter: {self.improvement_counter}") - if self.improvement_counter > 20: + if self.improvement_counter >= 20: print("No improvements in 20 generations - RIP") self.species.sort(key=lambda x: self.rank_species(x), reverse=True) if len(self.species) > 2: @@ -194,6 +194,7 @@ def check_population_improvements(self): def check_individual_impovements(self): """ Check each species if they are improving, remove the ones that are not after 15 generations""" + best_fitness = 0 for specie in self.species: specie.genomes.sort(key=lambda x: x.fitness_value, reverse=True) if specie.best_genome_fitness < specie.genomes[0].fitness_value: @@ -201,9 +202,20 @@ def check_individual_impovements(self): specie.improvement_counter = 0 else: specie.improvement_counter += 1 + if specie.best_genome_fitness > best_fitness: + best_fitness = specie.best_genome_fitness for specie in self.species: - print(f"Specie number: {specie.species_number}, Best fitness: {specie.best_genome_fitness}, Improvement counter: {specie.improvement_counter}") - if specie.improvement_counter > 20: + print(f"Specie number: {specie.species_number}, Best fitness: {specie.best_genome_fitness}, Stagnation counter: {specie.improvement_counter}") + if specie.improvement_counter >= 15: + if specie.best_genome_fitness == best_fitness: + specie.improvement_counter = 0 + print("Best specie is not removed") + break + print("Specie has stagnated, removing it!") + self.species.remove(specie) + # TODO: Ludvig, kan du forklare hvorfor det er sånn? En liten specie som er shit burde vel ikke overleve videre? + # Bare slett kommentarene hvis det ikke skal være der. + """ if len(self.species) > 2: self.species.remove(specie) else: @@ -211,6 +223,7 @@ def check_individual_impovements(self): self.improvement_counter = 0 for specie in self.species: specie.improvement_counter = 0 + """ if self.species == []: print("All species have been removed - RIP") self.initiate_genomes() diff --git a/src/utils/config.py b/src/utils/config.py index 59780c6..4d00773 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -7,8 +7,8 @@ class Config: c2: float = 1.5 c3: float = 0.4 genomic_distance_threshold: float = 2.69 - population_size: int = 20 - generations: int = 1 + population_size: int = 120 + generations: int = 50 connection_weight_mutation_chance: float = 0.8 # if mutate gene: @@ -34,7 +34,7 @@ class Config: # Activation function # What we use: ReLU # Paper: 1/(1+exp(-0.49*x)) - activation_func: str = "sigmoid" + activation_func: str = "tanh" elitism_rate: float = 0.2 # percentage of the best genomes are copied to the next generation remove_worst_percentage: float = 0.3 # percentage of the worst genomes are removed from the population when breeding diff --git a/src/utils/utils.py b/src/utils/utils.py index f1cf860..98dca2d 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -46,20 +46,21 @@ def insert_input(genome:Genome, state: np.ndarray) -> None: for i, node in enumerate(genome.nodes[start_idx_input_node:start_idx_input_node+num_input_nodes]): # get all input nodes node.value = state[i//num_columns][i % num_columns] -def save_fitness(best: list, avg: list, min: list, append: bool = False): - os.makedirs('data/fitness', exist_ok=True) - with open("data/fitness/fitness_values.txt", "w") as f: +def save_fitness(best: list, avg: list, min: list, name: str): + os.makedirs(f'data/{name}/fitness', exist_ok=True) + with open(f"data/{name}/fitness/fitness_values.txt", "w") as f: for i in range(len(best)): f.write(f"Generation: {i} - Best: {best[i]} - Avg: {avg[i]} - Min: {min[i]}\n") -def save_best_genome(genome: Genome, generation: int): - os.makedirs('data/good_genomes', exist_ok=True) - with open(f'data/good_genomes/best_genome_{generation}.obj', 'wb') as f: +def save_best_genome(genome: Genome, generation: int, name: str): + path = f'data/{name}/good_genomes' + os.makedirs(path, exist_ok=True) + with open(f'{path}/best_genome_{generation}.obj', 'wb') as f: pickle.dump(genome, f) # type: ignore -def load_best_genome(generation: int): +def load_best_genome(generation: int, name: str) -> None: if generation == -1: - files = os.listdir('data/good_genomes') + files = os.listdir(f'data/{name}/good_genomes') pattern = re.compile(r'best_genome_(\d+).obj') generations = [] for file in files: @@ -72,31 +73,35 @@ def load_best_genome(generation: int): else: raise FileNotFoundError("No valid genome files found in 'data/good_genomes'.") - with open(f'data/good_genomes/best_genome_{generation}.obj', 'rb') as f: + with open(f'data/{name}/good_genomes/best_genome_{generation}.obj', 'rb') as f: return pickle.load(f) def save_neat(neat: 'NEAT', name: str): - os.makedirs('data/trained_population', exist_ok=True) - with open(f'data/trained_population/neat_{name}.obj', 'wb') as f: + os.makedirs(f'data/{name}/trained_population', exist_ok=True) + with open(f'data/{name}/trained_population/neat_{name}.obj', 'wb') as f: pickle.dump(neat, f) # type: ignore def load_neat(name: str): # Check if file exists first - if not os.path.exists(f'data/trained_population/neat_{name}.obj'): + if not os.path.exists(f'data/{name}/trained_population/neat_{name}.obj'): return None - with open(f'data/trained_population/neat_{name}.obj', 'rb') as f: + with open(f'data/{name}/trained_population/neat_{name}.obj', 'rb') as f: return pickle.load(f) # type: ignore # Function to read and parse the file -def read_fitness_file(filename): +def read_fitness_file(name: str): + """ + name - Name of the neat instance. + """ generations = [] best_values = [] avg_values = [] min_values = [] - os.makedirs('data/fitness', exist_ok=True) + filename = f'data/{name}/fitness' # Make sure the file is named 'fitness.txt' and is in the same directory + os.makedirs(filename, exist_ok=True) # Open the file and extract data - with open(filename, 'r') as file: + with open(f"{filename}/fitness_values.txt", 'r') as file: for line in file: match = re.match(r"Generation: (\d+) - Best: ([\d\.]+) - Avg: ([\d\.]+) - Min: ([\d\.]+)", line) if match: @@ -108,7 +113,7 @@ def read_fitness_file(filename): return generations, best_values, avg_values, min_values # Function to plot the data -def plot_fitness_data(generations, best_values, avg_values, min_values, show=False): +def plot_fitness_data(generations: list, best_values: list, avg_values: list, min_values: list, name: str, show=False): plt.clf() plt.plot(generations, best_values, label='Best') @@ -121,15 +126,10 @@ def plot_fitness_data(generations, best_values, avg_values, min_values, show=Fal plt.legend() plt.grid(True) - plt.savefig('data/fitness/fitness_plot.png') + plt.savefig(f'data/{name}/fitness/fitness_plot.png') if show: plt.show() -def save_fitness_data(show=False): - filename = 'data/fitness/fitness_values.txt' # Make sure the file is named 'fitness.txt' and is in the same directory - generations, best_values, avg_values, min_values = read_fitness_file(filename) - plot_fitness_data(generations, best_values, avg_values, min_values, show=show) - -def get_fitnesses_from_file(f_name: str): - filename = f'data/fitness/{f_name}.txt' # Make sure the file is named 'fitness.txt' and is in the same directory - return read_fitness_file(filename) \ No newline at end of file +def save_fitness_data(name, show=False): + generations, best_values, avg_values, min_values = read_fitness_file(name) + plot_fitness_data(generations, best_values, avg_values, min_values, name, show=show) \ No newline at end of file From 287cd3c1b80ead3fbc6a53d623287b345aaeb789 Mon Sep 17 00:00:00 2001 From: BrageNTNU Date: Thu, 24 Oct 2024 19:03:30 +0200 Subject: [PATCH 5/9] fix: bugs with folders --- main.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 9e9bacf..b506a44 100644 --- a/main.py +++ b/main.py @@ -17,9 +17,13 @@ def play_genome(args): else: from_gen = 0 test_genome(from_gen, to_gen) + + neat_name = "latest" + if args.neat_name != '': + neat_name = args.neat_name generation_num = args.generation if args.generation is not None else -1 - genome = load_best_genome(generation_num) + genome = load_best_genome(generation_num, neat_name) env, state = env_debug_init() run_game_debug(env, state, genome, 0, visualize=False) @@ -50,9 +54,6 @@ def main(args): profiler = cProfile.Profile() profiler.enable() min_fitnesses, avg_fitnesses, best_fitnesses = [], [], [] - - if neat_name == '': - neat_name = "latest" neat = load_neat(neat_name) if neat is not None: # TODO: Add option to insert new config into NEAT object. @@ -109,6 +110,9 @@ def command_line_interface(): subparsers = parser.add_subparsers(dest="command", help="Choose 'train', 'test', 'graph', or 'play'") + # Global arguments for all functions + parser.add_argument('-n', '--neat_name', type=str, default='latest', help="The name of the NEAT object to load from 'trained_population/'") + # Train command (runs main()) train_parser = subparsers.add_parser('train', help="Run the training process") train_parser.add_argument('-n', '--neat_name', type=str, default='', help="The name of the NEAT object to load from 'trained_population/'") @@ -126,7 +130,7 @@ def command_line_interface(): if args.command == "train": main(args) elif args.command == "graph": - save_fitness_data(show=True) + save_fitness_data(args.neat_name, show=True) elif args.command == "play": play_genome(args) else: From a014b3fef4d5f8babcfbaf093f3149b786491ff9 Mon Sep 17 00:00:00 2001 From: Christian Fredrik Johnsen Date: Thu, 24 Oct 2024 19:08:06 +0200 Subject: [PATCH 6/9] fix: correct type hinting in load_best_genome(). --- src/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/utils.py b/src/utils/utils.py index 43885bc..65f4d96 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -58,7 +58,7 @@ def save_best_genome(genome: Genome, generation: int, name: str): with open(f'{path}/best_genome_{generation}.obj', 'wb') as f: pickle.dump(genome, f) # type: ignore -def load_best_genome(generation: int, name: str) -> None: +def load_best_genome(generation: int, name: str) -> Genome: """Loads the best genome from the given generation. If -1 is passed as argument, the latest generation is displayed.""" if generation == -1: # Find the genome from the latest generation. files = os.listdir(f'data/{name}/good_genomes') From 27f5f71f3bdd904e63fa12a74cd577d82500793f Mon Sep 17 00:00:00 2001 From: BrageNTNU Date: Thu, 24 Oct 2024 19:12:07 +0200 Subject: [PATCH 7/9] fix: bugfix again --- main.py | 13 ++++++------- src/utils/utils.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index b506a44..c39e3f7 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ from src.environments.debug_env import env_debug_init, run_game_debug from src.utils.config import Config from src.genetics.NEAT import NEAT -from src.utils.utils import read_fitness_file, save_fitness, save_best_genome, load_best_genome, save_neat, load_neat, save_fitness_data +from src.utils.utils import read_fitness_file, save_fitness, save_best_genome, load_best_genome, save_neat, load_neat, save_fitness_graph_file import warnings import cProfile import pstats @@ -51,6 +51,8 @@ def collect_fitnesses(genomes, generation, min_fitnesses, avg_fitnesses, best_fi def main(args): neat_name = args.neat_name + print("\nTraining NEAT with name: ", neat_name) + print() profiler = cProfile.Profile() profiler.enable() min_fitnesses, avg_fitnesses, best_fitnesses = [], [], [] @@ -58,9 +60,7 @@ def main(args): neat = load_neat(neat_name) if neat is not None: # TODO: Add option to insert new config into NEAT object. generation_nums, best_fitnesses, avg_fitnesses, min_fitnesses = read_fitness_file(neat_name) - print(f"Generation numbs: {generation_nums}") from_generation = generation_nums[-1] + 1 - print(f"From generation: {from_generation}") #config_instance = neat.config else: neat = NEAT(Config()) @@ -69,7 +69,7 @@ def main(args): generations = (neat.config.generations) if args.n_generations == -1 else args.n_generations - print(f"Training from generation {from_generation} to generation {from_generation + generations}") + print(f"Training from generation {from_generation} to generation {from_generation + generations}\n") try: for generation in range(from_generation, from_generation + generations): @@ -90,7 +90,7 @@ def main(args): for genome in neat.genomes: if not genome.elite: neat.add_mutation(genome) - save_fitness_data(neat_name) + save_fitness_graph_file(neat_name) except KeyboardInterrupt: print("\nProcess interrupted! Saving fitness data...") finally: @@ -115,7 +115,6 @@ def command_line_interface(): # Train command (runs main()) train_parser = subparsers.add_parser('train', help="Run the training process") - train_parser.add_argument('-n', '--neat_name', type=str, default='', help="The name of the NEAT object to load from 'trained_population/'") train_parser.add_argument('-g', '--n_generations', type=int, default=-1, help="The number of generations to train for") graph_parser = subparsers.add_parser('graph', help="Graph the fitness data") @@ -130,7 +129,7 @@ def command_line_interface(): if args.command == "train": main(args) elif args.command == "graph": - save_fitness_data(args.neat_name, show=True) + save_fitness_graph_file(args.neat_name, show=True) elif args.command == "play": play_genome(args) else: diff --git a/src/utils/utils.py b/src/utils/utils.py index 43885bc..5d2b28a 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -131,6 +131,6 @@ def plot_fitness_data(generations: list, best_values: list, avg_values: list, mi if show: plt.show() -def save_fitness_data(name, show=False): +def save_fitness_graph_file(name, show=False): generations, best_values, avg_values, min_values = read_fitness_file(name) plot_fitness_data(generations, best_values, avg_values, min_values, name, show=show) \ No newline at end of file From ecd9d11c65b5d6dab5b85bca109d5e1cc62b73fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20St=C3=B8ren?= Date: Thu, 24 Oct 2024 19:12:30 +0200 Subject: [PATCH 8/9] fix: use cogito colors --- docs/images/logo light blue name white.png | Bin 0 -> 88373 bytes gui_with_pygame.py | 109 +++++++++++---------- 2 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 docs/images/logo light blue name white.png diff --git a/docs/images/logo light blue name white.png b/docs/images/logo light blue name white.png new file mode 100644 index 0000000000000000000000000000000000000000..ffada908107406a580454d8333de1446d3850c9c GIT binary patch literal 88373 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∈t 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 An*aa+ literal 0 HcmV?d00001 diff --git a/gui_with_pygame.py b/gui_with_pygame.py index ccd7ebf..e706013 100644 --- a/gui_with_pygame.py +++ b/gui_with_pygame.py @@ -1,3 +1,4 @@ +import pygame.image import main as neat_test_file import pygame import sys @@ -118,8 +119,8 @@ def get_genomes(self): class Settings(): def __init__(self): self.fps = 60 - self.default_x_size = 1000 - self.default_y_size = 500 + self.default_x_size = 2560 + self.default_y_size = 1600 self.fullscreen = False self.fs_start_time = 0 self.fs_is_pressed = False @@ -127,22 +128,25 @@ def __init__(self): self.sc_selector = 0 # 0 = main, 1 = training, 2 = settings, etc. # Colors - self.DARK_BLUE = (8, 19, 169) - self.GREEN = (8, 166, 11) - self.YELLOW = (255, 226, 10) - self.LIGHT_BLUE = (173, 216, 230) - self.WHITE = (255, 255, 255) - self.GRAY = (200, 200, 200) + self.DARK_BLUE = (0x13, 0x39, 0x5B) + self.LIGHT_BLUE = (0x30, 0xB3, 0xEC) + self.WHITE = (0xFF, 0xFF, 0xFF) + self.ORANGE = (0xFF, 0x63, 0x48) + self.RED = (0xFF, 0x47, 0x57) + self.BLUE = (0x1E, 0x90, 0xFF) # Button and font settings - self.button_color = (100, 200, 255) - self.hover_color = (150, 230, 255) - self.pressed_color = (50, 150, 200) - self.text_color = (255, 255, 255) - self.input_field_bg = (58, 58, 58) - self.input_field_active_bg = (58, 58, 58) + self.background_color = self.DARK_BLUE + self.button_color = self.BLUE + self.hover_color = self.LIGHT_BLUE + self.pressed_color = self.DARK_BLUE + self.text_color = self.WHITE + self.input_field_bg = self.ORANGE + self.input_field_active_bg = self.RED pygame.font.init() - self.font = pygame.font.Font(None, 36) + self.normal_font = pygame.font.Font(None, 36) + self.big_font = pygame.font.Font(None, 72) + class SelectableListItem: """Represents a genome item that can be selected, with a checkbox.""" @@ -450,20 +454,20 @@ def __init__(self): # Main menu buttons self.main_menu_buttons = [ - Button(140, 100, 200, 100, "Train!", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.train_scene), - Button(460, 100, 200, 100, "Settings", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.settings_scene), - Button(140, 250, 200, 100, "Watch best gene", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.watch_gene_scene), - Button(460, 250, 200, 100, "Visualize best genome", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.visualize_genome_scene), - TextDisplay(300, 30, "Neat Tactics!", st.font, st.text_color, bg_color=st.DARK_BLUE) + Button(140, 100, 200, 100, "Train!", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.train_scene), + Button(460, 100, 200, 100, "Settings", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.settings_scene), + Button(140, 250, 200, 100, "Watch best gene", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.watch_gene_scene), + Button(460, 250, 200, 100, "Visualize best genome", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.visualize_genome_scene), + TextDisplay(300, 30, "Neat Tactics!", st.big_font, st.DARK_BLUE, bg_color=st.LIGHT_BLUE) ] # Training scene UI elements - self.training_input_fields= [InputField(140, 100, 200, 50, st.font, st.text_color, initial_text="Population size"), - InputField(140, 170, 200, 50, st.font, st.text_color, initial_text="Mutation rate"), - InputField(140, 240, 200, 50, st.font, st.text_color, initial_text="Generations")] + self.training_input_fields= [InputField(140, 100, 200, 50, st.normal_font, st.text_color, initial_text="Population size"), + InputField(140, 170, 200, 50, st.normal_font, st.text_color, initial_text="Mutation rate"), + InputField(140, 240, 200, 50, st.normal_font, st.text_color, initial_text="Generations")] self.training_UI = [ - Button(460, 170, 200, 50, "Start Training", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.start_training_process), - Button(460, 300, 200, 50, "Back to Menu", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene)] + Button(460, 170, 200, 50, "Start Training", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.start_training_process), + Button(460, 300, 200, 50, "Back to Menu", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene)] try: self.fitness_graph = ImageSprite("data/fitness/fitness_plot.png", (700, 100)) except: @@ -475,25 +479,25 @@ def __init__(self): # Settings scene self.settings_input_fields = [ - InputField(200, 50, 200, 30, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.c1)), - InputField(200, 100, 200, 30, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.c2)), - InputField(200, 150, 200, 30, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.c3)), - InputField(200, 200, 200, 30, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.genomic_distance_threshold)), - InputField(200, 250, 200, 30, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.population_size)), - InputField(200, 300, 200, 30, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.generations)) + InputField(200, 50, 200, 30, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.c1)), + InputField(200, 100, 200, 30, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.c2)), + InputField(200, 150, 200, 30, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.c3)), + InputField(200, 200, 200, 30, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.genomic_distance_threshold)), + InputField(200, 250, 200, 30, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.population_size)), + InputField(200, 300, 200, 30, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg, str(self.config.generations)) ] - self.apply_button = Button(200, 350, 200, 50, "Apply Changes", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.apply_changes) - self.settings_back_button = Button(200, 450, 200, 50, "Back", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene) + self.apply_button = Button(200, 350, 200, 50, "Apply Changes", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.apply_changes) + self.settings_back_button = Button(200, 450, 200, 50, "Back", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene) self.input_titles = [ - TextDisplay(50, 50, "c1:", st.font, st.text_color), - TextDisplay(50, 100, "c2:", st.font, st.text_color), - TextDisplay(50, 150, "c3:", st.font, st.text_color), - TextDisplay(50, 200, "Genomic Distance Threshold:", st.font, st.text_color), - TextDisplay(50, 250, "Population Size:", st.font, st.text_color), - TextDisplay(50, 300, "Generations:", st.font, st.text_color) + TextDisplay(50, 50, "c1:", st.normal_font, st.text_color), + TextDisplay(50, 100, "c2:", st.normal_font, st.text_color), + TextDisplay(50, 150, "c3:", st.normal_font, st.text_color), + TextDisplay(50, 200, "Genomic Distance Threshold:", st.normal_font, st.text_color), + TextDisplay(50, 250, "Population Size:", st.normal_font, st.text_color), + TextDisplay(50, 300, "Generations:", st.normal_font, st.text_color) ] @@ -513,21 +517,22 @@ def __init__(self): print(genome) ## Genome viewer - self.genome_viewer = GenomeViewer(self.genomes, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg) + self.genome_viewer = GenomeViewer(self.genomes, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg) self.watch_genes_visualize = ImageSprite("genome_frames/genome_0.png", (700, 50), (600, 400)) ## Run button - self.run_button = Button(600, 20, 200, 50, "Run Selected Genomes", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.start_watching_process) - self.watch_back_button = Button(200, 450, 200, 50, "Back", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene) + self.run_button = Button(600, 20, 200, 50, "Run Selected Genomes", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.start_watching_process) + self.watch_back_button = Button(200, 450, 200, 50, "Back", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene) #Visualize best genome - self.visualize_back_button = Button(50, 50, 150, 50, "Back", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene) - self.show_visualization_button = Button(100, 350, 200, 50, "Show Visualization", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.visualize_genomes) - self.get_which_frames_to_show_input = InputField(400, 350, 150, 50, st.font, st.text_color, st.input_field_bg, st.input_field_active_bg, initial_text="0") + self.visualize_back_button = Button(50, 50, 150, 50, "Back", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene) + self.show_visualization_button = Button(100, 350, 200, 50, "Show Visualization", st.normal_font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.visualize_genomes) + self.get_which_frames_to_show_input = InputField(400, 350, 150, 50, st.normal_font, st.text_color, st.input_field_bg, st.input_field_active_bg, initial_text="0") - self.scrollable_list = ScrollableList(50, 50, 500, 500, self.genomes, st.font, (255, 255, 255), (50, 50, 50), (0, 100, 255), visible_count=10, padding=10) + self.scrollable_list = ScrollableList(50, 50, 500, 500, self.genomes, st.normal_font, st.text_color, st.button_color, st.hover_color, visible_count=10, padding=10) + self.logo_sprite = ImageSprite("docs/images/logo light blue name white.png", (50, 450), (200, 200)) def event_handler(self): for event in pygame.event.get(): @@ -578,7 +583,7 @@ def run_selected_genomes(self): def watch_genome_scene(self): - self.screen.fill(st.LIGHT_BLUE) + self.screen.fill(st.background_color) # Draw the genome viewer list self.genome_viewer.draw(self.screen) @@ -623,7 +628,7 @@ def training_process(self): def draw_visualize_genome_scene(self): """Draw the 'Visualize Best Genome' scene.""" - self.screen.fill(st.LIGHT_BLUE) + self.screen.fill(st.background_color) # Draw the back button self.visualize_back_button.draw(self.screen) @@ -658,7 +663,7 @@ def start_training(self): def draw_settings_scene(self): - self.screen.fill(st.LIGHT_BLUE) + self.screen.fill(st.background_color) # Draw titles and input fields for title in self.input_titles: @@ -674,12 +679,12 @@ def draw_settings_scene(self): def update_screen(self): if st.sc_selector == 0: # Main menu scene - self.screen.fill(st.LIGHT_BLUE) + self.screen.fill(st.background_color) for button in self.main_menu_buttons: button.draw(self.screen) elif st.sc_selector == 1: # Train scene - self.screen.fill(st.LIGHT_BLUE) + self.screen.fill(st.background_color) for element in self.training_input_fields: element.draw(self.screen) @@ -698,6 +703,8 @@ def update_screen(self): self.watch_genome_scene() elif st.sc_selector == 4: self.draw_visualize_genome_scene() + + self.logo_sprite.draw(self.screen) pygame.display.flip() From 2f0798db71510a5908239b789fa259b652fc524d Mon Sep 17 00:00:00 2001 From: Vetlets05 Date: Thu, 24 Oct 2024 19:25:35 +0200 Subject: [PATCH 9/9] feat: merge conflict incoming (add graph and arguments) --- gui_with_pygame.py | 31 ++++++++++++++++++----- src/environments/mario_env.py | 2 +- src/utils/utils.py | 3 +++ src/visualization/colors_visualization.py | 2 ++ src/visualization/visualize_genome.py | 2 ++ 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/gui_with_pygame.py b/gui_with_pygame.py index 17148cf..300b0ac 100644 --- a/gui_with_pygame.py +++ b/gui_with_pygame.py @@ -3,6 +3,7 @@ import sys import threading import os +import argparse import src.utils.config as conf import pickle @@ -404,7 +405,7 @@ def __init__(self): self.fitness_graph = ImageSprite("data/fitness/fitness_plot.png", (700, 100)) except: print("ERROR: Could not find image path") - self.fitness_graph = 0 + self.fitness_graph = ImageSprite("genome_frames/genome_0.png", (700, 100)) @@ -454,7 +455,7 @@ def __init__(self): self.watch_genes_visualize = ImageSprite("genome_frames/genome_0.png", (700, 50), (600, 400)) ## Run button - self.run_button = Button(600, 20, 200, 50, "Run Selected Genomes", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.start_watching_process) + self.run_button = Button(600, 20, 200, 50, "Run Selected Genomes", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.run_selected_genomes) self.watch_back_button = Button(200, 450, 200, 50, "Back", st.font, st.text_color, st.button_color, st.hover_color, st.pressed_color, self.main_menu_scene) #Visualize best genome @@ -586,7 +587,20 @@ def start_training(self): generations = self.training_input_fields[2].text print(f"Starting training with Population: {population}, Mutation Rate: {mutation_rate}, Generations: {generations}") # Insert your training code here (e.g. NEAT training) - neat_test_file.main() + + + parser = argparse.ArgumentParser(description="Train or Test Genomes") + subparsers = parser.add_subparsers(dest="command", help="Choose 'train', 'test', 'graph', or 'play'") + + # Train command (runs main()) + train_parser = subparsers.add_parser('train', help="Run the training process") + train_parser.add_argument('-n', '--neat_name', type=str, default='', help="The name of the NEAT object to load from 'trained_population/'") + train_parser.add_argument('-g', '--n_generations', type=int, default=-1, help="The number of generations to train for") + + sim_args = ["train", "-g", generations] + args=parser.parse_args(sim_args) + + neat_test_file.main(args=args) def draw_settings_scene(self): @@ -617,11 +631,16 @@ def update_screen(self): element.draw(self.screen) for element in self.training_UI: element.draw(self.screen) - #self.fitness_graph.draw(self.screen) + + try: + self.fitness_graph = ImageSprite("data/fitness/fitness_plot.png", (700, 100)) + except: + pass + self.fitness_graph.draw(self.screen) gen_data,best_fitness_data,avg_fitness_data,min_fitness_data = util.read_fitness_file("data/fitness/fitness_values.txt") - self.new_fitness_graph = Graph(self.screen, (700, 100), (400, 400), best_fitness_data) + #self.new_fitness_graph = Graph(self.screen, (700, 100), (400, 400), best_fitness_data) - self.new_fitness_graph.draw() + #self.new_fitness_graph.draw() elif st.sc_selector == 2: diff --git a/src/environments/mario_env.py b/src/environments/mario_env.py index 0336938..969f18c 100644 --- a/src/environments/mario_env.py +++ b/src/environments/mario_env.py @@ -6,7 +6,7 @@ from gym.spaces import Box from gym.core import ObservationWrapper import numpy as np -# import cv2 +import cv2 from skimage.transform import resize class StepResult(NamedTuple): diff --git a/src/utils/utils.py b/src/utils/utils.py index f1cf860..971f88e 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -1,5 +1,8 @@ from src.genetics.genome import Genome from src.utils.config import Config +import matplotlib +matplotlib.use("Agg") + import matplotlib.pyplot as plt import numpy as np import os diff --git a/src/visualization/colors_visualization.py b/src/visualization/colors_visualization.py index 9956aaa..af05ec5 100644 --- a/src/visualization/colors_visualization.py +++ b/src/visualization/colors_visualization.py @@ -1,6 +1,8 @@ from src.utils.utils import normalize_negative_values, normalize_positive_values import numpy as np from typing import List +import matplotlib +matplotlib.use("Agg") import matplotlib.cm as cm from src.genetics.node import Node diff --git a/src/visualization/visualize_genome.py b/src/visualization/visualize_genome.py index 79d29a5..5a3734d 100644 --- a/src/visualization/visualize_genome.py +++ b/src/visualization/visualize_genome.py @@ -1,4 +1,6 @@ import networkx as nx +import matplotlib +matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.axes import Axes from src.genetics.genome import Genome