Files
ortools-clone/examples/notebook/contrib/scheduling_with_transitions_sat.ipynb
2025-12-19 15:01:21 +01:00

425 lines
19 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"id": "google",
"metadata": {},
"source": [
"##### Copyright 2025 Google LLC."
]
},
{
"cell_type": "markdown",
"id": "apache",
"metadata": {},
"source": [
"Licensed under the Apache License, Version 2.0 (the \"License\");\n",
"you may not use this file except in compliance with the License.\n",
"You may obtain a copy of the License at\n",
"\n",
" http://www.apache.org/licenses/LICENSE-2.0\n",
"\n",
"Unless required by applicable law or agreed to in writing, software\n",
"distributed under the License is distributed on an \"AS IS\" BASIS,\n",
"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
"See the License for the specific language governing permissions and\n",
"limitations under the License.\n"
]
},
{
"cell_type": "markdown",
"id": "basename",
"metadata": {},
"source": [
"# scheduling_with_transitions_sat"
]
},
{
"cell_type": "markdown",
"id": "link",
"metadata": {},
"source": [
"<table align=\"left\">\n",
"<td>\n",
"<a href=\"https://colab.research.google.com/github/google/or-tools/blob/main/examples/notebook/contrib/scheduling_with_transitions_sat.ipynb\"><img src=\"https://raw.githubusercontent.com/google/or-tools/main/tools/colab_32px.png\"/>Run in Google Colab</a>\n",
"</td>\n",
"<td>\n",
"<a href=\"https://github.com/google/or-tools/blob/main/examples/contrib/scheduling_with_transitions_sat.py\"><img src=\"https://raw.githubusercontent.com/google/or-tools/main/tools/github_32px.png\"/>View source on GitHub</a>\n",
"</td>\n",
"</table>"
]
},
{
"cell_type": "markdown",
"id": "doc",
"metadata": {},
"source": [
"First, you must install [ortools](https://pypi.org/project/ortools/) package in this colab."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "install",
"metadata": {},
"outputs": [],
"source": [
"%pip install ortools"
]
},
{
"cell_type": "markdown",
"id": "description",
"metadata": {},
"source": [
"Scheduling problem with transition time between tasks and transitions costs.\n",
"\n",
"@author: CSLiu2\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "code",
"metadata": {},
"outputs": [],
"source": [
"import argparse\n",
"import collections\n",
"\n",
"from ortools.sat.python import cp_model\n",
"\n",
"#----------------------------------------------------------------------------\n",
"# Command line arguments.\n",
"PARSER = argparse.ArgumentParser()\n",
"PARSER.add_argument(\n",
" '--problem_instance', default=0, type=int, help='Problem instance.')\n",
"PARSER.add_argument(\n",
" '--output_proto',\n",
" default='',\n",
" help='Output file to write the cp_model proto to.')\n",
"PARSER.add_argument('--params', default='', help='Sat solver parameters.')\n",
"\n",
"\n",
"#----------------------------------------------------------------------------\n",
"# Intermediate solution printer\n",
"class SolutionPrinter(cp_model.CpSolverSolutionCallback):\n",
" \"\"\"Print intermediate solutions.\"\"\"\n",
"\n",
" def __init__(self, makespan):\n",
" cp_model.CpSolverSolutionCallback.__init__(self)\n",
" self.__solution_count = 0\n",
" self.__makespan = makespan\n",
"\n",
" def OnSolutionCallback(self):\n",
" print('Solution %i, time = %f s, objective = %i, makespan = %i' %\n",
" (self.__solution_count, self.WallTime(), self.ObjectiveValue(),\n",
" self.Value(self.__makespan)))\n",
" self.__solution_count += 1\n",
"\n",
"\n",
"def main(args):\n",
" \"\"\"Solves the scheduling with transitions problem.\"\"\"\n",
"\n",
" instance = args.problem_instance\n",
" parameters = args.params\n",
" output_proto = args.output_proto\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Data.\n",
" small_jobs = [[[(100, 0, 'R6'), (2, 1, 'R6')]],\n",
" [[(2, 0, 'R3'), (100, 1, 'R3')]],\n",
" [[(100, 0, 'R1'), (16, 1, 'R1')]],\n",
" [[(1, 0, 'R1'), (38, 1, 'R1')]], [[(14, 0, 'R1'), (10, 1,\n",
" 'R1')]],\n",
" [[(16, 0, 'R3'), (17, 1, 'R3')]],\n",
" [[(14, 0, 'R3'), (14, 1, 'R3')]],\n",
" [[(14, 0, 'R3'), (15, 1, 'R3')]],\n",
" [[(14, 0, 'R3'), (13, 1, 'R3')]],\n",
" [[(100, 0, 'R1'), (38, 1, 'R1')]]]\n",
"\n",
" large_jobs = [\n",
" [[(-1, 0, 'R1'), (10, 1, 'R1')]], [[(9, 0, 'R3'),\n",
" (22, 1, 'R3')]],\n",
" [[(-1, 0, 'R3'), (13, 1, 'R3')]], [[(-1, 0, 'R3'), (38, 1, 'R3')]],\n",
" [[(-1, 0, 'R3'), (38, 1, 'R3')]], [[(-1, 0, 'R3'), (16, 1, 'R3')]],\n",
" [[(-1, 0, 'R3'), (11, 1, 'R3')]], [[(-1, 0, 'R3'), (13, 1, 'R3')]],\n",
" [[(13, 0, 'R3'), (-1, 1, 'R3')]], [[(13, 0, 'R3'), (-1, 1, 'R3')]],\n",
" [[(-1, 0, 'R3'), (15, 1, 'R3')]], [[(12, 0, 'R1'), (-1, 1, 'R1')]],\n",
" [[(14, 0, 'R1'), (-1, 1, 'R1')]], [[(13, 0, 'R3'), (-1, 1, 'R3')]],\n",
" [[(-1, 0, 'R3'), (15, 1, 'R3')]], [[(14, 0, 'R1'), (-1, 1, 'R1')]],\n",
" [[(13, 0, 'R3'), (-1, 1, 'R3')]], [[(14, 0, 'R3'), (-1, 1, 'R3')]],\n",
" [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(15, 0, 'R1'), (-1, 1, 'R1')]],\n",
" [[(-1, 0, 'R2'), (16, 1, 'R2')]], [[(-1, 0, 'R2'), (16, 1, 'R2')]],\n",
" [[(-1, 0, 'R5'), (27, 1, 'R5')]], [[(-1, 0, 'R5'), (13, 1, 'R5')]],\n",
" [[(-1, 0, 'R5'), (13, 1, 'R5')]], [[(-1, 0, 'R5'), (13, 1, 'R5')]],\n",
" [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(-1, 0, 'R1'), (17, 1, 'R1')]],\n",
" [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],\n",
" [[(16, 0, 'R4'), (-1, 1, 'R4')]], [[(16, 0, 'R4'), (-1, 1, 'R4')]],\n",
" [[(16, 0, 'R4'), (-1, 1, 'R4')]], [[(16, 0, 'R4'), (-1, 1, 'R4')]],\n",
" [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],\n",
" [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],\n",
" [[(12, 0, 'R1'), (-1, 1, 'R1')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]],\n",
" [[(-1, 0, 'R5'), (14, 1, 'R5')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]],\n",
" [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]],\n",
" [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]],\n",
" [[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]],\n",
" [[(13, 0, 'R6'), (-1, 1, 'R6')]], [[(13, 0, 'R2'), (-1, 1, 'R2')]],\n",
" [[(-1, 0, 'R6'), (12, 1, 'R6')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]],\n",
" [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(-1, 0, 'R6'), (14, 1, 'R6')]],\n",
" [[(-1, 0, 'R5'), (5, 1, 'R5')]], [[(3, 0, 'R2'), (-1, 1, 'R2')]],\n",
" [[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]],\n",
" [[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R5'), (1, 1, 'R5')]],\n",
" [[(1, 0, 'R2'), (-1, 1, 'R2')]], [[(-1, 0, 'R2'), (19, 1, 'R2')]],\n",
" [[(13, 0, 'R4'), (-1, 1, 'R4')]], [[(12, 0, 'R4'), (-1, 1, 'R4')]],\n",
" [[(-1, 0, 'R3'), (2, 1, 'R3')]], [[(11, 0, 'R4'), (-1, 1, 'R4')]],\n",
" [[(6, 0, 'R6'), (-1, 1, 'R6')]], [[(6, 0, 'R6'), (-1, 1, 'R6')]],\n",
" [[(1, 0, 'R2'), (-1, 1, 'R2')]], [[(12, 0, 'R4'), (-1, 1, 'R4')]]\n",
" ]\n",
"\n",
" jobs = small_jobs if instance == 0 else large_jobs\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Helper data.\n",
" num_jobs = len(jobs)\n",
" all_jobs = range(num_jobs)\n",
" num_machines = 2\n",
" all_machines = range(num_machines)\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Model.\n",
" model = cp_model.CpModel()\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Compute a maximum makespan greedily.\n",
" horizon = 0\n",
" for job in jobs:\n",
" for task in job:\n",
" max_task_duration = 0\n",
" for alternative in task:\n",
" max_task_duration = max(max_task_duration, alternative[0])\n",
" horizon += max_task_duration\n",
"\n",
" print('Horizon = %i' % horizon)\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Global storage of variables.\n",
" intervals_per_machines = collections.defaultdict(list)\n",
" presences_per_machines = collections.defaultdict(list)\n",
" starts_per_machines = collections.defaultdict(list)\n",
" ends_per_machines = collections.defaultdict(list)\n",
" resources_per_machines = collections.defaultdict(list)\n",
" ranks_per_machines = collections.defaultdict(list)\n",
"\n",
" job_starts = {} # indexed by (job_id, task_id).\n",
" job_presences = {} # indexed by (job_id, task_id, alt_id).\n",
" job_ranks = {} # indexed by (job_id, task_id, alt_id).\n",
" job_ends = [] # indexed by job_id\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Scan the jobs and create the relevant variables and intervals.\n",
" for job_id in all_jobs:\n",
" job = jobs[job_id]\n",
" num_tasks = len(job)\n",
" previous_end = None\n",
" for task_id in range(num_tasks):\n",
" task = job[task_id]\n",
"\n",
" min_duration = task[0][0]\n",
" max_duration = task[0][0]\n",
"\n",
" num_alternatives = len(task)\n",
" all_alternatives = range(num_alternatives)\n",
"\n",
" for alt_id in range(1, num_alternatives):\n",
" alt_duration = task[alt_id][0]\n",
" min_duration = min(min_duration, alt_duration)\n",
" max_duration = max(max_duration, alt_duration)\n",
"\n",
" # Create main interval for the task.\n",
" suffix_name = '_j%i_t%i' % (job_id, task_id)\n",
" start = model.NewIntVar(0, horizon, 'start' + suffix_name)\n",
" duration = model.NewIntVar(min_duration, max_duration,\n",
" 'duration' + suffix_name)\n",
" end = model.NewIntVar(0, horizon, 'end' + suffix_name)\n",
"\n",
" # Store the start for the solution.\n",
" job_starts[(job_id, task_id)] = start\n",
"\n",
" # Add precedence with previous task in the same job.\n",
" if previous_end:\n",
" model.Add(start >= previous_end)\n",
" previous_end = end\n",
"\n",
" # Create alternative intervals.\n",
" l_presences = []\n",
" for alt_id in all_alternatives:\n",
" ### add to link interval with eqp constraint.\n",
" ### process time = -1 cannot be processed at this machine.\n",
" if jobs[job_id][task_id][alt_id][0] == -1:\n",
" continue\n",
" alt_suffix = '_j%i_t%i_a%i' % (job_id, task_id, alt_id)\n",
" l_presence = model.NewBoolVar('presence' + alt_suffix)\n",
" l_start = model.NewIntVar(0, horizon, 'start' + alt_suffix)\n",
" l_duration = task[alt_id][0]\n",
" l_end = model.NewIntVar(0, horizon, 'end' + alt_suffix)\n",
" l_interval = model.NewOptionalIntervalVar(\n",
" l_start, l_duration, l_end, l_presence, 'interval' + alt_suffix)\n",
" l_rank = model.NewIntVar(-1, num_jobs, 'rank' + alt_suffix)\n",
" l_presences.append(l_presence)\n",
" l_machine = task[alt_id][1]\n",
" l_type = task[alt_id][2]\n",
"\n",
" # Link the original variables with the local ones.\n",
" model.Add(start == l_start).OnlyEnforceIf(l_presence)\n",
" model.Add(duration == l_duration).OnlyEnforceIf(l_presence)\n",
" model.Add(end == l_end).OnlyEnforceIf(l_presence)\n",
"\n",
" # Add the local variables to the right machine.\n",
" intervals_per_machines[l_machine].append(l_interval)\n",
" starts_per_machines[l_machine].append(l_start)\n",
" ends_per_machines[l_machine].append(l_end)\n",
" presences_per_machines[l_machine].append(l_presence)\n",
" resources_per_machines[l_machine].append(l_type)\n",
" ranks_per_machines[l_machine].append(l_rank)\n",
"\n",
" # Store the variables for the solution.\n",
" job_presences[(job_id, task_id, alt_id)] = l_presence\n",
" job_ranks[(job_id, task_id, alt_id)] = l_rank\n",
"\n",
" # Only one machine can process each lot.\n",
" model.Add(sum(l_presences) == 1)\n",
"\n",
" job_ends.append(previous_end)\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Create machines constraints nonoverlap process\n",
" for machine_id in all_machines:\n",
" intervals = intervals_per_machines[machine_id]\n",
" if len(intervals) > 1:\n",
" model.AddNoOverlap(intervals)\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Transition times and transition costs using a circuit constraint.\n",
" switch_literals = []\n",
" for machine_id in all_machines:\n",
" machine_starts = starts_per_machines[machine_id]\n",
" machine_ends = ends_per_machines[machine_id]\n",
" machine_presences = presences_per_machines[machine_id]\n",
" machine_resources = resources_per_machines[machine_id]\n",
" machine_ranks = ranks_per_machines[machine_id]\n",
" intervals = intervals_per_machines[machine_id]\n",
" arcs = []\n",
" num_machine_tasks = len(machine_starts)\n",
" all_machine_tasks = range(num_machine_tasks)\n",
"\n",
" for i in all_machine_tasks:\n",
" # Initial arc from the dummy node (0) to a task.\n",
" start_lit = model.NewBoolVar('')\n",
" arcs.append([0, i + 1, start_lit])\n",
" # If this task is the first, set both rank and start to 0.\n",
" model.Add(machine_ranks[i] == 0).OnlyEnforceIf(start_lit)\n",
" model.Add(machine_starts[i] == 0).OnlyEnforceIf(start_lit)\n",
" # Final arc from an arc to the dummy node.\n",
" arcs.append([i + 1, 0, model.NewBoolVar('')])\n",
" # Self arc if the task is not performed.\n",
" arcs.append([i + 1, i + 1, machine_presences[i].Not()])\n",
" model.Add(machine_ranks[i] == -1).OnlyEnforceIf(\n",
" machine_presences[i].Not())\n",
"\n",
" for j in all_machine_tasks:\n",
" if i == j:\n",
" continue\n",
"\n",
" lit = model.NewBoolVar('%i follows %i' % (j, i))\n",
" arcs.append([i + 1, j + 1, lit])\n",
" model.AddImplication(lit, machine_presences[i])\n",
" model.AddImplication(lit, machine_presences[j])\n",
"\n",
" # Maintain rank incrementally.\n",
" model.Add(machine_ranks[j] == machine_ranks[i] + 1).OnlyEnforceIf(lit)\n",
"\n",
" # Compute the transition time if task j is the successor of task i.\n",
" if machine_resources[i] != machine_resources[j]:\n",
" transition_time = 3\n",
" switch_literals.append(lit)\n",
" else:\n",
" transition_time = 0\n",
" # We add the reified transition to link the literals with the times\n",
" # of the tasks.\n",
" model.Add(machine_starts[j] == machine_ends[i] +\n",
" transition_time).OnlyEnforceIf(lit)\n",
" if arcs:\n",
" model.AddCircuit(arcs)\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Objective.\n",
" makespan = model.NewIntVar(0, horizon, 'makespan')\n",
" model.AddMaxEquality(makespan, job_ends)\n",
" makespan_weight = 1\n",
" transition_weight = 5\n",
" model.Minimize(makespan * makespan_weight +\n",
" sum(switch_literals) * transition_weight)\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Write problem to file.\n",
" if output_proto:\n",
" print('Writing proto to %s' % output_proto)\n",
" with open(output_proto, 'w') as text_file:\n",
" text_file.write(str(model))\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Solve.\n",
" solver = cp_model.CpSolver()\n",
" solver.parameters.max_time_in_seconds = 60 * 60 * 2\n",
" if parameters:\n",
" solver.parameters.merge_text_format(parameters)\n",
" solution_printer = SolutionPrinter(makespan)\n",
" status = solver.Solve(model, solution_printer)\n",
"\n",
" #----------------------------------------------------------------------------\n",
" # Print solution.\n",
" if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL:\n",
" for job_id in all_jobs:\n",
" for task_id in range(len(jobs[job_id])):\n",
" start_value = solver.Value(job_starts[(job_id, task_id)])\n",
" machine = 0\n",
" duration = 0\n",
" select = 0\n",
" rank = -1\n",
"\n",
" for alt_id in range(len(jobs[job_id][task_id])):\n",
" if jobs[job_id][task_id][alt_id][0] == -1:\n",
" continue\n",
"\n",
" if solver.BooleanValue(job_presences[(job_id, task_id, alt_id)]):\n",
" duration = jobs[job_id][task_id][alt_id][0]\n",
" machine = jobs[job_id][task_id][alt_id][1]\n",
" select = alt_id\n",
" rank = solver.Value(job_ranks[(job_id, task_id, alt_id)])\n",
"\n",
" print(\n",
" ' Job %i starts at %i (alt %i, duration %i) with rank %i on machine %i'\n",
" % (job_id, start_value, select, duration, rank, machine))\n",
"\n",
" print('Solve status: %s' % solver.StatusName(status))\n",
" print('Objective value: %i' % solver.ObjectiveValue())\n",
" print('Makespan: %i' % solver.Value(makespan))\n",
"\n",
"\n",
"main(PARSER.parse_args())\n",
"\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}