-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGenetic_Algorithm_Scheduling.py
More file actions
1811 lines (1477 loc) · 64.3 KB
/
Genetic_Algorithm_Scheduling.py
File metadata and controls
1811 lines (1477 loc) · 64.3 KB
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
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# coding: utf-8
# ## Hard Constraint Evaluation Function
#
# This function evaluates a timetable to check for violations of hard constraints — rules that must not be broken for the schedule to be valid. It counts different types of violations and prints a summary, helping us understand where the schedule fails.
#
# ### Parameters:
# - **`timetable`:** A dictionary where each timeslot contains rooms and the activities scheduled in them.
# - **`activities_dict`:** A dictionary of all activities, indexed by activity ID.
# - **`groups_dict`:** A dictionary of student groups, including their sizes.
# - **`spaces_dict`:** A dictionary of rooms, including their capacities.
#
# ### What the function checks:
#
# 1. **Vacant Rooms:**
# If a room is left empty in a timeslot, it’s counted as a vacant room.
#
# 2. **Lecturer Conflicts:**
# If the same lecturer is assigned to more than one activity in the same timeslot, that counts as a conflict.
#
# 3. **Student Group Conflicts:**
# If a student group is scheduled for multiple activities in the same timeslot, it creates a conflict. The function uses set intersections to catch these overlaps.
#
# 4. **Room Capacity Violations:**
# The function checks if the total number of students in an activity exceeds the room’s capacity. If it does, that counts as a violation.
#
# 5. **Unassigned Activities:**
# The function also counts how many activities were never assigned to any room or timeslot.
#
# ### The output:
# After checking all the constraints, the function prints a summary:
# - **Vacant Rooms Count** — how many rooms were left empty.
# - **Lecturer Conflict Violations** — how many times lecturers were double-booked.
# - **Student Group Conflict Violations** — how many times student groups were double-booked.
# - **Room Capacity Violations** — how many times room size was insufficient for the assigned activity.
# - **Unassigned Activity Violations** — how many activities weren’t placed in the timetable.
#
# Finally, it adds up all the violations to give a **total hard constraint violation score**. This score gives a quick sense of how well the timetable satisfies the strictest rules. Lower scores are better, meaning fewer conflicts and a more feasible schedule.
#
# This function is essential for checking the raw feasibility of a schedule before worrying about things like preferences or soft constraints.
#
# In[14]:
def evaluate_hard_constraints(timetable, activities_dict, groups_dict, spaces_dict):
vacant_rooms = []
vacant_room = 0
prof_conflicts = 0
room_size_conflicts = 0
sub_group_conflicts = 0
unasigned_activities = len(activities_dict)
activities_set = set()
for slot in timetable:
prof_set = set()
sub_group_set = set()
for room in timetable[slot]:
activity = timetable[slot][room]
if not isinstance(activity, type(list(activities_dict.values())[0])): # Ensure it's an Activity object
vacant_room += 1
vacant_rooms.append((slot, room))
else:
activities_set.add(activity.id)
# Lecturer Conflict Check
if activity.teacher_id in prof_set:
prof_conflicts += 1
prof_set.add(activity.teacher_id)
# Student Group Conflict Check
sub_group_conflicts += len(
set(activity.group_ids).intersection(sub_group_set))
group_size = 0
for group_id in activity.group_ids:
group_size += groups_dict[group_id].size
sub_group_set.add(group_id)
# Room Capacity Constraint Check
if group_size > spaces_dict[room].size:
room_size_conflicts += 1
# Unassigned Activity Count
unasigned_activities -= len(activities_set)
# Print Results
print("\n--- Hard Constraint Evaluation Results ---")
print(f"Vacant Rooms Count: {vacant_room}")
print(f"Lecturer Conflict Violations: {prof_conflicts}")
print(f"Student Group Conflict Violations: {sub_group_conflicts}")
print(f"Room Capacity Violations: {room_size_conflicts}")
print(f"Unassigned Activity Violations: {unasigned_activities}")
# Final Hard Constraint Violation Score
total_violations = prof_conflicts + sub_group_conflicts + room_size_conflicts + unasigned_activities
print(f"\nTotal Hard Constraint Violations: {total_violations}")
# ## Soft Constraint Evaluation Function
#
# This function evaluates the **soft constraints** of a timetable — factors that influence schedule quality but can be compromised if necessary. It measures aspects like student and lecturer fatigue, idle time, spread of lectures, and lecturer workload balance. The function then computes an overall score to help us understand how well the schedule performs.
#
# ### Parameters:
# - **`schedule`:** A dictionary representing the scheduled activities, organized by time slots and room assignments.
# - **`groups_dict`:** A dictionary containing student group details (e.g., group size).
# - **`lecturers_dict`:** A dictionary containing lecturer details.
# - **`slots`:** An ordered list of available time slots.
#
# ### What the function checks:
#
# 1. **Student Metrics:**
# - **Fatigue:** Number of lectures attended.
# - **Idle Time:** Gaps between lectures within the same day.
# - **Lecture Spread:** Distribution of lectures across slots (more spread = more scattered, less compact).
#
# 2. **Lecturer Metrics:**
# - **Fatigue:** Number of lectures conducted.
# - **Idle Time:** Gaps between lectures.
# - **Lecture Spread:** How scattered the lectures are across the slots.
# - **Workload Balance:** Variance in workload across lecturers (lower variance = better balance).
#
# ### How the function works:
#
# - It loops through each timeslot and room to gather relevant data on activities.
# - It updates fatigue, spread, and workload metrics directly during this loop.
# - It calculates idle time by checking gaps between consecutive lectures.
# - It normalizes all metrics for fair comparison and calculates workload balance using variance.
#
# ### Scoring the constraints:
#
# The function prints individual scores for:
# - **Student Fatigue Factor**
# - **Student Idle Time Factor**
# - **Student Lecture Spread Factor**
# - **Lecturer Fatigue Factor**
# - **Lecturer Idle Time Factor**
# - **Lecturer Lecture Spread Factor**
# - **Lecturer Workload Balance Factor**
#
# Finally, it computes a **weighted final score**. Higher scores indicate better schedule quality, with a balance between minimizing fatigue, idle time, and spread, while maximizing workload balance for lecturers.
#
# The weights reflect the relative importance of each factor, but these can be adjusted as needed.
#
# This function is invaluable for refining a feasible schedule into an optimized one that enhances the well-being of both students and lecturers.
#
# In[15]:
import numpy as np
def evaluate_soft_constraints(schedule, groups_dict, lecturers_dict, slots):
"""
Evaluates the soft constraints of a given schedule, handling missing (None) activities.
This function measures:
- Student group metrics: fatigue, idle time, lecture spread.
- Lecturer metrics: fatigue, idle time, lecture spread, and workload balance.
Parameters:
- schedule (dict): The scheduled activities mapped by time slots and locations.
- groups_dict (dict): Dictionary of student groups with group IDs as keys.
- lecturers_dict (dict): Dictionary of lecturers with lecturer IDs as keys.
- slots (list): Ordered list of available time slots.
Returns:
- final_score (float): Computed soft constraint score representing
schedule quality based on fatigue, idle time, spread, and workload balance.
"""
# Initialize student group metrics
group_fatigue = {g: 0 for g in groups_dict.keys()}
group_idle_time = {g: 0 for g in groups_dict.keys()}
group_lecture_spread = {g: 0 for g in groups_dict.keys()}
# Initialize lecturer metrics
lecturer_fatigue = {l: 0 for l in lecturers_dict.keys()}
lecturer_idle_time = {l: 0 for l in lecturers_dict.keys()}
lecturer_lecture_spread = {l: 0 for l in lecturers_dict.keys()}
lecturer_workload = {l: 0 for l in lecturers_dict.keys()}
# Track the lecture slots assigned to each group and lecturer
group_lecture_slots = {g: [] for g in groups_dict.keys()}
lecturer_lecture_slots = {l: [] for l in lecturers_dict.keys()}
# Process the schedule and accumulate lecture-related data
for slot, rooms in schedule.items():
for room, activity in rooms.items():
if activity is None:
continue # Skip empty slots (None values)
# Process student groups
if not isinstance(activity,Activity):
continue
if not isinstance(activity.group_ids,list):
continue
for group_id in activity.group_ids:
if group_id in groups_dict:
group_fatigue[group_id] += 1 # Increase fatigue per lecture
group_lecture_spread[group_id] += 2 # Increase spread factor
group_lecture_slots[group_id].append(slot) # Store time slot
# Process lecturers
lecturer_id = activity.teacher_id
if lecturer_id in lecturers_dict:
lecturer_fatigue[lecturer_id] += 1
lecturer_lecture_spread[lecturer_id] += 2
lecturer_workload[lecturer_id] += activity.duration # Add workload
lecturer_lecture_slots[lecturer_id].append(slot) # Store time slot
# Compute idle time for each student group
for group_id, lectures in group_lecture_slots.items():
if lectures:
lecture_indices = sorted([slots.index(s) for s in lectures])
idle_time = sum(
(lecture_indices[i + 1] - lecture_indices[i] - 1) for i in range(len(lecture_indices) - 1)
)
group_idle_time[group_id] = idle_time / (len(slots) - 1) # Normalize
# Compute idle time for each lecturer
for lecturer_id, lectures in lecturer_lecture_slots.items():
if lectures:
lecture_indices = sorted([slots.index(s) for s in lectures])
idle_time = sum(
(lecture_indices[i + 1] - lecture_indices[i] - 1) for i in range(len(lecture_indices) - 1)
)
lecturer_idle_time[lecturer_id] = idle_time / (len(slots) - 1) # Normalize
# Helper function to normalize values within a dictionary
def normalize(dictionary):
max_val = max(dictionary.values(), default=1)
return {k: v / max_val if max_val else 0 for k, v in dictionary.items()}
# Normalize metrics for fair comparison
group_fatigue = normalize(group_fatigue)
group_idle_time = normalize(group_idle_time)
group_lecture_spread = normalize(group_lecture_spread)
lecturer_fatigue = normalize(lecturer_fatigue)
lecturer_idle_time = normalize(lecturer_idle_time)
lecturer_lecture_spread = normalize(lecturer_lecture_spread)
# Compute lecturer workload balance
workload_values = np.array(list(lecturer_workload.values()))
lecturer_workload_balance = 1 # Default balance
if len(workload_values) > 1 and np.mean(workload_values) != 0:
lecturer_workload_balance = max(0, 1 - (np.var(workload_values) / np.mean(workload_values)))
# Compute the final soft constraint metrics
student_fatigue_score = np.mean(list(group_fatigue.values()))
student_idle_time_score = np.mean(list(group_idle_time.values()))
student_lecture_spread_score = np.mean(list(group_lecture_spread.values()))
lecturer_fatigue_score = np.mean(list(lecturer_fatigue.values()))
lecturer_idle_time_score = np.mean(list(lecturer_idle_time.values()))
lecturer_lecture_spread_score = np.mean(list(lecturer_lecture_spread.values()))
# Print individual final metric scores
print("\n--- Soft Constraint Evaluation Results ---")
print(f"Student Fatigue Factor: {student_fatigue_score:.2f}")
print(f"Student Idle Time Factor: {student_idle_time_score:.2f}")
print(f"Student Lecture Spread Factor: {student_lecture_spread_score:.2f}")
print(f"Lecturer Fatigue Factor: {lecturer_fatigue_score:.2f}")
print(f"Lecturer Idle Time Factor: {lecturer_idle_time_score:.2f}")
print(f"Lecturer Lecture Spread Factor: {lecturer_lecture_spread_score:.2f}")
print(f"Lecturer Workload Balance Factor: {lecturer_workload_balance:.2f}")
# Compute final soft constraint score based on weighted factors
final_score = (
student_fatigue_score * 0.2 +
(1 - student_idle_time_score) * 0.2 +
(1 - student_lecture_spread_score) * 0.2 +
(1 - lecturer_fatigue_score) * 0.1 +
(1 - lecturer_idle_time_score) * 0.1 +
(1 - lecturer_lecture_spread_score) * 0.1 +
lecturer_workload_balance * 0.1
)
print(f"\nFinal Soft Constraint Score: {final_score:.2f}")
#return final_score
# ### Constraint Evaluation Function
#
# This function evaluates a schedule by checking both **hard** and **soft constraints**:
#
# - **Hard Constraints:**
# - Room availability and capacity.
# - Lecturer and student group conflicts.
# - Unassigned activities.
#
# - **Soft Constraints:**
# - Student fatigue, idle time, and lecture spread.
# - Lecturer fatigue, idle time, spread, and workload balance.
#
# By running both evaluations, we get a complete view of schedule feasibility and quality, helping us identify and fix violations while optimizing for better resource usage and well-being.
# In[16]:
# Constraint Evaluation Metrics
def evaluate(schedule, groups_dict, lecturers_dict, activities_dict, spaces_dict, slots):
# Evaluate Hard Constraints
evaluate_hard_constraints(schedule, activities_dict, groups_dict, spaces_dict)
# Evaluate Soft Constraints
evaluate_soft_constraints(schedule, groups_dict, lecturers_dict, slots)
# # Timetable Data Processing
#
# This script processes timetable data from a JSON file, structuring it using classes for spaces, groups, activities, periods, and lecturers.
#
# ### Class Definitions
#
# - **Space**: Represents a location with a unique code and capacity.
# - **Group**: Defines a student group with an ID and size.
# - **Activity**: Represents a subject, assigned teacher, student groups, and duration.
# - **Period**: Associates an activity with a time slot and space.
# - **Lecturer**: Stores lecturer details, including ID, name, username, and department.
#
# ### Data Loading and Processing
#
# The script reads `sliit_computing_dataset.json` and organizes data into:
#
# - `spaces_dict`: Maps spaces by code.
# - `groups_dict`: Maps student groups by ID.
# - `activities_dict`: Maps activities by code.
# - `lecturers_dict`: Stores lecturers filtered by role.
#
# Each section is processed by iterating over the JSON file and creating class instances.
#
# ### Time Slot Generation
#
# A list of time slots is created for Monday to Friday, with eight slots per day.
#
# ### Data Verification
#
# The script prints the structured dictionaries to confirm correct data loading, providing a foundation for scheduling and further analysis.
#
# In[17]:
class Space:
def __init__(self, *args):
self.code = args[0]
self.size = args[1]
def __repr__(self):
return f"Space(code={self.code}, size={self.size})"
class Group:
def __init__(self, *args):
self.id = args[0]
self.size = args[1]
def __repr__(self):
return f"Group(id={self.id}, size={self.size})"
class Activity:
def __init__(self, id, *args):
self.id = id
self.subject = args[0]
self.teacher_id = args[1]
self.group_ids = args[2]
self.duration = args[3]
def __repr__(self):
return f"Activity(id={self.id}, subject={self.subject}, teacher_id={self.teacher_id}, group_ids={self.group_ids}, duration={self.duration})"
class Period:
def __init__(self, *args):
self.space = args[0]
self.slot = args[1]
self.activity = args[2]
def __repr__(self):
return f"Period(space={self.space}, group={self.group}, activity={self.activity})"
class Lecturer:
def __init__(self, id, first_name, last_name, username, department):
self.id = id
self.first_name = first_name
self.last_name = last_name
self.username = username
self.department = department
def __repr__(self):
return f"Lecturer(id={self.id}, name={self.first_name} {self.last_name}, department={self.department})"
import json
# Load data from JSON file
with open('sliit_computing_dataset.json', 'r') as file:
data = json.load(file)
# Create dictionaries to store instances
spaces_dict = {}
groups_dict = {}
activities_dict = {}
lecturers_dict = {}
slots = []
# Populate the dictionaries with data from the JSON file
for space in data['spaces']:
spaces_dict[space['code']] = Space(space['code'], space['capacity'])
for group in data['years']:
groups_dict[group['id']] = Group(group['id'], group['size'])
for activity in data['activities']:
activities_dict[activity['code']] = Activity(
activity['code'], activity['subject'], activity['teacher_ids'][0], activity['subgroup_ids'], activity['duration'])
for user in data["users"]:
if user["role"] == "lecturer":
lecturers_dict[user["id"]] = Lecturer(
user["id"], user["first_name"], user["last_name"], user["username"], user["department"]
)
for day in ["MON", "TUE", "WED", "THU", "FRI"]:
for id in range(1, 9):
slots.append(day+str(id))
# Print the dictionaries to verify
print("spaces_dict=", spaces_dict)
print("groups_dict=", groups_dict)
print("activities_dict=", activities_dict)
print("lecturers_dict=", lecturers_dict)
print("slots=",slots)
# In[18]:
class Period:
def __init__(self, space, slot, activity=None):
self.space = space
self.slot = slot
self.activity = activity
def __repr__(self):
return f"Period(space={self.space}, slot={self.slot}, activity={self.activity})"
slots = ['MON1', 'MON2', 'MON3', 'MON4', 'MON5', 'MON6', 'MON7', 'MON8',
'TUE1', 'TUE2', 'TUE3', 'TUE4', 'TUE5', 'TUE6', 'TUE7', 'TUE8',
'WED1', 'WED2', 'WED3', 'WED4', 'WED5', 'WED6', 'WED7', 'WED8',
'THU1', 'THU2', 'THU3', 'THU4', 'THU5', 'THU6', 'THU7', 'THU8',
'FRI1', 'FRI2', 'FRI3', 'FRI4', 'FRI5', 'FRI6', 'FRI7', 'FRI8']
spaces = ['LH401', 'LH501', 'LAB501', 'LAB502']
# schedule = {f"{slot}_{space}": Period(space, slot) for slot in slots for space in spaces}
# for key, value in sorted(schedule.items()):
# print(f"{key}: {value}")
schedule = {slot: {space: None for space in spaces} for slot in slots}
#
# # NSGA-II: Multi-Objective Genetic Algorithm for Timetable Optimization
#
# **Overview**
# This implementation of **NSGA-II** optimizes a timetable scheduling problem by minimizing conflicts such as professor clashes, room capacity violations, and unassigned activities. The timetable is represented as a dictionary mapping slots to room-activity assignments.
#
# **Inputs and Parameters**
# - `POPULATION_SIZE`: Number of individuals in the population.
# - `NUM_GENERATIONS`: Number of evolutionary iterations.
# - `MUTATION_RATE`: Probability of mutation in offspring.
# - `CROSSOVER_RATE`: Probability of crossover between parents.
# - `vacant_rooms`: Stores vacant room slots.
#
# ---
#
# ## Functions
#
# **`evaluator(timetable)`**
# Evaluates the timetable based on:
# - Vacant rooms.
# - Professor conflicts (same professor assigned to multiple activities).
# - Room size violations.
# - Subgroup conflicts (overlapping student groups).
# - Unassigned activities.
#
# **Returns:** A tuple `(vacant_room, prof_conflicts, room_size_conflicts, sub_group_conflicts, unasigned_activities)`.
#
# **`get_classsize(activity: Activity) -> int`**
# Computes total class size by summing the sizes of associated groups.
#
# **`evaluate_population(population)`**
# Computes fitness values for individuals in the population using `evaluator()`.
#
# **Returns:** List of fitness tuples.
#
# **`mutate(individual)`**
# Performs mutation by randomly swapping activities between two slots.
#
# **`crossover(parent1, parent2)`**
# Performs crossover by swapping timetable slots between parents.
#
# **Returns:** Two new offspring timetables.
#
# **`fast_nondominated_sort(fitness_values)`**
# Ranks individuals based on Pareto dominance.
#
# **Returns:** List of ranked fronts.
#
# **`dominates(fitness1, fitness2)`**
# Checks if one fitness tuple dominates another.
#
# **Returns:** `True` if `fitness1` dominates `fitness2`, otherwise `False`.
#
# **`calculate_crowding_distance(front, fitness_values)`**
# Computes crowding distances to maintain diversity.
#
# **Returns:** List of crowding distances.
#
# **`select_parents(population, fitness_values)`**
# Performs tournament selection using Pareto fronts and crowding distance.
#
# **Returns:** List of selected parents.
#
# **`generate_initial_population()`**
# Creates an initial random population of timetables.
#
# **Returns:** List of randomly initialized timetable dictionaries.
#
# **`nsga2()`**
# Main evolutionary loop:
# 1. Initializes the population.
# 2. Evaluates fitness.
# 3. Applies selection, crossover, and mutation.
# 4. Uses non-dominated sorting for diversity.
#
# **Returns:** The evolved population.
#
# ---
#
# ## Running the Algorithm
# The best timetable is selected based on the lowest sum of evaluation metrics:
#
# ```python
# final_populations = nsga2()
#
# schedule = {}
# minimum = 1000
# for population in final_populations:
# if sum(evaluator(population)) < minimum:
# minimum = sum(evaluator(population))
# schedule = population
# ```
#
# #### **Defining the chromosome as slot:{room: Activity}**
# In[19]:
# Code
import random
POPULATION_SIZE = 50
NUM_GENERATIONS = 100
MUTATION_RATE = 0.1
CROSSOVER_RATE = 0.8
vacant_rooms = []
def evaluator(timetable):
vacant_room = 0
prof_conflicts = 0
room_size_conflicts = 0
sub_group_conflicts = 0
unasigned_activities = len(activities_dict)
activities_set = set()
for slot in timetable:
prof_set = set()
sub_group_set = set()
for room in timetable[slot]:
activity = timetable[slot][room]
if not isinstance(activity, Activity):
vacant_room += 1
vacant_rooms.append((slot, room))
else:
activities_set.add(activity.id)
if activity.teacher_id in prof_set:
prof_conflicts += 1
sub_group_conflicts += len(
set(activity.group_ids).intersection(sub_group_set))
group_size = 0
for group_id in activity.group_ids:
group_size += groups_dict[group_id].size
sub_group_set.add(group_id)
if group_size > spaces_dict[room].size:
room_size_conflicts += 1
teacher_id = activity.teacher_id
prof_set.add(teacher_id)
unasigned_activities -= len(activities_set)
return vacant_room, prof_conflicts, room_size_conflicts, sub_group_conflicts, unasigned_activities
def get_classsize(activity: Activity) -> int:
classsize = 0
for id in activity.group_ids:
classsize += groups_dict[id].size
return classsize
def evaluate_population(population):
"""Evaluate each individual using the provided evaluator function."""
fitness_values = []
for timetable in population:
fitness_values.append(evaluator(timetable))
return fitness_values
def mutate(individual):
"""Perform mutation by randomly swapping activities in the timetable."""
slots = list(individual.keys())
slot1, slot2 = random.sample(slots, 2)
room1, room2 = random.choice(
list(individual[slot1])), random.choice(list(individual[slot2]))
individual[slot1][room1], individual[slot2][room2] = individual[slot2][room2], individual[slot1][room1]
def crossover(parent1, parent2):
"""Perform crossover by swapping time slots between two parents."""
child1, child2 = parent1.copy(), parent2.copy()
slots = list(parent1.keys())
split = random.randint(0, len(slots) - 1)
for i in range(split, len(slots)):
child1[slots[i]], child2[slots[i]
] = parent2[slots[i]], parent1[slots[i]]
return child1, child2
def fast_nondominated_sort(fitness_values):
"""Perform non-dominated sorting based on the multi-objective fitness values."""
fronts = [[]]
S = [[] for _ in range(len(fitness_values))]
n = [0] * len(fitness_values)
rank = [0] * len(fitness_values)
for p in range(len(fitness_values)):
for q in range(len(fitness_values)):
if dominates(fitness_values[p], fitness_values[q]):
S[p].append(q)
elif dominates(fitness_values[q], fitness_values[p]):
n[p] += 1
if n[p] == 0:
rank[p] = 0
fronts[0].append(p)
i = 0
while fronts[i]:
next_front = []
for p in fronts[i]:
for q in S[p]:
n[q] -= 1
if n[q] == 0:
rank[q] = i + 1
next_front.append(q)
i += 1
fronts.append(next_front)
return fronts[:-1]
def dominates(fitness1, fitness2):
"""Return True if fitness1 dominates fitness2."""
return all(f1 <= f2 for f1, f2 in zip(fitness1, fitness2)) and any(f1 < f2 for f1, f2 in zip(fitness1, fitness2))
def calculate_crowding_distance(front, fitness_values):
"""Calculate crowding distance for a front."""
distances = [0] * len(front)
num_objectives = len(fitness_values[0])
for m in range(num_objectives):
front.sort(key=lambda x: fitness_values[x][m])
distances[0] = distances[-1] = float('inf')
min_value = fitness_values[front[0]][m]
max_value = fitness_values[front[-1]][m]
if max_value == min_value:
continue
for i in range(1, len(front) - 1):
distances[i] += (fitness_values[front[i + 1]][m] -
fitness_values[front[i - 1]][m]) / (max_value - min_value)
return distances
def select_parents(population, fitness_values):
"""Perform tournament selection based on non-dominated sorting and crowding distance."""
fronts = fast_nondominated_sort(fitness_values)
selected = []
for front in fronts:
if len(selected) + len(front) > POPULATION_SIZE:
crowding_distances = calculate_crowding_distance(
front, fitness_values)
sorted_front = sorted(
zip(front, crowding_distances), key=lambda x: x[1], reverse=True)
selected.extend(
[x[0] for x in sorted_front[:POPULATION_SIZE - len(selected)]])
break
else:
selected.extend(front)
return [population[i] for i in selected]
def generate_initial_population():
"""Generate an initial population with random timetables."""
population = []
for _ in range(POPULATION_SIZE):
timetable = {}
activity_slots = {activity.id: []
for activity in activities_dict.values()}
activities_remain = [activity.id for activity in activities_dict.values()
for _ in range(activity.duration)]
for slot in slots:
timetable[slot] = {}
for space_id in spaces_dict.keys():
space = spaces_dict[space_id]
a_activities = [activity for activity in activities_remain if get_classsize(
activities_dict[activity]) <= space.size]
a_activities = [
activity for activity in a_activities if slot not in activity_slots[activity]]
activity = random.sample(a_activities, 1)[0]
timetable[slot][space_id] = activities_dict[activity]
activity_slots[activity].append(slot)
population.append(timetable)
return population
def nsga2():
"""Main NSGA-II algorithm loop."""
population = generate_initial_population()
for generation in range(NUM_GENERATIONS):
fitness_values = evaluate_population(population)
new_population = []
while len(new_population) < POPULATION_SIZE:
parent1, parent2 = random.sample(population, 2)
if random.random() < CROSSOVER_RATE:
child1, child2 = crossover(parent1, parent2)
else:
child1, child2 = parent1.copy(), parent2.copy()
if random.random() < MUTATION_RATE:
mutate(child1)
if random.random() < MUTATION_RATE:
mutate(child2)
new_population.extend([child1, child2])
population = select_parents(
new_population, evaluate_population(new_population))
return population
# Run NSGA-II (Dictionary-based)
nsga2_dict_populations = nsga2()
schedule = {}
minimum = 1000
for population in nsga2_dict_populations:
if sum(evaluator(population))<minimum:
minimum = sum(evaluator(population))
schedule = population
# In[20]:
evaluate(schedule,groups_dict, lecturers_dict, activities_dict, spaces_dict,slots)
#
# ### **Defining the Chromosome as {Activity: (Room, Slot)}**
#
# **Overview**
# This approach represents a timetable chromosome as a mapping of activities to assigned rooms and slots. The algorithm optimizes assignments by minimizing conflicts in student schedules, professor availability, and room capacity constraints.
#
# **Functions**
#
# **`fitness(solution)`**
# Evaluates a timetable based on:
# - Room overbooking (capacity exceeded).
# - Slot conflicts (students assigned to multiple activities at the same time).
# - Professor conflicts (same professor assigned to multiple courses in overlapping slots).
#
# **Returns:** `(room_overbooking, slot_conflicts, prof_conflicts)`.
#
# **`get_classsize(activity: Activity) -> int`**
# Computes the total class size for an activity based on assigned student groups.
#
# **`create_population(pop_size)`**
# Generates a random initial population of timetable solutions, ensuring:
# - Activities are assigned to rooms with sufficient capacity.
# - Valid slots are selected considering room and professor availability.
#
# **Returns:** A list of randomly generated timetable solutions.
#
# **`crossover(parent1, parent2)`**
# Performs a single-point crossover on two parent timetables.
#
# **Returns:** Two offspring solutions.
#
# **`non_dominated_sorting(population, fitness_values)`**
# Ranks individuals based on Pareto dominance for multi-objective optimization.
#
# **Returns:** A list of ranked fronts.
#
# **`crowding_distance(front, fitness_values)`**
# Calculates crowding distances to maintain diversity among solutions.
#
# **Returns:** A list of crowding distances for individuals in the front.
#
# **`tournament_selection(population, fitness_values)`**
# Selects parents for crossover using tournament selection based on dominance.
#
# **Returns:** Index of the selected parent.
#
# **`mutate(solution)`**
# Randomly alters a timetable solution by modifying room and slot assignments.
#
# **Returns:** The mutated solution.
#
# **`dominates(fit1, fit2)`**
# Determines if one fitness tuple dominates another.
#
# **Returns:** `True` if `fit1` dominates `fit2`, otherwise `False`.
#
# ---
#
# ## NSGA-II Algorithm
#
# **`run_nsga2(pop_size=50, generations=200)`**
# Executes the NSGA-II optimization loop:
# 1. Generates an initial population.
# 2. Evaluates fitness and applies non-dominated sorting.
# 3. Performs selection, crossover, and mutation.
# 4. Maintains diversity using crowding distance.
#
# **Returns:** The final evolved population.
#
# **`adapter(solution)`**
# Converts a chromosome-based solution into a structured timetable format.
#
# **Returns:** A dictionary-based timetable representation.
#
# ---
#
# ## Running the Algorithm
# The best timetable is selected based on the lowest sum of evaluation metrics:
#
# ```python
# final_populations = run_nsga2()
#
# schedule = {}
# minimum = 1000
# for population in final_populations:
# timetable = adapter(population)
# if sum(evaluator(timetable)) < minimum:
# minimum = sum(evaluator(timetable))
# schedule = timetable
# ```
# ```
# In[21]:
import random
def fitness(solution):
room_overbooking = 0
slot_conflicts = 0
prof_conflicts = 0
slot_assignments = {}
prof_assignments = {}
# Evaluate room overbooking and slot conflicts
for entry in solution:
course_id, student_group, room_id, valid_slots, professor = entry
course = activities_dict[course_id]
room = spaces_dict[room_id]
# Room overbooking
if room.size < get_classsize(course):
room_overbooking += 1
# Slot conflicts (same student in multiple classes at the same time)
for slot_id in valid_slots:
if slot_id not in slot_assignments:
slot_assignments[slot_id] = []
slot_assignments[slot_id].append((student_group, course_id))
# Professor conflicts (same professor teaching multiple courses at the same time)
if course.teacher_id not in prof_assignments:
prof_assignments[course.teacher_id] = []
prof_assignments[course.teacher_id].append(slot_id)
# Calculate slot conflicts
for slot_id, assignments in slot_assignments.items():
student_groups_in_slot = {}
for student_groups, course_id in assignments:
for student_group in student_groups:
if student_group in student_groups_in_slot:
slot_conflicts += 1 # Conflict when a student is assigned to two courses at the same time
else:
student_groups_in_slot[student_group] = course_id
# Calculate professor conflicts
for professor, assigned_slots in prof_assignments.items():
if len(assigned_slots) > len(set(assigned_slots)): # Duplicate slots means conflict
prof_conflicts += 1
return room_overbooking, slot_conflicts, prof_conflicts
def get_classsize(activity: Activity) -> int:
classsize = 0
for id in activity.group_ids:
classsize += groups_dict[id].size
return classsize
# Updated create_population function
def create_population(pop_size):
population = []
for _ in range(pop_size):
solution = []
room_slot_assignments = {space.code: set()
# Track slots per room
for space in spaces_dict.values()}
prof_slot_assignments = {prof: set() for prof in set(
activity.teacher_id for activity in activities_dict.values())} # Track professor slots
for activity in activities_dict.values():
# course = courses[course_id]
student_groups = activity.group_ids
rooms = [room for room in spaces_dict.values(
) if room.size >= get_classsize(activity)]
# student_group = course.student # Get student group
selected_room = random.choice(rooms) # Random room
# Find consecutive slots for the course
valid_slots = [
slot for slot in slots if slot not in room_slot_assignments[selected_room.code] and slot not in prof_slot_assignments[activity.teacher_id]]
if len(valid_slots) >= activity.duration:
selected_slots = random.sample(valid_slots, activity.duration)
else:
selected_slots = random.sample(slots, activity.duration)
for slot in selected_slots:
room_slot_assignments[selected_room.code].add(slot)
prof_slot_assignments[activity.teacher_id].add(slot)
solution.append(
(activity.id, student_groups, selected_room.code, selected_slots, activity.teacher_id))
population.append(solution)
return population
# Crossover function
def crossover(parent1, parent2):
crossover_point = random.choice(range(1, len(parent1)))
child1 = parent1[:crossover_point] + parent2[crossover_point:]
child2 = parent2[:crossover_point] + parent1[crossover_point:]
return child1, child2