Connection

Last updated on 26th March 2024

The Simudyne SDK allows you to define topologies, that is pairs of groups of agents equipped with a strategy of connection that defines links between them.

The following section will guide you through:

  • What Links are and how they permit interactions between agents.
  • How to create topologies using tools called Groups and Connectors.
  • An overview of the Connectors available and some of their advanced features.
  • How to synthesize a graph topology on a Group using probabilistic distributions defining the graph properties.

Agent Links

Agent-Based Modelling aims to provide a bottom up approach to simulating different situations. With entities, called agents, that have their own behaviour and interact with one and other.

In the Simudyne SDK, those interactions are made possible through message passing. As we shall see, Links are a good way to ease this process.

It is necessary to register Agents and Links in the init of your model. This is required in order to assure that the shape of your data remains constant during the course of the simulation.

To register links and agents simply call the provided methods passing in all of the Agents and Links you require.

AgentBasedModels (Java)

@Override
public void init() {
  registerAgentTypes(MyAgent.class);
  registerLinkTypes(MyLink.class);
}

Each Agent can register links from itself to others agents. Its links allow it to have a reference to other agents it knows or it communicates with so that it can send messages to them.

From one agent, you can create links to other agents using their IDs like so:

Adding a link (Java)

MyAgent agent;
// The ID of another agent to connect to
long targetID;

agent.addLink(targetID, Links.Neighbour.class);

Where we define a Neighbour link class that extends Empty.Link. Defining meaningful link names is encouraged in order to avoid confusion as your models get more complex.

Defining a link (Java)

public class Links {
  public static class Neighbour extends Link.Empty {}
}

Links are not just simple references: they can embed data as well. This data generally deals with a relation between an agent and one of its peers.

Here is how to create a Link that embeds data. First, define a specific class for your link:

Definition of a Link class (Java)

public class MyLink extends Link {
  int someData;
}

When adding links you can now provide a serializable consumer to addLink in order to assign link fields.

Adding a custom Link with fields(Java)

agent.addLink(targetID, MyLink.class, myLink -> myLink.someData=2));

The Link can in the same manner:

  • be accessed by an agent using getLinks() and getLinksTo()
  • be removed by an agent using removeLinks() and removeLinksTo()

An agent can also check if it has links with, hasLinks() and hasLinksTo().

Links are unidirectional

For now, links are unidirectional in the Simudyne SDK. Bidirectional links (links shared by two agents at the same time) do not exist yet.

However, you are still able to create pairs of unidirectional links between agents.

When an agent wants to interact with its environment or with its peers, it has to send messages to them. An agent can:

  • Send messages to a particular agent; the sender has to use the id of the agent it wants to send a message to.
  • Send message along its links; this way all the agents that are connected to the sender will get the messages.

Links give rise to specific behaviours of the model.

The Simudyne SDK exposes tools to automate and ease the creation of links and complex link structures known as topologies.

The next part will get you started with topologies.

Getting started with Topologies

Before getting started, let's define concepts introduced in the Simudyne SDK to create topologies, that is the concepts of Group and Connectors.

Groups as sets of agents

Intuitively, in modelling one tends to define their models in terms of sets or groups of agents having identical properties and behaviours.

This is exactly what a Group in the Simudyne SDK is : a set of agents of a particular class. In a Group, all the agents are identical in the sense that they are instances of the same class. Thus a Group is parametrised by its type of agent and also by its size, the number of agents present in the Group.

Using a MyAgent class defined as above, Groups can be created using the AgentSystem like so:

Generating Groups (Java)

int nbAgents = 10;

// Creating a group of 10 agents of class MyAgent
Group<MyAgentClass> group = generateGroup(MyAgent.class, nbAgents);

After these snippets being executed and the system being set up, 10 agents will be spawned in the model and will live their life according to their behaviour defined in the model.

Note Group created is typed by the agent class (in this example MyAgentClass).

How are the attributes of the agents in the group defined when they are spawned ?

The attributes of the agents will have the specified value given in the class definition.

If you are interested to define your agents' fields, we recommend you check the section Defining agents more precisely in Groups.

Connectors as macroscopic description of connections

Now that we have Groups, we would like to describe the way to connect their agents together.

Connectors implement this concept and are used to specify how to connect agents in one group together or how to connect agents of two different groups together.

