Engee documentation
Notebook

A multi-stage inventory management model based on the example of optimizing fertilizer production

Introduction

This example shows how to use Engee to create an intelligent production and inventory planning model. It is necessary to make strategic decisions for the whole year ahead.

Let's imagine that we run a factory that produces two types of fertilizers. We have several types of raw materials (ingredients) at our disposal, the price of which varies from month to month according to a well—known schedule (for example, seasonally). At the same time, we know exactly how many tons of each type of fertilizer customers will order each month. The
goal is to maximize profits. To do this, you need to find the perfect balance between:

  • Produce exactly as much as you need (or you can sell it profitably);
  • Purchase of raw materials in the cheapest months;
  • Storing finished products in a warehouse when it is economically feasible.

For such a long-term strategy, you can use financial instruments (for example, futures contracts) to fix commodity prices for the future and protect yourself from risks. Our mathematical model will help you calculate the most profitable plan, taking into account all these factors.

We will connect the necessary libraries.

In [ ]:
import Pkg 
neededLibs = ["JuMP" ,"GLPK", "NamedArrays", "Measures" ]

for lib in neededLibs
    try
        eval(Meta.parse("using $lib"))
    catch ex
        Pkg.add(lib)
        eval(Meta.parse("using $lib"))
    end
end

Fertilizers and their composition

Granular fertilizers contain three key nutrients: nitrogen, phosphorus and potassium. At the factory, we do not produce them from scratch, but mix ready—made raw materials to obtain commercial brands of fertilizers with the right composition.
We have several "ingredients" - each with its own unique set of nutrients. By combining them in the right proportions, we create a finished product — a balanced fertilizer that our customers need.

Initial data

We will determine the data on the basis of which we will optimize production.:

  • the amount of demand for fertilizers;
  • list of months in a year;
  • Initial stock values;
  • percentage of nutrients in each type of fertilizer;
  • the size of fertilizer orders;
  • prices for fertilizers;
  • Storage capacity;
  • cost of storage;
  • Production capacity;
  • cost of raw materials;
  • the content of nutrients in the raw materials.
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, :Питательное_вещество => вещества)
Out[0]:
3×7 DataFrame
RowПитательное_веществоМоноаммонийфосфатХлорид_калияАммиачная_селитраСульфат_аммонияТройной_суперфосфатПесок
StringInt64Int64Int64Int64Int64Int64
1Азот110352100
2Фосфор48000460
3Калий0600000

Let's define a function for displaying data in the form of a table.

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
Out[0]:
вывести_таблицу (generic function with 2 methods)

There are two types of fertilizers available:

  • Balanced — the standard composition is 10% nitrogen, 10% phosphorus, 10% potassium.

  • High nitrogen — nitrogen-enhanced version: 20% nitrogen, 10% phosphorus, 10% potassium.

The only difference is the doubled proportion of nitrogen in "High Nitrogen", which makes it more expensive to produce.

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

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

Raw materials and their nutritional composition (in % by weight):

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

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

Sand is a neutral filler. There are no nutrients in it. It is used as a "diluent" to accurately maintain the required percentage in the finished mixture when the active ingredients are too concentrated.

Volume of orders and prices for fertilizers

For the entire planning period, we know the exact monthly order volume for each brand of fertilizers.

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

Prices are fixed for the whole year. This means that our revenue depends only on the volume of sales, not on time. This stability simplifies planning and allows you to focus on cost optimization.

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

The cost of raw materials

Raw material prices are a key variable in our model. Unlike stable prices for finished products, the cost of ingredients changes every month (for example, due to seasonality or market conditions).

In [ ]:
println("The cost of raw materials:")
вывести_таблицу(стоимость_сырья_с_месяцами)
Стоимость сырья:
Месяц     Моноаммонийфосфат  Хлорид_калия  Аммиачная_селитра  Сульфат_аммония  Тройной_суперфосфат  Песок  
-----------------------------------------------------------------------------------------------------------
Январь    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     

Production and storage

We will display the cost of unit storage, warehouse capacity, and production capacity.

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

If we were unable to produce and ship the entire volume of orders in one month, the balance will not be transferred to the next month. This forces the model to always plan production ahead of demand. Thus, the model is looking for a balance: to produce enough to fulfill orders and reach the target balances, but not to produce too much, so as not to incur storage costs.

