Friday, November 29, 2013

Modeling a simple system in Mason...

In this post I'm describing how to implement a simple agent model in Mason multiagent simulation environment. See introductory post for additional details about this endeavour.

Installing Mason

Mason installation is easy. Just download the newest archive and unpack it somewhere on the disk. That's all that has to be done. In the following text I'm referring to this unpacked installation, and anything done is done within that directory. It doesn't have to, but it is easier for a start.

Running simulation

The next thing is how to run Mason simulation. But it turns out to be easy. As an example I'll show you how to run Tutorial2 example. This example simulates Conway's game of life and has a GUI that can be used to control the simulation. So, go to the directory where you unpacked archive that you've downloaded in the previous step and then enter sim/app/tutorial1and2 subdirectory. Java file is already precompiled, but nevertheless, we'll compile it again because it is easy and instructive. To compile Tutorial2 issue the following command:
CLASSPATH=../../../jar/mason.17.jar javac Tutorial2.java
Note that Mason framework is in mason.17.jar and that you have to specify it to Java compiler using CLASSPATH variable. The previous command shouldn't give you any messages. To run compiled example, issue the following command:
CLASSPATH=../../../jar/mason.17.jar:. java sim.app.tutorial1and2.Tutorial2
All in all, compiling and running models built using Mason framework is relatively straightforward.

Evolving the target system

The idea I'll pursue in this section is to gradually build a simulation system. The simulation system will be represented by one class that will instantiate and control all the other classes. Those other classes I'll call agents. There will be an agent that represents a job, one for server(s) and one for a queue that will hold jobs until the server is free to take them.

The simplest possible simulation

We'll start with the simplest possible simulation in Mason, and that is the following one:
package hr.fer.zemris.queue;

import sim.engine.*;

public class QueueSystem extends SimState
{
    public QueueSystem(long seed)
    {
        super(seed);
    }

    public static void main(String[] args)
    {
        doLoop(QueueSystem.class, args);
        System.exit(0);
    }
}
To compile it you have to place it into hr/fer/zemris/queue directory (corresponds to package statement at the beginning of the source). I'll assume that this directory is in the mason's toplevel directory. The name of the Java file has to be QueueSystem.java. In order to compile it, issue the following command:
CLASSPATH=jar/mason.17.jar javac hr/fer/zemris/queue/QueueSystem.java
and run it in the following way:
$ CLASSPATH=jar/mason.17.jar:. java hr/fer/zemris/queue/QueueSystem
MASON Version 17.  For further options, try adding ' -help' at end.
Job: 0 Seed: -1713501367
Starting hr.fer.zemris.queue.QueueSystem
Exhausted
Don't forget that dot at the end of the CLASSPATH variable's value, or else, you'll get an error about being unable to find a class.

This simulation is a very simple one and, as expected, it doesn't do anything useful. All it does is call doLoop method of SimState class which will instantiate QueueSystem object. In our case, we didn't specify anything for the simulation, so nothing happens.

In the following text this simulation will be extended so that it create and coordinate other agents.

First agent

Ok, let's create an agent. Our initial agent will, again, be very simple. It will only print it was instantiated, but nothing else. So, here it is:
package hr.fer.zemris.queue;

import sim.engine.*;

public class Server implements Steppable
{
    public Server()
    {
        System.out.println("Instantiated one Server");
    }

    public void step(final SimState state)
    {
        System.out.println("step() method called");
    }
}
Note that we have to define step() method, because it is required by Steppable interface. But, for the moment, it doesn't do anything.

Ok, to compile this agent, use the usual command:
CLASSPATH=jar/mason.17.jar javac hr/fer/zemris/queue/Server.java
Again, I assumed that you are positioned into mason's root directory, the agent is placed within hr/fer/zemris/queue directory and it is called Server.java.

Note that you can not directly run agents, at least not in this form (i.e. without main method). So, we'll instantiate and schedule execution of our agent in the main class that represents the whole simulation. The change is simple, in the class QueueSystem.java add the following method:
public void start()
{
    super.start();

    Server server = new Server();
    schedule.scheduleOnce(server);
}
Now, recompile QueueSystem.java class, and run it:
$ CLASSPATH=jar/mason.17.jar:. java hr/fer/zemris/queue/QueueSystem
MASON Version 17. For further options, try adding ' -help' at end.
Job: 0 Seed: -1710667392
Starting hr.fer.zemris.queue.QueueSystem
Instantiated one Server
step() method called
Exhausted
Note the lines in bold. First line is printed when constructor of our simple agent was called. The second one is outputted when agent's step() method was called. Note that step method was called only once, and that is because we used method scheduleOnce() that schedules a single occurrence of an event. Try to change scheduleOnce() into scheduleRepeating() and see what will change.

