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