Линейное программирование Python Pulp с динамическим ограничением

В настоящее время я использую Solver в Excel, чтобы найти оптимальное решение для производства. Вот текущая настройка:  введите описание изображения здесь

Это касается изготовления обуви на роторном станке, то есть производство осуществляется партиями и повторами. Например, одна партия будет «10x A1» (см. A1 в таблице), что даст 10x размер 36, 20x размер 37 ... 10x размер 41.

Есть несколько настроек с префиксом; A1, A2; R7 ... как вы видите в таблице выше.

Затем есть переменная requested (или, скорее, список переменных), которая в основном говорит о том, что заказчик запрашивал, количество на размер.

целевая функция состоит в том, чтобы найти набор повторений, который максимально соответствует запрошенным количествам. Следовательно, в решателе (извините за неанглийский снимок экрана) вы можете видеть, что цель равна N21 (это сумма абсолютных различий по размеру). Переменные равны N2:N9 - это количество повторов для каждой настройки, и единственное ограничение состоит в том, что N2:N9 является целым числом.

Как я могу смоделировать такое поведение с помощью Python? Мой старт:

from collections import namedtuple

from pulp import *


class Setup(namedtuple('IAmReallyLazy', 'name ' + ' '.join(f's{s}' for s in range(36, 47)))):
    # inits with name and sizes 's36', 's37'... 's46'
    repetitions = 0


setups = [
    Setup('A1', 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0),
    Setup('A2', 0, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0),
    Setup('R7', 0, 0, 1, 1, 1, 1, 2, 0, 0, 0, 0),
    Setup('D1', 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
    # and others
]

setup_names = [s.name for s in setups]

requested = {
    's36': 100,
    's37': 250,
    's38': 300,
    's39': 450,
    's40': 450,
    's41': 250,
    's42': 200,
}


def get_quantity_per_size(size: str) -> int:
    return sum([getattr(setup, size) * setup.repetitions for setup in setups])


def get_abs_diff(size: str) -> int:
    requested_size = requested.get(size, 0)
    return abs(get_quantity_per_size(size) - requested_size)


problem = LpProblem('Optimize Batches', LpMinimize)
# goal is to minimise the sum(get_abs_diff(f's{size}') for size in range(36, 47))
# variables are [setup.repetitions for setup in setups]
# constraints are all([isinstance(setup.repetitions, int) for setup in setups])

В идеальном мире, если существует более одного оптимального решения, следует выбрать то, у которого наибольший разброс abs diff (то есть тот, у которого наименьшая наибольшая разница). То есть, если одно решение имеет разницу абс 10 для каждого размера и 10 размеров (всего 100), а другое - 20 + 80 = 100, первое решение более оптимально для клиента.

Другое ограничение должно быть min(setup.repetitions for setup in setups if setup.repetitions > 0) > 9, в основном ограничение повторений должно быть:

  • Целое число
  • Либо 0 , либо больше 9 - это невозможно в линейном программировании из того, что я понял до сих пор.

person emihir0    schedule 26.04.2019    source источник


Ответы (1)


Здесь есть кое-что. Во-первых, если вы используете abs(), проблема будет нелинейной. Вместо этого вам следует ввести новые переменные, называемые, скажем, over_mfg и under_mfg, которые представляют количество произведенных единиц выше цели и число ниже цели соответственно. Вы можете объявить это так:

over_mfg = LpVariable.dicts("over_mfg", sizes, 0, None, LpInteger)
under_mfg = LpVariable.dicts("under_mfg", sizes, 0, None, LpInteger)

Я объявил список под названием sizes, который используется в определениях выше:

min_size = 36
max_size = 46
sizes = ['s' + str(s) for s in range(min_size, max_size+1)]

Вам также потребуются переменные, указывающие на повторение каждой настройки:

repetitions = LpVariable.dicts("repetitions", setup_names, 0, None, LpInteger)

Ваша целевая функция затем объявляется как:

problem += lpSum([over_mfg[size] + under_mfg[size] for size in sizes])

(Обратите внимание, что в pulp вы используете lpSum, а не sum.) Теперь вам нужны ограничения, которые говорят, что over_mfg - это избыток, а under_mfg - недостаток:

for size in sizes:
    problem += over_mfg[size] >= lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]) - requested[size], "DefineOverMfg" + size
    problem += under_mfg[size] >= requested[size] - lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]), "DefineUnderMfg" + size

Также обратите внимание, что я не использовал ваши get_quantity_per_size() и get_abs_diff() функции. Это также запутает pulp, поскольку он не поймет, что это простые линейные функции.

Вот мой полный код:

from collections import namedtuple

from pulp import *


class Setup(namedtuple('IAmReallyLazy', 'name ' + ' '.join(f's{s}' for s in range(36, 47)))):
    # inits with name and sizes 's36', 's37'... 's46'
    repetitions = 0


