6. Inventory 3: Multi-Echelon Inventory Systems¶

ISE 754, Fall 2024

Package Used: No new packages used.

Ex: Two-Echelon Supply Chain¶

Estimate Performance Measures¶

Estimate the out-of-stock $\pi_0$ and average-inventory $\overline{q}$ performance measures.

In [1]:
using Random, DataStructures, DataFrames, Statistics

function base_stock_two_echelon(qmax_r, qmax_dc, sch_r, sch_dc, t_stop)
    n = length(qmax_r)                  # Number of retailers

    E = PriorityQueue{Tuple{Symbol, Int, Int}, Float64}()  # Event queue
    for i in 1:n                        # Initial demand events for each retailer
        enqueue!(E, (:demand, i, 0), 0.0)
    end

    # State variables
    S = Dict(
        :q_r => copy(qmax_r),            # Inventory levels at retailers (vector)
        :q_dc => qmax_dc,                # Inventory level at DCr
    )

    # Output log
    out = DataFrame(t=Float64[], evt=Symbol[], loc=Int[], q_r=Vector{Int}[], q_dc=Int[])

    id = 0
    while !isempty(E)
        (evt, i, _), t = dequeue_pair!(E)  # Get next event and time

        t > t_stop && break              # Stop simulation if t > t_stop

        if evt == :demand                # Schedule next demand event for retailer i
            id += 1
            enqueue!(E, (:demand, i, id), sch_r[i][1](t))

            if S[:q_r][i] > 0            # Inventory available, fulfill demand
                S[:q_r][i] -= 1
                id += 1
                enqueue!(E, (:ret2dc_order, i, id), t)  # Immediate order placement
            end

        elseif evt == :ret2dc_order      # Retailer i places order to DC
            if S[:q_dc] > 0              # DC has inventory, fulfill retailer's order
                S[:q_dc] -= 1

                id += 1                  # Schedule arrival of inventory at retailer i
                enqueue!(E, (:dc2ret_ship, i, id), sch_r[i][2](t))

                id += 1                  # DC places immediate replenishment order
                enqueue!(E, (:sup2dc_ship, 0, id), sch_dc(t))
            else                         # DC is out of stock
                id += 1                  # Schedule sup2dc2ret crossdock for backorder
                enqueue!(E, (:sup2dc2ret_ship, i, id), sch_r[i][2](sch_dc(t)))
            end

        elseif evt == :dc2ret_ship        # Inventory arrives at retailer i from DC
            S[:q_r][i] += 1

        elseif evt == :sup2dc_ship        # DC's replenishment arrives from supplier
            S[:q_dc] += 1
        
        elseif evt == :sup2dc2ret_ship    # Backorder arrives at retailer i
            S[:q_r][i] += 1
        end

        push!(out, (t, evt, i, copy(S[:q_r]), S[:q_dc]))   # Log state
    end

    # Calculate metrics
    π₀_r = zeros(n)
    for i in 1:n
        demands = [row for row in eachrow(out) if row.evt == :demand && row.loc == i]
        stockouts = [row for row in demands if row.q_r[i] == 0]
        π₀_r[i] = length(stockouts) / length(demands)
    end

    # Average inventory levels
    q̄_r = [mean([row.q_r[i] for row in eachrow(out)]) for i in 1:n]
    q̄_dc = mean([row.q_dc for row in eachrow(out)])

    return π₀_r, q̄_r, q̄_dc, out
end
Out[1]:
base_stock_two_echelon (generic function with 1 method)
In [2]:
qmax_r, qmax_dc = [8, 8], 30 
rₐ = [3.0, 3.0]
t_dc2ret = [1.0, 1.0]
t_sup2dc = 7.0

n = length(qmax_r)
sch_r = [[] for _ in 1:n]
for i = 1:n
    sch_r[i], rng = [], Xoshiro(1234+i)                                          
    push!(sch_r[i], t -> t + randexp(rng)/rₐ[i])
    push!(sch_r[i], t -> t + t_dc2ret[i])
end                                         
sch_dc = t -> t + t_sup2dc

t_stop = 1000.0
π₀_r, q̄_r, q̄_dc, out = base_stock_two_echelon(qmax_r, qmax_dc, sch_r, sch_dc, t_stop)
@show π₀_r
@show q̄_r
@show q̄_dc;
π₀_r = [0.3802485723883104, 0.4130360205831904]
q̄_r = [2.578978399528327, 2.5653642064640616]
q̄_dc = 4.166532668703436
In [3]:
out
Out[3]:
18657×5 DataFrame
18632 rows omitted
Rowtevtlocq_rq_dc
Float64SymbolInt64Array…Int64
10.0demand1[7, 8]30
20.0demand2[7, 7]30
30.0ret2dc_order1[7, 7]29
40.0ret2dc_order2[7, 7]28
50.456446demand1[6, 7]28
60.456446ret2dc_order1[6, 7]27
70.572171demand1[5, 7]27
80.572171ret2dc_order1[5, 7]26
90.955705demand1[4, 7]26
100.955705ret2dc_order1[4, 7]25
111.0dc2ret_ship2[4, 8]25
121.0dc2ret_ship1[5, 8]25
131.11036demand1[4, 8]25
⋮⋮⋮⋮⋮⋮
18646999.23sup2dc_ship0[0, 2]6
18647999.355dc2ret_ship1[1, 2]6
18648999.427sup2dc_ship0[1, 2]7
18649999.494sup2dc_ship0[1, 2]8
18650999.551demand1[0, 2]8
18651999.551ret2dc_order1[0, 2]7
18652999.632demand2[0, 1]7
18653999.632ret2dc_order2[0, 1]6
18654999.692dc2ret_ship2[0, 2]6
18655999.718demand1[0, 2]6
18656999.735sup2dc_ship0[0, 2]7
18657999.858dc2ret_ship1[1, 2]7
In [4]:
filter(r -> r.evt == :sup2dc2ret_ship, out)
Out[4]:
875×5 DataFrame
850 rows omitted
Rowtevtlocq_rq_dc
Float64SymbolInt64Array…Int64
113.431sup2dc2ret_ship1[2, 1]13
213.47sup2dc2ret_ship1[3, 1]13
313.4785sup2dc2ret_ship1[4, 1]13
413.6191sup2dc2ret_ship1[4, 0]11
513.6471sup2dc2ret_ship2[3, 1]10
614.0319sup2dc2ret_ship2[3, 3]10
714.0383sup2dc2ret_ship2[3, 4]10
814.4642sup2dc2ret_ship1[3, 3]9
914.465sup2dc2ret_ship2[3, 4]9
1014.8347sup2dc2ret_ship1[5, 4]9
1115.515sup2dc2ret_ship1[3, 4]3
1215.6855sup2dc2ret_ship2[3, 6]4
1325.1261sup2dc2ret_ship2[1, 1]16
⋮⋮⋮⋮⋮⋮
864990.305sup2dc2ret_ship1[1, 2]14
865990.387sup2dc2ret_ship1[2, 2]14
866990.495sup2dc2ret_ship2[2, 3]15
867990.56sup2dc2ret_ship2[2, 4]15
868990.682sup2dc2ret_ship1[2, 4]14
869990.683sup2dc2ret_ship1[3, 4]14
870990.895sup2dc2ret_ship1[3, 5]13
871991.048sup2dc2ret_ship1[2, 5]10
872991.057sup2dc2ret_ship1[3, 5]10
873991.263sup2dc2ret_ship1[4, 5]11
874991.269sup2dc2ret_ship2[4, 6]11
875991.689sup2dc2ret_ship2[1, 7]7
In [ ]: