Перейти к основному содержимому

Планирование производственных операций

В данном разделе рассматривается классическая задача составления оптимального расписания выполнения производственных работ на станках (Job Shop Scheduling Problem). Эта задача широко встречается в производственном планировании, где необходимо эффективно распределить ресурсы и минимизировать общее время производства.

Каждая производственная работа представляет собой последовательность технологических операций, которые должны выполняться в строго определённом порядке. Для выполнения каждой операции требуется конкретный станок и известное время обработки. Например, изготовление детали может включать последовательные этапы: токарная обработка на станке 1, фрезеровка на станке 2, шлифовка на станке 3.

Ключевая сложность задачи заключается в том, что станки являются общими ресурсами — на одном станке могут выполняться операции из разных работ, но не одновременно. Необходимо так распределить операции по времени, чтобы:

  • Соблюдалась технологическая последовательность операций внутри каждой работы
  • На каждом станке операции не пересекались во времени
  • Общее время завершения всех работ было минимальным

Подобные задачи возникают в машиностроении, металлообработке, производстве электроники и других отраслях, где требуется координация использования ограниченных производственных мощностей.

Постановка задачи

Имеется три работы, каждая из которых состоит из нескольких операций. Для каждой операции известен станок, на котором она выполняется, и её длительность:

  • Работа 0: Операция 0 (станок 0, 3 ед. времени) → Операция 1 (станок 1, 2 ед.) → Операция 2 (станок 2, 2 ед.);
  • Работа 1: Операция 0 (станок 0, 2 ед. времени) → Операция 1 (станок 2, 1 ед.) → Операция 2 (станок 1, 4 ед.);
  • Работа 2: Операция 0 (станок 1, 4 ед. времени) → Операция 1 (станок 2, 3 ед.);
  • Операции в рамках одной работы должны выполняться последовательно в заданном порядке;
  • На одном станке операции не могут выполняться одновременно;
  • Требуется минимизировать общее время завершения всех работ (makespan).

Решение задачи

1. Выбор модели

Для решения задачи планирования производства (Job Shop Scheduling Problem) используется модель ограничений. Эта модель естественным образом работает с интервалами времени и логическими ограничениями на последовательность и непересечение операций. Модель ограничений позволяет описать технологические зависимости и найти оптимальное расписание.

2. Создание модели

Создадим объект модели, в котором будем описывать переменные, ограничения и целевую функцию.

// Создание модели ограничений
Модель = О2
.Модели()
.МодельОграничений()
.СоздатьМодель();

3. Ввод данных

Описываются все работы и их операции. Для каждой операции задаётся структура с указанием номера станка и длительности выполнения.

// Входные данные по работам и операциям
ДанныеРабот = Новый Массив;

// Работа 0
Работа0 = Новый Массив;
Работа0.Добавить(Новый Структура("Станок, Длительность", 0, 3)); // Операция 0
Работа0.Добавить(Новый Структура("Станок, Длительность", 1, 2)); // Операция 1
Работа0.Добавить(Новый Структура("Станок, Длительность", 2, 2)); // Операция 2
ДанныеРабот.Добавить(Работа0);

// Работа 1
Работа1 = Новый Массив;
Работа1.Добавить(Новый Структура("Станок, Длительность", 0, 2));
Работа1.Добавить(Новый Структура("Станок, Длительность", 2, 1));
Работа1.Добавить(Новый Структура("Станок, Длительность", 1, 4));
ДанныеРабот.Добавить(Работа1);

// Работа 2
Работа2 = Новый Массив;
Работа2.Добавить(Новый Структура("Станок, Длительность", 1, 4));
Работа2.Добавить(Новый Структура("Станок, Длительность", 2, 3));
ДанныеРабот.Добавить(Работа2);

Также определяется количество станков и вычисляется горизонт планирования — верхняя граница времени, равная сумме длительностей всех операций:

// Определяем количество станков
НомерПоследнегоСтанка = 0;
Для Каждого Работа Из ДанныеРабот Цикл
Для Каждого Операция Из Работа Цикл
Если Операция.Станок > НомерПоследнегоСтанка Тогда
НомерПоследнегоСтанка = Операция.Станок;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КоличествоСтанков = НомерПоследнегоСтанка + 1;

ВсеСтанки = Новый Массив;
Для К = 0 По КоличествоСтанков - 1 Цикл
ВсеСтанки.Добавить(К);
КонецЦикла;

// Вычисляем горизонт планирования
ГоризонтПланирования = 0;
Для Каждого Работа Из ДанныеРабот Цикл
Для Каждого Операция Из Работа Цикл
ГоризонтПланирования = ГоризонтПланирования + Операция.Длительность;
КонецЦикла;
КонецЦикла;

4. Регистрация переменных

