Modeling Discrete Event Simulation (DES) in Java

Discrete Event Simulation (DES) is a powerful method for modeling the behavior and performance of complex systems over time. This article describes how to implement a DES framework in Java to manage multiple entities, resources, and events efficiently.

What is Discrete Event Simulation?

DES models a system as it evolves through discrete events. Each event occurs at a specific time and represents a change in the system’s state. Examples include the arrival of a customer at a queue, the start of a process, or the release of a resource.

Core Components of the DES Framework

  1. Event Class: Represents an event in the system with a scheduled time, an associated entity, and an action to execute.
public class Event implements Comparable<Event> {
    public final int time;
    public final Entity entity;
    public final Runnable action;

    public Event(int time, Entity entity, Runnable action) {
        this.time   = Util.validateNotNegative(time);
        this.entity = Objects.requireNonNull(entity);
        this.action = Objects.requireNonNull(action);
    }

    @Override
    public int compareTo(Event other) {
        return Integer.compare(this.time, other.time);
    }
}
  1. Entity Class: Represents an individual item or process interacting with the system. Each entity has a name and an interval (the time it spends using a resource).
public class Entity {
    public final String name;
    public final int delay;

    public Entity(String name, int delay) {
        this.name = Objects.requireNonNull(name);
        this.delay = Util.validateNotNegative(delay);
    }
}
  1. Resource Class: Models a resource with limited capacity. It includes a queue for waiting entities and tracks active entities currently using the resource.
public class Resource {
    public final String name;
    public final int capacity;
    private final List<Entity> activeEntities = new ArrayList<>();
    private final Queue<Runnable> waiting = new LinkedList<>();
    private final TimeRange timeRange;

    public Resource(String name, int capacity, TimeRange timeRange) {
        this.name       = Objects.requireNonNull(name);
        this.capacity   = Util.validateIsPositive(capacity);
        this.timeRange  = Objects.requireNonNull(timeRange);
    }

    public boolean allocate(Entity entity) {
        if (activeEntities.size() < capacity) {
            activeEntities.add(entity);
            return true;
        } else {
            return false;
        }
    }

    public Entity release() {
        if (!activeEntities.isEmpty()) {
            Entity entity = activeEntities.remove(0);
            if (!waiting.isEmpty()) {
                waiting.poll().run();
            }
            return entity;
        }
        return null;
    }

    public void notifyCapacity(Runnable runnable) {
        waiting.offer(runnable);
    }

    public int processingTime() {
        int start = timeRange.start;
        int end = timeRange.end;

        if (start == end) {
            return start;
        }
        return end + (int) (Math.random() * ((end - start) + 1));
    }
}
  1. Simulation Class: Manages the global clock, event scheduling, and resources. The processEntity method handles resource allocation and scheduling of subsequent events.
public class Simulation {
    private final PriorityQueue<Event> eventQueue = new PriorityQueue<>();
    private final Map<String, Resource> resources = new HashMap<>();
    private int globalClock = 0;

    public void addResource(Resource resource) {
        resources.put(resource.name, Objects.requireNonNull(resource));
    }

    private void scheduleEvent(int time, Entity entity, Runnable action) {
        Event event = new Event(time, entity, action);
        eventQueue.add(event);
    }

    public void run(int maxTime) {
        while (!eventQueue.isEmpty() && globalClock <= maxTime) {
            Event event = eventQueue.poll();
            globalClock = event.time;
            event.action.run();
        }
    }

    public void processEntity(Entity entity, List<String> resourceNames) {
        if (resourceNames.isEmpty()) {
            return;
        }

        String currentResourceName = resourceNames.get(0);
        Resource resource = resources.get(currentResourceName);

        Runnable action = () -> {
            if (resource.allocate(entity)) {
                System.out.println(globalClock + ": " + entity.name + " started using " + resource.name);
                scheduleEvent(calculateTime(entity, resource), entity, () -> {
                    releaseResource(currentResourceName);
                    processEntity(entity, resourceNames.subList(1, resourceNames.size()));
                });
            } else {
                System.out.println(globalClock + ": " + entity.name + " queued for " + resource.name);
                resource.notifyCapacity(() -> scheduleEvent(globalClock, entity, () -> processEntity(entity, resourceNames)));
            }
        };

        scheduleEvent(globalClock, entity, action);
    }

    private int calculateTime(Entity entity, Resource resource) {
        return globalClock + entity.delay + resource.processingTime();
    }

    private void releaseResource(String resourceName) {
        Resource resource = resources.get(resourceName);
        Entity releasedEntity = resource.release();
        if (releasedEntity != null) {
            System.out.println(globalClock + ": " + releasedEntity.name + " finished using " + resource.name);
        }
    }
}

Main Simulation Logic

The Main class sets up the simulation, defines resources and entities, and runs the simulation for a fixed duration.

public class Des4jApplication {

    public static void main(String[] args) {
        Simulation sim = new Simulation();

        Resource resource1 = new Resource("Resource1", 2, TimeRange.of(3, 5));
        Resource resource2 = new Resource("Resource2", 1, TimeRange.of(7));
        
        sim.addResource(resource1);
        sim.addResource(resource2);

        Entity entity1 = new Entity("Entity1", 1);
        Entity entity2 = new Entity("Entity2", 2);
        Entity entity3 = new Entity("Entity3", 1);

        sim.processEntity(entity1, Arrays.asList("Resource1", "Resource2"));
        sim.processEntity(entity2, Arrays.asList("Resource1", "Resource2"));
        sim.processEntity(entity3, Arrays.asList("Resource2", "Resource1"));

        sim.run(50);
    }

}

Example Output

When executed, the simulation will produce output similar to the following:

0: Entity1 started using Resource1
0: Entity3 started using Resource2
0: Entity2 started using Resource1
7: Entity1 finished using Resource1
7: Entity1 queued for Resource2
8: Entity3 finished using Resource2
8: Entity3 started using Resource1
8: Entity1 started using Resource2
9: Entity2 finished using Resource1
9: Entity2 queued for Resource2
14: Entity3 finished using Resource1
16: Entity1 finished using Resource2
16: Entity2 started using Resource2
25: Entity2 finished using Resource2

The code for this article found here

Leave a comment