#pyomo
Вопрос:
В задаче распределения задач давайте предположим:
- нам нужно выделить 4 задачи на 4 дня.
- Есть только один работник, и он должен работать не более 8 часов в день.
- Мы хотим минимизировать общее рабочее время.
- Мы можем объединить задачи и сэкономить время за один день. Необходимое количество часов равно
0 задач —> 0 часов
1 задача -> 6 часов
2 задачи -> 8 часов
Иллюстрация для # задач V.S. # необходимых часов:
Синяя пунктирная линия: y = 6 * n
Черная строка: фактическое время = min(6 * n, 6 2 * (n — 1))
Красная строка: дневной лимит
Эта вычислительная функция (для черной линии) может быть определена в Python как
def compute_hours(n):
return min(6*n, 6 2 * (n - 1))
Используя функцию, определенную выше в настройке ограничения, pyomo
выдавайте ошибки, когда я помещаю это в свой список ограничений.
> File "<ipython-input-69-57dbda13a71f>", line 23, in compute_hours
> return min(6*n, 6 2 * (n - 1)) File "pyomocoreexprlogical_expr.pyx", line 183, in
> pyomo.core.expr.logical_expr.InequalityExpression.__bool__
> pyomo.common.errors.PyomoException: Cannot convert non-constant
> expression to bool. This error is usually caused by using an
> expression in a boolean context such as an if statement. For example,
> m.x = Var()
> if m.x <= 0:
> ... would cause this exception.
Ошибку также легко воспроизвести, используя следующий пример кода:
# Allocating tasks to days
from pyomo.environ import *
import pandas as pd
m = ConcreteModel()
# We have 4 tasks in total. Each cost 6 hours
m.TASKS = ('task1', 'task2', 'task3', 'task4')
# There are 4 days to allocate to
m.DAYS = (1, 2, 3, 4)
# Hours per task
HOURS_PER_TASK = 6
def compute_hours(n):
return min(6*n, 6 2 * (n - 1))
# for x in range(0, 4):
# print(x, compute_hours(x))
# Create a decision array for the allocation
m.ALLOCATION = Var(m.TASKS, m.DAYS, domain=Binary)
# Create an array for total hours computation
m.TOTAL_HOURS = Var(m.DAYS, domain=Integers)
# minimize the number of days that are allocated tasks
m.OBJ = Objective(expr=sum([m.TOTAL_HOURS[day] for day in m.DAYS]), sense=minimize)
m.c = ConstraintList()
# One task is done once only in all days
for task in m.TASKS:
m.c.add(sum([m.ALLOCATION[task, day] for day in m.DAYS]) == 1)
# Compute Total hours a day
for day in m.DAYS:
m.c.add(sum([m.ALLOCATION[task, day] for task in m.TASKS])*HOURS_PER_TASK == m.TOTAL_HOURS[day])
# The following computation does not work
#m.c.add(compute_hours(sum([m.ALLOCATION[task, day] for task in m.TASKS])*HOURS_PER_TASK) == m.TOTAL_HOURS[day])
# Add max working hours per day
for day in m.DAYS:
m.c.add(m.TOTAL_HOURS[day] <= 8)
SolverFactory('glpk').solve(m).write()
# Show the results
SCHEDULE_df = pd.DataFrame(index = m.TASKS, columns= m.DAYS)
SCHEDULE_HOURS_df = pd.DataFrame(index = m.TASKS, columns= m.DAYS)
for i in m.TASKS:
for j in m.DAYS:
SCHEDULE_df.loc[i,j] = m.ALLOCATION[i,j]()
print('------------------------------------------')
print('ALLOCATION:')
print(SCHEDULE_df)
print('------------------------------------------')
print('Total hours per day:')
print(m.TOTAL_HOURS.get_values())
print('Total hours:')
print(value(m.OBJ))
Этот пример все еще может дать результаты, если мы предположим, что каждая задача всегда занимает 6 часов.
------------------------------------------
ALLOCATION:
1 2 3 4
task1 1 0 0 0
task2 0 1 0 0
task3 0 0 1 0
task4 0 0 0 1
------------------------------------------
Total hours per day:
{1: 6.0, 2: 6.0, 3: 6.0, 4: 6.0}
Total hours:
24.0
Если мы сможем реализовать объединение, то этому работнику нужно будет работать только два дня с рабочей нагрузкой 8 часов в день (вместо работы 6 часов в день в течение 4 дней).
Ответ №1:
Джон.
Pyomo
получил кусочно-линейное выражение для этих случаев. ( kernel
слой также получил библиотеку кусочных функций)
В вашем случае, на самом деле, легко использовать Piecewise
in Pyomo
.
Вам нужно создать вспомогательную переменную с тем же индексом, что m.TOTAL_HOURS
и (запрошенный pyomo
), назовем его m.TASK_PER_DAY
, который будет учитывать количество задач, назначенных на каждый день. Затем этот TASK_PER_DAY
параметр будет вычисляться (ограничен) с m.ALLOCATION
использованием суммирования задачи каждый день (аналогично тому, как вы пытаетесь ограничить). Затем вы можете использовать pyomo
Piecewise
для вычисления в TOTAL_HOURS
зависимости от TASK_PER_DAY
Редактировать: Дополнительные пояснения к pw_pts
и f_rule
, которые являются источником ошибок в моделировании Piecewise
. pw_pts
являются ли точки останова (независимые значения, домен) в Piecewise
. f_rule
является известным значением в заданных точках pw_pts
. Это может быть функция или пара точек. Если вы хотите линейно интерполировать точки (как ваше изображение), вам просто нужно определить такие точки pw_pts=[0,1,2,3,...]
и f_rule=[0,6,8,10,...]
, но для постоянных кусочных функций вам нужно указать одну и ту же точку дважды, чтобы гарантировать, что для точек домена кусочно возвращает то же значение, что и в пошаговой функции. В вашем моделировании, поскольку переменные есть integers
, проблем нет, но лучше устранить проблему.
m.TASK_PER_DAY = Var(m.DAYS, domain=Integers)
m.PIECEWISE_DAILY_HOURS = Piecewise(
m.DAYS,
m.TOTAL_HOURS,
m.TASK_PER_DAY,
pw_pts=[0,1,2,3],
f_rule=[0,6,8,10],
pw_repn='SOS2',
pw_constr_type='EQ',
unbounded_domain_var=True
)
#Constraining TASK_PER_DAY
def Assign_Task(model, day):
return model.TASK_PER_DAY[day] == sum(model.ALLOCATION[task,day] for task in model.TASKS)
m.assign_task = Constraint(m.DAYS, rule=Assign_Task)
При таком подходе вы не ограничиваете день для выполнения задачи, поэтому назначенный день может быть любой парой дней, но каждый день, безусловно, получает по 2 задачи в день. Если вы хотите ограничить модель для выполнения задачи как можно скорее (в первые дни), вам нужно добавить это новое ограничение.
Следующий код может воспроизвести желаемый результат:
import pyomo.environ as pyo
m = pyo.ConcreteModel()
#Sets
# We have 4 tasks in total. Each cost 6 hours
m.TASKS = ('task1', 'task2', 'task3', 'task4')
# There are 4 days to allocate to
m.DAYS = (1, 2, 3, 4)
# Hours per task
HOURS_PER_TASK = 6
#Variables
# Create a decision array for the allocation
m.ALLOCATION = pyo.Var(m.TASKS, m.DAYS, domain=Binary)
m.TOTAL_HOURS = pyo.Var(m.DAYS, domain=Integers)
m.TASK_PER_DAY = pyo.Var(m.DAYS, domain=Integers)
#Piecewise
m.PIECEWISE_DAILY_HOURS = pyo.Piecewise(
m.DAYS,
m.TOTAL_HOURS,
m.TASK_PER_DAY,
pw_pts=[0,0,1,1,2,2,3,3],
f_rule=[0,0,6,6,8,8,10,10],
pw_repn='SOS2',
pw_constr_type='EQ',
unbounded_domain_var=True
)
#Objective
# minimize the number of days that are allocated tasks
m.OBJ = pyo.Objective(expr=sum([m.TOTAL_HOURS[day] for day in m.DAYS]), sense=minimize)
m.c = pyo.ConstraintList()
# One task is done once only in all days
for task in m.TASKS:
m.c.add(sum([m.ALLOCATION[task, day] for day in m.DAYS]) == 1)
#Compute the number of tasks per day
def Assign_Task(model, day):
return model.TASK_PER_DAY[day] == sum(model.ALLOCATION[task,day] for task in model.TASKS)
m.assign_task = pyo.Constraint(m.DAYS, rule=Assign_Task)
# Add max working hours per day
for day in m.DAYS:
m.c.add(m.TOTAL_HOURS[day] <= 8)
pyo.SolverFactory('gurobi').solve(m, tee=True)
Затем время настаивания раствора сокращают до 16 часов.
>>>m.OBJ()
16.0
>>>m.TOTAL_HOURS.display()
TOTAL_HOURS : Size=4, Index=TOTAL_HOURS_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
1 : None : -0.0 : None : False : False : Integers
2 : None : 8.0 : None : False : False : Integers
3 : None : -0.0 : None : False : False : Integers
4 : None : 8.0 : None : False : False : Integers
>>>m.TASK_PER_DAY.display()
TASK_PER_DAY : Size=4, Index=TASK_PER_DAY_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
1 : None : 0.0 : None : False : False : Integers
2 : None : 2.0 : None : False : False : Integers
3 : None : 0.0 : None : False : False : Integers
4 : None : 2.0 : None : False : False : Integers
>>>m.ALLOCATION.display()
ALLOCATION : Size=16, Index=ALLOCATION_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
('task1', 1) : 0 : -0.0 : 1 : False : False : Binary
('task1', 2) : 0 : 1.0 : 1 : False : False : Binary
('task1', 3) : 0 : -0.0 : 1 : False : False : Binary
('task1', 4) : 0 : -0.0 : 1 : False : False : Binary
('task2', 1) : 0 : -0.0 : 1 : False : False : Binary
('task2', 2) : 0 : -0.0 : 1 : False : False : Binary
('task2', 3) : 0 : -0.0 : 1 : False : False : Binary
('task2', 4) : 0 : 1.0 : 1 : False : False : Binary
('task3', 1) : 0 : -0.0 : 1 : False : False : Binary
('task3', 2) : 0 : 1.0 : 1 : False : False : Binary
('task3', 3) : 0 : -0.0 : 1 : False : False : Binary
('task3', 4) : 0 : -0.0 : 1 : False : False : Binary
('task4', 1) : 0 : -0.0 : 1 : False : False : Binary
('task4', 2) : 0 : -0.0 : 1 : False : False : Binary
('task4', 3) : 0 : -0.0 : 1 : False : False : Binary
('task4', 4) : 0 : 1.0 : 1 : False : False : Binary
Комментарии:
1. Большое вам спасибо 🙂 Кстати, знаете ли вы, что какой-либо решатель с открытым исходным кодом также может поддерживать ограничения уровня SOS 2? Я тестировал
glpk
, и это не работает.2. Нет, извините. Но если вы измените
Piecewise
аргумент наpw_repn='DCC'
(проверьте другие параметры в документации Pyomo), вы можете использоватьglpk
3. Да! Это работает! Спасибо.