SupplyNetPy in 10 Minutes

Installation

SupplyNetPy can be installed using pip:

pip install supplynetpy

Quick Start: Building a Simple Three-Node Supply Chain

Follow these steps to create and simulate a basic supply chain with a supplier and a manufacturer:

A three node supply chain.

Import the Library

import SupplyNetPy.Components as scm

The Components module gives you the building blocks of a supply chain — nodes (suppliers, factories, warehouses, retailers), products and inventory, demand, and the links that connect everything together — which you assemble into a model that fits your scenario.

Create Nodes

Let us create a supplier node in the supply chain that has infinite inventory and can supply any required quantity of product units to a consumer node. The supplier node requires several parameters, including ID, name, and node type. To set it as an infinite supplier, we must specify the node type as infinite_supplier.


supplier1 = {'ID': 'S1', 'name': 'Supplier1', 'node_type': 'infinite_supplier'}

A distributor or warehouse node that purchases products from a supplier is created below by specifying configurable parameters, including ID, name, inventory capacity, replenishment policy, policy parameters, product buy price, and product sell price.


distributor1 = {'ID': 'D1', 'name': 'Distributor1', 'node_type': 'distributor', 
                'capacity': 150, 'initial_level': 50, 'inventory_holding_cost': 0.2, 
                'replenishment_policy': scm.SSReplenishment, 'policy_param': {'s':100,'S':150},
                'product_buy_price': 100,'product_sell_price': 105}

When creating a manufacturer, distributor, wholesaler, or retailer, we must specify the inventory replenishment policy and its parameters.

The SupplyNetPy Components module includes an InventoryReplenishment class that can be customized to define specific replenishment policies. Currently, SupplyNetPy supports the following replenishment policies:

  • Reorder-level (s,S) — continuously monitor inventory and replenish up to S when the level drops below s. Parameters: {s, S}    (class SSReplenishment)

  • Reorder-level (s,S) with Safety Stock — reorder-level replenishment that factors in a safety stock buffer. Parameters: {s, S, safety_stock} (SSReplenishment)

  • Replenish Quantity (RQ) — reorder a fixed quantity Q when placing an order. Parameters: {R, Q} (RQReplenishment)

  • Replenish Quantity (RQ) with safety stock — reorder a fixed quantity Q when placing an order. Parameters: {R, Q, safety_stock} (RQReplenishment)

  • Periodic (T,Q) — replenish inventory every T days with Q units. Parameters: {T, Q} (PeriodicReplenishment)

  • Periodic (T,Q) with safety stock — replenish inventory every T days with Q units. If safety stock is specified, then when the safety stock level is violated, order Q units in addition to the quantity needed to maintain safety stock levels. Parameters: {T, Q, safety_stock} (PeriodicReplenishment)

A link is created as described below. It is configured using parameters such as transportation cost and lead time. The lead time parameter accepts a generative function that produces random lead times based on a specified distribution. Users can create this function according to their needs or define a constant lead time using a Python lambda function.


link1 = {'ID': 'L1', 'source': 'S1', 'sink': 'D1', 'cost': 5, 'lead_time': lambda: 2}

Specify Demand

A demand is created by specifying an ID, name, the node where the demand occurs, how often orders arrive, and how big each order is. The "how often" and "how big" values are each given as a small zero-argument function that returns a number. If you want randomness — say, customer arrivals drawn from an exponential distribution — write a function that samples from that distribution and returns one number per call. If you want a fixed value, just use a one-line lambda such as lambda: 1. Demand can be placed at either a distributor or a retailer. The example below sets up a steady demand of 10 units per day at distributor D1.


demand1 = {'ID': 'd1', 'name': 'Demand1', 'order_arrival_model': lambda: 1,
            'order_quantity_model': lambda: 10, 'demand_node': 'D1'}

Assemble and Simulate the Network

