Supply Chain Toolkit Overview

Last updated on 16th July 2024

Introduction

The simudyne.core.abm.supplykit module provides a set of classes for quickly defining and building a network-based simulation of a supply chain. The API sits on top of core-abm allowing you to implement supply chain models characterized by distribution facilities, the products and goods they process, and the transport methods involved in driving the flow throughout the network.

The Supply Chain Toolkit contains 4 new modules for representing the supply chain as a network:

  1. Facility Agent: Agent class that inherits simudyne.core.abm.Agent with added functionality.
  2. TransportMessage: Message class that inherits simudyne.core.graph.Message that contains predefined fields for handling transport between Facility agents.
  3. Loading Bay: Loading Bay class that contains a transport queue for holding pending incoming and outgoing TransportMessage and an initializable loading dock for loading and unloading cargo.
  4. Product: Product class that can be extended to represent any type of unit that is moved through the system.

These 4 modules can be extended and used to represent a supply chain network with Facility Agents that make up different types of distribution and warehouse facilities. These facilities interact by moving inventory via differentiated Transport Messages (Trucks, planes, etc), capturing the flow across the network.

In this overview, the basic concepts, best practices, and strengths of the toolkit will be highlighted. Greater detail on the implementation of each module, including specific best practices and code examples for using the modules are provided on the individual module docs.

The Best Problems for Applying the Supply Chain Toolkit

