Designing networks
The combination of agents and locations that exist in a model makes the life of an agent-based modeler much easier. Managing the relationships between agents in agent-based models is now very convenient. However, it is still very tedious to connect a large number of agents through a specific network structure. This is where the real magic of Pop2net comes into play.
Pop2net enables you to design a specific network structure by the definition of a special kind of class: the LocationDesigner
. A LocationDesigner
can be considered as an interface with a special syntax to create a certain number of location instances that connect specific agents in a specific way. The assignment of agents to locations by the definition of a LocationDesigner
is primarely done through agent attributes. Hence, the creation of networks using pop2net makes the most
sense when agents have informative attributes. However, even when the agents in your simulation do not have any attributes the LocationDesigner
class makes it easy to quickly connect agents in a certain way, for instance using classic network models.
The creation of the network based on what is defined in a LocationDesigner
is managed by the Creator
class. The Creator
reads the definition of a LocationDesigner
and translates it into a certain number of certain location instances and then assigns the agents to the corresponding location instances.
The Creator class
In order to design a specific network in Pop2net, the first thing we need is an instance of the Creator
class. The purpose of the Creator
is to generate agents and locations as well as to connect agents and locations. Hence, the first step after creating a Model
is the creation of an instance of Creator
.
[1]:
import pop2net as p2n
model = p2n.Model()
creator = p2n.Creator(model)
Generating Agents
Although the main task of Pop2net and the Creator
class is connecting existing agents via the definition of LocationDesigner
s, the Creator
can also generate agents for you. A special feature of the Creator
class is that it has some convenient methods to create agents based on micro-level data. This makes it easy to create empirical agents from survey data, for instance.
Important to know is that while the generation of locations highly depents on the agents and their attributes, the generation of agents is independent from the generation of locations and their connection. This may be different from classic network generators in which you generate nodes and edges in one step.
Tip: You do not have to generate your agent population using Pop2net or the Creator class. If you plan to create your agents in your own way and just want to connect your agents using Pop2net and the Creator class, you can skip this subsection! However, to connect agents using Pop2net, they must always inherit from the Pop2net agent class.
The simplest way to generate agents using a creator
is by using the method creator.create_agents()
and define the desired number or agents by setting a certain n
:
[3]:
creator.create_agents(n=10)
[3]:
AgentList (10 objects)
[4]:
model.agents
[4]:
AgentList (10 objects)
That is not very impressive. We could have done the same using a simple for-loop. Let’s reset everything by removing those agents from the model:
[5]:
model.remove_agents(model.agents)
[6]:
model.agents
[6]:
AgentList (0 objects)
Now let’s have a look at an example for what Pop2net was originally designed: The creation of a population of agents based on survey data and their flexible connection via locations. For the following example, we assume that we want to create a network model for schools and have access to a sample of individual data collected in a school. This is our example data:
[7]:
import numpy as np
import pandas as pd
np.random.seed(0)
df_school = (
pd.read_csv("example_school_data.csv", sep=";")
.sample(frac=1, replace=False)
.reset_index(drop=True)
)
df_school.head()
[7]:
status | gender | grade | hours | friend_group | |
---|---|---|---|---|---|
0 | pupil | m | 3.0 | 5 | 14 |
1 | teacher | m | NaN | 6 | 0 |
2 | pupil | w | 2.0 | 4 | 12 |
3 | pupil | m | 2.0 | 4 | 11 |
4 | pupil | m | 3.0 | 5 | 6 |
[8]:
df_school.tail()
[8]:
status | gender | grade | hours | friend_group | |
---|---|---|---|---|---|
37 | teacher | w | NaN | 9 | 0 |
38 | pupil | w | 3.0 | 5 | 5 |
39 | social_worker | w | NaN | 6 | 0 |
40 | pupil | w | 1.0 | 4 | 2 |
41 | pupil | m | 1.0 | 4 | 1 |
To create agents from this dataset, we can simply use the method creator.create_agents()
. This method creates one agent object for each row in the given dataset. Each column is translated into an agent attribute with the corresponding value.
[9]:
creator.create_agents(df=df_school)
[9]:
AgentList (42 objects)
The created agents are added to the model’s network and thus appear in model.agents
:
[10]:
model.agents
[10]:
AgentList (42 objects)
Let’s also take a look at the network graph to better understand that the agents are now in the graph of the model, but not yet connected to each other:
[11]:
inspector = p2n.NetworkInspector(model=model)
inspector.plot_bipartite_network()
Let’s have look at the attributes of the first agent. We can see that each cell of the first row of the given data was translated in an agent attribute with the name of the corresponding column. That was done for each row of the given data.
[12]:
vars(model.agents[0])
[12]:
{'_var_ignore': [],
'id': 11,
'type': 'Agent',
'log': {},
'model': Model,
'p': {},
'status': 'pupil',
'gender': 'm',
'grade': 3.0,
'hours': 5,
'friend_group': 14}
If we do not insert an agent class via the parameter agent_class
, the default agent class of Pop2net is used to create the agent instances, but we could also use our own agent class (that inherits from p2n.Agent
):
[13]:
class MyAgent(p2n.Agent):
pass
model = p2n.Model()
creator = p2n.Creator(model)
agents = creator.create_agents(
df=df_school,
agent_class=MyAgent,
)
agents
[13]:
AgentList (42 objects)
[14]:
type(agents[0])
[14]:
__main__.MyAgent
For now, this is enough information on how to create agents based on micro-level data using Pop2net.
Tip: More useful options for the generation of agents based on micro-level data can be found in chapter >>>From survey participants to agents<<< (not online yet).
The LocationDesigner
To generate locations using the Creator
class, we have to use it in combination with the MagicLoation
class. The LocationDesigner
can be considered as an interface to controll the automatic generation of locations and assignment of agents to the locations by the creator
.
The LocationDesigner
class has various attributes and methods which can be customized by the user to specify which kind of locations are created and how the agents are assigned to which location instances. The attributes and methods of the LocationDesigner
are read by the Creator
and then translated into the bipartite network of agents and locations. Here is an overview of the attributes and methods of the LocationDesigner
class:
[15]:
class MyLocation(p2n.LocationDesigner):
n_agents: int | None = None
overcrowding: bool | None = None
only_exact_n_agents: bool = False
n_locations: int | None = None
recycle: bool = True
def filter(self, agent: p2n.Agent) -> bool:
"""Assigns only agents with certain attributes to this location."""
return True
def split(self, agent: p2n.Agent) -> str | float | str | list | None:
"""Creates seperated locations for each value of an agent-attribute."""
return None
def bridge(self, agent: p2n.Agent) -> float | str | list | None:
"""Create locations with one agent for each unique value returned."""
return None
def stick_together(self, agent: p2n.Agent) -> str | float:
"""Ensures that agents with a shared value on an attribute are assigned to the same
location instance."""
return agent.id
def weight(self, agent: p2n.Agent) -> float:
"""Defines the edge weight between the agent and the location instance."""
return 1
def nest(self) -> str | None:
"""Ensures that the agents assigned to the same instance of this location class
are also assigned to the same instance of the returned location class."""
return None
def melt(self) -> list[p2n.Location] | tuple[p2n.Location]:
"""Merges the agents assigned to the instances of the returned location classes
into one instance of this location class."""
return []
def project_weights(self, agent1: p2n.Agent, agent2: p2n.Agent) -> float:
"""Calculates the edge weight between two agents that are assigned to the same location
instance."""
return min([self.get_weight(agent1), self.get_weight(agent2)])
Generating locations (and assigning agents)
We start by just using the default class of LocationDesigner
without defining any further attributes or methods. Every location class defined by the user must inherit - directly or indirectly - from p2n.LocationDesigner
. We call our first location ClassRoom
.
[16]:
class ClassRoom(p2n.LocationDesigner):
pass
Now we use the creator.create_locations()
to create the location instances and - and this is important - to assign the agents, which are already in the model, to the location instances:
[17]:
creator.create_locations(location_designers=[ClassRoom])
[17]:
LocationList (1 object)
[18]:
model.locations
[18]:
LocationList (1 object)
creator.create_locations()
has not only created the location instances with respect to the given agent population, but has also assigned the agents to the location according to the rules specified by the location classes. We can check this by looking at the bipartite network:
[19]:
inspector = p2n.NetworkInspector(model)
inspector.plot_bipartite_network()
The node in the center of the graph is the location. All other nodes are agents. This means that if we use the default location class without further customization, only one location instance is created to which all agents are connected. Since each agent is assigned to this one location instance, the result is a fully connected agent graph:
[20]:
inspector.plot_agent_network()
Generating agents and locations in one step
Before diving into all the details of the definition of location classes, let’s simplify the process of generating agents and locations. The method create()
combines create_agents()
and create_locations()
into one simple method. However, note that create()
always creates the agents based on a given dataset. If you already have a population of agents, use create_locations()
instead.
[21]:
model = p2n.Model()
creator = p2n.Creator(model)
inspector = p2n.NetworkInspector(model)
# Let the Creator create agents and locations
creator.create(
df=df_school,
location_designers=[ClassRoom],
)
[21]:
(AgentList (42 objects), LocationList (1 object))
The method create()
also has the convenient argument clear
. Set this to True
if you want to remove all existing agents and locations from the model before adding new ones to the model. This way you do not have to create a new instances of Model
, Creator
and Inspector
if you want to create a completely new network. In the following, we first run create()
with clear
explicitly set to False
and then with clear
set to True
.
If clear
is False
(which is the default value) the new agents and locations are added to the model without removing the existing ones:
[22]:
creator.create(
df=df_school,
location_designers=[ClassRoom],
clear=False,
)
print(model.agents, model.locations)
AgentList (84 objects) LocationList (2 objects)
If clear
is True
only the new agents and locations remain in the model:
[23]:
creator.create(
df=df_school,
location_designers=[ClassRoom],
clear=True,
)
print(model.agents, model.locations)
AgentList (42 objects) LocationList (1 object)
The keyword clear
is also available in create_agents()
and create_locations()
, where it removes either all existing agents or all existing locations from the model.
Setting the location size
In the next step, we specify the number of people in one classroom. In this example, we assume tiny classrooms of four agents. To set the number of agents per location, we need to set the class attribute n_agents
to the desired value.
[24]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
Let’s use the modified ClassRoom
to create the network:
[25]:
model = p2n.Model()
creator = p2n.Creator(model)
inspector = p2n.NetworkInspector(model)
creator.create(df=df_school, location_designers=[ClassRoom])
inspector.plot_networks()
The network diagrams above now show multiple clusters. Each cluster represents one classroom. If we set a specific size for a location, the Creator creates as many location instances with that size as needed. The agents are then assigned randomly to one of these location instances. As you can see, some classrooms have have more than four members because the number of agents assigned to classrooms cannot be divided exactly by the desired number of four.
The overcrowding
attribute determines how the number of required locations is rounded. By default, overcrowding
is set to None
which means that the number of required locations is either rounded up or rounded down using round
. Below we change overcrowding
to False
to create one more location instance to avoid overcrowding the classrooms.
[26]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
overcrowding = False
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_bipartite_network()
The plot above shows that there is now is no class room that has more than 4 members. However, now there is a class room which only has two members. We could also set overcrowding
to True
to always round down the number of necessary location instances.
If we do not want locations that are either overcrowded or undercrowded but only locations that have exactly the size we defined, we could use the attribute only_exact_n_agents
to True
:
[27]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
only_exact_n_agents = True
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_bipartite_network()
As a consequence, two agents are not assigned to any location.
Defining the number of locations
The attribute n_agents
implicitly changes the number of the created locations. Using the attribute n_locations
, you can also set the number of locations explicitly:
[28]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
n_locations = 4
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_bipartite_network()
While in the example above four classrooms with the defined size of n_agents = 4
are created, in the example below n_agents
is not set explicitly:
[29]:
class ClassRoom(p2n.LocationDesigner):
n_locations = 4
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_bipartite_network()
Specifying location visitors
The classrooms above are made of all agents. But in many cases we want specific locations to be exclusively accessible to certain agents. For this scenario the method filter()
exists. If this method returns True
, an agent gets assigned to an instance of this location class. The most common way to use this method is to specify a condition that requires a certain agent attribute to contain a certain value.
In this example we want classrooms to be only accessible for pupils.
[30]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_bipartite_network(agent_color="status")
Now classrooms consist only of pupils, while all other agents do not belong to any location.
By the way: Besides looking at the network graph, the function inspector.location_information()
and inspector.location_crosstab()
provide usefull overviews of the created location instances and the assigned agents:
Building separated locations
The above table shows that the classrooms are not separated by grade. To seperate agents by grade, we could define one location class for each grade and use filter()
to assign only agents with a specific grade value to a specific location.
A more convenient way to do it is to use the method split()
. For each agent, the method split()
returns one value. For each unique value, seperated location instances are created. In this case, the method split()
returns the attribute grade
for each agents. Thus, the Creator builds seperate classroom instances for each unique value of the agent attribute grade
.
[31]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_networks(agent_color="grade")
Keeping agents together
In the following plot the nodes are colored by their attribute friend_group
. It shows that the members of friend groups are distributed over different classrooms.
[32]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_networks(agent_color="friend_group")
Although this is a very realistic situation, in this example, we want that all friend group members are always in the same class. To implement that, we use the location method stick_together()
: For each agent, the method stick_together()
returns a specific value. Agents with the same return value are sticked together.
[33]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
creator.create(df=df_school, location_designers=[ClassRoom])
inspector.plot_networks(agent_color="friend_group")
Edge weights
Until now, all edges between nodes have a weight of 1
. The location method weight()
can be used to set different weights. In the following, we set the weight of all edges generated by a classroom to 5
. This number could, for instance, represent that agents are together in classrooms for five hours.
[34]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
def weight(self, agent):
return 5
agents, locations = creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_networks(agent_color="grade")
To implement individual weights between an agent and a location, we could also let weight()
return an agent attribute. In this case we use the agent attribute agent.hours
:
[35]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
def weight(self, agent):
return agent.hours
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_networks(agent_color="grade")
As the value returned by location.weight()
refers to the weight between the agent and the location, all the weights between the agents and the locations must be somehow combined when determining the weight between agents (aka graph projection). The location method project_weights()
defines how those weights are combined. By default, project_weights()
returns the smallest weight of the two to be combined. The code cell below shows how project_weights()
combines the two weights by
default. In this example, we keep this way of combining the weights, but this method could be easily rewritten.
[36]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
def weight(self, agent):
return agent.hours
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
Note that we use the method location.get_weight()
to access the weight between the agent and the location.
Bringing together different agents
So far, we are able to connect agents who share a certain attribut value. But what if we want to explicitly connect agents who have different values on a certain attribute? One solution could be to simply give those agents we want to be in the same location the same value on a certain attribute and then define a location class that brings together these agents. Besides this, Pop2net offers two more convinient solutions.
The quick way: bridge()
With the bridge()
method, we can quickly connect agents that have different values on an attribute. The bridge()
method instructs the creator to create location instances that only connect agents with different values for an attribute (or more precisely: location instances that connect agents for which bridge()
has returned different values). That sounds complicated. Let’s look at an example.
Let’s define a location PupilsAndTeachers
that connects teachers and pupils. Since pupils and teachers differ in their agent.status
attribute, we can use this attribute to bring them together. To make it clearer, we first create only one location by setting n_locations
to 1
:
[37]:
class PupilsAndTeachers(p2n.LocationDesigner):
n_locations = 1
def bridge(self, agent):
return agent.status
creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)
inspector.plot_networks(agent_color="status")
We can see that there is one location instance of type PupilsAndTeachers
that connects three agents that have different values on agent.status
, including social workers. As we only want teachers and pupils in that location, we add a filter statement:
[38]:
class PupilsAndTeachers(p2n.LocationDesigner):
n_locations = 1
def bridge(self, agent):
return agent.status
def filter(self, agent):
return agent.status in ["teacher", "pupil"]
creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)
inspector.plot_networks(agent_color="status")
As we do not only want to have one of that location, let’s remove the specifiction of n_locations
:
[39]:
class PupilsAndTeachers(p2n.LocationDesigner):
def filter(self, agent):
return agent.status in ["teacher", "pupil"]
def bridge(self, agent):
return agent.status
creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)
inspector.plot_networks(agent_color="status")
What happened? If you take a closer look, you can see that there is still one location for one edge beteween one teacher and one pupil. However, teachers are assigned to more than one location of this type. This is because the LocationDesigner attribute recycle
is set to True
by default. If recycle
is True
the creator builds as many locations as needed to assign every agent at least to one instance of the corresponding location type. That means that the agents with the most
frequent value returned by bridge()
determine the number of the location instances to build. Therefore it can be that agents from the other groups are assigned multiple times so that everybody has a partner.
To turn this behavior off, set recycle
to False
. As a result, nobody will be assigned multiple times and the agents with the least frequent value determine the number of locations of this type. In the following, we set recycle
ot False
and thus only get 8 location instances of this type, as there are only 8 teachers.
[40]:
class PupilsAndTeachers(p2n.LocationDesigner):
recycle = False
def filter(self, agent):
return agent.status in ["teacher", "pupil"]
def bridge(self, agent):
return agent.status
creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)
inspector.plot_networks(agent_color="status")
The detailed way: melt()
The method bridge()
is a quick way to bring together different agents. However, it is limited. To have more controll over the composition of agents that are assigned to a location, we have to melt different locations together. To do this, we have to define at least three location classes: Two or more locations that are the components that get melted into one location and one location that melts those components together.
Assume we want to create classrooms that consist of one teacher and four pupils. To create such a location, we first define a MeltLocationDesigner
(TeachersInClassRoom
) that consists of only one teacher. Then we define a MeltLocationDesignerDesigner
(PupilsInClassRoom
) that consists of four pupils. Finally, we define a location (ClassRoom
) that uses the method melt()
to melt the two previously defined locations into one location. The method melt()
must return a tuple
or list of at least two location classes that shall be melted into one.
[41]:
# a location for teachers
class TeachersInClassRoom(p2n.MeltLocationDesigner):
n_agents = 1
def filter(self, agent):
return agent.status == "teacher"
# a location for pupils
class PupilsInClassRoom(p2n.MeltLocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
# a location for teachers and pupils
class ClassRoom(p2n.LocationDesigner):
def melt(self):
return TeachersInClassRoom, PupilsInClassRoom
[42]:
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_networks(agent_color="status")
Now we bring back all other settings we made so far:
[43]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
n_agents = 1
def filter(self, agent):
return agent.status == "teacher"
class PupilsInClassRoom(p2n.MeltLocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
class ClassRoom(p2n.LocationDesigner):
def melt(self):
return TeachersInClassRoom, PupilsInClassRoom
def weight(self, agent):
return agent.hours
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
[44]:
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_networks(agent_color="status")
More than one location
The melting of locations combines different locations into one location, but does not create multiple different location classes. If we want to generate multiple different location types, we have to simply feed more than one location class into the Creator
.
In the following, we introduce a School
as a second type of location. (In order to keep the code clean, we skip the melting of locations temporarely.)
[45]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
def weight(self, agent):
return agent.hours
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
class School(p2n.LocationDesigner):
n_locations = 2
[46]:
creator.create(df=df_school, location_designers=[ClassRoom, School], clear=True)
inspector.plot_networks(location_color="label")
Nesting locations
The plot above shows two big clusters. Each of those clusters represents one school. The plot above also shows something unrealistic: The schools are connected because members of one class are not always in the same school.
Nesting locations using stick_together()
We can use the School
-method stick_together()
to fix this issue. This works because whenever an agent is assigned to a location instance, the agent gets a new attribute named after the location class. This new attribute is set to a location instance identifier value.
[47]:
class ClassRoom(p2n.LocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
def weight(self, agent):
return agent.hours
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
class School(p2n.LocationDesigner):
n_locations = 2
def stick_together(self, agent):
return agent.ClassRoom
[48]:
creator.create(df=df_school, location_designers=[ClassRoom, School], clear=True)
inspector.plot_networks(location_color="label")
Note that it is very important that the agents get assigned to classrooms before getting assigned to schools. This means that the order of the creation of the locations is important and, hence, the order of the location classes given to the location_designers
argument.
If we build schools before classrooms, the above method does not work the way intended:
[49]:
creator.create(df=df_school, location_designers=[School, ClassRoom], clear=True)
inspector.plot_networks(location_color="label")
As we saw, the method stick_together()
can be used to nest locations into other locations. However, this approach is limited because we can only specify one location class as the return value of stick_together()
.
Nesting locations using nest()
Another way to nest locations into other locations is to use the location method nest()
. The method nest()
can return None
or a location class. If nest()
returns a location class, the location is nested into the returned location class. For instance, to nest classrooms within schools, we must define the method nest()
for the location ClassRoom
and let this method return School
. Again, the order of location creation plays a crucial role: The level-1 location must
always be created after the level-2 location.
[50]:
class ClassRoom(p2n.LocationDesigner):
def setup(self):
self.n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
def weight(self, agent):
return agent.hours
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
def nest(self):
return "School"
class School(p2n.LocationDesigner):
n_locations = 2
[51]:
creator.create(df=df_school, location_designers=[School, ClassRoom], clear=True)
inspector.plot_networks(location_color="label")
nest()
allows us to nest as many locations in as many levels as we want. However, nest()
has one disadvantage: Because the agents are first grouped into the level-2 location and then into the level-1 location, specific compositions defined at level 1 are not considered when grouping the agents into the level-2 locations.
The following example demonstrates that:
[52]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
def setup(self):
self.n_agents = 1
def filter(self, agent):
return agent.status == "teacher"
class PupilsInClassRoom(p2n.MeltLocationDesigner):
def setup(self):
self.n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
class ClassRoom(p2n.LocationDesigner):
def melt(self):
return [TeachersInClassRoom, PupilsInClassRoom]
def weight(self, agent):
return agent.hours * 10
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
def nest(self):
return "School"
class School(p2n.LocationDesigner):
n_locations = 2
[53]:
creator.create(df=df_school, location_designers=[School, ClassRoom], clear=True)
inspector.plot_networks(location_color="label", agent_color="status")
As you can see in the graph and the table above, the compositions defined by ClassRoom
are not always met. That is due to the fact that the agents are first assigned to the schools independently of the settings defined by ClassRoom
. When the classrooms are built, there are not always the necessary agents in each school needed to meet the composition defined in ClassRoom
. This might not always be a problem. However, if we want to ensure that classrooms always have the defined
composition of agents, they have to be created before the schools are created and, thus, we have to use the method stick_together()
to nest classrooms into schools:
[54]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
def setup(self):
self.n_agents = 1
def filter(self, agent):
return agent.status == "teacher"
class PupilsInClassRoom(p2n.MeltLocationDesigner):
def setup(self):
self.n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
class ClassRoom(p2n.LocationDesigner):
def melt(self):
return [TeachersInClassRoom, PupilsInClassRoom]
def weight(self, agent):
return agent.hours * 10
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
class School(p2n.LocationDesigner):
n_locations = 2
def stick_together(self, agent):
return agent.ClassRoom
[55]:
creator.create(df=df_school, location_designers=[ClassRoom, School], clear=True)
inspector.plot_networks(location_color="label")
It is also possible to combine both nesting techniques. We could first nest the class ClassRoom
into School
using stick_together()
in order to get the composition wanted for ClassRoom
and then use nest()
to nest further locations into School
, as it is done in the next example:
[56]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
n_agents = 1
def filter(self, agent):
return agent.status == "teacher"
class PupilsInClassRoom(p2n.MeltLocationDesigner):
n_agents = 4
def filter(self, agent):
return agent.status == "pupil"
def split(self, agent):
return agent.grade
def stick_together(self, agent):
return agent.friend_group
class ClassRoom(p2n.LocationDesigner):
def melt(self):
return [TeachersInClassRoom, PupilsInClassRoom]
def weight(self, agent):
return agent.hours * 10
def project_weights(self, agent1, agent2) -> float:
return min([self.get_weight(agent1), self.get_weight(agent2)])
class School(p2n.LocationDesigner):
n_locations = 2
def stick_together(self, agent):
return agent.ClassRoom
class SoccerTeam(p2n.LocationDesigner):
n_agents = 11
def nest(self):
return "School"
[57]:
creator.create(
df=df_school,
location_designers=[
ClassRoom, # nested into School using `stick_together()`
School,
SoccerTeam, # nested into School using `nest()`
],
clear=True,
)
[57]:
(AgentList (42 objects), LocationList (14 objects))
[58]:
inspector.plot_networks(location_color="label", agent_color="status")
This way we can nest multiple locations into one level-2 location and at the same time ensure that the composition of agents defined in ClassRoom
is met in each school. The order of the locations plays a crucial role: 1. The classrooms with the desired compositions are created 2. The schools are created keeping together whole classrooms 3. Soccer teams are created within the schools
Integrating networkx graph generators
One great features of Pop2net is that we do not have to forego classic network models because networkX graph generators are easily integrateble. To build a network component based on a networkX graph or networkX graph generator, we only have to set the LocationDesigner class attribute nxgraph
to the desired graph (generator).
In the following example, we define a location class that creates a small world network from a networkx graph generator:
[59]:
import networkx as nx
model = p2n.Model()
creator = p2n.Creator(model)
inspector = p2n.NetworkInspector(model)
N_AGENTS = 50
class SmallWorld(p2n.LocationDesigner):
nxgraph = nx.watts_strogatz_graph(n=N_AGENTS, k=4, p=0.05)
creator.create_agents(n=N_AGENTS)
creator.create_locations(
location_designers=[
SmallWorld,
],
)
inspector.plot_networks()
As we can see in the bipartite network graph, networkX graphs are translated into a pop2net graph by creating one location instance for each bilateral relation between nodes. Thus, agents are assigned to multiple instances of the SmallWorld
location class:
[60]:
agent = model.agents[0]
for location in agent.locations:
print(location.label)
SmallWorld
SmallWorld
SmallWorld
SmallWorld
To get all neighbors that are connected to an agent via the small world graph we could do the follwoing:
[61]:
agent.neighbors(location_labels=["SmallWorld"])
[61]:
AgentList (4 objects)
When building locations based on networkX graphs we still retain the flexibility of Pop2net and its LocationDesigners. For instance, we could create seperated small worlds for each group of agents or layer multiple network structures by defining multiple location classes:
[62]:
model = p2n.Model()
creator = p2n.Creator(model)
inspector = p2n.NetworkInspector(model)
N_AGENTS = 50
N_GROUPS = 2
class SmallWorld(p2n.LocationDesigner):
nxgraph = nx.watts_strogatz_graph(n=int(N_AGENTS / N_GROUPS), k=4, p=0.05)
def split(self, agent):
return agent.group
class Bridge(p2n.LocationDesigner):
n_locations = 1
def bridge(self, agent):
return agent.group
creator.create_agents(n=N_AGENTS)
for i, agent in enumerate(model.agents):
agent.group = i % N_GROUPS
creator.create_locations(
location_designers=[
SmallWorld,
Bridge,
],
)
inspector.plot_networks(agent_color="group")
Magic agent attributes
When creating locations with the Creator class, the Creator temporarily sets certain agent attributes that provide information about the specific location to which the agent has been assigned and the position of the agent within this location. These temporary agent attributes can be used to address specific agents when defining a second location type. Each of the temporary agent attributes begins with the name of the location label.
The following list shows all magic agent attributes:
agent.LOCATIONLABEL
: An identifier of the location the agent was assigned to.agent.LOCATIONLABEL_assigned
: A bool which isTrue
if the agent was assigned to this location class.agent.LOCATIONLABEL_id
: An identifier of the sub-location instance.agent.LOCATIONLABEL_position
: An identifier of the agent’s position within the location. The first agent gets a0
.agent.LOCATIONLABEL_head
: A bool which isTrue
if the agent is the first agent within the location.agent.LOCATIONLABEL_tail
: A bool which isTrue
if the agent is the last agent within the location.
In the following example, we first create 3 simple clusters by defining the ‘Cluster’ location class. In the next step we select an agent from each cluster and connect them to the location class Bridge
. To select only one agent from each cluster, we filter by the magic agent attribute agent.Cluster_head
that was set when the clusters were created. This way, only the first agent of each cluster is assigned to the Bridge
location.
[63]:
model = p2n.Model()
creator = p2n.Creator(model)
inspector = p2n.NetworkInspector(model)
class Cluster(p2n.LocationDesigner):
n_locations = 3
class Bridge(p2n.LocationDesigner):
def filter(self, agent):
return agent.Cluster_head
creator.create_agents(n=30)
creator.create_locations(
location_designers=[
Cluster,
Bridge,
],
)
inspector.plot_networks()
As the creator builds one location class after another, it is very important that the order of the location classes provided to the creator is correct. In the following, the connection of the clusters fails due to an incorrect order of the location classes provided to the creator.
[64]:
class Cluster(p2n.LocationDesigner):
n_locations = 3
class Bridge(p2n.LocationDesigner):
def filter(self, agent):
return agent.Cluster_head
creator.create_agents(n=30, clear=True)
creator.create_locations(
location_designers=[
Bridge,
Cluster,
],
clear=True,
)
inspector.plot_networks()