Coordinator Pattern

Last updated on 29th April 2024

Connecting Groups of Agents

Agents within models can be arranged in groups where each group of agents represents a larger entity, such as a pool of investors. These groups can be connected together by creating links between agents that span the groups. When two groups are being connected together, one group is considered the "source", while the other is the "target". When two groups are connected together, the usual layout of those connections is to link every agent in the source group with every agent in the target group.

If the number of agents in the source group is represented by M, and the number of agents in the target group is represented by N, it takes M x N links to connect the two groups completely. This works well for small models, where there are less than one million agents in total, but the number of connections can skyrocket as the number of agents increases. The large number of connections can cause major slowdowns in model execution.

Creating a Coordinator Group

It is possible to dramatically reduce the number of links between the source and target groups. This reduction can be accomplished with a third group of agents called a coordinator group. The agents in a coordinator group are dedicated to transferring messages between the source and target groups. They are able to reduce the number of links because they often have the ability to aggregate messages together.

The size of the coordinators group should be a logarithmic expression related to the number of agents in the source and target groups. Simudyne recommends using log(M x N) for the number of agents in the coordinators group. This is a good balance between keeping the number of links low and preventing a bottleneck in the coordinators.

If P represents the size of the coordinators group, the number of agents in the source group is represented by M, and the number of agents in the target group is represented by N, the total number of links needed to link the source and target groups is equal to P x (M + N).

Coordinator groups can reduce the number of links a model must maintain by more than 99%.

Adding Coordinators to an Existing Model

Defining a model with two groups, and a specific number of agents in those groups, is simple with the Simudyne SDK. In this example, the source agents have a minimal class definition, while the target agents also contain a single function, so they have specific behavior. A coordinator message class handles messages sent between the source and target groups. At each model time step, each source agent sends a transfer message to all the target agents. Once a target agent receives a transfer message, that agent can then use its specific functionality to handle the message.

The source and target groups are linked together using the 'fullyConnected' function, which connects every agent in the source group to every agent in the target group.

// Agent-based model class with a source and target group.
public class CoordinatorPatternExample extends AgentBasedModel<GlobalState> {

    // Number of agents for each group.
    public int n = 1000;
    public int m = 10000;

    @Override
    public void setup() {

        // Create the source and target groups.
        Group<SourceAgent> sourceGroup = generateGroup(SourceAgent.class, n);
        Group<TargetAgent> targetGroup = generateGroup(TargetAgent.class, m);

        // Create connections between the agents, then set everything up.
        sourceGroup.fullyConnected(targetGroup, Links.CoordinatorLink.class);
        super.setup();
    }

    @Override
    public void step() {
        run(

            // Create the action that sends a message from the source agent.
            Action.create(SourceAgent.class, sAgent -> {
                sAgent.getLinks(Links.CoordinatorLink.class).send(Messages.CoordinatorMessage.class, 1);
            }),

            // Create the action that receives a message from the source agent and does something with it.
            Action.create(TargetAgent.class, tAgent -> {
                List<CoordinatorMessage> transfers = tAgent.getMessagesOfType(Messages.CoordinatorMessage.class);

                List<Long> sourceAgentIDs = new ArrayList<>();
                int totalAmount = 0;

                for(CoordinatorMessage message: transfers) {
                    sourceAgentIDs.add(message.getSender());
                    totalAmount += message.getBody();
                }

                tAgent.doSomething(totalAmount, sourceAgentIDs)
            });
        );
    }
}

// Source agent class.
public class SourceAgent extends Agent<GlobalState> {}

// Target agent class.
public class TargetAgent extends Agent<GlobalState> {
    public void doSomething(int totalAmount, List<Long> sourceAgentIDs) {}
}

// Coordinator message class.
class CoordinatorMessage extends Message.Integer {}

Adding a coordinator group to the model requires the modeller to include a coordinator agent class. A type of message which will handle information about groups of source agents is also needed. This updated model creates a coordinator group in the setup function, and inserts an extra action in the step function to handle communcation with the coordinator group. The action used to send messages from source agents is the same, but the functionality of the target group is changed slightly. Since the target group now receives messages from the coordinator group, it uses the new type of message about groups of source agents, and counts the total number of agents differently.

The source and coordinator groups are linked together using the 'partitionConnected' function, which aggregates many messages from source agents into a single coordinator agent. The coordinator and target groups are linked together using the 'fullyConnected' function, which connects every agent in the coordinator group to every agent in the target group.

import java.util.ArrayList;
import java.util.List;

// Agent-based model class with a source group, target group, and coordinator group.
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() {

        // Create the source and target groups.
        Group<SourceAgent> sourceGroup = generateGroup(SourceAgent.class, n);
        Group<TargetAgent> targetGroup = generateGroup(TargetAgent.class, m);

        // Create the coordinator group and connect it to the source and target groups.
        Group<Coordinator> coordinatorGroup = generateGroup(Coordinator.class, p);
        sourceGroup.partitionConnected(coordinatorGroup, Links.CoordinatorLink.class);
        coordinatorGroup.fullyConnected(targetGroup, Links.CoordinatorLink.class);

        // Set everything up.
        super.setup();
    }

    @Override
    public void step() {
        run(

            // Create the action that sends a message from the source agent.
            Action.create(SourceAgent.class, sAgent -> {
                sAgent.getLinks(Links.CoordinatorLink.class).send(Messages.CoordinatorMessage.class, 1);
            }),

            // Create the action that aggregates messages from source agents into a coordinator agent.
            Action.create(Coordinator.class, coordinator -> {
                List<AggregatedTransferMessage> transfers =
                    coordinator.getMessagesOfType(AggregatedTransferMessage.class);

                List<Long> sourceAgentIDs = new ArrayList<>();
                int totalAmount = 0;

                for(AggregatedTransferMessage message: transfers) {
                    sourceAgentIDs.add(message.getSender());
                    totalAmount += message.amount;
                }

                int finalTotalAmount = totalAmount;
                coordinator.getLinks(Links.CoordinatorLink.class).send(
                    AggregatedTransferMessage.class,
                    message -> {
                        message.amount = finalTotalAmount;
                        message.sourceAgents = sourceAgentIDs;
                    }
                );
            }),

            // Create the action that receives a message from the coordinator agent and does something with it.
            Action.create(TargetAgent.class, tAgent -> {
                List<AggregatedTransferMessage> aggregatedTransfers =
                    tAgent.getMessagesOfType(AggregatedTransferMessage.class);

                List<Long> sourceAgentIDs = new ArrayList<>();
                int totalAmount = 0;

                for(AggregatedTransferMessage message: aggregatedTransfers) {
                    sourceAgentIDs.addAll(message.sourceAgents);
                    totalAmount += message.amount;
                }

                tAgent.doSomething(totalAmount, sourceAgentIDs)
            });
        );
    }
}

// Source agent class.
public class SourceAgent extends Agent<GlobalState> {}

// Target agent class.
public class TargetAgent extends Agent<GlobalState> {
    public void doSomething(int totalAmount, List<Long> sourceAgentIDs) {}
}

// Coordinator agent class.
class Coordinator extends Agent<GlobalState> {}

// Coordinator message class.
class CoordinatorMessage extends Message.Integer {}

// Coordinator aggregated message class.
class AggregatedTransferMessage extends Message {
    int amount;
    List<Long> sourceAgentIDs;
}