import random import numpy as np import streamlit as st # import all functions from src.backend.chatbot from src.backend.chatbot import * def genetic_algorithm_plants(model, demo_lite): # Define the compatibility matrix compatibility_matrix = st.session_state.full_mat # Define the list of plants plant_list = st.session_state.plant_list # Define the user-selected plants, number of plant beds, and constraints user_plants = st.session_state.input_plants_raw num_plant_beds = st.session_state.n_plant_beds # 1 <= min_species_per_bed <= max_species_per_bed <= len(user_plants) min_species_per_bed = st.session_state.min_species # max_species_per_bed >= floor(length(user_plants)-(min_species_per_bed*num_plant_beds-1) & max_species_per_bed <= len(user_plants) max_species_per_bed = st.session_state.max_species # Genetic Algorithm parameters population_size = st.session_state.population_size num_generations = st.session_state.num_generations tournament_size = st.session_state.tournament_size crossover_rate = st.session_state.crossover_rate mutation_rate = st.session_state.mutation_rate seed_population_rate = st.session_state.seed_population_rate def generate_initial_population(model, demo_lite): population = [] # Add seed groupings to the population, validated and replaced as necessary num_seeds = int( population_size * st.session_state.seed_population_rate ) # 10% of the population as seeds # we generate just one seed grouping for this beta language model suggestion feature seed_grouping = get_language_model_suggestions(model, demo_lite) if seed_grouping != "no response yet": valid_seed_grouping = validate_and_replace(seed_grouping) population.append(valid_seed_grouping) # Fill the rest of the population with random groupings, also validated and replaced while len(population) < population_size: random_grouping = generate_random_grouping() valid_random_grouping = validate_and_replace(random_grouping) population.append(valid_random_grouping) return population def generate_random_grouping(): random.shuffle(user_plants) remaining_plants = user_plants.copy() grouping = [] total_plants = len(user_plants) plants_per_bed = total_plants // num_plant_beds extra_plants = total_plants % num_plant_beds for bed_index in range(num_plant_beds): if bed_index < extra_plants: # Distribute extra plants among the first few beds num_species_in_bed = plants_per_bed + 1 else: num_species_in_bed = plants_per_bed # Ensure the bed size is within the min and max constraints num_species_in_bed = max( min_species_per_bed, min(num_species_in_bed, max_species_per_bed) ) bed = remaining_plants[:num_species_in_bed] remaining_plants = remaining_plants[num_species_in_bed:] grouping.append(bed) return grouping # Perform crossover between two parents, preserving at least one occurrence of each plant def crossover(parent1, parent2): if random.random() < crossover_rate: crossover_point = random.randint(1, num_plant_beds - 1) child1 = parent1[:crossover_point] + parent2[crossover_point:] child2 = parent2[:crossover_point] + parent1[crossover_point:] # Ensure each plant appears at least once in the offspring for plant in user_plants: if all(plant not in bed for bed in child1): # Find a bed with fewer species and add the missing plant min_bed_index = min( range(len(child1)), key=lambda i: len(child1[i]) ) child1[min_bed_index].append(plant) if all(plant not in bed for bed in child2): # Find a bed with fewer species and add the missing plant min_bed_index = min( range(len(child2)), key=lambda i: len(child2[i]) ) child2[min_bed_index].append(plant) return child1, child2 else: return parent1, parent2 # Perform mutation on an individual, ensuring no bed exceeds the maximum species constraint def mutate(individual): if random.random() < mutation_rate: mutated_bed = random.randint(0, num_plant_beds - 1) species_in_bed = individual[mutated_bed] # Remove excess species if there are more than the maximum constraint if len(species_in_bed) > max_species_per_bed: species_in_bed = random.sample(species_in_bed, max_species_per_bed) # Add missing plants by performing swaps between current species and missing plants missing_plants = [ plant for plant in user_plants if plant not in species_in_bed ] num_missing_plants = min( len(missing_plants), max_species_per_bed - len(species_in_bed) ) for _ in range(num_missing_plants): swap_species = random.choice(missing_plants) missing_plants.remove(swap_species) species_in_bed.append(swap_species) species_in_bed.remove(random.choice(species_in_bed)) individual[mutated_bed] = species_in_bed return individual # Calculate the fitness score of the grouping def calculate_fitness(grouping): positive_reward_factor = ( 1000 # Adjust this to increase the reward for compatible species ) negative_penalty_factor = ( 2000 # Adjust this to increase the penalty for incompatible species ) # Define penalties for not meeting constraints penalty_for_exceeding_max = 500 # Adjust as needed penalty_for_not_meeting_min = 500 # Adjust as needed penalty_for_not_having_all_plants = 1000 # Adjust as needed score = 0 # Iterate over each plant bed for bed in grouping: for i in range(len(bed)): for j in range(i + 1, len(bed)): # get the plant name species1_name = bed[i] species2_name = bed[j] species1_index = plant_list.index(species1_name) species2_index = plant_list.index(species2_name) # Compatibility score between two species in the same bed compatibility_score = compatibility_matrix[species1_index][ species2_index ] if compatibility_score > 0: # Positive reward for compatible species score += compatibility_score * positive_reward_factor elif compatibility_score < 0: # Negative penalty for incompatible species score += compatibility_score * negative_penalty_factor # Apply penalties for not meeting constraints if len(bed) > max_species_per_bed: score -= penalty_for_exceeding_max if len(bed) < min_species_per_bed: score -= penalty_for_not_meeting_min if len(set(plant for bed in grouping for plant in bed)) < len(user_plants): score -= penalty_for_not_having_all_plants return score # Perform tournament selection def tournament_selection(population): selected = [] for _ in range(population_size): participants = random.sample(population, tournament_size) winner = max(participants, key=calculate_fitness) selected.append(winner) return selected # Perform replacement of the population with the offspring, ensuring maximum species constraint is met def replacement(population, offspring): sorted_population = sorted(population, key=calculate_fitness, reverse=True) sorted_offspring = sorted(offspring, key=calculate_fitness, reverse=True) # Adjust the offspring to meet the maximum species constraint adjusted_offspring = [] for individual in sorted_offspring: for bed_idx in range(num_plant_beds): species_in_bed = individual[bed_idx] if len(species_in_bed) > max_species_per_bed: species_in_bed = random.sample(species_in_bed, max_species_per_bed) individual[bed_idx] = species_in_bed adjusted_offspring.append(individual) return ( sorted_population[: population_size - len(adjusted_offspring)] + adjusted_offspring ) # Genetic Algorithm main function def genetic_algorithm(model, demo_lite): population = generate_initial_population(model, demo_lite) for generation in range(num_generations): print(f"Generation {generation + 1}") selected_population = tournament_selection(population) offspring = [] for _ in range(population_size // 2): parent1 = random.choice(selected_population) parent2 = random.choice(selected_population) child1, child2 = crossover(parent1, parent2) child1 = mutate(child1) child2 = mutate(child2) offspring.extend([child1, child2]) population = replacement(population, offspring) # Validate and replace any missing plants in the new population population = [validate_and_replace(grouping) for grouping in population] best_grouping = max(population, key=calculate_fitness) best_grouping = validate_and_replace(best_grouping) best_fitness = calculate_fitness(best_grouping) print(f"Best Grouping: {best_grouping}") print(f"Fitness Score: {best_fitness}") st.session_state.best_grouping = best_grouping st.session_state.best_fitness = best_fitness # st.write(f"Best Grouping: {best_grouping}") # st.write(f"Fitness Score: {best_fitness}") return best_grouping # def validate_and_replace(grouping): # print("Grouping structure before validation:", grouping) # all_plants = set(user_plants) # for bed in grouping: # all_plants -= set(bed) # # Replace missing plants # for missing_plant in all_plants: # replaced = False # for bed in grouping: # if len(set(bed)) != len(bed): # Check for duplicates # for i, plant in enumerate(bed): # if bed.count(plant) > 1: # Found a duplicate # bed[i] = missing_plant # replaced = True # break # if replaced: # break # # If no duplicates were found, replace a random plant # if not replaced: # random_bed = random.choice(grouping) # random_bed[random.randint(0, len(random_bed) - 1)] = missing_plant # return grouping ############ ############ experimental def adjust_grouping(grouping): # Determine the plants that are missing in the grouping plants_in_grouping = set(plant for bed in grouping for plant in bed) missing_plants = set(user_plants) - plants_in_grouping for missing_plant in missing_plants: # Find a bed that can accommodate the missing plant without exceeding max_species_per_bed suitable_bed = next( (bed for bed in grouping if len(bed) < max_species_per_bed), None ) if suitable_bed is not None: suitable_bed.append(missing_plant) else: # If no suitable bed is found, replace a random plant in a random bed random_bed = random.choice(grouping) random_bed[random.randint(0, len(random_bed) - 1)] = missing_plant # Ensure min_species_per_bed and max_species_per_bed constraints for bed in grouping: while len(bed) < min_species_per_bed: additional_plant = random.choice( [plant for plant in user_plants if plant not in bed] ) bed.append(additional_plant) while len(bed) > max_species_per_bed: bed.remove(random.choice(bed)) return grouping def validate_and_replace(grouping): best_grouping = None best_fitness = float("-inf") for _ in range(5): # Generate 5 different configurations temp_grouping = [bed.copy() for bed in grouping] temp_grouping = adjust_grouping(temp_grouping) current_fitness = calculate_fitness(temp_grouping) if current_fitness > best_fitness: best_fitness = current_fitness best_grouping = temp_grouping return best_grouping ############ def get_language_model_suggestions(model, demo_lite): # This returns a list of seed groupings based on the compatibility matrix st.session_state.seed_groupings = get_seed_groupings_from_LLM(model, demo_lite) return st.session_state.seed_groupings # Run the genetic algorithm best_grouping = genetic_algorithm(model, demo_lite) return best_grouping