Discrete-time simulation environment for Inventory Management in Supply Chain Networks.
- Overview
- Dependencies
- Installation
- Sequence of Events
- Inventory replenishment policies
- Model Assumptions
- Model Limitations
- Creating a Supply Chain Network
- Creating a Supply Chain Environment
- Simulation Outputs
- Examples
- Contact
InventoryManagement.jl allows modeling a multi-period multi-product supply chain network under stochastic stationary demand and stochastic lead times. A supply network can be constructed using the following types of nodes:
Producers
: Nodes where inventory transformation takes place (e.g., raw materials are converted to intermediates or finished goods). Material transformation, including reactive systems with co-products, are modeled using Bills of Materials (see Model Inputs section).Distributors
: Nodes where inventory is stored and distributed (e.g., distribution centers).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 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 usingdistributor
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).
The simplest network that can be modeled is one with a single retailer (distributor
) with external demand (market
) that is supplied by a warehouse (distributor
). However, more complex systems can be modelled as well.
When defining a supply network, a SupplyChainEnv
object is created based on system inputs and network structure. This object can then be used to simulate the inventory dynamics under a stochastic environment and a specified inventory management policy. Stochasticity is modeled in the external demand quantities at the market
nodes and in the lead times between connected nodes in the network. However, deterministic values can also be used for external demand or lead times if desired. In each period of the simulation, a decision-maker can specify inventory replenishnment orders throughout the network (refered to as actions
), which consist of the inventory quantities requested by each node to each immediate supplier for each material in the system. If no action is taken during the simulation, the inventory levels will eventually be depleted by the external demand. Depending on the system configuration, unfulfilled external or internal demand can be either backlogged or considered a lost sale.
The SupplyChainEnv
can also potentially be used in conjunction with ReinforcementLearning.jl to train a Reinforcement Learning agent
that places replenishment orders (actions
) throughout the network.
This package generalizes and extends and the inventory management environment available in OR-Gym.
InventoryManagement.jl relies primarily on the following packages:
- DataFrames.jl: Tabulate results.
- Distributions.jl: Define probability distributions for the lead times on the network arcs and the demand quantities at the market nodes.
- Graphs.jl: Define supply network topology
- MetaGraphs.jl: Specify system parameters for each node or arc, or for the system as a whole.
The package can be installed with the Julia package manager. From the Julia REPL, type ]
to enter the Pkg
REPL mode and run:
pkg> add InventoryManagement
For the master branch, run:
pkg> add https://github.com/hdavid16/InventoryManagement.jl
The sequence of events in each period of the simulation is patterned after that of the News Vendor Problem:
- Start period.
- Receive any incoming shipments from upstream nodes.
- Place inventory replenishment orders at each node by traversing the supply network downstream (using topological sorting).
- If
backlog = true
, the previous period's backlog is added to the replenishment order. - Each supplier attempts to fulfill downstream orders via its on-hand inventory.
Producer
nodes fulfill production requests via material production (if there is sufficientproduction capacity
andraw material inventory
). - If
reallocate = true
, then any amount that cannot be satisfied is reallocated to the next supplier in thesupplier priority
list (the lowest priority supplier will reallocate back to the highest priority supplier). - Accepted replenishment orders are immediately shipped with a lead time sampled from the specified distribution. For
distributor
nodes, the lead time is the in-transit (transportation) time betweendistributor
nodes. Forproducer
nodes, the lead time is the plant production time. - If the lead time is 0, the stock will be immediately available to the requesting node so that it can be used to fulfill downstream orders as they arrive (possible due to the topological sorting).
- Market (external) demand for each material occurs after tossing a weighted coin with the probability of demand occurring defined by the
demand_frequency
(likelihood of demand occuring in each period; inverse of the average number of periods between positive external demands).
- For example, if
demand_requency = 0.5
, there is a50%
chance of having positive demand, or once every 2 days on average.
- Demand (including any backlog if
backlog = true
) is fulfilled up to available inventory at themarket
nodes. Make-to-order nodes trigger production requests. - Unfulfilled demand is backlogged (if
backlog = true
). - Accounts for each node are generated:
- Accounts payable: invoice for fulfilled replenishment orders (payable to suppliers), invoice for delivered replenishment orders (payable to third-party shipper), pipeline inventory holding cost for in-transit inventory (cost to requestor), on-hand inventory holding cost, penalties for unfulfilled demand (cost to supplier).
- Accounts receivable: sales for internal and external demand.
At each iteration in the simulation, an action
can be provided to the system, which consists of the replenishment orders placed on every link in the supply network. This action
must be of type Vector{Real}
and must be nonnegative
of the form: [Arc_1_Material_1, Arc_1_Material_2, ..., Arc_1Material_M, Arc_2_Material_1, ..., Arc_2_Material_M, ..., Arc_A_Material_1, ..., Arc_A_Material_M]
, where the ordering in the arcs is given by edges(SupplyChainEnv.network)
and the ordering in the materials by SupplyChainEnv.materials
.
An action
vector can be visualized as a NamedArray
using show_action(SupplyChainEnv, action)
:
material ╲ arc │ :Arc_1 :Arc_2 ... :Arc_A
───────────────┼──────────────────────────
:Material_1 │
:Material_2 │
... │
:Material_M │
The function reorder_policy
can be used to implement an inventory reorder policy at each node based its inventory position or echelon stock. Reorder quantities are placed to the node's priority supplier. The reorder policy is applied for each material
at each node
in reverse topological order. This allows upstream nodes to determine their reorder quantities with information about the reorder quantities placed by their successors (relevant for producer
nodes to ensure that raw material replenishments are synced with production orders). The two most common policies used in industry are the (s,S)
and (r,Q)
policies.
The reorder_policy
takes the following inputs and returns an action
vector.
env::SupplyChainEnv
: inventory management environmentreorder_point::Dict
: thes
orr
parameter in each node for each material in the system. Thekeys
are of the form(node, material)
.policy_param::Dict
: theS
orQ
parameter in each node for each material in the system. Thekeys
are of the form(node, material)
.policy_type::Union{Symbol, Dict}
::rQ
for an(r,Q)
policy, or:sS
for an(s,S)
policy. If passing aDict
, the policy type should be specified for each node (keys
).review_period::Union{Int, AbstractRange, Vector, Dict}
: number of periods between each inventory review (Default =1
for continuous review.). If aAbstractRange
orVector
is used, thereview_period
indicates which periods the review is performed on. If aDict
is used, the review period should be specified for each(node, material)
Tuple
(keys
). The values of thisDict
can be eitherInt
,AbstractRange
, orVector
. Any missing(node, material)
key will be assigned a default value of 1.min_order_qty::Union{Real, Dict}
: minimum order quantity (MOQ) at each supply node. If aDict
is passed, the MOQ should be specified for each(node, material)
Tuple
(keys). The values should beReal
. Any missing key will be assigned a default value of 0.order_multiples::Union{Real, Dict}
: size increments for each order (default is -1, which means no constraint on order sizes). If aDict
is passed, the order multiples should be specified for each(node, material)
Tuple
(keys). The values should beReal
. Any missing key will be assigned a default value of -1 (meaning no order multiples enforced).adjust_expected_consumption::Bool
: indicator if the reorder point should be increased (temporarilly) at aproducer
node by the expected raw material consumption for an expected incoming production order.
The following assumptions hold in the current implementation, but can be modified in future releases.
- Production lead times are independent of the amount being produced.
- Transportation costs are paid to a third party (not a node in the network).
- Replenishment orders are placed in topological order. This means that upstream nodes place orders first. This allows the following scenario: if the lead time is 0, the ordered inventory will immediately be available so that the node can use it to fulfill downstream orders.
The following features are not currently supported:
- Alternate bills of materials (see Model Inputs) for the same material at the same node are not currently supported. This is particularly relevant for chemical systems (e.g., there are two reactions that can be used to make the same product).
- Capacity limitations on shared feedstock inventory among
producer
nodes (e.g., shared inventory tanks) are not enforced in a straightforward way sinceproducer
nodes have dedicated raw material storage. Shared feedstock inventory can be modeled by having an upstream storage node with zero lead time to each of theproducer
nodes. Each of theproducer
nodes should use an(s,S)
replenishment policy withs = 0, S = 0
. When a production order is of sizex
is going to be placed in a period, the policy will assume the feedstock position at theproducer
node is going to derop to-x
and will orderx
to bring the position up to0
. Since the lead time is0
and orders are placed with topological sorting (see Sequence of Events), the inventory will be immediately sent to theproducer
node and be available for when the production order comes in. However, if there is not enough production capacity to processx
, the excess will be left in the dedicated storage for thatproducer
node, making it possible to violate the shared inventory capacity constraint. - If a
producer
can produce more than 1 material, it is possible for it to produce all materials it is capable of producing simultaneously (if there are enough raw materials). This occurs because the model does not account for resource constraints (e.g., single reactor can only do reaction 1 or reaction 2, but not both simultaneously). However, these can be enforced manually with the reorder actions. Potential fixes (requires changing the source code):- Drop production capacities for all other products to 0 when the production equipment is occupied. Requires modeling each production unit as its own node.
- Develop a production model (perhaps based on the Resource-Task Network paradigm)
The supply network topology must be mapped on a network graph using Graphs.jl. The system parameters are stored in the network's metadata using MetaGraphs.jl. The network can be generated by using the MetaDiGraph
function and passing one of the following:
- Number of nodes in the network, which can the be connected via the
connect_nodes!
function:
net = MetaDiGraph(3)
connect_nodes!(net,
1 => 2,
2 => 3
)
- An adjacency matrix for the network:
net = MetaDiGraph(
[0 1 0;
0 0 1;
0 0 0]
)
- An existing
DiGraph
, such as a serial directed graph (path_digraph
):
net = MetaDiGraph(path_digraph(3))
General network, node-specific, and arc-specific metadata can be added using the set_prop!
and set_props!
functions. The following subsections describe the property keys accepted for the supply chain network metadata.
The graph metadata should have the following fields in its metadata:
:materials::Vector
with a list of all materials in the system.
Producers
will have the following fields in their node metadata:
:initial_inventory::Dict
: initial inventory for each material (keys
). Default =0
.:inventory_capacity::Dict
: maximum inventory for each material (keys
). Default =Inf
.:holding_cost::Dict
: unit holding cost for each material (keys
). Default =0
.:early_fulfillment::Dict
: (only when the node has at least 1 supplier) (true
/false
) on if the node accepts orders being fulfilled before their due date for each material (keys
). Default =true
.:partial_fulfillment::Dict
: (only when the node has at least 1 supplier) (true
/false
) on if the node accepts orders being fulfilled partially for each material (keys
). Default =true
.: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 (ifreallocate = true
). Default =[node] ∪ inneighbors(SupplyChainEnv.network, node)
.:customer_priority::Dict
: (only when the node has at least 1 customer)Vector
of customers (from high to low priority) for each material (keys
). Downstream requests are fulfilled by this prioritization (after giving priority first to due and then expired orders). Default =outneighbors(SupplyChainEnv.network, node)
.:production_capacity::Dict
: maximum production capacity for each material (keys
). Default =Inf
.:make_to_order::Vector
: list of materials that are make-to-order. Default =[]
.:bill_of_materials::Union{Dict,NamedArray}
:keys
are materialTuples
, where the first element is the input material and the second element is the product/output material; thevalues
indicate the amount of input material consumed to produce 1 unit of output material. Alternatively, aNamedArray
can be passed where the input materials are the rows and the output materials are the columns. The following convention is used for the bill of material (BOM) values:zero
: input not involved in production of output.negative number
: input is consumed in the production of output.positive number
: input is a co-product of the output.
Distributors
will have the following fields in their node metadata:
:initial_inventory::Dict
: initial inventory for each material (keys
). Default =0
.:inventory_capacity::Dict
: maximum inventory for each material (keys
). Default =Inf
.:holding_cost::Dict
: unit holding cost for each material (keys
). Default =0
.:early_fulfillment::Dict
: (only when the node has at least 1 supplier) (true
/false
) on if the node accepts orders being fulfilled before their due date for each material (keys
). Default =true
.:partial_fulfillment::Dict
: (only when the node has at least 1 supplier) (true
/false
) on if the node accepts orders being fulfilled partially for each material (keys
). Default =true
.: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 (ifreallocate = true
). Default =inneighbors(SupplyChainEnv.network, node)
.:customer_priority::Dict
: (only when the node has at least 1 customer)Vector
of customers (from high to low priority) for each material (keys
). Downstream requests are fulfilled by this prioritization (after giving priority first to due and then expired orders). Default =outneighbors(SupplyChainEnv.network, node)
.
Markets
will have the following fields in their node metadata:
:initial_inventory::Dict
: initial inventory for each material (keys
). Default =0
.:inventory_capacity::Dict
: maximum inventory for each material (keys
). Default =Inf
.:holding_cost::Dict
: unit holding cost for each material (keys
). Default =0
.:early_fulfillment::Dict
: (only when the node has at least 1 supplier) (true
/false
) indicates if the node accepts orders being fulfilled before their due date for each material (keys
). Default =true
.:market_early_fulfillment::Dict
: (true
/false
) indicates if the market accepts orders being fulfilled before their due date for each material (keys
). Default =true
.:partial_fulfillment::Dict
: (only when the node has at least 1 supplier) (true
/false
) indicates if the node accepts orders being fulfilled partially for each material (keys
). Default =true
.:market_partial_fulfillment::Dict
: (true
/false
) indicates if the market accepts orders being fulfilled partially for each material (keys
). Default =true
.: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 (ifreallocate = true
). Default =inneighbors(SupplyChainEnv.network, node)
.:demand_distribution::Dict
: probability distributions from Distributions.jl for the market demands for each material (keys
). For deterministic demand, instead of using a probability distribution, useD 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 aTuple
/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
.
All arcs have the following fields in their metadata:
:sales_price::Dict
: unit sales price for inventory sent on that edge (from supplier to receiver) for each material (keys
). Default =0
.:transportation_cost::Dict
: unit transportation cost for shipped inventory for each material (keys
). Default =0
.: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 for the lead times for each material (keys
) on that edge. Lead times are transportation times when the edge has twodistributor
nodes and production times when the edge joins theproducer
anddistributor
nodes in a plant. For deterministic lead times, instead of using a probability distribution, useL 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
.
The SupplyChainEnv
function can be used to create a SupplyChainEnv
Constructor.
This function takes the following inputs:
- Positional Arguments:
Network::MetaDiGraph
: supply chain network with embedded metadatanum_periods::Int
: number of periods to simulate
- Keyword Arguments (system options):
backlog::Bool = true
: backlogging allowed iftrue
; otherwise, orders that reach their due date and are not fulfilled become lost sales.reallocate::Bool = false
: the system try to reallocate requests if they cannot be satisfied iftrue
; otherwise, no reallocation is attempted.guaranteed_service::Bool = false
: the simulation will operate under the assumptions in the Guaranteed Service Model (GSM). Iftrue
,backlog = true
will be forced. Orders that are open and within the service lead time window will be backlogged. Once the service lead time expires, the orders become lost sales. In order to replicate the GSM assumption that extraordinary measures will be used to fulfill any expired orders, a dummy node with infinite supply can be attached to each node and set as the lowest priority supplier to that node.adjusted_stock::Bool = true
: the simulation will discount orders that have been placed, but have not been fulfilled from the inventory position (and echelon stock) from the supplier node, and will add them to the on-order inventory of the requested node.capacitated_inventory::Bool = true
: the simulation will enforce inventory capacity constraints by discarding excess inventory at the end of each period iftrue
; otherwise, the system will allow the inventory to exceed the specified capacity.evaluate_profit::Bool = true
: the simulation will calculate the proft at each node iftrue
and save the results inSupplyChainEnv.profit
.
- Aditional Keyword Arguments:
discount::Float64 = 0.
: discount factor (i.e., interest rate) to account for the time-value of money.numerical_precision::Int = 6
: Numerical precision (number of digits) for external demand sampling.seed::Int = 0
: random seed for simulation.
The SupplyChainEnv
Constructor has the following fields to store the simulation results in DataFrames
:
inventory
: on-hand, level, position, echelon, pipeline, and discarded inventory for eachlocation
(arc
ornode
),material
, andperiod
. Discarded inventory is marked whencapacitated_inventory = true
.orders
: internal and external orders for eachmaterial
on eacharc
(for internal demand) and eachnode
(for external demand). The ID, creation date, and quantity are stored for each order.open_orders
: open (not yet fulfilled) internal and external orders for eachmaterial
on eacharc
(for internal demand) and eachnode
(for external demand). The ID, creation date, and quantity are stored for each open order. Thedue
column indicates the time left until the order is due (as specified by theservice_lead_time
).fulfillments
: order fulfillment quantities for each order ID. Thetype
column indicates if the amount on that row is materialsent
,delivered
, or is alost sale
.shipments
: current in-transit inventory for eacharc
andmaterial
with remaining lead time.profit
: time-discounted profit for eachnode
at eachperiod
.metrics
: service metrics (service level and fillrate) on eacharc
for eachmaterial
.
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.
See code with system and policy parameters here.
This example has a distributor with unlimited inventory (Node 1) that sells :A
to a retailer with market demand (Node 2).
Demand and lead time is stochastic. A periodic review (r,Q) policy is used. 100 periods are simulated.
See code with system and policy parameters here.
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 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.
See code with system and policy parameters here.
Author: Hector D. Perez
Position: Ph. D. Candidate @ Carnegie Mellon University
Email: hdperez@cmu.edu
Year: 2021