There is also a question of when this event was called. We used a simple version of schedule methods that schedule execution 1 time unit in the future, i.e. in getTime() + 1.0. Well, at least documentation says so! Try to check it by youself. Hint: to get current time in agent's step() method use state.schedule.getTime() method.

Creating jobs

Jobs are a bit different. They are not created at the start of the simulation, but instead are created dynamically according to Poisson distribution. So, what I'm going to do is to create class named JobFactory that will create Job. Each job will be represented using the following class:
package hr.fer.zemris.queue;

import sim.engine.*;

public class Job
{
    public double createTime;
    public double processingTime;
    public double finishTime;
}
Note that job isn't agent! It doesn't have step() method neither it's subclassed from some Mason's class. What I decided is that Job class will only have fields to keep statistical data and that's it.

To create jobs, I written JobFactory agent. Here is the agent:
package hr.fer.zemris.queue;

import sim.engine.*;
import sim.util.distribution.*;
import ec.util.MersenneTwisterFast;

public class JobFactory implements Steppable
{
    private Poisson poisson;
    private Exponential exponential;
    private QueueSystem queueSystem;

    public JobFactory(double lambda, double mu, QueueSystem qs)
    {
        MersenneTwisterFast randomGenerator = new MersenneTwisterFast();
        poisson = new Poisson(lambda, randomGenerator);
        exponential = new Exponential(mu, randomGenerator);
        queueSystem = qs;
    }

    public void step(final SimState state)
    {
        double currentTime = state.schedule.getTime();
        double nextEventTime = currentTime + poisson.nextDouble();

        Job job = new Job();
        job.createTime = currentTime;
        job.processingTime = exponential.nextDouble();
        queueSystem.pushNewJob(job);

        state.schedule.scheduleOnce(nextEventTime, this);
    }
}
So, how this JobFactory agent works? First, we have a constructor. Constructor instantiates two classes, Poisson and Exponential, that will be used to generate random numbers from respective distributions. The first two parameters of the constructor define distributions' mean values. The third parameter is used for sending newly created jobs into a system queue.

Note that, apart from generating new jobs according to the Poisson distribution, we also have to specify for how long will a single job be processed within the server. I think that a natural place to determine this is when the job is created since it is the characteristic of the job itself.

I thought about sending Job objects directly to the server agent. But the problem with that approach is that server has to schedule itself in case there are no other jobs waiting, i.e. the job immediately enters server. Namely, server has to wake up when some job is finished and remove it from the system.

But, in order to be able to do scheduling I had to have access to SimState object, which is accessible only from step() method. Now, I could save this object internally, but it would be a hack. Namely, I would have to somehow provoke step() to be executed immediately at the beginning. Oh, yeah, I could send SimState object via constructor. But in the end, I gave up from pursuing this approach as I haven't been able to find someone else already doing this (nor in the examples directory, nor on the Internet).

The second part of the JobFactory class, and the its workhorse, is the method step(). What this method does is create a new Job class initializes its processing time (job.processingTime) and adds it to the queue of jobs waiting for the server (via call to the method queueSystem.pushNewJob). Finally, this method draws new random number for the Poisson distribution which defines when a new job will be created. It schedules itself at that point in time.

Ok, our simulation class, QueueSystem, has to have a method for accepting new jobs. This method has name pushNewJob, and the code is the following:
public void pushNewJob(Job job)
{
    jobQueue.add(job);

    if (jobQueue.size() == 1)
        schedule.scheduleOnce(schedule.getTime() + job.processingTime, server);
}
jobQueue is a linked list, i.e. FIFO queue, that is used to hold jobs while being processed in Server and waiting for the Server. The job that is in front of the queue is the job that is currently processed by Server. Maybe I should have written code a bit differently, i.e. so that Server holds the job it processes in some internal attribute, but I did it this way and I didn't bother to rewrite it.