Setting the task

The model is based on an objective function that needs to be maximized. In our case, it's a profit.

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

Variables

The variables in the task are the volumes of fertilizer mixtures that we produce and sell each month, as well as the raw materials used to make them.

In [ ]:
@variable(модель, производить[1:количество_месяцев, 1:количество_видов_удобрений] >= 0)
@variable(модель, продавать[1:количество_месяцев, 1:количество_видов_удобрений] >= 0)
@variable(модель, использовать[1:количество_месяцев, 1:количество_видов_сырья, 1:количество_видов_удобрений] >= 0)
Out[0]:
12×6×2 Array{VariableRef, 3}:
[:, :, 1] =
 использовать[1,1,1]   использовать[1,2,1]   …  использовать[1,6,1]
 использовать[2,1,1]   использовать[2,2,1]      использовать[2,6,1]
 использовать[3,1,1]   использовать[3,2,1]      использовать[3,6,1]
 использовать[4,1,1]   использовать[4,2,1]      использовать[4,6,1]
 использовать[5,1,1]   использовать[5,2,1]      использовать[5,6,1]
 использовать[6,1,1]   использовать[6,2,1]   …  использовать[6,6,1]
 использовать[7,1,1]   использовать[7,2,1]      использовать[7,6,1]
 использовать[8,1,1]   использовать[8,2,1]      использовать[8,6,1]
 использовать[9,1,1]   использовать[9,2,1]      использовать[9,6,1]
 использовать[10,1,1]  использовать[10,2,1]     использовать[10,6,1]
 использовать[11,1,1]  использовать[11,2,1]  …  использовать[11,6,1]
 использовать[12,1,1]  использовать[12,2,1]     использовать[12,6,1]

[:, :, 2] =
 использовать[1,1,2]   использовать[1,2,2]   …  использовать[1,6,2]
 использовать[2,1,2]   использовать[2,2,2]      использовать[2,6,2]
 использовать[3,1,2]   использовать[3,2,2]      использовать[3,6,2]
 использовать[4,1,2]   использовать[4,2,2]      использовать[4,6,2]
 использовать[5,1,2]   использовать[5,2,2]      использовать[5,6,2]
 использовать[6,1,2]   использовать[6,2,2]   …  использовать[6,6,2]
 использовать[7,1,2]   использовать[7,2,2]      использовать[7,6,2]
 использовать[8,1,2]   использовать[8,2,2]      использовать[8,6,2]
 использовать[9,1,2]   использовать[9,2,2]      использовать[9,6,2]
 использовать[10,1,2]  использовать[10,2,2]     использовать[10,6,2]
 использовать[11,1,2]  использовать[11,2,2]  …  использовать[11,6,2]
 использовать[12,1,2]  использовать[12,2,2]     использовать[12,6,2]

In addition, we will create a variable representing the amount of inventory at each point in time.

In [ ]:
@variable(модель, 0 <= запасы[1:количество_месяцев, 1:количество_видов_удобрений] <= вместимость_склада)
Out[0]:
12×2 Matrix{VariableRef}:
 запасы[1,1]   запасы[1,2]
 запасы[2,1]   запасы[2,2]
 запасы[3,1]   запасы[3,2]
 запасы[4,1]   запасы[4,2]
 запасы[5,1]   запасы[5,2]
 запасы[6,1]   запасы[6,2]
 запасы[7,1]   запасы[7,2]
 запасы[8,1]   запасы[8,2]
 запасы[9,1]   запасы[9,2]
 запасы[10,1]  запасы[10,2]
 запасы[11,1]  запасы[11,2]
 запасы[12,1]  запасы[12,2]

The upper limit for sales is the demand for each time period and each brand of fertilizer.

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

Expressions

To calculate the objective function using task variables, you need to calculate revenue and costs. Revenue is the sales volume of each type of fertilizer multiplied by its price, summed up over all time periods and all types of fertilizers. Let's define an expression for revenue.

