Designing networks

Having actors and locations as agents makes the life of an agent-based modeler much easier. Managing the relationships between interacting entities in agent-based models is now very convenient. However, it is still very tedious to connect a large number of actors 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 actors in a specific way. The assignment of actors to locations by the definition of a LocationDesigner is primarely done through actor attributes. Hence, the creation of networks using pop2net makes the most sense when actors have informative attributes. However, even when the actors in your simulation do not have any attributes the LocationDesigner class makes it easy to quickly connect actors 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 actors 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 actors and locations as well as to connect actors and locations. Hence, the first step after creating an Environment is the creation of an instance of Creator.

[1]:
import pop2net as p2n

env = p2n.Environment()
creator = p2n.Creator(env)

Generating Actors

Although the main task of Pop2net and the Creator class is connecting existing actors via the definition of LocationDesigners, the Creator can also generate actors for you. A special feature of the Creator class is that it has some convenient methods to create actors based on micro-level data. This makes it easy to create empirical actors from survey data, for instance.

Important to know is that while the generation of locations highly depents on the actors and their attributes, the generation of actors 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 actor population using Pop2net or the Creator class. If you plan to create your actors in your own way and just want to connect your actors using Pop2net and the Creator class, you can skip this subsection! However, to connect actors using Pop2net, they must inherit from the Pop2net actor class and must be added to the environment.

The simplest way to generate actors using a creator is by using the method creator.create_actors() and define the desired number of actors by setting a certain n:

[3]:
creator.create_actors(n=10)
[3]:
[<pop2net.actor.Actor at 0x7f9b00d120f0>,
 <pop2net.actor.Actor at 0x7f9b57618140>,
 <pop2net.actor.Actor at 0x7f9aa7310d70>,
 <pop2net.actor.Actor at 0x7f9aa7313f80>,
 <pop2net.actor.Actor at 0x7f9b57544200>,
 <pop2net.actor.Actor at 0x7f9b575456d0>,
 <pop2net.actor.Actor at 0x7f9b57545970>,
 <pop2net.actor.Actor at 0x7f9b57544bc0>,
 <pop2net.actor.Actor at 0x7f9b57545430>,
 <pop2net.actor.Actor at 0x7f9b57544a70>]
[4]:
env.actors
[4]:
[<pop2net.actor.Actor at 0x7f9b00d120f0>,
 <pop2net.actor.Actor at 0x7f9b57618140>,
 <pop2net.actor.Actor at 0x7f9aa7310d70>,
 <pop2net.actor.Actor at 0x7f9aa7313f80>,
 <pop2net.actor.Actor at 0x7f9b57544200>,
 <pop2net.actor.Actor at 0x7f9b575456d0>,
 <pop2net.actor.Actor at 0x7f9b57545970>,
 <pop2net.actor.Actor at 0x7f9b57544bc0>,
 <pop2net.actor.Actor at 0x7f9b57545430>,
 <pop2net.actor.Actor at 0x7f9b57544a70>]

That is not very impressive. We could have done the same using a simple for-loop. Let’s reset everything by removing those actors from the environment:

[5]:
env.remove_actors(env.actors)
[6]:
env.actors
[6]:
[]

Now let’s have a look at an example for what Pop2net was originally designed: The creation of a population of actors 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 actors from this dataset, we can simply use the method creator.create_actors(). This method creates one actor object for each row in the given dataset. Each column is translated into an actor attribute with the corresponding value.

