mbCrypto's picture
Upload 3 files
4167694
raw
history blame
15.6 kB
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