To create and simulate the supply chain, use the create_sc_net function to instantiate the supply chain nodes and assemble them into a network. This function adds metadata to the supply chain, such as the number of nodes, and other relevant information, keeping everything organized. It returns a Python dictionary containing all supply chain components and metadata. The simulate_sc_net function then simulates the supply chain network over a specified period and provides a log of the simulation run. It also calculates performance measures such as net profit, throughput, and more. Let's use these functions to build and simulate our supply chain.


# create a supply chain network
supplychainnet = scm.create_sc_net(nodes=[supplier1, distributor1], links=[link1], demands=[demand1])

# simulate for 20 days
supplychainnet = scm.simulate_sc_net(supplychainnet, sim_time=20, logging=True)

Review Results

After the simulation, inspect supplychainnet to view performance metrics for the supply chain nodes. By default, the simulation log is displayed in the console and saved to a local file named simulation_trace.log, which is located in the same directory as the Python script. Each node in the simulation has its own logger, and logging can be enabled or disabled by providing an additional parameter: logging=True or logging=False while creating the node. SupplyNetPy also exposes a package-level handle scm.global_logger for bulk toggling all simulation logs at once: call scm.global_logger.enable_logging() or scm.global_logger.disable_logging().

Repeating the same run. Some parts of the simulation involve randomness — for example, when a node or a link fails by chance (failure_p, node_disrupt_time, and similar settings on Link). By default, every run draws different random numbers, so two back-to-back runs may give slightly different results. If you want to repeat exactly the same run — for instance, to compare two policies under the same sequence of failures — call scm.set_seed(n) once before you build the network. If you want even finer control, you can also create your own random-number generator with random.Random() and pass it as the rng= argument when you create a node or a link; that node or link will then use its own random stream, independent of the rest.

Below is an example of a simulation log generated by this program. At the end of the log, supply chain-level performance metrics are calculated and printed. These performance measures are computed for each node in the supply chain and include:

  • Inventory carry cost (holding cost)
  • Inventory spend (replenishment cost)
  • Transportation cost
  • Total cost
  • Revenue
  • Profit