[9]:
creator.create_actors(df=df_school)
[9]:
[<pop2net.actor.Actor at 0x7f9aa70ff620>,
 <pop2net.actor.Actor at 0x7f9aa70ff680>,
 <pop2net.actor.Actor at 0x7f9aa70ff590>,
 <pop2net.actor.Actor at 0x7f9aa70ff950>,
 <pop2net.actor.Actor at 0x7f9aa70ff530>,
 <pop2net.actor.Actor at 0x7f9aa70ff410>,
 <pop2net.actor.Actor at 0x7f9aa70ff560>,
 <pop2net.actor.Actor at 0x7f9aa70ff440>,
 <pop2net.actor.Actor at 0x7f9aa70ff920>,
 <pop2net.actor.Actor at 0x7f9aa70ff4a0>,
 <pop2net.actor.Actor at 0x7f9aa70ffaa0>,
 <pop2net.actor.Actor at 0x7f9aa70ff980>,
 <pop2net.actor.Actor at 0x7f9aa70ffb30>,
 <pop2net.actor.Actor at 0x7f9aa70ff9b0>,
 <pop2net.actor.Actor at 0x7f9aa70ffa10>,
 <pop2net.actor.Actor at 0x7f9aa70ffa40>,
 <pop2net.actor.Actor at 0x7f9aa70ff6b0>,
 <pop2net.actor.Actor at 0x7f9aa70ff860>,
 <pop2net.actor.Actor at 0x7f9aa70ff380>,
 <pop2net.actor.Actor at 0x7f9aa70ff6e0>,
 <pop2net.actor.Actor at 0x7f9aa70ff830>,
 <pop2net.actor.Actor at 0x7f9aa70ff710>,
 <pop2net.actor.Actor at 0x7f9aa70ff650>,
 <pop2net.actor.Actor at 0x7f9aa70ffa70>,
 <pop2net.actor.Actor at 0x7f9aa70ffb60>,
 <pop2net.actor.Actor at 0x7f9aa70ffb90>,
 <pop2net.actor.Actor at 0x7f9aa70ffbc0>,
 <pop2net.actor.Actor at 0x7f9aa70ff8c0>,
 <pop2net.actor.Actor at 0x7f9aa70ff9e0>,
 <pop2net.actor.Actor at 0x7f9aa70ffad0>,
 <pop2net.actor.Actor at 0x7f9aa70ffc80>,
 <pop2net.actor.Actor at 0x7f9aa70ffe00>,
 <pop2net.actor.Actor at 0x7f9aa70fff50>,
 <pop2net.actor.Actor at 0x7f9aa70ff890>,
 <pop2net.actor.Actor at 0x7f9aa70ffce0>,
 <pop2net.actor.Actor at 0x7f9aa70ffd10>,
 <pop2net.actor.Actor at 0x7f9aa70ffd40>,
 <pop2net.actor.Actor at 0x7f9aa70ffd70>,
 <pop2net.actor.Actor at 0x7f9aa70ff320>,
 <pop2net.actor.Actor at 0x7f9aa70ffda0>,
 <pop2net.actor.Actor at 0x7f9aa70ffdd0>,
 <pop2net.actor.Actor at 0x7f9aa70ffe30>]

The created actors are added to the environment’s network and thus appear in env.actors:

[10]:
env.actors
[10]:
[<pop2net.actor.Actor at 0x7f9aa70ff620>,
 <pop2net.actor.Actor at 0x7f9aa70ff680>,
 <pop2net.actor.Actor at 0x7f9aa70ff590>,
 <pop2net.actor.Actor at 0x7f9aa70ff950>,
 <pop2net.actor.Actor at 0x7f9aa70ff530>,
 <pop2net.actor.Actor at 0x7f9aa70ff410>,
 <pop2net.actor.Actor at 0x7f9aa70ff560>,
 <pop2net.actor.Actor at 0x7f9aa70ff440>,
 <pop2net.actor.Actor at 0x7f9aa70ff920>,
 <pop2net.actor.Actor at 0x7f9aa70ff4a0>,
 <pop2net.actor.Actor at 0x7f9aa70ffaa0>,
 <pop2net.actor.Actor at 0x7f9aa70ff980>,
 <pop2net.actor.Actor at 0x7f9aa70ffb30>,
 <pop2net.actor.Actor at 0x7f9aa70ff9b0>,
 <pop2net.actor.Actor at 0x7f9aa70ffa10>,
 <pop2net.actor.Actor at 0x7f9aa70ffa40>,
 <pop2net.actor.Actor at 0x7f9aa70ff6b0>,
 <pop2net.actor.Actor at 0x7f9aa70ff860>,
 <pop2net.actor.Actor at 0x7f9aa70ff380>,
 <pop2net.actor.Actor at 0x7f9aa70ff6e0>,
 <pop2net.actor.Actor at 0x7f9aa70ff830>,
 <pop2net.actor.Actor at 0x7f9aa70ff710>,
 <pop2net.actor.Actor at 0x7f9aa70ff650>,
 <pop2net.actor.Actor at 0x7f9aa70ffa70>,
 <pop2net.actor.Actor at 0x7f9aa70ffb60>,
 <pop2net.actor.Actor at 0x7f9aa70ffb90>,
 <pop2net.actor.Actor at 0x7f9aa70ffbc0>,
 <pop2net.actor.Actor at 0x7f9aa70ff8c0>,
 <pop2net.actor.Actor at 0x7f9aa70ff9e0>,
 <pop2net.actor.Actor at 0x7f9aa70ffad0>,
 <pop2net.actor.Actor at 0x7f9aa70ffc80>,
 <pop2net.actor.Actor at 0x7f9aa70ffe00>,
 <pop2net.actor.Actor at 0x7f9aa70fff50>,
 <pop2net.actor.Actor at 0x7f9aa70ff890>,
 <pop2net.actor.Actor at 0x7f9aa70ffce0>,
 <pop2net.actor.Actor at 0x7f9aa70ffd10>,
 <pop2net.actor.Actor at 0x7f9aa70ffd40>,
 <pop2net.actor.Actor at 0x7f9aa70ffd70>,
 <pop2net.actor.Actor at 0x7f9aa70ff320>,
 <pop2net.actor.Actor at 0x7f9aa70ffda0>,
 <pop2net.actor.Actor at 0x7f9aa70ffdd0>,
 <pop2net.actor.Actor at 0x7f9aa70ffe30>]

