Actions and Sequencing

Last updated on 16th July 2024

Actions and Sequences are used to define a sequence of actions for the agents to follow at every step, or at setup.

A Sequence is a list of actions that are executed consecutively. Sequences are composable, which means a sequence can be a list of other sequences as well as actions. Multiple sequences can be run in a single setup() or step().

Every sequence needs to be run in order for the sequence to be executed.

Creating and running an empty Sequence (Java)

public class MyClass extends AgentBasedModel<GlobalState> {
  ...

  run (Sequence.create());

  ...
}

Actions have to be created for a specific class. It is best practice to create actions inside the class of the Agent the action is being defined for.

When creating an action, two parameters need to defined.

  1. The agent that will execute this action
  2. The function to be executed by the agent

Creating actions and adding them to a sequence (Java)

public static Action<AgentA> sendMessageToAgentB =
  Action.create(
    AgentA.class, agentA -> { /** send message */ });

public static Action<AgentB> recieveMessage =
  Action.create(
    AgentB.class, agentB -> { /** recieve message */ });

run (
	Sequence.create(
  	sendMessageToAgentB,
  	recieveMessage
));

The run method can also take a list of actions which it will execute as though its a sequence, so the above code could also be written like this.

(Java)

public static Action<AgentA> sendMessageToAgentB =
  Action.create(
    AgentA.class, agentA -> { /** send message */ });

public static Action<AgentB> recieveMessage =
  Action.create(
    AgentB.class, agentB -> { /** recieve message */ });

run (
  	sendMessageToAgentB,
  	recieveMessage
);
An action will only be executed by an agent if that agent has received a message. If an action needs to be executed by all agents of a type, even if some of those agents have not received a message, this action needs to be at the beginning of a new sequence.

Message passing(Java)

//If AgentB isnt sent message, it will not do other processing
run (
  Sequence.create(
    AgentA.conditionallySendMessage(),
    AgentB.recieveMessageActionAndDoOtherProcessing())
  ),

  //Split other processing into another sequence to ensure all AgentB do processing
  Sequence.create(
    AgentA.conditionallySendMessage()),
    AgentB.recieveMessageAction())
  ),

  Sequence.create(
    AgentB.doOtherProcessing())
  )
);

Messages cannot be passed between sequence runs

All message passing and receiving needs to be done within a single sequence run. (This does not apply to sequences that are composed of other sequences, messages will be passed between sequences called from inside another sequence.)

Message passing (Java)

//Message will be passed from AgentA to AgentB
run (
  Sequence.create(
    Sequence.create(AgentA.sendMessageAction()),
    Sequence.create(AgentB.recieveMessageAction())
  ),

  //Message will NOT be passed from AgentA to AgentB
  Sequence.create(
    AgentA.sendMessageAction())
  ),

  Sequence.create(
    AgentB.recieveMessageAction())
  )
);

Executing Action in Parallel

In all the sequences we have shown until now, the actions happen one after the next, like in the following diagram.

line

It is also possible to have two actions happening at the same time, as in the diagram below. Here, AgentA can send a message that both AgentB and AgentC receive and process at the same time. They can then both send a message to AgentA to process.

split

Put the actions that need to happen in parallel in a split.

As for other sequence runs, message cannot be passed between splits.

Split (Java)

run (
    AgentA.doAction1(),
    Split.create(
      AgentB.doAction1(),
      AgentC.doAction1()
    ),
    AgentA.doAction2())
);

It is also possible to want to do several actions in parallel, or sequences of their own in parallel.

complex split

Multi-Split(Java)

run (
    AgentA.doAction1(),
    Split.create(
      AgentB.doAction1(),
      Sequence.create(
        AgentC.doAction1(),
        AgentB.doAction2()
      )
    ),
    AgentA.doAction2())
);

The sequence on which actions will be processed in this case is

  1. AgentA process action1,
  2. AgentB and AgentC process action1
  3. AgentB process action2
  4. AgentA process action2 (having received messages from both AgentB and AgentC)
In all the examples above, doAction includes sending a message to the next agent in the sequence

Best Practice

In cases where there are sequences inside splits, or splits inside splits, the model will be a lot easier to work with if sequences and splits were created as variables

(Java)