Для каждой операции создаются переменные времени начала и окончания, а также формируется интервал — специальная конструкция модели ограничений, связывающая начало, длительность и окончание операции. Интервалы группируются по станкам для последующего наложения ограничений на непересечение.

// Регистрируем переменные
ВсеЗадачи = Новый Массив(ДанныеРабот.Количество());
ИнтервалыПоСтанкам = Новый Соответствие;

Для Каждого Станок Из ВсеСтанки Цикл
ИнтервалыПоСтанкам.Вставить(Станок, Новый Массив);
КонецЦикла;

Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];
ВсеЗадачи[НомерРаботы] = Новый Массив(Работа.Количество());

Для НомерОперации = 0 По Работа.Количество() - 1 Цикл
Операция = Работа[НомерОперации];
Станок = Операция.Станок;
ДлительностьОперации = Операция.Длительность;

Начало = Модель.ПеременнаяДиапазона(0, ГоризонтПланирования);
Конец = Модель.ПеременнаяДиапазона(0, ГоризонтПланирования);
Интервал = Модель.Ограничения().Интервал(Начало, ДлительностьОперации, Конец);

ВсеЗадачи[НомерРаботы][НомерОперации] = Новый Структура("Начало,Конец,Интервал", Начало, Конец, Интервал);

МассивИнтервалов = ИнтервалыПоСтанкам.Получить(Станок);
МассивИнтервалов.Добавить(Интервал);
КонецЦикла;
КонецЦикла;

5. Описание ограничений

Запрет перекрытий на станках. Операции, выполняемые на одном станке, не могут пересекаться во времени. Используется специальное ограничение ЗапретПерекрытий, которое автоматически обеспечивает непересечение всех интервалов.

// Ограничение: операции на одном станке не пересекаются
Для Каждого Станок Из ВсеСтанки Цикл
ИнтервалыНаСтанке = ИнтервалыПоСтанкам.Получить(Станок);
Модель.Ограничения().ЗапретПерекрытий(ИнтервалыНаСтанке);
КонецЦикла;

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

// Ограничение: операции в рамках одной работы выполняются последовательно
Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];

Для НомерОперации = 0 По Работа.Количество() - 2 Цикл
Текущая = ВсеЗадачи[НомерРаботы][НомерОперации];
Следующая = ВсеЗадачи[НомерРаботы][НомерОперации + 1];

Модель.Ограничения().ЗначениеМеньшеИлиРавно(Текущая.Конец, Следующая.Начало);
КонецЦикла;
КонецЦикла;

6. Описание целевой функции

Целевая функция — минимизация времени завершения всех работ (makespan). Вводится переменная МаксимальноеВремя, которая должна быть не меньше времени окончания любой работы. Минимизируя эту переменную, решатель находит наиболее компактное расписание.

// Целевая функция: минимизируем время завершения всех работ
МаксимальноеВремя = Модель.ПеременнаяДиапазона(0, ГоризонтПланирования, "МаксимальноеВремя");

Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];
НомерПоследнейОперации = Работа.Количество() - 1;

ДанныеПоследнейОперации = ВсеЗадачи[НомерРаботы][НомерПоследнейОперации];
КонецПоследнейОперации = ДанныеПоследнейОперации.Конец;

Модель.Ограничения().ЗначениеБольшеИлиРавно(МаксимальноеВремя, КонецПоследнейОперации);
КонецЦикла;

Модель.Минимизировать(МаксимальноеВремя);

7. Решение модели

Запускаем процесс оптимизации методом Решить.

// Решение модели
Решение = Модель.Решить();

8. Вывод результатов

Результаты извлекаются из решения, сортируются по станкам и времени начала, затем выводится детальное расписание с указанием времени начала и окончания каждой операции на каждом станке.

// Вывод результатов
Если Решение.РешениеНайдено() Тогда
НазначенныеЗадачи = Новый Соответствие;
Для Каждого Станок Из ВсеСтанки Цикл
НазначенныеЗадачи.Вставить(Станок, Новый Массив);
КонецЦикла;

Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];

Для НомерОперации = 0 По Работа.Количество() - 1 Цикл
Операция = Работа[НомерОперации];
Станок = Операция.Станок;
ДлительностьОперации = Операция.Длительность;

ДанныеОперации = ВсеЗадачи[НомерРаботы][НомерОперации];
Начало = Решение.ЗначениеПеременной(ДанныеОперации.Начало);

Задача = Новый Структура(
"Начало, Работа, Операция, Длительность",
Начало, НомерРаботы, НомерОперации, ДлительностьОперации);

МассивЗадач = НазначенныеЗадачи.Получить(Станок);
МассивЗадач.Добавить(Задача);
КонецЦикла;
КонецЦикла;

// Сортировка задач по времени начала
Для Каждого Станок Из ВсеСтанки Цикл
МассивЗадач = НазначенныеЗадачи.Получить(Станок);