Let’s also take a look at the network graph to better understand that the actors are now in the graph of the environment, but not yet connected to each other:

[11]:
inspector = p2n.NetworkInspector(env)
inspector.plot_bipartite_network()

Let’s have look at the attributes of the first actor. We can see that each cell of the first row of the given data was translated in an actor attribute with the name of the corresponding column. That was done for each row of the given data.

[12]:
vars(env.actors[0])
[12]:
{'env': <pop2net.environment.Environment at 0x7f9afc34e750>,
 'id_p2n': 10,
 'model': None,
 'type': 'Actor',
 'status': 'pupil',
 'gender': 'm',
 'grade': 3.0,
 'hours': 5,
 'friend_group': 14}

If we do not insert an actor class via the parameter actor_class, the default actor class of Pop2net is used to create the actor instances, but we could also use our own actor class (that inherits from p2n.Actor):

[13]:
class MyActor(p2n.Actor):
    pass


env = p2n.Environment()
creator = p2n.Creator(env)
actors = creator.create_actors(
    df=df_school,
    actor_class=MyActor,
)
actors
[13]:
[<__main__.MyActor at 0x7f9aa6f22300>,
 <__main__.MyActor at 0x7f9aa6f22660>,
 <__main__.MyActor at 0x7f9aa6f227b0>,
 <__main__.MyActor at 0x7f9aa6f22720>,
 <__main__.MyActor at 0x7f9aa6f22210>,
 <__main__.MyActor at 0x7f9aa6f225a0>,
 <__main__.MyActor at 0x7f9aa6f226f0>,
 <__main__.MyActor at 0x7f9aa6f227e0>,
 <__main__.MyActor at 0x7f9aa6f21160>,
 <__main__.MyActor at 0x7f9aa6f22780>,
 <__main__.MyActor at 0x7f9aa6f22750>,
 <__main__.MyActor at 0x7f9aa6f20bf0>,
 <__main__.MyActor at 0x7f9aa6f22840>,
 <__main__.MyActor at 0x7f9aa6f223c0>,
 <__main__.MyActor at 0x7f9aa6f22870>,
 <__main__.MyActor at 0x7f9aa6f228a0>,
 <__main__.MyActor at 0x7f9aa6f228d0>,
 <__main__.MyActor at 0x7f9aa6f22900>,
 <__main__.MyActor at 0x7f9aa6f22930>,
 <__main__.MyActor at 0x7f9aa6f22960>,
 <__main__.MyActor at 0x7f9aa6f22990>,
 <__main__.MyActor at 0x7f9aa6f229c0>,
 <__main__.MyActor at 0x7f9aa6f229f0>,
 <__main__.MyActor at 0x7f9aa6f22a20>,
 <__main__.MyActor at 0x7f9aa6f22a50>,
 <__main__.MyActor at 0x7f9aa6f22a80>,
 <__main__.MyActor at 0x7f9aa6f22ab0>,
 <__main__.MyActor at 0x7f9aa6f22ae0>,
 <__main__.MyActor at 0x7f9aa6f22b10>,
 <__main__.MyActor at 0x7f9aa6f22b40>,
 <__main__.MyActor at 0x7f9aa6f22b70>,
 <__main__.MyActor at 0x7f9aa6f22ba0>,
 <__main__.MyActor at 0x7f9aa6f22bd0>,
 <__main__.MyActor at 0x7f9aa6f22c00>,
 <__main__.MyActor at 0x7f9aa6f22c30>,
 <__main__.MyActor at 0x7f9aa6f22c60>,
 <__main__.MyActor at 0x7f9aa6f22c90>,
 <__main__.MyActor at 0x7f9aa6f22cc0>,
 <__main__.MyActor at 0x7f9aa6f22cf0>,
 <__main__.MyActor at 0x7f9aa6f22d20>,
 <__main__.MyActor at 0x7f9aa6f22d50>,
 <__main__.MyActor at 0x7f9aa6f22d80>]