In [ ]:
@expression(модель, выручка, sum(цены_на_удобрения[1, j] * sum(продавать[i, j] for i in 1:количество_месяцев) for j in 1:количество_видов_удобрений))
Out[0]:
$ 400 продавать_{1,1} + 400 продавать_{2,1} + 400 продавать_{3,1} + 400 продавать_{4,1} + 400 продавать_{5,1} + 400 продавать_{6,1} + 400 продавать_{7,1} + 400 продавать_{8,1} + 400 продавать_{9,1} + 400 продавать_{10,1} + 400 продавать_{11,1} + 400 продавать_{12,1} + 550 продавать_{1,2} + 550 продавать_{2,2} + 550 продавать_{3,2} + 550 продавать_{4,2} + 550 продавать_{5,2} + 550 продавать_{6,2} + 550 продавать_{7,2} + 550 продавать_{8,2} + 550 продавать_{9,2} + 550 продавать_{10,2} + 550 продавать_{11,2} + 550 продавать_{12,2} $

Let's define an expression for the cost of raw materials. The cost of raw materials is the cost of each ingredient used at each point in time, summed up over all periods. Since the volume of raw materials used at each moment is divided by the amount used for each fertilizer, the summation is also carried out by types of fertilizers.

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:количество_видов_сырья))
Out[0]:
$ 350 использовать_{1,1,1} + 350 использовать_{1,1,2} + 610 использовать_{1,2,1} + 610 использовать_{1,2,2} + 300 использовать_{1,3,1} + 300 использовать_{1,3,2} + 135 использовать_{1,4,1} + 135 использовать_{1,4,2} + 250 использовать_{1,5,1} + 250 использовать_{1,5,2} + 80 использовать_{1,6,1} + 80 использовать_{1,6,2} + 360 использовать_{2,1,1} + 360 использовать_{2,1,2} + 630 использовать_{2,2,1} + 630 использовать_{2,2,2} + 300 использовать_{2,3,1} + 300 использовать_{2,3,2} + 140 использовать_{2,4,1} + 140 использовать_{2,4,2} + 275 использовать_{2,5,1} + 275 использовать_{2,5,2} + 80 использовать_{2,6,1} + 80 использовать_{2,6,2} + 350 использовать_{3,1,1} + 350 использовать_{3,1,2} + 630 использовать_{3,2,1} + 630 использовать_{3,2,2} + 300 использовать_{3,3,1} + 300 использовать_{3,3,2} + [[\ldots\text{84 terms omitted}\ldots]] + 125 использовать_{10,4,1} + 125 использовать_{10,4,2} + 240 использовать_{10,5,1} + 240 использовать_{10,5,2} + 80 использовать_{10,6,1} + 80 использовать_{10,6,2} + 310 использовать_{11,1,1} + 310 использовать_{11,1,2} + 600 использовать_{11,2,1} + 600 использовать_{11,2,2} + 300 использовать_{11,3,1} + 300 использовать_{11,3,2} + 125 использовать_{11,4,1} + 125 использовать_{11,4,2} + 240 использовать_{11,5,1} + 240 использовать_{11,5,2} + 80 использовать_{11,6,1} + 80 использовать_{11,6,2} + 340 использовать_{12,1,1} + 340 использовать_{12,1,2} + 600 использовать_{12,2,1} + 600 использовать_{12,2,2} + 300 использовать_{12,3,1} + 300 использовать_{12,3,2} + 125 использовать_{12,4,1} + 125 использовать_{12,4,2} + 240 использовать_{12,5,1} + 240 использовать_{12,5,2} + 80 использовать_{12,6,1} + 80 использовать_{12,6,2} $

Let's define an expression for storage costs. Storage costs are the cost of maintaining stocks during each time period, summed up by time and types of fertilizers.

In [ ]:
@expression(модель, стоимость_хранения_общая, стоимость_хранения_единицы * sum(запасы))
Out[0]:
$ 10 запасы_{1,1} + 10 запасы_{2,1} + 10 запасы_{3,1} + 10 запасы_{4,1} + 10 запасы_{5,1} + 10 запасы_{6,1} + 10 запасы_{7,1} + 10 запасы_{8,1} + 10 запасы_{9,1} + 10 запасы_{10,1} + 10 запасы_{11,1} + 10 запасы_{12,1} + 10 запасы_{1,2} + 10 запасы_{2,2} + 10 запасы_{3,2} + 10 запасы_{4,2} + 10 запасы_{5,2} + 10 запасы_{6,2} + 10 запасы_{7,2} + 10 запасы_{8,2} + 10 запасы_{9,2} + 10 запасы_{10,2} + 10 запасы_{11,2} + 10 запасы_{12,2} $