Connectors as macroscopic description of connections.

Source Group and Target Group terminology

In the following, we will call "source group" the group from which connections are originating and "target group" the other group where connections are ending.

We provide different Connectors ready to use; here we are going to use the most popular and simple one, FullyConnected, to explains how to connect Groups together`.

Using this Connector, every agent from the source group will be connected to all other agents in the target group.

Let's create and connect to groups together:

FullyConnected on 2 Groups (Java)

Group<MyAgentClass> sourceGroup = generateGroup(MyAgent.class, 5);
Group<MyAgentClass> targetGroup = generateGroup(MyAgent.class, 5);

// Connecting the two groups together using the FullyConnected ConnectionStrategy
sourceGroup.fullyConnected(targetGroup, MyLink.class);

Similar to before, we create two Group using the system generateGroup() method. We then use fullyConnected() on the source group with the target group this Connector to register the topology. Here is a graphical representation - the nodes represent the agents and the edges their links:

FullyConnecting 2 groups of 5 agents.

You can also use a Connector on only one group. This group would be the source but also its own target. Taking a groups created above, we can link its agents using this Connector with this line:

Registering a ConnectionStrategy on a Group (Java)

Group<MyAgentClass> group = generateGroup(MyAgent.class, 5);

// FullyConnecting one group
group.fullyConnected(group, MyLink.class);

And that's all ! After the setup being setup, the agents would be created with their link as specified by the Connector.

What are the other Connectors out there?

If you want to have an overview of all the different Connector, you can refer to the next section that presents them in more details.

About restrictions for connectors on two different groups?

If the FullyConnected strategy can be used on two different groups, some other Connectors can only be used on one Group. This is, for example, the case of GridConnected whose role is to connect agents as if they were on a grid.

Such restrictions on Connectors exist because it is hard to conceive what would some strategy look like on two different Groups.

In every case, the Simudyne SDK only gives you access to topologies that are possible to create so that you don't have to worry about it..

Topologies as the alliance of Groups and Connectors

With those concept defined, you are now able to create topologies, that is a pair of Groups and Connectors!

More precisely, you are able:

  • to create as many Groups of agents of a specific type and of a certain population as you want;
  • to create as many specific connections between as many Groups as you want, shall those groups be identical or different;
  • to parametrize the links created – i.e. embedding data in them.
  • to define your own Connectors if you want to (see the following dedicated section)

Let's give an concrete example to recap but also show all the possibilities of this API.

Example: Transportation problem

Someone might want to simulate some transport problems using Agent Based Modelling. What they can begin with is defining the structure of countries and cities in a continent. One can come with this natural modelling using UML:

UML -- A Transport Problem : using an ABM approach.

City and Country are two classes of agents that extend the class Agent used in Simudyne SDK. Each City belongs to one and only Country. Cities can be connected together via Roads and Countries can be adjacent on behalf of their Borders

Obviously, this modelling needs more details like the number of each type of agents to spawn or the way to connect agents together but this simplified representation helps to understand the context better.

Using our previous definition, the situation can be implemented using 2 groups of agents, a Group<City> and a Group<Country>.

You can firstly define your agents classes like so:

City and Country Definitions (Java)

public class City extends Agent<GlobalState> {
  String name;
  int population;
  String region;
  Float latitude;
  Float longitude;

  void specificBehavior() {
    // ...
  }
  // ...
}

You can also defined the Link classes:

public static class Links {
  public class Road extends Link {
    Float length;
  }

	public class Border extends Link {
    boolean open;
  }

  public class Country extends Link.Empty { }
}

Then in your model, you can generate groups of agents like so:

Groups Generation (Java)

class MyModel extends AgentBasedModel<GlobalState> {   
  // ...

  @Override
  void setup(){
    Group<City> cities = generateGroup(City.class,10000);
    Group<Country> countries = generateGroup(Country.class,10);

    // ...
  }

  // ...
}

Here 10 00 Cities will be spawned, as well as 10 Countries.

For now they are not connected. You can proceed to 3 types of connections:

  • connections between Cities, with Roads as links (as random connection)
  • connections between Countries , with Borders as links (with all the Countries linked to each other)
  • connections between Cities and their Country with no specific link (with Cities being uniformly distributed across Countries)

Group Connection (Java)

@Override
void setup(){
  Group<City> cities = generateGroup(City.class,10000);
  Group<Country> countries = generateGroup(Country.class,10);

  // Connectiong Cities together with Roads
  cities.smallWorldConnected(10,0.7, Road.class);

  // Connecting Countries together with Borders
  countries.fullyConnected(countries, Border.class);

  // Connecting Cities to their Country with a Country Link
  cities.partitionConnected(countries, Country.class);
}

To model random connections between Cities here, we use a SmallWorld.

What is SmallWorld?

You can have a better overview of SmallWorld as well as how to parametrise this Connector in the next section, Connectors Overview.

And we are done !

Defining agents more precisely in Groups

For now, we only populated groups of agents without specifying their attributes. This can be changed using a optional parameter when using the system generateGroup() method.

This extra parameter is a function you have to define to inject data into each agent: using a dataInjector, you can have access to different information (such as the ID that will be given to the agent in the system but also a sequence ID that specify the place of the agent in its group) and tools like a pseudo-random number generator, SeededRandom.

Let's give an example with the class City defined above:

Specifying agents in Groups (Java)

Group<City> cities =
  generateGroup(City.class,10000, city -> {
    SeededRandom ran = city.getPrng();
    RealDistribution populationDistrib = ran.gaussian(43000,6000);
    RealDistribution coordinatesDistrib = ran.uniform(-10,20);

    // Changing the city fields
    city.population = (int) populationDistrib.sample();
    city.latitude = coordinatesDistrib.sample();
    city.longitude = coordinatesDistrib.sample();
  });

Here, we get access to the agent's pseudo random number generator that we have named ran. With this object, we the define two distributions to use to change fields for each city.

More precisely we choose to spawn Cities with:

  • Their population being drawn from a normal distribution of mean `43000` and standard deviation `6000`.
  • Their latitude and longitude being drawn from a uniform distribution on the interval `[-10, 20]`.

Defining links more precisely when using Connectors As it is the case for agent, link will be spawned with fields having default values. You can define the links to create for precisely using the optional lambda when connecting groups.

Here is an example with FullyConnected:

Defining Link (Java)

cities.fullyConnected(City.class, Road.class, road -> {
  SeededRandom ran = road.getPrng();
  RealDistribution lengthDistribution = ran.gaussian(50,25);

  road.length = lengthDistribution.sample();
});

Connectors

A summary of the Connectors available in the SDK as well as their parametrisation.

FullyConnected

In a FullyConnected network, all source agents are connected to all target agents.

FullyConnected -- 1 Group.

1 Group (Java)

group.fullyConnected(group, MyLink.class);
sourceGroup.fullyConnected(targetGroup, MyLink.class);

This connector can be used on one or two groups. In the latter case, the connection will be similar to connections in a complete bipartite graph (like connections between two layers in a simple artificial neural network).

FullyConnected -- 2 Group.

2 Groups (Java)

sourceGroup.fullyConnected(targetGroup, MyLink.class);

This topology is also called Complete Graph in mathematics and graph theory.

FullyConnected and scaling

FullyConnected does not scale very well. The number of links is on the order of n * m (where n and m are the respective number of agents of each group).

For better scaling, use the PartitionConnected connector and the coordinator pattern to produce a similar topology.

Complete graph - Wikipedia

PartitionConnected

In a PartitionConnected network, agents from the source group are equally connected to agents of the target group.

The behaviour of this connector adapts to the number of source and target agents:

  • If the source is bigger than the target, target agents will be connected to several source agents
  • If the two groups contain the same number of agents, then agents will be connected on a one to one basis.
  • If the target is bigger than the source, source agents will be connected to several target agents.

PartitionConnected -- 3 dif cases.

PartitionConnected - 2 Groups (Java)

sourceGroup.partitionConnected(targetGroup, MyLink.class);

PartitionConnected has two sub-strategies for connections.

Shard connections

The shard() sub-strategy will partition the two groups of agents in term of proximity of agents.

ShardConnected -- shards.

PartitionConnected -- Shard (Java)

sourceGroup.partitionConnected(targetGroup, MyLink.class).shard();

By default, Shard connections are used for PartitionConnected. Thus indicating shard() won't change anything but it will make this explicit in the code.

Weaved connections

The weave() sub-stategy will partition the two groups of agents weaving the links together.

PartitionConnected -- Weave.

PartitionConnected -- Weave (Java)

sourceGroup.partitionConnected(targetGroup, MyLink.class).weave();

Used alone, those sub-strategies seems identical and will indeed give the same results. However if you use another connector on one of the groups this will give a completely different behaviour.

Using PartitionConnected to scale your model

PartitionConnected can be used to scale your model easily using the coordinator pattern. The idea is to use a set of agents called coordinators that act as intermediary agent between your two original groups.

SmallWorldConnected

SmallWorldConnected operates on one group and will create a small-world network.

In essence, a small-world network is an intermediate between a ring lattice and a random graph.

It is parametrised by two coefficients:

  • inDegree, a positive integer that specifies the number of outgoing links to create for each agent - it must be strictly smaller than n, the number of agents in the Group;
  • beta, a double in [0,1] that specifies the degree of randomness of the graph

More exactly, inDegree is equal to the number of agents in the group minus one. Furthermore, if beta is equal to 0, the graph will be a complete ring lattice and if beta is equal to 1 the graph will be completely random.

SmallWorldConnected -- Weave.

SmallWorldConnected (Java)

// the number of outgoing links to create for each agent
int inDegree;

// a double in `[0,1]` that specifies the degree of randomness of the graph
double beta;

group.smallWorldConnected(inDegree, beta, MyLink.class);

This type of structure has been used a lot to model social networks in simulations.

More about Small-World network on Wikipedia:

Small-world network - Wikipedia

GridConnected

GridConnected connects agents as if they were on a square grid. This is the main connector used for cellular automata.

GridConnected.

By default, the grid used is a square and is also responsive to the number of agents (its width is adapted so that all the agents can fit in it).

However you can change the grid using several sub-strategies, let's present them.

Wrapping the grid

The grid can be wrapped (so that it becomes a torus): this way, agents on a grid edge will be connected to the agent on the opposite edge.

This can be triggered using GridConnection.wrapped().

GridConnected.

GridConnected -- wrapped2(Java)

group.gridConnected(MyLink.class).wrapped();

Specifying the width of the grid

By default, the grid is a square but you can make it a rectangle if you specify the width to use.

GridConnected -- width 2.

GridConnected -- width of 2(Java)

group.gridConnected(MyLink.class).width(2);

Changing the neighbourhood to use By default and in normal cases, each agent is connected to 8 other agents, called neighbours. This default neighbourhood is the Moore Neighbourhood defined for a Chebyshev distance of 1.

This is the common neighbourhood defined in a variety on agent based model like Conway's Game of Life or Thomas Schelling's models of segregation

This Chebyshev distance can be changed to a number n (supposed greater than 1), making each agents connected to (2 * n + 1)^2 - 1 other agents.

GridConnected -- Moore Neighbourhood (Java)

group.gridConnected(MyLink.class).mooreConnected();

// Specifying a distance for the neighboorhood.
int maxDistance;
group.gridConnected(MyLink.class).mooreConnected(maxDistance);

More about the Moore Neighbourhood on Wikipedia:

Moore Neighborhood - Wikipedia

The other neighbourhood available is the Von Neumann Neighbourhood that is by default defined for a Manhattan distance of 1. Using this neighbourhood, each agent is connected to 4 other agents.

von grid

GridConnected -- Von Neumann Neighbourhood (Java)

group.gridConnected(MyLink.class).vonNeumannConnected();

// Specifying a distance for the neighboorhood.
int maxDistance;
group.gridConnected(MyLink.class).vonNeumannConnected(maxDistance);

This Manhattan distance can also be changed to a number n (supposed greater than 1), connecting agents to 2 * n * (n + 1) other agents.

More about the Von Neumann Neighbourhood on Wikipedia:

Von Neumann Neighbourhood - Wikipedia

BananaTree

A Banana Tree is a graph obtained by connecting one leaf of each of n copies of a star graph with a single root vertex that is distinct from all the stars.

Here are examples of different Banana Trees.

Banana.

BananaTree(Java)

// the number of subgraphs to use
int nbStars;

group.bananaTreeConnected(nbStars, MyLink.class);

Banana Trees can be centred or not:

  • In non-centred trees, each star is connected to the central node by one of its outer node.
  • In centred trees, each star is connected to the central node by its own central node.
By default, Banana Trees are not centred.

Banana -- centered.

BananaTree -- Centered (Java)

group.bananaTreeConnected(nbStars, MyLink.class).setCentered();

GraphConnected

GraphConnected can be used to generate a graph using a degree distribution and a local clustering coefficient. The Apache commons math distributions are expected as the distribution parameters. Apache commons distributions can be created using the generator methods provided in the Simudyne class SeededRandom.

The degree distribution lower bound must be a positive number, and the clustering coefficient must have an upper bound of 1 and a lower bound of 0.

GraphConnected

SeededRandom randomGenerator = getContext().getPRNG();
PoissonDistribution degreeDistribution = randomGenerator.poison(10);
UniformRealDistribution clusterCoefficient = randomGenerator.uniform(0, 1);
group.graphConnected(degreeDistribution, clusterCoefficient, MyLink.class);

The way the degree distribution and clustering coefficent is used to create a graph internally is based on the Darwini method.

Composability of connectors

Connectors are composable: you can combine their parameters and different substrategies. Let's give an example using GridConnected:

Composing substrategies(Java)

// Using a flat square grid (default case)
group.gridConnected(MyLink.class);

// Using a wrapped grid
group.gridConnected(MyLink.class).wrapped();

// Using a flat grid of width w
group.gridConnected(MyLink.class).width(w);

// Using a grid with a different neighborhood definition
group.gridConnected(MyLink.class).vonNeumannConnected();

// Composition them all !
// Using a torus of width w with a Von Neumann neighborhood
group.gridConnected(MyLink.class).wrapped().width(w).vonNeumannConnected();

Connector Summary

With n and m being respectively the sourceGroup and targetGroup size, we can summaries the number of links created:

Connector # of links on 1 Group # of links on 2 Groups
FullyConnected n*(n-1) n*m
PartitionConnected n max(n,m)
SmallWorldConnected n*inDegree Does not apply
GridConnected 8 * w^2 - 12w + 4 with w = ceil(sqrt(n)) (in the base case ) Does not apply
BananaTree 2 * n - 2 Does not apply

Defining your own connector

The Simudyne SDK provides some useful connectors you can use to build ABM easily.

If the 'out the box' connections do not suffice, the Simudyne SDK lets you define and use your own connector. Here is how to proceed.

You first need to extend the the Connector abstract class like this:

Defining a Connector(Java)

public class CustomConnector<T> implements Connector {
    private final Class<T> linkClass;
    private final SerializableBiConsumer<InitContext, T> dataInjector;

    public CustomConnector(Class<T> linkClass,
                           SerializableBiConsumer<InitContext, T> dataInjector) {
        this.linkClass = linkClass;
        this.dataInjector = dataInjector;
    }

    @Override
    public void connectAgent(ConnectionInitializer connectInit,
                             GroupInformation sourceInfo,
                             GroupInformation targetInfo) {
        // ... the connection strategy is to define here
    }
}

The algorithm to use for you to define for CustomConnector is in connectAgent(). You will have to analytically define an algorithm that can be applied individually to each node.

In order to define this algorithm:

  • the GroupInformation sourceInfo and targetInfo give you respectively global information about each of the group used as source and target for your CustomConnector.
  • the Agent, is the current agent and gives you access in your algorithm to a given node information.

Let's create a custom ConnectionStrategy that will connect each agent in a ring.

Example of definition of a Connector(Java)

public class CustomConnector<A extends Agent<?>, L extends Link> implements Connector {
  private final Class<L> linkClass;
  private final SerializableBiConsumer<A, L> dataInjector;

  public CustomConnector(Class<L> linkClass, SerializableBiConsumer<A, L> dataInjector) {
    this.linkClass = linkClass;
    this.dataInjector = dataInjector;
  }

  @Override
  public void connectAgent(Agent agent, GroupInformation sourceInfo, GroupInformation targetInfo) {
    agent.addLink(targetInfo.getBaseID(), linkClass, link -> dataInjector.accept(agent, link));
  }
}

You can then use your CustomConnector using Group.connect():

Using your CustomConnector (Java)

    cellsGroup.connect(cellsGroup, new CustomConnector<>(MyLink.class, (cell, link) -> link.x = cell.id);