Сообщество Engee

Оптимизация производства удобрений

Автор
avatar-artpgchartpgch
Notebook

Многоэтапная модель управления запасами на примере оптимизации производства удобрений

Введение

В данном примере представлено, как использовать Engee для создания интеллектуальной модели планирования производства и складских запасов. Необходимо принять стратегические решения на целый год вперёд.

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

  • Производством ровно столько, сколько нужно (или можно выгодно продать);
  • Закупкой сырья в самые дешёвые месяцы;
  • Хранением готовой продукции на складе, когда это экономически оправдано.

Для такой долгосрочной стратегии можно использовать финансовые инструменты (например, фьючерсные контракты), чтобы зафиксировать цены на сырьё на будущее и обезопасить себя от рисков. Наша математическая модель поможет рассчитать самый прибыльный план, учитывая все эти факторы.

Подключим необходимые библиотеки.

In [ ]:
using JuMP, DataFrames, GLPK, NamedArrays, Measures

Удобрения и их состав

Гранулированные удобрения содержат три ключевых питательных элемента: азот, фосфор и калий. На заводе мы не производим их с нуля, а смешиваем готовые виды сырья, чтобы получить товарные марки удобрений с нужным составом.
У нас есть несколько «ингредиентов» — каждый со своим уникальным набором питательных веществ. Комбинируя их в правильных пропорциях, мы создаём готовый продукт — сбалансированное удобрение, которое нужно нашим клиентам.

Исходные данные

Определим данные, на основании которых будем выполнять оптимизацию производства:

  • величины спроса на удобрения;
  • список месяцев в году;
  • величины начальных запасов;
  • процент содержания питательных веществ в каждом виде удобрений;
  • величины заказов на удобрения;
  • цены на удобрения;
  • вместимость склада;
  • стоимость хранения;
  • производственная мощность;
  • стоимость сырья;
  • содержание питательных веществ в сырье.
In [ ]:
спрос_на_удобрения = 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, :Питательное_вещество => вещества)

Определим функцию для отображения данных в виде таблицы.

In [ ]:
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% калия.

Единственное различие — удвоенная доля азота в "Высокоазотном", что делает его более дорогим в производстве.

In [ ]:
println("Содержание питательных веществ в удобрениях (в процентах):\n")
вывести_таблицу(содержание_удобрений_с_названиями)
Содержание питательных веществ в удобрениях (в процентах):

Питательное_вещество  Сбалансированное  Высокоазотное  
-------------------------------------------------------
Азот                  10                20             
Фосфор                10                10             
Калий                 10                10             

Сырьё и его питательный состав (в % по массе):

In [ ]:
println("Содержание питательных веществ в сырье (в процентах):\n")
вывести_таблицу(содержание_сырья_с_названиями)
Содержание питательных веществ в сырье (в процентах):

Питательное_вещество  Моноаммонийфосфат  Хлорид_калия  Аммиачная_селитра  Сульфат_аммония  Тройной_суперфосфат  Песок  
-----------------------------------------------------------------------------------------------------------------------
Азот                  11                 0             35                 21               0                    0      
Фосфор                48                 0             0                  0                46                   0      
Калий                 0                  60            0                  0                0                    0      

Песок — это нейтральный наполнитель. В нём нет питательных веществ. Его используют как «разбавитель», чтобы точно выдержать требуемый процентный состав в готовой смеси, когда активные ингредиенты слишком концентрированы.

Объём заказов и цены на удобрения

На весь период планирования нам известен точный объём заказов по месяцам для каждой марки удобрений.

In [ ]:
println("Объём заказов:")
вывести_таблицу(спрос_с_месяцами)
Объём заказов:
Месяц     Сбалансированное  Высокоазотное  
-------------------------------------------
Январь    750               300            
Февраль   800               310            
Март      900               600            
Апрель    850               400            
Май       700               350            
Июнь      700               300            
Июль      700               200            
Август    600               200            
Сентябрь  600               200            
Октябрь   550               200            
Ноябрь    550               200            
Декабрь   550               200            

Цены фиксированы на весь год. Это значит, что наша выручка зависит только от объёма продаж, а не от времени. Такая стабильность упрощает планирование и позволяет сосредоточиться на оптимизации затрат.

In [ ]:
println("Цены на удобрения:")
вывести_таблицу(цены_на_удобрения)
Цены на удобрения:
Сбалансированное  Высокоазотное  
---------------------------------
400               550            

Стоимость сырья

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

In [ ]:
println("Стоимость сырья:")
вывести_таблицу(стоимость_сырья_с_месяцами)
Стоимость сырья:
Месяц     Моноаммонийфосфат  Хлорид_калия  Аммиачная_селитра  Сульфат_аммония  Тройной_суперфосфат  Песок  
-----------------------------------------------------------------------------------------------------------
Январь    350                610           300                135              250                  80     
Февраль   360                630           300                140              275                  80     
Март      350                630           300                135              275                  80     
Апрель    350                610           300                125              250                  80     
Май       320                600           300                125              250                  80     
Июнь      320                600           300                125              250                  80     
Июль      320                600           300                125              250                  80     
Август    320                600           300                125              240                  80     
Сентябрь  320                600           300                125              240                  80     
Октябрь   310                600           300                125              240                  80     
Ноябрь    310                600           300                125              240                  80     
Декабрь   340                600           300                125              240                  80     