The toolkit is best for studying network-level aspects of a supply chain and is most useful in solving problems related to operational constraints, facility placement, inventory placement, and especially good at dealing with large-scale systems with significant flow between the nodes (100's of millions of units).

The Simudyne SDK uses a patented graph computation approach to an agent-based model, this means that because we treat the model as essentially a graph of nodes (agents) and edges (links) where communication between the agents is handled via the passing of messages. This means that the SDK is best suited at treating the supply chain system as a graph but not as well suited to representing the physics of motion. This doesn't mean it is not possible to simulate these types of systems, but it requires more complex features to handle real-time traffic information, etc.

Below are some examples comparing and contrasting the less complex and more complex aspects of the approach:

supply chain table

The benefits of the Simudyne SDK Supply Chain Toolkit lies in its capabilities for parallel processing of large sets of agents and behaviors as well as studying the network level impacts of policy decisions, network optimizations, and novel strategy deployment.

The weaknesses of the approach lie in the simulation of the physical movement of agents where routing is being simulated or the internal processes require simulating movement across a grid space. In some cases these can be abstracted or calculated in a layer built on top of the SDK simulation but is not an advised use case for the toolkit.

Use Case Example - Simple Supply Chain

Before checking the use case example, it is advised to read the individual module doc overview sections to get an idea for the layout of the classes and fields / functions that will need to be implemented.

The following section contains some use cases for using the Supply Chain Toolkit with code examples, and a provided sample project for reference and extension.

The model provided below is a simple example using the toolkit where 2 facilities are extended from the Facility.class agent (FacilityTypeA and FacilityTypeB).

Facility Type A: Generates Product objects, processes them, and ships them to Facility Type B

Facility Type B: Receives Product object shipments from Facility Type A and stores them in inventory.

Download Sample Model

The following subsections are divided into different ways in which the toolkit can be used and are based on the high-level implementation provided in the sample code. Reference the individual module docs for more specific examples for optimizing your code for different modelling scenarios.

The flow of the model can be modified as desired, but the general flow follows the below steps:

  1. Process Outbound Loading Bay
  2. Send Outbound & Receive Inbound
  3. Process Inbound Loading Bay

The below diagram highlights the general flow of the model:

supply demo diagram

Data Preparation

Before laying out the model classes, a good starting point is to prepare the data for ingestion into the model. This data will be used for setup of the agents and the topology of the network.

This follows the same structure as the sections Agent Creation & Agent Connection, the only difference is that the Facility Agent has custom fields that need to be added to your CSV file that include the name, latitude, and longitude.

In the sample model you can find the sample data files provided. The 2 key steps are laid out below:

  1. Generate Agent Source files: create columns for id, name, latitude, and longitude (note that each unique id in the id column must be unique across all agent groups of all types)
  2. Generate Topology File: create a from and to column, that contains all the connections expected between the facilities.

Sample Facility Source File facility A source

Sample Topology File topology

Main Supply Chain Model Class

The Supply Chain Model Class is defined in the same manner as any other model built in the Simudyne SDK.

Just like how other models are established in the Simudyne SDK, your main model class will contain the standard init(), setup(), and step() functions.

The below code represents a basic structure for a setup() method for generating the agents and environment using the Supply Chain Model Class.

SupplyChainDemo.java

public class SupplyChainDemo extends AgentBasedModel<SupplyChainDemo.Globals> {

   public static final class Globals extends GlobalState {
       // map for going from String Name of facility to long address
       public Map<String, Long> destinationAddresses;
   }

   @Override
   public void init() {
      registerAgentTypes(FacilityTypeA.class, FacilityTypeB.class);
      registerLinkTypes(Links.AToBLink.class);
   }

   @Override
   public void setup() {
      CSVSource facilityTypeASource = new CSVSource("data/facility_A_source.csv");
      
      // temp map for populating globals name -> long id map (makes sending transport messages easier)
      Map<String, Long> destAddr = new HashMap<>();
      Group<FacilityTypeA> facilityTypeAGroup = loadGroup(FacilityTypeA.class, facilityTypeASource, fA -> {
         fA.outboundLoadingBay = new LoadingBay(2);
         destAddr.put(fA.name, fA.getID());
      });

      CSVSource facilityTypeBSource = new CSVSource("data/facility_B_source.csv");
      Group<FacilityTypeB> facilityTypeBGroup = loadGroup(FacilityTypeB.class, facilityTypeBSource, fB -> {
         fB.inboundLoadingBay = new LoadingBay(2);
         destAddr.put(fB.name, fB.getID());
      });

      getGlobals().destinationAddresses = destAddr;

      CSVSource topology = new CSVSource("data/topology.csv");
      facilityTypeAGroup.loadConnections(facilityTypeBGroup, Links.AToBLink.class, topology);

      super.setup();
   }

The FacilityTypeA.class and FacilityTypeB.class are extending the Facility.class and uses a CSVSource to generate the agent fields and the agent topology.

In the sample model files you will also see the dummy data used to generate the agents.

The 2 different facility groups also have LoadingBay classes initialized to handle inbound and outbound TransportMessage.

The step() method has the following sequences of behaviors defined that call the underlying functions from the Facility.class

SupplyChainDemo.java

@Override
public void step() {
    super.step();
    run(FacilityTypeA.processOutboundLoadingBay);
    run(FacilityTypeA.departOutboundTransport, FacilityTypeB.receiveInboundTransport);
    run(FacilityTypeB.processInboundLoadingBay);
}

The specific behaviors that drive these actions are defined in the Facility implementation in the next section.

Facility Classes

The following functionality implemented is simply an example and there are numerous ways to define behaviors.

2 different Facility classes have been implemented:

  1. Facility Type A - generates products and ships them out
  2. Facility Type B - receives product shipments and stores them in inventory

Facility Type A

Facility Type A represents an abstract type of shipping facility where products or units originate and are sent to other facilities. For the purposes of this model there is a single Facility Type A and a single Facility Type B.

This facility only handles shipping and implements an OutboundLoadingBay and sends TransportMessage messages.

The first behavior this facility executes is the processOutboundLoadingBay action that implements the following methods.

FacilityTypeA.java

@Override
protected void processOutboundLoadingBay() {
        // pull trucks from transport queue
        pullTrucksFromQueue();
        // generate products
        long processingTime = getContext().getTick() + getPrng().getNextInt(60);
        processedProducts.computeIfAbsent(processingTime, k -> new ArrayList<>()).addAll(generateProducts());
        if (processedProducts.containsKey(getContext().getTick())) {
            List<Product> readyToDepart = processedProducts.get(processingTime);
            if (readyToDepart != null) {
                readyToDepart.stream().forEach(product -> {
                String destination = "W";
                Optional<TransportMessage> potentialTransport = outboundLoadingBay.loadingDocks.values().stream().filter(transportMessage -> transportMessage != null && transportMessage.destination == destination).findAny();
            if (potentialTransport.isPresent()) {
                potentialTransport.get().objectContents.add(product);
            } else {
                generateTruck("W");
                pullTrucksFromQueue();
            }
        });
     }
   }
}

The first function pullTrucksFromQueue(), pulls trucks from the transport queue using the logic below. The above functionality calls a function called generateProducts() which just randomly creates Product objects to be added to the processedProducts map where a random tick in the future is the key and a list of Product objects is the value. There is also a generateTruck() method that can be seen in the sample model that generates a new TransportMessage for any location that cannot be found in the outbound loading dock.

FacilityTypeA.java

    // placeholder function for handling transport queue
public void pullTrucksFromQueue() {
        if (outboundLoadingBay.transportQueue.size() != 0) {
            List<Integer> emptyBays = findEmptyBays(outboundLoadingBay.loadingDocks);
            while (emptyBays.size() > 0 && outboundLoadingBay.transportQueue.size() > 0) {
                long nextQueuePosition = Collections.min(outboundLoadingBay.transportQueue.keySet());
                TransportMessage nextTruck = outboundLoadingBay.transportQueue.get(nextQueuePosition);
                int emptyBay = emptyBays.remove(0);
                outboundLoadingBay.loadingDocks.put(emptyBay, nextTruck);
                outboundLoadingBay.transportQueue.remove(nextQueuePosition);
            }
        }
}

// Placeholder function for finding empty bays
public List<Integer> findEmptyBays(Map<Integer, TransportMessage> loadingDocks) {
        List<Integer> emptyBays = new ArrayList<>();
            for (Map.Entry<Integer, TransportMessage> bay : loadingDocks.entrySet()) {
                if (bay.getValue() == null) {
                    emptyBays.add(bay.getKey());
                }
        }
        return emptyBays;
}

The logic above pulls trucks based on queue position and places them in the OutboundLoadingBay so that they can be loaded with items ready to depart.

Once processOutboundLoadingBay() is executed, the next function called in the run sequence is also a behavior of Facility Type A called departTransport() which can be seen below:

FacilityTypeA.java

@Override
protected void departTransport() {
        List<TransportMessages.TransportMessage> departedTransport = new ArrayList<>();
        outboundLoadingBay.loadingDocks.values().stream().filter(transport -> transport != null && transport.departureTime == getContext().getTick()).forEach(t -> {
            departedTransport.add(t);
            send(TransportMessages.TransportMessage.class, tMsg -> tMsg.constructWithProducts(t.id,
                t.transportType, t.weightCapacity, t.volumeCapacity,
                t.weight, t.volume, t.objectContents, t.arrivalTime,
                t.departureTime, t.origin, t.destination
            )).to(getGlobals().destinationAddresses.get(t.destination));
        });
        outboundLoadingBay.loadingDocks.entrySet().stream().forEach(bay -> {
            if (departedTransport.contains(bay.getValue())) {
                bay.setValue(null);
            }
        });
}

Here the facility is simply checking to see if any of the TransportMessage objects in the loading dock are ready to depart and sends them to their destination where Facility Type B executes its behaviors.

Facility Type B

Facility Type B represents an abstract facility that only receives shipments and stores them in inventory.

This facility implements an InboundLoadingBay only and handles incoming TransportMessage messages.

The first function that is implemented receives TransportMessage that have been sent from Facility Type A and places them in the TransportQueue of the InboundLoadingBay.

FacilityTypeB.java

@Override
protected void receiveInboundTransport() {
    super.receiveInboundTransport();
}

Once inbound shipments have arrived, they are placed into the TransportQueue where the next behavior checks to see whether any messages have met the time condition necessary for them to be processed. The message has been received by the agent at this point, however an arrival time at some tick in the future is in the message and it will not be available at the loading dock until this time has passed.

FacilityTypeB.java

@Override
protected void processInboundLoadingBay() {
    pullArrivingTransport();
    unloadTransportCargo();
}

The pullArrivingTransport() function checks to see if any messages have an arrival time equal to or before the getContext().getTick() (current tick) as below:

FacilityTypeB.java

public void pullArrivingTransport() {
        List<TransportMessages.TransportMessage> arrivedTransport = inboundLoadingBay.transportQueue.entrySet()
            .stream()
            .filter(entry -> entry.getKey() <= getContext().getTick())
            .map(Map.Entry::getValue)
            .collect(Collectors.toList());

        if (!arrivedTransport.isEmpty()) {
            List<Integer> emptyBays = findEmptyBays(inboundLoadingBay.loadingDocks);
                while (arrivedTransport.size() > 0 && emptyBays.size() > 0) {
                    TransportMessages.TransportMessage t = arrivedTransport.remove(0);
                    int emptyBay = emptyBays.remove(0);
                    inboundLoadingBay.loadingDocks.put(emptyBay, t);
                }
        }
}

This takes into account available loading bays to ensure that there a available bays to process the arriving shipment.

The final step is to unload the trucks and place the inbound items in inventory as can be seen below in the implementation of unloadTransportCargo()

FacilityTypeB.java

public void unloadTransportCargo() {
        List<TransportMessages.TransportMessage> processedtrucks = new ArrayList<>();
        inboundLoadingBay.loadingDocks.values().stream().filter(dock -> dock != null).forEach(t -> {
            List<Product> cargo = t.objectContents;
            cargo.stream().forEach(p -> {
                nbUnitsProcessed++;
                totalShipmentCost = p.weight * shipmentCostPeLb;
                inventory.computeIfAbsent(p.productID, k -> new ArrayList<>()).add(p);
            });
            processedtrucks.add(t);
        });
        inboundLoadingBay.loadingDocks.entrySet().stream().forEach(bay -> {
            if (processedtrucks.contains(bay.getValue())) {
                bay.setValue(null);
            }
        });
}

Conclusion

The provided code and classes can be extended to any number of unique facility types and unique forms of transportation to best represent the desired real-world supply chain.

Check out the individual model docs for the classes in the Supply Chain Toolkit for best practices on different scenarios and how to improve performance using multi-threaded processing techniques.

  1. Facility
  2. Transport Message
  3. Loading Bay
  4. Product