Target function

Let's define the objective function.

In [ ]:
@objective(модель, Max, выручка - стоимость_сырья_общая - стоимость_хранения_общая)
Out[0]:
$ 400 продавать_{1,1} + 400 продавать_{2,1} + 400 продавать_{3,1} + 400 продавать_{4,1} + 400 продавать_{5,1} + 400 продавать_{6,1} + 400 продавать_{7,1} + 400 продавать_{8,1} + 400 продавать_{9,1} + 400 продавать_{10,1} + 400 продавать_{11,1} + 400 продавать_{12,1} + 550 продавать_{1,2} + 550 продавать_{2,2} + 550 продавать_{3,2} + 550 продавать_{4,2} + 550 продавать_{5,2} + 550 продавать_{6,2} + 550 продавать_{7,2} + 550 продавать_{8,2} + 550 продавать_{9,2} + 550 продавать_{10,2} + 550 продавать_{11,2} + 550 продавать_{12,2} - 350 использовать_{1,1,1} - 350 использовать_{1,1,2} - 610 использовать_{1,2,1} - 610 использовать_{1,2,2} - 300 использовать_{1,3,1} - 300 использовать_{1,3,2} - [[\ldots\text{132 terms omitted}\ldots]] - 125 использовать_{12,4,1} - 125 использовать_{12,4,2} - 240 использовать_{12,5,1} - 240 использовать_{12,5,2} - 80 использовать_{12,6,1} - 80 использовать_{12,6,2} - 10 запасы_{1,1} - 10 запасы_{2,1} - 10 запасы_{3,1} - 10 запасы_{4,1} - 10 запасы_{5,1} - 10 запасы_{6,1} - 10 запасы_{7,1} - 10 запасы_{8,1} - 10 запасы_{9,1} - 10 запасы_{10,1} - 10 запасы_{11,1} - 10 запасы_{12,1} - 10 запасы_{1,2} - 10 запасы_{2,2} - 10 запасы_{3,2} - 10 запасы_{4,2} - 10 запасы_{5,2} - 10 запасы_{6,2} - 10 запасы_{7,2} - 10 запасы_{8,2} - 10 запасы_{9,2} - 10 запасы_{10,2} - 10 запасы_{11,2} - 10 запасы_{12,2} $

Limitations

The model must comply with key production rules.:

  • Inventory balance: the balance in the warehouse is the sum of the balance of the previous month and the difference in the quantities of products produced and sold;
  • target stocks: by the end of the year, it is necessary to reach the set balance level;
  • Limits: Warehouse capacity and factory capacity are limited;
  • balance of materials: the amount of finished products is equal to the amount of consumed raw materials;
  • Quality: the chemical composition of each batch must strictly correspond to the formulation.