setups = [
    Setup('A1', 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0),
    Setup('A2', 0, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0),
    Setup('R7', 0, 0, 1, 1, 1, 1, 2, 0, 0, 0, 0),
    Setup('D1', 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
    # and others
]

setup_names = [s.name for s in setups]

min_size = 36
max_size = 46
sizes = ['s' + str(s) for s in range(min_size, max_size+1)]

requested = {
    's36': 100,
    's37': 250,
    's38': 300,
    's39': 450,
    's40': 450,
    's41': 250,
    's42': 200,
    's43': 0,    # I added these for completeness
    's44': 0,
    's45': 0,
    's46': 0
}

problem = LpProblem('Optimize Batches', LpMinimize)
# goal is to minimise the sum(get_abs_diff(f's{size}') for size in range(36, 47))
# variables are [setup.repetitions for setup in setups]
# constraints are all([isinstance(setup.repetitions, int) for setup in setups])

repetitions = LpVariable.dicts("repetitions", setup_names, 0, None, LpInteger)
over_mfg = LpVariable.dicts("over_mfg", sizes, 0, None, LpInteger)
under_mfg = LpVariable.dicts("under_mfg", sizes, 0, None, LpInteger)

problem += lpSum([over_mfg[size] + under_mfg[size] for size in sizes])

for size in sizes:
    problem += over_mfg[size] >= lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]) - requested[size], "DefineOverMfg" + size
    problem += under_mfg[size] >= requested[size] - lpSum([repetitions[setup.name] * getattr(setup, size) for setup in setups]), "DefineUnderMfg" + size

# Solve problem
problem.solve()

# Print status
print("Status:", LpStatus[problem.status])

# Print optimal values of decision variables
for v in problem.variables():
    if v.varValue is not None and v.varValue > 0:
        print(v.name, "=", v.varValue)

Вот результат:

Status: Optimal
over_mfg_s38 = 62.0
over_mfg_s41 = 62.0
repetitions_A1 = 25.0
repetitions_A2 = 88.0
repetitions_D1 = 110.0
repetitions_R7 = 1.0
under_mfg_s36 = 75.0
under_mfg_s37 = 2.0
under_mfg_s40 = 25.0

Итак, мы делаем 25 повторов A1, 88 из A2, 110 из D1 и 1 из R7. Это дает 25 единиц s36 (следовательно, 75 единиц меньше целевого значения 100); 248 единиц s37 (2 ниже цели); 362 единицы s38 (62 единицы больше целевого показателя в 300); и так далее.


Теперь для вашего ограничения, которое говорит, что вы либо должны создать 0 из набора или> 9, вы можете ввести новые двоичные переменные, указывающие, создается ли каждая настройка:

is_produced = LpVariable.dicts("is_produced", setup_names, 0, 1, LpInteger)

Затем добавьте эти ограничения:

M = 1000
min_reps = 9
for s in setup_names:
    problem += M * is_produced[s] >= repetitions[s] # if it's produced at all, must set is_produced = 1
    problem += min_reps * (1 - is_produced[s]) + repetitions[s] >= min_reps

M - большое число; оно должно быть больше максимально возможного количества повторений, но не больше. И я определил min_reps, чтобы избежать "жесткого кодирования" 9 в ограничениях. Итак, эти ограничения говорят, что (1) если repetitions[s] > 0, то is_produced[s] должно быть равно 1, и (2) либо is_produced[s] = 1 , либо repetitions[s]> 9.

Выход:

Status: Optimal
is_produced_A1 = 1.0
is_produced_A2 = 1.0
is_produced_D1 = 1.0
over_mfg_s38 = 63.0
over_mfg_s39 = 1.0
over_mfg_s41 = 63.0
repetitions_A1 = 25.0
repetitions_A2 = 88.0
repetitions_D1 = 112.0
under_mfg_s36 = 75.0
under_mfg_s40 = 24.0

Обратите внимание, что теперь у нас нет настроек с> 0, но ‹9 повторений.


В идеальном мире, если существует более одного оптимального решения, следует выбрать решение с наибольшим разбросом abs diff (т. Е. То, которое имеет наименьшую наибольшую разницу).

Это сложнее, и (по крайней мере, на данный момент) я оставлю это идеальному миру или ответу другого человека.


Кстати: есть попытка запустить сайт Stack Exchange для Исследование операций, где мы будем отвечать на подобные вопросы. Если вам интересно, я рекомендую вам щелкнуть ссылку и «зафиксировать».

person LarrySnyder610    schedule 26.04.2019
comment
Спасибо @ LarrySnyder610 - я сделал на его основе небольшое приложение, см. Здесь: youtu.be/0qXo1QCPkvc - прекрасно работает. - person emihir0; 28.04.2019
comment
Еще одна вещь, если можно - как бы вы ограничили максимальное количество разрешенных настроек? То есть я хочу использовать, например, максимум 3 различных настройки. Я предполагаю, что is_produced_<name> можно использовать повторно, но как будет выглядеть ограничение? - person emihir0; 28.04.2019
comment
неважно, понял, problem += lpSum([is_produced[s] for s in setup_names]) <= max_setups =) - person emihir0; 28.04.2019