Для i = 0 По МассивЗадач.Количество() - 2 Цикл
Для j = i + 1 По МассивЗадач.Количество() - 1 Цикл
Если МассивЗадач[i].Начало > МассивЗадач[j].Начало Тогда
ВременнаяЗадача = МассивЗадач[i];
МассивЗадач[i] = МассивЗадач[j];
МассивЗадач[j] = ВременнаяЗадача;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецЦикла;

ОбщееВремя = Решение.ЗначениеПеременной(МаксимальноеВремя);

Сообщение = "Оптимальное расписание работ" + Символы.ПС + Символы.ПС;
Сообщение = Сообщение + "Общее время выполнения: " + ОбщееВремя + Символы.ПС + Символы.ПС;

Для Каждого Станок Из ВсеСтанки Цикл
МассивЗадач = НазначенныеЗадачи.Получить(Станок);

Сообщение = Сообщение + "Станок " + Станок + ":" + Символы.ПС;

Для Каждого Задача Из МассивЗадач Цикл
ВремяНачала = Задача.Начало;
ВремяОкончания = ВремяНачала + Задача.Длительность;

Сообщение = Сообщение + СтрШаблон(
" Работа %1, Операция %2: начало %3, окончание %4 (длительность: %5)%6",
Задача.Работа,
Задача.Операция,
ВремяНачала,
ВремяОкончания,
Задача.Длительность,
Символы.ПС
);
КонецЦикла;

Сообщение = Сообщение + Символы.ПС;
КонецЦикла;

Сообщить(Сообщение);
Иначе
Сообщить("Решение не найдено!");
КонецЕсли;

Полный код решения задачи

Ниже представлен полный код решения. Вы также можете скачать пример в виде готовой обработки.

// Создание модели ограничений
Модель = О2
.Модели()
.МодельОграничений()
.СоздатьМодель();

// Входные данные по работам и операциям
ДанныеРабот = Новый Массив;

// Работа 0
Работа0 = Новый Массив;
Работа0.Добавить(Новый Структура("Станок, Длительность", 0, 3)); // Операция 0
Работа0.Добавить(Новый Структура("Станок, Длительность", 1, 2)); // Операция 1
Работа0.Добавить(Новый Структура("Станок, Длительность", 2, 2)); // Операция 2
ДанныеРабот.Добавить(Работа0);

// Работа 1
Работа1 = Новый Массив;
Работа1.Добавить(Новый Структура("Станок, Длительность", 0, 2));
Работа1.Добавить(Новый Структура("Станок, Длительность", 2, 1));
Работа1.Добавить(Новый Структура("Станок, Длительность", 1, 4));
ДанныеРабот.Добавить(Работа1);

// Работа 2
Работа2 = Новый Массив;
Работа2.Добавить(Новый Структура("Станок, Длительность", 1, 4));
Работа2.Добавить(Новый Структура("Станок, Длительность", 2, 3));
ДанныеРабот.Добавить(Работа2);

// Определяем количество станков
НомерПоследнегоСтанка = 0;
Для Каждого Работа Из ДанныеРабот Цикл
Для Каждого Операция Из Работа Цикл
Если Операция.Станок > НомерПоследнегоСтанка Тогда
НомерПоследнегоСтанка = Операция.Станок;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КоличествоСтанков = НомерПоследнегоСтанка + 1;

ВсеСтанки = Новый Массив;
Для К = 0 По КоличествоСтанков - 1 Цикл
ВсеСтанки.Добавить(К);
КонецЦикла;

// Вычисляем горизонт планирования
ГоризонтПланирования = 0;
Для Каждого Работа Из ДанныеРабот Цикл
Для Каждого Операция Из Работа Цикл
ГоризонтПланирования = ГоризонтПланирования + Операция.Длительность;
КонецЦикла;
КонецЦикла;

// Регистрируем переменные
ВсеЗадачи = Новый Массив(ДанныеРабот.Количество());
ИнтервалыПоСтанкам = Новый Соответствие;

Для Каждого Станок Из ВсеСтанки Цикл
ИнтервалыПоСтанкам.Вставить(Станок, Новый Массив);
КонецЦикла;

Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];
ВсеЗадачи[НомерРаботы] = Новый Массив(Работа.Количество());

Для НомерОперации = 0 По Работа.Количество() - 1 Цикл
Операция = Работа[НомерОперации];
Станок = Операция.Станок;
ДлительностьОперации = Операция.Длительность;

Начало = Модель.ПеременнаяДиапазона(0, ГоризонтПланирования);
Конец = Модель.ПеременнаяДиапазона(0, ГоризонтПланирования);
Интервал = Модель.Ограничения().Интервал(Начало, ДлительностьОперации, Конец);

