使用线性规划的班次自动化程序的目标函数和最小约束定义

问题描述 投票:0回答:1

我正在为一家小公司开发轮班自动化系统。旨在根据每个班次的不同需求优化班次分配。然而,我遇到了一个问题,即工人的分配与提供的比例不符。

如何确保工人的分配符合规定的比例?我已经彻底检查了我的代码,但无法确定问题的根源。

import json
import pulp

days = 7  # One week (7 days)
# Number of weeks (modify as needed)
employees = 200  # Replace with the actual number of employees

var_8_16 = pulp.LpVariable.dicts("8_16", (range(days), range(employees)), 0, 1, "Binary")
var_16_24 = pulp.LpVariable.dicts("16_24", (range(days), range(employees)), 0, 1, "Binary")
var_24_8 = pulp.LpVariable.dicts("24_8", (range(days), range(employees)), 0, 1, "Binary")
var_Holiday = pulp.LpVariable.dicts("Tatil", (range(days), range(employees)), 0, 1, "Binary")

# Objective function to minimize the difference between workload for fairness
obj = None
for i in range(days):
    for j in range(employees):
        shifts_worked = var_8_16[i][j] + var_16_24[i][j] + var_24_8[i][j]
        difference = shifts_worked - 5  # Desired target is 5 shifts
        obj += difference
problem = pulp.LpProblem("Vardiya", pulp.LpMinimize)
problem += obj

## Constraint 0: Every worker has to either work or be on holiday each day
for i in range(days):
    for j in range(employees):
        c = None
        c += var_8_16[i][j] + var_16_24[i][j] + var_24_8[i][j] + var_Holiday [i][j]
        problem += c == 1

## Constraint 1: Each shift must have at least c, d, f employees per week
worker_daily_ratio = 0.7
min_worker_ratio = {
    "8_16": 0.5,
    "16_24": 0.2,
    "24_8": 0.3
}

for i in range(days):
    c_min =+ sum(var_8_16[i][j] for j in range(employees))
    d_min =+ sum(var_16_24[i][j] for j in range(employees))
    f_min =+ sum(var_24_8[i][j] for j in range(employees))
# Defining the lower and upper bounds for the constraint
problem += c_min >= (min_worker_ratio["8_16"] * worker_daily_ratio * employees)
problem += c_min <= (min_worker_ratio["8_16"] * worker_daily_ratio * employees) + 4
problem += d_min >= (min_worker_ratio["16_24"] * worker_daily_ratio * employees)
problem += d_min <= (min_worker_ratio["16_24"] * worker_daily_ratio * employees) + 4
problem += f_min >= (min_worker_ratio["24_8"] * worker_daily_ratio * employees)
problem += f_min <= (min_worker_ratio["24_8"] * worker_daily_ratio * employees) + 4

# Constraint 1: Each employee must work 5 shifts per week
desired_workload = 5
for j in range(employees):
    c = None
    for i in range(0, days):
        c += var_8_16[i][j] + var_16_24[i][j] + var_24_8[i][j]
    problem += c == desired_workload

## Constraint 2: Each employee must have 2 days off per week
for j in range(employees):
    c = None
    for i in range(0, days):
        c += var_Holiday[i][j]
    problem += c == 2

# Constraint 3: Each employee works on the same shift throughout the week
for j in range(employees):
    # If the holidays are back to back
    for i in range(days - 3):
        a_btb = var_8_16[i][j] + var_16_24[i + 3][j] + var_24_8[i + 3][j]
        b_btb = var_16_24[i][j] + var_8_16[i + 3][j] + var_24_8[i + 3][j]
        k_btb = var_24_8[i][j] + var_8_16[i + 3][j] + var_16_24[i + 3][j]
        problem += a_btb <= 1
        problem += b_btb <= 1
        problem += k_btb <= 1
    # If the holidays are not back to back
    for i in range(days - 1):
        c_nbtb = var_8_16[i][j] + var_16_24[i + 1][j] + var_24_8[i + 1][j]
        d_nbtb = var_16_24[i][j] + var_8_16[i + 1][j] + var_24_8[i + 1][j]
        f_nbtb = var_24_8[i][j] + var_8_16[i + 1][j] + var_16_24[i + 1][j]
        problem += c_nbtb <= 1
        problem += d_nbtb <= 1
        problem += f_nbtb <= 1

