Files
ortools-clone/examples/tests/issue33.cs
2020-10-26 18:41:49 +01:00

734 lines
25 KiB
C#

// Authors: Johan Wessén
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Google.OrTools.ConstraintSolver;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System;
using Xunit;
namespace Google.OrTools.Test {
public class Task {
public int Id {
get;
private set;
}
public int TaskType {
get;
private set;
}
public int LocationId {
get;
private set;
}
public Dictionary<int, int> Durations {
get;
private set;
}
public int TaskPosition {
get;
private set;
}
public Task(int id, int taskType, int locationIndex, int taskPosition,
Dictionary<int, int> durations) {
Id = id;
TaskType = taskType;
LocationId = locationIndex;
Durations = durations;
TaskPosition = taskPosition;
}
public Task(int id, int taskType, int locationIndex, int taskPosition) {
Id = id;
TaskType = taskType;
LocationId = locationIndex;
TaskPosition = taskPosition;
Durations = new Dictionary<int, int>();
}
}
public class WorkLocation {
public int Id {
get;
private set;
}
public int NbTasks {
get {
Debug.Assert(Tasks != null);
return Tasks.Length;
}
set {
Debug.Assert(Tasks == null);
Tasks = new Task[value];
}
}
public Task[] Tasks {
get;
private set;
}
public WorkLocation(int index) { Id = index; }
}
public class Tool {
public int Id {
get;
private set;
}
public HashSet<int> TaskTypes {
get;
set;
}
public int[, ] TravellingTime {
get;
set;
}
public int InitialLocationId {
get;
set;
}
public Tool(int index, int initialLocation = 0) {
Id = index;
InitialLocationId = initialLocation;
TaskTypes = new HashSet<int>();
}
public void AddTaskType(int t) { TaskTypes.Add(t); }
public bool CanPerformTaskType(int taskType) {
return TaskTypes.Contains(taskType);
}
}
public class FactoryDescription {
public Tool[] Tools {
get;
private set;
}
public WorkLocation[] Locations {
get;
private set;
}
public int NbWorkLocations {
get { return Locations.Length; }
}
public int NbTools {
get { return Tools.Length; }
}
public int NbTaskPerCycle {
get;
private set;
}
// TaskType go typically from 0 to 6. InspectionType indicates which
// is the TaskType that correspond to Inspection.
public int Inspection {
get;
private set;
}
// All the time within the schedule horizon in which the blast can start.
public long[] InspectionStarts {
get;
private set;
}
public int Horizon {
get;
private set;
}
// horizon equal to 2 weeks (in minutes).
public FactoryDescription(int nbTools, int nbLocations, int nbTaskPerCycle,
int horizon = 14 * 24 * 60) {
Debug.Assert(nbTools > 0);
Debug.Assert(nbLocations > 0);
Debug.Assert(nbTaskPerCycle > 0);
Debug.Assert(horizon > 0);
NbTaskPerCycle = nbTaskPerCycle;
Inspection = NbTaskPerCycle - 1;
Tools = new Tool[nbTools];
Horizon = horizon;
for (int i = 0; i < nbTools; i++) Tools[i] = new Tool(i);
Locations = new WorkLocation[nbLocations];
for (int i = 0; i < nbLocations; i++) Locations[i] = new WorkLocation(i);
InspectionStarts = new long[]{-1, 600, 1200, 1800, 2400, 2800};
}
public Tool[] getToolPerTaskType(int taskType) {
var elements = from tool in Tools where tool.CanPerformTaskType(taskType)
select tool;
return elements.ToArray();
}
public Task[] getFlatTaskList() {
return (from location in Locations from task in
location.Tasks orderby task.Id select task)
.ToArray();
}
public int[] getTaskTypes() {
return (from location in Locations from task in location.Tasks select
task.TaskType)
.Distinct()
.ToArray();
}
// TODO: This should be enhanced
public void SanityCheck() {
foreach (Tool tool in Tools) {
Debug.Assert(tool.TravellingTime.GetLength(0) == NbWorkLocations);
Debug.Assert(tool.TravellingTime.GetLength(1) == NbWorkLocations);
for (int i = 0; i < NbWorkLocations; i++)
Debug.Assert(tool.TravellingTime[i, i] == 0);
}
}
}
interface DataReader {
FactoryDescription FetchData();
}
public class SmallSyntheticData : DataReader {
public SmallSyntheticData() {}
public FactoryDescription FetchData() {
// deterministic seed for result reproducibility
Random randomDuration = new Random(2);
// FactoryDescription(nbTools, nblocations, nbTasks per cycle)
FactoryDescription factoryDescription = new FactoryDescription(5, 4, 3);
// Travelling time and distance are temporarily identical and they
// are no different for different tools
int[, ] travellingTime = new int[factoryDescription.NbWorkLocations,
factoryDescription.NbWorkLocations];
for (int i = 0; i < travellingTime.GetLength(0); i++) {
for (int j = 0; j < travellingTime.GetLength(1); j++) {
if (i == j)
travellingTime[i, j] = 0;
else
travellingTime[i, j] = (5 * Math.Abs(i - j)) * 10;
}
}
factoryDescription
.Tools [0]
.AddTaskType(0);
factoryDescription
.Tools [1]
.AddTaskType(0);
factoryDescription
.Tools [2]
.AddTaskType(1);
factoryDescription
.Tools [3]
.AddTaskType(1);
factoryDescription
.Tools [4]
.AddTaskType(2);
factoryDescription
.Tools [1]
.AddTaskType(1);
foreach (Tool tool in factoryDescription.Tools)
tool.TravellingTime = travellingTime;
int c = 0;
int nbCyclePerWorkLocation = 2;
int[] boll = new int[100];
for (int i = 0; i < factoryDescription.NbWorkLocations; i++) {
factoryDescription.Locations[i].NbTasks =
nbCyclePerWorkLocation * factoryDescription.NbTaskPerCycle;
for (int j = 0; j < nbCyclePerWorkLocation; j++) {
for (int k = 0; k < factoryDescription.NbTaskPerCycle; k++) {
Task t =
new Task(c, k, i, k + j * factoryDescription.NbTaskPerCycle);
// Filling in tool-dependent durations
Tool[] compatibleTools = factoryDescription.getToolPerTaskType(k);
foreach (Tool tool in compatibleTools) {
boll[c] = randomDuration.Next(13, 17) * 10;
;
t.Durations[tool.Id] = boll[c];
}
factoryDescription.Locations[i].Tasks[t.TaskPosition] = t;
c++;
}
}
}
factoryDescription.SanityCheck();
return factoryDescription;
}
}
public class RandomSelectToolHeuristic : NetDecisionBuilder {
private FactoryScheduling factoryScheduling;
private Random rnd;
public RandomSelectToolHeuristic(FactoryScheduling factoryScheduling,
int seed) {
this.factoryScheduling = factoryScheduling;
// deterministic seed for result reproducibility
this.rnd = new Random(seed);
}
public override Decision Next(Solver solver) {
foreach (IntVar var in factoryScheduling.SelectedTool) {
if (!var.Bound()) {
int min = (int) var.Min();
int max = (int) var.Max();
int rndVal = rnd.Next(min, max + 1);
while (!var.Contains(rndVal)) rndVal = rnd.Next(min, max + 1);
return solver.MakeAssignVariableValue(var, rndVal);
}
}
return null;
}
}
class TaskAlternative {
public Task Task {
get;
private set;
}
public IntVar ToolVar {
get;
set;
}
public List<IntervalVar> Intervals {
get;
private set;
}
public TaskAlternative(Task t) {
Task = t;
Intervals = new List<IntervalVar>();
}
}
public class FactoryScheduling {
private FactoryDescription factoryData;
private Solver solver;
private Task[] tasks;
private int[] taskTypes;
/* Flat list of all the tasks */
private TaskAlternative[] taskStructures;
/* Task per WorkLocation: location2Task[d][i]: the i-th task of the
* d-th location */
private TaskAlternative[][] location2Task;
/* Task per Tool: tool2Task[t][i]: the i-th task of the t-th tool.
Note that it does NOT imply that the it will be the i-th
executed. In other words, it should be considered as an unordered
set. Furthermore, tool2Task[t][i] can also be *unperformed* */
private List<IntervalVar>[] tool2Task;
/* All the transition times for the tools.
tool2TransitionTimes[t][i]: the transition time of the t-th tool
from the i-th task to the next */
private List<IntVar>[] tool2TransitionTimes;
/* Map between the interval var of a tool to its related task id.
toolIntervalVar2TaskId[t][k] = i: in the t-th tool, the k-th
interval var correspond to tasks[i] */
private List<int>[] toolIntervalVar2TaskId;
/* Tools per task type: taskType2Tool[tt][t]: the t-th tool capable
* of doing the tt-th task type */
private List<Tool>[] taskType2Tool;
/* For each task which tools is performed upon */
private List<IntVar> selectedTool;
public List<IntVar> SelectedTool {
get { return selectedTool; }
}
/* Sequence of task for each tool */
private SequenceVar[] allToolSequences;
public SequenceVar[] AllToolSequences {
get { return allToolSequences; }
}
/* Makespan var */
private IntVar makespan;
/* Objective */
private OptimizeVar objective;
/* maximum horizon */
private int horizon;
/* Start & End times of IntervalVars*/
IntVar[][] startingTimes;
IntVar[][] endTimes;
public FactoryScheduling(FactoryDescription data) { factoryData = data; }
private void Init() {
horizon = factoryData.Horizon;
solver = new Solver("Factory Scheduling");
tasks = factoryData.getFlatTaskList();
taskTypes = factoryData.getTaskTypes();
taskStructures = new TaskAlternative[tasks.Length];
location2Task = new TaskAlternative [factoryData.NbWorkLocations]
[];
tool2Task = new List<IntervalVar>[ factoryData.NbTools ];
toolIntervalVar2TaskId = new List<int>[ factoryData.NbTools ];
tool2TransitionTimes = new List<IntVar>[ factoryData.NbTools ];
taskType2Tool = new List<Tool>[ taskTypes.Length ];
selectedTool = new List<IntVar>();
for (int tt = 0; tt < taskTypes.Length; tt++)
taskType2Tool[tt] = new List<Tool>();
foreach (Tool tool in factoryData.Tools)
foreach (int taskType in tool.TaskTypes)
taskType2Tool [taskType]
.Add(tool);
for (int d = 0; d < factoryData.NbWorkLocations; d++)
location2Task[d] =
new TaskAlternative[factoryData.Locations[d].NbTasks];
for (int t = 0; t < factoryData.NbTools; t++) {
tool2Task[t] = new List<IntervalVar>();
toolIntervalVar2TaskId[t] = new List<int>();
tool2TransitionTimes[t] = new List<IntVar>();
}
allToolSequences = new SequenceVar[factoryData.NbTools - 1];
startingTimes = new IntVar [factoryData.NbTools - 1]
[];
endTimes = new IntVar [factoryData.NbTools - 1]
[];
}
private void PostTransitionTimeConstraints(
int t, bool postTransitionsConstraint = true) {
Tool tool = factoryData.Tools[t];
// if it is a inspection, we make sure there are no transitiontimes
if (tool.CanPerformTaskType(factoryData.Inspection))
tool2TransitionTimes [t]
.Add(null);
else {
int[, ] tt = tool.TravellingTime;
SequenceVar seq = allToolSequences[t];
long s = seq.Size();
IntVar[] nextLocation = new IntVar[s + 1];
// The seq.Next(i) represents the task performed after the i-th
// task in the sequence seq.Next(0) represents the first task
// performed for extracting travelling times we need to get the
// related location In case a task is not performed (seq.Next(i)
// == i), i.e. it's pointing to itself The last performed task
// (or pre-start task, if no tasks are performed) will have
// seq.Next(i) == s + 1 therefore we add a virtual location
// whose travelling time is equal to 0
//
// NOTE: The index of a SequenceVar are 0..n, but the domain
// range is 1..(n+1), this is due to that the start node = 0 is
// a dummy node, and the node where seq.Next(i) == n+1 is the
// end node
// Extra elements for the unreachable start node (0), and the
// end node whose next task takes place in a virtual location
int[] taskIndex2locationId = new int[s + 2];
taskIndex2locationId[0] = -10;
for (int i = 0; i < s; i++)
taskIndex2locationId[i + 1] = tasks [toolIntervalVar2TaskId [t]
[i]
]
.LocationId;
// this is the virtual location for unperformed tasks
taskIndex2locationId[s + 1] = factoryData.NbWorkLocations;
// Build the travelling time matrix with the additional virtual location
int[][] ttWithVirtualLocation =
new int [factoryData.NbWorkLocations + 1]
[];
for (int d1 = 0; d1 < ttWithVirtualLocation.Length; d1++) {
ttWithVirtualLocation[d1] = new int[factoryData.NbWorkLocations + 1];
for (int d2 = 0; d2 < ttWithVirtualLocation.Length; d2++)
if (d1 == factoryData.NbWorkLocations) {
ttWithVirtualLocation [d1]
[d2] = 0;
} else {
ttWithVirtualLocation [d1]
[d2] = (d2 == factoryData.NbWorkLocations) ? 0 : tt[d1, d2];
}
}
for (int i = 0; i < nextLocation.Length; i++) {
// this is the next-location associated with the i-th task
nextLocation[i] =
solver.MakeElement(taskIndex2locationId, seq.Next(i)).Var();
int d = (i == 0) ? tool.InitialLocationId
: tasks [toolIntervalVar2TaskId [t]
[i - 1]
]
.LocationId;
if (i == 0) {
// To be changed - right now we don't have meaningful indata
// of previous location Ugly way of setting initial travel
// time to = 0, as this is how we find common grounds
// between benchmark algorithm and this
tool2TransitionTimes [t]
.Add(solver
.MakeElement(new int[ttWithVirtualLocation[d].Length],
nextLocation[i])
.Var());
} else {
tool2TransitionTimes [t]
.Add(solver
.MakeElement(ttWithVirtualLocation[d], nextLocation[i])
.Var());
}
}
// Extra elements for the unreachable start node (0), and the
// end node whose next task takes place in a virtual location
startingTimes[t] = new IntVar[s + 2];
endTimes[t] = new IntVar[s + 2];
startingTimes [t]
[0] = solver.MakeIntConst(0);
// Tbd: Set this endtime to the estimated time of finishing
// previous task for the current tool
endTimes [t]
[0] = solver.MakeIntConst(0);
for (int i = 0; i < s; i++) {
startingTimes [t]
[i + 1] = tool2Task [t]
[i]
.SafeStartExpr(-1)
.Var();
endTimes [t]
[i + 1] = tool2Task [t]
[i]
.SafeEndExpr(-1)
.Var();
}
startingTimes [t]
[s + 1] = solver.MakeIntConst(factoryData.Horizon);
endTimes [t]
[s + 1] = solver.MakeIntConst(factoryData.Horizon);
// Enforce (or not) that each task is separated by the
// transition time to the next task
for (int i = 0; i < nextLocation.Length; i++) {
IntVar nextStart =
solver.MakeElement(startingTimes[t], seq.Next(i).Var()).Var();
if (postTransitionsConstraint)
solver.Add(endTimes [t]
[i] +
tool2TransitionTimes [t]
[i] <=
nextStart);
}
}
}
private void Model() {
/* Building basic task data structures */
for (int i = 0; i < tasks.Length; i++) {
/* Create a new set of possible IntervalVars & IntVar to decide
* which one (and only 1) is performed */
taskStructures[i] = new TaskAlternative(tasks[i]);
/* Container to use when posting constraints */
location2Task[tasks[i].LocationId][tasks[i].TaskPosition] =
taskStructures[i];
/* Get task type */
int taskType = tasks[i].TaskType;
/* Possible tool for this task */
List<Tool> tools = taskType2Tool[taskType];
bool optional = tools.Count > 1;
/* List of boolean variables. If performedOnTool[t] == true then
* the task is performed on tool t */
List<IntVar> performedOnTool = new List<IntVar>();
for (int t = 0; t < tools.Count; t++) {
/* Creating an IntervalVar. If tools.Count > 1 the intervalVar
* is *OPTIONAL* */
int toolId = tools[t].Id;
Debug.Assert(tasks[i].Durations.ContainsKey(toolId));
int duration = tasks[i].Durations[toolId];
string name = "J " + tasks[i].Id + " [" + toolId + "]";
IntervalVar intervalVar;
if (taskType == factoryData.Inspection) {
/* We set a 0 time if the task is an inspection */
duration = 0;
intervalVar = solver.MakeFixedDurationIntervalVar(
0, horizon, duration, optional, name);
IntVar start = intervalVar.SafeStartExpr(-1).Var();
intervalVar.SafeStartExpr(-1).Var().SetValues(
factoryData.InspectionStarts);
} else {
intervalVar = solver.MakeFixedDurationIntervalVar(
0, horizon, duration, optional, name);
}
taskStructures[i].Intervals.Add(intervalVar);
tool2Task [toolId]
.Add(intervalVar);
toolIntervalVar2TaskId [toolId]
.Add(i);
/* Collecting all the bool vars, even if they are optional */
performedOnTool.Add(intervalVar.PerformedExpr().Var());
}
/* Linking the bool var to a single integer variable: */
/* if alternativeToolVar == t <=> performedOnTool[t] == true */
string alternativeName = "J " + tasks[i].Id;
IntVar alternativeToolVar =
solver.MakeIntVar(0, tools.Count - 1, alternativeName);
taskStructures[i].ToolVar = alternativeToolVar;
solver.Add(solver.MakeMapDomain(alternativeToolVar,
performedOnTool.ToArray()));
Debug.Assert(performedOnTool.ToArray().Length ==
alternativeToolVar.Max() + 1);
selectedTool.Add(alternativeToolVar);
}
/* Creates precedences on a work Location in order to enforce a
* fully ordered set within the same location
*/
for (int d = 0; d < location2Task.Length; d++) {
for (int i = 0; i < location2Task[d].Length - 1; i++) {
TaskAlternative task1 = location2Task [d]
[i];
TaskAlternative task2 = location2Task [d]
[i + 1];
/* task1 must end before task2 starts */
/* Adding precedence for each possible alternative pair */
for (int t1 = 0; t1 < task1.Intervals.Count(); t1++) {
IntervalVar task1Alternative = task1.Intervals[t1];
for (int t2 = 0; t2 < task2.Intervals.Count(); t2++) {
IntervalVar task2Alternative = task2.Intervals[t2];
Constraint precedence = solver.MakeIntervalVarRelation(
task2Alternative, Solver.STARTS_AFTER_END, task1Alternative);
solver.Add(precedence);
}
}
}
}
/* Adds disjunctive constraints on unary resources, and creates
* sequence variables. */
for (int t = 0; t < factoryData.NbTools; t++) {
string name = "Tool " + t;
if (!factoryData
.Tools [t]
.CanPerformTaskType(factoryData.Inspection)) {
DisjunctiveConstraint ct =
solver.MakeDisjunctiveConstraint(tool2Task [t]
.ToArray(),
name);
solver.Add(ct);
allToolSequences[t] = ct.SequenceVar();
}
PostTransitionTimeConstraints(t, true);
}
/* Collecting all tasks end for makespan objective function */
List<IntVar> intervalEnds = new List<IntVar>();
for (int i = 0; i < tasks.Length; i++)
foreach (IntervalVar var in taskStructures[i].Intervals)
intervalEnds.Add(var.SafeEndExpr(-1).Var());
/* Objective: minimize the makespan (maximum end times of all tasks) */
makespan = solver.MakeMax(intervalEnds.ToArray()).Var();
objective = solver.MakeMinimize(makespan, 1);
}
private void Search() {
int seed = 2; // This is a good seed to show the crash
/* Assigning first tools */
DecisionBuilder myToolAssignmentPhase =
new RandomSelectToolHeuristic(this, seed);
/* Ranking of the tools */
DecisionBuilder sequencingPhase =
solver.MakePhase(allToolSequences, Solver.SEQUENCE_DEFAULT);
/* Then fixing time of tasks as early as possible */
DecisionBuilder timingPhase = solver.MakePhase(
makespan, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE);
/* Overall phase */
DecisionBuilder mainPhase =
solver.Compose(myToolAssignmentPhase, sequencingPhase, timingPhase);
/* Logging */
const int logFrequency = 1000000;
SearchMonitor searchLog = solver.MakeSearchLog(logFrequency, objective);
/* Restarts */
SearchMonitor searchRestart = solver.MakeLubyRestart(100);
/* Search Limit in ms */
SearchLimit limit = solver.MakeTimeLimit(180 * 1000);
/* Collecting best solution */
SolutionCollector collector = solver.MakeLastSolutionCollector();
collector.AddObjective(makespan);
// collector.Add( pile.ToArray() );
solver.NewSearch(mainPhase, searchLog, searchRestart, objective, limit);
while (solver.NextSolution()) {
Console.WriteLine("MAKESPAN: " + makespan.Value());
}
}
public void Solve() {
Init();
Model();
Search();
}
}
public class Issue18Test {
[Fact]
public void FactorySchedulingTest() {
FactoryScheduling scheduling =
new FactoryScheduling(new SmallSyntheticData().FetchData());
scheduling.Solve();
}
}
} // namespace Google.OrTools.Test