Skip to content

Commit

Permalink
new feature: add demand data or lead time data
Browse files Browse the repository at this point in the history
  • Loading branch information
hdavid16 committed Nov 25, 2022
1 parent 19a677a commit 08c9a60
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "InventoryManagement"
uuid = "2ad91f63-398d-4379-af6a-5a85689656d5"
authors = ["hdavid16 <[email protected]> and contributors"]
version = "0.6.1"
version = "0.6.2"

[deps]
Chain = "8be319e6-bccf-4806-a6f7-6fae938471bc"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ The graph metadata should have the following fields in its metadata:
- `:supplier_priority::Dict`: (*only when the node has at least 1 supplier*) `Vector` of suppliers (from high to low priority) for each material (`keys`). When a request cannot be fulfilled due to insufficient production capacity or on-hand inventory, the system will try to reallocate it to the supplier that is next in line on the priority list (if `reallocate = true`). Default = `inneighbors(SupplyChainEnv.network, node)`.
- `:demand_distribution::Dict`: probability distributions from [Distributions.jl](https://github.com/JuliaStats/Distributions.jl) for the market demands for each material (`keys`). For deterministic demand, instead of using a probability distribution, use `D where D <: Number`. Default = `0`.
- `:demand_frequency::Dict`: number of times demand occurs per period on average for each material (`keys`). Default = `1`.
- `:demand_data::Dict`: Vector of orders for each period in the simulation, for each material (`keys`). For each period, a single order amount can be specified, or a `Tuple`/`Vector` of orders amounts. Demand data takes precedence over any demand distribution/demand frequency provided. Default = `nothing`.
- `:sales_price::Dict`: market sales price for each material (`keys`). Default = `0`.
- `:unfulfilled_penalty::Dict`: unit penalty for unsatisfied market demand for each material (`keys`). Default = `0`.
- `:service_lead_time::Dict`: service lead time (probability distribution or deterministic value) allowed to fulfill market demand for each material (`keys`). Default = `0`.
Expand All @@ -214,6 +215,7 @@ All arcs have the following fields in their metadata:
- `:pipeline_holding_cost::Dict`: unit holding cost per period for inventory in-transit for each material (`keys`). Default = `0`.
- `:unfulfilled_penalty::Dict`: unit penalty for unsatisfied internal demand for each material (`keys`). Default = `0`.
- `:lead_time::Dict`: probability distributions from [Distributions.jl](https://github.com/JuliaStats/Distributions.jl) for the lead times for each material (`keys`) on that edge. Lead times are transportation times when the edge has two `distributor` nodes and production times when the edge joins the `producer` and `distributor` nodes in a plant. For deterministic lead times, instead of using a probability distribution, use `L where L <: Number`. Default = `0`.
- `:lead_time_data::Dict`: Vector of lead times with a non-negative value for each period in the simulation, for each material (`keys`) on that edge. Lead time data takes precedence over any lead time distribution/value. Default = `nothing`.
- `:service_lead_time::Dict`: service lead time (probability distribution or deterministic value) allowed to fulfill (goods issue) internal demand for each material (`keys`). Default = `0`.

## Creating a Supply Chain Environment
Expand Down
2 changes: 2 additions & 0 deletions docs/src/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ The graph metadata should have the following fields in its metadata:
- `:supplier_priority::Dict`: (*only when the node has at least 1 supplier*) `Vector` of suppliers (from high to low priority) for each material (`keys`). When a request cannot be fulfilled due to insufficient production capacity or on-hand inventory, the system will try to reallocate it to the supplier that is next in line on the priority list (if `reallocate = true`). Default = `inneighbors(SupplyChainEnv.network, node)`.
- `:demand_distribution::Dict`: probability distributions from [Distributions.jl](https://github.com/JuliaStats/Distributions.jl) for the market demands for each material (`keys`). For deterministic demand, instead of using a probability distribution, use `D where D <: Number`. Default = `0`.
- `:demand_frequency::Dict`: number of times demand occurs per period on average for each material (`keys`). Default = `1`.
- `:demand_data::Dict`: Vector of orders for each period in the simulation, for each material (`keys`). For each period, a single order amount can be specified, or a `Tuple`/`Vector` of orders amounts. Demand data takes precedence over any demand distribution/demand frequency provided. Default = `nothing`.
- `:sales_price::Dict`: market sales price for each material (`keys`). Default = `0`.
- `:unfulfilled_penalty::Dict`: unit penalty for unsatisfied market demand for each material (`keys`). Default = `0`.
- `:service_lead_time::Dict`: service lead time (probability distribution or deterministic value) allowed to fulfill market demand for each material (`keys`). Default = `0`.
Expand All @@ -96,6 +97,7 @@ All arcs have the following fields in their metadata:
- `:pipeline_holding_cost::Dict`: unit holding cost per period for inventory in-transit for each material (`keys`). Default = `0`.
- `:unfulfilled_penalty::Dict`: unit penalty for unsatisfied internal demand for each material (`keys`). Default = `0`.
- `:lead_time::Distribution{Univariate, Discrete}`: probability distributions from [Distributions.jl](https://github.com/JuliaStats/Distributions.jl) for the lead times for each material (`keys`) on that edge. Lead times are transportation times when the edge has two `distributor` nodes and production times when the edge joins the `producer` and `distributor` nodes in a plant. For deterministic lead times, instead of using a probability distribution, use `L where L <: Number`. Default = `0`.
- `:lead_time_data::Dict`: Vector of lead times with a non-negative value for each period in the simulation, for each material (`keys`) on that edge. Lead time data takes precedence over any lead time distribution/value. Default = `nothing`.
- `:service_lead_time::Dict`: service lead time (probability distribution or deterministic value) allowed to fulfill internal demand for each material (`keys`). Default = `0`.

## Creating a Supply Chain Environment
Expand Down
4 changes: 2 additions & 2 deletions examples/ex1.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ set_props!(net, 1, 1, Dict(
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))
)
:lead_time => Dict(:A => 7, :B => 7)
))

#define reorder policy parameters
policy_type = :sS #(s, S) policy
Expand Down
37 changes: 25 additions & 12 deletions src/demand.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function replenishment_orders!(x::SupplyChainEnv, act::NamedArray)
#store original production capacities (to account for commited capacity and commited inventory in next section)
capacities = Dict(n => get_prop(x.network, n, :production_capacity) for n in x.producers)
#sample lead times and service times
leads = Dict((a,mat) => ceil(Int,rand(get_prop(x.network, a, :lead_time)[mat])) for a in edges(x.network), mat in mats)
leads = Dict((a,mat) => get_lead_time(x,a,mat) for a in edges(x.network), mat in mats)
servs = Dict((a,mat) => ceil(Int,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
supplier_nodes = filter( #nodes supplying inventory
Expand Down Expand Up @@ -264,21 +264,30 @@ function simulate_markets!(x::SupplyChainEnv)
for n in x.markets
dmnd_freq_dict = get_prop(x.network, n, :demand_frequency)
dmnd_dist_dict = get_prop(x.network, n, :demand_distribution)
dmnd_data_dict = get_prop(x.network, n, :demand_data)
serv_dist_dict = get_prop(x.network, n, :service_lead_time)
n_mats = get_prop(x.network, n, :node_materials)
for mat in n_mats
#get demand parameters
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
last_order_id = x.num_orders #last order created in system
#place p orders
for _ in 1:floor(p) + rand(Bernoulli(p % 1)) #p is the number of orders. If fractional, the fraction is the likelihood of rounding up.
q = rand(dmnd) #sample demand
slt = ceil(Int,rand(serv_lt)) #sample service lead time
if q > 0
dmnd_data = dmnd_data_dict[mat] #demand data
if !isnothing(dmnd_data)
for q in dmnd_data[x.period]
slt = ceil(Int,rand(serv_lt)) #sample service lead time
create_order!(x, n, :market, mat, q, slt)
end
else
p = dmnd_freq_dict[mat] #probability of demand occuring
dmnd = dmnd_dist_dict[mat] #demand distribution
#place p orders
for _ in 1:floor(p) + rand(Bernoulli(p % 1)) #p is the number of orders. If fractional, the fraction is the likelihood of rounding up.
q = rand(dmnd) #sample demand
slt = ceil(Int,rand(serv_lt)) #sample service lead time
if q > 0
create_order!(x, n, :market, mat, q, slt)
end
end
end
#if no new orders were created, check if there are any pending orders; if not, the exit iteration
if last_order_id == x.num_orders
Expand All @@ -292,10 +301,14 @@ function simulate_markets!(x::SupplyChainEnv)
#if MTO and lead time is 0, try to fulfill from production
if last_order_id > x.num_orders && ismto(x,n,mat) #fulfill make-to-order from production if at least 1 new MTO created (NOTE: COULD RECHECK IF THERE IS ANY PENDING ORDER THAT WASN"T FULFILLED FROM STOCK)
lt_dist = get_prop(x.network, n, n, :lead_time)[mat]
if iszero(lt_dist)
capacities = get_prop(x.network, n, :production_capacity)
prod_lt = ceil(Int,rand(lt_dist))
fulfill_from_production!(x, n, :market, mat, prod_lt, capacities)
lt_data = get_prop(x.network, n, n, :lead_time_data)[mat]
capacities = get_prop(x.network, n, :production_capacity)
if !isnothing(lt_data)
if iszero(lt_data[x.period])
fulfill_from_production!(x, n, :market, mat, 0, capacities)
end
elseif iszero(lt_dist)
fulfill_from_production!(x, n, :market, mat, 0, capacities)
end
end
end
Expand Down
3 changes: 1 addition & 2 deletions src/environment.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function SupplyChainEnv(
net = copy(network)
#check inputs
mrkts, plants = identify_nodes(net) #identify nodes
check_inputs!(net, mrkts, plants)
check_inputs!(net, mrkts, plants, num_periods)
#get model parameters
nodes = vertices(net) #network nodes
echelons = Dict(n => identify_echelons(net, n) for n in nodes) #get nodes in each echelon
Expand All @@ -90,7 +90,6 @@ function SupplyChainEnv(
logging_dfs = create_logging_dfs(net, tmp)
#initialize other params
period, reward, num_orders = 0, 0, 0
num_periods = num_periods
options = Dict(
:backlog => guaranteed_service ? true : backlog, #override backlog if guaranteed_service
:reallocate => reallocate,
Expand Down
28 changes: 19 additions & 9 deletions src/parameters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Identify market and producer nodes in the network.
function identify_nodes(net::MetaDiGraph)
nodes = vertices(net)
#get end distributors, producers, and distribution centers
market_keys = [:demand_distribution, :demand_frequency, :sales_price, :unfulfilled_penalty] #keys to identify a market
market_keys = [:demand_distribution, :demand_frequency, :demand_data, :sales_price, :unfulfilled_penalty] #keys to identify a market
plant_keys = [:bill_of_materials, :production_capacity, :make_to_order] #keys to identify a plant (producer)
mrkts = [n for n in nodes if !isempty(intersect(market_keys, keys(props(net,n))))]
plants = [n for n in nodes if !isempty(intersect(plant_keys, keys(props(net,n))))]
Expand All @@ -33,11 +33,11 @@ function identify_echelons(network::MetaDiGraph, n::Int)
end

"""
check_inputs!(network::MetaDiGraph, mrkts::Vector, plants::Vector)
check_inputs!(network::MetaDiGraph, mrkts::Vector, plants::Vector, num_periods::Int)
Check inputs when creating a `SupplyChainEnv`.
"""
function check_inputs!(network::MetaDiGraph, mrkts::Vector, plants::Vector)
function check_inputs!(network::MetaDiGraph, mrkts::Vector, plants::Vector, num_periods::Int)

nodes = vertices(network) #network nodes
arcs = [(e.src,e.dst) for e in edges(network)] #network edges as Tuples (not Edges)
Expand All @@ -58,7 +58,9 @@ function check_inputs!(network::MetaDiGraph, mrkts::Vector, plants::Vector)
elseif key == :make_to_order
check_make_to_order!(network, obj)
else
!(key in keys(props(network, obj...))) && set_prop!(network, obj..., key, Dict()) #create empty params for object if not specified
if !(key in keys(props(network, obj...)))
set_prop!(network, obj..., key, Dict()) #create empty params for object if not specified
end
param_dict = get_prop(network, obj..., key) #parameter dictionary
for mat in mats
#set defaults
Expand All @@ -73,6 +75,8 @@ function check_inputs!(network::MetaDiGraph, mrkts::Vector, plants::Vector)
param_dict = check_customer_priority(network, obj, mat)
elseif key in [:demand_distribution, :lead_time, :service_lead_time]
param_dict, replace_flag, truncate_flag, roundoff_flag = check_stochastic_variable!(network, key, obj, mat)
elseif key in [:demand_data, :lead_time_data]
!isnothing(param) && @assert length(param) == num_periods && all(union(param...) .>= 0) "A valid (non-negative) data point must be provided for every period in the simulation for $key at $obj."
else
@assert param isa Real && param >= 0 "Parameter $key for material $mat at $obj must be a non-negative `Real`."
end
Expand All @@ -93,9 +97,9 @@ Create a dictionary with the parameter keys for each node/arc in the network
function map_env_keys(nodes::Base.OneTo, arcs::Vector, mrkts::Vector, plants::Vector, nonsources::Vector, nonsinks::Vector)
#lists of parameter keys
all_keys = [:initial_inventory, :inventory_capacity, :holding_cost]
market_keys = [:demand_distribution, :demand_frequency, :sales_price, :unfulfilled_penalty, :service_lead_time, :market_partial_fulfillment, :market_early_fulfillment]
market_keys = [:demand_distribution, :demand_frequency, :demand_data, :sales_price, :unfulfilled_penalty, :service_lead_time, :market_partial_fulfillment, :market_early_fulfillment]
plant_keys = [:bill_of_materials, :production_capacity, :make_to_order]
arc_keys = [:sales_price, :unfulfilled_penalty, :transportation_cost, :pipeline_holding_cost, :lead_time, :service_lead_time]
arc_keys = [:sales_price, :unfulfilled_penalty, :transportation_cost, :pipeline_holding_cost, :lead_time, :lead_time_data, :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)
Expand Down Expand Up @@ -207,6 +211,8 @@ function set_default!(network::MetaDiGraph, key::Symbol, obj::Union{Int, Tuple},
set_prop!(network, obj, key, merge(param_dict, Dict(mat => Inf)))
elseif key == :demand_frequency #demand at every period
merge!(param_dict, Dict(mat => 1))
elseif key in [:demand_data, :lead_time_data] #none provided
set_prop!(network, obj..., key, merge(param_dict, Dict(mat => nothing)))
elseif key == :supplier_priority #random ordering of supplier priority
merge!(param_dict, Dict(mat => inneighbors(network, obj) |> pred -> obj in pred ? [obj] pred : pred)) #if producer, set itself as first supplier priority
elseif key == :customer_priority #random ordering of customer priority
Expand Down Expand Up @@ -341,14 +347,18 @@ end
"""
store_arc_materials!(network::MetaDiGraph)
Sore the materials that have a specified lead time on each arc.
Store the materials that have a specified lead time on each arc.
"""
function store_arc_materials!(network::MetaDiGraph)
materials = get_prop(network, :materials)
for e in edges(network)
arc_mats = Set()
if :lead_time in keys(props(network, e))
arc_mats = Set(keys(get_prop(network, e, :lead_time)))
set_prop!(network, e, :arc_materials, arc_mats materials)
union!(arc_mats, keys(get_prop(network, e, :lead_time)))
end
if :lead_time_data in keys(props(network, e))
union!(arc_mats, keys(get_prop(network, e, :lead_time_data)))
end
set_prop!(network, e, :arc_materials, arc_mats materials)
end
end
14 changes: 14 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,18 @@ function material_graph(bill_of_materials::NamedArray)
end

return g
end

"""
get_lead_time(x::SupplyChainEnv, arc::Edge, mat::Material)
"""
function get_lead_time(x::SupplyChainEnv, arc::Edge, mat::Material)
lt_data = get_prop(x.network, arc, :lead_time_data)[mat]
if !isnothing(lt_data)
lt = lt_data[x.period]
else
lt = rand(get_prop(x.network, arc, :lead_time)[mat])
end

return ceil(Int,lt)
end

2 comments on commit 08c9a60

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

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.6.2 -m "<description of version>" 08c9a6024a4a7ea6f88cea477bf70efcbd1306cf
git push origin v0.6.2

Please sign in to comment.