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.
All messages sent must extend Message. There are a number of ways in which you may do so, each suitable for different scenarios.
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.
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.
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.
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
Registering messages
(Java)
public class MyClass extends AgentBasedModel<GlobalState> {
...
registerMessageTypes(Boolean.class, Messages.Vacancy.class);
...
}
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.
getLinks()
must be called providing the link class you want to send messages alongsend()
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 {}
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 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);
}
);
}
}
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.
The method deferTask
is a member of the interface Environment
, see its declaration below.
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.
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
is a Java Interface. It is used to define Messages that are processed automatically right at the beginning of the phase, before the Actions.
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
.
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;
}
}