// nested sequence removed and created as a variable
private Sequence agentCActions =
  Sequence.create(
    	AgentC.doAction1(),
      AgentB.doAction2()
    )

// main sequence is now easier to read
run (
  Sequence.create(
    AgentA.doAction1(),
    Split.create(
      AgentB.doAction1(),
      agentCActions
    ),
    AgentA.doAction2())
  )
);

It is possible to compose sequence's and actions to create the complex sequences as needed. Example of more complex sequence - with a sequence inside a sequence.

splitception

Complex Split (Java)

private Sequence subSequence =
  Sequence.create(
    AgentB.doAction2(),
    Split.create(
      AgentC.doAction1(),
      AgentA.doAction2()
    ),
    AgentD.doAction1())
	);

run (
  Sequence.create(
    Split.create(
      subSequence,
      AgentB.doAction1()
    )
  )
);

Messaging Semantics

The Simudyne SDK adopts the Pregel approach to graph processing. This means that all computation is driven by messages. At every action in the sequence, every agent will process its action only if it has received a message.

In addition to this, Pregel adopts a pure message passing model that eliminates the need of shared memory and remote read, so all sharing of information between agents is done by message passing.

Message Types

All messages sent must extend Message. There are a number of ways in which you may do so, each suitable for different scenarios.

Empty messages

For messages that have no value one can extend Message.Empty. This is useful if your message type alone is sufficient to convey the required semantics.

Single primitive type messages

For messages that contain only a single primitive value, the Simudyne SDK provides, Integer, Long, Float, Double, and Boolean message classes that can be extended. They all provide a getBody() method that returns the primitive value. Use this whenever you need to send only one field.

Complex data types

For messages that require sending complex data types extend Message.Object<T>. This will allow you to send a message who's body is the object. It also allows the receiver access to methods on the object.

Messages with arbitrary fields

If your message needs to send an arbitrary amount of data, but no explict type exists, consider extending Message

For organising custom message classes, see Message Organisation

When running the model in distributed mode, all message types need to be registered with the model. This isn't necessary if running the model locally.

Registering messages

(Java)

public class MyClass extends AgentBasedModel<GlobalState> {
  ...
  registerMessageTypes(Boolean.class, Messages.Vacancy.class);
  ...
}

Sending Messages

A fluent API provides the capability to filter and customise messages sent based on the link the message will be sent along.

All message sending is done by the agents, and so can only be done from inside an agent class.

Messages can be sent along specific links that an agent holds. Methods must be chained in a particular order.

  • Firstly, getLinks() must be called providing the link class you want to send messages along
  • Secondly, send() must be called, which takes the message class and possibly either a biconsumer or message body, used to construct the message. This will send the messages along the previously got links.

Below shows how to use a generic message class.

Generic message along a link (Java)

getLinks(Links.Neighbour.class)
         .send(Messages.Alive.class, (message, link) -> message.alive=true);

We see the send message takes as second argument a BiConsumer allowing access to link data while constructing the message. In this example we did not require accessing any data on the link; however, it is available if needed. Note, our message class looks like so.

 public static class Alive extends Message {
    boolean alive;
  }

However, if our message only contains a single primitive field, like above, extend one of the specialised message classes. Below, a value given is assigned into the body of the constructed message automatically.

Specialised messages

cell.getLinks(Links.Neighbour.class)
    .send(Messages.Aliveness.class, cell.alive)

Where we now extend Message.Boolean.

 public static class Alive extends Message.Boolean {}

Sending messages directly to agents.

Messages can also be sent directly to an agent, by providing the id associated with the recipient. This is useful when you wish to employ a request-response pattern, where an agent replies directly to the sender of a message.

Request-response pattern (Java)

Messages.Bid bid = seller.getMessageOfType(Messages.Bid.class);
if (bid.getBody() > minBid) {
  seller.send(Messages.Asset.class)
        .to(bid.getSender());
}

It is useful to know that you can succinctly filter the list of links getLinks returns. Simply chain filter on the end of getLinks and pass in the predicate you wish to filter on.

Filtering links (Java)

bank.getLinks(Links.MortgageLinks)
  .filter(mortgageLink -> mortgageLink == isActive)
  ...

This will return only mortgages that are currently active.

Receiving Messages

