Spaces:
Runtime error
Runtime error
import copy | |
import random | |
from typing import Callable, Optional, Tuple | |
def initialize_population(services: dict, users: dict, population_size: int) -> list: | |
""" | |
Initialize the population of assignment solutions for the genetic algorithm. | |
Args: | |
services (dict): A dictionary containing service constraints. | |
users (dict): A dictionary containing user preferences and constraints. | |
population_size (int): The number of assignment solutions to generate. | |
Returns: | |
list: A list of generated assignment solutions. | |
""" | |
population = [] | |
# Generate population_size number of assignment solutions | |
for _ in range(population_size): | |
assignment_solution = {} | |
for service in services.keys(): | |
# Randomly assign users to each service, while considering user preferences and constraints | |
assigned_users = [] | |
for user, user_info in users.items(): | |
# Check if user cannot be assigned to this service | |
if service not in user_info["cannot_assign"]: | |
# Assign user to service based on their preference | |
if service in user_info["preferences"]: | |
assigned_users.append(user) | |
# Assign user to service with a small probability if not in their preferences | |
elif random.random() < 0.1: | |
assigned_users.append(user) | |
# Shuffle the list of assigned users to create random assignments | |
random.shuffle(assigned_users) | |
assignment_solution[service] = assigned_users | |
# Add the generated assignment solution to the population | |
population.append(assignment_solution) | |
return population | |
def calculate_fitness(population: list, services: dict, users: dict, fitness_fn: Optional[Callable] = None) -> list: | |
""" | |
Calculate the fitness of each assignment solution in the population. | |
Args: | |
population (list): A list of assignment solutions. | |
services (dict): A dictionary containing service constraints. | |
users (dict): A dictionary containing user preferences and constraints. | |
fitness_fn (Optional[Callable]): An optional custom fitness function. | |
Returns: | |
list: A list of fitness scores for each assignment solution in the population. | |
""" | |
if not fitness_fn: | |
fitness_fn = default_fitness_function | |
fitness_scores = [] | |
# Calculate the fitness score for each assignment solution in the population | |
for assignment_solution in population: | |
fitness_score = fitness_fn(assignment_solution, services, users) | |
fitness_scores.append(fitness_score) | |
return fitness_scores | |
def default_fitness_function(assignment_solution: dict, services: dict, users: dict) -> float: | |
""" | |
Calculate the fitness of an assignment solution based on the criteria described in the problem statement, | |
including user preferences and cannot_assign constraints. | |
Args: | |
assignment_solution (dict): An assignment solution to evaluate. | |
services (dict): A dictionary containing service constraints. | |
users (dict): A dictionary containing user preferences and constraints. | |
Returns: | |
float: The fitness score of the given assignment solution. | |
""" | |
fitness = 0 | |
for service, assigned_users in assignment_solution.items(): | |
service_info = services[service] | |
num_assigned_users = len(assigned_users) | |
# Bonus for solutions that assign users near the recommended value | |
if service_info["min"] <= num_assigned_users <= service_info["max"]: | |
fitness += abs(num_assigned_users - service_info["rec"]) | |
# Punish solutions that assign users below the minimum value | |
elif num_assigned_users < service_info["min"]: | |
fitness -= (service_info["min"] - num_assigned_users) * service_info["priority"] | |
# Punish solutions that assign users above the maximum value | |
else: # num_assigned_users > service_info["max"]: | |
fitness -= (num_assigned_users - service_info["max"]) * service_info["priority"] | |
# Punish solutions that assign users to their cannot_assign services | |
for user in assigned_users: | |
if service in users[user]["cannot_assign"]: | |
fitness -= 100 * service_info["priority"] | |
# Bonus solutions that assign users to their preferred services | |
for user, user_info in users.items(): | |
if service in user_info["preferences"] and user in assigned_users: | |
fitness += 10 | |
return -fitness | |
def selection(fitness_scores: list) -> Tuple[int, int]: | |
""" | |
Select two parent solutions from the population based on their fitness scores. | |
Args: | |
fitness_scores (list): A list of fitness scores for each assignment solution in the population. | |
Returns: | |
Tuple[int, int]: The indices of the two selected parent solutions in the population. | |
""" | |
# Calculate the total fitness of the population | |
total_fitness = sum(fitness_scores) | |
# Calculate the relative fitness of each solution | |
relative_fitness = [f / total_fitness for f in fitness_scores] | |
# Select the first parent using roulette wheel selection | |
parent1_index = -1 | |
r = random.random() | |
accumulator = 0 | |
for i, rf in enumerate(relative_fitness): | |
accumulator += rf | |
if accumulator >= r: | |
parent1_index = i | |
break | |
# Select the second parent using roulette wheel selection, ensuring it's different from the first parent | |
parent2_index = -1 | |
while parent2_index == -1 or parent2_index == parent1_index: | |
r = random.random() | |
accumulator = 0 | |
for i, rf in enumerate(relative_fitness): | |
accumulator += rf | |
if accumulator >= r: | |
parent2_index = i | |
break | |
return parent1_index, parent2_index | |
def crossover(parent1: dict, parent2: dict) -> dict: | |
""" | |
Combine two parent assignment solutions to create a child solution. | |
Args: | |
parent1 (dict): The first parent assignment solution. | |
parent2 (dict): The second parent assignment solution. | |
Returns: | |
dict: The child assignment solution created by combining the parents. | |
""" | |
child_solution = {} | |
# Iterate over the services in the parents | |
for service in parent1.keys(): | |
# Create two sets of users assigned to the current service in parent1 and parent2 | |
assigned_users_parent1 = set(parent1[service]) | |
assigned_users_parent2 = set(parent2[service]) | |
# Perform set union to combine users assigned in both parents | |
combined_assigned_users = assigned_users_parent1 | assigned_users_parent2 | |
# Randomly assign each user from the combined set to the child solution | |
child_assigned_users = [] | |
for user in combined_assigned_users: | |
if random.random() < 0.5: | |
child_assigned_users.append(user) | |
child_solution[service] = child_assigned_users | |
return child_solution | |
def mutation(solution: dict, users: dict, mutation_rate: float = 0.01) -> dict: | |
""" | |
Mutate an assignment solution by randomly reassigning users to services. | |
Args: | |
solution (dict): The assignment solution to mutate. | |
users (dict): A dictionary containing user preferences and constraints. | |
mutation_rate (float): The probability of mutation for each user in the solution (default: 0.01). | |
Returns: | |
dict: The mutated assignment solution. | |
""" | |
mutated_solution = copy.deepcopy(solution) | |
# Iterate over the services in the solution | |
for service, assigned_users in mutated_solution.items(): | |
for user in assigned_users: | |
# Check if the user should be mutated based on the mutation rate | |
if random.random() < mutation_rate: | |
# Remove the user from the current service | |
assigned_users.remove(user) | |
# Find a new service for the user while considering their cannot_assign constraints | |
new_service = service | |
while new_service == service or new_service in users[user]["cannot_assign"]: | |
new_service = random.choice(list(mutated_solution.keys())) | |
# Assign the user to the new service | |
mutated_solution[new_service].append(user) | |
return mutated_solution | |
def report_generation(generation: int, fitness_scores: list, best_solution: dict, services: dict, users: dict) -> None: | |
""" | |
Print a report of the genetic algorithm's progress for the current generation. | |
Args: | |
generation (int): The current generation number. | |
fitness_scores (list): The fitness scores for the current population. | |
best_solution (dict): The best assignment solution found so far. | |
services (dict): The input services dictionary. | |
users (dict): The input users dictionary. | |
""" | |
best_fitness = min(fitness_scores) | |
worst_fitness = max(fitness_scores) | |
avg_fitness = sum(fitness_scores) / len(fitness_scores) | |
generation_errors = polish_errors(calculate_errors(best_solution, services, users)) | |
print(f"Generation {generation}:") | |
print(f" Best fitness: {best_fitness}") | |
print(f" Worst fitness: {worst_fitness}") | |
print(f" Average fitness: {avg_fitness}") | |
print(f" Best solution so far: {best_solution}") | |
print(f" Errors so far: {generation_errors}") | |
def calculate_errors(solution: dict, services: dict, users: dict) -> dict: | |
""" | |
Calculate the errors in the assignment solution based on the user and service constraints. | |
Args: | |
solution (dict): The assignment solution to analyze. | |
services (dict): The input services dictionary. | |
users (dict): The input users dictionary. | |
Returns: | |
dict: A dictionary containing the errors for each user and service in the assignment solution. | |
""" | |
errors = {"users": {}, "services": {}} | |
# Analyze user errors | |
for user, user_data in users.items(): | |
errors["users"][user] = {"unmet_max_assignments": False, "unmet_preference": [], "unmet_cannot_assign": []} | |
user_assignments = [service for service, assigned_users in solution.items() if user in assigned_users] | |
if len(user_assignments) > user_data["max_assignments"]: | |
errors["users"][user]["unmet_max_assignments"] = True | |
errors["users"][user]["effective_assignments"] = len(user_assignments) | |
for preferred_service in user_data["preferences"]: | |
if preferred_service not in user_assignments: | |
errors["users"][user]["unmet_preference"].append(preferred_service) | |
for cannot_assign_service in user_data["cannot_assign"]: | |
if cannot_assign_service in user_assignments: | |
errors["users"][user]["unmet_cannot_assign"].append(cannot_assign_service) | |
# Analyze service errors | |
for service, service_data in services.items(): | |
errors["services"][service] = {"unmet_constraint": None, "extra_users": []} | |
assigned_users = solution[service] | |
num_assigned_users = len(assigned_users) | |
if num_assigned_users < service_data["min"]: | |
errors["services"][service]["unmet_constraint"] = "min" | |
elif num_assigned_users > service_data["rec"]: | |
errors["services"][service]["unmet_constraint"] = "rec" | |
elif num_assigned_users > service_data["max"]: | |
errors["services"][service]["unmet_constraint"] = "max" | |
extra_users = assigned_users[service_data["max"]:] | |
errors["services"][service]["extra_users"] = extra_users | |
return errors | |
def polish_errors(errors: dict) -> dict: | |
""" | |
Remove users and services without unmet constraints from the errors object. | |
Args: | |
errors (dict): The errors object to polish. | |
Returns: | |
dict: A polished errors object without users and services with no unmet constraints. | |
""" | |
polished_errors = {"users": {}, "services": {}} | |
for user, user_errors in errors["users"].items(): | |
polished_user_errors = {} | |
if user_errors["unmet_max_assignments"]: | |
polished_user_errors["unmet_max_assignments"] = True | |
for key, value in user_errors.items(): | |
if key not in ["unmet_max_assignments"] and value: | |
polished_user_errors[key] = value | |
if polished_user_errors: | |
polished_errors["users"][user] = polished_user_errors | |
for service, service_errors in errors["services"].items(): | |
polished_service_errors = {} | |
for key, value in service_errors.items(): | |
if value: | |
polished_service_errors[key] = value | |
if polished_service_errors: | |
polished_errors["services"][service] = polished_service_errors | |
return polished_errors | |
def genetic_algorithm(services: dict, users: dict, population_size: int = 100, num_generations: int = 100, | |
mutation_rate: float = 0.01, fitness_fn: Optional[Callable] = None) -> dict: | |
""" | |
Run the genetic algorithm to find an optimal assignment solution based on user preferences and constraints. | |
Args: | |
services (dict): The input services dictionary. | |
users (dict): The input users dictionary. | |
population_size (int): The size of the population for each generation (default: 100). | |
num_generations (int): The number of generations for the genetic algorithm to run (default: 100). | |
mutation_rate (float): The probability of mutation for each individual in the population (default: 0.01). | |
fitness_fn (Callable, optional): An optional custom fitness function. | |
Returns: | |
dict: The best assignment solution found by the genetic algorithm. | |
""" | |
# Initialize the population | |
population = initialize_population(services, users, population_size) | |
# If no custom fitness function is provided, use the default fitness function | |
if fitness_fn is None: | |
fitness_fn = default_fitness_function | |
# Calculate the initial fitness scores for the population | |
fitness_scores = calculate_fitness(population, services, users, fitness_fn) | |
best_solution = None | |
best_fitness = float('inf') | |
# Main loop of the genetic algorithm | |
for generation in range(num_generations): | |
# Select two parent solutions based on their fitness scores | |
parent1_index, parent2_index = selection(fitness_scores) | |
# Create a child solution by combining the parents using crossover | |
child_solution = crossover(population[parent1_index], population[parent2_index]) | |
# Mutate the child solution | |
mutated_child_solution = mutation(child_solution, users, mutation_rate) | |
# Calculate the fitness of the child solution | |
child_fitness = fitness_fn(mutated_child_solution, services, users) | |
# Replace the least-fit solution in the population with the child solution | |
worst_fitness_index = fitness_scores.index(max(fitness_scores)) | |
population[worst_fitness_index] = mutated_child_solution | |
fitness_scores[worst_fitness_index] = child_fitness | |
# Update the best solution found so far | |
if child_fitness < best_fitness: | |
best_solution = mutated_child_solution | |
best_fitness = child_fitness | |
# Print the progress of the algorithm | |
report_generation(generation, fitness_scores, best_solution, services, users) | |
return best_solution | |