├── README.md ├── __init__.py ├── examples ├── backpack.py ├── gameoflife.py ├── image.py ├── string_evolution.py └── tsp.py ├── generic.py ├── layers ├── __init__.py ├── array_layers.py ├── float_arrays.py ├── image_layers.py └── universal_layers.py ├── prompt_maker.py ├── rates.py ├── selectors.py ├── tests └── tests.py └── universal.py /README.md: -------------------------------------------------------------------------------- 1 | # Finch: Evolutionary Algorithm Framework 2 | 3 | Finch is a Python framework for implementing evolutionary algorithms. It provides a modular approach to building and experimenting with various evolutionary computation techniques. 4 | 5 | ## Key Features 6 | 7 | - Modular design with customizable components 8 | - Support for different types of genes (float arrays, strings, arrays) 9 | - Various selection, crossover, and mutation operators 10 | - GPU acceleration support using CuPy 11 | - Visualization tools for monitoring evolution progress 12 | 13 | ## Main Components 14 | 15 | 1. **GenePool**: Generates initial populations 16 | - FloatPool, StringPool, ArrayPool, ImagePool 17 | 18 | 2. **Individual**: Represents a single solution in the population 19 | 20 | 3. **Layer**: Defines genetic operators 21 | - Selection layers 22 | - Crossover layers (e.g., N-Point, Uniform) 23 | - Mutation layers (e.g., Gaussian, Uniform, Polynomial, Swap, Inversion, Scramble) 24 | 25 | 4. **Environment**: Manages the evolution process 26 | 27 | 5. **Competition**: Allows comparing multiple evolutionary strategies 28 | 29 | ## Usage 30 | 31 | 1. Define your fitness function 32 | 2. Create a GenePool 33 | 3. Set up Layers for selection, crossover, and mutation 34 | 4. Initialize an Environment with your layers and individuals 35 | 5. Run the evolution process 36 | 37 | ## Example 38 | 39 | ```python 40 | 41 | import Finch.layers as layers 42 | from Finch.selectors import * 43 | from Finch.generic import * 44 | 45 | def fitness_function(individual): 46 | return sum(individual.item) 47 | 48 | gene_pool = layers.float_arrays.FloatPool(ranges=[[-5, 5]] * 10, length=10, fitness_function=fitness_function) 49 | mutation_selection = RandomSelection(percent_to_select=.1) 50 | crossover_selection = RandomSelection(amount_to_select=2) 51 | 52 | # Set up layers 53 | layers = [ 54 | layers.universal_layers.Populate(population=500, gene_pool=gene_pool), 55 | layers.array_layers.ParentNPoint(selection_function=crossover_selection.select, families=4, children=4), 56 | layers.float_arrays.GaussianMutation(mutation_rate=0.1, sigma=0.5, selection_function=mutation_selection.select), 57 | layers.universal_layers.SortByFitness(), 58 | layers.universal_layers.CapPopulation(1000), 59 | ] 60 | 61 | env = Environment(layers) 62 | env.compile() 63 | env.evolve(generations=1000) 64 | 65 | print(env.best_ever.item) 66 | env.plot() 67 | ``` 68 | 69 | ## Installation 70 | 71 | ``` 72 | pip install finch-genetics 73 | ``` 74 | 75 | Find this project on [Gitstar](http://127.0.0.1:5000/repository/2) - where repositories battle for glory! 76 | 77 | ## Contributing 78 | 79 | Contributions are welcome! Please feel free to submit a Pull Request. 80 | 81 | ## License 82 | 83 | This project is licensed under the MIT License. 84 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Finch: Evolutionary Algorithm Framework 2 | # Version: 3.5.2 3 | from . import generic, universal, selectors, rates, layers 4 | 5 | # Define public API 6 | __all__ = [ 7 | "generic", 8 | "universal", 9 | "selectors", 10 | "rates", 11 | "layers" 12 | ] 13 | 14 | # Version information 15 | __version__ = "3.5.0" 16 | 17 | # Package-level docstring 18 | __doc__ = """ 19 | Finch: Evolutionary Algorithm Framework 20 | 21 | Finch is a flexible and powerful framework for implementing various 22 | evolutionary algorithms in Python. It provides a modular approach to 23 | building and experimenting with different evolutionary computation techniques. 24 | 25 | Key Features: 26 | - Modular design with customizable components 27 | - Support for different types of genes (float arrays, strings, arrays, images) 28 | - Various selection, crossover, and mutation operators 29 | - GPU acceleration support using CuPy 30 | - Visualization tools for monitoring evolution progress 31 | 32 | For more information, visit: https://github.com/your-username/finch 33 | """ -------------------------------------------------------------------------------- /examples/backpack.py: -------------------------------------------------------------------------------- 1 | from Finch.generic import Environment, Individual 2 | from Finch.layers.universal_layers import Populate, SortByFitness, CapPopulation 3 | from Finch.selectors import RankBasedSelection, RandomSelection 4 | from Finch.layers.array_layers import ArrayPool, ParentNPoint, SwapMutation, ReplaceMutation, InversionMutation 5 | import numpy as np 6 | 7 | # Define the backpack problem parameters 8 | items = [ 9 | {"name": "Laptop", "weight": 3, "value": 10}, 10 | {"name": "Headphones", "weight": 0.5, "value": 3}, 11 | {"name": "Book", "weight": 1, "value": 1}, 12 | {"name": "Water Bottle", "weight": 1, "value": 2}, 13 | {"name": "Snacks", "weight": 0.5, "value": 1}, 14 | {"name": "Camera", "weight": 2, "value": 4}, 15 | {"name": "First Aid Kit", "weight": 1, "value": 9}, 16 | {"name": "Jacket", "weight": 1, "value": 8}, 17 | {"name": "Flashlight", "weight": 0.5, "value": 6}, 18 | {"name": "Portable Charger", "weight": 0.5, "value": 5}, 19 | {"name": "Smartphone", "weight": 0.3, "value": 9}, 20 | {"name": "Tablet", "weight": 1, "value": 7}, 21 | {"name": "Sunglasses", "weight": 0.2, "value": 2}, 22 | {"name": "Umbrella", "weight": 1, "value": 3}, 23 | {"name": "Hiking Boots", "weight": 2, "value": 6}, 24 | {"name": "Tent", "weight": 4, "value": 8}, 25 | {"name": "Sleeping Bag", "weight": 2, "value": 7}, 26 | {"name": "Camping Stove", "weight": 1.5, "value": 5}, 27 | {"name": "Map and Compass", "weight": 0.2, "value": 4}, 28 | {"name": "Binoculars", "weight": 1, "value": 3}, 29 | {"name": "Insect Repellent", "weight": 0.2, "value": 2}, 30 | {"name": "Sunscreen", "weight": 0.3, "value": 2}, 31 | {"name": "Multi-tool", "weight": 0.3, "value": 5}, 32 | {"name": "Rope", "weight": 1, "value": 3}, 33 | {"name": "Water Filter", "weight": 0.5, "value": 6}, 34 | {"name": "Fire Starter", "weight": 0.1, "value": 4}, 35 | {"name": "Emergency Whistle", "weight": 0.1, "value": 2}, 36 | {"name": "Hammock", "weight": 1, "value": 4}, 37 | {"name": "Solar Charger", "weight": 0.5, "value": 5}, 38 | {"name": "Hand Sanitizer", "weight": 0.2, "value": 1} 39 | ] 40 | 41 | MAX_WEIGHT = 7 42 | 43 | # Define the fitness function 44 | def fitness_function(individual): 45 | total_weight = sum(items[i]["weight"] for i, gene in enumerate(individual.item) if gene) 46 | 47 | if total_weight > MAX_WEIGHT: 48 | return -1 # Penalty for exceeding weight limit 49 | total_value = sum(items[i]["value"] for i, gene in enumerate(individual.item) if gene) 50 | return total_value 51 | 52 | # Create the gene pool 53 | pool = ArrayPool(gene_array=np.array([0, 1]), fitness_function=fitness_function, length=len(items)) 54 | 55 | # Define the layers 56 | layers = [ 57 | Populate(population=100, gene_pool=pool), 58 | # Keep only positive mutations. 59 | SwapMutation(selection_function=RandomSelection(percent_to_select=0.05).select, overpowered=True), 60 | ParentNPoint(selection_function=RankBasedSelection(amount_to_select=2, factor=10).select, families=8, children=2, 61 | n_points=5), 62 | SortByFitness(), 63 | CapPopulation(max_population=90) 64 | ] 65 | 66 | # Create and compile the environment 67 | env = Environment(layers=layers, verbose_every=10) 68 | env.compile() 69 | 70 | # Evolve the population 71 | env.evolve(generations=1000) 72 | 73 | # Get and print the best solution 74 | best = env.best_ever 75 | print("Best solution:") 76 | total_weight = 0 77 | total_value = 0 78 | for i, gene in enumerate(best.item): 79 | if gene: 80 | print(f"- {items[i]['name']}") 81 | total_weight += items[i]['weight'] 82 | total_value += items[i]['value'] 83 | print(f"Total weight: {total_weight}/{MAX_WEIGHT}") 84 | print(f"Total value: {total_value}") 85 | 86 | # Plot the fitness history 87 | env.plot() -------------------------------------------------------------------------------- /examples/gameoflife.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from Finch.generic import Environment, Individual 3 | from Finch.layers.universal_layers import Populate, SortByFitness, CapPopulation 4 | from Finch.selectors import RandomSelection, RankBasedSelection 5 | from Finch.layers.array_layers import ArrayPool, ParentNPoint, SwapMutation 6 | import matplotlib.pyplot as plt 7 | 8 | # Define the grid size 9 | GRID_SIZE = 50 10 | 11 | # Define the Game of Life rules 12 | def apply_rules(grid): 13 | new_grid = grid.copy() 14 | for i in range(GRID_SIZE): 15 | for j in range(GRID_SIZE): 16 | total = int((grid[i, (j-1)%GRID_SIZE] + grid[i, (j+1)%GRID_SIZE] + 17 | grid[(i-1)%GRID_SIZE, j] + grid[(i+1)%GRID_SIZE, j] + 18 | grid[(i-1)%GRID_SIZE, (j-1)%GRID_SIZE] + grid[(i-1)%GRID_SIZE, (j+1)%GRID_SIZE] + 19 | grid[(i+1)%GRID_SIZE, (j-1)%GRID_SIZE] + grid[(i+1)%GRID_SIZE, (j+1)%GRID_SIZE])) 20 | if grid[i, j] == 1: 21 | if (total < 2) or (total > 3): 22 | new_grid[i, j] = 0 23 | else: 24 | if total == 3: 25 | new_grid[i, j] = 1 26 | return new_grid 27 | 28 | # Define the fitness function 29 | def fitness_function(individual): 30 | grid = individual.item.reshape((GRID_SIZE, GRID_SIZE)) 31 | next_gen = apply_rules(grid) 32 | return np.sum(next_gen) # Return the number of live cells in the next generation 33 | 34 | # Create the gene pool 35 | pool = ArrayPool(gene_array=np.array([0, 1]), fitness_function=fitness_function, length=GRID_SIZE**2) 36 | 37 | # Define the layers 38 | layers = [ 39 | Populate(population=100, gene_pool=pool), 40 | SwapMutation(selection_function=RandomSelection(amount_to_select=10).select), 41 | ParentNPoint(selection_function=RankBasedSelection(amount_to_select=2, factor=1).select, families=8, children=2, n_points=3), 42 | SortByFitness(), 43 | CapPopulation(max_population=100) 44 | ] 45 | 46 | # Create and compile the environment 47 | env = Environment(layers=layers, verbose_every=10) 48 | env.compile() 49 | 50 | # Evolve the population 51 | env.evolve(generations=500) 52 | 53 | # Get the best individual 54 | best = env.best_ever 55 | best_grid = best.item.reshape((GRID_SIZE, GRID_SIZE)) 56 | 57 | # Visualize the best grid 58 | plt.imshow(apply_rules(best_grid), cmap='binary') 59 | plt.title("Best Game of Life Grid") 60 | plt.show() 61 | -------------------------------------------------------------------------------- /examples/image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Note: This example does not work well yet. 3 | """ 4 | import numpy as np 5 | from PIL import Image 6 | from Finch.generic import Environment, Individual 7 | from Finch.layers.universal_layers import Populate, SortByFitness, CapPopulation 8 | from Finch.selectors import RankBasedSelection, RandomSelection 9 | from Finch.layers.float_arrays import FloatPool, GaussianMutation, UniformMutation, ParentBlendFloat 10 | 11 | # Load the target image 12 | 13 | height, width, channels = 250, 250, 3 14 | # Define the fitness functions 15 | def fitness_function(individual): 16 | e = sum(individual.item) 17 | return e 18 | 19 | 20 | # Create the float pool 21 | pool = FloatPool(ranges=[[0, 0]] * (height * width * channels), 22 | length=height * width * channels, 23 | fitness_function=fitness_function) 24 | 25 | # Define the layers 26 | layers = [ 27 | Populate(population=50, gene_pool=pool), 28 | UniformMutation( 29 | mutation_rate=0.1, 30 | upper_bound=.1, 31 | lower_bound=-.1, 32 | selection_function=RandomSelection(amount_to_select=16).select, 33 | ), 34 | ParentBlendFloat(selection_function=RankBasedSelection(amount_to_select=2, factor=4).select, families=4, children=2), 35 | SortByFitness(), 36 | 37 | CapPopulation(max_population=50) 38 | ] 39 | 40 | # Create the environment 41 | env = Environment(layers=layers, verbose_every=10) 42 | 43 | # Compile the environment 44 | env.compile() 45 | 46 | # Evolve the population 47 | env.evolve(generations=2000) 48 | 49 | # Get the best individual 50 | best = env.best_ever 51 | # Reshape and denormalize the best individual 52 | best_image = (best.item.reshape(height, width, channels) * 255).astype(np.uint8) 53 | 54 | # Save the best image 55 | Image.fromarray(best_image).save("evolved_image.png") 56 | 57 | # Print the final fitness 58 | print(f"Final fitness: {best.fitness}") 59 | 60 | # Plot the fitness history 61 | env.plot() -------------------------------------------------------------------------------- /examples/string_evolution.py: -------------------------------------------------------------------------------- 1 | from Finch.generic import Environment, Individual 2 | from Finch.layers.universal_layers import Populate, SortByFitness, CapPopulation 3 | from Finch.selectors import RandomSelection, RankBasedSelection 4 | from Finch.layers.array_layers import ParentNPoint, InsertionDeletionMutation, ArrayPool, SwapMutation, ReplaceMutation 5 | from difflib import SequenceMatcher 6 | import string 7 | 8 | # Define the target sentence 9 | TARGET = "genetic algos are lit" 10 | 11 | # Define the character set (lowercase letters and space) 12 | CHAR_SET = string.ascii_lowercase + " " 13 | 14 | # Define the fitness function 15 | def fitness_function(individual): 16 | return SequenceMatcher(None, TARGET, ''.join(individual.item)).ratio() * 100 17 | 18 | # Create the gene pool 19 | pool = ArrayPool(gene_array=list(CHAR_SET), fitness_function=fitness_function, length=len(TARGET)) 20 | 21 | # Define the layers 22 | layers = [ 23 | Populate(population=100, gene_pool=pool), 24 | ParentNPoint(selection_function=RankBasedSelection(amount_to_select=2, factor=2).select, families=8, children=2, refit=True), 25 | # InsertionDeletionMutation(gene_pool=pool, selection_function=RandomSelection(percent_to_select=0.2).select, overpowered=True, refit=False), # If overpowered is True ALWAYS set refit to False. 26 | SwapMutation(selection_function=RandomSelection(percent_to_select=0.2).select, overpowered=True, 27 | refit=False), # If overpowered is True ALWAYS set refit to False. 28 | ReplaceMutation(mutation_rate=.1, possible_values=list(CHAR_SET), 29 | selection_function=RandomSelection(percent_to_select=0.2).select, overpowered=True, refit=False), 30 | SortByFitness(), 31 | CapPopulation(max_population=900) # Kill 1 so that 1 is generated randomly again, can help diversity 32 | ] 33 | 34 | # Create the environment 35 | 36 | env = Environment(layers=layers, verbose_every=10) 37 | 38 | # Compile the environment 39 | env.compile() 40 | 41 | # Evolve the population 42 | env.evolve(generations=2000) 43 | 44 | # Print the best individual 45 | best = env.best_ever 46 | print(f"Best solution: '{''.join(best.item)}'") 47 | print(f"Fitness: {best.fitness}") 48 | 49 | # Plot the fitness history 50 | env.plot() -------------------------------------------------------------------------------- /examples/tsp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from Finch.generic import Environment, Individual, Layer 3 | from Finch.layers.universal_layers import Populate, SortByFitness, CapPopulation 4 | from Finch.selectors import RandomSelection, RankBasedSelection 5 | from Finch.layers.array_layers import ArrayPool 6 | import matplotlib.pyplot as plt 7 | import sys 8 | 9 | sys.setrecursionlimit(10000) 10 | 11 | # Define the number of cities and their coordinates 12 | NUM_CITIES = 200 13 | CITIES = np.random.rand(NUM_CITIES, 2) # Random 2D coordinates for cities 14 | 15 | # Calculate distances between all pairs of cities 16 | DISTANCES = np.sqrt(((CITIES[:, np.newaxis, :] - CITIES[np.newaxis, :, :]) ** 2).sum(axis=2)) 17 | 18 | def calculate_total_distance(route): 19 | return sum(DISTANCES[route[i], route[(i + 1) % NUM_CITIES]] for i in range(NUM_CITIES)) 20 | 21 | # Define the fitness function (we want to minimize the total distance) 22 | def fitness_function(individual): 23 | return -calculate_total_distance(individual.item) 24 | 25 | # Create the gene pool 26 | pool = ArrayPool(gene_array=np.arange(NUM_CITIES), fitness_function=fitness_function, length=NUM_CITIES, unique=True) 27 | 28 | # Custom crossover operator for TSP (Order Crossover - OX) 29 | class OrderCrossover(Layer): 30 | def __init__(self, selection_function, families, children): 31 | super().__init__(application_function=self.crossover, selection_function=selection_function, repeat=families) 32 | self.children = children 33 | 34 | def crossover(self, parents): 35 | for _ in range(self.children): 36 | parent1, parent2 = parents 37 | # Choose two random crossover points 38 | a, b = sorted(np.random.choice(NUM_CITIES, 2, replace=False)) 39 | # Create a child with a segment from parent1 40 | child = [-1] * NUM_CITIES 41 | child[a:b] = parent1.item[a:b] 42 | # Fill the remaining positions with cities from parent2 43 | parent2_cities = [city for city in parent2.item if city not in child[a:b]] 44 | for i in range(NUM_CITIES): 45 | if child[i] == -1: 46 | child[i] = parent2_cities.pop(0) 47 | # Add the child to the population 48 | self.environment.add_individuals([Individual(item=np.array(child), fitness_function=fitness_function)]) 49 | 50 | # Custom mutation operator for TSP (2-opt mutation) 51 | class TwoOptMutation(Layer): 52 | def __init__(self, selection_function, mutation_rate=0.1): 53 | super().__init__(application_function=self.mutate_all, selection_function=selection_function) 54 | self.mutation_rate = mutation_rate 55 | 56 | def mutate_all(self, individuals): 57 | for individual in individuals: 58 | if np.random.random() < self.mutation_rate: 59 | self.mutate(individual) 60 | 61 | def mutate(self, individual): 62 | route = individual.item 63 | # Choose two random points 64 | i, j = sorted(np.random.choice(NUM_CITIES, 2, replace=False)) 65 | # Reverse the segment between i and j 66 | route[i:j+1] = route[i:j+1][::-1] 67 | individual.item = route 68 | 69 | # Define the layers 70 | layers = [ 71 | Populate(population=100, gene_pool=pool), 72 | OrderCrossover(selection_function=RankBasedSelection(amount_to_select=2, factor=2).select, families=8, children=2), 73 | TwoOptMutation(selection_function=RandomSelection(percent_to_select=0.1).select, mutation_rate=0.1), 74 | SortByFitness(), 75 | CapPopulation(max_population=100) 76 | ] 77 | 78 | # Create and compile the environment 79 | env = Environment(layers=layers, verbose_every=100) 80 | env.compile() 81 | 82 | # Evolve the population 83 | env.evolve(generations=10000) 84 | 85 | # Get the best solution 86 | best = env.best_ever 87 | best_route = best.item 88 | best_distance = -best.fitness 89 | 90 | print(f"Best route found: {best_route}") 91 | print(f"Total distance: {best_distance}") 92 | 93 | # Plot the best route 94 | plt.figure(figsize=(10, 10)) 95 | plt.scatter(CITIES[:, 0], CITIES[:, 1], c='red', s=50) 96 | for i in range(NUM_CITIES): 97 | plt.annotate(str(i), (CITIES[i, 0], CITIES[i, 1])) 98 | for i in range(NUM_CITIES): 99 | start = best_route[i] 100 | end = best_route[(i + 1) % NUM_CITIES] 101 | plt.plot([CITIES[start, 0], CITIES[end, 0]], [CITIES[start, 1], CITIES[end, 1]], 'b-') 102 | 103 | plt.title(f"Best TSP Route (Distance: {best_distance:.2f})") 104 | plt.show() 105 | 106 | # Plot the fitness history 107 | env.plot() -------------------------------------------------------------------------------- /generic.py: -------------------------------------------------------------------------------- 1 | from Finch.universal import ARRAY_MANAGER 2 | import copy 3 | from typing import Callable, Union, List, Dict 4 | from matplotlib import pyplot as plt 5 | import math 6 | 7 | make_callable = lambda x: x if callable(x) else lambda: x 8 | 9 | 10 | class Individual: 11 | def __init__(self, item, fitness_function): 12 | self.fitness = -math.inf 13 | self.item = item 14 | self.fitness_function = fitness_function 15 | def fit(self): 16 | self.fitness = self.fitness_function(self) 17 | return self.fitness 18 | def copy(self): 19 | return copy.deepcopy(self) 20 | 21 | 22 | class Layer: 23 | def __init__(self, application_function: Callable, selection_function: Union[Callable, int], repeat: int = 1, refit=True): 24 | self.application_function = application_function 25 | self.selection_function = selection_function 26 | self.repeat = repeat 27 | self.environment = None 28 | self.refit = refit 29 | 30 | def set_environment(self, environment): 31 | self.environment = environment 32 | 33 | def execute(self, individuals: List[Individual]): 34 | assert self.environment, "Environment is not set, please compile the environment or call set_environment(...)" 35 | for i in range(self.repeat): 36 | selected = self.selection_function(individuals) 37 | self.application_function(selected) 38 | if self.refit: 39 | for individual in selected: 40 | individual.fit() 41 | 42 | 43 | class Environment: 44 | def __init__(self, layers: List[Layer], individuals=[], verbose_every=1, early_stopping=0): 45 | self.layers = layers 46 | self.individuals = individuals 47 | self.best_ever = None 48 | self.early_stopping = early_stopping 49 | 50 | self.history = { 51 | 'fitness': [], 52 | 'population': [], 53 | } 54 | self.verbose_every = verbose_every 55 | def add_layer(self, layer: Layer): 56 | self.layers.append(layer) 57 | def evolve(self, generations: int): 58 | for i in range(generations): 59 | for layer in self.layers: 60 | layer.execute(self.individuals) 61 | 62 | fitness = self.individuals[0].fitness 63 | 64 | if self.best_ever: 65 | if fitness > self.best_ever.fitness: 66 | self.best_ever = self.individuals[0].copy() 67 | else: 68 | self.best_ever = self.individuals[0].copy() 69 | if fitness > self.best_ever.fitness: 70 | return 71 | self.history['fitness'].append(fitness) 72 | self.history['population'].append(len(self.individuals)) 73 | if i % self.verbose_every == 0: 74 | print(f"Generation: {i} Fitness: {fitness} Population: {len(self.individuals)}") 75 | 76 | def add_individuals(self, individuals: List[Individual]): 77 | for individual in individuals: 78 | individual.environment = self 79 | self.individuals.extend(individuals) 80 | 81 | def compile(self): 82 | for layer in self.layers: 83 | layer.set_environment(self) 84 | 85 | def plot(self): 86 | plt.plot(self.history['fitness']) 87 | plt.legend(['fitness', 'population']) 88 | plt.show() 89 | 90 | class GenePool: 91 | def __init__(self, generator_function, fitness_function: Callable): 92 | self.generator_function = generator_function 93 | self.fitness_function = fitness_function 94 | 95 | def generate_individuals(self, amount: int): 96 | return [self.generator_function() for _ in range(amount)] 97 | 98 | 99 | class Competition: 100 | def __init__(self, environments: Dict[Environment, str], adaptive_mode: str = 'neither', verbose_every: int = 10): 101 | self.environments = environments 102 | self.adaptive_mode = adaptive_mode 103 | self.verbose_every = verbose_every 104 | self.history = {name: {'fitness': [], 'population': []} for name in environments.values()} 105 | 106 | if adaptive_mode not in ['best', 'worst', 'neither']: 107 | raise ValueError("adaptive_mode must be 'best', 'worst', or 'neither'") 108 | 109 | def evolve(self, total_generations: int): 110 | env_names = list(self.environments.values()) 111 | env_count = len(env_names) 112 | 113 | for gen in range(total_generations): 114 | gen_allocation = self._allocate_generations(env_count) if self.adaptive_mode != 'neither' else {name: 1 for 115 | name in 116 | env_names} 117 | 118 | for env, name in self.environments.items(): 119 | env.evolve(gen_allocation[name]) 120 | fitness = env.individuals[0].fitness 121 | population = len(env.individuals) 122 | 123 | self.history[name]['fitness'].append(fitness) 124 | self.history[name]['population'].append(population) 125 | 126 | if gen % self.verbose_every == 0: 127 | best_fitness = max(env.individuals[0].fitness for env in self.environments) 128 | print(f"Generation {gen}: Best fitness = {best_fitness}") 129 | 130 | def _allocate_generations(self, env_count): 131 | performances = [(name, env.individuals[0].fitness) for env, name in self.environments.items()] 132 | performances.sort(key=lambda x: x[1], reverse=(self.adaptive_mode == 'best')) 133 | 134 | total_weight = sum(range(1, env_count + 1)) 135 | return { 136 | name: max(1, int(((env_count - i if self.adaptive_mode == 'best' else i + 1) / total_weight) * env_count)) 137 | for i, (name, _) in enumerate(performances)} 138 | 139 | def plot(self): 140 | plt.figure(figsize=(12, 6)) 141 | for name, data in self.history.items(): 142 | plt.plot(data['fitness'], label=f'{name} (Fitness)') 143 | plt.title('Fitness History Across Environments') 144 | plt.xlabel('Generation') 145 | plt.ylabel('Fitness') 146 | plt.legend() 147 | plt.grid(True) 148 | plt.show() 149 | 150 | plt.figure(figsize=(12, 6)) 151 | for name, data in self.history.items(): 152 | plt.plot(data['population'], label=f'{name} (Population)') 153 | plt.title('Population History Across Environments') 154 | plt.xlabel('Generation') 155 | plt.ylabel('Population Size') 156 | plt.legend() 157 | plt.grid(True) 158 | plt.show() 159 | 160 | def get_best_environment(self): 161 | return max(((env, name, env.best_ever.fitness) for env, name in self.environments.items()), key=lambda x: x[2]) 162 | 163 | def get_worst_environment(self): 164 | return min(((env, name, env.best_ever.fitness) for env, name in self.environments.items()), key=lambda x: x[2]) -------------------------------------------------------------------------------- /layers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import array_layers, float_arrays, image_layers, universal_layers 2 | 3 | __all__ = ["image_layers", "universal_layers", "array_layers"] -------------------------------------------------------------------------------- /layers/array_layers.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from Finch.universal import ARRAY_MANAGER 4 | from Finch.generic import GenePool, Individual, Layer, make_callable 5 | from typing import Callable, List, Any, Union 6 | import numpy as np 7 | 8 | 9 | class ArrayPool(GenePool): 10 | def __init__(self, gene_array: ARRAY_MANAGER.ndarray, fitness_function: Callable, length: int, device="cpu", unique=False): 11 | """ 12 | A GenePool meant for the creation of individuals by picking items from an array. 13 | :param gene_array: The array from which genes will be picked. 14 | :param length: Amount of genes in each individual 15 | :param device: Where genes in Individuals should be kept 'gpu' or 'cpu' 16 | :param unique: Whether the generated genes should be unique 17 | """ 18 | super().__init__(generator_function=self.generate_array, fitness_function=fitness_function) 19 | self.length = length 20 | self.device = device 21 | self.gene_array = gene_array 22 | self.unique = unique 23 | 24 | def generate_array(self): 25 | """ 26 | :param amount: Amount of genes to generate 27 | :return: numpy or cupy array containing genes picked from the array 28 | """ 29 | if self.device == "cpu": 30 | if self.unique: 31 | genes = np.random.choice(self.gene_array, size=self.length, replace=False) 32 | else: 33 | genes = np.random.choice(self.gene_array, size=self.length) 34 | 35 | elif self.device == "gpu": 36 | if self.unique: 37 | genes = ARRAY_MANAGER.random.choice(self.gene_array, size=self.length, replace=False) 38 | else: 39 | genes = ARRAY_MANAGER.random.choice(self.gene_array, size=self.length) 40 | ind = Individual(item=genes, fitness_function=self.fitness_function) 41 | 42 | return ind 43 | 44 | 45 | class ParentNPoint(Layer): 46 | def __init__(self, selection_function: Callable, families: int, children: int, n_points: int = 3, device='cpu', refit=True): 47 | super().__init__(application_function=self.parent, selection_function=selection_function, repeat=families, refit=refit) 48 | self.children = make_callable(children) 49 | self.device = device 50 | self.n_points = n_points 51 | 52 | def parent(self, individuals): 53 | parent1, parent2 = individuals 54 | children = [] 55 | 56 | for _ in range(self.children()): 57 | # Generate n random crossover points 58 | if self.device == "cpu": 59 | crossover_points = sorted(np.random.choice(len(parent1.item), size=self.n_points, replace=False)) 60 | child_genes = np.zeros_like(parent1.item) 61 | elif self.device == "gpu": 62 | crossover_points = sorted( 63 | ARRAY_MANAGER.random.choice(len(parent1.item), size=self.n_points, replace=False)) 64 | child_genes = ARRAY_MANAGER.zeros_like(parent1.item) 65 | 66 | # Perform n-point crossover 67 | current_parent = parent1 68 | start = 0 69 | for point in crossover_points: 70 | child_genes[start:point] = current_parent.item[start:point] 71 | current_parent = parent2 if current_parent is parent1 else parent1 72 | start = point 73 | 74 | # Fill in the last segment 75 | child_genes[start:] = current_parent.item[start:] 76 | 77 | # Create new individual 78 | child = Individual(item=child_genes, fitness_function=parent1.fitness_function) 79 | children.append(child) 80 | 81 | self.environment.add_individuals(children) 82 | 83 | 84 | class ParentUniform(Layer): 85 | def __init__(self, selection_function: Callable, families: int, children: int, crossover_rate: float = 0.5, 86 | device='cpu', refit=True): 87 | super().__init__(application_function=self.parent, selection_function=selection_function, repeat=families, refit=refit) 88 | self.children = make_callable(children) 89 | self.device = device 90 | self.crossover_rate = crossover_rate 91 | 92 | def parent(self, individuals): 93 | parent1, parent2 = individuals 94 | children = [] 95 | 96 | for _ in range(self.children()): 97 | if self.device == "cpu": 98 | mask = np.random.random(len(parent1.item)) < self.crossover_rate 99 | child_genes = np.where(mask, parent1.item, parent2.item) 100 | elif self.device == "gpu": 101 | mask = ARRAY_MANAGER.random.random(len(parent1.item)) < self.crossover_rate 102 | child_genes = ARRAY_MANAGER.where(mask, parent1.item, parent2.item) 103 | 104 | child = Individual(item=child_genes, fitness_function=parent1.fitness_function) 105 | children.append(child) 106 | 107 | self.environment.add_individuals(children) 108 | 109 | 110 | class SwapMutation(Layer): 111 | def __init__(self, selection_function, device: str = 'cpu', overpowered: bool = False, refit=True): 112 | super().__init__(application_function=self.mutate_all, selection_function=selection_function, refit=refit) 113 | self.device = device 114 | self.overpowered = overpowered 115 | 116 | def mutate_all(self, individuals: List[Individual]): 117 | for individual in individuals: 118 | self.mutate(individual) 119 | 120 | def mutate(self, individual: Individual) -> Individual: 121 | original_fitness = individual.fitness if self.overpowered else None 122 | original_item = individual.item.copy() if self.overpowered else None 123 | 124 | if self.device == "cpu": 125 | idx = np.random.choice(len(individual.item), size=2, replace=False) 126 | individual.item[idx[0]], individual.item[idx[1]] = individual.item[idx[1]], individual.item[idx[0]] 127 | elif self.device == "gpu": 128 | idx = ARRAY_MANAGER.random.choice(len(individual.item), size=2, replace=False) 129 | temp = individual.item[idx[0]].copy() 130 | individual.item[idx[0]] = individual.item[idx[1]] 131 | individual.item[idx[1]] = temp 132 | 133 | if self.overpowered: 134 | new_fitness = individual.fit() 135 | if new_fitness < original_fitness: 136 | individual.item = original_item 137 | individual.fitness = original_fitness 138 | 139 | return individual 140 | 141 | 142 | class InversionMutation(Layer): 143 | def __init__(self, selection_function, device: str = 'cpu', overpowered: bool = False, refit=True): 144 | super().__init__(application_function=self.mutate_all, selection_function=selection_function, refit=refit) 145 | self.device = device 146 | self.overpowered = overpowered 147 | 148 | def mutate_all(self, individuals: List[Individual]): 149 | for individual in individuals: 150 | self.mutate(individual) 151 | 152 | def mutate(self, individual: Individual) -> Individual: 153 | original_fitness = individual.fitness if self.overpowered else None 154 | original_item = individual.item.copy() if self.overpowered else None 155 | 156 | if self.device == "cpu": 157 | start, end = sorted(np.random.choice(len(individual.item), size=2, replace=False)) 158 | individual.item[start:end] = individual.item[start:end][::-1] 159 | elif self.device == "gpu": 160 | start, end = sorted(ARRAY_MANAGER.random.choice(len(individual.item), size=2, replace=False)) 161 | individual.item[start:end] = ARRAY_MANAGER.flip(individual.item[start:end]) 162 | 163 | if self.overpowered: 164 | new_fitness = individual.fit() 165 | if new_fitness < original_fitness: 166 | individual.item = original_item 167 | individual.fitness = original_fitness 168 | 169 | return individual 170 | 171 | 172 | class ScrambleMutation(Layer): 173 | def __init__(self, selection_function, device: str = 'cpu', overpowered: bool = False, refit=True): 174 | super().__init__(application_function=self.mutate_all, selection_function=selection_function, refit=refit) 175 | self.device = device 176 | self.overpowered = overpowered 177 | 178 | def mutate_all(self, individuals: List[Individual]): 179 | for individual in individuals: 180 | self.mutate(individual) 181 | 182 | def mutate(self, individual: Individual) -> Individual: 183 | original_fitness = individual.fitness if self.overpowered else None 184 | original_item = individual.item.copy() if self.overpowered else None 185 | 186 | if self.device == "cpu": 187 | start, end = sorted(np.random.choice(len(individual.item), size=2, replace=False)) 188 | subset = individual.item[start:end] 189 | np.random.shuffle(subset) 190 | individual.item[start:end] = subset 191 | elif self.device == "gpu": 192 | start, end = sorted(ARRAY_MANAGER.random.choice(len(individual.item), size=2, replace=False)) 193 | subset = individual.item[start:end] 194 | ARRAY_MANAGER.random.shuffle(subset) 195 | individual.item[start:end] = subset 196 | 197 | if self.overpowered: 198 | new_fitness = individual.fit() 199 | if new_fitness < original_fitness: 200 | individual.item = original_item 201 | individual.fitness = original_fitness 202 | 203 | return individual 204 | 205 | 206 | class ReplaceMutation(Layer): 207 | def __init__(self, mutation_rate: float, selection_function, possible_values: Union[List[Any], np.ndarray], 208 | device: str = 'cpu', overpowered: bool = False, refit=True): 209 | super().__init__(application_function=self.mutate_all, selection_function=selection_function, refit=refit) 210 | self.mutation_rate = mutation_rate 211 | self.possible_values = np.array(possible_values) # Convert to numpy array 212 | self.device = device 213 | self.overpowered = overpowered 214 | 215 | def mutate_all(self, individuals: List[Individual]): 216 | for individual in individuals: 217 | self.mutate(individual) 218 | 219 | def mutate(self, individual: Individual) -> Individual: 220 | original_fitness = individual.fitness if self.overpowered else None 221 | original_item = individual.item.copy() if self.overpowered else None 222 | 223 | if self.device == "cpu": 224 | mask = np.random.random(individual.item.shape) < self.mutation_rate 225 | replacements = np.random.choice(self.possible_values, size=int(np.sum(mask))) 226 | individual.item[mask] = replacements 227 | elif self.device == "gpu": 228 | mask = ARRAY_MANAGER.random.random(individual.item.shape) < self.mutation_rate 229 | replacements = ARRAY_MANAGER.random.choice(self.possible_values, size=int(ARRAY_MANAGER.sum(mask))) 230 | individual.item[mask] = replacements 231 | 232 | if self.overpowered: 233 | new_fitness = individual.fit() 234 | if new_fitness < original_fitness: 235 | individual.item = original_item 236 | individual.fitness = original_fitness 237 | 238 | return individual 239 | 240 | 241 | class InsertionDeletionMutation(Layer): 242 | def __init__(self, selection_function, gene_pool: GenePool, device: str = 'cpu', 243 | overpowered: bool = False, refit=True, insert_prob: float = 0.5): 244 | super().__init__(application_function=self.mutate_all, selection_function=selection_function, refit=refit) 245 | self.device = device 246 | self.overpowered = overpowered 247 | self.gene_pool = gene_pool 248 | self.insert_prob = insert_prob 249 | 250 | def mutate_all(self, individuals: List[Individual]): 251 | for individual in individuals: 252 | self.mutate(individual) 253 | 254 | def mutate(self, individual: Individual) -> Individual: 255 | original_fitness = individual.fitness if self.overpowered else None 256 | original_item = individual.item.copy() if self.overpowered else None 257 | 258 | if random.random() < self.insert_prob: 259 | self.insert_item(individual) 260 | else: 261 | self.delete_item(individual) 262 | 263 | if self.overpowered: 264 | new_fitness = individual.fit() 265 | if new_fitness < original_fitness: 266 | individual.item = original_item 267 | individual.fitness = original_fitness 268 | 269 | return individual 270 | 271 | def insert_item(self, individual: Individual): 272 | new_gene = self.gene_pool.generator_function().item[0] # Generate a single new gene 273 | insert_idx = random.randint(0, len(individual.item)) 274 | 275 | if self.device == "cpu": 276 | individual.item = ARRAY_MANAGER.insert(individual.item, insert_idx, new_gene) 277 | elif self.device == "gpu": 278 | individual.item = ARRAY_MANAGER.concatenate([ 279 | individual.item[:insert_idx], 280 | ARRAY_MANAGER.array([new_gene]), 281 | individual.item[insert_idx:] 282 | ]) 283 | 284 | def delete_item(self, individual: Individual): 285 | if len(individual.item) > 1: 286 | remove_idx = random.randint(0, len(individual.item) - 1) 287 | if self.device == "cpu": 288 | individual.item = ARRAY_MANAGER.delete(individual.item, remove_idx) 289 | elif self.device == "gpu": 290 | individual.item = ARRAY_MANAGER.concatenate([ 291 | individual.item[:remove_idx], 292 | individual.item[remove_idx + 1:] 293 | ]) 294 | else: 295 | # If there's only one item, insert instead of delete 296 | self.insert_item(individual) -------------------------------------------------------------------------------- /layers/float_arrays.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from Finch.universal import ARRAY_MANAGER 3 | from Finch.generic import GenePool, Individual, Layer, make_callable 4 | from typing import Callable, List, Union 5 | 6 | 7 | class FloatPool(GenePool): 8 | def __init__(self, ranges: List[List[float]], length: int, fitness_function: Callable, device="cpu"): 9 | """ 10 | A GenePool for creating individuals with float genes within specified ranges. 11 | :param ranges: List of [min, max] ranges for each gene 12 | :param length: Number of genes in each individual 13 | :param device: Where genes in Individuals should be kept 'gpu' or 'cpu' 14 | """ 15 | super().__init__(generator_function=self.generate_float_array, fitness_function=fitness_function) 16 | self.ranges = np.array(ranges) 17 | self.length = length 18 | self.device = device 19 | 20 | def generate_float_array(self): 21 | if self.device == "cpu": 22 | genes = np.random.uniform( 23 | low=self.ranges[:, 0], 24 | high=self.ranges[:, 1], 25 | size=self.length 26 | ) 27 | elif self.device == "gpu": 28 | genes = ARRAY_MANAGER.random.uniform( 29 | low=self.ranges[:, 0], 30 | high=self.ranges[:, 1], 31 | size=self.length 32 | ) 33 | return Individual(item=genes, fitness_function=self.fitness_function) 34 | 35 | 36 | class ParentBlendFloat(Layer): 37 | def __init__(self, selection_function: Callable, families: int, children: int, alpha: float = 0.5, device='cpu'): 38 | super().__init__(application_function=self.parent, selection_function=selection_function, repeat=families) 39 | self.children = make_callable(children) 40 | self.device = device 41 | self.alpha = alpha 42 | 43 | def parent(self, individuals): 44 | parent1, parent2 = individuals 45 | children = [] 46 | 47 | for _ in range(self.children()): 48 | if self.device == "cpu": 49 | gamma = np.random.uniform(-self.alpha, 1 + self.alpha, size=len(parent1.item)) 50 | child_genes = parent1.item + gamma * (parent2.item - parent1.item) 51 | elif self.device == "gpu": 52 | gamma = ARRAY_MANAGER.random.uniform(-self.alpha, 1 + self.alpha, size=len(parent1.item)) 53 | child_genes = parent1.item + gamma * (parent2.item - parent1.item) 54 | 55 | child = Individual(item=child_genes, fitness_function=parent1.fitness_function) 56 | children.append(child) 57 | 58 | self.environment.add_individuals(children) 59 | 60 | 61 | class ParentSimulatedBinaryFloat(Layer): 62 | def __init__(self, selection_function: Callable, families: int, children: int, eta: float = 1.0, device='cpu'): 63 | super().__init__(application_function=self.parent, selection_function=selection_function, repeat=families) 64 | self.children = make_callable(children) 65 | self.device = device 66 | self.eta = eta 67 | 68 | def parent(self, individuals): 69 | parent1, parent2 = individuals 70 | children = [] 71 | 72 | for _ in range(self.children()): 73 | if self.device == "cpu": 74 | u = np.random.random(len(parent1.item)) 75 | beta = np.where(u <= 0.5, 76 | (2 * u) ** (1 / (self.eta + 1)), 77 | (1 / (2 * (1 - u))) ** (1 / (self.eta + 1))) 78 | child_genes = 0.5 * ((1 + beta) * parent1.item + (1 - beta) * parent2.item) 79 | elif self.device == "gpu": 80 | u = ARRAY_MANAGER.random.random(len(parent1.item)) 81 | beta = ARRAY_MANAGER.where(u <= 0.5, 82 | (2 * u) ** (1 / (self.eta + 1)), 83 | (1 / (2 * (1 - u))) ** (1 / (self.eta + 1))) 84 | child_genes = 0.5 * ((1 + beta) * parent1.item + (1 - beta) * parent2.item) 85 | 86 | child = Individual(item=child_genes, fitness_function=parent1.fitness_function) 87 | children.append(child) 88 | 89 | self.environment.add_individuals(children) 90 | 91 | 92 | class ParentArithmeticFloat(Layer): 93 | def __init__(self, selection_function: Callable, families: int, children: int, alpha: Union[float, str] = 'uniform', 94 | device='cpu'): 95 | super().__init__(application_function=self.parent, selection_function=selection_function, repeat=families) 96 | self.children = make_callable(children) 97 | self.device = device 98 | self.alpha = alpha 99 | 100 | def parent(self, individuals): 101 | parent1, parent2 = individuals 102 | children = [] 103 | 104 | for _ in range(self.children()): 105 | if self.alpha == 'uniform': 106 | if self.device == "cpu": 107 | alpha = np.random.random() 108 | elif self.device == "gpu": 109 | alpha = float(ARRAY_MANAGER.random.random()) 110 | else: 111 | alpha = self.alpha 112 | 113 | child_genes = alpha * parent1.item + (1 - alpha) * parent2.item 114 | 115 | child = Individual(item=child_genes, fitness_function=parent1.fitness_function) 116 | children.append(child) 117 | 118 | self.environment.add_individuals(children) 119 | 120 | 121 | class GaussianMutation(Layer): 122 | def __init__(self, mutation_rate: float, sigma: float, selection_function: Callable, device: str = 'cpu', 123 | overpowered: bool = False): 124 | super().__init__(application_function=self.mutate_all, selection_function=selection_function) 125 | self.mutation_rate = mutation_rate 126 | self.sigma = sigma 127 | self.device = device 128 | self.overpowered = overpowered 129 | 130 | def mutate_all(self, individuals: List[Individual]): 131 | for individual in individuals: 132 | self.mutate(individual) 133 | 134 | def mutate(self, individual: Individual) -> Individual: 135 | original_fitness = individual.fitness if self.overpowered else None 136 | original_item = individual.item.copy() if self.overpowered else None 137 | 138 | if self.device == "cpu": 139 | mask = np.random.random(individual.item.shape) < self.mutation_rate 140 | mutation = np.random.normal(0, self.sigma, individual.item.shape) 141 | individual.item = np.where(mask, individual.item + mutation, individual.item) 142 | elif self.device == "gpu": 143 | mask = ARRAY_MANAGER.random.random(individual.item.shape) < self.mutation_rate 144 | mutation = ARRAY_MANAGER.random.normal(0, self.sigma, individual.item.shape) 145 | individual.item = ARRAY_MANAGER.where(mask, individual.item + mutation, individual.item) 146 | 147 | if self.overpowered: 148 | new_fitness = individual.fit() 149 | if new_fitness < original_fitness: 150 | individual.item = original_item 151 | individual.fitness = original_fitness 152 | 153 | return individual 154 | 155 | 156 | class UniformMutation(Layer): 157 | def __init__(self, mutation_rate: float, lower_bound: float, upper_bound: float, selection_function: Callable, 158 | device: str = 'cpu', overpowered: bool = False, refit=True): 159 | super().__init__(application_function=self.mutate_all, selection_function=selection_function, refit=refit) 160 | self.mutation_rate = mutation_rate 161 | self.lower_bound = lower_bound 162 | self.upper_bound = upper_bound 163 | self.device = device 164 | self.overpowered = overpowered 165 | 166 | def mutate_all(self, individuals: List[Individual]): 167 | for individual in individuals: 168 | self.mutate(individual) 169 | 170 | def mutate(self, individual: Individual) -> Individual: 171 | original_fitness = individual.fitness if self.overpowered else None 172 | original_item = individual.item.copy() if self.overpowered else None 173 | 174 | if self.device == "cpu": 175 | mask = np.random.random(individual.item.shape) < self.mutation_rate 176 | mutation = np.random.uniform(self.lower_bound, self.upper_bound, individual.item.shape) 177 | individual.item = np.where(mask, mutation, individual.item) 178 | elif self.device == "gpu": 179 | mask = ARRAY_MANAGER.random.random(individual.item.shape) < self.mutation_rate 180 | mutation = ARRAY_MANAGER.random.uniform(self.lower_bound, self.upper_bound, individual.item.shape) 181 | individual.item = ARRAY_MANAGER.where(mask, mutation, individual.item) 182 | 183 | if self.overpowered: 184 | new_fitness = individual.fit() 185 | if new_fitness < original_fitness: 186 | individual.item = original_item 187 | individual.fitness = original_fitness 188 | 189 | return individual 190 | 191 | 192 | class PolynomialMutation(Layer): 193 | def __init__(self, mutation_rate: float, eta: float, bounds: List[List[float]], selection_function: Callable, 194 | device: str = 'cpu', overpowered: bool = False): 195 | super().__init__(application_function=self.mutate_all, selection_function=selection_function) 196 | self.mutation_rate = mutation_rate 197 | self.eta = eta 198 | self.bounds = np.array(bounds) 199 | self.device = device 200 | self.overpowered = overpowered 201 | 202 | def mutate_all(self, individuals: List[Individual]): 203 | for individual in individuals: 204 | self.mutate(individual) 205 | 206 | def mutate(self, individual: Individual) -> Individual: 207 | original_fitness = individual.fitness if self.overpowered else None 208 | original_item = individual.item.copy() if self.overpowered else None 209 | 210 | if self.device == "cpu": 211 | mask = np.random.random(individual.item.shape) < self.mutation_rate 212 | u = np.random.random(individual.item.shape) 213 | delta = np.where( 214 | u < 0.5, 215 | (2 * u) ** (1 / (self.eta + 1)) - 1, 216 | 1 - (2 * (1 - u)) ** (1 / (self.eta + 1)) 217 | ) 218 | lower, upper = self.bounds[:, 0], self.bounds[:, 1] 219 | mutation = individual.item + delta * (upper - lower) 220 | individual.item = np.where(mask, np.clip(mutation, lower, upper), individual.item) 221 | elif self.device == "gpu": 222 | mask = ARRAY_MANAGER.random.random(individual.item.shape) < self.mutation_rate 223 | u = ARRAY_MANAGER.random.random(individual.item.shape) 224 | delta = ARRAY_MANAGER.where( 225 | u < 0.5, 226 | (2 * u) ** (1 / (self.eta + 1)) - 1, 227 | 1 - (2 * (1 - u)) ** (1 / (self.eta + 1)) 228 | ) 229 | lower, upper = self.bounds[:, 0], self.bounds[:, 1] 230 | mutation = individual.item + delta * (upper - lower) 231 | individual.item = ARRAY_MANAGER.where(mask, ARRAY_MANAGER.clip(mutation, lower, upper), individual.item) 232 | 233 | if self.overpowered: 234 | new_fitness = individual.fit() 235 | if new_fitness < original_fitness: 236 | individual.item = original_item 237 | individual.fitness = original_fitness 238 | 239 | return individual 240 | 241 | 242 | class InsertionDeletionMutationFloat(Layer): 243 | def __init__(self, selection_function: Callable, device: str = 'cpu', overpowered: bool = False): 244 | super().__init__(application_function=self.mutate_all, selection_function=selection_function) 245 | self.device = device 246 | self.overpowered = overpowered 247 | 248 | def mutate_all(self, individuals: List[Individual]): 249 | for individual in individuals: 250 | self.mutate(individual) 251 | 252 | def mutate(self, individual: Individual) -> Individual: 253 | # If there's only one gene, don't perform the mutation 254 | if len(individual.item) <= 1: 255 | return individual 256 | 257 | original_fitness = individual.fitness if self.overpowered else None 258 | original_item = individual.item.copy() if self.overpowered else None 259 | 260 | if self.device == "cpu": 261 | # Select a random gene to remove 262 | remove_idx = np.random.randint(0, len(individual.item)) 263 | # Select a random position to insert (can be the same as remove_idx) 264 | insert_idx = np.random.randint(0, len(individual.item)) 265 | 266 | # Remove the gene and insert it at the new position 267 | gene = individual.item[remove_idx] 268 | individual.item = np.delete(individual.item, remove_idx) 269 | individual.item = np.insert(individual.item, insert_idx, gene) 270 | 271 | elif self.device == "gpu": 272 | # Select a random gene to remove 273 | remove_idx = int(ARRAY_MANAGER.random.randint(0, len(individual.item))) 274 | # Select a random position to insert (can be the same as remove_idx) 275 | insert_idx = int(ARRAY_MANAGER.random.randint(0, len(individual.item))) 276 | 277 | # Remove the gene and insert it at the new position 278 | gene = individual.item[remove_idx].copy() 279 | individual.item = ARRAY_MANAGER.delete(individual.item, remove_idx) 280 | individual.item = ARRAY_MANAGER.insert(individual.item, insert_idx, gene) 281 | 282 | if self.overpowered: 283 | new_fitness = individual.fit() 284 | if new_fitness < original_fitness: 285 | individual.item = original_item 286 | individual.fitness = original_fitness 287 | 288 | return individual 289 | 290 | -------------------------------------------------------------------------------- /layers/image_layers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import Image, ImageDraw 3 | from Finch.universal import ARRAY_MANAGER 4 | from Finch.generic import GenePool, Individual, Layer, make_callable 5 | from typing import Callable, List, Tuple, Union 6 | 7 | class ImagePool(GenePool): 8 | def __init__(self, width: int, height: int, channels: int, fitness_function: Callable, device="cpu"): 9 | super().__init__(generator_function=self.generate_image, fitness_function=fitness_function) 10 | self.width = width 11 | self.height = height 12 | self.channels = channels 13 | self.device = device 14 | 15 | def generate_image(self): 16 | if self.device == "cpu": 17 | image = np.random.randint(0, 256, (self.height, self.width, self.channels), dtype=np.uint8) 18 | image = Image.fromarray(image) 19 | elif self.device == "gpu": 20 | image = ARRAY_MANAGER.random.randint(0, 256, (self.height, self.width, self.channels), dtype=ARRAY_MANAGER.uint8) 21 | image = Image.fromarray(image) 22 | return Individual(item=image, fitness_function=self.fitness_function) 23 | 24 | -------------------------------------------------------------------------------- /layers/universal_layers.py: -------------------------------------------------------------------------------- 1 | from Finch.universal import ARRAY_MANAGER 2 | from Finch.generic import Layer 3 | from Finch.generic import GenePool 4 | from Finch.generic import make_callable 5 | from typing import Union, Callable 6 | 7 | 8 | class Populate(Layer): 9 | def __init__(self, population: Union[Callable, int], gene_pool: GenePool): 10 | super().__init__(application_function=self.populate, selection_function=lambda x: x, repeat=1, refit=False) 11 | self.population = make_callable(population) 12 | self.gene_pool = gene_pool 13 | 14 | def populate(self, individuals): 15 | new_individuals = [] 16 | needed = self.population() - len(individuals) 17 | if needed > 0: 18 | new_individuals.extend(self.gene_pool.generate_individuals(needed)) 19 | self.environment.add_individuals(new_individuals) 20 | 21 | 22 | class SortByFitness(Layer): 23 | def __init__(self): 24 | super().__init__(application_function=self.sort, selection_function=lambda x: x, repeat=1, refit=False) 25 | 26 | def sort(self, individuals): 27 | self.environment.individuals = list(sorted(individuals, key=lambda individual: -individual.fitness)) 28 | 29 | 30 | class CapPopulation(Layer): 31 | def __init__(self, max_population: Union[Callable, int]): 32 | super().__init__(application_function=self.cap_population, selection_function=lambda x: x, repeat=1, refit=False) 33 | self.max_population = make_callable(max_population) 34 | 35 | def cap_population(self, individuals): 36 | self.environment.individuals = individuals[:self.max_population()] 37 | -------------------------------------------------------------------------------- /prompt_maker.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def format_py_files(): 5 | # Get all .py files in the current directory and subdirectories 6 | py_files = [] 7 | for root, dirs, files in os.walk('.'): 8 | for file in files: 9 | if file.endswith('.py'): 10 | py_files.append(os.path.join(root, file)) 11 | 12 | # Create or overwrite the output file 13 | with open('formatted_py_files.txt', 'w') as output_file: 14 | for py_file in py_files: 15 | # Write the filename with its relative path 16 | output_file.write(f"{py_file}\n") 17 | 18 | # Read and write the contents of the .py file 19 | with open(py_file, 'r') as file: 20 | output_file.write(file.read()) 21 | 22 | # Add a newline between files for better readability 23 | output_file.write('\n\n') 24 | 25 | print("Formatting complete. Check 'formatted_py_files.txt' for the result.") 26 | 27 | 28 | if __name__ == "__main__": 29 | format_py_files() -------------------------------------------------------------------------------- /rates.py: -------------------------------------------------------------------------------- 1 | import random 2 | import matplotlib.pyplot as plt 3 | from typing import Union 4 | 5 | 6 | class Rate: 7 | def __init__(self, start, end, epochs, return_int=False): 8 | """ 9 | :param start: The starting value (int or float) 10 | :param end: The target value after epochs 11 | :param epochs: The number of epochs to reach the target value 12 | :param return_int: Whether to return only integers 13 | """ 14 | self.start = start 15 | self.value = start # use a more descriptive name than start 16 | self.end = end 17 | self.epochs = epochs 18 | self.change_rate = (end - start) / epochs # use underscores for variable names 19 | self.return_int = return_int # use the same name as the parameter 20 | 21 | def next(self): 22 | # return the current value and update it by the change rate 23 | if self.value < self.end < self.start: 24 | return self.end 25 | if self.value > self.end > self.start: 26 | return self.end 27 | result = self.value 28 | self.value += self.change_rate 29 | 30 | if self.return_int: 31 | return int(result) 32 | return result 33 | 34 | def get(self): 35 | # return the current value without updating it 36 | if self.return_int: 37 | return int(self.value) 38 | else: 39 | return self.value 40 | 41 | def graph(self): 42 | # plot the history of the value over epochs 43 | history = [] 44 | temp = self.value # store the current value temporarily 45 | for i in range(self.epochs): 46 | history.append(temp) 47 | temp += self.change_rate 48 | print("min", min(history)) 49 | print("max", max(history)) 50 | plt.plot(history) 51 | plt.show() 52 | self.value = temp # restore the current value 53 | 54 | 55 | def make_switcher(x) -> callable(any): 56 | x = make_callable(x) 57 | n = x() 58 | 59 | def r(): 60 | # return a random choice of -n or n 61 | return random.choice([-n, n]) 62 | 63 | return r 64 | 65 | 66 | def make_callable(x: Union[callable, int, float]) -> callable: 67 | if x is None: 68 | return None 69 | if not callable(x): 70 | # return a function that always returns x 71 | def constant() -> int | float: 72 | return x 73 | 74 | return constant 75 | else: 76 | return x 77 | -------------------------------------------------------------------------------- /selectors.py: -------------------------------------------------------------------------------- 1 | import random 2 | from Finch.generic import Individual, make_callable 3 | 4 | 5 | 6 | class Select: 7 | """ 8 | Base class for selection strategies. 9 | 10 | Parameters: 11 | - percent_to_select: A callable returning the percentage of individuals to select. 12 | - amount_to_select: A callable returning the number of individuals to select. 13 | 14 | Usage: 15 | ``` 16 | selector = Select(percent_to_select=lambda: 0.2) 17 | selected_individuals = selector.select(individuals) 18 | ``` 19 | """ 20 | 21 | def __init__(self, percent_to_select=None, amount_to_select=None): 22 | if percent_to_select is not None and amount_to_select is not None: 23 | raise ValueError("Only one of percent_to_select or amount_to_select can be given") 24 | 25 | self.percent_to_select = make_callable(percent_to_select) 26 | 27 | self.amount_to_select = make_callable(amount_to_select) 28 | if percent_to_select is None: 29 | self.percent_to_select = None 30 | if amount_to_select is None: 31 | self.amount_to_select = None 32 | 33 | def select(self, individuals: list[Individual]): 34 | """ 35 | Abstract method for selecting individuals. 36 | 37 | Parameters: 38 | - individuals: List of individuals to select from. 39 | 40 | Returns: 41 | - list[Individual]: Selected individuals. 42 | """ 43 | pass 44 | 45 | 46 | class TournamentSelection(Select): 47 | """ 48 | Tournament selection strategy. 49 | 50 | Parameters: 51 | - percent_to_select: A callable returning the percentage of individuals to select. 52 | - amount_to_select: A callable returning the number of individuals to select. 53 | 54 | Usage: 55 | ``` 56 | selector = TournamentSelection(percent_to_select=lambda: 0.2) 57 | selected_individuals = selector.select(individuals) 58 | ``` 59 | """ 60 | 61 | def __init__(self, percent_to_select=None, amount_to_select=None): 62 | super().__init__(percent_to_select, amount_to_select) 63 | 64 | def select(self, individuals): 65 | """ 66 | Select individuals using tournament selection. 67 | 68 | Parameters: 69 | - individuals: List of individuals to select from. 70 | 71 | Returns: 72 | - list[Individual]: Selected individuals. 73 | """ 74 | selected_individuals = [] 75 | 76 | if self.percent_to_select is not None: 77 | amount = int(self.percent_to_select() * len(individuals)) 78 | else: 79 | amount = self.amount_to_select() 80 | 81 | for _ in range(amount): 82 | tournament_size = len(individuals) 83 | tournament_individuals = random.sample(individuals, k=tournament_size) 84 | winner = max(tournament_individuals, key=lambda individual: individual.fitness) 85 | selected_individuals.append(winner) 86 | 87 | return selected_individuals 88 | 89 | 90 | class RandomSelection(Select): 91 | """ 92 | Random selection strategy. 93 | 94 | Parameters: 95 | - percent_to_select: A callable returning the percentage of individuals to select. 96 | - amount_to_select: A callable returning the number of individuals to select. 97 | 98 | Usage: 99 | ``` 100 | selector = RandomSelection(percent_to_select=lambda: 0.2) 101 | selected_individuals = selector.select(individuals) 102 | ``` 103 | """ 104 | 105 | def __init__(self, percent_to_select=None, amount_to_select=None): 106 | super().__init__(percent_to_select, amount_to_select) 107 | 108 | def select(self, individuals): 109 | """ 110 | Select individuals randomly. 111 | 112 | Parameters: 113 | - individuals: List of individuals to select from. 114 | 115 | Returns: 116 | - list[Individual]: Selected individuals. 117 | """ 118 | if self.percent_to_select is not None: 119 | amount = int(self.percent_to_select() * len(individuals)) 120 | else: 121 | amount = self.amount_to_select() 122 | selected_individuals = random.choices(individuals, k=amount) 123 | return selected_individuals 124 | 125 | 126 | class RankBasedSelection(Select): 127 | """ 128 | Rank-based selection strategy. 129 | 130 | Parameters: 131 | - factor: Selection pressure factor. 132 | - percent_to_select: A callable returning the percentage of individuals to select. 133 | - amount_to_select: A callable returning the number of individuals to select. 134 | 135 | Usage: 136 | ``` 137 | selector = RankBasedSelection(factor=1.5, percent_to_select=lambda: 0.2) 138 | selected_individuals = selector.select(individuals) 139 | ``` 140 | """ 141 | 142 | def __init__(self, factor, percent_to_select=None, amount_to_select=None): 143 | super().__init__(percent_to_select, amount_to_select) 144 | self.factor = factor 145 | 146 | def select(self, individuals): 147 | """ 148 | Select individuals using rank-based selection. 149 | 150 | Parameters: 151 | - individuals: List of individuals to select from. 152 | 153 | Returns: 154 | - list[Individual]: Selected individuals. 155 | """ 156 | population_size = len(individuals) 157 | ranks = list(range(1, population_size + 1)) 158 | 159 | selection_probs = [pow(2.71828, -self.factor * rank / population_size) for rank in ranks] 160 | 161 | sum_probs = sum(selection_probs) 162 | selection_probs = [prob / sum_probs for prob in selection_probs] 163 | 164 | if self.percent_to_select is not None: 165 | amount = int(self.percent_to_select() * len(individuals)) 166 | else: 167 | amount = self.amount_to_select() 168 | 169 | selected_indices = random.choices(range(population_size), weights=selection_probs, k=amount) 170 | selected_individuals = [individuals[i] for i in selected_indices] 171 | 172 | return selected_individuals 173 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dadukhankevin/Finch/442d4984df1a52003b5ed434bd42455325d54770/tests/tests.py -------------------------------------------------------------------------------- /universal.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Set the array manager 4 | ARRAY_MANAGER = np 5 | 6 | def use_cupy(): 7 | global ARRAY_MANAGER 8 | import cupy 9 | ARRAY_MANAGER = cupy --------------------------------------------------------------------------------