Network

../_images/network.svg

At the core of any Phantom environment is the network. This defines the relationships between all actors and agents, controls who can send messages to who and handles the way in which messages are sent and resolved.

Each actor and agent, or ‘entity’, in the network is identified with a unique ID. Usually a string is used for this purpose. Any entity can be connected to any number of other actors and agents. When two entities are connected, bi-directional communication is allowed. Connected entities also have read-only access to each others View’s allowing the publishing of information to other entities without having to send messages.

Consider a simple environment where we have some agents learn to play a card game. We have several ‘PlayerAgent’s and a single ‘DealerAgent’.

Creating the Network

The Network class is always initialised in the __init__() method of our environments. First we need to gather all our actors and agents into a single list:

players = [PlayerAgent("p1"), PlayerAgent("p2"), PlayerAgent("p3")]

dealer = DealerAgent("d1")

agents = players + [dealer]

Next we create our network with the list of agents. By default a BatchResolver is used as the message resolver. Alternatively a custom Resolver class instance can be provided.

network = ph.Network(agents)

Our agents are now in the network and we must now define the connections. We want to connect all our players to the dealer. In this environment players do not communicate to each other. We can manually add each connection one by one:

network.add_connection("p1", "d1")
network.add_connection("p2", "d1")
network.add_connection("p3", "d1")

Or we can use one of the convenience methods of the Network class. The following examples all acheive the same outcome. Use whichever one works best for your situation.

network.add_connections_from([
    ("d1", "p1"),
    ("d1", "p2"),
    ("d1", "p3"),
])
network.add_connections_between(["d1"], ["p1", "p2", "p3"])
network.add_connections_with_adjmat(
    ["d1", "p1", "p2", "p3"],
    np.array([
        [0, 1, 1, 1],
        [1, 0, 0, 0],
        [1, 0, 0, 0],
        [1, 0, 0, 0],
    ])
)

Accessing the Network

The easiest way to retrieve a single actor/agent from the Network is to use the subscript operator:

dealer = network["d1"]

The Network class also provides three methods for retrieving multiple actors/agents at once:

players = network.get_actors_with_type(PlayerAgent)
dealer = network.get_actors_without_type(PlayerAgent)

odd_players = network.get_actors_where(lambda a: a.id in ["p1", "p3"])

StochasticNetwork

Phantom has a StochasticNetwork class that implements connection sampling once top of the standard Network class where each connection has a strength 0.0 <= x <= 1.0. Every time the network’s reset() method is called connections are created or destroyed randomly, weighted by the connection’s strength.

../_images/stochastic-network.svg
agents = [
    Agent("a1"),
    Agent("a2"),
    Agent("a3"),
]

network = StochasticNetwork(agents)

# Agent a1 will be connected to agent a2 in 50% of episodes:
network.add_connection("a1", "a2", 0.5)

# Agent a1 will always be connected to agent a3:
network.add_connection("a1", "a3", 1.0)

# This is equivalent as the default connectivity is 1.0:
network.add_connection("a1", "a3")

# Agent a2 will never be connected to agent a3. There is no real reason to do this:
network.add_connection("a2", "a3", 0.0)

# This samples the connectivities, creating new combinations of connections:
network.reset()

# This has a 50% chance of returning True:
print(network.has_edge("a1", "a2"))

Message Resolution

The process of resolving messages is configurable by the user by choosing or implementing a Resolver class. The default provided resolver class is the BatchResolver and should adequately cover most typical use-cases. It works as follows:

1. Agents send messages and the network checks that the message is being sent along a valid connection before passing the message to the resolver:

../_images/batch-resolver-1.svg

2. The BatchResolver gathers messages sent from all agents into batches based on the message’s recipients. The BatchResolver can optionally shuffle the ordering of the messages within each batch using the shuffle_batches argument.

../_images/batch-resolver-2.svg

3. The agents receive their messages in batches via the Agent class handle_batch() method. By default, these are then automatically distributed to and handled with the handle_message() method.

../_images/batch-resolver-3.svg
  1. Any further sent messages are then resolved again and delivered.

../_images/batch-resolver-4.svg

Each step of collecting messages from the agents, batching the messages and then delivering the messages is known as a ‘round’. By default the BatchResolver will continue processing infinite rounds until no messages are left to be sent. Alternatively this can be limited using the round_limit argument.