simevents. Examples of programs that use the process view, supported by the package simprocs, are provided in the third part. Note that the process-oriented.
SSJ User’s Guide Examples of Simulation programs using SSJ Version: September 10, 2007
Pierre L’Ecuyer
1
Chaire du Canada en simulation et optimisation stochastiques D´epartement d’Informatique et de Recherche Op´erationnelle Universit´e de Montr´eal, Canada
We present examples of Java programs based on the SSJ library. The examples are commented and are an excellent starting point for learning how to use SSJ. The first part of the guide gives very simple examples that do not need event or process scheduling. The second part contains examples of discreteevent simulation programs implemented with an event view using the package simevents. Examples of programs that use the process view, supported by the package simprocs, are provided in the third part. Note that the process-oriented programs based on simprocs must be run using green threads, which are currently available only in JDK version 1.3.1 or earlier (very unfortunately).
1
SSJ was implemented with the contribution of several individuals, listed at the end of the SSJ overview document.
CONTENTS 1
Contents Summary
2
1 Some Elementary Examples
3
1.1
A discrete-time inventory system . . . . . . . . . . . . . . . . . . . . . . . .
3
1.2
A single-server queue with Lindley’s recurrence . . . . . . . . . . . . . . . .
8
1.3
Using the observer design pattern . . . . . . . . . . . . . . . . . . . . . . . .
9
1.4
Pricing an Asian option . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
2 Discrete-Event Simulation
18
2.1
The single-server queue with an event view . . . . . . . . . . . . . . . . . . .
18
2.2
Continuous simulation: A prey-predator system . . . . . . . . . . . . . . . .
21
2.3
A simplified bank . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
2.4
A call center . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
3 Process-Oriented Programs
35
3.1
The single queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
3.2
A job shop model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
3.3
A time-shared computer system . . . . . . . . . . . . . . . . . . . . . . . . .
42
3.4
Guided visits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
3.5
Return to the simplified bank . . . . . . . . . . . . . . . . . . . . . . . . . .
49
2 CONTENTS
Summary We present examples of Java simulation programs that use the SSJ library. Studying these examples is a good way to start learning about SSJ. While studying the examples, the reader can refer to the functional definitions of SSJ classes and methods in the guides of the corresponding packages. It is preferable to refer to the .pdf versions of the guides, because they contain a more detailed and complete documentation than the .html versions, which are better suited for quick on-line referencing for those who are already familiar with SSJ. The examples are collected in three groups: (1) those that need no event or process scheduling, (2) those based on the discrete-event simulation paradigm and implemented with an event view using the package simevents, and (3) those implemented with the process view, supported by the package simprocs. The three sections of the guide correspond to these groups. Some examples (e.g., the single-server queue) are carried across two or three sections to illustrate different ways of implementing the same model. The Java code of all these examples is available on-line.
3
1
Some Elementary Examples
We start with elementary examples that illustrate how to generate random numbers, compute distribution functions, and collect elementary statistics with SSJ. The models considered in this section are quite simple and some of the performance measures can be computed by (more accurate) numerical methods rather than by simulation. The fact that we use these models to give a first tasting of SSJ should not be interpreted to mean that simulation is necessarily the best tool for them.
1.1
A discrete-time inventory system
Consider a simple inventory system where the demands for a given product on successive days are independent Poisson random variables with mean λ. If Xj is the stock level at the beginning of day j and Dj is the demand on that day, then there are min(Dj , Xj ) sales, max(0, Dj − Xj ) lost sales, and the stock at the end of the day is Yj = max(0, Xj − Dj ). There is a revenue c for each sale and a cost h for each unsold item at the end of the day. The inventory is controlled using a (s, S) policy: If Yj < s, order S − Yj items, otherwise do not order. When an order is made in the evening, with probability p it arrives during the night and can be used for the next day, and with probability 1 − p it never arrives (in which case a new order will have to be made the next evening). When the order arrives, there is a fixed cost K plus a marginal cost of k per item. The stock at the beginning of the first day is X0 = S. We want to simulate this system for m days, for a given set of parameters and a given control policy (s, S), and replicate this simulation n times independently to estimate the expected profit per day over a time horizon of m days. Eventually, we might want to optimize the values of the decision parameters (s, S) via simulation, but we do not do that here. (In practice, this is usually done for more complicated models.) Listing 1: A simulation program for the simple inventory system import import import import import
umontreal.iro.lecuyer.rng.*; umontreal.iro.lecuyer.randvar.*; umontreal.iro.lecuyer.probdist.PoissonDist; umontreal.iro.lecuyer.stat.Tally; umontreal.iro.lecuyer.util.Chrono;
public class Inventory { double double double double double double
lambda; c; h; K; k; p;
// // // // // //
Mean demand size. Sale price. Inventory cost per item per day. Fixed ordering cost. Marginal ordering cost per item. Probability that an order arrives.
RandomVariateGenInt genDemand;
4 1 SOME ELEMENTARY EXAMPLES RandomStream streamDemand = new MRG32k3a(); RandomStream streamOrder = new MRG32k3a(); Tally statProfit = new Tally ("stats on profit"); public Inventory (double lambda, double c, double h, double K, double k, double p) { this.lambda = lambda; this.c = c; this.h = h; this.K = K; this.k = k; this.p = p; genDemand = new PoissonGen (streamDemand, new PoissonDist (lambda)); } // Simulates the system for m days, with the (s,S) policy, // and returns the average profit per day. public double simulateOneRun (int m, int s, int S) { int Xj = S, Yj; // Stock in the morning and in the evening. double profit = 0.0; // Cumulated profit. for (int j=0; j 0. This is a Lotka-Volterra system of differential equations, which has a known analytical solution. Here, in the program of
22 2 DISCRETE-EVENT SIMULATION
Listing 12: Simulation of the prey-predator system import umontreal.iro.lecuyer.simevents.*; public class PreyPred { double r = 0.005, c = 0.00001, s = 0.01, d = 0.000005, double x0 = 2000.0, z0 = 150.0; double horizon = 501.0; Continuous x = new Preys(); Continuous z = new Preds();
h = 5.0;
public static void main (String[] args) { new PreyPred(); } public PreyPred() { Sim.init(); new EndOfSim().schedule (horizon); new PrintPoint().schedule (h); Continuous.selectRungeKutta4 (h); x.startInteg (x0); z.startInteg (z0); Sim.start(); } public class Preys extends Continuous { public double derivative (double t) { return (r * value() - c * value() * z.value()); } } public class Preds extends Continuous { public double derivative (double t) { return (-s * value() + d * x.value() * value()); } } class PrintPoint extends Event { public void actions() { System.out.println (Sim.time() + " " + x.value() + " " + z.value()); this.schedule (h); } } class EndOfSim extends Event { public void actions() { Sim.stop(); } } }
2.3 A simplified bank 23
Listing 12, we simply simulate its evolution, to illustrate the continuous simulation facilities of SSJ. This program prints the triples (t, x(t), z(t)) at values of t that are multiples of h, one triple per line. This is done by an event of class PrintPoint, which is rescheduled at every h units of time. This output can be redirected to a file for later use, for example to plot a graph of the trajectory. The continuous variables x and z are instances of the classes Preys and Preds, whose method derivative give their derivative x0 (t) and z 0 (t), respectively. The differential equations are integrated by a Runge-Kutta method of order 4.
2.3
A simplified bank
This is Example 1.4.1 of [2], page 14. A bank has a random number of tellers every morning. On any given day, the bank has t tellers with probability qt , where q3 = 0.80, q2 = 0.15, and q1 = 0.05. All the tellers are assumed to be identical from the modeling viewpoint. 1 arrival 0.5 rate 9:00
.............................. .............................. .............................. . . . . . . . . . . . . . . . . . . . .. . .. . .. . .. . .. . .. . .. . .. . .. . .. . .. . .. . .. . .. . .. . . .. . .. . .. . .. . .. ........................................... .......... 9:45 11:00 14:00 15:00
time
Figure 1: Arrival rate of customers to the bank.
Listing 13: Event-oriented simulation of the bank model import import import import import
umontreal.iro.lecuyer.simevents.*; umontreal.iro.lecuyer.rng.*; umontreal.iro.lecuyer.probdist.*; umontreal.iro.lecuyer.randvar.*; umontreal.iro.lecuyer.stat.*;
public class BankEv { static final double minute int nbTellers; int nbBusy; int nbWait; int nbServed; double meanDelay; Event nextArriv = RandomStream streamArr = ErlangGen genServ = (new MRG32k3a(), new RandomStream streamTeller RandomStream streamBalk
= 1.0 / 60.0; // Number of tellers. // Number of tellers busy. // Queue length. // Number of customers served so far // Mean time between arrivals. new Arrival(); // The next arrival. new MRG32k3a(); // Customer’s arrivals new ErlangConvolutionGen ErlangDist (2, 1.0/minute)); = new MRG32k3a(); // Number of tellers = new MRG32k3a(); // Balking decisions
24 2 DISCRETE-EVENT SIMULATION Tally statServed = new Tally ("Nb. served per day"); Tally avWait = new Tally ("Average wait per day (hours)"); Accumulate wait = new Accumulate ("cumulated wait for this day"); Event e9h45 = new Event() { public void actions() { meanDelay = 2.0*minute; nextArriv.schedule (ExponentialGen.nextDouble (streamArr, 1.0/meanDelay)); } }; Event e10h = new Event() { public void actions() { double u = streamTeller.nextDouble(); if (u >= 0.2) nbTellers = 3; else if (u < 0.05) nbTellers = 1; else nbTellers = 2; while (nbWait > 0 && nbBusy < nbTellers) { nbBusy++; nbWait--; new Departure().schedule (genServ.nextDouble()); } wait.update (nbWait); } }; Event e11h = new Event() { public void actions() { nextArriv.reschedule ((nextArriv.time() - Sim.time())/2.0); meanDelay = minute; } }; Event e14h = new Event() { public void actions() { nextArriv.reschedule ((nextArriv.time() - Sim.time())*2.0); meanDelay = 2.0*minute; } }; Event e15h = new Event() { public void actions() { nextArriv.cancel(); } }; private boolean balk() {
2.3 A simplified bank 25
return nbWait > 9 || (nbWait > 5 && (5.0*streamBalk.nextDouble() < nbWait-5)); } class Arrival extends Event { public void actions() { nextArriv.schedule (ExponentialGen.nextDouble (streamArr, 1.0/meanDelay)); if (nbBusy < nbTellers) { nbBusy++; new Departure().schedule (genServ.nextDouble()); } else if (!balk()) { nbWait++; wait.update (nbWait); } } } class Departure extends Event { public void actions() { nbServed++; if (nbWait > 0) { new Departure().schedule (genServ.nextDouble()); nbWait--; wait.update (nbWait); } else nbBusy--; } }; public void simulOneDay() { Sim.init(); wait.init(); nbTellers = 0; nbBusy = 0; nbWait = 0; nbServed = 0; e9h45.schedule (9.75); e10h.schedule (10.0); e11h.schedule (11.0); e14h.schedule (14.0); e15h.schedule (15.0); Sim.start(); statServed.add (nbServed); wait.update(); avWait.add (wait.sum()); } public void simulateDays (int numDays) { for (int i=1; i Nb. served per day min max average standard dev. num. obs. 152.000 285.000 240.590 19.210 100 REPORT on Tally stat. collector ==> Average wait per day (hours) min max average standard dev. num. obs. 0.816 35.613 4.793 5.186 100
2.4
A call center
We consider here a simplified model of a telephone contact center (or call center ) where agents answer incoming calls. Each day, the center operates for m hours. The number of agents answering calls and the arrival rate of calls vary during the day; we shall assume that they are constant within each hour of operation but depend on the hour. Let nj be the number of agents in the center during hour j, for j = 0, . . . , m − 1. For example, if the center operates from 8 am to 9 pm, then m = 13 and hour j starts at (j + 8) o’clock. All agents are assumed to be identical. When the number of occupied agents at the end of hour j is larger than nj+1 , ongoing calls are all completed but new calls are answered only when there are less than nj+1 agents busy. After the center closes, ongoing calls are completed and calls already in the queue are answered, but no additional incoming call is taken. The calls arrive according to a Poisson process with piecewise constant rate, equal to Rj = Bλj during hour j, where the λj are constants and B is a random variable having the gamma distribution with parameters (α0 , α0 ). Thus, B has mean 1 and variance 1/α0 , and it represents the busyness of the day; it is more busy than usual when B > 1 and less busy when B < 1. The Poisson process assumption means that conditional on B, the number of incoming calls during any subinterval (t1 , t2 ] of hour j is a Poisson random variable with
28 2 DISCRETE-EVENT SIMULATION mean (t2 − t1 )Bλj and that the arrival counts in any disjoint time intervals are independent random variables. This arrival process model is motivated and studied in [8] and [1]. Incoming calls form a FIFO queue for the agents. A call is lost (abandons the queue) when its waiting time exceed its patience time. The patience times of calls are assumed to be i.i.d. random variables with the following distribution: with probability p the patience time is 0 (so the person hangs up unless there is an agent available immediately), and with probability 1−p it is exponential with mean 1/ν. The service times are i.i.d. gamma random variables with parameters (α, β). We want to estimate the following quantities in the long run (i.e., over an infinite number of days): (a) w, the average waiting time per call, (b) g(s), the fraction of calls whose waiting time is less than s seconds for a given threshold s, and (c) `, the fraction of calls lost due to abandonment. Suppose we simulate the model for n days. For each day i, let Ai be the number of arrivals, Wi the total waiting time of all calls, Gi (s) the number of calls who waited less than s seconds, and Li the number of abandonments. Pm−1 For this model, the expected number of incoming calls in a day is a = E[Ai ] = j=0 λj . Then, Wi /a, Gi (s)/a, and Li /a, i = 1, . . . , n, are i.i.d. unbiased estimators of w, g(s), and `, respectively, and can be used to compute confidence intervals for these quantities in a standard way if n is large. Listing 15: Simulation of a simplified call center import import import import import import import import
umontreal.iro.lecuyer.simevents.*; umontreal.iro.lecuyer.rng.*; umontreal.iro.lecuyer.randvar.*; umontreal.iro.lecuyer.probdist.*; umontreal.iro.lecuyer.stat.Tally; java.io.*; java.util.StringTokenizer; java.util.LinkedList;
public class CallCenter { static final double HOUR = 3600.0;
// Time is in seconds.
// Data // Arrival rates are per hour, service and patience times are in seconds. double openingTime; // Opening time of the center (in hours). int numPeriods; // Number of working periods (hours) in the day. int[] numAgents; // Number of agents for each period. double[] lambda; // Base arrival rate lambda_j for each j. double alpha0; // Parameter of gamma distribution for B. double p; // Probability that patience time is 0. double nu; // Parameter of exponential for patience time. double alpha, beta; // Parameters of gamma service time distribution. double s; // Want stats on waiting times smaller than s.
2.4 A call center 29
// Variables double busyness; double arrRate = 0.0; int nAgents; int nBusy; int nArrivals; int nAbandon; int nGoodQoS; double nCallsExpected;
// // // // // // // //
Current value of B. Current arrival rate. Number of agents in current period. Number of agents occupied; Number of arrivals today; Number of abandonments during the day. Number of waiting times less than s today. Expected number of calls per day.
Event nextArrival = new Arrival(); LinkedList waitList = new LinkedList(); RandomStream streamB = RandomStream streamArr = RandomStream streamPatience = GammaGen genServ; // For Tally Tally Tally Tally Tally
statArrivals statWaits statWaitsDay statGoodQoS statAbandon
= = = = =
new new new new new
// The next Arrival event.
new MRG32k3a(); // For new MRG32k3a(); // For new MRG32k3a(); // For service times; created
Tally Tally Tally Tally Tally
B. arrivals. patience times. in readData().
("Number of arrivals per day"); ("Average waiting time per customer"); ("Waiting times within a day"); ("Proportion of waiting times < s"); ("Proportion of calls lost");
public CallCenter (String fileName) throws IOException { readData (fileName); // genServ can be created only after its parameters are read. // The acceptance/rejection method is much faster than inversion. genServ = new GammaAcceptanceRejectionGen (new MRG32k3a(), new GammaDist (alpha, beta)); } // Reads data and construct arrays. public void readData (String fileName) throws IOException { BufferedReader input = new BufferedReader (new FileReader (fileName)); StringTokenizer line = new StringTokenizer (input.readLine()); openingTime = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); numPeriods = Integer.parseInt (line.nextToken()); numAgents = new int[numPeriods]; lambda = new double[numPeriods]; nCallsExpected = 0.0; for (int j=0; j < numPeriods; j++) { line = new StringTokenizer (input.readLine()); numAgents[j] = Integer.parseInt (line.nextToken());
30 2 DISCRETE-EVENT SIMULATION lambda[j] = Double.parseDouble (line.nextToken()); nCallsExpected += lambda[j]; } line = new StringTokenizer (input.readLine()); alpha0 = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); p = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); nu = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); alpha = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); beta = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); s = Double.parseDouble (line.nextToken()); input.close(); } // A phone call. class Call { double arrivalTime, serviceTime, patienceTime; public Call() { serviceTime = genServ.nextDouble(); // Generate service time. if (nBusy < nAgents) { // Start service immediately. nBusy++; nGoodQoS++; statWaitsDay.add (0.0); new CallCompletion().schedule (serviceTime); } else { // Join the queue. patienceTime = generPatience(); arrivalTime = Sim.time(); waitList.addLast (this); } } public void endWait() { double wait = Sim.time() - arrivalTime; if (patienceTime < wait) { // Caller has abandoned. nAbandon++; wait = patienceTime; // Effective waiting time. } else { nBusy++; new CallCompletion().schedule (serviceTime); } if (wait < s) nGoodQoS++; statWaitsDay.add (wait);
2.4 A call center 31
} } // Event: A new period begins. class NextPeriod extends Event { int j; // Number of the new period. public NextPeriod (int period) { j = period; } public void actions() { if (j < numPeriods) { nAgents = numAgents[j]; arrRate = busyness * lambda[j] / HOUR; if (j == 0) nextArrival.schedule (ExponentialDist.inverseF (arrRate, streamArr.nextDouble())); else { checkQueue(); nextArrival.reschedule ((nextArrival.time() - Sim.time()) * lambda[j-1] / lambda[j]); } new NextPeriod(j+1).schedule (1.0 * HOUR); } else nextArrival.cancel(); // End of the day. } } // Event: A call arrives. class Arrival extends Event { public void actions() { nextArrival.schedule (ExponentialDist.inverseF (arrRate, streamArr.nextDouble())); nArrivals++; new Call(); // Call just arrived. } } // Event: A call is completed. class CallCompletion extends Event { public void actions() { nBusy--; }
checkQueue(); }
// Start answering new calls if agents are free and queue not empty. public void checkQueue() { while ((waitList.size() > 0) && (nBusy < nAgents)) ((Call)waitList.removeFirst()).endWait(); }
32 2 DISCRETE-EVENT SIMULATION // Generates the patience time for a call. public double generPatience() { double u = streamPatience.nextDouble(); if (u Average waiting time per customer min max average standard dev. num. obs. 0.000 570.757 11.834 34.078 1000 90.0% confidence interval for mean: ( 10.060, 13.608 ) REPORT on Tally stat. collector ==> Proportion of waiting times < s min max average standard dev. num. obs. 0.277 1.155 0.853 0.169 1000 90.0% confidence interval for mean: ( 0.844, 0.862 ) REPORT on Tally stat. collector ==> Proportion of calls lost min max average standard dev. num. obs. 0.000 0.844 0.034 0.061 1000 90.0% confidence interval for mean: ( 0.0311, 0.0374 )
This model is certainly an oversimplification of actual call centers. It can be embellished and made more realistic by considering different types of agents, different types of calls, agents taking breaks for lunch, coffee, or going to the restroom, agents making outbound calls to reach customers when the inbound traffic is low (e.g., for marketing purpose or for returning calls), and so on. One could also model the revenue generated by calls and the operating costs for running the center, and use the simulation model to compare alternative operating strategies in terms of the expected net revenue, for example.
35
3
Process-Oriented Programs
The process-oriented programs discussed in this section are based on the simprocs package and must be run using Java green threads, which are actually simulated threads and are currently available only in JDK version 1.3.1 or earlier, unfortunately. With native threads, they run more slowly and may just crash if too many threads are initiated. This is a serious limitation of the package simprocs, due to the fact that Java threads are not designed for simulation. The green threads in JDK are obtained by running the program with the “java -classic” option.
3.1
The single queue
Typical simulation languages offer higher-level constructs than those used in the program of Listing 10, and so does SSJ. This is illustrated by our second implementation of the single-server queue model, in Listing 17, based on a paradigm called the process-oriented approach. In the event-oriented implementation, each customer was a passive object, storing two real numbers, and performing no action by itself. In the process-oriented implementation given in Listing 17, each customer (instance of the class Customer) is a process whose activities are described by its actions method. This is implemented by associating a Java Thread to each SimProcess. The server is an object of the class Resource, created when QueueProc is instantiated by main. It is a passive object, in the sense that it executes no code. Active resources, when needed, can be implemented as processes. When it starts executing its actions, a customer first schedules the arrival of the next customer, as in the event-oriented case. (Behind the scenes, this effectively schedules an event, in the event list, that will start a new customer instance. The class SimProcess contains all scheduling facilities of Event, which permits one to schedule processes just like events.) The customer then requests the server by invoking server.request. If the server is free, the customer gets it and can continue its execution immediately. Otherwise, the customer is automatically (behind the scenes) placed in the server’s queue, is suspended, and resumes its execution only when it obtains the server. When its service can start, the customer invokes delay to freeze itself for a duration equal to its service time, which is again generated from the exponential distribution with mean 1/µ using the random variate generator genServ. After this delay has elapsed, the customer releases the server and ends its life. Invoking delay(d) can be interpreted as scheduling an event that will resume the execution of the process in d units of time. Note that several distinct customers can co-exist in the simulation at any given point in time, and be at different phases of their actions method. However, only one process is executing at a time. The constructor QueueProc initializes the simulation, invokes setStatCollecting (true) to specify that detailed statistical collection must be performed automatically for the resource
36 3 PROCESS-ORIENTED PROGRAMS
Listing 17: Process-oriented simulation of an M/M/1 queue import umontreal.iro.lecuyer.simevents.*; import umontreal.iro.lecuyer.simprocs.*; import umontreal.iro.lecuyer.rng.*; import umontreal.iro.lecuyer.probdist.ExponentialDist; import umontreal.iro.lecuyer.randvar.RandomVariateGen; import umontreal.iro.lecuyer.stat.*; // import umontreal.iro.lecuyer.simprocs.dsol.SimProcess; public class QueueProc { Resource server = new Resource (1, "Server"); RandomVariateGen genArr; RandomVariateGen genServ; public QueueProc (double lambda, double mu) { genArr = new RandomVariateGen (new MRG32k3a(), new ExponentialDist(lambda)); genServ = new RandomVariateGen (new MRG32k3a(), new ExponentialDist (mu)); } public void simulateOneRun (double timeHorizon) { SimProcess.init(); server.setStatCollecting (true); new EndOfSim().schedule (timeHorizon); new Customer().schedule (genArr.nextDouble()); Sim.start(); } class Customer extends SimProcess { public void actions() { new Customer().schedule (genArr.nextDouble()); server.request (1); delay (genServ.nextDouble()); server.release (1); } } class EndOfSim extends Event { public void actions() { Sim.stop(); } } public static void main (String[] args) { QueueProc queue = new QueueProc (1.0, 2.0); queue.simulateOneRun (1000.0); System.out.println (queue.server.report()); } }
3.1 The single queue 37
server, schedules an event EndOfSim at the time horizon, schedules the first customer’s arrival, and starts the simulation. The EndOfSim event stops the simulation. The main program then regains control and prints a detailed statistical report on the resource server. It should be pointed out that in the QueueProc program, the service time of a customer is generated only when the customer starts its service, whereas for QueueEv, it was generated at customer’s arrival. For this particular model, it turns out that this makes no difference in the results, because the customers are served in a FIFO order and because one random number stream is dedicated to the generation of service times. However, this may have an impact in other situations. The process-oriented program here is more compact and more elegant than its eventoriented counterpart. This tends to be often true: Process-oriented programming frequently gives less cumbersome and better looking programs. On the other hand, the process-oriented implementations also tend to execute more slowly, because they involve more overhead. For example, the process-driven single-server queue simulation is two to three times slower than its event-driven counterpart. In fact, process management is done via the event list: processes are started, suspended, reactivated, and so on, by hidden events. If the execution speed of a simulation program is really important, it may be better to stick to an event-oriented implementation. Listing 18: Results of the program QueueProc REPORT ON RESOURCE : Server From time : 0.00 to time : min max Capacity 1 1 Utilization 0 1 Queue Size 0 10 Wait 0.000 6.262 Service 6.5E-4 3.437 Sojourn 8.2E-4 6.466
1000.00 average 1.000 0.530 0.513 0.495 0.511 1.005
standard dev.
0.835 0.512 0.979
nb. obs.
1037 1037 1037
Listing 18 shows the output of the program QueueProc. It contains more information than the output of QueueEv. It gives statistics on the server utilization, queue size, waiting times, service times, and sojourn times in the system. (The sojourn time of a customer is its waiting time plus its service time.) We see that by time T = 1000, 1037 customers have completed their waiting and all of them have completed their service. The maximal queue size has been 10 and its average length between time 0 and time 1000 was 0.513. The waiting times were in the range from 0 to 6.262, with an average of 0.495, while the service times were from 0.00065 to 3.437, with an average of 0.511 (recall that the theoretical mean service time is 1/µ = 0.5). Clearly, the largest waiting time and largest service time belong to different customers. The report also gives the empirical standard deviations of the waiting, service, and sojourn times. It is important to note that these standard deviations should not be used to
38 3 PROCESS-ORIENTED PROGRAMS compute confidence intervals for the expected average waiting times or sojourn times in the standard way, because the observations here (e.g., the successive waiting times) are strongly dependent, and also not identically distributed. Appropriate techniques for computing confidence intervals in this type of situation are described, e.g., in [3, 6].
3.2
A job shop model
This example is adapted from [6, Section 2.6], and from [7]. A job shop contains M groups of machines, the mth group having sm identical machines, for m = 1, . . . , M . It is modeled as a network of queues: each group has a single FIFO queue, with sm identical servers for the mth group. There are N types of tasks arriving to the shop at random. Tasks of type n arrive according to a Poisson process with rate λn per hour, for n = 1, . . . , N . Each type of task requires a fixed sequence of operations, where each operation must be performed on a specific type of machine and has a deterministic duration. A task of type n requires pn operations, to be performed on machines mn,1 , mn,2 , . . . , mn,pn , in that order, and whose respective durations are dn,1 , dn,2 , . . . , dn,pn , in hours. A task can pass more than once on the same machine type, so pn may exceed M . We want to simulate the job shop for T hours, assuming that it is initially empty, and start collecting statistics only after a warm-up period of T0 hours. We want to compute: (a) the average sojourn time in the shop for each type of task and (b) the average utilization rate, average length of the waiting queue, and average waiting time, for each type of machine, over the time interval [T0 , T ]. For the average sojourn times and waiting times, the counted observations are the sojourn times and waits that end during the time interval [T0 , T ]. Note that the only randomness in this model is in the task arrival process. The class Jobshop in Listing 19 performs this simulation. Each group of machine is viewed as a resource, with capacity sm for the group m. The different types of task are objects of the class TaskType. This class is used to store the parameters of the different types: their arrival rate, the number of operations, the machine type and duration for each operation, and a statistical collector for their sojourn times in the shop. (In the program, the machine types and task types are numbered from 0 to M − 1 and from 0 to N − 1, respectively, because array indices in Java start at 0.) The tasks that circulate in the shop are objects of the class Task. The actions method in class Task describes the behavior of a task from its arrival until it exits the shop. Each task, upon arrival, schedules the arrival of the next task of the same type. The task then runs through the list of its operations. For each operation, it requests the appropriate type of machine, keeps it for the duration of the operation, and releases it. When the task terminates, it sends its sojourn time as a new observation to the collector statSojourn. Before starting the simulation, the method simulateOneRun schedules an event for the end of the simulation and another one for the end of the warm-up period. The latter simply starts the statistical collection. It also schedules the arrival of a task of each type. Each task will in turn schedules the arrival of the next task of its own type. With this implementation, the event list always contain N “task arrival” events, one for each type of task. An alternative implementation would be that each task schedules
3.2 A job shop model 39
another task arrival in a number of hours that is an exponential r.v. with rate λ, where λ = λ0 + · · · + λN −1 is the global arrival rate, and then the type of each arriving task is n with probability λn /λ, independently of the others. Initially, a single arrival would be scheduled by the class Jobshop. This approach is stochastically equivalent to the current implementation (see, e.g., [2, 9]), but the event list contains only one “task arrival” event at a time. On the other hand, there is the additional work of generating the task type on each arrival. At the end of the simulation, the main program prints the statistical reports. Note that if one wanted to perform several independent simulation runs with this program, the statistical collectors would have to be reinitialized before each run, and additional collectors would be required to collect the run averages and compute confidence intervals. Listing 19: A job shop simulation import import import import import import import
umontreal.iro.lecuyer.simevents.*; umontreal.iro.lecuyer.simprocs.*; umontreal.iro.lecuyer.rng.*; umontreal.iro.lecuyer.randvar.ExponentialGen; umontreal.iro.lecuyer.stat.Tally; java.io.*; java.util.*;
public class Jobshop { int nbMachTypes; int nbTaskTypes; double warmupTime; double horizonTime; boolean warmupDone; Resource[] machType; TaskType[] taskType; RandomStream streamArr BufferedReader input;
// Number of machine types M. // Number of task types N. // Warmup time T_0. // Horizon length T. // Becomes true when warmup time is over. // The machines groups as resources. // The task types. = new MRG32k3a(); // Stream for arrivals.
public Jobshop() throws IOException { // SimProcess.init(); readData(); } // Reads data file, and creates machine types and task types. void readData() throws IOException { input = new BufferedReader (new FileReader ("Jobshop.dat")); StringTokenizer line = new StringTokenizer (input.readLine()); warmupTime = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); horizonTime = Double.parseDouble (line.nextToken()); line = new StringTokenizer (input.readLine()); nbMachTypes = Integer.parseInt (line.nextToken());
40 3 PROCESS-ORIENTED PROGRAMS nbTaskTypes = Integer.parseInt (line.nextToken()); machType = new Resource[nbMachTypes]; for (int m=0; m < nbMachTypes; m++) { line = new StringTokenizer (input.readLine()); String name = line.nextToken(); int nb = Integer.parseInt (line.nextToken()); machType[m] = new Resource (nb, name); } taskType = new TaskType[nbTaskTypes]; for (int n=0; n < nbTaskTypes; n++) taskType[n] = new TaskType(); input.close(); }
class TaskType { public String public double public int public Resource[] public double[] public Tally
name; arrivalRate; nbOper; machOper; lengthOper; statSojourn;
// // // // // //
Task name. Arrival rate. Number of operations. Machines where operations occur. Durations of operations. Stats on sojourn times.
// Reads data for new task type and creates data structures. TaskType() throws IOException { StringTokenizer line = new StringTokenizer (input.readLine()); statSojourn = new Tally (name = line.nextToken()); arrivalRate = Double.parseDouble (line.nextToken()); nbOper = Integer.parseInt (line.nextToken()); machOper = new Resource[nbOper]; lengthOper = new double[nbOper]; for (int i = 0; i < nbOper; i++) { int p = Integer.parseInt (line.nextToken()); machOper[i] = machType[p-1]; lengthOper[i] = Double.parseDouble (line.nextToken()); } } // Performs the operations of this task (to be called by a process). public void performTask (SimProcess p) { double arrivalTime = Sim.time(); for (int i=0; i < nbOper; i++) { machOper[i].request (1); p.delay (lengthOper[i]); machOper[i].release (1); } if (warmupDone) statSojourn.add (Sim.time() - arrivalTime); }
3.2 A job shop model 41
} public class Task extends SimProcess { TaskType type; Task (TaskType type) { this.type = type; } public void actions() { // First schedules next task of this type, then executes task. new Task (type).schedule (ExponentialGen.nextDouble (streamArr, type.arrivalRate)); type.performTask (this); } } Event endWarmup = new Event() { public void actions() { for (int m=0; m < nbMachTypes; m++) machType[m].setStatCollecting (true); warmupDone = true; } }; Event endOfSim = new Event() { public void actions() { Sim.stop(); } }; public void simulateOneRun() { SimProcess.init(); endOfSim.schedule (horizonTime); endWarmup.schedule (warmupTime); warmupDone = false; for (int n = 0; n < nbTaskTypes; n++) { new Task (taskType[n]).schedule (ExponentialGen.nextDouble (streamArr, taskType[n].arrivalRate)); } Sim.start(); } public void printReportOneRun() { for (int m=0; m < nbMachTypes; m++) System.out.println (machType[m].report()); for (int n=0; n < nbTaskTypes; n++) System.out.println (taskType[n].statSojourn.report()); } static public void main (String[] args) throws IOException {
42 3 PROCESS-ORIENTED PROGRAMS Jobshop shop = new Jobshop(); shop.simulateOneRun(); shop.printReportOneRun(); } }
3.3
A time-shared computer system
This example is adapted from [6], Section 2.4. Consider a simplified time-shared computer system comprised of T identical and independent terminals, all busy, using a common server (e.g., for database requests, or central processing unit (CPU) consumption, etc.). Each terminal user sends a task to the server at some random time and waits for the response. After receiving the response, he thinks for some random time before submitting a new task, and so on. We assume that the thinking time is an exponential random variable with mean µ, whereas the server’s time needed for a request is a Weibull random variable with parameters α, λ and δ. The tasks waiting for the server form a single queue with a round robin service policy with quantum size q, which operates as follows. When a task obtains the server, if it can be completed in less than q seconds, then it keeps the server until completion. Otherwise, it gets the server for q seconds and returns to the back of the queue to be continued later. In both cases, there is also h additional seconds of overhead for changing the task that has the server’s attention.
T s s s 2 1
End of task
Waiting queue -
CPU
End of quantum
Terminals
The response time of a task is defined as the difference between the time when the task ends (including the overhead h at the end) and the arrival time of the task to the server. We are interested in the mean response time, in steady-state. We will simulate the system until N tasks have ended, with all terminals initially in the “thinking” state. To reduce the initial bias, we will start collecting statistics only after N0 tasks have ended (so the first N0
3.3 A time-shared computer system 43
response times are not counted by our estimator, and we take the average response time for the N − N0 response times that remain). This entire simulation is repeated R times, independently, so we can estimate the variance of our estimator. Suppose we want to compare the mean response times for two different configurations of this system, where a configuration is characterized by the vector of parameters (T, q, h, µ, α, λ, δ). We will make R independent simulation runs (replications) for each configuration. To compare the two configurations, we want to use common random numbers, i.e., the same streams of random numbers across the two configurations. We couple the simulation runs by pairs: for run number i, let R1i and R2i be the mean response times for configurations 1 and 2, and let Di = R1i − R2i . We use the same random numbers to obtain R1i and R2i , for each i. The Di are nevertheless independent random variables (under the blunt assumption that the random streams really produce independent uniform random variables) and we can use them to compute a confidence interval for the difference d between the theoretical mean response times of the two systems. Using common random numbers across R1i and R2i should reduce the variance of the Di and the size of the confidence interval. Listing 20: Simulation of a time shared system import import import import import import import
umontreal.iro.lecuyer.simevents.*; umontreal.iro.lecuyer.simprocs.*; umontreal.iro.lecuyer.rng.*; umontreal.iro.lecuyer.randvar.RandomVariateGen; umontreal.iro.lecuyer.probdist.*; umontreal.iro.lecuyer.stat.Tally; java.io.*;
public class TimeShared { int nbTerminal = 20; double quantum; double overhead = 0.001; double meanThink = 5.0; double alpha = 0.5; double lambda = 1.0; double delta = 0.0; int N = 1100; int N0 = 100; int nbTasks;
// // // // // // // // // //
Number of terminals. Quantum size. Amount of overhead (h). Mean thinking time. Parameters of the Weibull service times. ’’ ’’ Total number of tasks to simulate. Number of tasks for warmup. Number of tasks ended so far.
RandomStream streamThink = new MRG32k3a(); RandomVariateGen genThink = new RandomVariateGen (streamThink, new ExponentialDist (1.0/meanThink)); RandomStream streamServ = new MRG32k3a ("Gen. for service requirements"); RandomVariateGen genServ = new RandomVariateGen (streamServ, new WeibullDist (alpha, lambda, delta));
44 3 PROCESS-ORIENTED PROGRAMS Resource server Tally meanInRep Tally statDiff
= new Resource (1, "The server"); = new Tally ("Average for current run"); = new Tally ("Diff. on mean response times");
class Terminal extends SimProcess { public void actions() { double arrivTime; // Arrival time of current request. double timeNeeded; // Server’s time still needed for it. while (nbTasks < N) { delay (genThink.nextDouble()); arrivTime = Sim.time(); timeNeeded = genServ.nextDouble(); while (timeNeeded > quantum) { server.request (1); delay (quantum + overhead); timeNeeded -= quantum; server.release (1); } server.request (1); // Here, timeNeeded N0) meanInRep.add (Sim.time() - arrivTime); // Take the observation if warmup is over. } Sim.stop(); // N tasks have now completed. } } private void simulOneRun() { SimProcess.init(); server.init(); meanInRep.init(); nbTasks = 0; for (int i=1; i Diff. on mean response times min max average standard dev. num. obs. -0.134 0.369 0.168 0.174 10 90.0% confidence interval for mean: ( 0.067, 0.269 )
For a concrete example, let T = 20, h = .001, µ = 5 sec., α = 1/2, λ = 1 and δ = 0 for the two configurations. With these parameters, the mean of the Weibull distribution is 2. Take q = 0.1 for configuration 1 and q = 0.2 for configuration 2. We also choose
46 3 PROCESS-ORIENTED PROGRAMS N0 = 100, N = 1100, and R = 10 runs. With these numbers, the program gives the results of Listing 21. The confidence interval on the difference between the response time with q = 0.1 and that with q = 0.2 contains only positive numbers. We can therefore conclude that the mean response time is significantly shorter (statistically) with q = 0.2 than with q = 0.1 (assuming that we can neglect the bias due to the choice of the initial state). To gain better confidence in this conclusion, we could repeat the simulation with larger values of N0 and N . Of course, the model could be made more realistic by considering, for example, different types of terminals, with different parameters, a number of terminals that changes with time, different classes of tasks with priorities, etc. SSJ offers the tools to implement these generalizations easily.
3.4
Guided visits
This example is translated from [7]. A touristic attraction offers guided visits, using three guides. The site opens at 10:00 and closes at 16:00. Visitors arrive in small groups (e.g., families) and the arrival process of those groups is assumed to be a Poisson process with rate of 20 groups per hour, from 9:45 until 16:00. The visitors arriving before 10:00 must wait for the opening. After 16:00, the visits already under way can be completed, but no new visit is undertaken, so that all the visitors still waiting cannot visit the site and are lost. The size of each arriving group of visitors is a discrete random variable taking the value i with probability pi given in the following table: i pi
1 .2
2 .6
3 .1
4 .1
Visits are normally made by groups of 8 to 15 visitors. Each visit requires one guide and lasts 45 minutes. People waiting for guides form a single queue. When a guide becomes free, if there is less than 8 people in the queue, the guide waits until the queue grows to at least 8 people, otherwise she starts a new visit right away. If the queue contains more than 15 people, the first 15 will go on this visit. At 16:00, if there is less than 8 people in the queue and a guide is free, she starts a visit with the remaining people. At noon, each free guide takes 30 minutes for lunch. The guides that are busy at that time will take 30 minutes for lunch as soon as they complete their on-going visit. Sometimes, an arriving group of visitors may decide to just go away (balk) because the queue is too long. We assume that the probability of balking when the queue size is n is given by ( 0 for n ≤ 10; R(n) = (n − 10)/30 for 10 < n < 40; 1 for n ≥ 40. The aim is to estimate the average number of visitors lost per day, in the long run. The visitors lost are those that balk or are still in the queue after 16:00.
3.4 Guided visits 47
A simulation program for this model is given in Listing 22. Here, time is measured in hours, starting at midnight. At time 9:45, for example, the simulation clock is at 9.75. The (process) class Guide describes the daily behavior of a guide (each guide is an instance of this class), whereas Arrival generates the arrivals according to a Poisson process, the group sizes, and the balking decisions. The event closing closes the site at 16:00. The Bin mechanism visitReady is used to synchronize the Guide processes. The number of tokens in this bin is 1 if there are enough visitors in the queue to start a visit (8 or more) and is 0 otherwise. When the queue size reaches 8 due to a new arrival, the Arrival process puts a token into the bin. This wakes up a guide if one is free. A guide must take a token from the bin to start a new visit. If there are still 8 people or more in the queue when she starts the visit, she puts the token back to the bin immediately, to indicate that another visit is ready to be undertaken by the next available guide. Listing 22: Simulation of guided visits import umontreal.iro.lecuyer.simevents.*; import umontreal.iro.lecuyer.simprocs.*; import umontreal.iro.lecuyer.rng.*; import umontreal.iro.lecuyer.probdist.*; import umontreal.iro.lecuyer.randvar.*; import umontreal.iro.lecuyer.stat.*; // import umontreal.iro.lecuyer.simprocs.dsol.SimProcess; public int int Bin
class Visits { queueSize; // Size of waiting queue. nbLost; // Number of visitors lost so far today. visitReady = new Bin ("Visit ready"); // A token becomes available when there // are enough visitors to start a new visit. Tally avLost = new Tally ("Nb. of visitors lost per day"); RandomVariateGen genArriv = new RandomVariateGen (new MRG32k3a(), new ExponentialDist (20.0)); // Interarriv. RandomStream streamSize = new MRG32k3a(); // Group sizes. RandomStream streamBalk = new MRG32k3a(); // Balking decisions. private void oneDay() { queueSize = 0; nbLost = 0; SimProcess.init(); visitReady.init(); closing.schedule (16.0); new Arrival().schedule (9.75); for (int i=1; i 12.0 && !lunchDone) { delay (0.5); lunchDone = true; } visitReady.take (1); // Starts the next visit. if (queueSize > 15) queueSize -= 15; else queueSize = 0; if (queueSize >= 8) visitReady.put (1); // Enough people for another visit. delay (0.75); } } } class Arrival extends SimProcess { public void actions() { while (true) { delay (genArriv.nextDouble()); // A new group of visitors arrives. int groupSize; // number of visitors in group. double u = streamSize.nextDouble(); if (u 9 || (n > 5 && 5.0*streamBalk.nextDouble() < n - 5); } } public void simulOneDay() { SimProcess.init(); new OneDay().schedule (9.75); Sim.start(); statServed.add (nbServed); avWait.add (tellers.waitList().statSize().sum()); } public void simulateDays (int numDays) { tellers.waitList().setStatCollecting (true); for (int i=1; i