Skip to content

Commit

Permalink
new release: plants are now modelled with a single node
Browse files Browse the repository at this point in the history
(with a self-loop)
  • Loading branch information
hdavid16 committed Jun 3, 2022
1 parent 7535741 commit cbd7fe2
Show file tree
Hide file tree
Showing 11 changed files with 50 additions and 100 deletions.
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name = "InventoryManagement"
uuid = "2ad91f63-398d-4379-af6a-5a85689656d5"
authors = ["hdavid16 <[email protected]> and contributors"]
version = "0.4.4"
version = "0.5.0"

[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5"
NamedArrays = "86f7a689-2022-50b4-a561-43c23ac3c673"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@
- `Markets`: Nodes where end-customers place final product orders (e.g., retailer).

These types of nodes can be used to model the following components of a supply network:
- `Manufacturing Plants`: Plants are modeled using at least two nodes joined by a directed arc:
- Raw material storage node: Upstream `producer` node that stores raw materials and the plant's `bill of materials`.
- Product storage node: Downstream `distributor` node that stores the materials produced at the plant.
- Arc: the time elapsed between the consumption of raw materials and the production of goods (production time) is modeled with the arc lead time.
- `Manufacturing Plants`: Plants are modeled using a single node with self-loop. The self-loop is an arc for the node to source products from itself via production. The time elapsed between the consumption of raw materials and the production of goods (production time) is modeled with the arc lead time.
- `Distribution Centers`: DCs are modeled using `distributor` nodes.

Note: Any node can be marked as a `market` node to indicate that there is external demand for one or more materials stored at that node. This allows external demand at distribution centers or at manufacturing plants (either for raw materials or products).
Expand Down
54 changes: 21 additions & 33 deletions examples/ex1.jl
Original file line number Diff line number Diff line change
@@ -1,59 +1,48 @@
#2 - echelon system with production

using Distributions
using InventoryManagement

#define network topology and materials:
# Nodes 1-2: Plant
# - Node 1: Stores raw material B; Converts B => A; receives orders for B from Node 3
# - Node 2: Stores product A; receives orders for A from Node 3
# - Node 3: Retail; has market demand for A and B
adj_matrix = [0 1 1;
0 0 1;
0 0 0]
# Node 1: Plant
# - Stores raw material B; Converts B => A; receives orders for B from Node 2
# - Stores product A; receives orders for A from Node 2
# Node 2: Retail; has market demand for A and B
adj_matrix = [1 1;
0 0]
net = MetaDiGraph(adj_matrix)
set_prop!(net, :materials, [:A, :B])

#specify parameters, holding costs and capacity, market demands and penalty for unfilfilled demand
set_props!(net, 1, Dict(
:initial_inventory => Dict(:B => 125),
:holding_cost => Dict(:B => 0.001),
:bill_of_materials => Dict((:B,:A) => -1)
:initial_inventory => Dict(:B => 125, :A => 125),
:holding_cost => Dict(:B => 0.001, :A => 0.002),
:bill_of_materials => Dict((:B,:A) => -1),
:supplier_priority => Dict(:A => 1)
))
set_props!(net, 2, Dict(
:initial_inventory => Dict(:A => 125),
:holding_cost => Dict(:A => 0.002)
))
set_props!(net, 3, Dict(
:initial_inventory => Dict(:A => 100, :B => 50),
:holding_cost => Dict(:A => 0.003, :B => 0.002),
:demand_distribution => Dict(:A => 2, :B => 1),
:demand_frequency => Dict(:A => 1/2, :B => 1/3),
:demand_frequency => Dict(:A => 1, :B => 1),#Dict(:A => 1/2, :B => 1/3),
:sales_price => Dict(:A => 3, :B => 2),
:unfulfilled_penalty => Dict(:A => 0.01, :B => 0.01),
:supplier_priority => Dict(:A => 2, :B => 1)
:supplier_priority => Dict(:A => 1, :B => 1)
))
#specify sales prices, transportation costs, lead time
set_props!(net, 1, 2, Dict(
set_props!(net, 1, 1, Dict(
:transportation_cost => Dict(:A => 0.1), #production cost
:lead_time => Dict(:A => 3) #production lead time
))
set_props!(net, 1, 3, Dict(
:sales_price => Dict(:B => 1),
:transportation_cost => Dict(:B => 0.01),
:lead_time => Dict(:B => 7))
)
set_props!(net, 2, 3, Dict(
:sales_price => Dict(:A => 2),
:transportation_cost => Dict(:A => 0.01),
:lead_time => Dict(:A => 7))
set_props!(net, 1, 2, Dict(
:sales_price => Dict(:A => 2, :B => 1),
:transportation_cost => Dict(:A => 0.01, :B => 0.01),
:lead_time => Dict(:A => 7, :B => 7))
)

#define reorder policy parameters
policy_type = :sS #(s, S) policy
review_period = 1 #continuous review
s = Dict((3,:A) => 50, (3,:B) => 25, (2,:A) => 100) #lower bound on inventory
S = Dict((3,:A) => 100, (3,:B) => 50, (2,:A) => 125) #base stock level
s = Dict((2,:A) => 50, (2,:B) => 25, (1,:A) => 100) #lower bound on inventory
S = Dict((2,:A) => 100, (2,:B) => 50, (1,:A) => 125) #base stock level

#create environment and run simulation with reorder policy
policy_variable = :inventory_position
Expand All @@ -65,7 +54,7 @@ simulate_policy!(env, s, S; policy_variable, policy_type, review_period)
using DataFrames, StatsPlots
#profit
entity_profit = transform(env.profit,
:node => ByRow(i -> i in [1,2] ? "Plant" : "Retailer") => :entity
:node => ByRow(i -> i == 1 ? "Plant" : "Retailer") => :entity
)
profit = transform(groupby(entity_profit, :entity), :value => cumsum)
fig1 = @df profit plot(
Expand All @@ -74,8 +63,7 @@ fig1 = @df profit plot(
)

#inventory position
inventory_position = filter(i -> i.node == 1 ? i.material == :B : i.node == 2 ? i.material == :A : true, env.inventory_position)
fig2 = @df inventory_position plot(
fig2 = @df env.inventory_position plot(
:period, :level, group={Node = :node, Mat = :material}, linetype=:steppost,
legend = :bottomleft, xlabel="period", ylabel="inventory position", yticks = 0:25:125
)
1 change: 0 additions & 1 deletion examples/ex2.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#2-echelon system with variable demand

using Distributions
using InventoryManagement

Expand Down
1 change: 1 addition & 0 deletions src/InventoryManagement.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ using Reexport
using Random
using IntervalSets
using Distributions
using LinearAlgebra
import StatsBase: mean, std

@reexport using Graphs
Expand Down
13 changes: 5 additions & 8 deletions src/demand.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function place_orders!(x::SupplyChainEnv, act::NamedArray)
leads = Dict((a,mat) => rand(get_prop(x.network, a, :lead_time)[mat]) for a in edges(x.network), mat in mats)
servs = Dict((a,mat) => rand(get_prop(x.network, a, :service_lead_time)[mat]) for a in edges(x.network), mat in mats)
#identify nodes that can place requests
nodes = topological_sort_by_dfs(x.network) #sort nodes in topological order so that orders are placed moving down the network
nodes = topological_sort(x.network) #sort nodes in topological order so that orders are placed moving down the network
source_nodes = filter(n -> isempty(inneighbors(x.network, n)), nodes) #source nodes (can't place replenishment orders)
request_nodes = setdiff(nodes, source_nodes) #nodes placing requests (all non-source nodes)
#get on hand inventory
Expand All @@ -45,10 +45,10 @@ function place_orders!(x::SupplyChainEnv, act::NamedArray)
end
#create order and save service lead time
create_order!(x, a..., mat, amount, serv)
if !isproduced(x, sup, mat) #if material is not produced, try to fulfill from stock
fulfill_from_stock!(x, a..., mat, lead, supply_grp, pipeline_grp)
else #if material is produced, try to fulfill with production
if sup == req
fulfill_from_production!(x, a..., mat, lead, supply_grp, pipeline_grp, capacities)
else
fulfill_from_stock!(x, a..., mat, lead, supply_grp, pipeline_grp)
end
end
end
Expand All @@ -60,9 +60,6 @@ function place_orders!(x::SupplyChainEnv, act::NamedArray)
filter(:id => id -> id in expired_orders, x.orders, view=true),
[:arc, :fulfilled] => ByRow((a,log) -> vcat(log, (time=x.period, supplier=a[1], amount=:lost_sale))) => :fulfilled
)
# for order_id in expired_orders
# push!(x.orders[order_id,:fulfilled], (time=x.period, supplier=src, amount=:lost_sale)) #update fulfilled column in order history (date, supplier, amount fulfilled)
# end
filter!(:due => t -> t > 0, x.open_orders)
end

Expand Down Expand Up @@ -93,7 +90,7 @@ function create_order!(x::SupplyChainEnv, sup::Int, req::Int, mat::Union{Symbol,
x.num_orders += 1 #create new order ID
push!(x.orders, [x.num_orders, x.period, (sup,req), mat, amount, []]) #update order history
push!(x.open_orders, [x.num_orders, (sup,req), mat, amount, service_lead_time]) #add order to temp order df
if isproduced(x, sup, mat)
if sup == req && isproduced(x, sup, mat)
bom = get_prop(x.network, sup, :bill_of_materials)
rmat_names = names(filter(k -> k < 0, bom[:,mat]), 1) #names of raw materials
for rmat in rmat_names
Expand Down
4 changes: 0 additions & 4 deletions src/fulfillment.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,9 @@ function fulfill_from_production!(
row.quantity -= accepted_prod #update x.open_orders (deduct fulfilled quantity)
#consume reactant
for rmat in rmat_names
# required = order_amount * bom[rmat,mat] #negative number
consumed = accepted_prod * bom[rmat,mat] #negative number
# unfulfilled = -required + consumed
# push!(x.demand, [x.period, (src,src), rmat, -required, -consumed, missing, unfulfilled, missing]) #log demand for raw material (mark the arc as (src,src) and set lead time to missing to indicate production demand)
supply_grp[(node = src, material = rmat)].level[1] += consumed
raw_orders_grp[(id = row.id, material = rmat)].quantity[1] += consumed
# push!(x.demand, [x.period, (src,:production), rmat, -required, -consumed, lead, 0, missing]) #log demand for raw material (mark the arc as (src,:production))
end
#schedule coproduction
for cmat in cmat_names
Expand Down
10 changes: 6 additions & 4 deletions src/inventory.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,14 @@ function inventory_components(
backlog = 0 #initialize backlog for orders placed by successors
if x.options[:backlog]
backlog_window = x.options[:adjusted_stock] ? Inf : 0 #Inf means that all orders placed are counted (even if not due); otherwise, only due orders are counted
n_out = vcat(:production,n,outneighbors(x.network, n)) #backlog includes raw material conversion, market sales, downstream replenishments
n_out = vcat( #nodes accounted for in backlog
:production, #raw material conversion
setdiff(outneighbors(x.network, n), n), #downstream replenishments
n in x.markets ? n : [], #market sales
) #backlog includes raw material conversion, market sales, downstream replenishments
backlog = calculate_backlog(x,n,n_out,mat,backlog_window)
n_in = vcat(inneighbors(x.network, n)) #backorder includes previous replenishment orders placed to upstream nodes
n_in = inneighbors(x.network, n) #backorder includes previous replenishment orders placed to upstream nodes
backorder = calculate_backlog(x,n_in,n,mat,backlog_window)
# backlog = sum(filter(:arc => i -> i[1] == n, orders_grp[(material = mat,)], view=true).unfulfilled)
# backorder = sum(filter(:arc => i -> i[2] == n && i[2] != i[1], orders_grp[(material = mat,)], view=true).unfulfilled)
end
ilevel = onhand - backlog #inventory level
iorder = pipeline + backorder #inventory on order
Expand Down
4 changes: 2 additions & 2 deletions src/policy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function reorder_policy(env::SupplyChainEnv, reorder_point::Dict, policy_param::
!isvalid_period(env.period, review_period) && return null_action

#read parameters
nodes = reverse(topological_sort_by_dfs(env.network)) #sort nodes in reverse topological order so that orders are placed moving up the network
nodes = reverse(topological_sort(env.network)) #sort nodes in reverse topological order so that orders are placed moving up the network
source_nodes = filter(n -> isempty(inneighbors(env.network, n)), nodes) #source nodes (can't place replenishment orders)
sink_nodes = filter(n -> isempty(outneighbors(env.network, n)), nodes) #sink nodes (can't fulfill replenishment orders)
request_nodes = setdiff(nodes, source_nodes) #nodes placing requests (all non-source nodes)
Expand Down Expand Up @@ -150,7 +150,7 @@ Get expected raw material consumption at the producer node for downstream reques
"""
function get_expected_consumption(env::SupplyChainEnv, n::Int, mat::Union{Symbol,String}, action::NamedArray)
consume = 0
successors = outneighbors(env.network, n) #get successor nodes
successors = setdiff(outneighbors(env.network, n), n) #get successor nodes
if isconsumed(env, n, mat)
bom = get_prop(env.network, n, :bill_of_materials)
mprods = names(filter(i -> i < 0, bom[mat,:]), 1) #find which products are produced from mat
Expand Down
43 changes: 0 additions & 43 deletions src/spaces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,3 @@ end
function state_space(env::SupplyChainEnv)

end

# function action_space(env::SupplyChainEnv)
# num_products = length(env.materials)
# num_edges = ne(env.network)
# num_actions = num_products * num_edges
# ubound = []
# for n in vertices(env.network)
# set_prop!(env.network, n, :max_order, Dict(p => 0. for p in env.materials))
# end
# srcs = [n for n in nodes if isempty(inneighbors(network, n))]
# for (source, sink) in Iterators.product(srcs, env.markets)
# paths = yen_k_shortest_paths(env.network, source, sink, weights(env.network), typemax(Int)).paths
# #NOTE: TODO revise logic here if producers can be intermediate nodes!!!
# for path in paths
# capacity = get_prop(env.network, path[1], :production_capacity)
# for i in 2:length(path)
# top_max_order = get_prop(env.network, path[i-1], :max_order)
# if i > 2
# top_init_inv = get_prop(env.network, path[i-1], :initial_inventory)
# end
# max_order = get_prop(env.network, path[i], :max_order)
# for p in env.materials
# if i == 2
# max_order[p] += capacity[p]
# elseif i == 3
# max_order[p] += top_init_inv[p] + top_max_order[p] * env.num_periods
# else
# max_order[p] += top_init_inv[p] + top_max_order[p]
# end
# end
# set_prop!(env.network, path[i], :max_order, max_order)
# end
# end
# end
# for e in edges(env.network)
# max_order = get_prop(env.network, e.dst, :max_order)
# for p in env.materials
# push!(ubound, max_order[p])
# end
# end

# ClosedInterval.(zeros(num_actions), ubound)
# end
12 changes: 12 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,15 @@ function get_capacity_and_supply(

return vcat(capacity, mat_supply)
end

"""
topological_sort(net::MetaDiGraph)
Break self-loops and perform topological sort.
"""
function topological_sort(net::MetaDiGraph)
A = adjacency_matrix(net) #get adjacency_matrix
A[diagind(A)] .= 0 #remove diagonal (self-loops)
g = SimpleDiGraph(A) #create new graph without self-loops
return topological_sort_by_dfs(g)
end

2 comments on commit cbd7fe2

@hdavid16
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/61700

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.0 -m "<description of version>" cbd7fe2009267d1ebe550fff3ffef1482984b933
git push origin v0.5.0

Please sign in to comment.