Can you create a school timetable with Schedule Nurse?
Yes, it is possible to create it flexibly using Python.
For example, We will try implementing it using a task working project.
This project is for a demonstration that shows the high descriptive power of ScheduleNurse.
Mapping
Regarding the task work schedule in Schedule Nurse, Staff, Day and Shift, and Task are variable targets.
Naturally, as is, they do not match the timetabling image.
So we map the timetable as follows.
Item | Timetable | Project in Schedule Nurse |
---|---|---|
1 | Class | Staff |
2 | Period | Phase |
3 | Day of the week | Day of the week |
4 | Subject | Task |
Class Name Definition
Describe on the staff property sheet.
Define the subjects
Implement them as tasks.
Set the period
There are up to 6 time periods, so we define phases 0-5.
Period, days of the week
The appropriate Monday through Friday is used as the display period, which is then used as the timetable for Monday through Friday as it is.
Item | Timetable | Schedule Nurse |
---|---|---|
1 | Monday | Monday |
2 | Thueday | Tuesday |
3 | Wednesday | Wednesday |
4 | Thursday | Thursday |
5 | Friday | Friday |
Calendar settings can be anywhere.
The task schedule now has the following squares for six periods on each day of the week.
Each subject does the required number of classes per week
One week on the timetable will be the entire month period on the project.
We will constrain the number of classes for English, Mathematics, Japanese, Social Sciences, and Science to 4 per week.
Constrain in the same way below.
Each subject adheres to the upper and lower limits of the number of classes per day
The number of classes per day for each subject is less than 1.
It should be constrained for each day of the week.
Physical education and other mobile classes are not consecutive
All combinations of Art, Physical education, Music, Technology, and Home economics are prohibited.
The next set of tasks is the subject that students should change the classroom.
Integral and Moral are to be done in the 6th period
Describe using column constraints; Integral and Moral are not allowed outside of the 6th period.
Integral and Moral should be done on the same day of the week in the school grade
They are described using pair constraints.
The first line is a constraint that says that if the representative class of year 1 is Integral, all grade 1 classes will be (and are) Integral.
Integral and Moral are not done at the same time in different grades
The description is written using column constraints.
Since the above assumes a unified movement through the grades, it is sufficient to constrain one class that represents the grade level.
The advantage of using GUI is that you can immediately solve each problem and check the validity of the description as you write it.
This process is surprisingly enjoyable.
The next step is to use Python because it is difficult to write in GUI or it is easier to write in Python.
Faculty Constraints
The source in the article downloads a CSV file.
lesson_df = pd.read_csv("https://raw.githubusercontent.com/sugawara-system/Schedule_Nurse3_Gallery/main/English/Project_Samples/or_tools/timetabling.csv")
If you look at this file, you will see that gr is the grade column and cl is the class name, for example, teacher 6 for English in 3-1, and once the class is determined, the teacher of the subject in charge is determined by this table.
Teacher 6 handles English classes for all 3rd-grade courses, not just 3rd-grade one and 3rd-grade 3 Integrated_Studies and Moral_Education.
gr | cl | English | Math | Japanese | Science | Social_Studies | Art | Music | Physical_Education | Technology | Home_Economics | Integrated_Studies | Moral_Education |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
3 | 1 | Teacher6 | Teacher9 | Teacher15 | Teacher14 | Teacher18 | Teacher0 | Teacher1 | Teacher2 | Teacher5 | Teacher21 | Teacher9 | Teacher9 |
3 | 2 | Teacher6 | Teacher9 | Teacher15 | Teacher14 | Teacher20 | Teacher0 | Teacher1 | Teacher2 | Teacher5 | Teacher21 | Teacher2 | Teacher2 |
3 | 3 | Teacher6 | Teacher11 | Teacher15 | Teacher13 | Teacher18 | Teacher0 | Teacher1 | Teacher2 | Teacher5 | Teacher21 | Teacher6 | Teacher6 |
3 | 4 | Teacher6 | Teacher9 | Teacher15 | Teacher13 | Teacher18 | Teacher0 | Teacher1 | Teacher2 | Teacher5 | Teacher21 | Teacher18 | Teacher18 |
3 | 5 | Teacher6 | Teacher9 | Teacher15 | Teacher13 | Teacher18 | Teacher0 | Teacher1 | Teacher2 | Teacher5 | Teacher21 | Teacher15 | Teacher15 |
2 | 1 | Teacher7 | Teacher10 | Teacher16 | Teacher12 | Teacher19 | Teacher0 | Teacher1 | Teacher3 | Teacher5 | Teacher21 | Teacher3 | Teacher3 |
2 | 2 | Teacher7 | Teacher10 | Teacher16 | Teacher12 | Teacher19 | Teacher0 | Teacher1 | Teacher3 | Teacher5 | Teacher21 | Teacher19 | Teacher19 |
2 | 3 | Teacher7 | Teacher10 | Teacher16 | Teacher12 | Teacher19 | Teacher0 | Teacher1 | Teacher3 | Teacher5 | Teacher21 | Teacher10 | Teacher10 |
2 | 4 | Teacher7 | Teacher10 | Teacher16 | Teacher12 | Teacher19 | Teacher0 | Teacher1 | Teacher3 | Teacher5 | Teacher21 | Teacher7 | Teacher7 |
1 | 1 | Teacher8 | Teacher11 | Teacher17 | Teacher13 | Teacher20 | Teacher0 | Teacher1 | Teacher4 | Teacher5 | Teacher21 | Teacher11 | Teacher11 |
1 | 2 | Teacher8 | Teacher11 | Teacher17 | Teacher13 | Teacher20 | Teacher0 | Teacher1 | Teacher4 | Teacher5 | Teacher21 | Teacher17 | Teacher17 |
1 | 3 | Teacher8 | Teacher11 | Teacher17 | Teacher14 | Teacher20 | Teacher0 | Teacher1 | Teacher4 | Teacher5 | Teacher21 | Teacher14 | Teacher14 |
1 | 4 | Teacher8 | Teacher11 | Teacher17 | Teacher14 | Teacher20 | Teacher0 | Teacher1 | Teacher4 | Teacher5 | Teacher21 | Teacher8 | Teacher8 |
1 faculty member teaches one course in the same period
One faculty member should assign no more than two courses to the same period.
List each faculty member’s possible shifts (courses) and constrain them so that the sum of the shifts (courses) is less than or equal to 1 in the same period.
The following source is the description part.
def empty_work(dic,dic_units):#1The sum of classes must be less than or equal one per teacher
for name in dic:
for day in ThisMonth:
week_vlist=[]
for ph in dayphase_list:
ph_val=ph[1]
vlist=[]
for tp in dic[name]:
subject=list_of_rows[0][tp[1]]
Class=tp[0]-1
#print(Class,day,subject)
v=sc3.GetTaskVar(Class,day,ph_val,subject)
vlist.append(v)
sc3.AddHard(sc3.SeqLE(0,1,vlist),'The sum of classes must be less than or equal one per teacher '+name)
v=sc3.Or(vlist)
week_vlist.append(v)
div=dic_units[name]/5
print(name+' Leveling classes',math.floor(div),math.ceil(div))
sc3.AddHard(sc3.SeqLE(math.floor(div),math.ceil(div),week_vlist),name)
The number of classes each teacher teaches per day should be equalized
It should not differ greatly on each day of the week.
The number of classes each teacher is responsible for is different, so it would be better to equalize the number of classes for each teacher each day.
Once the teachers have been determined, the average number of frames per day per timetable day is fixed.
The upper and lower limits are simply written as hard constraints based on the floor and ceilings of the average value.
In this case, hard constraints described the solution, and a solution existed.
If there were no solution here, it would have been necessary to change to a soft constraint, but since a solution existed, we left the hard constraint as it is.
The above description is also done in the above source.
The number of classes per grade level for each term by the teacher of the grade level to which they belong
It is an equalization of the number of classes to be taught in each period for each grade level.
Since the average number of classes is determined for each grade level, the upper and lower limits are simply written as hard constraints based on the floor and ceilings of the average value.
In this case, we used hard constraints, and a solution existed. If there were no solution here, it would have been necessary to change to a soft constraint, but since a solution existed, we left the hard constraint as it is.
The above description is done in the following source.
def empty_frame_avg(dic,grade_list,empty_avg_frames):#levelling the number of classes
for day in ThisMonth:
for ph in dayphase_list:
ph_val=ph[1]
Vlist={}
for name in dic:
vlist=[]
for tp in dic[name]:
subject=list_of_rows[0][tp[1]]
Class=tp[0]-1
#print(Class,day,subject)
v=sc3.GetTaskVar(Class,day,ph_val,subject)
vlist.append(v)
v=sc3.Or(vlist)
grade=get_grade(name,grade_list)
if grade not in Vlist:
list=[]
list.append(~v)
Vlist[grade]=list
else:
Vlist[grade].append(~v)
for item in Vlist:
f=empty_avg_frames[item]
s=str(item)+' Grade levelling the number of classes for each day of the week for each teacher '+str(ph_val+1)+'Period'
print(s,math.floor(f),math.ceil(f))
sc3.AddHard(sc3.SeqLE(math.floor(f),math.ceil(f),Vlist[item]),s)
Entire Source
import sc3
import csv
import urllib.request
import math
import re
def get_grade(name,grade_list):
t_ind = int(re.sub(r"\D", "", name))
grade=grade_list[t_ind]
return grade
def empty_work(dic,dic_units):#1The sum of classes must be less than or equal one per teacher
for name in dic:
for day in ThisMonth:
week_vlist=[]
for ph in dayphase_list:
ph_val=ph[1]
vlist=[]
for tp in dic[name]:
subject=list_of_rows[0][tp[1]]
Class=tp[0]-1
#print(Class,day,subject)
v=sc3.GetTaskVar(Class,day,ph_val,subject)
vlist.append(v)
sc3.AddHard(sc3.SeqLE(0,1,vlist),'The sum of classes must be less than or equal one per teacher '+name)
v=sc3.Or(vlist)
week_vlist.append(v)
div=dic_units[name]/5
print(name+' Leveling classes',math.floor(div),math.ceil(div))
sc3.AddHard(sc3.SeqLE(math.floor(div),math.ceil(div),week_vlist),name)
def empty_frame_avg(dic,grade_list,empty_avg_frames):#levelling the number of classes
for day in ThisMonth:
for ph in dayphase_list:
ph_val=ph[1]
Vlist={}
for name in dic:
vlist=[]
for tp in dic[name]:
subject=list_of_rows[0][tp[1]]
Class=tp[0]-1
#print(Class,day,subject)
v=sc3.GetTaskVar(Class,day,ph_val,subject)
vlist.append(v)
v=sc3.Or(vlist)
grade=get_grade(name,grade_list)
if grade not in Vlist:
list=[]
list.append(~v)
Vlist[grade]=list
else:
Vlist[grade].append(~v)
for item in Vlist:
f=empty_avg_frames[item]
s=str(item)+' Grade levelling the number of classes for each day of the week for each teacher '+str(ph_val+1)+'Period'
print(s,math.floor(f),math.ceil(f))
sc3.AddHard(sc3.SeqLE(math.floor(f),math.ceil(f),Vlist[item]),s)
def get_list_of_rows():
url="https://raw.githubusercontent.com/sugawara-system/Schedule_Nurse3_Gallery/main/English/Project_Samples/or_tools/timetabling.csv"
response = urllib.request.urlopen(url)
lines = [l.decode('utf-8') for l in response.readlines()]
#print(lines)
reader=csv.reader(lines)
list_of_rows=list(reader)
return list_of_rows
def get_teacher(Class,subject,list_of_rows):
ind= list_of_rows[0].index(subject)
return list_of_rows[Class+1][ind]
def get_teacher_ind(Class,subject,list_of_rows):
name=get_teacher(Class,subject,list_of_rows)
t_ind = int(re.sub(r"\D", "", name))
return t_ind
def post_main():
print('Executing Post Main')
grade_list=[3,3,3,2,1,1,3,2,1,3,2,1,2,1,1,3,2,1,3,2,3,2]
list_of_rows=get_list_of_rows()
tmap={}
grade_map={}
for Class in A_Member_in_All:
for D in ThisMonth:
for ph in dayphase_list:
ph_val=ph[1]
day=D*len(dayphase_list)+ph_val
subject=task_solution[Class][day]
t_ind=get_teacher_ind(Class,subject,list_of_rows)
grade=grade_list[t_ind]
if grade not in grade_map:
list=[0]* len(ThisMonth)*len(dayphase_list)
list[day]+=1
grade_map[grade]=list
else:
grade_map[grade][day]+=1
#print(teacher,subject)
Day=D
if t_ind not in tmap:
list=[0,0,0,0,0]
list[Day]+=1
tmap[t_ind]=list
else:
tmap[t_ind][Day]+=1
tmap2=sorted(tmap.items())
#print(tmap2)
#print(grade_map)
print('Results of levelling the number of classes for each day of the week for each teacher')
print(' MonTueWedThuFri')
for t in tmap2:
print('Teacher'+str(t))
print('')
print('Equal number of classes for each period for each year group = equal number of available classes Result of equal number of classes for each year group')
for g in grade_map:
print(str(g)+'Grade:')
print(' MonTueWedThuFri')
for i in range(len(dayphase_list)):
print(str(i+1)+'Period ',end='')
for day in ThisMonth:
print(grade_map[g][day*len(dayphase_list)+i],' ',end='')
print('')
print('')
list_of_rows=get_list_of_rows()
#exit()
print(list_of_rows)
dic={}
dic_units={}
grade_units={}
grade_list=[3,3,3,2,1,1,3,2,1,3,2,1,2,1,1,3,2,1,3,2,3,2]
for list in list_of_rows:
col=0
subject_units=[0,0,4,4,4,4,4,2,2,2,1,1,1,1]
for item in list:
if 'Teacher' in item:
if item not in dic:
l=[]
c=list.index(item)
dic_units[item]=subject_units[c]
l.append((list_of_rows.index(list),list.index(item)))
dic[item]=l
else:
dic_units[item]+=subject_units[col]
dic[item].append((list_of_rows.index(list),col))
col+=1
for item in dic_units:
grade=get_grade(item,grade_list)
if grade not in grade_units:
grade_units[grade]=dic_units[item]
else:
grade_units[grade]+=dic_units[item]
print(grade_units)
print(dic)
print(dic_units)
empty_work(dic,dic_units)
teachers={1:grade_list.count(1),2:grade_list.count(2),3:grade_list.count(3)}
print(teachers)
empty_avg_frames={}
for t in teachers:
empty_avg_frames[t]=(teachers[t]*6*5-grade_units[t])/(6*5)#平均空きコマ数を求めて、そのfloor,ceilを制約範囲とする
print(empty_avg_frames)
empty_frame_avg(dic,grade_list,empty_avg_frames)
Solution
Verification
After the solution is obtained, the post-processing is used to verify the validity of the solution.
The first step is to check for any bias in the number of Monday-Friday classes for each teacher.
Since floor ceil is done for the mean, the deviation is within 1.
Next, the number of classes for each grade is checked for bias.
This is also checked by floor and ceil for the average value, so the deviation is within 1.
It seems to be working as constrained.
The above Python description is post_main in the Python source code.
Solution Speed
Algorithm | Speed | Remarks |
---|---|---|
1 | 2.8sec | |
2(Highs) | 589sec |
Load the Project File
File → Open Project File from GitHub