我尝试了另一种方法,试图最大限度地增加每个工人在一周内轮班的次数。不幸的是,这个策略也没有产生预期的结果。

由于我目前是一名实习生,所以我可能无法完全了解可能出现的问题。

python automation linear-programming pulp
1个回答
0
投票

“公平”目标没有意义,因为每个工人的工作量固定为五天。我的论证没有这个目的。对于轮班工人来说,更有用的目标是最大化轮班分配的连续性,但这表达起来有点复杂。也许这就是您在“背对背”逻辑中的意图,但我认为该代码没有达到其预期效果。

“假期”实际上只是一个不工作的轮班,因此您可以删除该变量。

我不明白

worker_daily_ratio
应该达到什么目的。如所写,每个轮班每周必须至少有 c、d、f 名员工 不会使用
worker_daily_ratio

import pandas as pd
import pulp

n_days = 7
n_workdays = 5
n_workers = 20

worker_idx = pd.RangeIndex(name='worker', start=0, stop=n_workers)
day_idx = pd.RangeIndex(name='day', start=0, stop=n_days)
shift_idx = pd.RangeIndex(name='shift', start=0, stop=24, step=8)

# For each day, shift and worker: are they working?
day_shift_idx = pd.MultiIndex.from_product((worker_idx, day_idx, shift_idx))
day_shift_names = (
    'w' + day_shift_idx.get_level_values('worker').astype(str) +
    '_d' + day_shift_idx.get_level_values('day').astype(str) +
    '_s' + day_shift_idx.get_level_values('shift').astype(str)
)
day_shifts = (
    day_shift_names.to_series(name='day_shift', index=day_shift_idx)
    .apply(pulp.LpVariable, cat=pulp.LpBinary)
)

# For each shift and worker across all of their week's work days: are they working?
worker_shift_idx = pd.MultiIndex.from_product((worker_idx, shift_idx))
worker_shift_names = (
    'w' + worker_shift_idx.get_level_values('worker').astype(str) +
    '_s' + worker_shift_idx.get_level_values('shift').astype(str)
)
worker_shifts = (
    worker_shift_names.to_series(name='worker_shift', index=worker_shift_idx)
    .apply(pulp.LpVariable, cat=pulp.LpBinary)
)

problem = pulp.LpProblem(name='Shifts', sense=pulp.LpMinimize)

# Workers may work at most one shift per day
for (worker, day), total in day_shifts.groupby(['worker', 'day']).sum().items():
    problem.addConstraint(
        name=f'dayexcl_w{worker}_d{day}',
        constraint=total <= 1,
    )

# Workers must work exactly five shifts
for worker, total in day_shifts.groupby('worker').sum().items():
    problem.addConstraint(
        name=f'week_w{worker}',
        constraint=total == n_workdays,
    )

# Workers must work exactly one shift type
for worker, total in worker_shifts.groupby('worker').sum().items():
    problem.addConstraint(
        name=f'shiftexcl_w{worker}',
        constraint=total == 1,
    )

# Constrain the day shifts based on shift type
for (worker, shift), total in day_shifts.groupby(['worker', 'shift']).sum().items():
    shift_type = worker_shifts[(worker, shift)]
    problem.addConstraint(
        name=f'shifttype_lo_w{worker}_s{shift}',
        constraint=total >= shift_type,
    )
    problem.addConstraint(
        name=f'shifttype_hi_w{worker}_s{shift}',
        constraint=total <= shift_type*n_days,
    )

