Actions & Sequencing

Last updated on 6th May 2024

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

Executing Action in Sequences

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 it's 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 within a sequence 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 isn't 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 sequences 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()
    )
  )
);

Executing Actions and Code at Set Times

In conjunction with the @ModelSettings annotation referred to here you can specify a 'kickoff' and 'end' time. This allows you to make usage of a set of functions to manage your sequencing of actions.

  • firstStep(Sections ...) works exactly the same as run() however this will only execute on the first tick.
  • lastStep(Sections ...) works exactly the same as run() however this will only execute on the last tick as defined by the end annotation in @ModelSettings
  • kickOff(Sections ...) works exactly the same as run() however this will only execute on the kickoff tick as defined by the kickoff annotation in @ModelSettings
  • done() this is an optional function within the lifecycle that will be called either once the model has reached the end if specified or if the finish() function has been called. Unlike the above this function is overwritten and not contained within the step function.

With this you can easily setup a model flow that will make for a more readable and understandable sequencing of actions as demonstrated below.

Complex Time Control (Java)

@Override
public void step() {
	super.step();
	
	firstStep(
		AgentA.startupAction(), 
		AgentB.startupAction()
	);
	
	kickOff(
		AgentA.kickOffAction(), 
		AgentB.kickOffAction()
	);
	
	run(
		AgentA.sendMessageAction(), 
		AgentB.recieveMessageAction()
	);
	
	lastStep(
		AgentA.finalAction(), 
		AgentB.finalAction()
	);
}

@Override
public void done() {
	super.done();
	...
	// execute Java code here
	...
}

While first/last and set kickoff times are made available, you are also able to get the current tick and write any additional checks during a standard run() action if your model has additional actions that require a specific timing.