Spaces:
Runtime error
Runtime error
File size: 15,646 Bytes
4167694 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
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
|