# Each shift type must have a minimum number of shifts worked
n_total_shifts = n_workdays * n_workers
min_worker_ratio = {0: 0.3, 8: 0.5, 16: 0.2}
for shift, total in day_shifts.groupby('shift').sum().items():
    lower = n_total_shifts * min_worker_ratio[shift]
    problem.addConstraint(
        name=f'shiftratio_lo_s{shift}',
        constraint=total >= lower,
    )
    problem.addConstraint(
        name=f'shiftratio_hi_s{shift}',
        constraint=total <= lower + 4,
    )

print(problem)
problem.solve()
assert problem.status == pulp.LpStatusOptimal

print('Shift type assignments:')
worker_shifts = worker_shifts.apply(pulp.LpVariable.value).astype(int)
print(worker_shifts.unstack(level='shift'), end='\n\n')

print('Shift day assignments:')
day_shifts = day_shifts.apply(pulp.LpVariable.value).astype(int)
print(day_shifts.unstack(level='day'), end='\n\n')

print('Shift totals:')
shift_totals = day_shifts.groupby('shift').sum()
print(shift_totals, end='\n\n')
Shifts:
MINIMIZE
None
SUBJECT TO
dayexcl_w0_d0: w0_d0_s0 + w0_d0_s16 + w0_d0_s8 <= 1

dayexcl_w0_d1: w0_d1_s0 + w0_d1_s16 + w0_d1_s8 <= 1

dayexcl_w0_d2: w0_d2_s0 + w0_d2_s16 + w0_d2_s8 <= 1
...

week_w0: w0_d0_s0 + w0_d0_s16 + w0_d0_s8 + w0_d1_s0 + w0_d1_s16 + w0_d1_s8
 + w0_d2_s0 + w0_d2_s16 + w0_d2_s8 + w0_d3_s0 + w0_d3_s16 + w0_d3_s8
 + w0_d4_s0 + w0_d4_s16 + w0_d4_s8 + w0_d5_s0 + w0_d5_s16 + w0_d5_s8
 + w0_d6_s0 + w0_d6_s16 + w0_d6_s8 = 5

week_w1: w1_d0_s0 + w1_d0_s16 + w1_d0_s8 + w1_d1_s0 + w1_d1_s16 + w1_d1_s8
 + w1_d2_s0 + w1_d2_s16 + w1_d2_s8 + w1_d3_s0 + w1_d3_s16 + w1_d3_s8
 + w1_d4_s0 + w1_d4_s16 + w1_d4_s8 + w1_d5_s0 + w1_d5_s16 + w1_d5_s8
 + w1_d6_s0 + w1_d6_s16 + w1_d6_s8 = 5

week_w2: w2_d0_s0 + w2_d0_s16 + w2_d0_s8 + w2_d1_s0 + w2_d1_s16 + w2_d1_s8
 + w2_d2_s0 + w2_d2_s16 + w2_d2_s8 + w2_d3_s0 + w2_d3_s16 + w2_d3_s8
 + w2_d4_s0 + w2_d4_s16 + w2_d4_s8 + w2_d5_s0 + w2_d5_s16 + w2_d5_s8
 + w2_d6_s0 + w2_d6_s16 + w2_d6_s8 = 5
...

shiftexcl_w0: w0_s0 + w0_s16 + w0_s8 = 1

shiftexcl_w1: w1_s0 + w1_s16 + w1_s8 = 1

shiftexcl_w2: w2_s0 + w2_s16 + w2_s8 = 1
...

shifttype_lo_w0_s0: w0_d0_s0 + w0_d1_s0 + w0_d2_s0 + w0_d3_s0 + w0_d4_s0
 + w0_d5_s0 + w0_d6_s0 - w0_s0 >= 0

shifttype_hi_w0_s0: w0_d0_s0 + w0_d1_s0 + w0_d2_s0 + w0_d3_s0 + w0_d4_s0
 + w0_d5_s0 + w0_d6_s0 - 7 w0_s0 <= 0

shifttype_lo_w0_s8: w0_d0_s8 + w0_d1_s8 + w0_d2_s8 + w0_d3_s8 + w0_d4_s8
 + w0_d5_s8 + w0_d6_s8 - w0_s8 >= 0

