Last updated on 16th July 2024
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:
simudyne.core.abm.Agent
with added functionality.simudyne.core.graph.Message
that contains predefined fields for
handling transport between Facility agents.TransportMessage
and an initializable loading dock for loading and unloading cargo.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 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:
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.
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.
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:
The below diagram highlights the general flow of the model:
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:
id
, name
, latitude
, and longitude
(note that each unique id in the id
column must be unique across all agent groups of all types)from
and to
column, that contains all the connections expected between the facilities.Sample Facility Source File
Sample Topology File
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.
The following functionality implemented is simply an example and there are numerous ways to define behaviors.
2 different Facility classes have been implemented:
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 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);
}
});
}
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.