261 lines
9.6 KiB
Plaintext
261 lines
9.6 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": [
|
|
"# ranking_circuit_sample_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/sat/ranking_circuit_sample_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/ortools/sat/samples/ranking_circuit_sample_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": [
|
|
"\n",
|
|
"Code sample to demonstrates how to rank intervals using a circuit.\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "code",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from collections.abc import Sequence\n",
|
|
"\n",
|
|
"from ortools.sat.python import cp_model\n",
|
|
"\n",
|
|
"\n",
|
|
"def rank_tasks_with_circuit(\n",
|
|
" model: cp_model.CpModel,\n",
|
|
" starts: Sequence[cp_model.IntVar],\n",
|
|
" durations: Sequence[int],\n",
|
|
" presences: Sequence[cp_model.IntVar],\n",
|
|
" ranks: Sequence[cp_model.IntVar],\n",
|
|
") -> None:\n",
|
|
" \"\"\"This method uses a circuit constraint to rank tasks.\n",
|
|
"\n",
|
|
" This method assumes that all starts are disjoint, meaning that all tasks have\n",
|
|
" a strictly positive duration, and they appear in the same NoOverlap\n",
|
|
" constraint.\n",
|
|
"\n",
|
|
" To implement this ranking, we will create a dense graph with num_tasks + 1\n",
|
|
" nodes.\n",
|
|
" The extra node (with id 0) will be used to decide which task is first with\n",
|
|
" its only outgoing arc, and which task is last with its only incoming arc.\n",
|
|
" Each task i will be associated with id i + 1, and an arc between i + 1 and j +\n",
|
|
" 1 indicates that j is the immediate successor of i.\n",
|
|
"\n",
|
|
" The circuit constraint ensures there is at most 1 hamiltonian cycle of\n",
|
|
" length > 1. If no such path exists, then no tasks are active.\n",
|
|
" We also need to enforce that any hamiltonian cycle of size > 1 must contain\n",
|
|
" the node 0. And thus, there is a self loop on node 0 iff the circuit is empty.\n",
|
|
"\n",
|
|
" Args:\n",
|
|
" model: The CpModel to add the constraints to.\n",
|
|
" starts: The array of starts variables of all tasks.\n",
|
|
" durations: the durations of all tasks.\n",
|
|
" presences: The array of presence variables of all tasks.\n",
|
|
" ranks: The array of rank variables of all tasks.\n",
|
|
" \"\"\"\n",
|
|
"\n",
|
|
" num_tasks = len(starts)\n",
|
|
" all_tasks = range(num_tasks)\n",
|
|
"\n",
|
|
" arcs: list[cp_model.ArcT] = []\n",
|
|
" for i in all_tasks:\n",
|
|
" # if node i is first.\n",
|
|
" start_lit = model.new_bool_var(f\"start_{i}\")\n",
|
|
" arcs.append((0, i + 1, start_lit))\n",
|
|
" model.add(ranks[i] == 0).only_enforce_if(start_lit)\n",
|
|
"\n",
|
|
" # As there are no other constraints on the problem, we can add this\n",
|
|
" # redundant constraint.\n",
|
|
" model.add(starts[i] == 0).only_enforce_if(start_lit)\n",
|
|
"\n",
|
|
" # if node i is last.\n",
|
|
" end_lit = model.new_bool_var(f\"end_{i}\")\n",
|
|
" arcs.append((i + 1, 0, end_lit))\n",
|
|
"\n",
|
|
" for j in all_tasks:\n",
|
|
" if i == j:\n",
|
|
" arcs.append((i + 1, i + 1, ~presences[i]))\n",
|
|
" model.add(ranks[i] == -1).only_enforce_if(~presences[i])\n",
|
|
" else:\n",
|
|
" literal = model.new_bool_var(f\"arc_{i}_to_{j}\")\n",
|
|
" arcs.append((i + 1, j + 1, literal))\n",
|
|
" model.add(ranks[j] == ranks[i] + 1).only_enforce_if(literal)\n",
|
|
"\n",
|
|
" # To perform the transitive reduction from precedences to successors,\n",
|
|
" # we need to tie the starts of the tasks with 'literal'.\n",
|
|
" # In a pure problem, the following inequality could be an equality.\n",
|
|
" # It is not true in general.\n",
|
|
" #\n",
|
|
" # Note that we could use this literal to penalize the transition, add an\n",
|
|
" # extra delay to the precedence.\n",
|
|
" model.add(starts[j] >= starts[i] + durations[i]).only_enforce_if(\n",
|
|
" literal\n",
|
|
" )\n",
|
|
"\n",
|
|
" # Manage the empty circuit\n",
|
|
" empty = model.new_bool_var(\"empty\")\n",
|
|
" arcs.append((0, 0, empty))\n",
|
|
"\n",
|
|
" for i in all_tasks:\n",
|
|
" model.add_implication(empty, ~presences[i])\n",
|
|
"\n",
|
|
" # Add the circuit constraint.\n",
|
|
" model.add_circuit(arcs)\n",
|
|
"\n",
|
|
"\n",
|
|
"def ranking_sample_sat() -> None:\n",
|
|
" \"\"\"Ranks tasks in a NoOverlap constraint.\"\"\"\n",
|
|
"\n",
|
|
" model = cp_model.CpModel()\n",
|
|
" horizon = 100\n",
|
|
" num_tasks = 4\n",
|
|
" all_tasks = range(num_tasks)\n",
|
|
"\n",
|
|
" starts = []\n",
|
|
" durations = []\n",
|
|
" intervals = []\n",
|
|
" presences = []\n",
|
|
" ranks = []\n",
|
|
"\n",
|
|
" # Creates intervals, half of them are optional.\n",
|
|
" for t in all_tasks:\n",
|
|
" start = model.new_int_var(0, horizon, f\"start[{t}]\")\n",
|
|
" duration = t + 1\n",
|
|
" presence = model.new_bool_var(f\"presence[{t}]\")\n",
|
|
" interval = model.new_optional_fixed_size_interval_var(\n",
|
|
" start, duration, presence, f\"opt_interval[{t}]\"\n",
|
|
" )\n",
|
|
" if t < num_tasks // 2:\n",
|
|
" model.add(presence == 1)\n",
|
|
"\n",
|
|
" starts.append(start)\n",
|
|
" durations.append(duration)\n",
|
|
" intervals.append(interval)\n",
|
|
" presences.append(presence)\n",
|
|
"\n",
|
|
" # Ranks = -1 if and only if the tasks is not performed.\n",
|
|
" ranks.append(model.new_int_var(-1, num_tasks - 1, f\"rank[{t}]\"))\n",
|
|
"\n",
|
|
" # Adds NoOverlap constraint.\n",
|
|
" model.add_no_overlap(intervals)\n",
|
|
"\n",
|
|
" # Adds ranking constraint.\n",
|
|
" rank_tasks_with_circuit(model, starts, durations, presences, ranks)\n",
|
|
"\n",
|
|
" # Adds a constraint on ranks.\n",
|
|
" model.add(ranks[0] < ranks[1])\n",
|
|
"\n",
|
|
" # Creates makespan variable.\n",
|
|
" makespan = model.new_int_var(0, horizon, \"makespan\")\n",
|
|
" for t in all_tasks:\n",
|
|
" model.add(starts[t] + durations[t] <= makespan).only_enforce_if(presences[t])\n",
|
|
"\n",
|
|
" # Minimizes makespan - fixed gain per tasks performed.\n",
|
|
" # As the fixed cost is less that the duration of the last interval,\n",
|
|
" # the solver will not perform the last interval.\n",
|
|
" model.minimize(2 * makespan - 7 * sum(presences[t] for t in all_tasks))\n",
|
|
"\n",
|
|
" # Solves the model model.\n",
|
|
" solver = cp_model.CpSolver()\n",
|
|
" status = solver.solve(model)\n",
|
|
"\n",
|
|
" if status == cp_model.OPTIMAL:\n",
|
|
" # Prints out the makespan and the start times and ranks of all tasks.\n",
|
|
" print(f\"Optimal cost: {solver.objective_value}\")\n",
|
|
" print(f\"Makespan: {solver.value(makespan)}\")\n",
|
|
" for t in all_tasks:\n",
|
|
" if solver.value(presences[t]):\n",
|
|
" print(\n",
|
|
" f\"Task {t} starts at {solver.value(starts[t])} \"\n",
|
|
" f\"with rank {solver.value(ranks[t])}\"\n",
|
|
" )\n",
|
|
" else:\n",
|
|
" print(\n",
|
|
" f\"Task {t} in not performed and ranked at {solver.value(ranks[t])}\"\n",
|
|
" )\n",
|
|
" else:\n",
|
|
" print(f\"Solver exited with nonoptimal status: {status}\")\n",
|
|
"\n",
|
|
"\n",
|
|
"ranking_sample_sat()\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"language_info": {
|
|
"name": "python"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|