Last updated on 16th July 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:
Groups
and Connectors
.Connectors
available and some of their advanced features.Group
using probabilistic distributions defining the graph properties.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:
getLinks()
and getLinksTo()
removeLinks()
and removeLinksTo()
An agent can also check if it has links with, hasLinks()
and hasLinksTo()
.
When an agent wants to interact with its environment or with its peers, it has to send messages to them. An agent can:
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.
Before getting started, let's define concepts introduced in the Simudyne SDK to create topologies, that is the concepts of Group
and Connectors
.
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
).
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.
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:
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
.
With those concept defined, you are now able to create topologies, that is a pair of Groups and Connectors!
More precisely, you are able:
Groups
of agents of a specific type and of a certain population as you want;Groups
as you want, shall those groups be identical or different;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.
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:
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
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:
Cities
, with Roads
as links (as random connection)Countries
, with Borders
as links (with all the Countries linked to each other)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
.
And we are done !
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:
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();
});
A summary of the Connectors
available in the SDK as well as their parametrisation.
In a FullyConnected
network, all source agents are connected to all target agents.
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).
2 Groups (Java)
sourceGroup.fullyConnected(targetGroup, MyLink.class);
This topology is also called Complete Graph in mathematics and graph theory.
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:
PartitionConnected - 2 Groups (Java)
sourceGroup.partitionConnected(targetGroup, MyLink.class);
PartitionConnected
has two sub-strategies for connections.
The shard()
sub-strategy will partition the two groups of agents in term of proximity of agents.
PartitionConnected -- Shard (Java)
sourceGroup.partitionConnected(targetGroup, MyLink.class).shard();
The weave()
sub-stategy will partition the two groups of agents weaving the links together.
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.
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 graphMore 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 (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 connects agents as if they were on a square grid. This is the main connector used for cellular automata.
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.
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 -- wrapped2(Java)
group.gridConnected(MyLink.class).wrapped();
By default, the grid is a square but you can make it a rectangle if you specify the width to use.
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.
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
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.
BananaTree(Java)
// the number of subgraphs to use
int nbStars;
group.bananaTreeConnected(nbStars, MyLink.class);
Banana Trees can be centred or not:
BananaTree -- Centered (Java)
group.bananaTreeConnected(nbStars, MyLink.class).setCentered();
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.
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();
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 |
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:
GroupInformation
sourceInfo
and targetInfo
give you respectively global information about each of the group used as source and target for your CustomConnector
.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);
This part will get you started with populating models using data.
Let's take a simplified version of the agent class City
and let's say you have the following kind of data where each City
has:
name
, that is a String
population
, that is an int
surface
, that is a float
More precisely the data is organised like so:
id | name | population | surface |
---|---|---|---|
23323443 | Kodiak | 783 904 | 20 229 |
48217039 | Firebrick | 13 003 | 4320 |
37410058 | Onalaska | 2 377 843 | 61 839 |
93749281 | Ak-Chin Village | 453 150 | 17 323 |
55283047 | Blacktail | 1 921 613 | 49 315 |
... | ... | ... | ... |
Here, there is an additional field called id
(which is a unique number) that identifies each record.
We can come up with a simplified version of the Agent class City
described above to match this data:
To import data, you have to annotate your fields like so so that records in your data can be mapped to Cities.
Class definition (Java)
class City extends Agent {
@Constant
String name;
@Constant
int population;
@Constant
float surface;
//...
}
Finally, to import your agents in your model, you have to use the system loadGroup() method.
You have to specify a Source to use ; this object is dedicated to import data from your file or data base. Let's use a CSVSource to import the data from a CSV file !
Creating data group (Java)
// Creating a Source to import data -- you can use another type of source too
CSVSource citySource = new CSVSource("/path/to/myCities.csv");
Group<City> cities = loadGroup(City.class, citySource);
And now, all the Cities contains as records in your data are imported into your model !
Let's now imagine that you want to introduce Roads between your Cities.
And let's say your have the same dataset of Cities ...
id | name | population | surface |
---|---|---|---|
23323443 | Kodiak | 783904 | 20229 |
48217039 | Firebrick | 13003 | 4320 |
37410058 | Onalaska | 2377843 | 61839 |
93749281 | Ak-Chin Village | 453150 | 17323 |
55283047 | Blacktail | 1921613 | 49315 |
... | ... | ... | ... |
... and that you have another a data sets of Roads linking Cities together:
from | to | length |
---|---|---|
23323443 | 48217039 | 23.32 |
37410058 | 23323443 | 343.3 |
23323443 | 55283047 | 43.13 |
55283047 | 93749281 | 33.38 |
... | ... | ... |
Here, from
and to
identify respectively the source agents and the target agents.
The remaining fields will be injected in the link at their creation. In our context, there is just one remaining field, length
, that represent the length of a Road
from the City
identified by the ID from to the City
identified by the ID to
.
You can import those links in your model and between your two groups using the connect()
with a 'Source'.
Connecting a Group Using Data (Java)
CSVSource citySource = new CSVSource("/path/to/myCities.csv");
CSVSource roadSource = new CSVSource("/path/to/myRoads.csv");
Group<City> cities = loadGroup(City.class, citySource);
cities.connect(cities, Road.class, roadSource);
You can also connect two different Groups
as well:
Connect two Groups using Data
CSVSource citySource = new CSVSource("/path/to/myCities.csv");
CSVSource countrySource = new CSVSource("/path/to/myCountry.csv");
CSVSource linkSource = new CSVSource("/path/to/myLinks.csv");
Group<City> cities = loadGroup(City.class, citySource);
Group<Country> countries = loadGroup(Country.class, countrySource);
cities.connect(countries, MyLink.class, linkSource);
If you have data you want to import from a database, you can use a JDBCSource
that supports MySQL, SQLite as well as PostGreSQL. Here is an example for SQLite:
Using JDBC Sources (java)
String connectionString = s"jdbc:sqlite:path/to/myDataBase.db";
JDBCSource citySource = new JDBCSource(connectionString, "Country", "username", "password");