Skip to content

Commit

Permalink
bug fixes; close #74
Browse files Browse the repository at this point in the history
  • Loading branch information
hdavid16 committed Jun 4, 2022
1 parent 99633b3 commit 824ab82
Show file tree
Hide file tree
Showing 12 changed files with 41 additions and 49 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,9 @@ The `SupplyChainEnv` Constructor has the following fields to store the simulatio

### Example 1

This example has plant that converts `:B` to `:A` with a 1:1 stoichiometry. The plant sells both materials to a downstream retailer that has market demand for both materials. This system is modeled using 3 nodes:
- Plant: Node 1 (stores `:B`) => Node 2 (stores `:A`)
- Retailer: Node 3 buys `:B` from Node 1 and `:A` from Node 2
This example has plant with unlimited raw material supply that converts `:B` to `:A` with a 1:1 stoichiometry. The plant sells both materials to a downstream retailer that has market demand for both materials. This system is modeled using 2 nodes:
- Plant: Node 1 with a self-loop for the production of `:A` from `:B`
- Retailer: Node 2 buys `:B` and `:A` from Node 1

Demand and lead times are deterministic. A continuous review (s,S) policy is used. 100 periods are simulated.

Expand All @@ -270,10 +270,10 @@ Demand and lead time is stochastic. A periodic review (r,Q) policy is used. 100

### Example 3

This example has plant that converts `:C` to `:B` to `:A` with a 1:1 stoichiometry for each reaction. The plant acquires raw materials from a supplier upstream with unlimited supply of `:C` and sells `:A` to a retailer downstream. There is direct market demand of `:A` at both the retailer and the plant. Thus, the plant has both internal and external demand. This system is modeled using 5 nodes:
This example has plant that converts `:C` to `:B` to `:A` with a 1:1 stoichiometry for each reaction. The plant acquires raw materials from a supplier upstream with unlimited supply of `:C` and sells `:A` to a retailer downstream. There is direct market demand of `:A` at both the retailer and the plant. Thus, the plant has both internal and external demand. This system is modeled using 3 nodes:
- Supplier: Node 1 (unlimited supply `:C`)
- Plant: Node 2 (stores raw `:C`) => Node 3 (stores intermediate `:B`) => Node 4 (stores product `:A` and sells it to both the retailer and the market)
- Retailer: Node 5 buys `:A` from Node 4 and sells `:A` to the market.
- Plant: Node 2 with a self-loop for production of `:B` from `:C` and `:A` from `:B`
- Retailer: Node 3 buys `:A` from Node 2 and sells `:A` to the market.

Demand and lead times are stochastic. A continuous review (s,S) policy is used. 100 periods are simulated.

Expand Down
Binary file modified docs/src/assets/ex1_position.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/src/assets/ex1_profit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/src/assets/ex3_echelon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 17 additions & 29 deletions examples/ex3.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ using InventoryManagement

##define network topology and materials involved:
# Node 1 = Raw material supplier
# Nodes 2-4 = Plant
# - Node 2 = Store raw C; Convert C => B
# - Node 3 = Store intermediate B; Convert B => A
# - Node 4 = Store product A (direct demand of A occurs here)
# Node 5 = Retail (direct demand of A occurs here)
net = MetaDiGraph(5)
# Nodes 2 = Plant
# Node 3 = Retail (direct demand of A occurs here)
net = MetaDiGraph(3)
connect_nodes!(net,
1 => 2,
1 => 2,
2 => 2, #production self-loop
2 => 3,
3 => 4,
4 => 5
)
set_prop!(net, :materials, [:A, :B, :C])

