Last updated on 16th July 2024
When connecting two Groups
together, FullyConnected
is generally the canonical choice as it gives the flexibility of having each agent from the source Group know each agent in the target Group.
This is fine for local models where the number of agents is generally small (less than 1 million agents) but when it comes time to scale your model up, you may encounter some critical slow downs.
This is due to the fact that the number of links increases fast compared to the number of agents.
If the size of your source Group
and target Group
are respectively n
and m
, the number of link to create is exactly n * m
:
One way to shrink down the number of links is to introduce an extra group between the source Group
and the target Group
of a smaller size. Its agents will be dedicated to transfer the messages from the source to the target, possibly aggregating them together. Those agents are called Coordinators
.
If p
is the size of the Coordinators
' Group
; then the number of link is equal to p * m + n
.
To give you an brief idea of the number of links for each situations, here are some results for a small number of agents.
n | m | # of links w/o Coordinators | # of links with Coordinators with p = log(n * m) |
proportion of links avoided |
---|---|---|---|---|
10 | 100 | 1000 | 310 | 69.0 % |
200 | 100 | 20000 | 630 | 96.8 % |
1000 | 200 | 200000 | 2060 | 98.9 % |
200 | 1000 | 200000 | 5501 | 97.2 % |
10000 | 1000 | 10000000 | 17000 | 99.8 % |
Let's say that you start with a model like this:
Naive implementation (Java)
public class CoordinatorPatternExample extends AgentBasedModel<GlobalState> {
// Number of agents for each group
public int n = 1000;
public int m = 10000;
@Override
public void setup() {
Group<SourceAgent> sourceGroup = generateGroup(SourceAgent.class, n);
Group<TargetAgent> targetGroup = generateGroup(TargetAgent.class, m);
sourceGroup.fullyConnected(targetGroup, BlankLink.class);
super.setup();
}
}
With those agents and this message minimally defined like so :
public class SourceAgent extends Agent<GlobalState> {}
public class TargetAgent extends Agent<GlobalState> {
public void doSomething(int totalAmount, List<Long> sourceAgentIDs){}
}
We definitively in the first case described by the first figure.
Let's say the behaviour of the model is defined as a sequence with SourceAgents
sending a message to the TargetAgents
:
@Override
public void step() {
run(
Action.create(SourceAgent.class, sAgent -> {
sAgent.broadcastMessage(1);
}),
Action.create(TargetAgent.class, tAgent -> {
List<Message<AggregatedTransfer>> aggregatedTransfers =
tAgent.getMessagesOfType(AggregatedTransfer.class);
List<Long> sourceAgentIDs = new ArrayList<>();
int totalAmount = 0;
for (Message<AggregatedTransfer> message : aggregatedTransfers) {
sourceAgentIDs.addAll(message.getBody().sourceAgents);
totalAmount = +message.getBody().amount;
}
tAgent.doSomething(totalAmount, sourceAgentIDs);
})
);
}
At each tick, each SourceAgent
sends a transfer message to all the theTargetAgents
. Those latter agents can then doSomething
with it then.
Here you can convert your model to use Coordinators
between the SourceAgents
and the TargetAgents
as shown on the second figure.
You will also need another type of message, AggregatedTransfer
.
public class SourceAgent extends Agent<GlobalState> {}
public class TargetAgent extends Agent<GlobalState> {}
public class Coordinator extends Agent<GlobalState> {}
public class AggregatedTransfer{
int amount;
List<Long> sourceAgents;
public AggregatedTransfer(int amount, List<Long> sourceAgents) {
this.amount = amount;
this.sourceAgents = sourceAgents;
}
}
Essentially, the Coordinators
will gather the Transfers
sent by their SourceAgents
and will remap them to all the TargetAgents
.
@Override
public void step() {
run(
Action.create(SourceAgent.class, sAgent -> sAgent.broadcastMessage(1)),
Action.create(Coordinator.class, coordinator -> {
List<Message<AggregatedTransfer>> transfers =
coordinator.getMessagesOfType(AggregatedTransfer.class);
List<Long> sourceAgentIDs = new ArrayList<>();
int totalAmount = 0;
for (Message<AggregatedTransfer> message : transfers) {
sourceAgentIDs.add(message.getSender());
totalAmount += message.getBody().amount;
}
coordinator.broadcastMessage(
new AggregatedTransfer(totalAmount, sourceAgentIDs));
}),
Action.create(TargetAgent.class, tAgent -> {
List<Message<AggregatedTransfer>> aggregatedTransfers =
tAgent.getMessagesOfType(AggregatedTransfer.class);
List<Long> sourceAgentIDs = new ArrayList<>();
int totalAmount = 0;
for (Message<AggregatedTransfer> message : aggregatedTransfers) {
sourceAgentIDs.addAll(message.getBody().sourceAgents);
totalAmount = +message.getBody().amount;
}
tAgent.doSomething(totalAmount, sourceAgentIDs);
})
);
}
Creating a coordinator group(Java)
public int p = (int) Math.log(n * m);
public Group<Coordinator> coordinators = system.generateGroup(Coordinator.class, p);
sourceGroup.partitionConnected(coordinators);
coordinators.fullyConnected(targetGroup);
We introduce p
to represent the number of Coordinators
to create.
Coordinator pattern (Java)
import simudyne.core.abm.Agent;
import simudyne.core.abm.AgentBasedModel;
import simudyne.core.abm.GlobalState;
import simudyne.core.abm.Group;
import java.util.List;
public class CoordinatorPatternExample extends AgentBasedModel<GlobalState> {
// Number of agents for each group
public int n = 1000;
public int m = 10000;
public int p = (int) Math.log(n * m);
@Override
public void setup() {
// Creating the groups
Group<SourceAgent> sourceGroup = generateGroup(SourceAgent.class, n);
Group<TargetAgent> targetGroup = generateGroup(TargetAgent.class, m);
Group<Coordinatior> coordinatiorGroup = generateGroup(Coordinator.class, p);
sourceGroup.partitionConnected(coordinators);
coordinators.fullyConnected(targetGroup);
super.setup();
}
}
@Override
public void step() {
run(
Action.create(SourceAgent.class, sAgent -> sAgent.broadcastMessage(1)),
Action.create(Coordinator.class, coordinator -> {
List<Message<AggregatedTransfer>> transfers =
coordinator.getMessagesOfType(AggregatedTransfer.class);
List<Long> sourceAgentIDs = new ArrayList<>();
int totalAmount = 0;
for (Message<AggregatedTransfer> message : transfers) {
sourceAgentIDs.add(message.getSender());
totalAmount += message.getBody().amount;
}
coordinator.broadcastMessage(
new AggregatedTransfer(totalAmount, sourceAgentIDs));
}),
Action.create(TargetAgent.class, tAgent -> {
List<Message<AggregatedTransfer>> aggregatedTransfers =
tAgent.getMessagesOfType(AggregatedTransfer.class);
List<Long> sourceAgentIDs = new ArrayList<>();
int totalAmount = 0;
for (Message<AggregatedTransfer> message : aggregatedTransfers) {
sourceAgentIDs.addAll(message.getBody().sourceAgents);
totalAmount = +message.getBody().amount;
}
tAgent.doSomething(totalAmount, sourceAgentIDs);
})
);
}
}
If you have a look more closely at PartitionConnected
, you'll see that this ConnectionStrategy
exposes two sub-strategies:
weave()
for weaved connectionsshard()
for shard connections.In the case of the Coordinator Pattern
, using one or the other can have different meanings.
Here is an example of coordinating agents on a grid. Here, the Coordinator Pattern
allows agents to have information about others agents that are not directly next to them.
This is the snippet used to create this topology:
Partition connected weave pattern (Java)
public Group<MyAgent> agents = generateGroup(MyAgent.class,16);
public Group<Coordinator> coordinators = generateGroup(Coordinators.class, 3);
agents.gridConnected(OnGrid.class);
agents.partitionConnected(coordinators, ToCoordinator.class);
This example is very simple but gives the intuition about what weaving the links between the agents and the coordinators allows.