Actions and Sequencing

Last updated on 22nd April 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
);

On the type of data supported

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.

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

Every message sent has a body, which is an object where the message data is contained. This object can be any type, including Java types, or custom classes, depending on if the data being sent is a single value or a complex object.

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

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

broadcastMessage is used to send messages to an agent's links, with the first parameter being the message body.

In the examples below, the message being sent is always of type Boolean, because the only information to be sent is if the Cell is alive or not.

(Java)

public class Cell extends Agent<GlobalState> {
  public boolean alive = false;

  public Action<Cell> sendAliveMessage() {
    return Action.create(Cell.class, cell -> {
      // send message to all links
      cell.broadcastMessage(cell.alive);

      // send message only to links of specific type
      cell.broadcastMessage(cell.alive, OtherAgent.class);
    });                        
  }
}

An example of sending a message with a more complex body might be a case where there is more than one piece of data to send in a message.

(Java)

public class Cell extends Agent<GlobalState> {
  public boolean alive = false;
  public int age = 0;

  public Action<Cell> sendAliveMessage() {
    return Action.create(
      Cell.class,
      cell -> {
        cell.broadcastMessage(new CellStatus(cell.alive, cell.age));
      }
    );
  }
}

public class CellStatus {
  public CellStatus(boolean alive, int age) {}
}

If an agent has the id of another agent (even if it is not directly connected/linked to this agent in any way), it can send it a message using sendMessage.

To get the id of an agentB that isn't connected to agentA via a link, send the id of agentB to agentA via a message.

(Java)

public class Cell extends Agent<GlobalState> {
  public boolean alive = false;
  public Action<Cell> sendAliveMessage() {
    return Action.create(
      Cell.class,
      cell -> {
        cell.sendMessage(cell.alive, someId);
      }
    );
  }
}

Receiving Messages

Receiving messages is driven by message type. An agent has to specify what type of message they are expecting, in order to get those messages.

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<Boolean> isAliveMessage = cell.getMessageOfType(Boolean.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<Message<Boolean>> aliveMessages = cell.getMessagesOfType(Boolean.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 -> {
        Boolean hasAliveMessage = cell.hasMessageOfType(Boolean.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.