INFO D1 - 0.0000:D1: Inventory levels:50, on hand:50
INFO D1 - 0.0000:D1:Replenishing inventory from supplier:Supplier1, order placed for 100 units.
INFO D1 - 0.0000:D1:shipment in transit from supplier:Supplier1.
INFO d1 - 0.0000:d1:Customer1:Order quantity:10, available.
INFO D1 - 0.0000:D1: Inventory levels:40, on hand:140
INFO d1 - 1.0000:d1:Customer2:Order quantity:10, available.
INFO D1 - 1.0000:D1: Inventory levels:30, on hand:130
INFO D1 - 2.0000:D1:Inventory replenished. reorder_quantity=100, Inventory levels:130
INFO d1 - 2.0000:d1:Customer3:Order quantity:10, available.
INFO D1 - 2.0000:D1: Inventory levels:120, on hand:120
INFO d1 - 3.0000:d1:Customer4:Order quantity:10, available.
INFO D1 - 3.0000:D1: Inventory levels:110, on hand:110
INFO d1 - 4.0000:d1:Customer5:Order quantity:10, available.
INFO D1 - 4.0000:D1: Inventory levels:100, on hand:100
INFO D1 - 4.0000:D1:Replenishing inventory from supplier:Supplier1, order placed for 50 units.
INFO D1 - 4.0000:D1:shipment in transit from supplier:Supplier1.
INFO d1 - 5.0000:d1:Customer6:Order quantity:10, available.
INFO D1 - 5.0000:D1: Inventory levels:90, on hand:140
INFO D1 - 6.0000:D1:Inventory replenished. reorder_quantity=50, Inventory levels:140
INFO d1 - 6.0000:d1:Customer7:Order quantity:10, available.
INFO D1 - 6.0000:D1: Inventory levels:130, on hand:130
INFO d1 - 7.0000:d1:Customer8:Order quantity:10, available.
INFO D1 - 7.0000:D1: Inventory levels:120, on hand:120
INFO d1 - 8.0000:d1:Customer9:Order quantity:10, available.
INFO D1 - 8.0000:D1: Inventory levels:110, on hand:110
INFO d1 - 9.0000:d1:Customer10:Order quantity:10, available.
INFO D1 - 9.0000:D1: Inventory levels:100, on hand:100
INFO D1 - 9.0000:D1:Replenishing inventory from supplier:Supplier1, order placed for 50 units.
INFO D1 - 9.0000:D1:shipment in transit from supplier:Supplier1.
INFO d1 - 10.0000:d1:Customer11:Order quantity:10, available.
INFO D1 - 10.0000:D1: Inventory levels:90, on hand:140
INFO D1 - 11.0000:D1:Inventory replenished. reorder_quantity=50, Inventory levels:140
INFO d1 - 11.0000:d1:Customer12:Order quantity:10, available.
INFO D1 - 11.0000:D1: Inventory levels:130, on hand:130
INFO d1 - 12.0000:d1:Customer13:Order quantity:10, available.
INFO D1 - 12.0000:D1: Inventory levels:120, on hand:120
INFO d1 - 13.0000:d1:Customer14:Order quantity:10, available.
INFO D1 - 13.0000:D1: Inventory levels:110, on hand:110
INFO d1 - 14.0000:d1:Customer15:Order quantity:10, available.
INFO D1 - 14.0000:D1: Inventory levels:100, on hand:100
INFO D1 - 14.0000:D1:Replenishing inventory from supplier:Supplier1, order placed for 50 units.
INFO D1 - 14.0000:D1:shipment in transit from supplier:Supplier1.
INFO d1 - 15.0000:d1:Customer16:Order quantity:10, available.
INFO D1 - 15.0000:D1: Inventory levels:90, on hand:140
INFO D1 - 16.0000:D1:Inventory replenished. reorder_quantity=50, Inventory levels:140
INFO d1 - 16.0000:d1:Customer17:Order quantity:10, available.
INFO D1 - 16.0000:D1: Inventory levels:130, on hand:130
INFO d1 - 17.0000:d1:Customer18:Order quantity:10, available.
INFO D1 - 17.0000:D1: Inventory levels:120, on hand:120
INFO d1 - 18.0000:d1:Customer19:Order quantity:10, available.
INFO D1 - 18.0000:D1: Inventory levels:110, on hand:110
INFO d1 - 19.0000:d1:Customer20:Order quantity:10, available.
INFO D1 - 19.0000:D1: Inventory levels:100, on hand:100
INFO D1 - 19.0000:D1:Replenishing inventory from supplier:Supplier1, order placed for 50 units.
INFO D1 - 19.0000:D1:shipment in transit from supplier:Supplier1.
INFO sim_trace - Supply chain info:
INFO sim_trace - available_inv                     : 100
INFO sim_trace - avg_available_inv                 : 112.5
INFO sim_trace - avg_cost_per_item                 : 50.87
INFO sim_trace - avg_cost_per_order                : 1017.4
INFO sim_trace - backorders                        : [0, 0]
INFO sim_trace - demand_by_customers               : [20, 200]
INFO sim_trace - demand_by_site                    : [5, 300]
INFO sim_trace - demands                           : {'d1': Demand1}
INFO sim_trace - env                               : <simpy.core.Environment object at 0x0000028D55F67C10>
INFO sim_trace - fulfillment_received_by_customers : [20, 200]
INFO sim_trace - fulfillment_received_by_site      : [4, 250]
INFO sim_trace - inventory_carry_cost              : 410.0
INFO sim_trace - inventory_spend_cost              : 25000
INFO sim_trace - inventory_waste                   : 0
INFO sim_trace - links                             : {'L1': S1 to D1}
INFO sim_trace - nodes                             : {'S1': Supplier1, 'D1': Distributor1}
INFO sim_trace - num_distributors                  : 1
INFO sim_trace - num_manufacturers                 : 0
INFO sim_trace - num_of_links                      : 1
INFO sim_trace - num_of_nodes                      : 2
INFO sim_trace - num_retailers                     : 0
INFO sim_trace - num_suppliers                     : 1
INFO sim_trace - profit                            : -4435.0
INFO sim_trace - revenue                           : 21000
INFO sim_trace - shortage                          : [0, 0]
INFO sim_trace - total_cost                        : 25435.0
INFO sim_trace - total_demand                      : [25, 500]
INFO sim_trace - total_fulfillment_received        : [24, 450]
INFO sim_trace - transportation_cost               : 25

