336 lines
14 KiB
Python
336 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Scheduling problem with transition time between tasks and transitions costs.
|
|
|
|
@author: CSLiu2
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
|
|
import argparse
|
|
import collections
|
|
|
|
from ortools.sat.python import cp_model
|
|
from google.protobuf import text_format
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Command line arguments.
|
|
PARSER = argparse.ArgumentParser()
|
|
PARSER.add_argument('--problem_instance', default=0, type=int,
|
|
help='Problem instance.')
|
|
PARSER.add_argument('--output_proto', default="",
|
|
help='Output file to write the cp_model proto to.')
|
|
PARSER.add_argument('--params', default="",
|
|
help='Sat solver parameters.')
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Intermediate solution printer
|
|
class SolutionPrinter(cp_model.CpSolverSolutionCallback):
|
|
"""Print intermediate solutions."""
|
|
|
|
def __init__(self, makespan):
|
|
cp_model.CpSolverSolutionCallback.__init__(self)
|
|
self.__solution_count = 0
|
|
self.__makespan = makespan
|
|
|
|
def OnSolutionCallback(self):
|
|
print('Solution %i, time = %f s, objective = %i, makespan = %i' %
|
|
(self.__solution_count, self.WallTime(), self.ObjectiveValue(),
|
|
self.Value(self.__makespan)))
|
|
self.__solution_count += 1
|
|
|
|
|
|
def main(args):
|
|
"""Solves the scheduling with transitions problem."""
|
|
|
|
instance = args.problem_instance
|
|
parameters = args.params
|
|
output_proto = args.output_proto
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Data.
|
|
small_jobs = [[[(100, 0, 'R6'), (2, 1, 'R6')]],
|
|
[[(2, 0, 'R3'), (100, 1, 'R3')]],
|
|
[[(100, 0, 'R1'), (16, 1, 'R1')]],
|
|
[[(1, 0, 'R1'), (38, 1, 'R1')]],
|
|
[[(14, 0, 'R1'), (10, 1, 'R1')]],
|
|
[[(16, 0, 'R3'), (17, 1, 'R3')]],
|
|
[[(14, 0, 'R3'), (14, 1, 'R3')]],
|
|
[[(14, 0, 'R3'), (15, 1, 'R3')]],
|
|
[[(14, 0, 'R3'), (13, 1, 'R3')]],
|
|
[[(100, 0, 'R1'), (38, 1, 'R1')]]]
|
|
|
|
large_jobs = [
|
|
[[(-1, 0, 'R1'), (10, 1, 'R1')]], [[(9, 0, 'R3'), (22, 1, 'R3')]],
|
|
[[(-1, 0, 'R3'), (13, 1, 'R3')]], [[(-1, 0, 'R3'), (38, 1, 'R3')]],
|
|
[[(-1, 0, 'R3'), (38, 1, 'R3')]], [[(-1, 0, 'R3'), (16, 1, 'R3')]],
|
|
[[(-1, 0, 'R3'), (11, 1, 'R3')]], [[(-1, 0, 'R3'), (13, 1, 'R3')]],
|
|
[[(13, 0, 'R3'), (-1, 1, 'R3')]], [[(13, 0, 'R3'), (-1, 1, 'R3')]],
|
|
[[(-1, 0, 'R3'), (15, 1, 'R3')]], [[(12, 0, 'R1'), (-1, 1, 'R1')]],
|
|
[[(14, 0, 'R1'), (-1, 1, 'R1')]], [[(13, 0, 'R3'), (-1, 1, 'R3')]],
|
|
[[(-1, 0, 'R3'), (15, 1, 'R3')]], [[(14, 0, 'R1'), (-1, 1, 'R1')]],
|
|
[[(13, 0, 'R3'), (-1, 1, 'R3')]], [[(14, 0, 'R3'), (-1, 1, 'R3')]],
|
|
[[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(15, 0, 'R1'), (-1, 1, 'R1')]],
|
|
[[(-1, 0, 'R2'), (16, 1, 'R2')]], [[(-1, 0, 'R2'), (16, 1, 'R2')]],
|
|
[[(-1, 0, 'R5'), (27, 1, 'R5')]], [[(-1, 0, 'R5'), (13, 1, 'R5')]],
|
|
[[(-1, 0, 'R5'), (13, 1, 'R5')]], [[(-1, 0, 'R5'), (13, 1, 'R5')]],
|
|
[[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(-1, 0, 'R1'), (17, 1, 'R1')]],
|
|
[[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],
|
|
[[(16, 0, 'R4'), (-1, 1, 'R4')]], [[(16, 0, 'R4'), (-1, 1, 'R4')]],
|
|
[[(16, 0, 'R4'), (-1, 1, 'R4')]], [[(16, 0, 'R4'), (-1, 1, 'R4')]],
|
|
[[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],
|
|
[[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],
|
|
[[(12, 0, 'R1'), (-1, 1, 'R1')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]],
|
|
[[(-1, 0, 'R5'), (14, 1, 'R5')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]],
|
|
[[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]],
|
|
[[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]],
|
|
[[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]],
|
|
[[(13, 0, 'R6'), (-1, 1, 'R6')]], [[(13, 0, 'R2'), (-1, 1, 'R2')]],
|
|
[[(-1, 0, 'R6'), (12, 1, 'R6')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],
|
|
[[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(-1, 0, 'R6'), (14, 1, 'R6')]],
|
|
[[(-1, 0, 'R5'), (5, 1, 'R5')]], [[(3, 0, 'R2'), (-1, 1, 'R2')]],
|
|
[[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]],
|
|
[[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R5'), (1, 1, 'R5')]],
|
|
[[(1, 0, 'R2'), (-1, 1, 'R2')]], [[(-1, 0, 'R2'), (19, 1, 'R2')]],
|
|
[[(13, 0, 'R4'), (-1, 1, 'R4')]], [[(12, 0, 'R4'), (-1, 1, 'R4')]],
|
|
[[(-1, 0, 'R3'), (2, 1, 'R3')]], [[(11, 0, 'R4'), (-1, 1, 'R4')]],
|
|
[[(6, 0, 'R6'), (-1, 1, 'R6')]], [[(6, 0, 'R6'), (-1, 1, 'R6')]],
|
|
[[(1, 0, 'R2'), (-1, 1, 'R2')]], [[(12, 0, 'R4'), (-1, 1, 'R4')]]
|
|
]
|
|
|
|
jobs = small_jobs if instance == 0 else large_jobs
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Helper data.
|
|
num_jobs = len(jobs)
|
|
all_jobs = range(num_jobs)
|
|
num_machines = 2
|
|
all_machines = range(num_machines)
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Model.
|
|
model = cp_model.CpModel()
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Compute a maximum makespan greedily.
|
|
horizon = 0
|
|
for job in jobs:
|
|
for task in job:
|
|
max_task_duration = 0
|
|
for alternative in task:
|
|
max_task_duration = max(max_task_duration, alternative[0])
|
|
horizon += max_task_duration
|
|
|
|
print('Horizon = %i' % horizon)
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Global storage of variables.
|
|
intervals_per_machines = collections.defaultdict(list)
|
|
presences_per_machines = collections.defaultdict(list)
|
|
starts_per_machines = collections.defaultdict(list)
|
|
ends_per_machines = collections.defaultdict(list)
|
|
resources_per_machines = collections.defaultdict(list)
|
|
ranks_per_machines = collections.defaultdict(list)
|
|
|
|
job_starts = {} # indexed by (job_id, task_id).
|
|
job_presences = {} # indexed by (job_id, task_id, alt_id).
|
|
job_ranks = {} # indexed by (job_id, task_id, alt_id).
|
|
job_ends = [] # indexed by job_id
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Scan the jobs and create the relevant variables and intervals.
|
|
for job_id in all_jobs:
|
|
job = jobs[job_id]
|
|
num_tasks = len(job)
|
|
previous_end = None
|
|
for task_id in range(num_tasks):
|
|
task = job[task_id]
|
|
|
|
min_duration = task[0][0]
|
|
max_duration = task[0][0]
|
|
|
|
num_alternatives = len(task)
|
|
all_alternatives = range(num_alternatives)
|
|
|
|
for alt_id in range(1, num_alternatives):
|
|
alt_duration = task[alt_id][0]
|
|
min_duration = min(min_duration, alt_duration)
|
|
max_duration = max(max_duration, alt_duration)
|
|
|
|
# Create main interval for the task.
|
|
suffix_name = '_j%i_t%i' % (job_id, task_id)
|
|
start = model.NewIntVar(0, horizon, 'start' + suffix_name)
|
|
duration = model.NewIntVar(min_duration, max_duration,
|
|
'duration' + suffix_name)
|
|
end = model.NewIntVar(0, horizon, 'end' + suffix_name)
|
|
|
|
# Store the start for the solution.
|
|
job_starts[(job_id, task_id)] = start
|
|
|
|
# Add precedence with previous task in the same job.
|
|
if previous_end:
|
|
model.Add(start >= previous_end)
|
|
previous_end = end
|
|
|
|
# Create alternative intervals.
|
|
l_presences = []
|
|
for alt_id in all_alternatives:
|
|
### add to link interval with eqp constraint.
|
|
### process time = -1 cannot be processed at this machine.
|
|
if jobs[job_id][task_id][alt_id][0] == -1:
|
|
continue
|
|
alt_suffix = '_j%i_t%i_a%i' % (job_id, task_id, alt_id)
|
|
l_presence = model.NewBoolVar('presence' + alt_suffix)
|
|
l_start = model.NewIntVar(0, horizon, 'start' + alt_suffix)
|
|
l_duration = task[alt_id][0]
|
|
l_end = model.NewIntVar(0, horizon, 'end' + alt_suffix)
|
|
l_interval = model.NewOptionalIntervalVar(
|
|
l_start, l_duration, l_end, l_presence, 'interval' + alt_suffix)
|
|
l_rank = model.NewIntVar(-1, num_jobs, 'rank' + alt_suffix)
|
|
l_presences.append(l_presence)
|
|
l_machine = task[alt_id][1]
|
|
l_type = task[alt_id][2]
|
|
|
|
# Link the master variables with the local ones.
|
|
model.Add(start == l_start).OnlyEnforceIf(l_presence)
|
|
model.Add(duration == l_duration).OnlyEnforceIf(l_presence)
|
|
model.Add(end == l_end).OnlyEnforceIf(l_presence)
|
|
|
|
# Add the local variables to the right machine.
|
|
intervals_per_machines[l_machine].append(l_interval)
|
|
starts_per_machines[l_machine].append(l_start)
|
|
ends_per_machines[l_machine].append(l_end)
|
|
presences_per_machines[l_machine].append(l_presence)
|
|
resources_per_machines[l_machine].append(l_type)
|
|
ranks_per_machines[l_machine].append(l_rank)
|
|
|
|
# Store the variables for the solution.
|
|
job_presences[(job_id, task_id, alt_id)] = l_presence
|
|
job_ranks[(job_id, task_id, alt_id)] = l_rank
|
|
|
|
# Only one machine can process each lot.
|
|
model.Add(sum(l_presences) == 1)
|
|
|
|
job_ends.append(previous_end)
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Create machines constraints nonoverlap process
|
|
for machine_id in all_machines:
|
|
intervals = intervals_per_machines[machine_id]
|
|
if len(intervals) > 1:
|
|
model.AddNoOverlap(intervals)
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Transition times and transition costs using a circuit constraint.
|
|
switch_literals = []
|
|
for machine_id in all_machines:
|
|
machine_starts = starts_per_machines[machine_id]
|
|
machine_ends = ends_per_machines[machine_id]
|
|
machine_presences = presences_per_machines[machine_id]
|
|
machine_resources = resources_per_machines[machine_id]
|
|
machine_ranks = ranks_per_machines[machine_id]
|
|
intervals = intervals_per_machines[machine_id]
|
|
arcs = []
|
|
num_machine_tasks = len(machine_starts)
|
|
all_machine_tasks = range(num_machine_tasks)
|
|
|
|
for i in all_machine_tasks:
|
|
# Initial arc from the dummy node (0) to a task.
|
|
start_lit = model.NewBoolVar('')
|
|
arcs.append([0, i + 1, start_lit])
|
|
# If this task is the first, set both rank and start to 0.
|
|
model.Add(machine_ranks[i] == 0).OnlyEnforceIf(start_lit)
|
|
model.Add(machine_starts[i] == 0).OnlyEnforceIf(start_lit)
|
|
# Final arc from an arc to the dummy node.
|
|
arcs.append([i + 1, 0, model.NewBoolVar('')])
|
|
# Self arc if the task is not performed.
|
|
arcs.append([i + 1, i + 1, machine_presences[i].Not()])
|
|
model.Add(machine_ranks[i] == -1).OnlyEnforceIf(
|
|
machine_presences[i].Not())
|
|
|
|
for j in all_machine_tasks:
|
|
if i == j:
|
|
continue
|
|
|
|
lit = model.NewBoolVar('%i follows %i' % (j, i))
|
|
arcs.append([i + 1, j + 1, lit])
|
|
model.AddImplication(lit, machine_presences[i])
|
|
model.AddImplication(lit, machine_presences[j])
|
|
|
|
# Maintain rank incrementally.
|
|
model.Add(machine_ranks[j] == machine_ranks[i] + 1).OnlyEnforceIf(lit)
|
|
|
|
# Compute the transition time if task j is the successor of task i.
|
|
if machine_resources[i] != machine_resources[j]:
|
|
transition_time = 3
|
|
switch_literals.append(lit)
|
|
else:
|
|
transition_time = 0
|
|
# We add the reified transition to link the literals with the times
|
|
# of the tasks.
|
|
model.Add(machine_starts[j] == machine_ends[i] +
|
|
transition_time).OnlyEnforceIf(lit)
|
|
|
|
model.AddCircuit(arcs)
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Objective.
|
|
makespan = model.NewIntVar(0, horizon, 'makespan')
|
|
model.AddMaxEquality(makespan, job_ends)
|
|
makespan_weight = 1
|
|
transition_weight = 5
|
|
model.Minimize(makespan * makespan_weight +
|
|
sum(switch_literals) * transition_weight)
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Write problem to file.
|
|
if output_proto:
|
|
print('Writing proto to %s' % output_proto)
|
|
with open(output_proto, 'w') as text_file:
|
|
text_file.write(str(model))
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Solve.
|
|
solver = cp_model.CpSolver()
|
|
solver.parameters.max_time_in_seconds = 60 * 60 * 2
|
|
if parameters:
|
|
text_format.Merge(parameters, solver.parameters)
|
|
solution_printer = SolutionPrinter(makespan)
|
|
status = solver.SolveWithSolutionCallback(model, solution_printer)
|
|
|
|
#----------------------------------------------------------------------------
|
|
# Print solution.
|
|
if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL:
|
|
for job_id in all_jobs:
|
|
for task_id in range(len(jobs[job_id])):
|
|
start_value = solver.Value(job_starts[(job_id, task_id)])
|
|
machine = 0
|
|
duration = 0
|
|
select = 0
|
|
rank = -1
|
|
|
|
for alt_id in range(len(jobs[job_id][task_id])):
|
|
if jobs[job_id][task_id][alt_id][0] == -1:
|
|
continue
|
|
|
|
if solver.BooleanValue(job_presences[(job_id, task_id, alt_id)]):
|
|
duration = jobs[job_id][task_id][alt_id][0]
|
|
machine = jobs[job_id][task_id][alt_id][1]
|
|
select = alt_id
|
|
rank = solver.Value(job_ranks[(job_id, task_id, alt_id)])
|
|
|
|
print(
|
|
' Job %i starts at %i (alt %i, duration %i) with rank %i on machine %i'
|
|
% (job_id, start_value, select, duration, rank, machine))
|
|
|
|
print('Solve status: %s' % solver.StatusName(status))
|
|
print('Objective value: %i' % solver.ObjectiveValue())
|
|
print('Makespan: %i' % solver.Value(makespan))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(PARSER.parse_args())
|