Model Assumptions

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.

Model Limitations

The following features are not currently supported:

  • Alternate bills of materials (see Model Inputs) for the same material 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 since producer 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 the producer nodes. Each of the producer nodes should use an (s,S) replenishment policy with s = 0, S = 0. When a production order is of size x is going to be placed in a period, the policy will assume the feedstock position at the producer node is going to derop to -x and will order x to bring the position up to 0. Since the lead time is 0 and orders are placed with topological sorting (see Sequence of Events), the inventory will be immediately sent to the producer node and be available for when the production order comes in. However, if there is not enough production capacity to process x, the excess will be left in the dedicated storage for that producer 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 inventory capacities 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)

Creating a Supply Chain Network

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.

General Network Parameters

The graph metadata should have the following fields in its metadata:

  • :materials::Vector with a list of all materials in the system.

Node-specific Parameters

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 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 = [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 material Tuples, where the first element is the input material and the second element is the product/output material; the values indicate the amount of input material consumed to produce 1 unit of output material. Alternatively, a NamedArray 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 (if reallocate = 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 (if reallocate = 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, 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.

Arc-specific Parameters

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::Distribution{Univariate, Discrete}: 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 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

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 metadata
    • num_periods::Int: number of periods to simulate
  • Keyword Arguments (system options):
    • backlog::Bool = true: backlogging allowed if true; 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 if true; otherwise, no reallocation is attempted.
    • guaranteed_service::Bool = false: the simulation will operate under the assumptions in the Guaranteed Service Model (GSM). If true, 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 if true; 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 if true and save the results in SupplyChainEnv.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.

Simulation Outputs

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 each location (arc or node), material, and period. Discarded inventory is marked when capacitated_inventory = true.
  • orders: internal and external orders for each material on each arc (for internal demand) and each node (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 each material on each arc (for internal demand) and each node (for external demand). The ID, creation date, and quantity are stored for each open order. The due column indicates the time left until the order is due (as specified by the service_lead_time).
  • fulfillments: order fulfillment quantities for each order ID. The type column indicates if the amount on that row is material sent, delivered, or is a lost sale.
  • shipments: current in-transit inventory for each arc and material with remaining lead time.
  • profit: time-discounted profit for each node at each period.
  • metrics: service metrics (service level and fillrate) on each arc for each material.