ВсеЗадачи[НомерРаботы][НомерОперации] = Новый Структура("Начало,Конец,Интервал", Начало, Конец, Интервал);

МассивИнтервалов = ИнтервалыПоСтанкам.Получить(Станок);
МассивИнтервалов.Добавить(Интервал);
КонецЦикла;
КонецЦикла;

// Ограничение: операции на одном станке не пересекаются
Для Каждого Станок Из ВсеСтанки Цикл
ИнтервалыНаСтанке = ИнтервалыПоСтанкам.Получить(Станок);
Модель.Ограничения().ЗапретПерекрытий(ИнтервалыНаСтанке);
КонецЦикла;

// Ограничение: операции в рамках одной работы выполняются последовательно
Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];

Для НомерОперации = 0 По Работа.Количество() - 2 Цикл
Текущая = ВсеЗадачи[НомерРаботы][НомерОперации];
Следующая = ВсеЗадачи[НомерРаботы][НомерОперации + 1];

Модель.Ограничения().ЗначениеМеньшеИлиРавно(Текущая.Конец, Следующая.Начало);
КонецЦикла;
КонецЦикла;

// Целевая функция: минимизируем время завершения всех работ
МаксимальноеВремя = Модель.ПеременнаяДиапазона(0, ГоризонтПланирования, "МаксимальноеВремя");

Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];
НомерПоследнейОперации = Работа.Количество() - 1;

ДанныеПоследнейОперации = ВсеЗадачи[НомерРаботы][НомерПоследнейОперации];
КонецПоследнейОперации = ДанныеПоследнейОперации.Конец;

Модель.Ограничения().ЗначениеБольшеИлиРавно(МаксимальноеВремя, КонецПоследнейОперации);
КонецЦикла;

Модель.Минимизировать(МаксимальноеВремя);

// Решение модели
Решение = Модель.Решить();

// Вывод результатов
Если Решение.РешениеНайдено() Тогда
НазначенныеЗадачи = Новый Соответствие;
Для Каждого Станок Из ВсеСтанки Цикл
НазначенныеЗадачи.Вставить(Станок, Новый Массив);
КонецЦикла;

Для НомерРаботы = 0 По ДанныеРабот.Количество() - 1 Цикл
Работа = ДанныеРабот[НомерРаботы];

Для НомерОперации = 0 По Работа.Количество() - 1 Цикл
Операция = Работа[НомерОперации];
Станок = Операция.Станок;
ДлительностьОперации = Операция.Длительность;

ДанныеОперации = ВсеЗадачи[НомерРаботы][НомерОперации];
Начало = Решение.ЗначениеПеременной(ДанныеОперации.Начало);

Задача = Новый Структура(
"Начало, Работа, Операция, Длительность",
Начало, НомерРаботы, НомерОперации, ДлительностьОперации);

МассивЗадач = НазначенныеЗадачи.Получить(Станок);
МассивЗадач.Добавить(Задача);
КонецЦикла;
КонецЦикла;

// Сортировка задач по времени начала
Для Каждого Станок Из ВсеСтанки Цикл
МассивЗадач = НазначенныеЗадачи.Получить(Станок);

Для i = 0 По МассивЗадач.Количество() - 2 Цикл
Для j = i + 1 По МассивЗадач.Количество() - 1 Цикл
Если МассивЗадач[i].Начало > МассивЗадач[j].Начало Тогда
ВременнаяЗадача = МассивЗадач[i];
МассивЗадач[i] = МассивЗадач[j];
МассивЗадач[j] = ВременнаяЗадача;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецЦикла;

ОбщееВремя = Решение.ЗначениеПеременной(МаксимальноеВремя);

Сообщение = "Оптимальное расписание работ" + Символы.ПС + Символы.ПС;
Сообщение = Сообщение + "Общее время выполнения: " + ОбщееВремя + Символы.ПС + Символы.ПС;

Для Каждого Станок Из ВсеСтанки Цикл
МассивЗадач = НазначенныеЗадачи.Получить(Станок);

Сообщение = Сообщение + "Станок " + Станок + ":" + Символы.ПС;

Для Каждого Задача Из МассивЗадач Цикл
ВремяНачала = Задача.Начало;
ВремяОкончания = ВремяНачала + Задача.Длительность;

Сообщение = Сообщение + СтрШаблон(
" Работа %1, Операция %2: начало %3, окончание %4 (длительность: %5)%6",
Задача.Работа,
Задача.Операция,
ВремяНачала,
ВремяОкончания,
Задача.Длительность,
Символы.ПС
);
КонецЦикла;

Сообщение = Сообщение + Символы.ПС;
КонецЦикла;

Сообщить(Сообщение);
Иначе
Сообщить("Решение не найдено!");
КонецЕсли;
  Скачать пример