shifttype_hi_w0_s8: w0_d0_s8 + w0_d1_s8 + w0_d2_s8 + w0_d3_s8 + w0_d4_s8
 + w0_d5_s8 + w0_d6_s8 - 7 w0_s8 <= 0
...

shiftratio_lo_s0: w0_d0_s0 + w0_d1_s0 + w0_d2_s0 + w0_d3_s0 + w0_d4_s0
 + w0_d5_s0 + w0_d6_s0 + w10_d0_s0 + w10_d1_s0 + w10_d2_s0 + w10_d3_s0
 + w10_d4_s0 + w10_d5_s0 + w10_d6_s0 + w11_d0_s0 + w11_d1_s0 + w11_d2_s0
 + w11_d3_s0 + w11_d4_s0 + w11_d5_s0 + w11_d6_s0 + w12_d0_s0 + w12_d1_s0
 + w12_d2_s0 + w12_d3_s0 + w12_d4_s0 + w12_d5_s0 + w12_d6_s0 + w13_d0_s0
 + w13_d1_s0 + w13_d2_s0 + w13_d3_s0 + w13_d4_s0 + w13_d5_s0 + w13_d6_s0
 + w14_d0_s0 + w14_d1_s0 + w14_d2_s0 + w14_d3_s0 + w14_d4_s0 + w14_d5_s0
 + w14_d6_s0 + w15_d0_s0 + w15_d1_s0 + w15_d2_s0 + w15_d3_s0 + w15_d4_s0
 + w15_d5_s0 + w15_d6_s0 + w16_d0_s0 + w16_d1_s0 + w16_d2_s0 + w16_d3_s0
 + w16_d4_s0 + w16_d5_s0 + w16_d6_s0 + w17_d0_s0 + w17_d1_s0 + w17_d2_s0
 + w17_d3_s0 + w17_d4_s0 + w17_d5_s0 + w17_d6_s0 + w18_d0_s0 + w18_d1_s0
 + w18_d2_s0 + w18_d3_s0 + w18_d4_s0 + w18_d5_s0 + w18_d6_s0 + w19_d0_s0
 + w19_d1_s0 + w19_d2_s0 + w19_d3_s0 + w19_d4_s0 + w19_d5_s0 + w19_d6_s0
 + w1_d0_s0 + w1_d1_s0 + w1_d2_s0 + w1_d3_s0 + w1_d4_s0 + w1_d5_s0 + w1_d6_s0
 + w2_d0_s0 + w2_d1_s0 + w2_d2_s0 + w2_d3_s0 + w2_d4_s0 + w2_d5_s0 + w2_d6_s0
 + w3_d0_s0 + w3_d1_s0 + w3_d2_s0 + w3_d3_s0 + w3_d4_s0 + w3_d5_s0 + w3_d6_s0
 + w4_d0_s0 + w4_d1_s0 + w4_d2_s0 + w4_d3_s0 + w4_d4_s0 + w4_d5_s0 + w4_d6_s0
 + w5_d0_s0 + w5_d1_s0 + w5_d2_s0 + w5_d3_s0 + w5_d4_s0 + w5_d5_s0 + w5_d6_s0
 + w6_d0_s0 + w6_d1_s0 + w6_d2_s0 + w6_d3_s0 + w6_d4_s0 + w6_d5_s0 + w6_d6_s0
 + w7_d0_s0 + w7_d1_s0 + w7_d2_s0 + w7_d3_s0 + w7_d4_s0 + w7_d5_s0 + w7_d6_s0
 + w8_d0_s0 + w8_d1_s0 + w8_d2_s0 + w8_d3_s0 + w8_d4_s0 + w8_d5_s0 + w8_d6_s0
 + w9_d0_s0 + w9_d1_s0 + w9_d2_s0 + w9_d3_s0 + w9_d4_s0 + w9_d5_s0 + w9_d6_s0
 >= 30

