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.
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
);
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())
)
);
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())
)
);
In all the sequences we have shown until now, the actions happen one after the next, like in the following diagram.
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.
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.
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
(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
.
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()
)
)
);
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.
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
Registering messages
(Java)
public class MyClass extends AgentBasedModel<GlobalState> {
...
registerMessageTypes(Boolean.class, Messages.Vacancy.class);
...
}
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 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);
}
);
}
}