Оптимизация производства удобрений
Многоэтапная модель управления запасами на примере оптимизации производства удобрений
Введение
В данном примере представлено, как использовать Engee для создания интеллектуальной модели планирования производства и складских запасов. Необходимо принять стратегические решения на целый год вперёд.
Представим, что мы управляем заводом, который производит два вида удобрений. В нашем распоряжении — несколько видов сырья (ингредиентов), цена на которые меняется от месяца к месяцу по известному графику (например, сезонно). При этом мы точно знаем, сколько тонн каждого вида удобрений покупатели будут заказывать каждый месяц.
Цель задачи — максимизация прибыли. Для этого нужно найти идеальный баланс между:
- Производством ровно столько, сколько нужно (или можно выгодно продать);
- Закупкой сырья в самые дешёвые месяцы;
- Хранением готовой продукции на складе, когда это экономически оправдано.
Для такой долгосрочной стратегии можно использовать финансовые инструменты (например, фьючерсные контракты), чтобы зафиксировать цены на сырьё на будущее и обезопасить себя от рисков. Наша математическая модель поможет рассчитать самый прибыльный план, учитывая все эти факторы.
Подключим необходимые библиотеки.
using JuMP, DataFrames, GLPK, NamedArrays, Measures
Удобрения и их состав
Гранулированные удобрения содержат три ключевых питательных элемента: азот, фосфор и калий. На заводе мы не производим их с нуля, а смешиваем готовые виды сырья, чтобы получить товарные марки удобрений с нужным составом.
У нас есть несколько «ингредиентов» — каждый со своим уникальным набором питательных веществ. Комбинируя их в правильных пропорциях, мы создаём готовый продукт — сбалансированное удобрение, которое нужно нашим клиентам.
Исходные данные
Определим данные, на основании которых будем выполнять оптимизацию производства:
- величины спроса на удобрения;
- список месяцев в году;
- величины начальных запасов;
- процент содержания питательных веществ в каждом виде удобрений;
- величины заказов на удобрения;
- цены на удобрения;
- вместимость склада;
- стоимость хранения;
- производственная мощность;
- стоимость сырья;
- содержание питательных веществ в сырье.
спрос_на_удобрения = DataFrame(
Сбалансированное = [750, 800, 900, 850, 700, 700, 700, 600, 600, 550, 550, 550],
Высокоазотное = [300, 310, 600, 400, 350, 300, 200, 200, 200, 200, 200, 200]
)
месяцы = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]
начальные_запасы = DataFrame(
Сбалансированное = [200, 200],
Высокоазотное = [200, 200]
)
содержание_питательных_в_удобрениях = DataFrame(
Сбалансированное = [10, 10, 10],
Высокоазотное = [20, 10, 10]
)
заказы_на_удобрения = DataFrame(
Сбалансированное = [200, 400, 400, 400, 400, 400, 200, 0, 0, 0, 0, 0],
Высокоазотное = [0, 100, 200, 200, 200, 200, 200, 0, 0, 0, 0, 0]
)
цены_на_удобрения = DataFrame(
Сбалансированное = [400],
Высокоазотное = [550]
)
вместимость_склада = 1000
стоимость_хранения_единицы = 10
производственная_мощность = 1200
стоимость_сырья = DataFrame(
Моноаммонийфосфат = [350, 360, 350, 350, 320, 320, 320, 320, 320, 310, 310, 340],
Хлорид_калия = [610, 630, 630, 610, 600, 600, 600, 600, 600, 600, 600, 600],
Аммиачная_селитра = [300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300],
Сульфат_аммония = [135, 140, 135, 125, 125, 125, 125, 125, 125, 125, 125, 125],
Тройной_суперфосфат = [250, 275, 275, 250, 250, 250, 250, 240, 240, 240, 240, 240],
Песок = [80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80]
)
содержание_питательных_в_сырье = DataFrame(
Моноаммонийфосфат = [11, 48, 0],
Хлорид_калия = [0, 0, 60],
Аммиачная_селитра = [35, 0, 0],
Сульфат_аммония = [21, 0, 0],
Тройной_суперфосфат = [0, 46, 0],
Песок = [0, 0, 0]
)
виды_удобрений = names(спрос_на_удобрения)
вещества = ["Азот", "Фосфор", "Калий"]
виды_сырья = names(стоимость_сырья)
количество_видов_удобрений = length(виды_удобрений)
количество_видов_сырья = length(виды_сырья)
количество_питательных = length(вещества)
количество_месяцев = length(месяцы)
спрос_с_месяцами = copy(спрос_на_удобрения)
insertcols!(спрос_с_месяцами, 1, :Месяц => месяцы)
стоимость_сырья_с_месяцами = copy(стоимость_сырья)
insertcols!(стоимость_сырья_с_месяцами, 1, :Месяц => месяцы)
содержание_удобрений_с_названиями = copy(содержание_питательных_в_удобрениях)
insertcols!(содержание_удобрений_с_названиями, 1, :Питательное_вещество => вещества)
содержание_сырья_с_названиями = copy(содержание_питательных_в_сырье)
insertcols!(содержание_сырья_с_названиями, 1, :Питательное_вещество => вещества)
Определим функцию для отображения данных в виде таблицы.
function вывести_таблицу(df::DataFrame, заголовок::String="")
if !isempty(заголовок)
println("\n$заголовок:")
end
имена_столбцов = names(df)
ширина_столбцов = []
for столбец in имена_столбцов
ширина = length(string(столбец))
for i in 1:nrow(df)
значение = df[i, столбец]
if isa(значение, Number)
строка_значения = string(round(Int, значение))
else
строка_значения = string(значение)
end
ширина = max(ширина, length(строка_значения))
end
push!(ширина_столбцов, ширина + 2)
end
заголовок_таблицы = ""
for (i, столбец) in enumerate(имена_столбцов)
заголовок_таблицы *= rpad(столбец, ширина_столбцов[i])
end
println(заголовок_таблицы)
разделитель = ""
for ширина in ширина_столбцов
разделитель *= repeat("-", ширина)
end
println(разделитель)
for i in 1:nrow(df)
строка = ""
for (j, столбец) in enumerate(имена_столбцов)
значение = df[i, столбец]
if isa(значение, Number)
строка_значения = string(round(Int, значение))
else
строка_значения = string(значение)
end
строка *= rpad(строка_значения, ширина_столбцов[j])
end
println(строка)
end
println()
end
Имеется два вида удобрений:
-
Сбалансированное — стандартный состав 10% азота, 10% фосфора, 10% калия.
-
Высокоазотное — усиленный азотом вариант: 20% азота, 10% фосфора, 10% калия.
Единственное различие — удвоенная доля азота в "Высокоазотном", что делает его более дорогим в производстве.
println("Содержание питательных веществ в удобрениях (в процентах):\n")
вывести_таблицу(содержание_удобрений_с_названиями)
Сырьё и его питательный состав (в % по массе):
println("Содержание питательных веществ в сырье (в процентах):\n")
вывести_таблицу(содержание_сырья_с_названиями)
Песок — это нейтральный наполнитель. В нём нет питательных веществ. Его используют как «разбавитель», чтобы точно выдержать требуемый процентный состав в готовой смеси, когда активные ингредиенты слишком концентрированы.
Объём заказов и цены на удобрения
На весь период планирования нам известен точный объём заказов по месяцам для каждой марки удобрений.
println("Объём заказов:")
вывести_таблицу(спрос_с_месяцами)
Цены фиксированы на весь год. Это значит, что наша выручка зависит только от объёма продаж, а не от времени. Такая стабильность упрощает планирование и позволяет сосредоточиться на оптимизации затрат.
println("Цены на удобрения:")
вывести_таблицу(цены_на_удобрения)
Стоимость сырья
Цены на сырьё — ключевая переменная в нашей модели. В отличие от стабильных цен на готовую продукцию, стоимость ингредиентов меняется каждый месяц (например, из-за сезонности или конъюнктуры рынка).
println("Стоимость сырья:")
вывести_таблицу(стоимость_сырья_с_месяцами)
Производство и хранение
Отобразим стоимость хранения единицы продукции, вместимость склада, и производственную мощность.
println("Стоимость хранения единицы: $стоимость_хранения_единицы")
println("Вместимость склада: $вместимость_склада")
println("Производственная мощность: $производственная_мощность")
Если в каком-то месяце мы не смогли произвести и отгрузить весь объём заказов, остаток не переносится на следующий месяц. Это заставляет модель всегда планировать производство с опережением спроса. Таким образом, модель ищет баланс: произвести достаточно, чтобы выполнить заказы и выйти на целевые остатки, но не произвести лишнего, чтобы не нести затраты на хранение.
Постановка задачи
В основе модели лежит целевая функция, которую необходимо максимизировать. В нашем случае это прибыль.
модель = Model(GLPK.Optimizer)
Переменные
Переменными в задаче являются объёмы смесей удобрений, которые мы производим и продаём каждый месяц, а также сырьё, используемое для их изготовления.
@variable(модель, производить[1:количество_месяцев, 1:количество_видов_удобрений] >= 0)
@variable(модель, продавать[1:количество_месяцев, 1:количество_видов_удобрений] >= 0)
@variable(модель, использовать[1:количество_месяцев, 1:количество_видов_сырья, 1:количество_видов_удобрений] >= 0)
Кроме того, создадим переменную, представляющая объём запасов на каждый момент времени.
@variable(модель, 0 <= запасы[1:количество_месяцев, 1:количество_видов_удобрений] <= вместимость_склада)
Верхней границей для продаж является спрос для каждого временного периода и каждой марки удобрения.
for i in 1:количество_месяцев, b in 1:количество_видов_удобрений
set_upper_bound(продавать[i, b], спрос_на_удобрения[i, b])
end
Выражения
Чтобы рассчитать целевую функцию через переменные задачи, необходимо вычислить выручку и затраты. Выручка — это объём продаж каждого вида удобрения, умноженный на её цену, просуммированный по всем временным периодам и всем видам удобрений. Определим выражение для выручки.
@expression(модель, выручка, sum(цены_на_удобрения[1, j] * sum(продавать[i, j] for i in 1:количество_месяцев) for j in 1:количество_видов_удобрений))
Определим выражение для затрат на сырьё. Затраты на сырьё — это стоимость каждого использованного ингредиента в каждый момент времени, суммированная по всем периодам. Поскольку объём использованного сырья в каждый момент разделён на количество, использованное для каждого удобрения, суммирование также проводится и по видам удобрений.
@expression(модель, сырье_использовано[i=1:количество_месяцев, r=1:количество_видов_сырья], sum(использовать[i, r, b] for b in 1:количество_видов_удобрений))
@expression(модель, стоимость_сырья_общая, sum(стоимость_сырья[i, r] * сырье_использовано[i, r] for i in 1:количество_месяцев for r in 1:количество_видов_сырья))
Определим выражение для затрат на хранение. Затраты на хранение — это стоимость содержания запасов в течение каждого временного периода, суммированная по времени и видам удобрений.
@expression(модель, стоимость_хранения_общая, стоимость_хранения_единицы * sum(запасы))
Целевая функция
Определим целевую функцию.
@objective(модель, Max, выручка - стоимость_сырья_общая - стоимость_хранения_общая)
Ограничения
Модель должна соблюдать ключевые правила производства:
- баланс запасов: остаток на складе является суммой остатка прошлого месяца и разности количеств произведённой и проданной продукции;
- целевые запасы: к концу года нужно выйти на заданный уровень остатков;
- лимиты: ёмкость склада и мощность завода ограничены;
- баланс материалов: количество готовой продукции равно количеству израсходованного сырья;
- качество: химический состав каждой партии должен строго соответствовать рецептуре.
Все эти правила формализуются в математические ограничения и загружаются в модель для поиска оптимального плана.
@constraint(модель, материальный_баланс1[i=2:количество_месяцев, b=1:количество_видов_удобрений],
запасы[i, b] == запасы[i-1, b] + производить[i, b] - продавать[i, b])
@constraint(модель, материальный_баланс2[b=1:количество_видов_удобрений],
запасы[1, b] == начальные_запасы[1, b] + производить[1, b] - продавать[1, b])
@constraint(модель, финальные_запасы[b=1:количество_видов_удобрений], запасы[количество_месяцев, b] == начальные_запасы[2, b])
@constraint(модель, ограничение_склада[i=1:количество_месяцев], sum(запасы[i, :]) <= вместимость_склада)
@constraint(модель, ограничение_мощности[i=1:количество_месяцев], sum(производить[i, :]) <= производственная_мощность)
@constraint(модель, использование_сырья[i=1:количество_месяцев, b=1:количество_видов_удобрений],
sum(использовать[i, r, b] for r in 1:количество_видов_сырья) == производить[i, b])
@constraint(модель, качество_удобрений[i=1:количество_месяцев, n=1:количество_питательных, b=1:количество_видов_удобрений],
sum(содержание_питательных_в_сырье[n, r] * использовать[i, r, b] for r in 1:количество_видов_сырья) == содержание_питательных_в_удобрениях[n, b] * производить[i, b])
Решение и визуализация результатов
Запустим решение поставленной задачи оптимизации.
optimize!(модель)
Отобразим решение в виде таблицы (если таблица не помещается по ширине, уменьшите масштаб в браузере).
статус = termination_status(модель);
if статус == MOI.OPTIMAL
прибыль = round(Int, objective_value(модель))
println("\nОптимальное решение найдено")
println("Прибыль: $прибыль")
значения_производить = value.(производить)
значения_продавать = value.(продавать)
значения_запасы = value.(запасы)
производить_округленные = round.(Int, значения_производить)
продавать_округленные = round.(Int, значения_продавать)
запасы_округленные = round.(Int, значения_запасы)
таблица_производства = DataFrame()
for b in 1:количество_видов_удобрений
таблица_производства[!, Symbol("произвести_" * replace(виды_удобрений[b], " " => "_"))] = производить_округленные[:, b]
end
таблица_продаж = DataFrame()
for b in 1:количество_видов_удобрений
таблица_продаж[!, Symbol("продать_" * replace(виды_удобрений[b], " " => "_"))] = продавать_округленные[:, b]
end
таблица_запасов = DataFrame()
for b in 1:количество_видов_удобрений
таблица_запасов[!, Symbol("запасы_" * replace(виды_удобрений[b], " " => "_"))] = запасы_округленные[:, b]
end
производственный_план = hcat(таблица_производства, таблица_продаж, таблица_запасов)
производственный_план[!, :Месяц] = месяцы
производственный_план = производственный_план[:, [end, 1:end-1...]]
println("\nПроизводственный план:")
вывести_таблицу(производственный_план)
end
производить_сбалансированное = производить_округленные[:, 1];
производить_высокоазотное = производить_округленные[:, 2];
продавать_сбалансированное = продавать_округленные[:, 1];
продавать_высокоазотное = продавать_округленные[:, 2];
запасы_сбалансированное = запасы_округленные[:, 1];
запасы_высокоазотное = запасы_округленные[:, 2];
Отобразим гистограмму оптимального производства, продаж и складских запасов сбалансированного удобрения.
p1 = bar(месяцы, производить_сбалансированное, title="Произведено", xlabel="", color=:green, ylabel="Количество")
p2 = bar(месяцы, продавать_сбалансированное, title="Продано", xlabel="", color=:green, ylabel="Количество")
p3 = bar(месяцы, запасы_сбалансированное, title="Запасы", color=:green, ylabel="Количество")
график1 = plot(p1, p2, p3, layout=(3, 1), size=(1000, 900), margin=10mm, legend=false)
display(график1)
Отобразим гистограмму оптимального производства, продаж и складских запасов высокоазотного удобрения.
p1 = bar(месяцы, производить_высокоазотное, title="Произведено", xlabel="", color=:orange, ylabel="Количество")
p2 = bar(месяцы, продавать_высокоазотное, title="Продано", xlabel="", color=:orange, ylabel="Количество")
p3 = bar(месяцы, запасы_высокоазотное, title="Запасы", color=:orange, ylabel="Количество")
график2 = plot(p1, p2, p3, layout=(3, 1), size=(1000, 900), margin=10mm, legend=false)
display(график2)
Заключение
Проведённое моделирование многоэтапной производственной задачи демонстрирует эффективность методов оптимизации для стратегического планирования в условиях динамичного рынка. Решение обеспечивает точный и практически реализуемый план.
Ключевые выводы:
-
Стратегия выпуска: Модель выявила приоритет производства высокомаржинальной продукции в периоды пикового спроса, в то время как базовый продукт поддерживает общую загрузку мощностей. Это прямо соответствует цели максимизации прибыли.
-
Динамическая роль запасов: Анализ показал, что запасы служат не резервом, а активным буфером, сглаживающим дисбаланс между переменным спросом и фиксированными мощностями. Используется стратегия планового накопления и последующего снижения запасов для минимизации совокупных издержек.
-
Влияние ограничений: Жёсткие ограничения по ёмкости и целевым конечным остаткам формируют специфическую структуру плана, вызывая всплески производства для выполнения граничных условий. Это подтверждает высокую чувствительность системы к управляющим параметрам.
Таким образом, модель адекватно формализует сложную задачу планирования, трансформируя её в оптимизационную. Результат представляет собой стратегическую карту, выявляющую точки роста прибыли и узкие места системы, что делает данный подход ценным инструментом для обоснования управленческих решений.