shiftratio_hi_s0: w0_d0_s0 + w0_d1_s0 + w0_d2_s0 + w0_d3_s0 + w0_d4_s0
 + w0_d5_s0 + w0_d6_s0 + w10_d0_s0 + w10_d1_s0 + w10_d2_s0 + w10_d3_s0
 + w10_d4_s0 + w10_d5_s0 + w10_d6_s0 + w11_d0_s0 + w11_d1_s0 + w11_d2_s0
 + w11_d3_s0 + w11_d4_s0 + w11_d5_s0 + w11_d6_s0 + w12_d0_s0 + w12_d1_s0
 + w12_d2_s0 + w12_d3_s0 + w12_d4_s0 + w12_d5_s0 + w12_d6_s0 + w13_d0_s0
 + w13_d1_s0 + w13_d2_s0 + w13_d3_s0 + w13_d4_s0 + w13_d5_s0 + w13_d6_s0
 + w14_d0_s0 + w14_d1_s0 + w14_d2_s0 + w14_d3_s0 + w14_d4_s0 + w14_d5_s0
 + w14_d6_s0 + w15_d0_s0 + w15_d1_s0 + w15_d2_s0 + w15_d3_s0 + w15_d4_s0
 + w15_d5_s0 + w15_d6_s0 + w16_d0_s0 + w16_d1_s0 + w16_d2_s0 + w16_d3_s0
 + w16_d4_s0 + w16_d5_s0 + w16_d6_s0 + w17_d0_s0 + w17_d1_s0 + w17_d2_s0
 + w17_d3_s0 + w17_d4_s0 + w17_d5_s0 + w17_d6_s0 + w18_d0_s0 + w18_d1_s0
 + w18_d2_s0 + w18_d3_s0 + w18_d4_s0 + w18_d5_s0 + w18_d6_s0 + w19_d0_s0
 + w19_d1_s0 + w19_d2_s0 + w19_d3_s0 + w19_d4_s0 + w19_d5_s0 + w19_d6_s0
 + w1_d0_s0 + w1_d1_s0 + w1_d2_s0 + w1_d3_s0 + w1_d4_s0 + w1_d5_s0 + w1_d6_s0
 + w2_d0_s0 + w2_d1_s0 + w2_d2_s0 + w2_d3_s0 + w2_d4_s0 + w2_d5_s0 + w2_d6_s0
 + w3_d0_s0 + w3_d1_s0 + w3_d2_s0 + w3_d3_s0 + w3_d4_s0 + w3_d5_s0 + w3_d6_s0
 + w4_d0_s0 + w4_d1_s0 + w4_d2_s0 + w4_d3_s0 + w4_d4_s0 + w4_d5_s0 + w4_d6_s0
 + w5_d0_s0 + w5_d1_s0 + w5_d2_s0 + w5_d3_s0 + w5_d4_s0 + w5_d5_s0 + w5_d6_s0
 + w6_d0_s0 + w6_d1_s0 + w6_d2_s0 + w6_d3_s0 + w6_d4_s0 + w6_d5_s0 + w6_d6_s0
 + w7_d0_s0 + w7_d1_s0 + w7_d2_s0 + w7_d3_s0 + w7_d4_s0 + w7_d5_s0 + w7_d6_s0
 + w8_d0_s0 + w8_d1_s0 + w8_d2_s0 + w8_d3_s0 + w8_d4_s0 + w8_d5_s0 + w8_d6_s0
 + w9_d0_s0 + w9_d1_s0 + w9_d2_s0 + w9_d3_s0 + w9_d4_s0 + w9_d5_s0 + w9_d6_s0
 <= 34
...

VARIABLES
0 <= w0_d0_s0 <= 1 Integer
0 <= w0_d0_s16 <= 1 Integer
0 <= w0_d0_s8 <= 1 Integer
0 <= w0_d1_s0 <= 1 Integer
0 <= w0_d1_s16 <= 1 Integer
...

0 <= w1_s0 <= 1 Integer
0 <= w1_s16 <= 1 Integer
0 <= w1_s8 <= 1 Integer
0 <= w2_d0_s0 <= 1 Integer
0 <= w2_d0_s16 <= 1 Integer
0 <= w2_d0_s8 <= 1 Integer
...