All these rules are formalized into mathematical constraints and loaded into the model to find the optimal plan.

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])
Out[0]:
12×3×2 Array{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.EqualTo{Float64}}, ScalarShape}, 3}:
[:, :, 1] =
 качество_удобрений[1,1,1] : -10 производить[1,1] + 11 использовать[1,1,1] + 35 использовать[1,3,1] + 21 использовать[1,4,1] = 0       …  качество_удобрений[1,3,1] : -10 производить[1,1] + 60 использовать[1,2,1] = 0
 качество_удобрений[2,1,1] : -10 производить[2,1] + 11 использовать[2,1,1] + 35 использовать[2,3,1] + 21 использовать[2,4,1] = 0          качество_удобрений[2,3,1] : -10 производить[2,1] + 60 использовать[2,2,1] = 0
 качество_удобрений[3,1,1] : -10 производить[3,1] + 11 использовать[3,1,1] + 35 использовать[3,3,1] + 21 использовать[3,4,1] = 0          качество_удобрений[3,3,1] : -10 производить[3,1] + 60 использовать[3,2,1] = 0
 качество_удобрений[4,1,1] : -10 производить[4,1] + 11 использовать[4,1,1] + 35 использовать[4,3,1] + 21 использовать[4,4,1] = 0          качество_удобрений[4,3,1] : -10 производить[4,1] + 60 использовать[4,2,1] = 0
 качество_удобрений[5,1,1] : -10 производить[5,1] + 11 использовать[5,1,1] + 35 использовать[5,3,1] + 21 использовать[5,4,1] = 0          качество_удобрений[5,3,1] : -10 производить[5,1] + 60 использовать[5,2,1] = 0
 качество_удобрений[6,1,1] : -10 производить[6,1] + 11 использовать[6,1,1] + 35 использовать[6,3,1] + 21 использовать[6,4,1] = 0       …  качество_удобрений[6,3,1] : -10 производить[6,1] + 60 использовать[6,2,1] = 0
 качество_удобрений[7,1,1] : -10 производить[7,1] + 11 использовать[7,1,1] + 35 использовать[7,3,1] + 21 использовать[7,4,1] = 0          качество_удобрений[7,3,1] : -10 производить[7,1] + 60 использовать[7,2,1] = 0
 качество_удобрений[8,1,1] : -10 производить[8,1] + 11 использовать[8,1,1] + 35 использовать[8,3,1] + 21 использовать[8,4,1] = 0          качество_удобрений[8,3,1] : -10 производить[8,1] + 60 использовать[8,2,1] = 0
 качество_удобрений[9,1,1] : -10 производить[9,1] + 11 использовать[9,1,1] + 35 использовать[9,3,1] + 21 использовать[9,4,1] = 0          качество_удобрений[9,3,1] : -10 производить[9,1] + 60 использовать[9,2,1] = 0
 качество_удобрений[10,1,1] : -10 производить[10,1] + 11 использовать[10,1,1] + 35 использовать[10,3,1] + 21 использовать[10,4,1] = 0     качество_удобрений[10,3,1] : -10 производить[10,1] + 60 использовать[10,2,1] = 0
 качество_удобрений[11,1,1] : -10 производить[11,1] + 11 использовать[11,1,1] + 35 использовать[11,3,1] + 21 использовать[11,4,1] = 0  …  качество_удобрений[11,3,1] : -10 производить[11,1] + 60 использовать[11,2,1] = 0
 качество_удобрений[12,1,1] : -10 производить[12,1] + 11 использовать[12,1,1] + 35 использовать[12,3,1] + 21 использовать[12,4,1] = 0     качество_удобрений[12,3,1] : -10 производить[12,1] + 60 использовать[12,2,1] = 0