Receiving messages is driven by the message types. Any message received is the message type itself.

For getting a single message. If there are multiple messages of this type, it will return one of them (at random).

Receiving Messages (Java)

class Cell extends Agent<GlobalState> {
  public Action<Cell> getAliveMessage() {
    return Action.create(
      Cell.class,
      cell -> {
        Message<Messages.Aliveness> messages = cell.getMessageOfType(Messages.Aliveness.class);
        boolean isAlive = isAliveMessage.getBody();
      }
    );
  }
}

If expecting multiple message of a particular type use getMessagesOfType. This will return the messages in a List.

Receiving a List of Messages (Java)

class Cell extends Agent<GlobalState> {
  public Action<Cell> getAliveMessage() {
    return Action.create(
      Cell.class,
      cell -> {
        List<Messages.Aliveness> aliveMessages = cell.getMessagesOfType(Messages.Aliveness.class);
        long count = aliveMessages.stream().filter(Message::getBody).count();
      }
    );
  }
}

hasMessage can be used to check if a message of a particular type has been received.

Checking for messages of given type (Java)

class Cell extends Agent<GlobalState> {
  public Action<Cell> getAliveMessage() {
    return Action.create(
      Cell.class,
      cell -> {
        Messages.Aliveness messages = cell.hasMessageOfType(Messages.Aliveness.class);
      }
    );
  }
}

Replying to a message

Use getSender on a message to get the id of the agent who sent the message, and use that id with sendMessage to send a message back to sender.

Pre & Post Phase Processing

You have the possibility to apply certain modifications on an Agent before the beginning of a phase or after its end .

The SystemMessage is a new subtype of Message that are processed before the beginning of the Actions. You can use them to apply changes before the phase even begins.

The new method deferTask(Consumer< Vertex< ? extends Serializable>>) allows to postpone the application of a modification after the end of the phase.

deferTask

The method deferTask is a member of the interface Environment, see its declaration below.

Declaration of deferTask() in Environment (Java)
public interface Environment<S extends Serializable> {
...
...

	void deferTask(Consumer<Vertex<S>> dataInjector);
}

You can call this method from an Agent, and use a lambda as argument. The lambda will be processed right after the Actions, at the end of the phase.

Example of use of deferTask() (Java)
deferTask(vertex -> vertex.getLinks().get(0).remove());

This will remove a Link of the Vertex at the end of the phase, but the Link would still exist and would still be available during this phase.

SystemMessage

SystemMessage is a Java Interface. It is used to define Messages that are processed automatically right at the beginning of the phase, before the Actions.

The SystemMessage Interface (Java)
public interface SystemMessage {
  boolean receivedByVertex(Agent<? extends GlobalState> agent, Environment<? extends GlobalState> env);
}

The only method you must implement is receivedByVertex. This method will be called by the Agent receiving the message. You must describe the modification you want to apply on the Agent receiving the message in this method (note that the argument agent represent the Agent receiving the message).

The type of return of this method is a boolean deciding whether or not the Agent will handle its Actions. If an Agent receives a SystemMessage with a receivedByVertex returning false, then its Actions will not be processed for this phase. If you do not wish to prevent the execution of the Actions, return true for all your receivedByVertex methods. Note that you only need one receivedByVertex returning false to prevent the Actions being processed, even if all the other receivedByVertex methods called by the Agent return true.

SystemMessages are used for lot of purposes
Be careful when using a SystemMessage , as some other functionalities are using this method. For instance, the PoisonPill feature is using SystemMessage. You should therefore be careful if you try to access datas with a SystemMessage as Agents may be in an 'intermediate' state and not ready yet for Actions.
Example of custom SystemMessage (Java)
public static class MyMessage extends Message implements SystemMessage {
    public int field1;
    public int field2;

    @Override
    public boolean receivedByVertex(Agent<? extends GlobalState> agent, Environment<? extends GlobalState> env) {
      return true;
    }
  }
SystemMessages cannot begin a phase
A SystemMessage will not be put into the Agent's inbox. This allows you to send 'invisible' messages to an Agent, as the Agent will not react to it. For instance, you could send a SystemMessage to an `Agent` to update one of its fields, and do so without triggering a phase for this `Agent` (because his inbox would still be empty).