Expand All @@ -24,22 +20,14 @@ set_props!(net, 1, Dict(
:inventory_capacity => Dict(:A => 0, :B => 0, :C => Inf)
))
set_props!(net, 2, Dict(
:initial_inventory => Dict(:C => 215), #initial inventory at plant
:inventory_capacity => Dict(:A => 0, :B => 0, :C => Inf),
:bill_of_materials => Dict((:C,:B) => -1) #C => B
:initial_inventory => Dict(:A => 105, :B => 190, :C => 215), #initial inventory at plant
:inventory_capacity => Dict(:A => Inf, :B => Inf, :C => Inf),
:bill_of_materials => Dict((:C,:B) => -1, (:B,:A) => -1), #C => B; B => A
:demand_distribution => Dict(:A => Normal(5,1)), #demand
:demand_frequency => Dict(:A => 1), #order every other day on average,
:supplier_priority => Dict(:A => 2, :B => 2, :C => 1)
))
set_props!(net, 3, Dict(
:initial_inventory => Dict(:B => 190), #initial inventory at plant
:inventory_capacity => Dict(:A => 0, :B => Inf, :C => 0),
:bill_of_materials => Dict((:B,:A) => -1) #B => A
))
set_props!(net, 4, Dict(
:initial_inventory => Dict(:A => 105), #initial inventory at storage
:inventory_capacity => Dict(:A => Inf, :B => 0, :C => 0),
:demand_distribution => Dict(:A => Normal(5,1)), #demand
:demand_frequency => Dict(:A => 1) #order every other day on average
))
set_props!(net, 5, Dict(
:initial_inventory => Dict(:A => 60), #initial inventory at retail
:inventory_capacity => Dict(:A => Inf, :B => 0, :C => 0),
:demand_distribution => Dict(:A => Normal(10,1)), #demand
Expand All @@ -48,21 +36,21 @@ set_props!(net, 5, Dict(

##specify lead times
set_prop!(net, 1, 2, :lead_time, Dict(:C => Poisson(8)))
set_prop!(net, 2, 3, :lead_time, Dict(:B => Poisson(6)))
set_prop!(net, 3, 4, :lead_time, Dict(:A => Poisson(4)))
set_prop!(net, 4, 5, :lead_time, Dict(:A => Poisson(2)))
set_prop!(net, 2, 2, :lead_time, Dict(:B => Poisson(6), :A => Poisson(4))) #production times
set_prop!(net, 2, 3, :lead_time, Dict(:A => Poisson(2)))

##define reorder policy parameters
policy_type = :sS #(s, S) policy
review_period = 1 #continuous review
policy_variable = :echelon_stock #variable tracked by policy
s = Dict((2,:C) => 215, (3,:B) => 190, (4,:A) => 165, (5,:A) => 60) #lower bound on inventory
S = Dict((2,:C) => 215, (3,:B) => 190, (4,:A) => 165, (5,:A) => 60) #base stock level
s = Dict((2,:C) => 215, (2,:B) => 190, (2,:A) => 165, (3,:A) => 60) #lower bound on inventory
S = Dict((2,:C) => 215, (2,:B) => 190, (2,:A) => 165, (3,:A) => 60) #base stock level
centralized = true #centralized behavior (orders are adjusted based on downstream orders)

##create environment and run simulation with reorder policy
num_periods = 100
env = SupplyChainEnv(net, num_periods, backlog = true, reallocate = true)
simulate_policy!(env, s, S; policy_type, review_period, policy_variable)
simulate_policy!(env, s, S; policy_type, review_period, policy_variable, centralized)

##make plots
using DataFrames, StatsPlots
Expand Down
Binary file modified examples/figs/ex1_position.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/figs/ex1_profit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/figs/ex3_echelon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 4 additions & 5 deletions src/demand.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ function exit_place_orders!(x::SupplyChainEnv, arcs::Vector)
end

"""
create_order!(x::SupplyChainEnv, sup::Int, req::Int, mat::Union{Symbol,String}, amount::Real, service_lead_time::Float64)
create_order!(x::SupplyChainEnv, sup::Int, req::Union{Int,Symbol}, mat::Union{Symbol,String}, amount::Real, service_lead_time::Float64)
Create and log order.
"""
function create_order!(x::SupplyChainEnv, sup::Int, req::Int, mat::Union{Symbol,String}, amount::Real, service_lead_time::Float64)
function create_order!(x::SupplyChainEnv, sup::Int, req::Union{Int,Symbol}, mat::Union{Symbol,String}, amount::Real, service_lead_time::Float64)
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
Expand Down Expand Up @@ -137,7 +137,6 @@ function simulate_markets!(x::SupplyChainEnv)
dmnd_dist_dict = get_prop(x.network, n, :demand_distribution)
serv_dist_dict = get_prop(x.network, n, :service_lead_time)
for mat in x.materials
@assert !(n in x.producers && isproduced(x,n,mat)) "Node $n is marked as a market node and producer node, but $mat is produced at this node. The market should be associated with the product storage node of the plant (downstream node), not the raw material storage node."
p = dmnd_freq_dict[mat] #probability of demand occuring
dmnd = dmnd_dist_dict[mat] #demand distribution
serv_lt = serv_dist_dict[mat] #service lead time
Expand All @@ -160,6 +159,6 @@ Create external demand at node `n` for material `mat` for quantity `q` with serv
function external_order!(x::SupplyChainEnv, n::Int, mat::Union{Symbol,String}, q::Real, serv::Real)
supply_df = filter([:period,:node] => (t,n) -> t == x.period && n in x.markets, x.inventory_on_hand, view=true) #on_hand inventory at node
supply_grp = groupby(supply_df, [:node, :material]) #group by node and material
create_order!(x, n, n, mat, q, serv)
fulfill_from_stock!(x, n, n, mat, 0., supply_grp, missing) #0 lead time since at market; pipeline_grp is missing since no arc betwen market node and market
create_order!(x, n, :market, mat, q, serv)
fulfill_from_stock!(x, n, :market, mat, 0., supply_grp, missing) #0 lead time since at market; pipeline_grp is missing since no arc betwen market node and market
end
12 changes: 7 additions & 5 deletions src/fulfillment.jl
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
"""
fulfill_from_stock!(
x::SupplyChainEnv, src::Int, dst::Int, mat::Union{Symbol,String},
x::SupplyChainEnv, src::Int, dst::Union{Int,Symbol}, mat::Union{Symbol,String},
lead::Float64, supply_grp::GroupedDataFrame, pipeline_grp::Union{Missing,GroupedDataFrame}
)
Fulfill request from on-hand inventory.
"""
function fulfill_from_stock!(
x::SupplyChainEnv, src::Int, dst::Int, mat::Union{Symbol,String},
x::SupplyChainEnv, src::Int, dst::Union{Int,Symbol}, mat::Union{Symbol,String},
lead::Float64, supply_grp::GroupedDataFrame, pipeline_grp::Union{Missing,GroupedDataFrame}
)
#available supply
supply = supply_grp[(node = src, material = mat)].level[1]
#node from which to check early_fulfillment and partial_fulfillment param values
indicator_node = dst == :market ? src : dst

#loop through orders
if get_prop(x.network, dst, :early_fulfillment)[mat]
if get_prop(x.network, indicator_node, :early_fulfillment)[mat]
orders_df = filter([:arc, :material] => (a,m) -> a == (src,dst) && m == mat, x.open_orders, view = true)
else #only attempt to fulfill orders that have expired their service lead time
orders_df = filter([:arc, :material, :due] => (a,m,t) -> a == (src,dst) && m == mat && t <= 0, x.open_orders, view = true)
end
for row in eachrow(orders_df)
order_amount = row.quantity
if get_prop(x.network, dst, :partial_fulfillment)[mat]
if get_prop(x.network, indicator_node, :partial_fulfillment)[mat]
accepted_inv = min(order_amount, supply) #amount fulfilled from inventory
else
accepted_inv = order_amount <= supply ? order_amount : 0 #only accept full order or nothing at all
Expand All @@ -31,7 +33,7 @@ function fulfill_from_stock!(
push!(x.orders[row.id,:fulfilled], (time=x.period, supplier=src, amount=accepted_inv)) #update fulfilled column in order history (date, supplier, amount fulfilled)
push!(x.demand, [x.period, (src,dst), mat, order_amount, accepted_inv, lead, 0, missing]) #log demand
supply_grp[(node = src, material = mat)].level[1] -= accepted_inv #remove inventory from site
!ismissing(pipeline_grp) && make_shipment!(x, src, dst, mat, accepted_inv, lead, pipeline_grp) #ship material (unless it is external demand)
dst != :market && make_shipment!(x, src, dst, mat, accepted_inv, lead, pipeline_grp) #ship material (unless it is external demand)
end
if row.quantity > 0 && row.due <= 0 #if some amount of the order is due and wasn't fulfilled log it as unfulfilled & try to reallocate
log_unfulfilled_demand!(x, row, accepted_inv)
Expand Down
3 changes: 3 additions & 0 deletions src/parameters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,15 @@ function map_env_keys(nodes::Base.OneTo, arcs::Vector, mrkts::Vector, plants::Ve
arc_keys = [:sales_price, :unfulfilled_penalty, :transportation_cost, :pipeline_holding_cost, :lead_time, :service_lead_time]
all_market_keys = vcat(all_keys, market_keys)
all_plant_keys = vcat(all_keys, plant_keys)
all_market_plant_keys = vcat(all_keys, market_keys, plant_keys)
#list of nodes and arcs
mrkt_plants = mrkts plants
env_obj = vcat(nodes, arcs)
#assign keys to each object
env_keys = Dict(
obj =>
obj in nodes ?
obj in mrkt_plants ? all_market_plant_keys :
obj in mrkts ? all_market_keys :
obj in plants ? all_plant_keys :
all_keys :
Expand Down
8 changes: 4 additions & 4 deletions src/policy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
policy_type::Union{Dict, Symbol} = :rQ,
review_period::Union{Int, AbstractRange, Vector, Dict} = 1,
min_order_qty::Union{Real, Dict} = 0,
adjust_expected_consumption::Bool = false)
centralized::Bool = false)
Apply an inventory policy to specify the replinishment orders for each material
throughout the `SupplyChainEnv`.
Expand All @@ -15,14 +15,14 @@ Apply an inventory policy to specify the replinishment orders for each material
- `policy_type`: `:rQ` for an `(r,Q)` policy, or `:sS` for an `(s,S)` policy. If passing a `Dict`, the policy type should be specified for each node (keys).
- `review_period`: number of periods between each inventory review (Default = `1` for continuous review.). If a `AbstractRange` or `Vector` is used, the `review_period` indicates which periods the review is performed on. If a `Dict` is used, the review period should be specified for each `(node, material)` `Tuple` (keys). The values of this `Dict` can be either `Int`, `AbstractRange`, or `Vector`. Any missing `(node, material)` key will be assigned a default value of 1.
- `min_order_qty`: minimum order quantity (MOQ) at each supply node. If a `Dict` is passed, the MOQ should be specified for each `(node, material)` `Tuple` (keys). The values should be `Real`. Any missing key will be assigned a default value of 0.
- `adjust_expected_consumption`: should the system be assumed to be centralized (default: `false`)? If `true` then the upstream nodes know how much each downstream node is going to request and adjust the stock state to account for this.
- `centralized`: should the system be assumed to be centralized (default: `false`)? If `true` then the upstream nodes know how much each downstream node is going to request and adjust the stock state to account for this.
"""
function reorder_policy(env::SupplyChainEnv, reorder_point::Dict, policy_param::Dict;
policy_variable::Union{Dict,Symbol} = :inventory_position,
policy_type::Union{Dict, Symbol} = :rQ,
review_period::Union{Int, AbstractRange, Vector, Dict} = 1,
min_order_qty::Union{Real, Dict} = 0,
adjust_expected_consumption::Bool = false)
centralized::Bool = false)

#check review period; if not in review period, send null action
null_action = zeros(length(env.materials)*ne(env.network))
Expand Down Expand Up @@ -59,7 +59,7 @@ function reorder_policy(env::SupplyChainEnv, reorder_point::Dict, policy_param::
#review inventory at the node
state = get_inventory_state(n, mat, policy_variable, state1_grp, state2_grp)
#if n is a plant, adjust the inventory state by the order from the downstream node
if adjust_expected_consumption
if centralized
state += get_expected_consumption(env, n, mat, action)
end
#check if reorder is triggered & calculate quantity
Expand Down

2 comments on commit 824ab82

@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/61717

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.1 -m "<description of version>" 824ab82da23da0bb3ea3796c746ae25fd99ec7f7
git push origin v0.5.1

Please sign in to comment.