To access node performance metrics easily, call node.stats.get_statistics(). In this example, the D1 node level statistics can be accessed with the following code:


D1_node = supplychainnet["nodes"]["D1"] # Get D1 node 
stats = D1_node.stats.get_statistics() # Get D1_node statistics
print(stats) # print

Here is the output produced by the code mentioned above.


{'name': 'D1 statistics',
'demand_placed': [5, 300],
'fulfillment_received': [4, 250],
'demand_received': [20, 200],
'demand_fulfilled': [20, 200],
'shortage': [0, 0],
'backorder': [0, 0],
'inventory_level': 100,
'inventory_waste': 0,
'inventory_carry_cost': 410.0,
'inventory_spend_cost': 25000,
'transportation_cost': 25,
'destroyed_qty': 0,
'destroyed_value': 0,
'node_cost': 25435.0,
'revenue': 21000,
'profit': -4435.0}


Alternative Approach: Using Object-Oriented API

This approach demonstrates how to build and simulate a supply chain using SupplyNetPy's object-oriented API. Instead of passing dictionaries to utility functions, we instantiate supply chain components as Python objects, providing greater flexibility and extensibility. Each node and link is created as an object, and the simulation is managed within a SimPy environment, allowing for more advanced customization and integration with other SimPy-based processes.


import simpy # importing simpy to create a simpy environment

env = simpy.Environment() # create a simpy environment

# create an infinite supplier
supplier1 = scm.Supplier(env=env, ID='S1', name='Supplier', node_type="infinite_supplier") 

# create a distributor node
distributor1 = scm.InventoryNode(env=env, ID='D1', name='Distributor1', node_type='distributor',
                                 capacity=150, initial_level=50, inventory_holding_cost=0.2,
                                 replenishment_policy=scm.SSReplenishment, 
                                 policy_param={'s':100, 'S':150}, product_buy_price=100,
                                 product_sell_price=105)

# create a link for distributor
link1 = scm.Link(env=env, ID='L1', source=supplier1, sink=distributor1, cost=5, lead_time=lambda: 2)

# create demand at distributor1
demand1 = scm.Demand(env=env, ID='d1', name='Demand1', order_arrival_model=lambda: 1, 
                     order_quantity_model=lambda:10, demand_node=distributor1)

# we can simulate the supply chain 
env.run(until=20)

This script generates an identical simulation log because the network configuration and demand are deterministic. Final statistics will not be included in the log, as overall supply chain statistics are calculated by the function simulate_sc_net. However, node-level statistics will still be available and can be accessed as mentioned earlier. We can proceed to create and simulate the supply chain network using the same functions, create_sc_net and simulate_sc_net, as demonstrated below.


# create a supply chain network
supplychainnet = scm.create_sc_net(env=env, nodes=[supplier1, distributor1], 
                                   links=[link1], demands=[demand1])

# simulate
supplychainnet = scm.simulate_sc_net(supplychainnet, sim_time=20, logging=True)

Note that an additional parameter, env, is passed to the function create_sc_net to create a supply chain network. This is necessary because the SimPy environment (env) is now created by us and the same needs to be used for creating the supply chain network and running the simulations.