Производство и хранение

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

In [ ]:
println("Стоимость хранения единицы: $стоимость_хранения_единицы")
println("Вместимость склада: $вместимость_склада")
println("Производственная мощность: $производственная_мощность")
Стоимость хранения единицы: 10
Вместимость склада: 1000
Производственная мощность: 1200

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

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

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

In [ ]:
модель = Model(GLPK.Optimizer)
Out[0]:
A JuMP Model
├ solver: GLPK
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none

Переменные

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

In [ ]:
@variable(модель, производить[1:количество_месяцев, 1:количество_видов_удобрений] >= 0)
@variable(модель, продавать[1:количество_месяцев, 1:количество_видов_удобрений] >= 0)
@variable(модель, использовать[1:количество_месяцев, 1:количество_видов_сырья, 1:количество_видов_удобрений] >= 0)

Кроме того, создадим переменную, представляющая объём запасов на каждый момент времени.

In [ ]:
@variable(модель, 0 <= запасы[1:количество_месяцев, 1:количество_видов_удобрений] <= вместимость_склада)

Верхней границей для продаж является спрос для каждого временного периода и каждой марки удобрения.

In [ ]:
for i in 1:количество_месяцев, b in 1:количество_видов_удобрений
    set_upper_bound(продавать[i, b], спрос_на_удобрения[i, b])
end

Выражения

Чтобы рассчитать целевую функцию через переменные задачи, необходимо вычислить выручку и затраты. Выручка — это объём продаж каждого вида удобрения, умноженный на её цену, просуммированный по всем временным периодам и всем видам удобрений. Определим выражение для выручки.

In [ ]:
@expression(модель, выручка, sum(цены_на_удобрения[1, j] * sum(продавать[i, j] for i in 1:количество_месяцев) for j in 1:количество_видов_удобрений))

Определим выражение для затрат на сырьё. Затраты на сырьё — это стоимость каждого использованного ингредиента в каждый момент времени, суммированная по всем периодам. Поскольку объём использованного сырья в каждый момент разделён на количество, использованное для каждого удобрения, суммирование также проводится и по видам удобрений.

In [ ]:
@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:количество_видов_сырья))

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

In [ ]:
@expression(модель, стоимость_хранения_общая, стоимость_хранения_единицы * sum(запасы))

Целевая функция

Определим целевую функцию.

In [ ]:
@objective(модель, Max, выручка - стоимость_сырья_общая - стоимость_хранения_общая)

Ограничения

Модель должна соблюдать ключевые правила производства:

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

Все эти правила формализуются в математические ограничения и загружаются в модель для поиска оптимального плана.

In [ ]:
@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])

Решение и визуализация результатов

Запустим решение поставленной задачи оптимизации.

In [ ]:
optimize!(модель)

Отобразим решение в виде таблицы (если таблица не помещается по ширине, уменьшите масштаб в браузере).

In [ ]:
статус = 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];
Оптимальное решение найдено
Прибыль: 2247394

Производственный план:
Месяц     произвести_Сбалансированное  произвести_Высокоазотное  продать_Сбалансированное  продать_Высокоазотное  запасы_Сбалансированное  запасы_Высокоазотное  
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Январь    1100                         100                       750                       300                    550                      0                     
Февраль   600                          310                       800                       310                    350                      0                     
Март      550                          650                       900                       600                    0                        50                    
Апрель    850                          350                       850                       400                    0                        0                     
Май       700                          350                       700                       350                    0                        0                     
Июнь      700                          300                       700                       300                    0                        0                     
Июль      700                          200                       700                       200                    0                        0                     
Август    600                          200                       600                       200                    0                        0                     
Сентябрь  600                          200                       600                       200                    0                        0                     
Октябрь   550                          200                       550                       200                    0                        0                     
Ноябрь    550                          200                       550                       200                    0                        0                     
Декабрь   750                          400                       550                       200                    200                      200                   

Отобразим гистограмму оптимального производства, продаж и складских запасов сбалансированного удобрения.

In [ ]:
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)

Отобразим гистограмму оптимального производства, продаж и складских запасов высокоазотного удобрения.

In [ ]:
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)

Заключение

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

Ключевые выводы:

  1. Стратегия выпуска: Модель выявила приоритет производства высокомаржинальной продукции в периоды пикового спроса, в то время как базовый продукт поддерживает общую загрузку мощностей. Это прямо соответствует цели максимизации прибыли.

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

  3. Влияние ограничений: Жёсткие ограничения по ёмкости и целевым конечным остаткам формируют специфическую структуру плана, вызывая всплески производства для выполнения граничных условий. Это подтверждает высокую чувствительность системы к управляющим параметрам.

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