Welcome to the CBC MILP Solver 
Version: 2.10.3 
Build Date: Dec 15 2019 

At line 2 NAME          MODEL
At line 3 ROWS
At line 311 COLUMNS
At line 3973 RHS
At line 4280 BOUNDS
At line 4762 ENDATA
Problem MODEL has 306 rows, 481 columns and 2700 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Continuous objective value is 0 - 0.00 seconds
...

Result - Optimal solution found

Objective value:                0.00000000
Enumerated nodes:               0
Total iterations:               714
Time (CPU seconds):             0.26
Time (Wallclock seconds):       0.26

Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.27   (Wallclock seconds):       0.27

Shift type assignments:
shift   0   8   16
worker            
0        0   1   0
1        0   0   1
2        0   1   0
3        1   0   0
4        1   0   0
5        0   1   0
6        0   1   0
7        0   1   0
8        0   1   0
9        0   0   1
10       0   1   0
11       0   0   1
12       0   1   0
13       0   1   0
14       1   0   0
15       1   0   0
16       1   0   0
17       0   0   1
18       0   1   0
19       1   0   0

Shift day assignments:
day           0  1  2  3  4  5  6
worker shift                     
0      0      0  0  0  0  0  0  0
       8      1  1  1  0  0  1  1
       16     0  0  0  0  0  0  0
1      0      0  0  0  0  0  0  0
       8      0  0  0  0  0  0  0
       16     1  1  0  1  1  1  0
2      0      0  0  0  0  0  0  0
       8      0  1  1  1  1  0  1
       16     0  0  0  0  0  0  0
3      0      1  1  0  1  1  1  0
       8      0  0  0  0  0  0  0
       16     0  0  0  0  0  0  0
4      0      1  0  1  1  1  1  0
       8      0  0  0  0  0  0  0
       16     0  0  0  0  0  0  0
5      0      0  0  0  0  0  0  0
       8      1  1  1  0  1  0  1
       16     0  0  0  0  0  0  0
6      0      0  0  0  0  0  0  0
       8      0  1  1  1  1  0  1
       16     0  0  0  0  0  0  0
7      0      0  0  0  0  0  0  0
       8      1  1  1  1  0  0  1
       16     0  0  0  0  0  0  0
8      0      0  0  0  0  0  0  0
       8      0  1  1  1  1  1  0
       16     0  0  0  0  0  0  0
9      0      0  0  0  0  0  0  0
       8      0  0  0  0  0  0  0
       16     1  1  1  1  0  0  1
10     0      0  0  0  0  0  0  0
       8      1  1  1  0  0  1  1
       16     0  0  0  0  0  0  0
11     0      0  0  0  0  0  0  0
       8      0  0  0  0  0  0  0
       16     1  1  1  0  1  1  0
12     0      0  0  0  0  0  0  0
       8      1  0  1  1  1  0  1
       16     0  0  0  0  0  0  0
13     0      0  0  0  0  0  0  0
       8      0  1  1  1  1  1  0
       16     0  0  0  0  0  0  0
14     0      1  0  1  0  1  1  1
       8      0  0  0  0  0  0  0
       16     0  0  0  0  0  0  0
15     0      1  0  1  1  1  0  1
       8      0  0  0  0  0  0  0
       16     0  0  0  0  0  0  0
16     0      1  1  1  0  1  1  0
       8      0  0  0  0  0  0  0
       16     0  0  0  0  0  0  0
17     0      0  0  0  0  0  0  0
       8      0  0  0  0  0  0  0
       16     1  1  1  1  0  0  1
18     0      0  0  0  0  0  0  0
       8      1  0  1  1  1  1  0
       16     0  0  0  0  0  0  0
19     0      0  1  1  0  1  1  1
       8      0  0  0  0  0  0  0
       16     0  0  0  0  0  0  0

Shift totals:
shift
0     30
8     50
16    20
Name: day_shift, dtype: int32
© www.soinside.com 2019 - 2024. All rights reserved.