[14]:
type(actors[0])
[14]:
__main__.MyActor

For now, this is enough information on how to create actors based on micro-level data using Pop2net.

Tip: More useful options for the generation of actors based on micro-level data can be found in chapter >>>From survey participants to actors<<< (not online yet).

The LocationDesigner

To generate locations using the Creator class, we have to use it in combination with the LocationDesigner class. The LocationDesigner can be considered as an interface to controll the automatic generation of locations and assignment of actors 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 actors 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 actors and locations. Here is an overview of the attributes and methods of the LocationDesigner class:

[15]:
from __future__ import annotations  # noqa: F404


class MyLocation(p2n.LocationDesigner):
    n_actors: int | None = None
    overcrowding: bool | None = None
    only_exact_n_actors: bool = False
    n_locations: int | None = None
    recycle: bool = True

    def filter(self, actor: p2n.Actor) -> bool:
        """Assigns only actors with certain attributes to this location."""
        return True

    def split(self, actor: p2n.Actor) -> str | float | str | list | None:
        """Creates seperated locations for each value of an agent-attribute."""
        return None

    def bridge(self, actor: p2n.Actor) -> float | str | list | None:
        """Create locations with one actor for each unique value returned."""
        return None

    def stick_together(self, actor: p2n.Actor) -> str | float:
        """Ensures that actors with a shared value on an attribute are assigned to the same
        location instance."""
        return actor.id_p2n

    def weight(self, actor: p2n.Actor) -> float:
        """Defines the edge weight between the actor and the location instance."""
        return 1

    def nest(self) -> str | None:
        """Ensures that the actors 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 actors assigned to the instances of the returned location classes
        into one instance of this location class."""
        return []

    def project_weights(self, actor1: p2n.Actor, actor2: p2n.Actor) -> float:
        """Calculates the edge weight between two actors that are assigned to the same location
        instance."""
        return min([self.get_weight(actor1), self.get_weight(actor2)])

Generating locations (and assigning actors)

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 actors, which are already in the environment, to the location instances:

[17]:
creator.create_locations(location_designers=[ClassRoom])
[17]:
[<pop2net.creator.Location object at 0x7f9aa6f23320>]
[18]:
env.locations
[18]:
[<pop2net.creator.Location object at 0x7f9aa6f23320>]

creator.create_locations() has not only created the location instances with respect to the given actor population, but has also assigned the actors 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(env)
inspector.plot_bipartite_network()

The node in the center of the graph is the location. All other nodes are actors. This means that if we use the default location class without further customization, only one location instance is created to which all actors are connected. Since each actor is assigned to this one location instance, the result is a fully connected actor graph:

[20]:
inspector.plot_actor_network()

Generating actors and locations in one step

Before diving into all the details of the definition of location classes, let’s simplify the process of generating actors and locations. The method create() combines create_actors() and create_locations() into one simple method. However, note that create() always creates the actors based on a given dataset. If you already have a population of actors, use create_locations() instead.

[21]:
env = p2n.Environment()
creator = p2n.Creator(env)
inspector = p2n.NetworkInspector(env)