[:, :, 2] =
 качество_удобрений[1,1,2] : -20 производить[1,2] + 11 использовать[1,1,2] + 35 использовать[1,3,2] + 21 использовать[1,4,2] = 0       …  качество_удобрений[1,3,2] : -10 производить[1,2] + 60 использовать[1,2,2] = 0
 качество_удобрений[2,1,2] : -20 производить[2,2] + 11 использовать[2,1,2] + 35 использовать[2,3,2] + 21 использовать[2,4,2] = 0          качество_удобрений[2,3,2] : -10 производить[2,2] + 60 использовать[2,2,2] = 0
 качество_удобрений[3,1,2] : -20 производить[3,2] + 11 использовать[3,1,2] + 35 использовать[3,3,2] + 21 использовать[3,4,2] = 0          качество_удобрений[3,3,2] : -10 производить[3,2] + 60 использовать[3,2,2] = 0
 качество_удобрений[4,1,2] : -20 производить[4,2] + 11 использовать[4,1,2] + 35 использовать[4,3,2] + 21 использовать[4,4,2] = 0          качество_удобрений[4,3,2] : -10 производить[4,2] + 60 использовать[4,2,2] = 0
 качество_удобрений[5,1,2] : -20 производить[5,2] + 11 использовать[5,1,2] + 35 использовать[5,3,2] + 21 использовать[5,4,2] = 0          качество_удобрений[5,3,2] : -10 производить[5,2] + 60 использовать[5,2,2] = 0
 качество_удобрений[6,1,2] : -20 производить[6,2] + 11 использовать[6,1,2] + 35 использовать[6,3,2] + 21 использовать[6,4,2] = 0       …  качество_удобрений[6,3,2] : -10 производить[6,2] + 60 использовать[6,2,2] = 0
 качество_удобрений[7,1,2] : -20 производить[7,2] + 11 использовать[7,1,2] + 35 использовать[7,3,2] + 21 использовать[7,4,2] = 0          качество_удобрений[7,3,2] : -10 производить[7,2] + 60 использовать[7,2,2] = 0
 качество_удобрений[8,1,2] : -20 производить[8,2] + 11 использовать[8,1,2] + 35 использовать[8,3,2] + 21 использовать[8,4,2] = 0          качество_удобрений[8,3,2] : -10 производить[8,2] + 60 использовать[8,2,2] = 0
 качество_удобрений[9,1,2] : -20 производить[9,2] + 11 использовать[9,1,2] + 35 использовать[9,3,2] + 21 использовать[9,4,2] = 0          качество_удобрений[9,3,2] : -10 производить[9,2] + 60 использовать[9,2,2] = 0
 качество_удобрений[10,1,2] : -20 производить[10,2] + 11 использовать[10,1,2] + 35 использовать[10,3,2] + 21 использовать[10,4,2] = 0     качество_удобрений[10,3,2] : -10 производить[10,2] + 60 использовать[10,2,2] = 0
 качество_удобрений[11,1,2] : -20 производить[11,2] + 11 использовать[11,1,2] + 35 использовать[11,3,2] + 21 использовать[11,4,2] = 0  …  качество_удобрений[11,3,2] : -10 производить[11,2] + 60 использовать[11,2,2] = 0
 качество_удобрений[12,1,2] : -20 производить[12,2] + 11 использовать[12,1,2] + 35 использовать[12,3,2] + 21 использовать[12,4,2] = 0     качество_удобрений[12,3,2] : -10 производить[12,2] + 60 использовать[12,2,2] = 0

Solving and visualizing the results

Let's start solving the optimization problem.

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

Let's display the solution as a table (if the table does not fit in width, zoom out in the browser).

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                   

We will display a histogram of optimal production, sales, and inventory of a balanced fertilizer.

In [ ]:
p1 = bar(месяцы, производить_сбалансированное, title="Produced by", xlabel="", color=:green, ylabel="Quantity")
p2 = bar(месяцы, продавать_сбалансированное, title="Sold", xlabel="", color=:green, ylabel="Quantity")
p3 = bar(месяцы, запасы_сбалансированное, title="Stocks", color=:green, ylabel="Quantity")
график1 = plot(p1, p2, p3, layout=(3, 1), size=(1000, 900), margin=10mm, legend=false)
display(график1)
No description has been provided for this image

We will display a histogram of optimal production, sales, and inventory of high nitrogen fertilizers.

In [ ]:
p1 = bar(месяцы, производить_высокоазотное, title="Produced by", xlabel="", color=:orange, ylabel="Quantity")
p2 = bar(месяцы, продавать_высокоазотное, title="Sold", xlabel="", color=:orange, ylabel="Quantity")
p3 = bar(месяцы, запасы_высокоазотное, title="Stocks", color=:orange, ylabel="Quantity")
график2 = plot(p1, p2, p3, layout=(3, 1), size=(1000, 900), margin=10mm, legend=false)
display(график2)
No description has been provided for this image

Conclusion

The conducted modeling of a multi-stage production task demonstrates the effectiveness of optimization methods for strategic planning in a dynamic market. The solution provides an accurate and practical plan.

Key findings:

  1. Release strategy: The model has identified the priority of producing high-margin products during periods of peak demand, while the base product supports overall capacity utilization. This is directly consistent with the goal of maximizing profits.

  2. Dynamic role of reserves: The analysis showed that reserves do not serve as a reserve, but as an active buffer smoothing the imbalance between variable demand and fixed capacities. A strategy of planned accumulation and subsequent reduction of stocks is used to minimize total costs.

  3. Impact of constraints: Tight constraints on capacity and target end balances form a specific structure of the plan, causing production spikes to meet the boundary conditions. This confirms the high sensitivity of the system to the control parameters.

Thus, the model adequately formalizes a complex planning task, transforming it into an optimization one. The result is a strategic map that identifies profit growth points and system bottlenecks, which makes this approach a valuable tool for justifying management decisions.