Metaheuristiques/src/main/java/jobshop/encodings/Schedule.java

244 lines
8.9 KiB
Java

package jobshop.encodings;
import jobshop.Instance;
import java.util.*;
import java.util.stream.IntStream;
/** Direct encoding of the solution to JobShop problem.
*
* Associates every task to its start time.
*/
public class Schedule extends Encoding {
// start times of each job and task
// times[j][i] is the start time of task (j,i) : i^th task of the j^th job
final int[][] times;
/** Creates a new schedule for the given instance where all start times are uninitialized. */
public Schedule(Instance instance) {
super(instance);
this.times = new int[instance.numJobs][];
for(int j = 0; j < instance.numJobs ; j++) {
this.times[j] = new int[instance.numTasks];
}
}
/** Start time of the given task. */
public int startTime(int job, int task) {
return times[job][task];
}
/** Start time of the given task. */
public int startTime(Task task) {
return startTime(task.job, task.task);
}
/** End time of the given task. */
public int endTime(int job, int task) {
return startTime(job, task) + instance.duration(job, task);
}
/** End time of the given task. */
public int endTime(Task task) {
return endTime(task.job, task.task);
}
/** Sets the start time of the given task. */
public void setStartTime(int job, int task, int startTime) {
times[job][task] = startTime;
}
/** Returns true if this schedule is valid (no constraint is violated) */
public boolean isValid() {
for(int j = 0; j<instance.numJobs ; j++) {
for(int t = 1; t< instance.numTasks ; t++) {
if(startTime(j, t-1) + instance.duration(j, t-1) > startTime(j, t))
return false;
}
for(int t = 0; t< instance.numTasks ; t++) {
if(startTime(j, t) < 0)
return false;
}
}
for (int machine = 0; machine < instance.numMachines ; machine++) {
for(int j1 = 0; j1< instance.numJobs ; j1++) {
int t1 = instance.task_with_machine(j1, machine);
for(int j2 = j1+1; j2< instance.numJobs ; j2++) {
int t2 = instance.task_with_machine(j2, machine);
boolean t1_first = endTime(j1, t1) <= startTime(j2, t2);
boolean t2_first = endTime(j2, t2) <= startTime(j1, t1);
if(!t1_first && !t2_first)
return false;
}
}
}
return true;
}
/** Makespan of the solution.
* The makespan is the end time of the latest finishing task.
*/
public int makespan() {
int max = -1;
for(int j = 0; j< instance.numJobs ; j++) {
max = Math.max(max, endTime(j, instance.numTasks-1));
}
return max;
}
/** Returns true if the given sequence of task is a critical path of the schedule. */
public boolean isCriticalPath(List<Task> path) {
if(startTime(path.get(0)) != 0) {
return false;
}
if(endTime(path.get(path.size()-1)) != makespan()) {
return false;
}
for(int i=0 ; i<path.size()-1 ; i++) {
if(endTime(path.get(i)) != startTime(path.get(i+1)))
return false;
}
return true;
}
/** Computes a critical path of the schedule.
*
* @return A sequence of task along a critical path.
*/
public List<Task> criticalPath() {
// select task with greatest end time
Task ldd = IntStream.range(0, instance.numJobs)
.mapToObj(j -> new Task(j, instance.numTasks-1))
.max(Comparator.comparing(this::endTime))
.get();
assert endTime(ldd) == makespan();
// list that will contain the critical path.
// we construct it from the end, starting with the
// task that finishes last
LinkedList<Task> path = new LinkedList<>();
path.add(0,ldd);
// keep adding tasks to the path until the first task in the path
// starts a time 0
while(startTime(path.getFirst()) != 0) {
Task cur = path.getFirst();
int machine = instance.machine(cur.job, cur.task);
// will contain the task that was delaying the start
// of our current task
Optional<Task> latestPredecessor = Optional.empty();
if(cur.task > 0) {
// our current task has a predecessor on the job
Task predOnJob = new Task(cur.job, cur.task -1);
// if it was the delaying task, save it to predecessor
if(endTime(predOnJob) == startTime(cur))
latestPredecessor = Optional.of(predOnJob);
}
if(latestPredecessor.isEmpty()) {
// no latest predecessor found yet, look among tasks executing on the same machine
latestPredecessor = IntStream.range(0, instance.numJobs)
.mapToObj(j -> new Task(j, instance.task_with_machine(j, machine)))
.filter(t -> endTime(t) == startTime(cur))
.findFirst();
}
// at this point we should have identified a latest predecessor, either on the job or on the machine
assert latestPredecessor.isPresent() && endTime(latestPredecessor.get()) == startTime(cur);
// insert predecessor at the beginning of the path
path.add(0, latestPredecessor.get());
}
assert isCriticalPath(path);
return path;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\nStart times of all tasks:\n");
for(int job = 0; job< instance.numJobs; job++) {
sb.append("Job ");
sb.append(job);
sb.append(": ");
for(int task = 0; task< instance.numTasks; task++) {
sb.append(String.format("%5d", startTime(job, task)));
}
sb.append("\n");
}
return sb.toString();
}
/**
* Returns a string containing the Gantt chart of the given schedule, in ASCII art.
*
* Each line of the Gantt chart, contains the tasks of a particular job. Each character in the output represents a
* fixed number of time units
* For each task, we indicate :
* - the machine on which the task must be executed
* - whether this task is on the critical path (task on the critical path are filled in with stars).
*/
public String asciiGantt() {
var criticalPath = this.criticalPath();
int minTaskDur = IntStream.range(0, instance.numJobs).flatMap(job -> IntStream.range(0, instance.numTasks).map(task -> instance.duration(job, task))).min().getAsInt();
// time units by character
int charsPerTimeUnit = minTaskDur >= 5 ? 1 : (5 / minTaskDur) +1;
StringBuilder sb = new StringBuilder();
sb.append("\nGantt Chart\n");
for(int job = 0; job< instance.numJobs; job++) {
sb.append(String.format("Job %2d: ", job));
int cursor = 0;
for(int task = 0; task< instance.numTasks; task++) {
Task t = new Task(job, task);
var st = startTime(job, task);
// add spaces until the start of our task
sb.append(" ".repeat(charsPerTimeUnit * (st - cursor )));
sb.append(formatTask(t, charsPerTimeUnit, criticalPath.contains(t)));
cursor = endTime(job, task);
}
sb.append("\n");
}
return sb.toString();
}
/** Utility function to display a set of characters representing a task in a gantt chart.
*
* @param t Task to display
* @param charPerTimeUnit How many characters to represent a time unit.
* @param isCritical Is the task on the critical path.
* @return Ascii representation of the task. Length is the duration * charPerTimeUnit.
*/
String formatTask(Task t, int charPerTimeUnit, boolean isCritical) {
StringBuilder sb = new StringBuilder();
String fill = isCritical ? "*" : "-";
int dur = instance.duration(t);
int machine = instance.machine(t);
int stringLength = dur * charPerTimeUnit;
int charsForMachine = machine < 10 ? 1 : 2;
int numSpaces = stringLength - 2 - charsForMachine; // we use 2 chars for '[' and '[' + 1 or 2 for the machine number
int startSpaces = numSpaces / 2;
int endSpaces = numSpaces - startSpaces;
sb.append("[");
sb.append(fill.repeat(startSpaces - 1));
sb.append(" ");
sb.append(machine);
sb.append(" ");
sb.append(fill.repeat(endSpaces - 1));
sb.append("]");
return sb.toString();
}
@Override
public Optional<Schedule> toSchedule() {
return Optional.of(this);
}
}