mbCrypto commited on
Commit
e831cc5
·
1 Parent(s): 349ac28

Added the update JSON heuristics

Browse files
Files changed (2) hide show
  1. app.py +117 -35
  2. main.py +157 -11
app.py CHANGED
@@ -3,60 +3,136 @@ import streamlit as st
3
  import json
4
  import clipboard
5
 
6
- from main import genetic_algorithm, polish_errors, calculate_errors
7
 
8
  # Initialize session state
9
  if 'services' not in st.session_state:
10
  st.session_state.services = {}
11
  if 'users' not in st.session_state:
12
  st.session_state.users = {}
 
 
13
 
14
  # App title
15
- st.title('Services and Users JSON Builder')
16
 
17
  # Add sliders for population_size, num_generations, and mutation_rate
18
  st.subheader('Genetic Algorithm Parameters')
19
- population_size = st.slider('Population Size', min_value=500, max_value=5000, value=2500, step=100)
20
- num_generations = st.slider('Number of Generations', min_value=1000, max_value=10000, value=5000, step=500)
21
- mutation_rate = st.slider('Mutation Rate', min_value=0.0, max_value=1.0, value=0.01, step=0.01)
22
 
23
  # Button to run the genetic algorithm
24
- if st.button('Run Genetic Algorithm'):
 
 
 
25
  # Call the genetic_algorithm function and get the best_solution
26
- best_solution = genetic_algorithm(st.session_state.services, st.session_state.users, population_size,
27
- num_generations, mutation_rate)
 
 
 
 
 
 
 
 
28
 
29
  # Convert the best_solution to JSON
30
- best_solution_json = json.dumps(best_solution, indent=4)
31
- best_solution_errors = calculate_errors(best_solution, st.session_state.services, st.session_state.users)
 
32
  best_solution_errors = polish_errors(best_solution_errors)
33
- best_solution_errors = json.dumps(best_solution_errors, indent=4)
34
 
35
  # Display the output JSON in a read-only form
36
- st.subheader('Best Solution JSON')
37
- st.text_area('Best Solution', value=best_solution_json, height=400, max_chars=None, key=None, disabled=True)
38
- st.text_area('Unmet constraints', value=best_solution_errors, height=200, max_chars=None, key=None, disabled=True)
 
 
39
 
40
  if st.button('Copy solution to Clipboard'):
41
  clipboard.copy(best_solution_json)
42
- st.success('JSON copied to clipboard!')
43
  if st.button('Copy unmet constraints to Clipboard'):
44
- clipboard.copy(best_solution_errors)
45
- st.success('JSON copied to clipboard!')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  # Sidebar for uploading previously generated JSON
48
- with st.sidebar.expander('Upload previously generated JSON'):
49
- uploaded_json = st.text_area('Paste your JSON here')
50
- merge_json = st.button('Merge with JSON')
51
- reset_json = st.button('Reset JSON')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  if reset_json:
54
  st.session_state.services = {}
55
  st.session_state.users = {}
56
 
57
- if merge_json and uploaded_json:
58
  try:
59
- loaded_data = json.loads(uploaded_json)
60
  st.session_state.services.update(loaded_data.get('services', {}))
61
  st.session_state.users.update(loaded_data.get('users', {}))
62
  st.success('JSON loaded successfully')
@@ -69,20 +145,26 @@ with st.sidebar.expander('Update existing user or service'):
69
 
70
  if object_type == 'Service':
71
  service_key = st.selectbox('Select a service', list(st.session_state.services.keys()), key='update_service_key')
72
- if service_key and st.button('Load Service'):
73
- st.session_state.service_name = service_key
74
- st.session_state.min_val = st.session_state.services[service_key]['min']
75
- st.session_state.rec_val = st.session_state.services[service_key]['rec']
76
- st.session_state.max_val = st.session_state.services[service_key]['max']
77
- st.session_state.priority = st.session_state.services[service_key]['priority']
 
 
 
78
 
79
  elif object_type == 'User':
80
  user_key = st.selectbox('Select a user', list(st.session_state.users.keys()), key='update_user_key')
