Scheduling problem - Nurse Rostering Problem (NRP) actual combat

This article has participated in the "Newcomer Creation Ceremony" event to start the road of gold creation together.

background

Last weekend, I met my girlfriend on the schedule. It took almost an hour. I thought about using the code to save her some time.

Question research

I checked the Internet and read several papers to understand the background.

citeseerx.ist.psu.edu/viewdoc/dow…

arxiv.org/pdf/1804.05…

Problems like scheduling are collectively known as the Nurse Rostering Problem (NRP) problem, which is an NP-hard problem in terms of complexity

P problems are problems that can be solved in polynomial time, while NP problems are problems that can be verified for correctness in polynomial time

What does polynomial time mean? I understand it is the algorithm complexity of the usual calculation

O(1) – constant-time
O(log_2(n)) – logarithmic-time
O(n) – linear-time
O(n^2) – quadratic-time
O(n^k) – polynomial-time
O(k^n) – exponential-time
O(n!) – factorial-time
复制代码

A description of the NP problem can be found in this article I found

The complexity theory is mentioned because the right tools can be found only after analyzing the problem more precisely.

We will think of using methods in computer science to deal with such problems, such as machine learning.

Tool Finder - Find the shoulders of giants

The technology stack of machine learning, what I have learned is scikit-learn, TensorFlow and Keras in Python to make choices

As far as I know, this is a topic that has been studied for many years. In this case, there should be ready-made trained models to do this kind of thing.

In the end, I found OR-Tools ( Official Site ) developed by Google. It already has models trained to handle this kind of scheduling problem.

Action

Code example

Official example Source Code

The main thing is to adjust the source code to meet the actual needs.

Basic usage of or-tools

Pseudocode excerpted to understand how it works

// 选择和定义一个model,然后设置所需的前置数据和限制条件
model = cp_model.CpModel()
model.NewBoolVar(...)
model.AddExactlyOne(...)
model.Add(...)

// 选择和定义一个solver,然后针对model进行optimizate
solver = cp_model.CpSolver()
status = solver.Solve(model, solution_printer)

// 创建一个矩阵进行拟合
work = {}
for e in range(num_employees):
    for s in range(num_shifts):
        for d in range(num_days):
            work[e, s, d] = model.NewBoolVar('work%i_%i_%i' % (e, s, d))
// 下面是打印结果的算法
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
     print()
     header = '          '
     for w in range(num_weeks):
         header += 'M T W T F S S '
     print(header)
     for e in range(num_employees):
         schedule = ''
         for d in range(num_days):
             for s in range(num_shifts):
                 if solver.BooleanValue(work[e, s, d]): // NOTE: 获取优化算法出来的结果
                     schedule += shifts[s] + ' '
         print('worker %i: %s' % (e, schedule))
     print()
复制代码

Parameter adjustment

The parameter comments in the example source code clearly indicate the usage, and it is enough to make appropriate adjustments to the needs of practical problems. The following pseudo code is just for some description of the parameters.

num_employees = 4 // 员工数,改为目标的员工数
num_weeks = 5 // 工作天数,目标是排一个月的班表,改为5周
shifts = ['O', 'M', 'A', 'N'] // 班期,和目标一样

# Fixed assignment: (employee, shift, day).
# 固定前两天的,排班前得动态调整下
fixed_assignments = [
    (0, 0, 0),
    (1, 1, 0),
    (2, 2, 0),
    (3, 3, 0),
    (0, 0, 1),
    (1, 1, 1),
    (2, 2, 1),
    (3, 3, 1),
]

# Request: (employee, shift, day, weight)
# 员工想要固定休息的时间(位置),权重值我理解为代表拟合时可调整的优先级
requests = [
    # Employee 3 does not want to work on the first Saturday (negative weight for the Off shift).
    (3, 0, 5, -2),
    # Employee 2 does not want a night shift on the first Friday (positive weight).
    (2, 3, 4, 4)
]

# Shift constraints on continuous sequence :
#     (shift, hard_min, soft_min, min_penalty,
#             soft_max, hard_max, max_penalty)
# hard_min: 硬性限制,在周期内最少连续要上的班期天数
# soft_min: 软性限制,在周期内最少连续要上的班期天数
# soft_max和hard_max同上去理解
shift_constraints = [
    # One or two consecutive days of rest, this is a hard constraint.
    (0, 1, 1, 0, 2, 2, 0),
    # betweem 2 and 3 consecutive days of night shifts, 1 and 4 are
    # possible but penalized.
    (3, 1, 2, 20, 3, 4, 5),
]

# Weekly sum constraints on shifts days:
#     (shift, hard_min, soft_min, min_penalty,
#             soft_max, hard_max, max_penalty)
weekly_sum_constraints = [
    # 每周最少要休息的天数
    (0, 1, 2, 7, 2, 3, 4),
]

# Penalized transitions:
#     (previous_shift, next_shift, penalty (0 means forbidden))
penalized_transitions = [
    # 尽量避免午班换晚班
    (2, 3, 4),
    # 晚班不能接着早班
    (3, 1, 0),
]

# daily demands for work shifts (morning, afternon, night) for each day
# of the week starting on Monday.
# 每个班期最少要有多少人,我这里的现实问题是有一个人就行了
weekly_cover_demands = [
        (1, 1, 1),  # Monday
        (1, 1, 1),  # Tuesday
        (1, 1, 1),  # Wednesday
        (1, 1, 1),  # Thursday
        (1, 1, 1),  # Friday
        (1, 1, 1),  # Saturday
        (1, 1, 1),  # Sunday
]

# Penalty for exceeding the cover constraint per shift type.
excess_cover_penalties = (2, 2, 5)

num_days = num_weeks * 7
num_shifts = len(shifts)
复制代码

draw a gantt chart

In the technology stack, matplot is selected to draw Gantt charts, which is also now learning and selling.

# Generate plot image. [reference: https://www.geeksforgeeks.org/python-basic-gantt-chart-using-matplotlib]
	shifts_colors = ['white', 'tab:green', 'tab:orange', 'tab:blue']
    if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
        offset = 2
        # Declaring a figure "gnt"
        fig, gnt = plt.subplots()
        # Setting Y-axis limits
        gnt.set_ylim(0, num_employees * 15)
        # Setting X-axis limits
        gnt.set_xlim(1, num_days + offset)
        # Setting labels for x-axis and y-axis
        gnt.set_xlabel('Dates')
        gnt.set_ylabel('Employees')
        # Setting ticks on y-axis
        # gnt.set_yticks([15, 25, 35, 45])
        gnt.set_xticks(list(range(1, num_days + offset, 1)))
        gnt.set_yticks(list(range(15, (num_employees + 1) * 10 + 5, 10)))
        # Labelling tickes of y-axis
        gnt.set_yticklabels(employees_names)
        # Setting graph attribute
        gnt.grid(True)

        for e in range(num_employees):
            for d in range(num_days):
                for s in range(num_shifts):
                    if solver.BooleanValue(work[e, s, d]):
                        gnt.broken_barh([(d+1, d+2)], (10*(e+1), 9),
                                        facecolors=(shifts_colors[s]))

        plt.savefig("gantt1.png")
复制代码

result:insert image description here

Summarize

  • Fully understand the problem in order to find the tool
  • Fortunately, this topic has already been studied. Through the topics that have been studied and the ideas of tools and solutions, we can better solve other problems.

Actual code: github.com/pascallin/N…

Guess you like

Origin juejin.im/post/7083302682802257956