Apart from adding new job to a queue there is one additional thing I had to do. In case there is no job in queue, that means the server is idle, and it is not scheduled for the execution! So, the if statement checks this condition, and if the server is idle it schedules its execution when jobs is finished! Otherwise, server will execute at some point and it will take next job and schedule itself. We'll come to that part a bit later.

One more thing hasn't been specified with respect to QueueSystem, namely jobQueue and activation of JobFactory. Server isn't activated until there is a job, and that is handled by pushNewJob method.

So, in order to take care of that case, here is the new start() method of QueueSystem simulation/class:
public void start()
{
    double alpha = 3;
    double beta = 5;

    super.start();

    jobQueue = new LinkedList<job>();

    server = new Server(jobQueue);

    jobFactory = new JobFactory(alpha, beta, this);
    schedule.scheduleOnce(jobFactory.getFirstInvocationTimeStamp(), jobFactory);
}
So, what's going on in this method. There are alpha and beta parameters for M/M/1 queue. Next, I'm initializing FIFO queue, jobQueue. It's defined as follows as a QueueSystem's class atribute:
Queue<job> jobQueue;
Then, server agent is instantiated. Note that I'm sending queue to server. That is necessary since server has to take jobs from a queue. I'm also instantiating JobFactory agent. Finally, I'm scheduling initial run of JobFactory.

There is a small probelm. Namely, I have to schedule first invocation according to Poisson distribution. It is not correct to invoke it immediately, at least not in the form I wrote it. And, this class, QueueSystem, doesn't have access to poison distribution in order to get first random number. It would be also error to create another Poisson distribution. So, I added a method to JobFactory class/agent that will return me first random number. It is the following method:
public double getFirstInvocationTimeStamp()
{
    return exponential.nextDouble();
}
and you should place it in JobFactory agent/class.

Ok, the final piece of puzzle, Server agent. First, constructor is now a bit different, namely, it has to take queue reference:
public Server(Queue jq)
{
    jobQueue = (LinkedList)jq;
}
step() method is also a bit more involved:
public void step(final SimState state)
{
    Job job = jobQueue.remove();
    job.finishTime = state.schedule.getTime();

    jobs++;
    systemTimeAvg = systemTimeAvg + (job.finishTime - job.createTime - systemTimeAvg) / jobs;
    jobNumberAvg = jobNumberAvg + (jobQueue.size() - jobNumberAvg) / jobs;
    currentStep++;
    if (skipSteps == currentStep) {
        System.out.println(systemTimeAvg + " " + jobNumberAvg);
        currentStep = 0;
    }

    if (jobQueue.size() > 0) {
        job = jobQueue.peek();
        state.schedule.scheduleOnce(state.schedule.getTime() + job.processingTime, this);
    }
}
What does this method do? First, it pops a job from the front of the queue, the job that was processed within the server. Then, it updates and prints some statistics. Finally, it checks if there is another job in the queue, and if it is, it schedules invocation of itself when that particular job has to finish.

Basically, that's it.

2 comments:

Stewart said...

Hi,

I tried to get this to work but I am having problems with errors cropping up when JobFactory is created.

Stewart said...

package sim.algQueue;

import sim.engine.*;

public class Server implements Steppable {
/**
*
*/
private static final long serialVersionUID = 1L;

public Server(Queue jq) {
jobQueue = {LinkedList} jq;
System.out.println("Instantiated one Server");
}

@Override
public void step(final SimState state) {
Job job = jobQueue.remove();
job.finishTime = state.schedule.getTime();

jobs++;
systemTimeAvg = systemTimeAvg + (job.finishTime - job.createTime
- systemTimeAvg) / jobs;
jobNumberAvg = jobNumberAvg + (jobQueue.size() - jobNumberAvg ) /
jobs;
currentStep++;
if (skipSteps == currentStep) {
System.out.println(systemTimeAvg + " " + jobNumberAvg);
currentStep = 0;
}
if (jobQueue.size() > 0) {
job = jobQueue.peek();
state.schedule.scheduleOnce(state.schedule.getTime() +
job.processingTime, this);

}

}

}


Here are all of the errors

About Me

scientist, consultant, security specialist, networking guy, system administrator, philosopher ;)

Blog Archive