# Let the Creator create actors and locations
creator.create(
    df=df_school,
    location_designers=[ClassRoom],
)
[21]:
([<pop2net.actor.Actor at 0x7f9aa6f8e2a0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e060>,
  <pop2net.actor.Actor at 0x7f9aa6f8e390>,
  <pop2net.actor.Actor at 0x7f9aa6f8e2d0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e360>,
  <pop2net.actor.Actor at 0x7f9aa6f8e030>,
  <pop2net.actor.Actor at 0x7f9aa6f8e1b0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e3f0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e270>,
  <pop2net.actor.Actor at 0x7f9aa6f8e300>,
  <pop2net.actor.Actor at 0x7f9aa6f8e330>,
  <pop2net.actor.Actor at 0x7f9aa6f8e3c0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e120>,
  <pop2net.actor.Actor at 0x7f9aa6f8e450>,
  <pop2net.actor.Actor at 0x7f9aa6f8e150>,
  <pop2net.actor.Actor at 0x7f9aa6f8e420>,
  <pop2net.actor.Actor at 0x7f9aa6f8e480>,
  <pop2net.actor.Actor at 0x7f9aa6f8e4b0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e4e0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e510>,
  <pop2net.actor.Actor at 0x7f9aa6f8e540>,
  <pop2net.actor.Actor at 0x7f9aa6f8e570>,
  <pop2net.actor.Actor at 0x7f9aa6f8e5a0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e5d0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e600>,
  <pop2net.actor.Actor at 0x7f9aa6f8e630>,
  <pop2net.actor.Actor at 0x7f9aa6f8e660>,
  <pop2net.actor.Actor at 0x7f9aa6f8e690>,
  <pop2net.actor.Actor at 0x7f9aa6f8e6c0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e6f0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e720>,
  <pop2net.actor.Actor at 0x7f9aa6f8e750>,
  <pop2net.actor.Actor at 0x7f9aa6f8e780>,
  <pop2net.actor.Actor at 0x7f9aa6f8e7b0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e7e0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e810>,
  <pop2net.actor.Actor at 0x7f9aa6f8e840>,
  <pop2net.actor.Actor at 0x7f9aa6f8e870>,
  <pop2net.actor.Actor at 0x7f9aa6f8e8a0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e8d0>,
  <pop2net.actor.Actor at 0x7f9aa6f8e900>,
  <pop2net.actor.Actor at 0x7f9aa6f8e930>],
 [<pop2net.creator.Location object at 0x7f9aa6f8ea50>])

The method create() also has the convenient argument clear. Set this to True if you want to remove all existing actors and locations from the environment before adding new ones to the environment. This way you do not have to create new instances of Environment, 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 actors and locations are added to the environment without removing the existing ones:

[22]:
creator.create(
    df=df_school,
    location_designers=[ClassRoom],
    clear=False,
)
print("actors:", len(env.actors), "locations:", len(env.locations))
actors: 84 locations: 2

If clear is True only the new actors and locations remain in the environment:

[23]:
creator.create(
    df=df_school,
    location_designers=[ClassRoom],
    clear=True,
)
print("actors:", len(env.actors), "locations:", len(env.locations))
actors: 42 locations: 1

The keyword clear is also available in create_actors() and create_locations(), where it removes either all existing actors or all existing locations from the environment.

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 actors. To set the number of actors per location, we need to set the class attribute n_actors to the desired value.

[24]:
class ClassRoom(p2n.LocationDesigner):
    n_actors = 4

Let’s use the modified ClassRoom to create the network:

[25]:
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
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 actors are then assigned randomly to one of these location instances. As you can see, some classrooms have more than four members because the number of actors 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_actors = 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_actors to True:

[27]:
class ClassRoom(p2n.LocationDesigner):
    n_actors = 4
    only_exact_n_actors = True


creator.create(df=df_school, location_designers=[ClassRoom], clear=True)

inspector.plot_bipartite_network()

As a consequence, two actors are not assigned to any location.

Defining the number of locations

The attribute n_actors 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_actors = 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_actors = 4 are created, in the example below n_actors 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 actors. But in many cases we want specific locations to be exclusively accessible to certain actors. For this scenario the method filter() exists. If this method returns True, an actor 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 actor attribute to contain a certain value.

In this example we want classrooms to be only accessible for pupils.

