The first prototype is an application that solves problems similar to the mail delivery; i.e. problems where resources are independent from each other. The resulting time plan is printed out on the screen in textual format.
3.4.1 Analysis
According to the domain models presented in Section 3.3, a task is made up of a temporal sequence of activities. The temporal consistency constraint states that, for a given task, each activity can only start when the previous one is finished. In order to process an activity, a resource should be avail- able. The kind of resource depends on the type of activity. The resource con- sistency constraint states that a resource can process only one activity at a time. In a give period of time, a resource can process a sequence of activities belonging to different tasks.
In the first scenario, each carrier represents a resource. The city is par- titioned into delivery areas and each area is assigned to only one carrier. A delivery area corresponds to a mail delivery task. A delivery task is made up of a sequence of delivery activities corresponding to different delivery routes. Each carrier performs the activities of a single task.
The scenario set in the fitness club brings an additional complexity to the scheduling problem. The scheduler tool should model and manage inter- dependent resources. This is the case of several athletes using the same set of training machines for their exercises. Both athletes and machines are resources that perform activities (i.e. the exercises). Each athlete is assigned a predefined set of workouts (the training task). The temporal and resource constraints apply both to the athletes and to the training machines.
Figure 3.4 depicts the revised analysis model. Relationship nextOfindicates that a resource executes sequences of non-overlapping activities.
Let’s consider the situation before the execution of the scheduling algorithm:
■ Each activity records the temporal parameters as specified by the user. They represent the preferences of the user and thus the scheduler should try to satisfy them as much as possible.
Figure 3.4 The revised analysis model
Resource executes made up of 1 .. 1 1 .. 1 1 .. n 1 .. n 1 .. 1 Task next of releaseTime activationTime terminationTime dueDate Activity
■ These temporal parameters do not (usually) satisfy the temporal and resource constraints of the entire scheduling problem.
The scheduling algorithm computes new values of the temporal para- meters of each activity in order to satisfy all the temporal and resource constraints and to maximize satisfaction of the user preferences.
3.4.2 Design
We need to transform the analysis diagram depicted in Figure 3.4 into a Design class diagram. This requires defining the attributes of each class, identifying their responsibilities and specifying the relationships between the classes. A set of decisions must be taken and the result is depicted in Figure 3.5.
Class Activityhas a simple data structure that records the temporal para- meters introduced in Figure 3.2. It implements a method for evaluating the performance value as defined in Equation 3.1 and a method for updating the temporal parameters according to Equation 3.2.
Class Taskis a simple collection (see Sidebar 3.1) of Activityobjects. It is used mainly to keep track of which activity is part of which task.
Class Resourceis an ordered collection of Activityobjects. It implements the scheduling algorithm presented in Section 3.3.1. In particular, it serializes the
Figure 3.5 The class diagram of the scheduling tool
Activity
serialize( a : Activity ) update( gain : double ) getPerformance() : double id : int
name : String releaseTime : int activation Time : int terminationTime : int dueTime : int Resource schedule() getPerformance() : double name : String gain : double addActivity( a : Activity ) getActivity(id : int) : Activity
Task Problem init() ConcreteProblem schedule() Scheduler addResource( r : Resource) addTask( t : Task)
getResource(id : int) : Resource getActivity(t : int, a : int) : Activity init()
Decision point
set of activities to be processed over a certain period of time, evaluates their activation and termination time, and measures their performance.
Sidebar 3.1 Collections
Collections are objects that encapsulate other objects, which are called items. Collections are commonly classified in three categories:
■ A list is just a group of items.
■ A set is a list that contains no duplicates.
■ A map is a set of key–value pairs.
The Java Collections Framework provides a set of interfaces for storing and manipulating groups of data as depicted in the figure below.
SortedSetand SortedMap represent collections of items ordered according to criteria defined in the items’ classes. For this purpose, item classes must imple- ment the comparable interface.
The Java Collections Framework also provides a set of concrete classes that implement the collection interfaces, as summarized in the table below.
<< Interface >> Collection << Interface >> List << Interface >> Set << Interface >> SortedSet << Interface >> Map << Interface >> SortedMap
Interface Class Description
Set HashSet The set is implemented as a hash table
SortedSet TreeSet The sorted set is implemented as a balanced binary tree
List ArrayList The list is implemented as a resizable array
List LinkedList The list is implemented as a doubly linked list
Map HashMap The map is implemented as a hash table
SortedMap TreeMap The sorted map is implemented as a balanced binary tree
Decision point
A scheduling problem is described in terms of resources and tasks and is represented by the abstract class Problem. Specific problems are described by concrete subclasses of Problem. During the initialization phase, activities are assigned to resources. The preferred order of execution is indicated by the activation time of each activity. This order might change during the time plan generation.
Since every Activity records its temporal parameters, we do not need to represent the time plan explicitly. The information that creates the time plan is distributed among the activities.
The scheduling algorithm is distributed among the resources. Each resource schedules its activities. We introduce class Scheduler to initialize the scheduling process and to activate the resources. It computes the global performance and evaluates when the iterative scheduling process of each resource should terminate.
In the fitness club scenario, each athlete works out on one exercise (activity) at a time. Similarly, each training machine can be used to perform only one exercise at a time. According to the analysis, each machine is ded- icated to a single athlete at a time. This means that, at a given point in time, an athlete performs an exercise using a machine exclusively.
Thus, we decide to represent both athletes and machines as instances of class Resourceand the exercises as instances of class Activity. Every activity is simultaneously assigned to a machine and to an athlete. Machines and athletes schedule their activities according to their temporal preferences and constraints.
3.4.3 Implementation
We start the implementation with class Activitysince it is the basic compo- nent of the architecture. The data structure records the name of an activity Decision point
How do we represent the Time Plan?
Decision point
Where is the scheduling algorithm implemented?
Decision point
and the temporal parameters as defined in Figure 3.5. Two more member variables are needed to store the values of the activation and termination times corresponding to the best performance of the scheduling algorithm.
The constructor initializes the temporal parameters and computes the activity’s duration. During the execution of the scheduling algorithm, only the activation time and the termination time will be updated. The reset() method initializes the activation time and termination time before any new run of the scheduling algorithm.
package scheduler; public class Activity {
private int id # 0; private String name;
private double releaseTime; private double activationTime; private double terminationTime; private double dueTime;
private double duration;
private double tempActivationTime # 0.0; private double tempTerminationTime # 0.0;
public Activity(String name, double r, double a, double t, double d) { this.name # name;
this.releaseTime # r; this.activationTime # a; this.terminationTime # t; this.dueTime # d; this.duration # t - a; }
public int getID() { return id; }
public String getName() { return name; }
public double getActivation() { return activationTime; } public double getRelease() { return releaseTime; }
public double getTermination() { return terminationTime; } public double getDueTime() { return dueTime; }
public double getDuration() { return duration; } public void setID(int id) { this.id # id; }
// The next two methods set the values of release time and // due date. If necessary, they change the values of // termination time and activation time consistently public void setReleaseTime(double time) {
if (time < 0.0) return;
if (time < activationTime || (time ! duration) < dueTime) releaseTime # time;
activationTime # releaseTime;
terminationTime # activationTime ! duration; }
}
public void setDueTime(double time) { if (time < 0.0) return;
if (time > dueTime || (time - duration) > releaseTime) dueTime # time;
if (dueTime < terminationTime) { terminationTime # dueTime;
activationTime # terminationTime - duration; }
}
public void reset() { activationTime # 0.0;
terminationTime # activationTime ! duration; }
// The next two methods update the temporal parameters of // the activity. The serialize() method enforces the // resource and temporal constraints. The update() method // takes into account the activity preferences; in
// particular, it updates the activation time in order to // start the activity after the release time and to // complete it as close as possible to the due date. The // parameter gain represents the strength of an activity's // preferences.
public void serialize(Activity previous) { if (previous.terminationTime > activationTime)
activationTime # previous.terminationTime; terminationTime # activationTime ! duration; }
public void update(double gain) {
activationTime !# gain * (dueTime - terminationTime); if (activationTime < releaseTime)
activationTime !# gain * (releaseTime-activationTime); terminationTime # activationTime ! duration;
}
// The getPerformance() method implements the formula // described in Equation 3.1, where the weights of // the earliness and tardiness performance parameters // are set to 1.0.
public double getPerformance() { double pe # 0.0;
double pt # 0.0;
if (terminationTime > dueTime) pt # (terminationTime - dueTime);
if (activationTime < releaseTime) pe # (releaseTime - activationTime); return pe ! pt;
}
// The store() method is used to store an activity’s // temporal parameters that correspond to the best
// performance solution found by the scheduling algorithm // during a given iteration step.
public void store() {
tempActivationTime # activationTime; tempTerminationTime # terminationTime; }
// The restore() method is invoked at the end of the // scheduling process to copy into an activity’s // temporal parameters the values corresponding to the // best performance solution.
public void restore() {
activationTime # tempActivationTime; terminationTime # tempTerminationTime; }
}
Class Resourceaggregates Activityobjects. We use class ArrayListto imple- ment the containment relationship. Since the Resourceneeds to sort the list of activities according to their activation time, we define the inner class CompareActivitiesthat implements the Comparatorinterface.
The core of class Resource is the schedule() method that enforces the temporal and resource constraints of the scheduling problem. The method executes three basic operations. First, it requests its activities to update their temporal parameters. They could have been modified by the previous iteration of the scheduling algorithm. As a consequence, the activities do not necessarily enforce the temporal and resource constraints of this resource. Thus, the schedule()method re-orders the activities according to their new activation time. Finally, the method sequences the activities in such a way that they do not overlap. This operation requires modifying the temporal parameters of the activities.
Since the schedule()method modifies the values of the activities’ temporal parameters, it is important to evaluate how these values enforce the tem- poral constraints represented by the release time and the due time of each activity. The getPerformance() method computes the performance of the scheduling algorithm with regard to all the activities.
package scheduler; import java.util.*; public class Resource {
class CompareActivity implements Comparator { public int compare(Object o1, Object o2) {
Activity t1 # (Activity) o1; Activity t2 # (Activity) o2;
if(t1.getActivation() > t2.getActivation()) return 1; if(t1.getActivation() < t2.getActivation()) return -1; return t1.getName().compareTo(t2.getName()); } }
private String name;
private ArrayList activities # new ArrayList(); private double gain # 0.0;
public Resource(String name) { this.name # name; } public String getName() { return name; }
public void addActivity(Activity activity) { activities.add(activity);
}
public void addTask(Task task) {
Iterator iterator # task.getActivities(); Activity activity;
while(iterator.hasNext())
activities.add( (Activity) iterator.next() ); }
public void setGain(double gain) { this.gain # gain; } public void reset() {
Iterator iterator # activities.iterator(); Activity activity;
while(iterator.hasNext()) {
activity # (Activity) iterator.next(); activity.reset();
} }
public void schedule() {
// updates the activationTime of each activity for(int i#0; i < activities.size(); i!!)
((Activity) activities.get(i)).update(gain); // sorts the list of activities for activationTime Collections.sort(activities, new CompareActivity()); // evaluates the new completionTime of each activity Activity previous # (Activity) activities.get(0); for(int i#1; i < activities.size(); i!!) {
Activity activity # (Activity) activities.get(i); activity.serialize(previous);
previous # activity; }
public double getPerformance() { double performance # 0.0;
for(int i#0; i < activities.size(); i!!) {
Activity activity # (Activity) activities.get(i); performance !# activity.getPerformance();
}
return performance; }
public void store() {
for(int i#0; i < activities.size(); i!!) {
Activity activity # (Activity) activities.get(i); activity.store();
} }
public void restore() {
for(int i#0; i < activities.size(); i!!) {
Activity activity # (Activity) activities.get(i); activity.restore();
} } }
Class Taskis a simple container that aggregates activities as specified by the problem at hand.
package scheduler; import java.util.*; public class Task {
private ArrayList activities # new ArrayList(); public int addActivity(Activity activity) {
activity.setID(activities.size()); activities.add(activity);
return activity.getID(); }
public Iterator getActivities() { return activities.iterator(); }
public Activity getActivity(int id) {
Iterator iterator # activities.iterator(); Activity activity;
while(iterator.hasNext()) {
activity # (Activity) iterator.next(); if(activity.getID() ## id) return activity; } return null; } }
Class Problem describes the scheduling problem in terms of tasks and resources. It is an abstract class that must be specialized in order to describe specific problems. Every subclass must implement method init().
package scheduler; import java.util.*;
public abstract class Problem {
private ArrayList tasks # new ArrayList(); private ArrayList resources # new ArrayList(); public Resource getResource(int i) {
return (Resource) resources.get(i); }
public Iterator getResources() { return resources.iterator(); }
public void addTask(Task task) { tasks.add(task);
}
public void addResource(Resource resource) { resources.add(resource);
}
public void clear() { tasks.removeAll(tasks);
resources.removeAll(resources); }
public Activity getActivity(int taskID, int activityID) { Task task # (Task) tasks.get(taskID);
if(task ## null) return null; Activity activity #
(Activity) task.getActivity(activityID); return activity;
}
public abstract void init(); }
Class Scheduleris the main class of the architecture. It is initialized with an instance of a concrete problem and supervises the execution of the scheduling algorithm by the resources.
The schedule() method simply iterates the basic scheduling step, which requests the resources to schedule their activities, evaluates the per- formance at each step, and stores the solution corresponding to the best performance computed so far. The algorithm terminates after a predefined number of iterations (80 in this case). A better implementation of this method could define a more general termination clause. At the end of the algorithm’s execution, the method restores the solution corresponding to the best performance value, which is the final values of the activities’ temporal parameters.
package scheduler; import java.util.*; public class Scheduler {
public static int STEPS # 80; private Problem problem # null;
private int bestStep; // the index of the best performance private double bestValue; // the best performance value public void setProblem(Problem problem) {
this.problem # problem; bestStep # 0;
bestValue # 1e9; problem.init(); }
public double bestValue() { return bestValue; } public void schedule(double gain) {
Iterator iterator # problem.getResources(); Resource resource;
while(iterator.hasNext()) {
resource # (Resource) iterator.next(); resource.reset();
resource.setGain(gain); }
// repeats the scheduling step 80 times for (int step # 0; step < STEPS; step!!) {
double totPerformance # 0.0;
// every resource schedules its activities iterator # problem.getResources();
while(iterator.hasNext())
((Resource) iterator.next()).schedule(); // evaluates the total performance
iterator # problem.getResources(); while(iterator.hasNext())
totPerformance !#
((Resource) iterator.next()).getPerformance(); // evaluates the performance of this scheduling step if (totPerformance < bestValue) {
bestValue # totPerformance;
// this is the best performance up to now iterator # problem.getResources(); while(iterator.hasNext()) ((Resource) iterator.next()).store(); bestStep # step; } }
// restores the the best performance values iterator # problem.getResources();
while(iterator.hasNext())
((Resource) iterator.next()).restore(); }
}
3.4.4 Test
As an example we implement class PostOfficeProblem that extends class Problemand specifies the temporal parameters of each mail delivery activity. We can assume that the values are expressed in minutes, but it is not relevant for testing the scheduler.
import scheduler.*;
public class PostOfficeProblem extends Problem { public void init() {
super.clear();
Task task1 # new Task(); Activity a; a # new Activity("Zone11", 5.0, 10.0, 15.0, 20.0); task1.addActivity(a); a # new Activity("Zone12", 5.0, 10.0, 15.0, 20.0); task1.addActivity(a); a # new Activity("Zone13", 15.0, 17.0, 22.0, 25.0); task1.addActivity(a); a # new Activity("Zone14", 20.0, 22.0, 27.0, 35.0); task1.addActivity(a); a # new Activity("Zone15", 20.0, 30.0, 35.0, 40.0); task1.addActivity(a); a # new Activity("Zone16", 20.0, 30.0, 35.0, 45.0); task1.addActivity(a); a # new Activity("Zone17", 25.0, 30.0, 35.0, 50.0); task1.addActivity(a); this.addTask(task1);
Resource carrier1 # new Resource("Carrier1"); carrier1.addTask(task1);
this.addResource(carrier1); Task task2 # new Task(); Activity a; a # new Activity("Zone21", 5.0, 7.0, 12.0, 15.0); task2.addActivity(a); a # new Activity("Zone22", 10.0, 15.0, 20.0, 30.0); task2.addActivity(a); a # new Activity("Zone23", 15.0, 25.0, 30.0, 40.0); task2.addActivity(a); a # new Activity("Zone24", 25.0, 30.0, 35.0, 55.0); task2.addActivity(a); a # new Activity("Zone25", 20.0, 30.0, 35.0, 45.0);
task2.addActivity(a); a # new Activity("Zone26", 15.0, 30.0, 35.0, 45.0); task2.addActivity(a); a # new Activity("Zone27", 10.0, 30.0, 35.0, 45.0); task2.addActivity(a); this.addTask(task2);
Resource carrier2 # new Resource("Carrier2"); carrier2.addTask(task2);
this.addResource(carrier2); }
We developed the test within the JUnit automatic test framework (see Sidebar 3.2). Accordingly, the test case is implemented in class TestScheduler.
import scheduler.*; import java.util.*; import junit.framework.*;
public class TestScheduler extends TestCase { Scheduler scheduler # new Scheduler();
public TestScheduler(String name) { super(name); } public void testPostOffice() {
Problem problem # new PostOfficeProblem(); double gain # 0.1;
scheduler.setProblem(problem); scheduler.schedule(gain);
// verifies the expected performance value assertEquals(0.0, scheduler.bestValue(), 0.1); HashMap expectedResults # new HashMap();
expectedResults.put("Zone11", new Double(8.0)); expectedResults.put("Zone12", new Double(13.0)); expectedResults.put("Zone13", new Double(18.0)); expectedResults.put("Zone14", new Double(23.0)); expectedResults.put("Zone15", new Double(28.0)); expectedResults.put("Zone16", new Double(33.0)); expectedResults.put("Zone17", new Double(38.0)); expectedResults.put("Zone21", new Double(5.0)); expectedResults.put("Zone22", new Double(14.0)); expectedResults.put("Zone23", new Double(28.0)); expectedResults.put("Zone24", new Double(43.0)); expectedResults.put("Zone25", new Double(38.0)); expectedResults.put("Zone26", new Double(33.0)); expectedResults.put("Zone27", new Double(23.0)); // verifies the expected values for the temporal parameters
Activity activity; Double value;
for(int taskID # 0; taskID < 2; taskID!!)
for(int activityID # 0; activityID < 7; activityID!!){ activity # (Activity) problem.getActivity(
taskID, activityID); value # (Double) expectedResults.get(
activity.getName()); assertEquals(value.doubleValue(),
activity.getActivation(), 1.0); }
}
public static Test suite() {
return new TestSuite(TestScheduler.class); }
public static void main (String[] args) { junit.textui.TestRunner.run (suite()); }
}
Sidebar 3.2 JUnit test framework
JUnit is an automated testing framework for Java programs (www.junit.org). More precisely, JUnit is a regression testing framework written by Erich Gamma and Kent Beck. It is used by the developer who implements unit tests in Java. There are plugins for several IDEs to make it easy the development and execution of tests.
To test a Java program using JUnit is very easy. It is sufficient to define a class that extends the class TestCase (defined in the framework), add public test methods that check that the program behaves as expected, and run the test.
The JUnit framework is made up of three essential elements that can be used by the programmer:
■ the assert methods,
■ class TestCase, and
■ class TestSuite.
The assert methods are used to check whether the results obtained from the program are as expected; they can be used in the test methods. Class TestCaseis the container of a set of test methods, its test methods are executed by the JUnit framework. Finally, class TestSuiteis a collection of test cases, it can be used to run in a single shot all the tests for a program.
For instance, to test a stack class, we can write the following test case:
// first we import the base class TestCase
import junit.framework.TestCase;
public class StackTester // the test case class must
extends TestCase { // extend class TestCase // a constructor like this is required by
// JUnit framework to work properly
public void StackTester(String name) { super(name); }