We already had a look at communication and messaging between agents. Large models can easily result in thousands of connections to create and maintain. For a fully interconnected mesh of n agents, the number of connections equals ½⋅n⋅(n-1). That is, for 1000 directly interconnected agents, there are about 500 000 connections to manage. Is there an easier way to organize this?
Building a model tree
Larger models can be divided into smaller sub-models by using the Domain
classes of JSimpleSim. Domains serve to group and subgroup various parts of the model and help to compartmentalize it. In terms of software design, domains and agents form a composite pattern. This leads to a tree-like hieratical structure with agents as leaves.
Example: Imagine a model for the well-known strategy game Civilization. There are tribes, cities, buildings, units, resources and even more game elements. A way to put these into a model hierarchy could be:
- Level 0 ⇒ There is a world (root domain).
- Level 1 ⇒ The world consists of tribes (domains).
- Level 2 ⇒ Each tribe has a leader, units and cites (agents).
Please note that at this point, we only consider the agent model. There may be lots of additional data (terrain info, technology tree, etc.) aside from these acting entities.
Addressing
To send messages in the model above, we determine sender and receiver by unique identifiers rather than by connections. In a tree-like structure, an address comprised of a small array of integers can be used as identifier. The size of the array equals the level of the agent within the model hierarchy. Each element of the array corresponds to the index of the agent (or an enclosing domain) within its parent’s domain.
Example: Imagine the address as a vector directly pointing to an agent. An address of [2, 5, 8] means:
- Within the root domain, look up entity with index 2 as domain A
- In domain A, look up entity with index 5 as domain B.
- In domain B, find entity with index 8 as the agent itself
For large simulation models, hierarchical models are often easier to manage and maintain. By this addressing concept, there have to be only two connections per agent: one in and one out. The number of connections grows proportionally with the number of entities and not by square (as in the fully interconnected mesh mentioned before). So this approach greatly reduces complexity.
Implementation
JSimpleSim builds an abstraction layer over address management, message forwarding and even model changes during runtime, so you can focus on model building and agent strategies. Simulation models with message routing have to use RoutingAgent
and RoutingDomain
as base classes and RoutingMessage
for messages. A simple agent could look like this:
package org.simplesim.examples; import org.simplesim.core.messaging.RoutingMessage; import org.simplesim.core.scheduling.Time; import org.simplesim.model.RoutingAgent; import org.simplesim.model.State; /** * Simple implementation of a {@code RoutingAgent} as template for own implementations */ public class SimpleAgent extends RoutingAgent<SimpleAgent.SimpleAgentState, SimpleAgent.Event> { static class SimpleAgentState implements State { /* * Place state variables, getters and setters here. */ } enum Event { EVENT1, EVENT2, EVENT3 } public SimpleAgent() { super(new SimpleAgentState()); /* * Do some other initialization here. Port management is done automatically by the base class. */ } @Override public Time doEvent(Time time) { while (getInport().hasMessages()) handleMessage(((RoutingMessage) getInport().poll())); while (getEventQueue().getMin().equals(time)) handleEvent(getEventQueue().dequeue(),time); /* * Do not forget to enqueue some new events. */ return getTimeOfNextEvent(); } protected void sendMessage(RoutingAgent<?,?> destination, Object content) { RoutingMessage message=new RoutingMessage(this.getAddress(),destination.getAddress(),content); getOutport().write(message); } private void handleMessage(RoutingMessage message) { /* * Do message handling here. */ } private void handleEvent(Event event, Time time) { switch(event) { case EVENT1: ; case EVENT2: ; case EVENT3: ; }; } }
A simple domain and the main()
method to construct and run the model could look like this:
package org.simplesim.examples; import org.simplesim.core.messaging.MessageForwardingStrategy; import org.simplesim.core.messaging.RecursiveMessageForwarding; import org.simplesim.core.scheduling.HeapEventQueue; import org.simplesim.core.scheduling.Time; import org.simplesim.model.RoutingDomain; import org.simplesim.simulator.SequentialDESimulator; import org.simplesim.simulator.Simulator; /** * Simple implementation of a {@code RoutingDomain} */ public class SimpleDomain extends RoutingDomain { public SimpleDomain() { } public static void main(String[] args) { SimpleDomain root=new SimpleDomain(); root.setAsRootDomain(); new SimpleAgent().addToDomain(root); SimpleDomain subdomain=new SimpleDomain(); new SimpleAgent().addToDomain(subdomain); new SimpleAgent().addToDomain(subdomain); subdomain.addToDomain(root); final MessageForwardingStrategy fs=new RecursiveMessageForwarding(); Simulator simulator=new SequentialDESimulator(root,new HeapEventQueue<>(),fs); simulator.runSimulation(Time.INFINITY); } }
Please note the following differences to models based on BasicAgent
and BasicDomain
and their direct connections between model entities (as seen in the example of the basic tutorial):
- Different base classes for agents and domains (use the
Routing
instead ofBasic
version of classes). - Do not care about ports or connections. This is done automatically when adding or removing entities!
- Routing model entities already have
getInport()
andgetOutport()
to access these ports. - During model building, the method
setAsRootDomain()
has to be called for the topmost model domain. - Use
RoutingMessage
as class for messages. - Use either
RecursiveMessageForwarding
orRoutingMessageForwarding
strategy.