Last updated on 16th July 2024
Models very quickly grow to have lots of actions with splits in a single sequence, and adding the logic for the Agents in the same place makes the sequence of actions hard to follow.
Actions should be created by methods inside the agent the action is for. The methods should have clear names indicating what the actions purpose is so that the sequence of steps becomes a sequence of step names rather than a sequence of lots of hard to read code.
An example of the Action being defined inside the Agent
public class Flat extends Agent<GlobalState> {
//Function sendVacancyMessage returns an Action
public Action<Flat> sendVacancyMessage() {
return Action.create(
Flat.class,
flat -> {
if (!flat.isOccupied()) {
flat.broadcastMessage(new Messages.Vacancy(flat.size));
}
});
}
}
// The sequence calls the method instead of defining the logic.
public class myModel extends AgentBasedModel<GlobalState> {
@Override
public void step() {
run(
Action.create(Flat.sendVacancyMessage())
);
}
}
As can be seen below the code becomes easy to read because the steps are all defined inside the agents.
Running a number of methods that return actions (Java)
run(
Split.create(
Flat.sendVacancyMessage(),
Household.bidIfHomeless(),
Sequence.create(
Household.giveUpFlat(),
Flat.notifyBucketOfVacancy()
),
Bucket.processBidsAndVacancies()
...
);
)
Once the actions are all being defined inside the agent classes, as the agents grow and the number of methods being defined for the agents increases, there starts to be lots of duplication in the code that can be avoided.
To avoid this, when an Agent class is created, it should be created with a static action method.
Static method that returns an action (Java)
public class Flat extends Agent<GlobalState> {
// Static action method
public static Action<Flat> action(SerializableConsumer<Flat> action) {
return Action.create(Flat.class, action)
}
}
This action method can now be used when creating the methods that return actions.
(Java)
public class Flat extends Agent<GlobalState> {
public static Action<Flat> action(SerializableConsumer<Flat> action) {
return Action.create(Flat.class, action);
}
public Action<Flat> sendVacancyMessage() {
return action(
flat -> {
if (!flat.isOccupied()) {
flat.broadcastMessage(new Messages.Vacancy(flat.size));
}
});
}
}
An alternative way of using this action method is from inside the Sequence, defining the methods that return actions as methods that can be called from inside the actions.
(Java)
public class Flat extends Agent<GlobalState> {
public static Action<Flat> action(SerializableConsumer<Flat> action) {
return Action.create(Flat.class, action);
}
public void sendVacancyMessage() {
if (!flat.isOccupied()) {
flat.broadcastMessage(new Messages.Vacancy(flat.size));
}
}
}
// Use the static action method from inside the sequence
run(
Split.create(
Flat.action(Flat::sendVacancyMessage),
...
)
);
When creating classes for the messages being sent, all message classes (used by the entire model) should be defined in a shared message class.
Creating them inside Agents, the Model or loose as their own separate class should be avoided.
Creating static nested message classes (Java)
public class Messages {
public static class Vacancy {
public double size;
public Vacancy(boolean size) {
this.size = size;
}
}
public static class Bid {
public double deposit;
public Bid(double deposit) {
this.deposit = deposit;
}
}
}
If multiple agent types need to share some common functionality, while having their own unique functionality and uses, it may be helpful to introduce inheritance, in which a superclass is created to hold the shared functionality.
For example, a Hotel and a Flat have some common functionality, but they also have some differences. Create an Accommodation class, for Hotel and Flat to extend. Accommodation can be used now when specifying an action for all Hotels and Flats.
public class Accomodation extends Agent<GlobalState> {
public int size;
public Action<Accomodation> sendVacancyMessage() {
return Action.create(
Accomodation.class,
accomodation -> {
// if vacant, send message ...
});
}
}
public class Flat extends Accomodation {
public Action<Flat> registerNewHousehold() {
return Action.create(
Flat.class,
flat -> {
// new household registration ...
});
}
}
public class Hotel extends Accomodation {
public Action<Hotel> registerBooking() {
return Action.create(
Hotel.class,
hotel -> {
// new booking ...
});
}
}
public class myModel extends AgentBasedModel<GlobalState> {
@Override
public void step() {
run(
// Some steps are for both Hotel and Flat
Accomodation.sendVacancyMessage(),
Household.requestHome(),
Split.create(
// Some steps are only for Hotel/Flat
Flat.registerNewHousehold(),
Hotel.registerBooking()
)
);
}
}
Functionality, or methods that are not agent specific but are for general tasks that are needed by one or more agents or by the model shouldn't be placed together with the agents, but should be in a class of their own.
Examples of this may be a data loader that an agent might use to load data to spawn new agents with, or a distribution class which builds distributions based on some input/loaded data.
Static methods should be created for these, so the methods can be accessed easily from anywhere in the model
public class Distribution {
public static EmpiricalDistribution loadDistribution(){
// load distribution
}
}
public class Flat extends AgentAge {
public Action<Flat> registerNewHousehold() {
return Action.create(
Flat.class,
flat -> {
EmpiricalDistribution distribution = Distribution.loadDistribution();
// use distribution
});
}
}