[30]:
class ClassRoom(p2n.LocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"


creator.create(df=df_school, location_designers=[ClassRoom], clear=True)

inspector.plot_bipartite_network(actor_color="status")

Now classrooms consist only of pupils, while all other actors do not belong to any location.

Building separated locations

Currently, classrooms are not separated by grade. To seperate actors by grade, we could define one location class for each grade and use filter() to assign only actors with a specific grade value to a specific location.

A more convenient way to do it is to use the method split(). For each actor, 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 actors. Thus, the Creator builds seperate classroom instances for each unique value of the actor attribute grade.

[31]:
class ClassRoom(p2n.LocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade


creator.create(df=df_school, location_designers=[ClassRoom], clear=True)

inspector.plot_networks(actor_color="grade")

Keeping actors 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_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade


creator.create(df=df_school, location_designers=[ClassRoom], clear=True)

inspector.plot_networks(actor_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 actor, the method stick_together() returns a specific value. Actors with the same return value are sticked together.

[33]:
class ClassRoom(p2n.LocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group


creator.create(df=df_school, location_designers=[ClassRoom])

inspector.plot_networks(actor_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 actors are together in classrooms for five hours.

[34]:
class ClassRoom(p2n.LocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group

    def weight(self, actor):
        return 5


actors, locations = creator.create(df=df_school, location_designers=[ClassRoom], clear=True)

inspector.plot_networks()

To implement individual weights between an actor and a location, we could also let weight() return an actor attribute. In this case we use the actor attribute actor.hours:

[35]:
class ClassRoom(p2n.LocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group

    def weight(self, actor):
        return actor.hours


creator.create(df=df_school, location_designers=[ClassRoom], clear=True)

inspector.plot_networks()

As the value returned by location.weight() refers to the weight between the actor and the location, all the weights between the actors and the locations must be somehow combined when determining the weight between actors (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_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group

    def weight(self, actor):
        return actor.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 actor and the location.

Bringing together different actors

So far, we are able to connect actors who share a certain attribut value. But what if we want to explicitly connect actors who have different values on a certain attribute? One solution could be to simply give those actors 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 actors. Besides this, Pop2net offers two more convinient solutions.

The quick way: bridge()

With the bridge() method, we can quickly connect actors that have different values on an attribute. The bridge() method instructs the creator to create location instances that only connect actors with different values for an attribute (or more precisely: location instances that connect actors 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 actor.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, actor):
        return actor.status


creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)

inspector.plot_networks(actor_color="status")

We can see that there is one location instance of type PupilsAndTeachers that connects three actors that have different values on actor.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, actor):
        return actor.status

    def filter(self, actor):
        return actor.status in ["teacher", "pupil"]


creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)

inspector.plot_networks(actor_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, actor):
        return actor.status in ["teacher", "pupil"]

    def bridge(self, actor):
        return actor.status


creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)

inspector.plot_networks(actor_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 actor at least to one instance of the corresponding location type. That means that the actors with the most frequent value returned by bridge() determine the number of the location instances to build. Therefore it can be that actors 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 actors 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, actor):
        return actor.status in ["teacher", "pupil"]

    def bridge(self, actor):
        return actor.status


creator.create(df=df_school, location_designers=[PupilsAndTeachers], clear=True)

inspector.plot_networks(actor_color="status")

The detailed way: melt()

The method bridge() is a quick way to bring together different actors. However, it is limited. To have more controll over the composition of actors 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_actors = 1

    def filter(self, actor):
        return actor.status == "teacher"


# a location for pupils
class PupilsInClassRoom(p2n.MeltLocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.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(actor_color="status")

Now we bring back all other settings we made so far:

[43]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
    n_actors = 1

    def filter(self, actor):
        return actor.status == "teacher"


class PupilsInClassRoom(p2n.MeltLocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group


class ClassRoom(p2n.LocationDesigner):
    def melt(self):
        return TeachersInClassRoom, PupilsInClassRoom

    def weight(self, actor):
        return actor.hours
[44]:
creator.create(df=df_school, location_designers=[ClassRoom], clear=True)
inspector.plot_networks(actor_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_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group

    def weight(self, actor):
        return actor.hours


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 actor is assigned to a location instance, the actor 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_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group

    def weight(self, actor):
        return actor.hours


class School(p2n.LocationDesigner):
    n_locations = 2

    def stick_together(self, actor):
        return actor.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 actors 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 label. If nest() returns a location label, the location is nested into the corresponding location type. 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_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group

    def weight(self, actor):
        return actor.hours

    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 actors 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 actors into the level-2 locations.

The following example demonstrates that:

[52]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
    def setup(self):
        self.n_actors = 1

    def filter(self, actor):
        return actor.status == "teacher"


class PupilsInClassRoom(p2n.MeltLocationDesigner):
    def setup(self):
        self.n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade


class ClassRoom(p2n.LocationDesigner):
    def melt(self):
        return [TeachersInClassRoom, PupilsInClassRoom]

    def weight(self, actor):
        return actor.hours * 10

    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", actor_color="status")

As you can see in the graph above, the compositions defined by ClassRoom are not always met. That is due to the fact that the actors are first assigned to the schools independently of the settings defined by ClassRoom. When the classrooms are built, there are not always the necessary actors 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 actors, 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:

[56]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
    def setup(self):
        self.n_actors = 1

    def filter(self, actor):
        return actor.status == "teacher"


class PupilsInClassRoom(p2n.MeltLocationDesigner):
    def setup(self):
        self.n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group


class ClassRoom(p2n.LocationDesigner):
    def melt(self):
        return [TeachersInClassRoom, PupilsInClassRoom]

    def weight(self, actor):
        return actor.hours * 10


class School(p2n.LocationDesigner):
    n_locations = 2

    def stick_together(self, actor):
        return actor.ClassRoom
[57]:
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:

[58]:
class TeachersInClassRoom(p2n.MeltLocationDesigner):
    n_actors = 1

    def filter(self, actor):
        return actor.status == "teacher"


class PupilsInClassRoom(p2n.MeltLocationDesigner):
    n_actors = 4

    def filter(self, actor):
        return actor.status == "pupil"

    def split(self, actor):
        return actor.grade

    def stick_together(self, actor):
        return actor.friend_group


class ClassRoom(p2n.LocationDesigner):
    def melt(self):
        return [TeachersInClassRoom, PupilsInClassRoom]

    def weight(self, actor):
        return actor.hours * 10


class School(p2n.LocationDesigner):
    n_locations = 2

    def stick_together(self, actor):
        return actor.ClassRoom


class SoccerTeam(p2n.LocationDesigner):
    n_actors = 11

    def nest(self):
        return "School"
[59]:
creator.create(
    df=df_school,
    location_designers=[
        ClassRoom,  # nested into School using `stick_together()`
        School,
        SoccerTeam,  # nested into School using `nest()`
    ],
    clear=True,
)
[59]:
([<pop2net.actor.Actor at 0x7f9aa64bac60>,
  <pop2net.actor.Actor at 0x7f9aa64bb230>,
  <pop2net.actor.Actor at 0x7f9aa64bae40>,
  <pop2net.actor.Actor at 0x7f9aa64b8650>,
  <pop2net.actor.Actor at 0x7f9aa64bb3b0>,
  <pop2net.actor.Actor at 0x7f9aa64baf90>,
  <pop2net.actor.Actor at 0x7f9aa64badb0>,
  <pop2net.actor.Actor at 0x7f9aa64bb050>,
  <pop2net.actor.Actor at 0x7f9aa64ba570>,
  <pop2net.actor.Actor at 0x7f9aa64bb1a0>,
  <pop2net.actor.Actor at 0x7f9aa64bae10>,
  <pop2net.actor.Actor at 0x7f9aa64bb170>,
  <pop2net.actor.Actor at 0x7f9aa64bb380>,
  <pop2net.actor.Actor at 0x7f9aa64bb830>,
  <pop2net.actor.Actor at 0x7f9aa64bbb60>,
  <pop2net.actor.Actor at 0x7f9aa64b86e0>,
  <pop2net.actor.Actor at 0x7f9aa64bb530>,
  <pop2net.actor.Actor at 0x7f9aa64bb950>,
  <pop2net.actor.Actor at 0x7f9aa64bb650>,
  <pop2net.actor.Actor at 0x7f9aa64bb8f0>,
  <pop2net.actor.Actor at 0x7f9aa64ba960>,
  <pop2net.actor.Actor at 0x7f9aa64bb920>,
  <pop2net.actor.Actor at 0x7f9aa62bdc40>,
  <pop2net.actor.Actor at 0x7f9aa62bf470>,
  <pop2net.actor.Actor at 0x7f9aa6383c80>,
  <pop2net.actor.Actor at 0x7f9aa62bc2c0>,
  <pop2net.actor.Actor at 0x7f9aa62bf020>,
  <pop2net.actor.Actor at 0x7f9aa62be840>,
  <pop2net.actor.Actor at 0x7f9aa62bf710>,
  <pop2net.actor.Actor at 0x7f9aa62befc0>,
  <pop2net.actor.Actor at 0x7f9aa62bf500>,
  <pop2net.actor.Actor at 0x7f9aa62bf440>,
  <pop2net.actor.Actor at 0x7f9aa62bf3b0>,
  <pop2net.actor.Actor at 0x7f9aa62bedb0>,
  <pop2net.actor.Actor at 0x7f9aa60e7500>,
  <pop2net.actor.Actor at 0x7f9aa60e7710>,
  <pop2net.actor.Actor at 0x7f9aa60e7320>,
  <pop2net.actor.Actor at 0x7f9aa60e69c0>,
  <pop2net.actor.Actor at 0x7f9aa60e42c0>,
  <pop2net.actor.Actor at 0x7f9aa60e75f0>,
  <pop2net.actor.Actor at 0x7f9aa60e74d0>,
  <pop2net.actor.Actor at 0x7f9aa60e74a0>],
 [<pop2net.creator.Location object at 0x7f9aa62bcf80>,
  <pop2net.creator.Location object at 0x7f9aa62a87a0>,
  <pop2net.creator.Location object at 0x7f9aa62beb10>,
  <pop2net.creator.Location object at 0x7f9aa62a9970>,
  <pop2net.creator.Location object at 0x7f9aa65ee180>,
  <pop2net.creator.Location object at 0x7f9aa62bfad0>,
  <pop2net.creator.Location object at 0x7f9aa62bf5c0>,
  <pop2net.creator.Location object at 0x7f9aa6298c20>,
  <pop2net.creator.Location object at 0x7f9aa65ee9c0>,
  <pop2net.creator.Location object at 0x7f9aa64bb350>,
  <pop2net.creator.Location object at 0x7f9aa6298c80>,
  <pop2net.creator.Location object at 0x7f9aa62bc800>,
  <pop2net.creator.Location object at 0x7f9aa62bf7d0>,
  <pop2net.creator.Location object at 0x7f9aa62bfa70>])
[60]:
inspector.plot_networks(location_color="label", actor_color="status")

This way we can nest multiple locations into one level-2 location and at the same time ensure that the composition of actors 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 graphs

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:

[61]:
import networkx as nx

env = p2n.Environment()
creator = p2n.Creator(env)
inspector = p2n.NetworkInspector(env)

N_ACTORS = 50


class SmallWorld(p2n.LocationDesigner):
    nxgraph = nx.watts_strogatz_graph(n=N_ACTORS, k=4, p=0.05)


creator.create_actors(n=N_ACTORS)
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, actors are assigned to multiple instances of the SmallWorld location class:

[62]:
actor = env.actors[0]

for location in actor.locations:
    print(location.label)
SmallWorld
SmallWorld
SmallWorld
SmallWorld

To get all neighbors that are connected to an actor via the small world graph we could do the follwoing:

[63]:
actor.neighbors(location_labels=["SmallWorld"])
[63]:
[<pop2net.actor.Actor at 0x7f9aa60e9a00>,
 <pop2net.actor.Actor at 0x7f9aa60eab70>,
 <pop2net.actor.Actor at 0x7f9aa60eb8c0>,
 <pop2net.actor.Actor at 0x7f9aa60eb710>]

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 actors or layer multiple network structures by defining multiple location classes:

[64]:
N_ACTORS = 50
N_GROUPS = 2


class SmallWorld(p2n.LocationDesigner):
    nxgraph = nx.watts_strogatz_graph(n=int(N_ACTORS / N_GROUPS), k=4, p=0.05)

    def split(self, actor):
        return actor.group


class Bridge(p2n.LocationDesigner):
    n_locations = 1

    def bridge(self, actor):
        return actor.group


creator.create_actors(n=N_ACTORS, clear=True)

for i, actor in enumerate(env.actors):
    actor.group = i % N_GROUPS

creator.create_locations(
    location_designers=[
        SmallWorld,
        Bridge,
    ],
    clear=True,
)
inspector.plot_networks(actor_color="group")

Magic actor attributes

When creating locations with the Creator class, the Creator temporarily sets certain actor attributes that provide information about the specific location to which the actor has been assigned and the position of the actor within this location. These temporary actor attributes can be used to address specific actors when defining a second location type. Each of the temporary actor attributes begins with the name of the location label.

The following list shows all magic actor attributes:

  • actor.LOCATIONLABEL: An identifier of the location the actor was assigned to.

  • actor.LOCATIONLABEL_assigned: A bool which is True if the actor was assigned to this location class.

  • actor.LOCATIONLABEL_id: An identifier of the sub-location instance.

  • actor.LOCATIONLABEL_position: An identifier of the agent’s position within the location. The first actor gets a 0.

  • actor.LOCATIONLABEL_head: A bool which is True if the actor is the first actor within the location.

  • actor.LOCATIONLABEL_tail: A bool which is True if the actor is the last actor 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 actor from each cluster and connect them to the location class Bridge. To select only one actor from each cluster, we filter by the magic actor attribute actor.Cluster_head that was set when the clusters were created. This way, only the first actor of each cluster is assigned to the Bridge location.

[65]:
env = p2n.Environment()
creator = p2n.Creator(env)
inspector = p2n.NetworkInspector(env)


class Cluster(p2n.LocationDesigner):
    n_locations = 3


class Bridge(p2n.LocationDesigner):
    def filter(self, actor):
        return actor.Cluster_head


creator.create_actors(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.

[66]:
class Cluster(p2n.LocationDesigner):
    n_locations = 3


class Bridge(p2n.LocationDesigner):
    def filter(self, actor):
        return actor.Cluster_head


creator.create_actors(n=30, clear=True)
creator.create_locations(
    location_designers=[
        Bridge,
        Cluster,
    ],
    clear=True,
)

inspector.plot_networks()