81
- if user_key and st.button('Load User'):
82
- st.session_state.user_name = user_key
83
- st.session_state.max_assignments = st.session_state.users[user_key]['max_assignments']
84
- st.session_state.preferences = st.session_state.users[user_key]['preferences']
85
- st.session_state.cannot_assign = st.session_state.users[user_key]['cannot_assign']
 
 
 
86
 
87
  # Add a service form
88
  with st.form(key='service_form'):
@@ -133,7 +215,7 @@ combined_data = {
133
  json_data = json.dumps(combined_data, indent=4)
134
 
135
  # Display the generated JSON
136
- st.subheader('Generated JSON')
137
  st.code(json_data, language='json')
138
 
139
  # Button to copy JSON to clipboard
 
3
  import json
4
  import clipboard
5
 
6
+ from main import genetic_algorithm, polish_errors, calculate_errors, update_genetic_algorithm, calculate_diff
7
 
8
  # Initialize session state
9
  if 'services' not in st.session_state:
10
  st.session_state.services = {}
11
  if 'users' not in st.session_state:
12
  st.session_state.users = {}
13
+ if 'solution' not in st.session_state:
14
+ st.session_state.solution = {}
15
 
16
  # App title
17
+ st.title('Services and Users Assignment Center')
18
 
19
  # Add sliders for population_size, num_generations, and mutation_rate
20
  st.subheader('Genetic Algorithm Parameters')
21
+ population_size = st.slider('Population Size', min_value=500, max_value=5000, value=1500, step=100)
22
+ num_generations = st.slider('Number of Generations', min_value=1000, max_value=10000, value=2500, step=250)
23
+ mutation_rate = st.slider('Mutation Rate', min_value=0.0, max_value=1.0, value=0.01, step=0.05)
24
 
25
  # Button to run the genetic algorithm
26
+ new_generation_run = st.button('Run new solution')
27
+ update_generation_run = st.button('Update solution')
28
+
29
+ if new_generation_run:
30
  # Call the genetic_algorithm function and get the best_solution
31
+ best_solution = genetic_algorithm(
32
+ services=st.session_state.services,
33
+ users=st.session_state.users,
34
+ population_size=population_size,
35
+ num_generations=num_generations,
36
+ mutation_rate=mutation_rate
37
+ )
38
+
39
+ # Save the state of the current best solution
40
+ st.session_state.solution = best_solution
41
 
42
  # Convert the best_solution to JSON
43
+ best_solution_json = json.dumps(st.session_state.solution, indent=4)
44
+ best_solution_errors = calculate_errors(st.session_state.solution, st.session_state.services,
45
+ st.session_state.users)
46
  best_solution_errors = polish_errors(best_solution_errors)
47
+ best_solution_errors_json = json.dumps(best_solution_errors, indent=4)
48
 
49
  # Display the output JSON in a read-only form
50
+ st.subheader('Best solution JSON')
51
+ st.text_area('Best solution',
52
+ value=best_solution_json, height=400, max_chars=None, key=None, disabled=True)
53
+ st.text_area('Unmet constraints',
54
+ value=best_solution_errors_json, height=200, max_chars=None, key=None, disabled=True)
55
 
56
  if st.button('Copy solution to Clipboard'):
57
  clipboard.copy(best_solution_json)
 
58
  if st.button('Copy unmet constraints to Clipboard'):
59
+ clipboard.copy(best_solution_errors_json)
60
+
61
+ if update_generation_run:
62
+ # Call the genetic_algorithm function and get the best_solution
63
+ best_updated_solution = update_genetic_algorithm(
64
+ prev_solution=st.session_state.solution,
65
+ updated_services=st.session_state.services,
66
+ updated_users=st.session_state.users,
67
+ population_size=population_size,
68
+ num_generations=num_generations,
69
+ mutation_rate=mutation_rate
70
+ )
71
+
72
+ change_report = calculate_diff(best_updated_solution, st.session_state.solution)
73
+ change_report_json = json.dumps(change_report, indent=4)
74
+
75
+ # Convert the best_solution to JSON
76
+ best_updated_solution_json = json.dumps(st.session_state.solution, indent=4)
77
+ best_updated_solution_errors = calculate_errors(
78
+ st.session_state.solution, st.session_state.services, st.session_state.users
79
+ )
80
+ best_updated_solution_errors = polish_errors(best_updated_solution_errors)
81
+ best_updated_solution_errors_json = json.dumps(best_updated_solution_errors, indent=4)
82
+
83
+ # Display the output JSON in a read-only form
84
+ st.subheader('Best updated solution JSON')
85
+ st.text_area('Best updated solution',
86
+ value=best_updated_solution_json, height=600, max_chars=None, key=None, disabled=True)
87
+ st.text_area('Updated unmet constraints',
88
+ value=best_updated_solution_errors_json, height=300, max_chars=None, key=None, disabled=True)
89
+ st.text_area('Change report',
90
+ value=change_report_json, height=200, max_chars=None, key=None, disabled=True)
91
+
92
+ if st.button('Copy updated solution to Clipboard'):
93
+ clipboard.copy(best_updated_solution_json)
94
+ if st.button('Copy updated unmet constraints to Clipboard'):
95
+ clipboard.copy(best_updated_solution_errors_json)
96
+ if st.button('Save this updated solution over the last fully generated one'):
97
+ # Save the state of the current best solution
98
+ st.session_state.solution = best_updated_solution
99
 
100
  # Sidebar for uploading previously generated JSON
101
+ with st.sidebar.expander('Previously generated solution JSON'):
102
+ uploaded_solution_json = st.text_area('Paste your previously generated JSON here',
103
+ value=json.dumps(st.session_state.get('solution', ''), indent=4))
104
+ merge_json = st.button('Upload previously generated JSON')
105
+ reset_json = st.button('Reset previously generated JSON')
106
+
107
+ if reset_json:
108
+ st.session_state.solution = {}
109
+
110
+ if merge_json and uploaded_solution_json:
111
+ try:
112
+ st.session_state.solution = json.loads(uploaded_solution_json)
113
+ st.success('JSON loaded successfully')
114
+ except json.JSONDecodeError:
115
+ st.error('Invalid JSON format')
116
+
117
+ # Sidebar for uploading previously user and service description JSON
118
+ with st.sidebar.expander('Previously generated user and service description JSON'):
119
+ previously_generated_user_service_json = {
120
+ 'services': st.session_state.services,
121
+ 'users': st.session_state.users
122
+ }
123
+ uploaded_user_service_json = st.text_area('Paste your user and service JSON here',
124
+ value=json.dumps(previously_generated_user_service_json, indent=4)
125
+ )
126
+ merge_json = st.button('Merge user and service description JSON')
127
+ reset_json = st.button('Reset user and service description JSON')
128
 
129
  if reset_json:
130
  st.session_state.services = {}
131
  st.session_state.users = {}
132
 
133
+ if merge_json and uploaded_user_service_json:
134
  try:
135
+ loaded_data = json.loads(uploaded_user_service_json)
136
  st.session_state.services.update(loaded_data.get('services', {}))
137
  st.session_state.users.update(loaded_data.get('users', {}))
138
  st.success('JSON loaded successfully')
 
145
 
146
  if object_type == 'Service':
147
  service_key = st.selectbox('Select a service', list(st.session_state.services.keys()), key='update_service_key')
148
+ if service_key:
149
+ if st.button('Load Service'):
150
+ st.session_state.service_name = service_key
151
+ st.session_state.min_val = st.session_state.services[service_key]['min']
152
+ st.session_state.rec_val = st.session_state.services[service_key]['rec']
153
+ st.session_state.max_val = st.session_state.services[service_key]['max']
154
+ st.session_state.priority = st.session_state.services[service_key]['priority']
155
+ if st.button('Drop Service'):
156
+ del st.session_state.services[service_key]
157
 
158
  elif object_type == 'User':
159
  user_key = st.selectbox('Select a user', list(st.session_state.users.keys()), key='update_user_key')
160
+ if user_key:
161
+ if st.button('Load User'):
162
+ st.session_state.user_name = user_key
163
+ st.session_state.max_assignments = st.session_state.users[user_key]['max_assignments']
164
+ st.session_state.preferences = st.session_state.users[user_key]['preferences']
165
+ st.session_state.cannot_assign = st.session_state.users[user_key]['cannot_assign']
166
+ if st.button('Drop User'):
167
+ del st.session_state.users[user_key]
168
 
169
  # Add a service form
170
  with st.form(key='service_form'):
 
215
  json_data = json.dumps(combined_data, indent=4)
216
 
217
  # Display the generated JSON
218
+ st.subheader('Generated user and services JSON')
219
  st.code(json_data, language='json')
220
 
221
  # Button to copy JSON to clipboard
main.py CHANGED
@@ -1,6 +1,7 @@
1
  import copy
2
  import random
3
- from typing import Callable, Optional, Tuple
 
4
 
5
 
6
  def initialize_population(services: dict, users: dict, population_size: int) -> list:
@@ -83,35 +84,101 @@ def default_fitness_function(assignment_solution: dict, services: dict, users: d
83
  Returns:
84
  float: The fitness score of the given assignment solution.
85
  """
86
- fitness = 0
87
 
88
  for service, assigned_users in assignment_solution.items():
89
  service_info = services[service]
90
  num_assigned_users = len(assigned_users)
91
 
92
- # Bonus for solutions that assign users near the recommended value
 
93
  if service_info["min"] <= num_assigned_users <= service_info["max"]:
94
- fitness += abs(num_assigned_users - service_info["rec"])
95
 
96
  # Punish solutions that assign users below the minimum value
 
97
  elif num_assigned_users < service_info["min"]:
98
- fitness -= (service_info["min"] - num_assigned_users) * service_info["priority"]
99
 
100
  # Punish solutions that assign users above the maximum value
 
101
  else: # num_assigned_users > service_info["max"]:
102
- fitness -= (num_assigned_users - service_info["max"]) * service_info["priority"]
103
 
104
  # Punish solutions that assign users to their cannot_assign services
 
105
  for user in assigned_users:
106
  if service in users[user]["cannot_assign"]:
107
- fitness -= 100 * service_info["priority"]
108
 
109
  # Bonus solutions that assign users to their preferred services
 
110
  for user, user_info in users.items():
111
  if service in user_info["preferences"] and user in assigned_users:
112
- fitness += 10
113
 
114
- return -fitness
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
 
117
  def selection(fitness_scores: list) -> Tuple[int, int]:
@@ -332,8 +399,51 @@ def polish_errors(errors: dict) -> dict:
332
  return polished_errors
333
 
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  def genetic_algorithm(services: dict, users: dict, population_size: int = 100, num_generations: int = 100,
336
- mutation_rate: float = 0.01, fitness_fn: Optional[Callable] = None) -> dict:
 
337
  """
338
  Run the genetic algorithm to find an optimal assignment solution based on user preferences and constraints.
339
 
@@ -344,12 +454,13 @@ def genetic_algorithm(services: dict, users: dict, population_size: int = 100, n
344
  num_generations (int): The number of generations for the genetic algorithm to run (default: 100).
345
  mutation_rate (float): The probability of mutation for each individual in the population (default: 0.01).
346
  fitness_fn (Callable, optional): An optional custom fitness function.
 
347
 
348
  Returns:
349
  dict: The best assignment solution found by the genetic algorithm.
350
  """
351
  # Initialize the population
352
- population = initialize_population(services, users, population_size)
353
 
354
  # If no custom fitness function is provided, use the default fitness function
355
  if fitness_fn is None:
@@ -389,3 +500,38 @@ def genetic_algorithm(services: dict, users: dict, population_size: int = 100, n
389
  report_generation(generation, fitness_scores, best_solution, services, users)
390
 
391
  return best_solution
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import copy
2
  import random
3
+ from functools import partial
4
+ from typing import Callable, Optional, Tuple, List
5
 
6
 
7
  def initialize_population(services: dict, users: dict, population_size: int) -> list:
 
84
  Returns:
85
  float: The fitness score of the given assignment solution.
86
  """
87
+ fitness = -100
88
 
89
  for service, assigned_users in assignment_solution.items():
90
  service_info = services[service]
91
  num_assigned_users = len(assigned_users)
92
 
93
+ # Prefers for solutions that assign users near the recommended value
94
+ # (positive or negative value is punishment, score 0 for the best fit)
95
  if service_info["min"] <= num_assigned_users <= service_info["max"]:
96
+ fitness += abs(num_assigned_users - service_info["rec"]) * service_info["priority"]
97
 
98
  # Punish solutions that assign users below the minimum value
99
+ # (positive value is punishment)
100
  elif num_assigned_users < service_info["min"]:
101
+ fitness += (service_info["min"] - num_assigned_users) * service_info["priority"]
102
 
103
  # Punish solutions that assign users above the maximum value
104
+ # (positive value is punishment)
105
  else: # num_assigned_users > service_info["max"]:
106
+ fitness += (num_assigned_users - service_info["max"]) * service_info["priority"]
107
 
108
  # Punish solutions that assign users to their cannot_assign services
109
+ # (positive value is punishment)
110
  for user in assigned_users:
111
  if service in users[user]["cannot_assign"]:
112
+ fitness += 100 * service_info["priority"]
113
 
114
  # Bonus solutions that assign users to their preferred services
115
+ # (negative value is bonus)
116
  for user, user_info in users.items():
117
  if service in user_info["preferences"] and user in assigned_users:
118
+ fitness -= 20
119
 
120
+ return fitness
121
+
122
+
123
+ def least_changed_fitness_function(prev_solution: dict, solution: dict, services: dict, users: dict) -> float:
124
+ """
125
+ A fitness function that favors solutions with the least changes from a previous solution.
126
+
127
+ Args:
128
+ solution (dict): The assignment solution to evaluate.
129
+ services (dict): The input services dictionary.
130
+ users (dict): The input users dictionary.
131
+ prev_solution (dict): The previous assignment solution to compare against.
132
+
133
+ Returns:
134
+ float: A fitness score for the assignment solution.
135
+ """
136
+ # Add the default fitness function score
137
+ fitness = default_fitness_function(solution, services, users)
138
+
139
+ # Bonus for users assigned to the same services as in the previous solution
140
+ same_user_assignments = 0
141
+ for service, assigned_users in solution.items():
142
+ if service in prev_solution:
143
+ prev_assigned_users = prev_solution[service]
144
+ same_users = set(assigned_users).intersection(set(prev_assigned_users))
145
+ same_user_assignments += len(same_users)
146
+
147
+ user_bonus = same_user_assignments * 100 # / sum(len(user_data["preferences"]) for user_data in users.values())
148
+ # (positive value is punishment)
149
+ fitness -= user_bonus
150
+
151
+ # Bonus for services having the same users assigned as in the previous solution
152
+ same_service_assignments = 0
153
+ for service, assigned_users in solution.items():
154
+ if service in prev_solution:
155
+ prev_assigned_users = prev_solution[service]
156
+ same_users = set(assigned_users).intersection(set(prev_assigned_users))
157
+ same_service_assignments += len(same_users)
158
+
159
+ service_bonus = same_service_assignments * 100
160
+ # (positive value is punishment)
161
+ fitness -= service_bonus
162
+
163
+ # Malus for user and service changes
164
+ user_changes = 0
165
+ service_changes = 0
166
+
167
+ for service, service_data in services.items():
168
+ if service in prev_solution:
169
+ prev_service_data = prev_solution[service]
170
+ if service_data != prev_service_data:
171
+ service_changes += 1
172
+ for user, user_data in users.items():
173
+ if user in prev_service_data and user not in service_data:
174
+ user_changes += 1
175
+
176
+ user_malus = user_changes
177
+ service_malus = service_changes
178
+ # (positive value is punishment)
179
+ fitness += (user_malus + service_malus)
180
+
181
+ return fitness
182
 
183
 
184
  def selection(fitness_scores: list) -> Tuple[int, int]:
 
399
  return polished_errors
400
 
401
 
402
+ def calculate_diff(solution1: dict, solution2: dict) -> dict:
403
+ """
404
+ Calculate the differences between two solution JSON objects and return the differences categorized into
405
+ "added" and "removed" attributes for each service.
406
+
407
+ Args:
408
+ solution1 (dict): The first solution JSON object.
409
+ solution2 (dict): The second solution JSON object.
410
+
411
+ Returns:
412
+ dict: A dictionary with the differences between the two solutions, categorized into "added" and
413
+ "removed" attributes for each service.
414
+ """
415
+ diff = {}
416
+
417
+ all_services = set(solution1.keys()).union(set(solution2.keys()))
418
+
419
+ for service in all_services:
420
+ service_diff = {
421
+ "added": [],
422
+ "removed": []
423
+ }
424
+
425
+ if service not in solution1:
426
+ service_diff["added"] = solution2[service]
427
+ elif service not in solution2:
428
+ service_diff["removed"] = solution1[service]
429
+ else:
430
+ added_users = set(solution2[service]) - set(solution1[service])
431
+ removed_users = set(solution1[service]) - set(solution2[service])
432
+
433
+ if added_users:
434
+ service_diff["added"] = list(added_users)
435
+ if removed_users:
436
+ service_diff["removed"] = list(removed_users)
437
+
438
+ if service_diff["added"] or service_diff["removed"]:
439
+ diff[service] = service_diff
440
+
441
+ return diff
442
+
443
+
444
  def genetic_algorithm(services: dict, users: dict, population_size: int = 100, num_generations: int = 100,
445
+ mutation_rate: float = 0.01, fitness_fn: Optional[Callable] = None,
446
+ initial_population: Optional[List[dict]] = None) -> dict:
447
  """
448
  Run the genetic algorithm to find an optimal assignment solution based on user preferences and constraints.
449
 
 
454
  num_generations (int): The number of generations for the genetic algorithm to run (default: 100).
455
  mutation_rate (float): The probability of mutation for each individual in the population (default: 0.01).
456
  fitness_fn (Callable, optional): An optional custom fitness function.
457
+ initial_population: An optional previous solution to be used as starting point
458
 
459
  Returns:
460
  dict: The best assignment solution found by the genetic algorithm.
461
  """
462
  # Initialize the population
463
+ population = initial_population or initialize_population(services, users, population_size)
464
 
465
  # If no custom fitness function is provided, use the default fitness function
466
  if fitness_fn is None:
 
500
  report_generation(generation, fitness_scores, best_solution, services, users)
501
 
502
  return best_solution
503
+
504
+
505
+ def update_genetic_algorithm(prev_solution: dict, updated_services: dict, updated_users: dict,
506
+ population_size: int = 100, num_generations: int = 100, mutation_rate: float = 0.01,
507
+ fitness_fn: Optional[Callable] = None) -> dict:
508
+ """
509
+ Update the previous assignment solution with updated services and users using a genetic algorithm.
510
+
511
+ Args:
512
+ prev_solution (dict): The previous assignment solution.
513
+ updated_services (dict): The updated services dictionary.
514
+ updated_users (dict): The updated users dictionary.
515
+ population_size (int): The size of the population for each generation in the genetic algorithm (default: 100).
516
+ num_generations (int): The number of generations for the genetic algorithm to run (default: 100).
517
+ mutation_rate (float): The probability of mutation for each individual in the population (default: 0.01).
518
+ fitness_fn (Optional[Callable]): An optional fitness function to use in the genetic algorithm.
519
+
520
+ Returns:
521
+ dict: The updated assignment solution.
522
+ """
523
+
524
+ # Create the initial population with the given previous solution
525
+ population = initialize_population(updated_services, updated_users, population_size - 1)
526
+ population.append(prev_solution)
527
+
528
+ if fitness_fn is None:
529
+ fitness_fn = partial(least_changed_fitness_function, prev_solution)
530
+
531
+ # Run the genetic algorithm using the initial population
532
+ updated_solution = genetic_algorithm(updated_services, updated_users, population_size, num_generations,
533
+ mutation_rate,
534
+ fitness_fn,
535
+ population)
536
+
537
+ return updated_solution