From c3164316fbd49a6f3b5f3924873737ff7b27c2bb Mon Sep 17 00:00:00 2001 From: Mizux Seiha Date: Fri, 19 Dec 2025 15:01:21 +0100 Subject: [PATCH] examples: regenerate notebook --- examples/notebook/algorithms/knapsack.ipynb | 1 + .../algorithms/simple_knapsack_program.ipynb | 1 + .../contrib/permutation_flow_shop.ipynb | 3 +- .../scheduling_with_transitions_sat.ipynb | 3 +- examples/notebook/examples/appointments.ipynb | 2 +- .../examples/arc_flow_cutting_stock_sat.ipynb | 3 +- .../notebook/examples/balance_group_sat.ipynb | 3 +- .../examples/bus_driver_scheduling_sat.ipynb | 11 +- .../car_sequencing_optimization_sat.ipynb | 349 ++++++++ .../notebook/examples/cryptarithm_sat.ipynb | 3 +- examples/notebook/examples/cvrptw_plot.ipynb | 833 ------------------ .../examples/gate_scheduling_sat.ipynb | 2 +- examples/notebook/examples/golomb8.ipynb | 2 +- examples/notebook/examples/golomb_sat.ipynb | 4 +- .../examples/horse_jumping_show.ipynb | 376 ++++++++ .../notebook/examples/knapsack_2d_sat.ipynb | 8 +- .../examples/line_balancing_sat.ipynb | 6 +- .../examples/linear_assignment_api.ipynb | 13 +- .../examples/maximize_combinations_sat.ipynb | 1 + .../notebook/examples/maze_escape_sat.ipynb | 6 +- .../memory_layout_and_infeasibility_sat.ipynb | 5 +- .../examples/music_playlist_sat.ipynb | 388 ++++++++ .../no_wait_baking_scheduling_sat.ipynb | 3 +- .../notebook/examples/pentominoes_sat.ipynb | 3 +- .../examples/prize_collecting_vrp.ipynb | 2 + .../notebook/examples/pyflow_example.ipynb | 7 +- examples/notebook/examples/rcpsp_sat.ipynb | 3 +- .../examples/shift_scheduling_sat.ipynb | 3 +- ...ing_with_setup_release_due_dates_sat.ipynb | 3 +- .../notebook/examples/spillover_sat.ipynb | 439 +++++++++ .../notebook/examples/spread_robots_sat.ipynb | 3 +- .../examples/steel_mill_slab_sat.ipynb | 7 +- .../examples/test_scheduling_sat.ipynb | 3 +- examples/notebook/examples/transit_time.ipynb | 1 - .../weighted_latency_problem_sat.ipynb | 8 +- .../assignment_linear_sum_assignment.ipynb | 1 + .../notebook/graph/assignment_min_flow.ipynb | 1 + .../notebook/graph/balance_min_flow.ipynb | 1 + .../graph/simple_max_flow_program.ipynb | 1 + .../graph/simple_min_cost_flow_program.ipynb | 1 + .../linear_solver/assignment_groups_mip.ipynb | 1 + .../linear_solver/assignment_mb.ipynb | 1 + .../linear_solver/assignment_mip.ipynb | 1 + .../assignment_task_sizes_mip.ipynb | 1 + .../linear_solver/assignment_teams_mip.ipynb | 1 + .../linear_solver/basic_example.ipynb | 1 + .../linear_solver/bin_packing_mb.ipynb | 1 + .../linear_solver/bin_packing_mip.ipynb | 2 + .../linear_solver/clone_model_mb.ipynb | 1 + .../integer_programming_example.ipynb | 1 + .../linear_programming_example.ipynb | 1 + .../linear_solver/mip_var_array.ipynb | 2 + .../linear_solver/multiple_knapsack_mip.ipynb | 1 + .../linear_solver/simple_lp_program.ipynb | 1 + .../linear_solver/simple_lp_program_mb.ipynb | 1 + .../linear_solver/simple_mip_program.ipynb | 1 + .../linear_solver/simple_mip_program_mb.ipynb | 1 + .../notebook/linear_solver/stigler_diet.ipynb | 1 + .../sat/ranking_circuit_sample_sat.ipynb | 8 +- .../sequences_in_no_overlap_sample_sat.ipynb | 10 +- .../notebook/sat/soft_constraints_sat.ipynb | 249 ++++++ ...transitions_in_no_overlap_sample_sat.ipynb | 11 +- examples/notebook/set_cover/set_cover.ipynb | 1 + 63 files changed, 1903 insertions(+), 909 deletions(-) create mode 100644 examples/notebook/examples/car_sequencing_optimization_sat.ipynb delete mode 100644 examples/notebook/examples/cvrptw_plot.ipynb create mode 100644 examples/notebook/examples/horse_jumping_show.ipynb create mode 100644 examples/notebook/examples/music_playlist_sat.ipynb create mode 100644 examples/notebook/examples/spillover_sat.ipynb create mode 100644 examples/notebook/sat/soft_constraints_sat.ipynb diff --git a/examples/notebook/algorithms/knapsack.ipynb b/examples/notebook/algorithms/knapsack.ipynb index c0d34c4ad7..553a09d2ac 100644 --- a/examples/notebook/algorithms/knapsack.ipynb +++ b/examples/notebook/algorithms/knapsack.ipynb @@ -86,6 +86,7 @@ "from ortools.algorithms.python import knapsack_solver\n", "\n", "\n", + "\n", "def main():\n", " # Create the solver.\n", " solver = knapsack_solver.KnapsackSolver(\n", diff --git a/examples/notebook/algorithms/simple_knapsack_program.ipynb b/examples/notebook/algorithms/simple_knapsack_program.ipynb index 820c62baa0..824f0a67c5 100644 --- a/examples/notebook/algorithms/simple_knapsack_program.ipynb +++ b/examples/notebook/algorithms/simple_knapsack_program.ipynb @@ -86,6 +86,7 @@ "from ortools.algorithms.python import knapsack_solver\n", "\n", "\n", + "\n", "def main():\n", " # Create the solver.\n", " solver = knapsack_solver.KnapsackSolver(\n", diff --git a/examples/notebook/contrib/permutation_flow_shop.ipynb b/examples/notebook/contrib/permutation_flow_shop.ipynb index 607f1e4847..6f99533cc0 100644 --- a/examples/notebook/contrib/permutation_flow_shop.ipynb +++ b/examples/notebook/contrib/permutation_flow_shop.ipynb @@ -95,7 +95,6 @@ "import numpy as np\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_PARAMS = flags.define_string(\n", @@ -217,7 +216,7 @@ "\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " solver.parameters.log_search_progress = log\n", " solver.parameters.max_time_in_seconds = time_limit\n", "\n", diff --git a/examples/notebook/contrib/scheduling_with_transitions_sat.ipynb b/examples/notebook/contrib/scheduling_with_transitions_sat.ipynb index f1676beeeb..2471e77050 100644 --- a/examples/notebook/contrib/scheduling_with_transitions_sat.ipynb +++ b/examples/notebook/contrib/scheduling_with_transitions_sat.ipynb @@ -90,7 +90,6 @@ "import collections\n", "\n", "from ortools.sat.python import cp_model\n", - "from google.protobuf import text_format\n", "\n", "#----------------------------------------------------------------------------\n", "# Command line arguments.\n", @@ -376,7 +375,7 @@ " solver = cp_model.CpSolver()\n", " solver.parameters.max_time_in_seconds = 60 * 60 * 2\n", " if parameters:\n", - " text_format.Merge(parameters, solver.parameters)\n", + " solver.parameters.merge_text_format(parameters)\n", " solution_printer = SolutionPrinter(makespan)\n", " status = solver.Solve(model, solution_printer)\n", "\n", diff --git a/examples/notebook/examples/appointments.ipynb b/examples/notebook/examples/appointments.ipynb index f4c85fec33..b2e075e178 100644 --- a/examples/notebook/examples/appointments.ipynb +++ b/examples/notebook/examples/appointments.ipynb @@ -244,7 +244,7 @@ "\n", "\n", "def get_optimal_schedule(\n", - " demand: list[tuple[float, str, int]]\n", + " demand: list[tuple[float, str, int]],\n", ") -> list[tuple[int, list[tuple[int, str]]]]:\n", " \"\"\"Computes the optimal schedule for the installation input.\n", "\n", diff --git a/examples/notebook/examples/arc_flow_cutting_stock_sat.ipynb b/examples/notebook/examples/arc_flow_cutting_stock_sat.ipynb index 89c9809afc..94e93cd8e5 100644 --- a/examples/notebook/examples/arc_flow_cutting_stock_sat.ipynb +++ b/examples/notebook/examples/arc_flow_cutting_stock_sat.ipynb @@ -89,7 +89,6 @@ "from ortools.sat.colab import flags\n", "import numpy as np\n", "\n", - "from google.protobuf import text_format\n", "from ortools.linear_solver.python import model_builder as mb\n", "from ortools.sat.python import cp_model\n", "\n", @@ -387,7 +386,7 @@ " # Solve model.\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " solver.parameters.log_search_progress = True\n", " solver.Solve(model)\n", "\n", diff --git a/examples/notebook/examples/balance_group_sat.ipynb b/examples/notebook/examples/balance_group_sat.ipynb index aa8477b300..3cea13a81a 100644 --- a/examples/notebook/examples/balance_group_sat.ipynb +++ b/examples/notebook/examples/balance_group_sat.ipynb @@ -78,7 +78,8 @@ "Each item has a color and a value. We want the sum of values of each group to\n", "be as close to the average as possible.\n", "Furthermore, if one color is an a group, at least k items with this color must\n", - "be in that group.\n" + "be in that group.\n", + "\n" ] }, { diff --git a/examples/notebook/examples/bus_driver_scheduling_sat.ipynb b/examples/notebook/examples/bus_driver_scheduling_sat.ipynb index 896dce73ab..ae52c1adc6 100644 --- a/examples/notebook/examples/bus_driver_scheduling_sat.ipynb +++ b/examples/notebook/examples/bus_driver_scheduling_sat.ipynb @@ -97,7 +97,6 @@ "import math\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_OUTPUT_PROTO = flags.define_string(\n", @@ -149,7 +148,7 @@ " [25, \"15:40\", \"15:56\", 940, 956, 16],\n", " [26, \"15:58\", \"16:45\", 958, 1005, 47],\n", " [27, \"16:04\", \"17:30\", 964, 1050, 86],\n", - "] # yapf:disable\n", + "]\n", "\n", "SAMPLE_SHIFTS_SMALL = [\n", " #\n", @@ -211,7 +210,7 @@ " [47, \"18:34\", \"19:58\", 1114, 1198, 84],\n", " [48, \"19:56\", \"20:34\", 1196, 1234, 38],\n", " [49, \"20:05\", \"20:48\", 1205, 1248, 43],\n", - "] # yapf:disable\n", + "]\n", "\n", "SAMPLE_SHIFTS_MEDIUM = [\n", " [0, \"04:30\", \"04:53\", 270, 293, 23],\n", @@ -414,7 +413,7 @@ " [197, \"00:02\", \"00:12\", 1442, 1452, 10],\n", " [198, \"00:07\", \"00:39\", 1447, 1479, 32],\n", " [199, \"00:25\", \"01:12\", 1465, 1512, 47],\n", - "] # yapf:disable\n", + "]\n", "\n", "SAMPLE_SHIFTS_LARGE = [\n", " [0, \"04:18\", \"05:00\", 258, 300, 42],\n", @@ -1773,7 +1772,7 @@ " [1353, \"00:47\", \"01:26\", 1487, 1526, 39],\n", " [1354, \"00:54\", \"01:04\", 1494, 1504, 10],\n", " [1355, \"00:57\", \"01:07\", 1497, 1507, 10],\n", - "] # yapf:disable\n", + "]\n", "\n", "\n", "def bus_driver_scheduling(minimize_drivers: bool, max_num_drivers: int) -> int:\n", @@ -2049,7 +2048,7 @@ " # Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", "\n", " status = solver.solve(model)\n", "\n", diff --git a/examples/notebook/examples/car_sequencing_optimization_sat.ipynb b/examples/notebook/examples/car_sequencing_optimization_sat.ipynb new file mode 100644 index 0000000000..0e0ff138be --- /dev/null +++ b/examples/notebook/examples/car_sequencing_optimization_sat.ipynb @@ -0,0 +1,349 @@ +{ + "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": [ + "# car_sequencing_optimization_sat" + ] + }, + { + "cell_type": "markdown", + "id": "link", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "
\n", + "Run in Google Colab\n", + "\n", + "View source on GitHub\n", + "
" + ] + }, + { + "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", + "Solve the car sequencing problem as an optimization problem.\n", + "\n", + "Problem Description: The Car Sequencing Problem with Optimization\n", + "-----------------------------------------------------------------\n", + "\n", + "See https://en.wikipedia.org/wiki/Car_sequencing_problem for more details.\n", + "\n", + "We are tasked with determining the optimal production sequence for a set of cars\n", + "on an assembly line. This is a classic and challenging combinatorial\n", + "optimization problem with the following characteristics:\n", + "\n", + "Fixed Production Demand: There is a specific, non-negotiable number of cars of\n", + "different types (or 'classes') that must be produced. In our case, we have 6\n", + "distinct classes of cars, and we must produce exactly 5 of each, for a total of\n", + "30 'real' cars.\n", + "\n", + "Diverse Car Configurations: Each car class is defined by a unique combination of\n", + "optional features. For example, 'Class 1' might require a sunroof (Option 1) and\n", + "a special engine (Option 4), while 'Class 3' only requires air conditioning\n", + "(Option 2).\n", + "\n", + "Specialized Assembly Stations: The assembly line is composed of a series of\n", + "specialized stations. Each station is responsible for installing one specific\n", + "option. For example, there is one station for sunroofs, one for special engines,\n", + "and so on.\n", + "\n", + "Capacity-Limited Stations: The core challenge of the problem lies here. The\n", + "stations cannot handle an unlimited, dense flow of cars requiring their specific\n", + "option. Their capacity is defined by a 'sliding window' constraint. For example,\n", + "the sunroof station might have a constraint of 'at most 1 car with a sunroof in\n", + "any sequence of 3 consecutive cars'. This means sequences like [Sunroof, No, No,\n", + "Sunroof] are valid, but [Sunroof, No, Sunroof, No] are not.\n", + "\n", + "The Need for Spacing (Optimization): The combination of high demand for certain\n", + "options and tight capacity constraints may make it impossible to produce the 30\n", + "real cars consecutively. To create a valid sequence, we may need to insert\n", + "'dummy' or 'filler' cars into the production line. These dummy cars have no\n", + "options and therefore do not consume capacity at any station. They serve purely\n", + "as spacers to break up dense sequences of option-heavy cars.\n", + "\n", + "The Goal: The objective is to find a production sequence that fulfills the\n", + "demand for all 30 real cars while using the minimum number of dummy cars. This\n", + "is equivalent to finding the shortest possible total production schedule (real\n", + "cars + dummy cars).\n", + "\n", + "Modeling and Solution Approach with CP-SAT\n", + "------------------------------------------\n", + "\n", + "To solve this problem, we use the CP-SAT solver from Google's OR-Tools library.\n", + "This is a constraint programming approach, which works by defining variables,\n", + "constraints, and an objective function.\n", + "\n", + "1. Decision Variables\n", + "The fundamental decision the solver must make is: 'Which class of car should be\n", + "placed in each production slot?'\n", + "We define a large number of boolean variables: produces[c][s]. This variable is\n", + "True if a car of class c is scheduled in slot s, and False otherwise. We create\n", + "these for all car classes (including the dummy class) and for an extended number\n", + "of slots (30 real + a buffer of 20 for dummies).\n", + "We introduce a key integer variable: makespan. This variable represents the\n", + "total length of the 'meaningful' part of our schedule. It's the slot number\n", + "where the first dummy car appears, after which all subsequent cars are also\n", + "dummies.\n", + "\n", + "2. Constraints (The Rules of the Game)\n", + "We translate the problem's rules into mathematical constraints that the solver\n", + "must obey:\n", + "\n", + "One Car Per Slot: For every production slot s, exactly one car class can be\n", + "assigned. We enforce this using an AddExactlyOne constraint over all\n", + "produces[c][s] variables for that slot.\n", + "\n", + "Fulfill Real Car Demand: The total number of times each real car class c appears\n", + "across all slots must equal its required demand (5 in our case). This is a\n", + "simple Add(sum(...) == 5) constraint.\n", + "\n", + "Station Capacity (Sliding Window): This is the most critical constraint. For\n", + "each option (e.g., 'sunroof') and its capacity rule (e.g., '1 in 3'), we create\n", + "constraints for every possible sliding window. For every subsequence of 3 slots,\n", + "we sum up the produces variables corresponding to car classes that require that\n", + "option and constrain this sum to be less than or equal to 1.\n", + "\n", + "Makespan Definition: This is the clever part of the model. We link our makespan\n", + "objective variable to the placement of dummy cars using logical equivalences for\n", + "each slot s:\n", + "(makespan <= s) is equivalent to (slot s contains a dummy car)\n", + "This ensures that if the solver chooses a makespan of 32, for example, it is\n", + "forced to place dummy cars in slots 32, 33, 34, and so on. Conversely, if the\n", + "solver is forced to place a dummy car in slot 32 to satisfy a capacity\n", + "constraint, the makespan must be at most 32.\n", + "\n", + "3. The Objective Function\n", + "\n", + "The objective is simple and directly tied to our goal:\n", + "\n", + "Minimize makespan: By instructing the solver to find a solution with the\n", + "smallest possible value for the makespan variable, we are asking it to find the\n", + "shortest possible production schedule that satisfies all the rules. This\n", + "inherently minimizes the number of dummy cars used.\n", + "\n", + "By defining the problem in this way, we let the CP-SAT solver explore the vast\n", + "search space of possible sequences efficiently, using its powerful constraint\n", + "propagation and search techniques to find an optimal arrangement that meets all\n", + "our complex requirements.\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 solve_car_sequencing_optimization() -> None:\n", + " \"\"\"Solves the car sequencing problem with an optimization approach.\"\"\"\n", + "\n", + " # --------------------\n", + " # 1. Data\n", + " # --------------------\n", + " num_real_cars: int = 30\n", + " max_dummy_cars: int = 20\n", + " num_slots = num_real_cars + max_dummy_cars\n", + " all_slots = range(num_slots)\n", + "\n", + " class_options = [\n", + " # Options: 1 2 3 4 5\n", + " [0, 0, 0, 0, 0], # Class 0 (Dummy)\n", + " [1, 0, 0, 1, 0], # Class 1\n", + " [0, 1, 0, 0, 1], # Class 2\n", + " [0, 1, 0, 0, 0], # Class 3\n", + " [0, 0, 1, 1, 0], # Class 4\n", + " [0, 0, 1, 0, 0], # Class 5\n", + " [0, 0, 0, 0, 1], # Class 6\n", + " ]\n", + " num_classes = len(class_options)\n", + " all_classes = range(num_classes)\n", + " real_classes = range(1, num_classes)\n", + " dummy_class = 0\n", + "\n", + " demands = [5, 5, 5, 5, 5, 5]\n", + "\n", + " capacity_constraints = [(1, 3), (1, 2), (1, 3), (2, 5), (1, 5)]\n", + " num_options = len(capacity_constraints)\n", + " all_options = range(num_options)\n", + "\n", + " classes_with_option = [\n", + " [c for c in real_classes if class_options[c][o] == 1] for o in all_options\n", + " ]\n", + "\n", + " # --------------------\n", + " # 2. Model Creation\n", + " # --------------------\n", + " model = cp_model.CpModel()\n", + "\n", + " # --------------------\n", + " # 3. Decision Variables\n", + " # --------------------\n", + " produces = {}\n", + " for c in all_classes:\n", + " for s in all_slots:\n", + " produces[(c, s)] = model.new_bool_var(f\"produces_c{c}_s{s}\")\n", + "\n", + " makespan = model.new_int_var(num_real_cars, num_slots, \"makespan\")\n", + "\n", + " # --------------------\n", + " # 4. Constraints\n", + " # --------------------\n", + "\n", + " # Constraint 1: Only one car produced per slot.\n", + " for s in all_slots:\n", + " model.add_exactly_one([produces[(c, s)] for c in all_classes])\n", + "\n", + " # Constraint 2: Meet the demand of real cars.\n", + " for i, c in enumerate(real_classes):\n", + " model.add(sum(produces[(c, s)] for s in all_slots) == demands[i])\n", + "\n", + " # Constraint 3: Enforce the capacity constraints on options.\n", + " for o in all_options:\n", + " max_cars, subsequence_len = capacity_constraints[o]\n", + " for start in range(num_slots - subsequence_len + 1):\n", + " window = range(start, start + subsequence_len)\n", + " cars_with_option_in_window = []\n", + " for c in classes_with_option[o]:\n", + " for s in window:\n", + " cars_with_option_in_window.append(produces[(c, s)])\n", + " model.add(sum(cars_with_option_in_window) <= max_cars)\n", + "\n", + " # Constraint 4 (Link objective and dummy cars at the end of the schedule)\n", + " for s in all_slots:\n", + " makespan_le_s = model.new_bool_var(f\"makespan_le_{s}\")\n", + "\n", + " # Enforce makespan_le_s <=> (makespan <= s)\n", + " model.add(makespan <= s).only_enforce_if(makespan_le_s)\n", + " # Use ~ for negation\n", + " model.add(makespan > s).only_enforce_if(~makespan_le_s)\n", + "\n", + " # Enforce makespan_le_s => produces[dummy_class, s]\n", + " model.add_implication(makespan_le_s, produces[dummy_class, s])\n", + "\n", + " # --------------------\n", + " # 5. Objective\n", + " # --------------------\n", + " model.minimize(makespan)\n", + "\n", + " # --------------------\n", + " # 6. Solve and Print Solution\n", + " # --------------------\n", + " solver = cp_model.CpSolver()\n", + " solver.parameters.max_time_in_seconds = 30.0\n", + " solver.parameters.num_search_workers = 1 # The problem is easy to solve.\n", + " # solver.parameters.log_search_progress = True # uncomment to see the log.\n", + "\n", + " status = solver.Solve(model)\n", + "\n", + " if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:\n", + " final_makespan = int(solver.ObjectiveValue())\n", + " num_dummies_needed = final_makespan - num_real_cars\n", + "\n", + " print(\n", + " f'\\n{\"Optimal\" if status == cp_model.OPTIMAL else \"Feasible\"}'\n", + " f\" solution found with a makespan of {final_makespan}.\"\n", + " )\n", + " print(\n", + " f\"This requires the conceptual equivalent of {num_dummies_needed} dummy\"\n", + " \" car(s) to be used as spacers.\"\n", + " )\n", + "\n", + " sequence = [-1] * num_slots\n", + " for s in all_slots:\n", + " for c in all_classes:\n", + " if solver.Value(produces[(c, s)]) == 1:\n", + " sequence[s] = c\n", + " break\n", + "\n", + " print(\"\\nFull Production Sequence (Class 0 is dummy):\")\n", + " print(\"Slot: | \" + \" | \".join(f\"{i:2}\" for i in range(num_slots)) + \" |\")\n", + " print(\"-------|-\" + \"--|-\" * num_slots)\n", + " print(\"Class: | \" + \" | \".join(f\"{c:2}\" for c in sequence) + \" |\")\n", + "\n", + " elif status == cp_model.INFEASIBLE:\n", + " print(\"\\nNo solution found.\")\n", + "\n", + " else:\n", + " print(f\"\\nSomething went wrong. Solver status: {status}\")\n", + "\n", + " print(\"\\nSolver statistics:\")\n", + " print(solver.response_stats())\n", + "\n", + "\n", + "def main(argv: Sequence[str]) -> None:\n", + " if len(argv) > 1:\n", + " raise app.UsageError(\"Too many command-line arguments.\")\n", + " solve_car_sequencing_optimization()\n", + "\n", + "\n", + "main()\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebook/examples/cryptarithm_sat.ipynb b/examples/notebook/examples/cryptarithm_sat.ipynb index d04c6c890f..2849b51f37 100644 --- a/examples/notebook/examples/cryptarithm_sat.ipynb +++ b/examples/notebook/examples/cryptarithm_sat.ipynb @@ -73,8 +73,7 @@ "metadata": {}, "source": [ "\n", - "Use CP-SAT to solve a simple cryptarithmetic problem: SEND+MORE=MONEY.\n", - "\n" + "Use CP-SAT to solve a simple cryptarithmetic problem: SEND+MORE=MONEY.\n" ] }, { diff --git a/examples/notebook/examples/cvrptw_plot.ipynb b/examples/notebook/examples/cvrptw_plot.ipynb deleted file mode 100644 index 5f7928f82f..0000000000 --- a/examples/notebook/examples/cvrptw_plot.ipynb +++ /dev/null @@ -1,833 +0,0 @@ -{ - "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": [ - "# cvrptw_plot" - ] - }, - { - "cell_type": "markdown", - "id": "link", - "metadata": {}, - "source": [ - "\n", - "\n", - "\n", - "
\n", - "Run in Google Colab\n", - "\n", - "View source on GitHub\n", - "
" - ] - }, - { - "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": [ - "Capacitated Vehicle Routing Problem with Time Windows (and optional orders).\n", - "\n", - " This is a sample using the routing library python wrapper to solve a\n", - " CVRPTW problem.\n", - " A description of the problem can be found here:\n", - " http://en.wikipedia.org/wiki/Vehicle_routing_problem.\n", - " The variant which is tackled by this model includes a capacity dimension,\n", - " time windows and optional orders, with a penalty cost if orders are not\n", - " performed.\n", - " To help explore the problem, two classes are provided Customers() and\n", - " Vehicles(): used to randomly locate orders and depots, and to randomly\n", - " generate demands, time-window constraints and vehicles.\n", - " Distances are computed using the Great Circle distances. Distances are in km\n", - " and times in seconds.\n", - "\n", - " A function for the displaying of the vehicle plan\n", - " display_vehicle_output\n", - "\n", - " The optimization engine uses local search to improve solutions, first\n", - " solutions being generated using a cheapest addition heuristic.\n", - " Numpy and Matplotlib are required for the problem creation and display.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "code", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import numpy as np\n", - "from matplotlib import pyplot as plt\n", - "from collections import namedtuple\n", - "from ortools.constraint_solver import pywrapcp\n", - "from ortools.constraint_solver import routing_enums_pb2\n", - "from datetime import datetime, timedelta\n", - "\n", - "\n", - "class Customers():\n", - " \"\"\"\n", - " A class that generates and holds customers information.\n", - "\n", - " Randomly normally distribute a number of customers and locations within\n", - " a region described by a rectangle. Generate a random demand for each\n", - " customer. Generate a random time window for each customer.\n", - " May either be initiated with the extents, as a dictionary describing\n", - " two corners of a rectangle in latitude and longitude OR as a center\n", - " point (lat, lon), and box_size in km. The default arguments are for a\n", - " 10 x 10 km square centered in Sheffield).\n", - "\n", - " Args: extents (Optional[Dict]): A dictionary describing a rectangle in\n", - " latitude and longitude with the keys 'llcrnrlat', 'llcrnrlon' &\n", - " 'urcrnrlat' & 'urcrnrlat' center (Optional(Tuple): A tuple of\n", - " (latitude, longitude) describing the centre of the rectangle. box_size\n", - " (Optional float: The length in km of the box's sides. num_stops (int):\n", - " The number of customers, including the depots that are placed normally\n", - " distributed in the rectangle. min_demand (int): Lower limit on the\n", - " randomly generated demand at each customer. max_demand (int): Upper\n", - " limit on the randomly generated demand at each customer.\n", - " min_tw: shortest random time window for a customer, in hours.\n", - " max_tw: longest random time window for a customer, in hours.\n", - " Examples: To place 100 customers randomly within 100 km x 100 km\n", - " rectangle, centered in the default location, with a random demand of\n", - " between 5 and 10 units: >>> customers = Customers(num_stops=100,\n", - " box_size=100, ... min_demand=5, max_demand=10)\n", - " alternatively, to place 75 customers in the same area with default\n", - " arguments for demand: >>> extents = {'urcrnrlon': 0.03403, 'llcrnrlon':\n", - " -2.98325, ... 'urcrnrlat': 54.28127, 'llcrnrlat': 52.48150} >>>\n", - " customers = Customers(num_stops=75, extents=extents)\n", - " \"\"\"\n", - "\n", - " def __init__(self,\n", - " extents=None,\n", - " center=(53.381393, -1.474611),\n", - " box_size=10,\n", - " num_stops=100,\n", - " min_demand=0,\n", - " max_demand=25,\n", - " min_tw=1,\n", - " max_tw=5):\n", - " self.number = num_stops #: The number of customers and depots\n", - " #: Location, a named tuple for locations.\n", - " Location = namedtuple('Location', ['lat', 'lon'])\n", - " if extents is not None:\n", - " self.extents = extents #: The lower left and upper right points\n", - " #: Location[lat,lon]: the centre point of the area.\n", - " self.center = Location(\n", - " extents['urcrnrlat'] - 0.5 *\n", - " (extents['urcrnrlat'] - extents['llcrnrlat']),\n", - " extents['urcrnrlon'] - 0.5 *\n", - " (extents['urcrnrlon'] - extents['llcrnrlon']))\n", - " else:\n", - " #: Location[lat,lon]: the centre point of the area.\n", - " (clat, clon) = self.center = Location(center[0], center[1])\n", - " rad_earth = 6367 # km\n", - " circ_earth = np.pi * rad_earth\n", - " #: The lower left and upper right points\n", - " self.extents = {\n", - " 'llcrnrlon': (clon - 180 * box_size /\n", - " (circ_earth * np.cos(np.deg2rad(clat)))),\n", - " 'llcrnrlat':\n", - " clat - 180 * box_size / circ_earth,\n", - " 'urcrnrlon': (clon + 180 * box_size /\n", - " (circ_earth * np.cos(np.deg2rad(clat)))),\n", - " 'urcrnrlat':\n", - " clat + 180 * box_size / circ_earth\n", - " }\n", - " # The 'name' of the stop, indexed from 0 to num_stops-1\n", - " stops = np.array(range(0, num_stops))\n", - " # normaly distributed random distribution of stops within the box\n", - " stdv = 6 # the number of standard deviations 99.9% will be within +-3\n", - " lats = (self.extents['llcrnrlat'] + np.random.randn(num_stops) *\n", - " (self.extents['urcrnrlat'] - self.extents['llcrnrlat']) / stdv)\n", - " lons = (self.extents['llcrnrlon'] + np.random.randn(num_stops) *\n", - " (self.extents['urcrnrlon'] - self.extents['llcrnrlon']) / stdv)\n", - " # uniformly distributed integer demands.\n", - " demands = np.random.randint(min_demand, max_demand, num_stops)\n", - "\n", - " self.time_horizon = 24 * 60**2 # A 24 hour period.\n", - "\n", - " # The customers demand min_tw to max_tw hour time window for each\n", - " # delivery\n", - " time_windows = np.random.randint(min_tw * 3600, max_tw * 3600,\n", - " num_stops)\n", - " # The last time a delivery window can start\n", - " latest_time = self.time_horizon - time_windows\n", - " start_times = [None for o in time_windows]\n", - " stop_times = [None for o in time_windows]\n", - " # Make random timedeltas, nominally from the start of the day.\n", - " for idx in range(self.number):\n", - " stime = int(np.random.randint(0, latest_time[idx]))\n", - " start_times[idx] = timedelta(seconds=stime)\n", - " stop_times[idx] = (\n", - " start_times[idx] + timedelta(seconds=int(time_windows[idx])))\n", - " # A named tuple for the customer\n", - " Customer = namedtuple(\n", - " 'Customer',\n", - " [\n", - " 'index', # the index of the stop\n", - " 'demand', # the demand for the stop\n", - " 'lat', # the latitude of the stop\n", - " 'lon', # the longitude of the stop\n", - " 'tw_open', # timedelta window open\n", - " 'tw_close'\n", - " ]) # timedelta window cls\n", - "\n", - " self.customers = [\n", - " Customer(idx, dem, lat, lon, tw_open, tw_close)\n", - " for idx, dem, lat, lon, tw_open, tw_close in zip(\n", - " stops, demands, lats, lons, start_times, stop_times)\n", - " ]\n", - "\n", - " # The number of seconds needed to 'unload' 1 unit of goods.\n", - " self.service_time_per_dem = 300 # seconds\n", - "\n", - " def set_manager(self, manager):\n", - " self.manager = manager\n", - "\n", - " def central_start_node(self, invert=False):\n", - " \"\"\"\n", - " Return a random starting node, with probability weighted by distance\n", - " from the centre of the extents, so that a central starting node is\n", - " likely.\n", - "\n", - " Args: invert (Optional bool): When True, a peripheral starting node is\n", - " most likely.\n", - "\n", - " Returns:\n", - " int: a node index.\n", - "\n", - " Examples:\n", - " >>> customers.central_start_node(invert=True)\n", - " 42\n", - " \"\"\"\n", - " num_nodes = len(self.customers)\n", - " dist = np.empty((num_nodes, 1))\n", - " for idx_to in range(num_nodes):\n", - " dist[idx_to] = self._haversine(self.center.lon, self.center.lat,\n", - " self.customers[idx_to].lon,\n", - " self.customers[idx_to].lat)\n", - " furthest = np.max(dist)\n", - "\n", - " if invert:\n", - " prob = dist * 1.0 / sum(dist)\n", - " else:\n", - " prob = (furthest - dist * 1.0) / sum(furthest - dist)\n", - " indexes = np.array([range(num_nodes)])\n", - " start_node = np.random.choice(\n", - " indexes.flatten(), size=1, replace=True, p=prob.flatten())\n", - " return start_node[0]\n", - "\n", - " def make_distance_mat(self, method='haversine'):\n", - " \"\"\"\n", - " Return a distance matrix and make it a member of Customer, using the\n", - " method given in the call. Currently only Haversine (GC distance) is\n", - " implemented, but Manhattan, or using a maps API could be added here.\n", - " Raises an AssertionError for all other methods.\n", - "\n", - " Args: method (Optional[str]): method of distance calculation to use. The\n", - " Haversine formula is the only method implemented.\n", - "\n", - " Returns:\n", - " Numpy array of node to node distances.\n", - "\n", - " Examples:\n", - " >>> dist_mat = customers.make_distance_mat(method='haversine')\n", - " >>> dist_mat = customers.make_distance_mat(method='manhattan')\n", - " AssertionError\n", - " \"\"\"\n", - " self.distmat = np.zeros((self.number, self.number))\n", - " methods = {'haversine': self._haversine}\n", - " assert (method in methods)\n", - " for frm_idx in range(self.number):\n", - " for to_idx in range(self.number):\n", - " if frm_idx != to_idx:\n", - " frm_c = self.customers[frm_idx]\n", - " to_c = self.customers[to_idx]\n", - " self.distmat[frm_idx, to_idx] = self._haversine(\n", - " frm_c.lon, frm_c.lat, to_c.lon, to_c.lat)\n", - " return (self.distmat)\n", - "\n", - " def _haversine(self, lon1, lat1, lon2, lat2):\n", - " \"\"\"\n", - " Calculate the great circle distance between two points\n", - " on the earth specified in decimal degrees of latitude and longitude.\n", - " https://en.wikipedia.org/wiki/Haversine_formula\n", - "\n", - " Args:\n", - " lon1: longitude of pt 1,\n", - " lat1: latitude of pt 1,\n", - " lon2: longitude of pt 2,\n", - " lat2: latitude of pt 2\n", - "\n", - " Returns:\n", - " the distace in km between pt1 and pt2\n", - " \"\"\"\n", - " # convert decimal degrees to radians\n", - " lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])\n", - "\n", - " # haversine formula\n", - " dlon = lon2 - lon1\n", - " dlat = lat2 - lat1\n", - " a = (np.sin(dlat / 2)**2 +\n", - " np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2)\n", - " c = 2 * np.arcsin(np.sqrt(a))\n", - "\n", - " # 6367 km is the radius of the Earth\n", - " km = 6367 * c\n", - " return km\n", - "\n", - " def get_total_demand(self):\n", - " \"\"\"\n", - " Return the total demand of all customers.\n", - " \"\"\"\n", - " return (sum([c.demand for c in self.customers]))\n", - "\n", - " def return_dist_callback(self, **kwargs):\n", - " \"\"\"\n", - " Return a callback function for the distance matrix.\n", - "\n", - " Args: **kwargs: Arbitrary keyword arguments passed on to\n", - " make_distance_mat()\n", - "\n", - " Returns:\n", - " function: dist_return(a,b) A function that takes the 'from' node\n", - " index and the 'to' node index and returns the distance in km.\n", - " \"\"\"\n", - " self.make_distance_mat(**kwargs)\n", - "\n", - " def dist_return(from_index, to_index):\n", - " # Convert from routing variable Index to distance matrix NodeIndex.\n", - " from_node = self.manager.IndexToNode(from_index)\n", - " to_node = self.manager.IndexToNode(to_index)\n", - " return (self.distmat[from_node][to_node])\n", - "\n", - " return dist_return\n", - "\n", - " def return_dem_callback(self):\n", - " \"\"\"\n", - " Return a callback function that gives the demands.\n", - "\n", - " Returns:\n", - " function: dem_return(a) A function that takes the 'from' node\n", - " index and returns the distance in km.\n", - " \"\"\"\n", - "\n", - " def dem_return(from_index):\n", - " # Convert from routing variable Index to distance matrix NodeIndex.\n", - " from_node = self.manager.IndexToNode(from_index)\n", - " return (self.customers[from_node].demand)\n", - "\n", - " return dem_return\n", - "\n", - " def zero_depot_demands(self, depot):\n", - " \"\"\"\n", - " Zero out the demands and time windows of depot. The Depots do not have\n", - " demands or time windows so this function clears them.\n", - "\n", - " Args: depot (int): index of the stop to modify into a depot.\n", - " Examples: >>> customers.zero_depot_demands(5) >>>\n", - " customers.customers[5].demand == 0 True\n", - " \"\"\"\n", - " start_depot = self.customers[depot]\n", - " self.customers[depot] = start_depot._replace(\n", - " demand=0, tw_open=None, tw_close=None)\n", - "\n", - " def make_service_time_call_callback(self):\n", - " \"\"\"\n", - " Return a callback function that provides the time spent servicing the\n", - " customer. Here is it proportional to the demand given by\n", - " self.service_time_per_dem, default 300 seconds per unit demand.\n", - "\n", - " Returns:\n", - " function [dem_return(a, b)]: A function that takes the from/a node\n", - " index and the to/b node index and returns the service time at a\n", - "\n", - " \"\"\"\n", - "\n", - " def service_time_return(a, b):\n", - " return (self.customers[a].demand * self.service_time_per_dem)\n", - "\n", - " return service_time_return\n", - "\n", - " def make_transit_time_callback(self, speed_kmph=10):\n", - " \"\"\"\n", - " Creates a callback function for transit time. Assuming an average\n", - " speed of speed_kmph\n", - " Args:\n", - " speed_kmph: the average speed in km/h\n", - "\n", - " Returns:\n", - " function [transit_time_return(a, b)]: A function that takes the\n", - " from/a node index and the to/b node index and returns the\n", - " transit time from a to b.\n", - " \"\"\"\n", - "\n", - " def transit_time_return(a, b):\n", - " return (self.distmat[a][b] / (speed_kmph * 1.0 / 60**2))\n", - "\n", - " return transit_time_return\n", - "\n", - "\n", - "class Vehicles():\n", - " \"\"\"\n", - " A Class to create and hold vehicle information.\n", - "\n", - " The Vehicles in a CVRPTW problem service the customers and belong to a\n", - " depot. The class Vehicles creates a list of named tuples describing the\n", - " Vehicles. The main characteristics are the vehicle capacity, fixed cost,\n", - " and cost per km. The fixed cost of using a certain type of vehicles can be\n", - " higher or lower than others. If a vehicle is used, i.e. this vehicle serves\n", - " at least one node, then this cost is added to the objective function.\n", - "\n", - " Note:\n", - " If numpy arrays are given for capacity and cost, then they must be of\n", - " the same length, and the number of vehicles are inferred from them.\n", - " If scalars are given, the fleet is homogeneous, and the number of\n", - " vehicles is determined by number.\n", - "\n", - " Args: capacity (scalar or numpy array): The integer capacity of demand\n", - " units. cost (scalar or numpy array): The fixed cost of the vehicle. number\n", - " (Optional [int]): The number of vehicles in a homogeneous fleet.\n", - " \"\"\"\n", - "\n", - " def __init__(self, capacity=100, cost=100, number=None):\n", - "\n", - " Vehicle = namedtuple('Vehicle', ['index', 'capacity', 'cost'])\n", - "\n", - " if number is None:\n", - " self.number = np.size(capacity)\n", - " else:\n", - " self.number = number\n", - " idxs = np.array(range(0, self.number))\n", - "\n", - " if np.isscalar(capacity):\n", - " capacities = capacity * np.ones_like(idxs)\n", - " elif np.size(capacity) != self.number:\n", - " print('capacity is neither scalar, nor the same size as num!')\n", - " else:\n", - " capacities = capacity\n", - "\n", - " if np.isscalar(cost):\n", - " costs = cost * np.ones_like(idxs)\n", - " elif np.size(cost) != self.number:\n", - " print(np.size(cost))\n", - " print('cost is neither scalar, nor the same size as num!')\n", - " else:\n", - " costs = cost\n", - "\n", - " self.vehicles = [\n", - " Vehicle(idx, capacity, cost)\n", - " for idx, capacity, cost in zip(idxs, capacities, costs)\n", - " ]\n", - "\n", - " def get_total_capacity(self):\n", - " return (sum([c.capacity for c in self.vehicles]))\n", - "\n", - " def return_starting_callback(self, customers, sameStartFinish=False):\n", - " # create a different starting and finishing depot for each vehicle\n", - " self.starts = [\n", - " int(customers.central_start_node()) for o in range(self.number)\n", - " ]\n", - " if sameStartFinish:\n", - " self.ends = self.starts\n", - " else:\n", - " self.ends = [\n", - " int(customers.central_start_node(invert=True))\n", - " for o in range(self.number)\n", - " ]\n", - " # the depots will not have demands, so zero them.\n", - " for depot in self.starts:\n", - " customers.zero_depot_demands(depot)\n", - " for depot in self.ends:\n", - " customers.zero_depot_demands(depot)\n", - "\n", - " def start_return(v):\n", - " return (self.starts[v])\n", - "\n", - " return start_return\n", - "\n", - "\n", - "def discrete_cmap(N, base_cmap=None):\n", - " \"\"\"\n", - " Create an N-bin discrete colormap from the specified input map\n", - " \"\"\"\n", - " # Note that if base_cmap is a string or None, you can simply do\n", - " # return plt.cm.get_cmap(base_cmap, N)\n", - " # The following works for string, None, or a colormap instance:\n", - "\n", - " base = plt.cm.get_cmap(base_cmap)\n", - " color_list = base(np.linspace(0, 1, N))\n", - " cmap_name = base.name + str(N)\n", - " return base.from_list(cmap_name, color_list, N)\n", - "\n", - "\n", - "def vehicle_output_string(manager, routing, plan):\n", - " \"\"\"\n", - " Return a string displaying the output of the routing instance and\n", - " assignment (plan).\n", - "\n", - " Args: routing (ortools.constraint_solver.pywrapcp.RoutingModel): routing.\n", - " plan (ortools.constraint_solver.pywrapcp.Assignment): the assignment.\n", - "\n", - " Returns:\n", - " (string) plan_output: describing each vehicle's plan.\n", - "\n", - " (List) dropped: list of dropped orders.\n", - "\n", - " \"\"\"\n", - " dropped = []\n", - " for order in range(routing.Size()):\n", - " if (plan.Value(routing.NextVar(order)) == order):\n", - " dropped.append(str(order))\n", - "\n", - " capacity_dimension = routing.GetDimensionOrDie('Capacity')\n", - " time_dimension = routing.GetDimensionOrDie('Time')\n", - " plan_output = ''\n", - "\n", - " for route_number in range(routing.vehicles()):\n", - " order = routing.Start(route_number)\n", - " plan_output += 'Route {0}:'.format(route_number)\n", - " if routing.IsEnd(plan.Value(routing.NextVar(order))):\n", - " plan_output += ' Empty \\n'\n", - " else:\n", - " while True:\n", - " load_var = capacity_dimension.CumulVar(order)\n", - " time_var = time_dimension.CumulVar(order)\n", - " node = manager.IndexToNode(order)\n", - " plan_output += \\\n", - " ' {node} Load({load}) Time({tmin}, {tmax}) -> '.format(\n", - " node=node,\n", - " load=plan.Value(load_var),\n", - " tmin=str(timedelta(seconds=plan.Min(time_var))),\n", - " tmax=str(timedelta(seconds=plan.Max(time_var))))\n", - "\n", - " if routing.IsEnd(order):\n", - " plan_output += ' EndRoute {0}. \\n'.format(route_number)\n", - " break\n", - " order = plan.Value(routing.NextVar(order))\n", - " plan_output += '\\n'\n", - "\n", - " return (plan_output, dropped)\n", - "\n", - "\n", - "def build_vehicle_route(manager, routing, plan, customers, veh_number):\n", - " \"\"\"\n", - " Build a route for a vehicle by starting at the strat node and\n", - " continuing to the end node.\n", - "\n", - " Args: routing (ortools.constraint_solver.pywrapcp.RoutingModel): routing.\n", - " plan (ortools.constraint_solver.pywrapcp.Assignment): the assignment.\n", - " customers (Customers): the customers instance. veh_number (int): index of\n", - " the vehicle\n", - "\n", - " Returns:\n", - " (List) route: indexes of the customers for vehicle veh_number\n", - " \"\"\"\n", - " veh_used = routing.IsVehicleUsed(plan, veh_number)\n", - " print('Vehicle {0} is used {1}'.format(veh_number, veh_used))\n", - " if veh_used:\n", - " route = []\n", - " node = routing.Start(veh_number) # Get the starting node index\n", - " route.append(customers.customers[manager.IndexToNode(node)])\n", - " while not routing.IsEnd(node):\n", - " route.append(customers.customers[manager.IndexToNode(node)])\n", - " node = plan.Value(routing.NextVar(node))\n", - "\n", - " route.append(customers.customers[manager.IndexToNode(node)])\n", - " return route\n", - " else:\n", - " return None\n", - "\n", - "\n", - "def plot_vehicle_routes(veh_route, ax1, customers, vehicles):\n", - " \"\"\"\n", - " Plot the vehicle routes on matplotlib axis ax1.\n", - "\n", - " Args: veh_route (dict): a dictionary of routes keyed by vehicle idx. ax1\n", - " (matplotlib.axes._subplots.AxesSubplot): Matplotlib axes customers\n", - " (Customers): the customers instance. vehicles (Vehicles): the vehicles\n", - " instance.\n", - " \"\"\"\n", - " veh_used = [v for v in veh_route if veh_route[v] is not None]\n", - "\n", - " cmap = discrete_cmap(vehicles.number + 2, 'nipy_spectral')\n", - "\n", - " for veh_number in veh_used:\n", - "\n", - " lats, lons = zip(*[(c.lat, c.lon) for c in veh_route[veh_number]])\n", - " lats = np.array(lats)\n", - " lons = np.array(lons)\n", - " s_dep = customers.customers[vehicles.starts[veh_number]]\n", - " s_fin = customers.customers[vehicles.ends[veh_number]]\n", - " ax1.annotate(\n", - " 'v({veh}) S @ {node}'.format(\n", - " veh=veh_number, node=vehicles.starts[veh_number]),\n", - " xy=(s_dep.lon, s_dep.lat),\n", - " xytext=(10, 10),\n", - " xycoords='data',\n", - " textcoords='offset points',\n", - " arrowprops=dict(\n", - " arrowstyle='->',\n", - " connectionstyle='angle3,angleA=90,angleB=0',\n", - " shrinkA=0.05),\n", - " )\n", - " ax1.annotate(\n", - " 'v({veh}) F @ {node}'.format(\n", - " veh=veh_number, node=vehicles.ends[veh_number]),\n", - " xy=(s_fin.lon, s_fin.lat),\n", - " xytext=(10, -20),\n", - " xycoords='data',\n", - " textcoords='offset points',\n", - " arrowprops=dict(\n", - " arrowstyle='->',\n", - " connectionstyle='angle3,angleA=-90,angleB=0',\n", - " shrinkA=0.05),\n", - " )\n", - " ax1.plot(lons, lats, 'o', mfc=cmap(veh_number + 1))\n", - " ax1.quiver(\n", - " lons[:-1],\n", - " lats[:-1],\n", - " lons[1:] - lons[:-1],\n", - " lats[1:] - lats[:-1],\n", - " scale_units='xy',\n", - " angles='xy',\n", - " scale=1,\n", - " color=cmap(veh_number + 1))\n", - "\n", - "\n", - "def main():\n", - " # Create a set of customer, (and depot) stops.\n", - " customers = Customers(\n", - " num_stops=50,\n", - " min_demand=1,\n", - " max_demand=15,\n", - " box_size=40,\n", - " min_tw=3,\n", - " max_tw=6)\n", - "\n", - " # Create a list of inhomgenious vehicle capacities as integer units.\n", - " capacity = [50, 75, 100, 125, 150, 175, 200, 250]\n", - "\n", - " # Create a list of inhomogeneous fixed vehicle costs.\n", - " cost = [int(100 + 2 * np.sqrt(c)) for c in capacity]\n", - "\n", - " # Create a set of vehicles, the number set by the length of capacity.\n", - " vehicles = Vehicles(capacity=capacity, cost=cost)\n", - "\n", - " # check to see that the problem is feasible, if we don't have enough\n", - " # vehicles to cover the demand, there is no point in going further.\n", - " assert (customers.get_total_demand() < vehicles.get_total_capacity())\n", - "\n", - " # Set the starting nodes, and create a callback fn for the starting node.\n", - " start_fn = vehicles.return_starting_callback(\n", - " customers, sameStartFinish=False)\n", - "\n", - " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", - " customers.number, # int number\n", - " vehicles.number, # int number\n", - " vehicles.starts, # List of int start depot\n", - " vehicles.ends) # List of int end depot\n", - "\n", - " customers.set_manager(manager)\n", - "\n", - " # Set model parameters\n", - " model_parameters = pywrapcp.DefaultRoutingModelParameters()\n", - "\n", - " # The solver parameters can be accessed from the model parameters. For example :\n", - " # model_parameters.solver_parameters.CopyFrom(\n", - " # pywrapcp.Solver.DefaultSolverParameters())\n", - " # model_parameters.solver_parameters.trace_propagation = True\n", - "\n", - " # Make the routing model instance.\n", - " routing = pywrapcp.RoutingModel(manager, model_parameters)\n", - "\n", - " parameters = pywrapcp.DefaultRoutingSearchParameters()\n", - " # Setting first solution heuristic (cheapest addition).\n", - " parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", - " # Routing: forbids use of TSPOpt neighborhood, (this is the default behaviour)\n", - " parameters.local_search_operators.use_tsp_opt = pywrapcp.BOOL_FALSE\n", - " # Disabling Large Neighborhood Search, (this is the default behaviour)\n", - " parameters.local_search_operators.use_path_lns = pywrapcp.BOOL_FALSE\n", - " parameters.local_search_operators.use_inactive_lns = pywrapcp.BOOL_FALSE\n", - "\n", - " parameters.time_limit.seconds = 10\n", - " parameters.use_full_propagation = True\n", - " #parameters.log_search = True\n", - "\n", - " # Create callback fns for distances, demands, service and transit-times.\n", - " dist_fn = customers.return_dist_callback()\n", - " dist_fn_index = routing.RegisterTransitCallback(dist_fn)\n", - "\n", - " dem_fn = customers.return_dem_callback()\n", - " dem_fn_index = routing.RegisterUnaryTransitCallback(dem_fn)\n", - "\n", - " # Create and register a transit callback.\n", - " serv_time_fn = customers.make_service_time_call_callback()\n", - " transit_time_fn = customers.make_transit_time_callback()\n", - " def tot_time_fn(from_index, to_index):\n", - " \"\"\"\n", - " The time function we want is both transit time and service time.\n", - " \"\"\"\n", - " # Convert from routing variable Index to distance matrix NodeIndex.\n", - " from_node = manager.IndexToNode(from_index)\n", - " to_node = manager.IndexToNode(to_index)\n", - " return serv_time_fn(from_node, to_node) + transit_time_fn(from_node, to_node)\n", - "\n", - " tot_time_fn_index = routing.RegisterTransitCallback(tot_time_fn)\n", - "\n", - " # Set the cost function (distance callback) for each arc, homogeneous for\n", - " # all vehicles.\n", - " routing.SetArcCostEvaluatorOfAllVehicles(dist_fn_index)\n", - "\n", - " # Set vehicle costs for each vehicle, not homogeneous.\n", - " for veh in vehicles.vehicles:\n", - " routing.SetFixedCostOfVehicle(veh.cost, int(veh.index))\n", - "\n", - " # Add a dimension for vehicle capacities\n", - " null_capacity_slack = 0\n", - " routing.AddDimensionWithVehicleCapacity(\n", - " dem_fn_index, # demand callback\n", - " null_capacity_slack,\n", - " capacity, # capacity array\n", - " True,\n", - " 'Capacity')\n", - " # Add a dimension for time and a limit on the total time_horizon\n", - " routing.AddDimension(\n", - " tot_time_fn_index, # total time function callback\n", - " customers.time_horizon,\n", - " customers.time_horizon,\n", - " True,\n", - " 'Time')\n", - "\n", - " time_dimension = routing.GetDimensionOrDie('Time')\n", - " for cust in customers.customers:\n", - " if cust.tw_open is not None:\n", - " time_dimension.CumulVar(manager.NodeToIndex(cust.index)).SetRange(\n", - " cust.tw_open.seconds, cust.tw_close.seconds)\n", - " \"\"\"\n", - " To allow the dropping of orders, we add disjunctions to all the customer\n", - " nodes. Each disjunction is a list of 1 index, which allows that customer to\n", - " be active or not, with a penalty if not. The penalty should be larger\n", - " than the cost of servicing that customer, or it will always be dropped!\n", - " \"\"\"\n", - " # To add disjunctions just to the customers, make a list of non-depots.\n", - " non_depot = set(range(customers.number))\n", - " non_depot.difference_update(vehicles.starts)\n", - " non_depot.difference_update(vehicles.ends)\n", - " penalty = 400000 # The cost for dropping a node from the plan.\n", - " nodes = [routing.AddDisjunction([manager.NodeToIndex(c)], penalty) for c in non_depot]\n", - "\n", - " # This is how you would implement partial routes if you already knew part\n", - " # of a feasible solution for example:\n", - " # partial = np.random.choice(list(non_depot), size=(4,5), replace=False)\n", - "\n", - " # routing.CloseModel()\n", - " # partial_list = [partial[0,:].tolist(),\n", - " # partial[1,:].tolist(),\n", - " # partial[2,:].tolist(),\n", - " # partial[3,:].tolist(),\n", - " # [],[],[],[]]\n", - " # print(routing.ApplyLocksToAllVehicles(partial_list, False))\n", - "\n", - " # Solve the problem !\n", - " assignment = routing.SolveWithParameters(parameters)\n", - "\n", - " # The rest is all optional for saving, printing or plotting the solution.\n", - " if assignment:\n", - " ## save the assignment, (Google Protobuf format)\n", - " #save_file_base = os.path.realpath(__file__).split('.')[0]\n", - " #if routing.WriteAssignment(save_file_base + '_assignment.ass'):\n", - " # print('succesfully wrote assignment to file ' + save_file_base +\n", - " # '_assignment.ass')\n", - "\n", - " print('The Objective Value is {0}'.format(assignment.ObjectiveValue()))\n", - "\n", - " plan_output, dropped = vehicle_output_string(manager, routing, assignment)\n", - " print(plan_output)\n", - " print('dropped nodes: ' + ', '.join(dropped))\n", - "\n", - " # you could print debug information like this:\n", - " # print(routing.DebugOutputAssignment(assignment, 'Capacity'))\n", - "\n", - " vehicle_routes = {}\n", - " for veh in range(vehicles.number):\n", - " vehicle_routes[veh] = build_vehicle_route(manager, routing, assignment,\n", - " customers, veh)\n", - "\n", - " # Plotting of the routes in matplotlib.\n", - " fig = plt.figure()\n", - " ax = fig.add_subplot(111)\n", - " # Plot all the nodes as black dots.\n", - " clon, clat = zip(*[(c.lon, c.lat) for c in customers.customers])\n", - " ax.plot(clon, clat, 'k.')\n", - " # plot the routes as arrows\n", - " plot_vehicle_routes(vehicle_routes, ax, customers, vehicles)\n", - " plt.show()\n", - "\n", - " else:\n", - " print('No assignment')\n", - "\n", - "\n", - "main()\n", - "\n" - ] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/notebook/examples/gate_scheduling_sat.ipynb b/examples/notebook/examples/gate_scheduling_sat.ipynb index 134683752d..97121aaf8e 100644 --- a/examples/notebook/examples/gate_scheduling_sat.ipynb +++ b/examples/notebook/examples/gate_scheduling_sat.ipynb @@ -92,8 +92,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.sat.colab import visualization\n", "from ortools.sat.python import cp_model\n", + "from ortools.sat.colab import visualization\n", "\n", "\n", "def main(_) -> None:\n", diff --git a/examples/notebook/examples/golomb8.ipynb b/examples/notebook/examples/golomb8.ipynb index bf943ae0de..1c4d8d66b3 100644 --- a/examples/notebook/examples/golomb8.ipynb +++ b/examples/notebook/examples/golomb8.ipynb @@ -137,7 +137,7 @@ " branches = collector.Branches(i)\n", " failures = collector.Failures(i)\n", " print(\n", - " (\"Solution #%i: value = %i, failures = %i, branches = %i,\" \"time = %i ms\")\n", + " \"Solution #%i: value = %i, failures = %i, branches = %i,time = %i ms\"\n", " % (i, obj_value, failures, branches, time)\n", " )\n", " time = solver.WallTime()\n", diff --git a/examples/notebook/examples/golomb_sat.ipynb b/examples/notebook/examples/golomb_sat.ipynb index 410ab75878..47476ffe2b 100644 --- a/examples/notebook/examples/golomb_sat.ipynb +++ b/examples/notebook/examples/golomb_sat.ipynb @@ -93,8 +93,8 @@ "outputs": [], "source": [ "from typing import Sequence\n", + "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_ORDER = flags.define_integer(\"order\", 8, \"Order of the ruler.\")\n", @@ -137,7 +137,7 @@ " # Solve the model.\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " solution_printer = cp_model.ObjectiveSolutionPrinter()\n", " print(f\"Golomb ruler(order={order})\")\n", " status = solver.solve(model, solution_printer)\n", diff --git a/examples/notebook/examples/horse_jumping_show.ipynb b/examples/notebook/examples/horse_jumping_show.ipynb new file mode 100644 index 0000000000..ac5ed19094 --- /dev/null +++ b/examples/notebook/examples/horse_jumping_show.ipynb @@ -0,0 +1,376 @@ +{ + "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": [ + "# horse_jumping_show" + ] + }, + { + "cell_type": "markdown", + "id": "link", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "
\n", + "Run in Google Colab\n", + "\n", + "View source on GitHub\n", + "
" + ] + }, + { + "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", + "Horse Jumping Show.\n", + "\n", + "A major three-day horse jumping competition is scheduled next winter in Geneva.\n", + "The show features riders and horses from all over the world, competing in\n", + "several different competitions throughout the show. Six months before the show,\n", + "riders submit the entries (i.e., rider name, horse, competition) to the\n", + "organizers. Riders can submit multiple entries, for example, to compete in the\n", + "same competition with multiple horses, or to compete in several competitions.\n", + "\n", + "There are additional space limitations. For example, the venue has 100 stalls,\n", + "4 arenas (where competitions can be scheduled), and 6 paddocks (where riders\n", + "warm up before their turn). It is also ideal that paddocks are not overloaded by\n", + "riders from multiple competitions.\n", + "\n", + "The organizer's goal is find a schedule in which competitions don't overlap, and\n", + "the times at which they happen are scattered throughout the day (and hopefully\n", + "not that early in the morning). The starting times of the competitions should be\n", + "at the hour or 30 minutes past the hour (e.g. 9:30, 10:00, 10:30, etc.).\n", + "Competitions can only be scheduled while there is daylight, except for\n", + "competitions scheduled in the Main Stage arena, which is covered and has proper\n", + "lighting. Also, beginner competitions (1.10m or less) are scheduled on the first\n", + "day, and advanced competitions (1.50m or more) are scheduled on the last day.\n", + "\n", + "The information for next winter's show is as follows:\n", + "Available stalls: 100\n", + "Number of riders: 100\n", + "Number of horses: 130\n", + "Number of requested Entries: 200\n", + "Number of competitions: 15\n", + "\n", + "Venue:\n", + "- Main Stage arena: Covered (9AM-11PM)\n", + "- Highlands arena: Daylight Only (9AM-5PM)\n", + "- Sawdust arena: Daylight Only (9AM-5PM)\n", + "- Paddock1 has capacity for 10 riders and serves Main Stage\n", + "- Paddock2 has capacity for 6 riders and serves Main Stage\n", + "- Paddock3 has capacity for 8 riders and serves Main Stage, Highlands\n", + "- Paddock4 has capacity for 8 riders and serves Highlands, Sawdust\n", + "- Paddock5 has capacity for 9 riders and serves Sawdust\n", + "- Paddock6 has capacity for 7 riders and serves Sawdust\n", + "\n", + "competitions:\n", + "- C_5_1.10m_Year_Olds 1.10m - 60 minutes\n", + "- C_6_1.25m_Year_Olds 1.25m - 90 minutes\n", + "- C_7_1.35m_Year_Olds 1.35m - 120 minutes\n", + "- C_0.8m_Jumpers 0.80m - 240 minutes\n", + "- C_1.0m_Jumpers 1.00m - 180 minutes\n", + "- C_1.10m_Jumpers 1.10m - 180 minutes\n", + "- C_1.20m_Jumpers 1.20m - 120 minutes\n", + "- C_1.30m_Jumpers 1.30m - 120 minutes\n", + "- C_1.40m_Jumpers 1.40m - 120 minutes\n", + "- C_1.20m_Derby 1.20m - 180 minutes\n", + "- C_1.35m_Derby 1.35m - 180 minutes\n", + "- C_1.45m_Derby 1.45m - 180 minutes\n", + "- C_1.40m_Open 1.40m - 120 minutes\n", + "- C_1.50m_Open 1.50m - 180 minutes\n", + "- C_1.60m_Grand_Prix 1.60m - 240 minutes\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "code", + "metadata": {}, + "outputs": [], + "source": [ + "import dataclasses\n", + "import numpy as np\n", + "from ortools.sat.python import cp_model\n", + "\n", + "\n", + "@dataclasses.dataclass(frozen=True)\n", + "class Arena:\n", + " \"\"\"Data for an arena.\"\"\"\n", + "\n", + " id: str\n", + " hours: str\n", + "\n", + "\n", + "@dataclasses.dataclass(frozen=True)\n", + "class Competition:\n", + " \"\"\"Data for a competition.\"\"\"\n", + "\n", + " id: str\n", + " height: float\n", + " duration: int\n", + "\n", + "\n", + "@dataclasses.dataclass(frozen=True)\n", + "class HorseJumpingShowData:\n", + " \"\"\"Horse Jumping Show Data.\"\"\"\n", + "\n", + " num_days: int\n", + " competitions: list[Competition]\n", + " arenas: list[Arena]\n", + "\n", + "\n", + "@dataclasses.dataclass(frozen=True)\n", + "class ScheduledCompetition:\n", + " \"\"\"Horse Jumping Show Schedule.\"\"\"\n", + "\n", + " completion: str\n", + " day: int\n", + " arena: str\n", + " start_time: str\n", + " end_time: str\n", + "\n", + "\n", + "def generate_horse_jumping_show_data() -> HorseJumpingShowData:\n", + " \"\"\"Generates the horse jumping show data.\"\"\"\n", + " arenas = [\n", + " Arena(id=\"Main Stage\", hours=\"9AM-9PM\"),\n", + " Arena(id=\"Highlands\", hours=\"9AM-5PM\"),\n", + " Arena(id=\"Sawdust\", hours=\"9AM-5PM\"),\n", + " ]\n", + " competitions = [\n", + " Competition(id=\"C_5_1.10m_Year_Olds\", height=1.1, duration=60),\n", + " Competition(id=\"C_6_1.25m_Year_Olds\", height=1.25, duration=90),\n", + " Competition(id=\"C_7_1.35m_Year_Olds\", height=1.35, duration=120),\n", + " Competition(id=\"C_0.8m_Jumpers\", height=0.8, duration=240),\n", + " Competition(id=\"C_1.0m_Jumpers\", height=1.0, duration=180),\n", + " Competition(id=\"C_1.10m_Jumpers\", height=1.10, duration=180),\n", + " Competition(id=\"C_1.20m_Jumpers\", height=1.20, duration=120),\n", + " Competition(id=\"C_1.30m_Jumpers\", height=1.30, duration=120),\n", + " Competition(id=\"C_1.40m_Jumpers\", height=1.40, duration=120),\n", + " Competition(id=\"C_1.20m_Derby\", height=1.20, duration=180),\n", + " Competition(id=\"C_1.35m_Derby\", height=1.35, duration=180),\n", + " Competition(id=\"C_1.45m_Derby\", height=1.45, duration=180),\n", + " Competition(id=\"C_1.40m_Open\", height=1.40, duration=120),\n", + " Competition(id=\"C_1.50m_Open\", height=1.50, duration=180),\n", + " Competition(id=\"C_1.60m_Grand_Prix\", height=1.60, duration=240),\n", + " ]\n", + " return HorseJumpingShowData(num_days=3, competitions=competitions, arenas=arenas)\n", + "\n", + "\n", + "def solve() -> list[ScheduledCompetition]:\n", + " \"\"\"Solves the horse jumping show problem.\"\"\"\n", + " data = generate_horse_jumping_show_data()\n", + " num_days = data.num_days\n", + " competitions = data.competitions\n", + " arenas = data.arenas\n", + " day_index = list(range(num_days))\n", + "\n", + " # Time parser.\n", + " def parse_time(t_str):\n", + " hour = int(t_str[:-2])\n", + " if \"PM\" in t_str and hour != 12:\n", + " hour += 12\n", + " if \"AM\" in t_str and hour == 12:\n", + " hour = 0\n", + " return hour * 60\n", + "\n", + " # Schedule time intervals for each arena.\n", + " schedule_interval_by_arena = {}\n", + " for arena in arenas:\n", + " start_h_str, end_h_str = arena.hours.split(\"-\")\n", + " start_time = parse_time(start_h_str)\n", + " end_time = parse_time(end_h_str)\n", + " schedule_interval_by_arena[arena.id] = (start_time, end_time)\n", + "\n", + " # Map time to 30-minute intervals and back.\n", + " time_slot_size = 30\n", + "\n", + " def time_to_slot(time_in_minutes: int):\n", + " return time_in_minutes // time_slot_size\n", + "\n", + " def slot_to_time(slot_index: int):\n", + " return slot_index * time_slot_size\n", + "\n", + " # --- Model Creation ---\n", + " model = cp_model.CpModel()\n", + "\n", + " # --- Variables ---\n", + " # Competition scheduling variables per arena and day.\n", + " competition_assignments = np.empty(\n", + " (len(competitions), len(arenas), num_days), dtype=object\n", + " )\n", + " for c, comp in enumerate(competitions):\n", + " for a, arena in enumerate(arenas):\n", + " for d in day_index:\n", + " competition_assignments[c, a, d] = model.new_bool_var(\n", + " f\"competition_scheduled_{comp.id}_{arena.id}_{d}\"\n", + " )\n", + " # Time intervals and start times for each competition. We model time steps\n", + " # 0,1,2,... to represent the start times in 30 minutes intervals, as opposed\n", + " # to represent the start times in minutes.\n", + " competition_start_times = np.empty(\n", + " (len(competitions), len(arenas), num_days), dtype=object\n", + " )\n", + " competition_intervals = np.empty(\n", + " (len(competitions), len(arenas), num_days), dtype=object\n", + " )\n", + " for c, comp in enumerate(competitions):\n", + " for a, arena in enumerate(arenas):\n", + " earliest_start_time, latest_end_time = schedule_interval_by_arena[arena.id]\n", + " latest_start_time = latest_end_time - comp.duration\n", + " for d in day_index:\n", + " competition_start_times[c, a, d] = model.new_int_var(\n", + " time_to_slot(earliest_start_time),\n", + " time_to_slot(latest_start_time),\n", + " f\"start_time_{comp.id}_{arena.id}_{d}\",\n", + " )\n", + " competition_intervals[c, a, d] = (\n", + " model.new_optional_fixed_size_interval_var(\n", + " competition_start_times[c, a, d],\n", + " time_to_slot(comp.duration),\n", + " competition_assignments[c, a, d],\n", + " f\"task_{comp.id}_{arena.id}_{d}\",\n", + " )\n", + " )\n", + "\n", + " # --- Constraints ---\n", + " # Every competition must be scheduled, enforcing that beginner competitions\n", + " # are on day 1, and advanced competitions are on day 3.\n", + " for c, comp in enumerate(competitions):\n", + " model.add(np.sum(competition_assignments[c, :, :]) == 1)\n", + " # Beginner competitions are on the first day.\n", + " if comp.height <= 1.10:\n", + " beginners_day = 0\n", + " model.add(np.sum(competition_assignments[c, :, beginners_day]) == 1)\n", + " # Advanced competitions are on the last day.\n", + " if comp.height >= 1.50:\n", + " advanced_day = num_days - 1\n", + " model.add(np.sum(competition_assignments[c, :, advanced_day]) == 1)\n", + "\n", + " # Competitions scheduled on the same arena and on the same day can't overlap.\n", + " for a, _ in enumerate(arenas):\n", + " for day in range(num_days):\n", + " model.add_no_overlap(competition_intervals[:, a, day])\n", + "\n", + " # Start times should be scattered across the day.\n", + " for a, _ in enumerate(arenas):\n", + " for day in day_index:\n", + " model.add_all_different(competition_start_times[:, a, day])\n", + "\n", + " # --- Objective ---\n", + " model.maximize(np.sum(competition_start_times))\n", + "\n", + " # --- Solve ---\n", + " solver = cp_model.CpSolver()\n", + " solver.parameters.max_time_in_seconds = 30.0\n", + " solver.parameters.log_search_progress = True\n", + " solver.parameters.num_workers = 16\n", + " status = solver.solve(model)\n", + "\n", + " # --- Print Solution ---\n", + " if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:\n", + " schedule = []\n", + " for day in range(num_days):\n", + " for c, comp in enumerate(competitions):\n", + " for a, arena in enumerate(arenas):\n", + " if solver.value(competition_assignments[c, a, day]):\n", + " start_time_minutes = slot_to_time(\n", + " solver.value(competition_start_times[c, a, day])\n", + " )\n", + " start_h, start_m = divmod(start_time_minutes, 60)\n", + " end_h, end_m = divmod(start_time_minutes + comp.duration, 60)\n", + " schedule.append(\n", + " ScheduledCompetition(\n", + " completion=comp.id,\n", + " day=day + 1,\n", + " arena=arena.id,\n", + " start_time=f\"{start_h:02d}:{start_m:02d}\",\n", + " end_time=f\"{end_h:02d}:{end_m:02d}\",\n", + " )\n", + " )\n", + " # Sort and print schedule for readability.\n", + " schedule.sort(key=lambda x: (x.day, x.start_time))\n", + " print(\"Schedule:\")\n", + " for item in schedule:\n", + " print(\n", + " f\"Day {item.day}: {item.completion} in {item.arena} from\"\n", + " f\" {item.start_time} to {item.end_time}.\"\n", + " )\n", + " return schedule\n", + " elif status == cp_model.INFEASIBLE:\n", + " print(\"Problem is infeasible.\")\n", + " else:\n", + " print(\"No solution found.\")\n", + " # Return an empty schedule if no solution is found.\n", + " return []\n", + "\n", + "\n", + "def main(_):\n", + " solve()\n", + "\n", + "\n", + "main()\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebook/examples/knapsack_2d_sat.ipynb b/examples/notebook/examples/knapsack_2d_sat.ipynb index 297bc81611..5cb5158596 100644 --- a/examples/notebook/examples/knapsack_2d_sat.ipynb +++ b/examples/notebook/examples/knapsack_2d_sat.ipynb @@ -93,8 +93,6 @@ "import numpy as np\n", "import pandas as pd\n", "\n", - "from google.protobuf import text_format\n", - "\n", "from ortools.sat.python import cp_model\n", "\n", "\n", @@ -227,7 +225,7 @@ " # Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", "\n", " status = solver.solve(model)\n", "\n", @@ -329,7 +327,7 @@ " # solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", "\n", " status = solver.solve(model)\n", "\n", @@ -450,7 +448,7 @@ " # solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", "\n", " status = solver.solve(model)\n", "\n", diff --git a/examples/notebook/examples/line_balancing_sat.ipynb b/examples/notebook/examples/line_balancing_sat.ipynb index a95f9c2443..f3d2220ec8 100644 --- a/examples/notebook/examples/line_balancing_sat.ipynb +++ b/examples/notebook/examples/line_balancing_sat.ipynb @@ -101,8 +101,6 @@ "from typing import Dict, Sequence\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", - "\n", "from ortools.sat.python import cp_model\n", "\n", "_INPUT = flags.define_string(\"input\", \"\", \"Input file to parse and solve.\")\n", @@ -340,7 +338,7 @@ " # solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " solver.parameters.log_search_progress = True\n", " solver.solve(model)\n", "\n", @@ -407,7 +405,7 @@ " # solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " solver.parameters.log_search_progress = True\n", " solver.solve(model)\n", "\n", diff --git a/examples/notebook/examples/linear_assignment_api.ipynb b/examples/notebook/examples/linear_assignment_api.ipynb index 4b02415e9d..fdfbd4b094 100644 --- a/examples/notebook/examples/linear_assignment_api.ipynb +++ b/examples/notebook/examples/linear_assignment_api.ipynb @@ -75,9 +75,9 @@ "\n", "Test linear sum assignment on a 4x4 matrix.\n", "\n", - " Example taken from:\n", - " http://www.ee.oulu.fi/~mpa/matreng/eem1_2-1.htm with kCost[0][1]\n", - " modified so the optimum solution is unique.\n", + "Example taken from:\n", + "http://www.ee.oulu.fi/~mpa/matreng/eem1_2-1.htm with kCost[0][1]\n", + "modified so the optimum solution is unique.\n", "\n" ] }, @@ -96,7 +96,12 @@ " \"\"\"Test linear sum assignment on a 4x4 matrix.\"\"\"\n", " num_sources = 4\n", " num_targets = 4\n", - " cost = [[90, 76, 75, 80], [35, 85, 55, 65], [125, 95, 90, 105], [45, 110, 95, 115]]\n", + " cost = [\n", + " [90, 76, 75, 80],\n", + " [35, 85, 55, 65],\n", + " [125, 95, 90, 105],\n", + " [45, 110, 95, 115],\n", + " ]\n", " expected_cost = cost[0][3] + cost[1][2] + cost[2][1] + cost[3][0]\n", "\n", " assignment = linear_sum_assignment.SimpleLinearSumAssignment()\n", diff --git a/examples/notebook/examples/maximize_combinations_sat.ipynb b/examples/notebook/examples/maximize_combinations_sat.ipynb index 4065103954..ff177562ff 100644 --- a/examples/notebook/examples/maximize_combinations_sat.ipynb +++ b/examples/notebook/examples/maximize_combinations_sat.ipynb @@ -84,6 +84,7 @@ "outputs": [], "source": [ "from typing import Sequence\n", + "\n", "from ortools.sat.python import cp_model\n", "\n", "\n", diff --git a/examples/notebook/examples/maze_escape_sat.ipynb b/examples/notebook/examples/maze_escape_sat.ipynb index a1be0ae6b3..47238ef652 100644 --- a/examples/notebook/examples/maze_escape_sat.ipynb +++ b/examples/notebook/examples/maze_escape_sat.ipynb @@ -79,7 +79,8 @@ "visit all boxes in order, and walk on each block in a 4x4x4 map exactly once.\n", "\n", "Admissible moves are one step in one of the 6 directions:\n", - " x+, x-, y+, y-, z+(up), z-(down)\n" + " x+, x-, y+, y-, z+(up), z-(down)\n", + "\n" ] }, { @@ -92,7 +93,6 @@ "from typing import Dict, Sequence, Tuple\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_OUTPUT_PROTO = flags.define_string(\n", @@ -207,7 +207,7 @@ " # Solve model.\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " solver.parameters.log_search_progress = True\n", " result = solver.solve(model)\n", "\n", diff --git a/examples/notebook/examples/memory_layout_and_infeasibility_sat.ipynb b/examples/notebook/examples/memory_layout_and_infeasibility_sat.ipynb index 71b8e27a30..cf369a5840 100644 --- a/examples/notebook/examples/memory_layout_and_infeasibility_sat.ipynb +++ b/examples/notebook/examples/memory_layout_and_infeasibility_sat.ipynb @@ -87,7 +87,6 @@ "from typing import List\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "\n", @@ -139,7 +138,7 @@ "\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " status = solver.solve(model)\n", " print(solver.response_stats())\n", "\n", @@ -225,7 +224,7 @@ "\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " status = solver.solve(model)\n", " print(solver.response_stats())\n", " if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:\n", diff --git a/examples/notebook/examples/music_playlist_sat.ipynb b/examples/notebook/examples/music_playlist_sat.ipynb new file mode 100644 index 0000000000..5b27bbb50b --- /dev/null +++ b/examples/notebook/examples/music_playlist_sat.ipynb @@ -0,0 +1,388 @@ +{ + "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": [ + "# music_playlist_sat" + ] + }, + { + "cell_type": "markdown", + "id": "link", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "
\n", + "Run in Google Colab\n", + "\n", + "View source on GitHub\n", + "
" + ] + }, + { + "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", + "Create a balanced music playlist.\n", + "\n", + "Create a music playlist by selecting tunes from a list of tunes.\n", + "\n", + "Each tune has a duration in seconds and a music genre (e.g. Rock, Disco, Techno,\n", + "etc).\n", + "\n", + "The total playlist duration must be as close as possible to a given total\n", + "duration. Each tune can appear at most once in the playlist. All existing\n", + "genres must appear at least once in the playlist. Two consecutive tunes must be\n", + "of different genres. There is a positive cost to go from a genre to another, and\n", + "the playlist must minimize this cost overall.\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 Solve():\n", + " \"\"\"Solves the music playlist problem.\"\"\"\n", + "\n", + " # --------------------\n", + " # 1. Data\n", + " # --------------------\n", + " tunes = [\n", + " (\"Song 01\", 202, \"Pop\"),\n", + " (\"Song 02\", 233, \"Techno\"),\n", + " (\"Song 03\", 108, \"Disco\"),\n", + " (\"Song 04\", 281, \"Disco\"),\n", + " (\"Song 05\", 129, \"Techno\"),\n", + " (\"Song 06\", 122, \"Techno\"),\n", + " (\"Song 07\", 244, \"Pop\"),\n", + " (\"Song 08\", 178, \"Techno\"),\n", + " (\"Song 09\", 213, \"Techno\"),\n", + " (\"Song 10\", 124, \"Rock\"),\n", + " (\"Song 11\", 120, \"Disco\"),\n", + " (\"Song 12\", 196, \"Rock\"),\n", + " (\"Song 13\", 249, \"Disco\"),\n", + " (\"Song 14\", 294, \"Disco\"),\n", + " (\"Song 15\", 103, \"Techno\"),\n", + " (\"Song 16\", 179, \"Disco\"),\n", + " (\"Song 17\", 146, \"Disco\"),\n", + " (\"Song 18\", 126, \"Techno\"),\n", + " (\"Song 19\", 100, \"Pop\"),\n", + " (\"Song 20\", 122, \"Disco\"),\n", + " (\"Song 21\", 190, \"Disco\"),\n", + " (\"Song 22\", 181, \"Techno\"),\n", + " (\"Song 23\", 273, \"Pop\"),\n", + " (\"Song 24\", 121, \"Disco\"),\n", + " (\"Song 25\", 159, \"Pop\"),\n", + " (\"Song 26\", 234, \"Rock\"),\n", + " (\"Song 27\", 169, \"Rock\"),\n", + " (\"Song 28\", 151, \"Rock\"),\n", + " (\"Song 29\", 142, \"Techno\"),\n", + " (\"Song 30\", 245, \"Pop\"),\n", + " (\"Song 31\", 281, \"Techno\"),\n", + " (\"Song 32\", 154, \"Rock\"),\n", + " (\"Song 33\", 148, \"Disco\"),\n", + " (\"Song 34\", 120, \"Pop\"),\n", + " (\"Song 35\", 163, \"Disco\"),\n", + " (\"Song 36\", 158, \"Pop\"),\n", + " (\"Song 37\", 235, \"Rock\"),\n", + " (\"Song 38\", 106, \"Techno\"),\n", + " (\"Song 39\", 117, \"Disco\"),\n", + " (\"Song 40\", 110, \"Pop\"),\n", + " (\"Song 41\", 144, \"Rock\"),\n", + " (\"Song 42\", 156, \"Disco\"),\n", + " (\"Song 43\", 204, \"Rock\"),\n", + " (\"Song 44\", 108, \"Pop\"),\n", + " (\"Song 45\", 255, \"Pop\"),\n", + " (\"Song 46\", 165, \"Rock\"),\n", + " (\"Song 47\", 290, \"Disco\"),\n", + " (\"Song 48\", 242, \"Pop\"),\n", + " (\"Song 49\", 272, \"Rock\"),\n", + " (\"Song 50\", 212, \"Pop\"),\n", + " ]\n", + "\n", + " # Genre transition costs. A higher cost means a less desirable transition.\n", + " genre_transition_costs = {\n", + " \"Rock\": {\"Pop\": 3, \"Disco\": 5, \"Techno\": 7},\n", + " \"Pop\": {\"Rock\": 3, \"Disco\": 6, \"Techno\": 8},\n", + " \"Disco\": {\"Rock\": 5, \"Pop\": 6, \"Techno\": 9},\n", + " \"Techno\": {\"Rock\": 7, \"Pop\": 8, \"Disco\": 9},\n", + " }\n", + "\n", + " num_tunes = len(tunes)\n", + " all_tunes = range(num_tunes)\n", + "\n", + " # Playlist target duration in seconds.\n", + " target_duration = 60 * 60 # 1 hour\n", + "\n", + " # We use a circuit constraint to model the playlist. In the circuit constraint\n", + " # graph, each node is a tune, and each arc represents a pair of consecutive\n", + " # tunes in the playlist. We introduce a dummy node to represent the start and\n", + " # the end of the playlist.\n", + " #\n", + " # The constraint that two consecutive tunes must be of different genres is\n", + " # encoded by not creating an arc between two tunes that are of the same genre.\n", + " # This is crucial in the modelisation of this problem: it reduces the number\n", + " # of variables in the model, and it avoids additional constraints to ensure\n", + " # two consecutive tunes are of different genres.\n", + "\n", + " # Dummy node representing the start and end of the playlist.\n", + " dummy_node = num_tunes\n", + "\n", + " # `possible_successors[i]` contains the list of nodes that can be reached\n", + " # after node `i`.\n", + " possible_successors = {}\n", + " possible_successors[dummy_node] = [dummy_node]\n", + " for i in all_tunes:\n", + " # Any node can be the first tune in the playlist.\n", + " possible_successors[dummy_node].append(i)\n", + " # Any node can be the last tune in the playlist.\n", + " possible_successors[i] = [dummy_node]\n", + " genre_i = tunes[i][2]\n", + " for j in all_tunes:\n", + " genre_j = tunes[j][2]\n", + " # If `i` and `j` are of different genres, we can go from `i` to `j`.\n", + " if genre_i != genre_j:\n", + " possible_successors[i].append(j)\n", + "\n", + " # --------------------\n", + " # 2. Model\n", + " # --------------------\n", + " model = cp_model.CpModel()\n", + "\n", + " # --------------------\n", + " # 3. Decision Variables\n", + " # --------------------\n", + " # `literals[i][j]` is true if tune `j` follows tune `i` in the playlist.\n", + " literals = {}\n", + "\n", + " # --------------------\n", + " # 4. Constraints\n", + " # --------------------\n", + "\n", + " # 4.1 Two consecutive tunes must be of different genres.\n", + " # This is encoded in possible_successors, which doesn't contain any arcs\n", + " # between two tunes that are of the same genre. Now we just have to add a\n", + " # circuit constraint.\n", + "\n", + " # `arcs` contains the list of possible arcs in the circuit graph, each arc\n", + " # is a tuple (i, j, literals[i][j]).\n", + " arcs = []\n", + "\n", + " def AddArc(i, j):\n", + " literals[(i, j)] = model.new_bool_var(f\"lit_{i}_{j}\")\n", + " arcs.append((i, j, literals[(i, j)]))\n", + "\n", + " # Add all possible arcs between different nodes.\n", + " for i, successors in possible_successors.items():\n", + " for j in successors:\n", + " AddArc(i, j)\n", + "\n", + " # Add self-arcs to let tunes not be in the playlist.\n", + " for i in all_tunes:\n", + " AddArc(i, i)\n", + "\n", + " # Add a circuit constraint with the arcs.\n", + " model.add_circuit(arcs)\n", + "\n", + " # 4.2 All genres must appear at least once.\n", + " # This is encoded by adding a constraint that the sum of all literals for a\n", + " # given genre is at least 1.\n", + "\n", + " # `is_active[i]` is true iff tune `i` is in the playlist, i.e. if its self-arc\n", + " # is not active in the circuit.\n", + " is_active = {}\n", + " for i in all_tunes:\n", + " is_active[i] = literals[(i, i)].Not()\n", + "\n", + " # `genre_tunes[genre]` contains the list of tunes that are of genre `genre`.\n", + " genre_tunes = {}\n", + " for genre in genre_transition_costs:\n", + " genre_tunes[genre] = []\n", + " for i in all_tunes:\n", + " genre_tunes[tunes[i][2]].append(i)\n", + "\n", + " # For each genre, at least one tune must be active: the sum of all literals\n", + " # for this genre is at least 1.\n", + " for t in genre_tunes.values():\n", + " model.add(sum(is_active[i] for i in t) >= 1)\n", + "\n", + " # --------------------\n", + " # 5. Objective\n", + " # --------------------\n", + "\n", + " # 5.1. Minimize genre transition costs.\n", + "\n", + " # Add a total_transition_cost variable representing the sum of all transition\n", + " # costs in the playlist.\n", + " max_transition_cost = 0\n", + " for genre_costs in genre_transition_costs.values():\n", + " for cost in genre_costs.values():\n", + " max_transition_cost = max(cost, max_transition_cost)\n", + " total_transition_cost_upper_bound = (num_tunes - 1) * max_transition_cost\n", + " total_transition_cost = model.new_int_var(\n", + " 0, total_transition_cost_upper_bound, \"total_transition_cost\"\n", + " )\n", + "\n", + " transition_cost_terms = []\n", + " for i, successors in possible_successors.items():\n", + " if i == dummy_node:\n", + " continue\n", + " genre_i = tunes[i][2]\n", + " for j in successors:\n", + " if j == dummy_node:\n", + " continue\n", + " genre_j = tunes[j][2]\n", + " cost = genre_transition_costs[genre_i][genre_j]\n", + " transition_cost_terms.append(cost * literals[(i, j)])\n", + " model.add(total_transition_cost == sum(transition_cost_terms))\n", + "\n", + " # 5.2. Minimize the deviation between the target duration and the actual total\n", + " # duration.\n", + "\n", + " # Add a total_duration variable representing the duration of all active tunes.\n", + " total_duration_upper_bound = sum([t[1] for t in tunes])\n", + " total_duration = model.new_int_var(0, total_duration_upper_bound, \"total_duration\")\n", + " model.add(total_duration == sum(tunes[i][1] * is_active[i] for i in all_tunes))\n", + "\n", + " # Minimize the absolute difference from the target duration.\n", + " deviation = model.new_int_var(0, target_duration, \"deviation\")\n", + " model.add_abs_equality(deviation, total_duration - target_duration)\n", + "\n", + " # 5.3 Combine the objectives.\n", + " #\n", + " # You can add a weight to prioritize one over the other.\n", + " # For example, `model.minimize(10 * total_transition_cost + deviation)`\n", + " model.minimize(total_transition_cost + deviation)\n", + "\n", + " # --------------------\n", + " # 6. Solve\n", + " # --------------------\n", + " solver = cp_model.CpSolver()\n", + " # Set a time limit for the solver\n", + " solver.parameters.max_time_in_seconds = 30.0\n", + " status = solver.solve(model)\n", + "\n", + " # -----------------------\n", + " # 7. Print the solution\n", + " # -----------------------\n", + " if status == cp_model.OPTIMAL:\n", + " print(\"Found Optimal Playlist:\")\n", + " elif status == cp_model.FEASIBLE:\n", + " print(\"Found Feasible Playlist:\")\n", + " else:\n", + " print(\"No solution found.\")\n", + " return\n", + "\n", + " print(f\" Total Transition Cost: {solver.value(total_transition_cost)}\")\n", + " print(\n", + " f\" Playlist Duration: {solver.value(total_duration)} seconds \"\n", + " f\"({solver.value(total_duration) / 60:.2f} minutes)\"\n", + " )\n", + " print(\n", + " f\" Deviation from target duration ({target_duration}):\"\n", + " f\" {solver.value(deviation)} seconds\"\n", + " )\n", + " print(\"-\" * 30)\n", + "\n", + " # Reconstruct the playlist sequence by starting from the dummy node.\n", + " playlist = []\n", + " current_node = dummy_node\n", + " while True:\n", + " # Find the successor of the current node.\n", + " next_node = dummy_node\n", + " for next_node in possible_successors[current_node]:\n", + " if solver.value(literals[(current_node, next_node)]):\n", + " break\n", + "\n", + " if next_node == dummy_node:\n", + " break # We've completed the loop back to the start.\n", + "\n", + " playlist.append(next_node)\n", + " current_node = next_node\n", + "\n", + " if not playlist:\n", + " print(\"Empty playlist.\")\n", + " else:\n", + " for i in playlist:\n", + " (name, duration, genre) = tunes[i]\n", + " print(f\"{i+1}. {name} ({genre}) - {duration}s\")\n", + "\n", + "\n", + "def main(argv: Sequence[str]) -> None:\n", + " if len(argv) > 1:\n", + " raise app.UsageError(\"Too many command-line arguments.\")\n", + " Solve()\n", + "\n", + "\n", + "main()\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebook/examples/no_wait_baking_scheduling_sat.ipynb b/examples/notebook/examples/no_wait_baking_scheduling_sat.ipynb index 93e6e8c811..24bf472667 100644 --- a/examples/notebook/examples/no_wait_baking_scheduling_sat.ipynb +++ b/examples/notebook/examples/no_wait_baking_scheduling_sat.ipynb @@ -93,7 +93,6 @@ "from typing import List, Sequence, Tuple\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_PARAMS = flags.define_string(\n", @@ -354,7 +353,7 @@ " # Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " solver.parameters.log_search_progress = True\n", " status = solver.solve(model)\n", "\n", diff --git a/examples/notebook/examples/pentominoes_sat.ipynb b/examples/notebook/examples/pentominoes_sat.ipynb index da74b189c8..7a0cc3df4b 100644 --- a/examples/notebook/examples/pentominoes_sat.ipynb +++ b/examples/notebook/examples/pentominoes_sat.ipynb @@ -98,7 +98,6 @@ "from typing import Dict, List\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "\n", @@ -211,7 +210,7 @@ " # Solve the model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " status = solver.solve(model)\n", "\n", " print(\n", diff --git a/examples/notebook/examples/prize_collecting_vrp.ipynb b/examples/notebook/examples/prize_collecting_vrp.ipynb index 1d0da49a88..4bf531de11 100644 --- a/examples/notebook/examples/prize_collecting_vrp.ipynb +++ b/examples/notebook/examples/prize_collecting_vrp.ipynb @@ -151,6 +151,8 @@ " total_distance = 0\n", " total_value_collected = 0\n", " for v in range(manager.GetNumberOfVehicles()):\n", + " if not routing.IsVehicleUsed(assignment, v):\n", + " continue\n", " index = routing.Start(v)\n", " plan_output = f'Route for vehicle {v}:\\n'\n", " route_distance = 0\n", diff --git a/examples/notebook/examples/pyflow_example.ipynb b/examples/notebook/examples/pyflow_example.ipynb index ef8a5b42f5..2f2a5389b6 100644 --- a/examples/notebook/examples/pyflow_example.ipynb +++ b/examples/notebook/examples/pyflow_example.ipynb @@ -120,7 +120,12 @@ " print(\"MinCostFlow on 4x4 matrix.\")\n", " num_sources = 4\n", " num_targets = 4\n", - " costs = [[90, 75, 75, 80], [35, 85, 55, 65], [125, 95, 90, 105], [45, 110, 95, 115]]\n", + " costs = [\n", + " [90, 75, 75, 80],\n", + " [35, 85, 55, 65],\n", + " [125, 95, 90, 105],\n", + " [45, 110, 95, 115],\n", + " ]\n", " expected_cost = 275\n", " smcf = min_cost_flow.SimpleMinCostFlow()\n", " for source in range(0, num_sources):\n", diff --git a/examples/notebook/examples/rcpsp_sat.ipynb b/examples/notebook/examples/rcpsp_sat.ipynb index 2dba41feeb..c8f3a1d7b5 100644 --- a/examples/notebook/examples/rcpsp_sat.ipynb +++ b/examples/notebook/examples/rcpsp_sat.ipynb @@ -93,7 +93,6 @@ "import collections\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "from ortools.scheduling import rcpsp_pb2\n", "from ortools.scheduling.python import rcpsp\n", @@ -428,7 +427,7 @@ "\n", " # Parse user specified parameters.\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", "\n", " # Favor objective_shaving over objective_lb_search.\n", " if solver.parameters.num_workers >= 16 and solver.parameters.num_workers < 24:\n", diff --git a/examples/notebook/examples/shift_scheduling_sat.ipynb b/examples/notebook/examples/shift_scheduling_sat.ipynb index e61259c2e2..9da86581ba 100644 --- a/examples/notebook/examples/shift_scheduling_sat.ipynb +++ b/examples/notebook/examples/shift_scheduling_sat.ipynb @@ -84,7 +84,6 @@ "outputs": [], "source": [ "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_OUTPUT_PROTO = flags.define_string(\n", @@ -477,7 +476,7 @@ " # Solve the model.\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " solution_printer = cp_model.ObjectiveSolutionPrinter()\n", " status = solver.solve(model, solution_printer)\n", "\n", diff --git a/examples/notebook/examples/single_machine_scheduling_with_setup_release_due_dates_sat.ipynb b/examples/notebook/examples/single_machine_scheduling_with_setup_release_due_dates_sat.ipynb index a01f93c92e..0126a9bce4 100644 --- a/examples/notebook/examples/single_machine_scheduling_with_setup_release_due_dates_sat.ipynb +++ b/examples/notebook/examples/single_machine_scheduling_with_setup_release_due_dates_sat.ipynb @@ -85,7 +85,6 @@ "source": [ "from typing import Sequence\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "# ----------------------------------------------------------------------------\n", @@ -566,7 +565,7 @@ " # Solve.\n", " solver = cp_model.CpSolver()\n", " if parameters:\n", - " text_format.Parse(parameters, solver.parameters)\n", + " solver.parameters.parse_text_format(parameters)\n", " solution_printer = SolutionPrinter()\n", " solver.best_bound_callback = lambda a: print(f\"New objective lower bound: {a}\")\n", " solver.solve(model, solution_printer)\n", diff --git a/examples/notebook/examples/spillover_sat.ipynb b/examples/notebook/examples/spillover_sat.ipynb new file mode 100644 index 0000000000..8b1e9fe692 --- /dev/null +++ b/examples/notebook/examples/spillover_sat.ipynb @@ -0,0 +1,439 @@ +{ + "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": [ + "# spillover_sat" + ] + }, + { + "cell_type": "markdown", + "id": "link", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "
\n", + "Run in Google Colab\n", + "\n", + "View source on GitHub\n", + "
" + ] + }, + { + "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", + "Solves the problem of buying physical machines to meet VM demand.\n", + "\n", + "The Spillover problem is defined as follows:\n", + "\n", + "You have M types of physical machines and V types of Virtual Machines (VMs). You\n", + "can use a physical machine of type m to get n_mv copies of VM v. Each physical\n", + "machine m has a cost of c_m. Each VM has a demand of d_v. VMs are assigned to\n", + "physical machines by the following rule. The demand for each VM type arrives\n", + "equally spaced out over the interval [0, 1]. For each VM type, there is a\n", + "priority order over the physical machine types that you must follow. When a\n", + "demand arrives, if there are any machines of the highest priority type\n", + "available, you use them first, then you move on to the second priority machine\n", + "type, and so on. Each VM type has a list of compatible physical machine types,\n", + "and when the list is exhausted, the remaining demand is not met. Your goal is\n", + "to pick quantities of the physical machines to buy (minimizing cost) so that at\n", + "least some target service level (e.g. 95%) of the total demand of all VM is met.\n", + "\n", + "The number of machines bought of each type and the number of VMs demanded of\n", + "each type is large enough that you can solve an approximate problem instead,\n", + "where the number of machines purchased and the assignment of machines to VMs is\n", + "fractional, if it is helpful to do so.\n", + "\n", + "The problem is not particularly interesting in isolation, it is more interesting\n", + "to embed this LP inside a larger optimization problem (e.g. consider a two stage\n", + "problem where in stage one, you buy machines, then in stage two, you realize VM\n", + "demand).\n", + "\n", + "The continuous approximation of this problem can be solved by LP (see the\n", + "MathOpt python examples). Doing this, instead of using MIP, is nontrivial.\n", + "Below, we show that continuous relaxation can be approximately solved by CP-SAT\n", + "as well, despite not having continuous variables. If you were solving the\n", + "problem in isolation, you should just use an LP solver, but if you were to add\n", + "side constraints or embed this within a more complex model, using CP-SAT could\n", + "be appropriate.\n", + "\n", + "If for each VM type, the physical machines that are most cost effective are the\n", + "highest priority, AND the target service level is 100%, then the problem has a\n", + "trivial optimal solution:\n", + " 1. Rank the VMs by lowest cost to meet a unit of demand with the #1 preferred\n", + " machine type.\n", + " 2. For each VM type in the order above, buy machines from #1 preferred machine\n", + " type, until either you have met all demand for the VM type.\n", + "\n", + "MOE:begin_strip\n", + "This example is motivated by the Cloudy problem, see go/fluid-model.\n", + "MOE:end_strip\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "code", + "metadata": {}, + "outputs": [], + "source": [ + "from collections.abc import Sequence\n", + "import dataclasses\n", + "import math\n", + "import random\n", + "\n", + "from ortools.sat.colab import flags\n", + "from ortools.sat.python import cp_model\n", + "\n", + "_MACHINE_TYPES = flags.define_integer(\n", + " \"machine_types\",\n", + " 100,\n", + " \"How many types of machines we can fulfill demand with.\",\n", + ")\n", + "\n", + "_VM_TYPES = flags.define_integer(\n", + " \"vm_types\", 500, \"How many types of VMs we need to supply.\"\n", + ")\n", + "\n", + "_FUNGIBILITY = flags.define_integer(\n", + " \"fungibility\",\n", + " 10,\n", + " \"Each VM type can be satisfied with this many machine types, selected\"\n", + " \" uniformly at random.\",\n", + ")\n", + "\n", + "_MAX_DEMAND = flags.define_integer(\n", + " \"max_demand\",\n", + " 100,\n", + " \"Demand for each VM type is in [max_demand//2, max_demand], uniformly at\"\n", + " \" random.\",\n", + ")\n", + "\n", + "_TEST_DATA = flags.define_bool(\n", + " \"test_data\", False, \"Use small test instance instead of random data.\"\n", + ")\n", + "\n", + "_SEED = flags.define_integer(\"seed\", 13, \"RNG seed for instance creation.\")\n", + "\n", + "_TIME_STEPS = flags.define_integer(\"time_steps\", 100, \"How much to discretize time.\")\n", + "\n", + "\n", + "@dataclasses.dataclass(frozen=True)\n", + "class MachineUse:\n", + " machine_type: int\n", + " vms_per_machine: int\n", + "\n", + "\n", + "@dataclasses.dataclass(frozen=True)\n", + "class VmDemand:\n", + " compatible_machines: tuple[MachineUse, ...]\n", + " vm_quantity: int\n", + "\n", + "\n", + "@dataclasses.dataclass(frozen=True)\n", + "class SpilloverProblem:\n", + " machine_cost: tuple[float, ...]\n", + " machine_limit: tuple[int, ...]\n", + " vm_demands: tuple[VmDemand, ...]\n", + " service_level: float\n", + " time_horizon: int\n", + "\n", + "\n", + "def _random_spillover_problem(\n", + " num_machines: int,\n", + " num_vms: int,\n", + " fungibility: int,\n", + " max_vm_demand: int,\n", + " horizon: int,\n", + ") -> SpilloverProblem:\n", + " \"\"\"Generates a random SpilloverProblem.\"\"\"\n", + " machine_costs = tuple(random.random() for _ in range(num_machines))\n", + " vm_demands = []\n", + " all_machines = list(range(num_machines))\n", + " min_vm_demand = max_vm_demand // 2\n", + " for _ in range(num_vms):\n", + " vm_use = []\n", + " for machine in random.sample(all_machines, fungibility):\n", + " vm_use.append(\n", + " MachineUse(machine_type=machine, vms_per_machine=random.randint(1, 10))\n", + " )\n", + " vm_demands.append(\n", + " VmDemand(\n", + " compatible_machines=tuple(vm_use),\n", + " vm_quantity=random.randint(min_vm_demand, max_vm_demand),\n", + " )\n", + " )\n", + " machine_need_ub = num_vms * max_vm_demand\n", + " machine_limit = (machine_need_ub,) * num_machines\n", + " return SpilloverProblem(\n", + " machine_cost=machine_costs,\n", + " machine_limit=machine_limit,\n", + " vm_demands=tuple(vm_demands),\n", + " service_level=0.95,\n", + " time_horizon=horizon,\n", + " )\n", + "\n", + "\n", + "def _test_problem() -> SpilloverProblem:\n", + " \"\"\"Creates a small SpilloverProblem with optimal objective of 360.\"\"\"\n", + " # To avoid machine type 2, ensure we buy enough of 1 to not stock out, cost\n", + " # 20\n", + " vm_a = VmDemand(\n", + " vm_quantity=10,\n", + " compatible_machines=(\n", + " MachineUse(machine_type=1, vms_per_machine=1),\n", + " MachineUse(machine_type=2, vms_per_machine=1),\n", + " ),\n", + " )\n", + " # machine type 0 is cheaper, but we don't want to stock out of machine type 1,\n", + " # so use all machine type 1, cost 40.\n", + " vm_b = VmDemand(\n", + " vm_quantity=20,\n", + " compatible_machines=(\n", + " MachineUse(machine_type=1, vms_per_machine=1),\n", + " MachineUse(machine_type=0, vms_per_machine=1),\n", + " ),\n", + " )\n", + " # Will use 3 copies of machine type 2, cost 300\n", + " vm_c = VmDemand(\n", + " vm_quantity=30,\n", + " compatible_machines=(MachineUse(machine_type=2, vms_per_machine=10),),\n", + " )\n", + " return SpilloverProblem(\n", + " machine_cost=(1.0, 2.0, 100.0),\n", + " machine_limit=(60, 60, 60),\n", + " vm_demands=(vm_a, vm_b, vm_c),\n", + " service_level=1.0,\n", + " time_horizon=100,\n", + " )\n", + "\n", + "\n", + "# Indices:\n", + "# * i in I, the VM demands\n", + "# * j in J, the machines supplied\n", + "#\n", + "# Data:\n", + "# * c_j: cost of a machine of type j\n", + "# * l_j: a limit of how many machines of type j you can buy.\n", + "# * n_ij: how many VMs of type i you get from a machine of type j\n", + "# * d_i: the total demand for VMs of type i\n", + "# * service_level: the target fraction of demand that is met.\n", + "# * P_i subset J: the compatible machine types for VM demand i.\n", + "# * UP_i(j) subset P_i, for j in P_i: for VM demand type i, the machines of\n", + "# priority higher than j\n", + "# * T: the number of integer time steps.\n", + "#\n", + "# Note: when d_i/n_ij is not integer, some approximation error is introduced in\n", + "# constraint 6 below.\n", + "#\n", + "# Decision variables:\n", + "# * s_j: the supply of machine type j\n", + "# * w_j: the time we run out of machine j, or 1 if we never run out\n", + "# * v_ij: when we start using supply j to meet demand i, or w_j if we never use\n", + "# this machine type for this demand.\n", + "# * o_i: the time we start failing to meet vm demand i\n", + "# * m_i: the total demand met for vm type i.\n", + "#\n", + "# Model the problem:\n", + "# min sum_{j in J} c_j s_j\n", + "# s.t.\n", + "# 1: sum_i m_i >= service_level * sum_{i in I} d_i\n", + "# 2: T * m_i <= o_i * d_i for all i in I\n", + "# 3: v_ij >= w_r for all i in I, j in C_i, r in UP_i(j)\n", + "# 4: v_ij <= w_j for all i in I, j in C_i\n", + "# 5: o_i = sum_{j in P_i} (w_j - v_ij) for all i in I\n", + "# 6: sum_{i in I: j in P_i}ceil(d_i/n_ij)(w_j - v_ij)<=T*s_j for all j in J\n", + "# o_i, w_j, v_ij in [0, T]\n", + "# 0 <= m_i <= d_i\n", + "# 0 <= s_j <= l_j\n", + "#\n", + "# The constraints say:\n", + "# 1. The amount of demand served must be at least 95% of total demand.\n", + "# 2. The demand served for VM type i is linear in the time we fail to keep\n", + "# serving demand.\n", + "# 3. Don't start using machine type j for demand i until all higher priority\n", + "# machine types r are used up.\n", + "# 4. The time we run out of machine type j must be after we start using it for\n", + "# VM demand type i.\n", + "# 5. The time we are unable to serve further VM demand i is the sum of the\n", + "# time spent serving the demand with each eligible machine type.\n", + "# 6. The total use of machine type j to serve demand does not exceed the\n", + "# supply. The ceil function above introduces some approximation error when\n", + "# d_i/n_ij is not integer.\n", + "def _solve_spillover_problem(problem: SpilloverProblem) -> None:\n", + " \"\"\"Solves the spillover problem and prints the optimal objective.\"\"\"\n", + " model = cp_model.CpModel()\n", + " num_machines = len(problem.machine_cost)\n", + " num_vms = len(problem.vm_demands)\n", + " horizon = problem.time_horizon\n", + " s = [\n", + " model.new_int_var(lb=0, ub=problem.machine_limit[j], name=f\"s_{j}\")\n", + " for j in range(num_machines)\n", + " ]\n", + " w = [\n", + " model.new_int_var(lb=0, ub=horizon, name=f\"w_{i}\") for i in range(num_machines)\n", + " ]\n", + " o = [model.new_int_var(lb=0, ub=horizon, name=f\"o_{j}\") for j in range(num_vms)]\n", + " m = [\n", + " model.new_int_var(lb=0, ub=problem.vm_demands[j].vm_quantity, name=f\"m_{j}\")\n", + " for j in range(num_vms)\n", + " ]\n", + " v = [\n", + " {\n", + " compat.machine_type: model.new_int_var(\n", + " lb=0, ub=horizon, name=f\"v_{i}_{compat.machine_type}\"\n", + " )\n", + " for compat in vm_demand.compatible_machines\n", + " }\n", + " for i, vm_demand in enumerate(problem.vm_demands)\n", + " ]\n", + "\n", + " obj = 0\n", + " for j in range(num_machines):\n", + " obj += s[j] * problem.machine_cost[j]\n", + " model.minimize(obj)\n", + "\n", + " # Constraint 1: demand served is at least service_level fraction of total.\n", + " total_vm_demand = sum(vm_demand.vm_quantity for vm_demand in problem.vm_demands)\n", + " model.add(sum(m) >= int(math.ceil(problem.service_level * total_vm_demand)))\n", + "\n", + " # Constraint 2: demand served is linear in time we stop serving.\n", + " for i in range(num_vms):\n", + " model.add(\n", + " problem.time_horizon * m[i] <= o[i] * problem.vm_demands[i].vm_quantity\n", + " )\n", + "\n", + " # Constraint 3: use machine type j for demand i after all higher priority\n", + " # machine types r are used up.\n", + " for i in range(num_vms):\n", + " for k, meet_demand in enumerate(problem.vm_demands[i].compatible_machines):\n", + " j = meet_demand.machine_type\n", + " for l in range(k):\n", + " r = problem.vm_demands[i].compatible_machines[l].machine_type\n", + " model.add(v[i][j] >= w[r])\n", + "\n", + " # Constraint 4: outage time of machine j is after start time for using j to\n", + " # meet VM demand i.\n", + " for i in range(num_vms):\n", + " for meet_demand in problem.vm_demands[i].compatible_machines:\n", + " j = meet_demand.machine_type\n", + " model.add(v[i][j] <= w[j])\n", + "\n", + " # Constraint 5: For VM demand i, time service ends is the sum of the time\n", + " # spent serving with each eligible machine type.\n", + " for i in range(num_vms):\n", + " sum_serving = 0\n", + " for meet_demand in problem.vm_demands[i].compatible_machines:\n", + " j = meet_demand.machine_type\n", + " sum_serving += w[j] - v[i][j]\n", + " model.add(o[i] == sum_serving)\n", + "\n", + " # Constraint 6: Total use of machine type j is at most the supply.\n", + " #\n", + " # We build the constraints in bulk because our data is transposed.\n", + " total_machine_use = [0 for _ in range(num_machines)]\n", + " for i in range(num_vms):\n", + " for meet_demand in problem.vm_demands[i].compatible_machines:\n", + " j = meet_demand.machine_type\n", + " nij = meet_demand.vms_per_machine\n", + " vm_quantity = problem.vm_demands[i].vm_quantity\n", + " # Want vm_quantity/nij, over estimate with ceil(vm_quantity/nij) to use\n", + " # integer coefficients.\n", + " rate = (vm_quantity + nij - 1) // nij\n", + " total_machine_use[j] += rate * (w[j] - v[i][j])\n", + " for j in range(num_machines):\n", + " model.add(total_machine_use[j] <= horizon * s[j])\n", + "\n", + " solver = cp_model.CpSolver()\n", + " solver.parameters.num_workers = 16\n", + " solver.parameters.log_search_progress = True\n", + " solver.max_time_in_seconds = 30.0\n", + " status = solver.solve(model)\n", + " if status != cp_model.OPTIMAL:\n", + " raise RuntimeError(f\"expected optimal, found: {status}\")\n", + " print(f\"objective: {solver.objective_value}\")\n", + "\n", + "\n", + "def main(argv: Sequence[str]) -> None:\n", + " del argv # Unused.\n", + " random.seed(_SEED.value)\n", + " if _TEST_DATA.value:\n", + " problem = _test_problem()\n", + " else:\n", + " problem = _random_spillover_problem(\n", + " _MACHINE_TYPES.value,\n", + " _VM_TYPES.value,\n", + " _FUNGIBILITY.value,\n", + " _MAX_DEMAND.value,\n", + " _TIME_STEPS.value,\n", + " )\n", + " print(problem)\n", + "\n", + " _solve_spillover_problem(problem)\n", + "\n", + "\n", + "main()\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebook/examples/spread_robots_sat.ipynb b/examples/notebook/examples/spread_robots_sat.ipynb index 09f31c81d4..fda17e41fb 100644 --- a/examples/notebook/examples/spread_robots_sat.ipynb +++ b/examples/notebook/examples/spread_robots_sat.ipynb @@ -86,7 +86,6 @@ "import math\n", "from typing import Sequence\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_NUM_ROBOTS = flags.define_integer(\"num_robots\", 8, \"Number of robots to place.\")\n", @@ -161,7 +160,7 @@ " # Creates a solver and solves the model.\n", " solver = cp_model.CpSolver()\n", " if params:\n", - " text_format.Parse(params, solver.parameters)\n", + " solver.parameters.parse_text_format(params)\n", " solver.parameters.log_search_progress = True\n", " status = solver.solve(model)\n", "\n", diff --git a/examples/notebook/examples/steel_mill_slab_sat.ipynb b/examples/notebook/examples/steel_mill_slab_sat.ipynb index c6124111f0..b20be405e7 100644 --- a/examples/notebook/examples/steel_mill_slab_sat.ipynb +++ b/examples/notebook/examples/steel_mill_slab_sat.ipynb @@ -88,7 +88,6 @@ "import time\n", "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "\n", @@ -360,7 +359,7 @@ " ### Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " objective_printer = cp_model.ObjectiveSolutionPrinter()\n", " status = solver.solve(model, objective_printer)\n", "\n", @@ -544,7 +543,7 @@ " ### Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", "\n", " solution_printer = SteelMillSlabSolutionPrinter(orders, assign, loads, losses)\n", " status = solver.solve(model, solution_printer)\n", @@ -614,7 +613,7 @@ " ### Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " solution_printer = cp_model.ObjectiveSolutionPrinter()\n", " status = solver.solve(model, solution_printer)\n", "\n", diff --git a/examples/notebook/examples/test_scheduling_sat.ipynb b/examples/notebook/examples/test_scheduling_sat.ipynb index 6d8c8394af..0235f85eac 100644 --- a/examples/notebook/examples/test_scheduling_sat.ipynb +++ b/examples/notebook/examples/test_scheduling_sat.ipynb @@ -101,7 +101,6 @@ "from ortools.sat.colab import flags\n", "import pandas as pd\n", "\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "\n", @@ -209,7 +208,7 @@ " # Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " status = solver.solve(model)\n", "\n", " # Report solution.\n", diff --git a/examples/notebook/examples/transit_time.ipynb b/examples/notebook/examples/transit_time.ipynb index 8f75f9028e..f8fa14e793 100644 --- a/examples/notebook/examples/transit_time.ipynb +++ b/examples/notebook/examples/transit_time.ipynb @@ -89,7 +89,6 @@ "outputs": [], "source": [ "from ortools.constraint_solver import pywrapcp\n", - "from ortools.constraint_solver import routing_enums_pb2\n", "\n", "\n", "###########################\n", diff --git a/examples/notebook/examples/weighted_latency_problem_sat.ipynb b/examples/notebook/examples/weighted_latency_problem_sat.ipynb index 7d0bb9980a..503155545a 100644 --- a/examples/notebook/examples/weighted_latency_problem_sat.ipynb +++ b/examples/notebook/examples/weighted_latency_problem_sat.ipynb @@ -85,8 +85,8 @@ "source": [ "import random\n", "from typing import Sequence\n", + "\n", "from ortools.sat.colab import flags\n", - "from google.protobuf import text_format\n", "from ortools.sat.python import cp_model\n", "\n", "_NUM_NODES = flags.define_integer(\"num_nodes\", 12, \"Number of nodes to visit.\")\n", @@ -94,7 +94,9 @@ "_PROFIT_RANGE = flags.define_integer(\"profit_range\", 50, \"Range of profit.\")\n", "_SEED = flags.define_integer(\"seed\", 0, \"Random seed.\")\n", "_PARAMS = flags.define_string(\n", - " \"params\", \"num_search_workers:16, max_time_in_seconds:5\", \"Sat solver parameters.\"\n", + " \"params\",\n", + " \"num_search_workers:16, max_time_in_seconds:5\",\n", + " \"Sat solver parameters.\",\n", ")\n", "_PROTO_FILE = flags.define_string(\n", " \"proto_file\", \"\", \"If not empty, output the proto to this file.\"\n", @@ -163,7 +165,7 @@ " # Solve model.\n", " solver = cp_model.CpSolver()\n", " if _PARAMS.value:\n", - " text_format.Parse(_PARAMS.value, solver.parameters)\n", + " solver.parameters.parse_text_format(_PARAMS.value)\n", " solver.parameters.log_search_progress = True\n", " solver.solve(model)\n", "\n", diff --git a/examples/notebook/graph/assignment_linear_sum_assignment.ipynb b/examples/notebook/graph/assignment_linear_sum_assignment.ipynb index 3692e1e9e6..7ff9ac1d4f 100644 --- a/examples/notebook/graph/assignment_linear_sum_assignment.ipynb +++ b/examples/notebook/graph/assignment_linear_sum_assignment.ipynb @@ -88,6 +88,7 @@ "from ortools.graph.python import linear_sum_assignment\n", "\n", "\n", + "\n", "def main():\n", " \"\"\"Linear Sum Assignment example.\"\"\"\n", " assignment = linear_sum_assignment.SimpleLinearSumAssignment()\n", diff --git a/examples/notebook/graph/assignment_min_flow.ipynb b/examples/notebook/graph/assignment_min_flow.ipynb index 330903a1ec..6edf050a5b 100644 --- a/examples/notebook/graph/assignment_min_flow.ipynb +++ b/examples/notebook/graph/assignment_min_flow.ipynb @@ -86,6 +86,7 @@ "from ortools.graph.python import min_cost_flow\n", "\n", "\n", + "\n", "def main():\n", " \"\"\"Solving an Assignment Problem with MinCostFlow.\"\"\"\n", " # Instantiate a SimpleMinCostFlow solver.\n", diff --git a/examples/notebook/graph/balance_min_flow.ipynb b/examples/notebook/graph/balance_min_flow.ipynb index 6f25892b61..229e499f9a 100644 --- a/examples/notebook/graph/balance_min_flow.ipynb +++ b/examples/notebook/graph/balance_min_flow.ipynb @@ -86,6 +86,7 @@ "from ortools.graph.python import min_cost_flow\n", "\n", "\n", + "\n", "def main():\n", " \"\"\"Solving an Assignment with teams of worker.\"\"\"\n", " smcf = min_cost_flow.SimpleMinCostFlow()\n", diff --git a/examples/notebook/graph/simple_max_flow_program.ipynb b/examples/notebook/graph/simple_max_flow_program.ipynb index 5e6cabde02..2e49a0289b 100644 --- a/examples/notebook/graph/simple_max_flow_program.ipynb +++ b/examples/notebook/graph/simple_max_flow_program.ipynb @@ -88,6 +88,7 @@ "from ortools.graph.python import max_flow\n", "\n", "\n", + "\n", "def main():\n", " \"\"\"MaxFlow simple interface example.\"\"\"\n", " # Instantiate a SimpleMaxFlow solver.\n", diff --git a/examples/notebook/graph/simple_min_cost_flow_program.ipynb b/examples/notebook/graph/simple_min_cost_flow_program.ipynb index 8f78329717..07efb1500a 100644 --- a/examples/notebook/graph/simple_min_cost_flow_program.ipynb +++ b/examples/notebook/graph/simple_min_cost_flow_program.ipynb @@ -88,6 +88,7 @@ "from ortools.graph.python import min_cost_flow\n", "\n", "\n", + "\n", "def main():\n", " \"\"\"MinCostFlow simple interface example.\"\"\"\n", " # Instantiate a SimpleMinCostFlow solver.\n", diff --git a/examples/notebook/linear_solver/assignment_groups_mip.ipynb b/examples/notebook/linear_solver/assignment_groups_mip.ipynb index 6a1a256796..126e0a16be 100644 --- a/examples/notebook/linear_solver/assignment_groups_mip.ipynb +++ b/examples/notebook/linear_solver/assignment_groups_mip.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " # Data\n", " costs = [\n", diff --git a/examples/notebook/linear_solver/assignment_mb.ipynb b/examples/notebook/linear_solver/assignment_mb.ipynb index ad5d07ee42..59aece13a8 100644 --- a/examples/notebook/linear_solver/assignment_mb.ipynb +++ b/examples/notebook/linear_solver/assignment_mb.ipynb @@ -90,6 +90,7 @@ "from ortools.linear_solver.python import model_builder\n", "\n", "\n", + "\n", "def main():\n", " # Data\n", " data_str = \"\"\"\n", diff --git a/examples/notebook/linear_solver/assignment_mip.ipynb b/examples/notebook/linear_solver/assignment_mip.ipynb index 60119e7f4c..bdb85d308c 100644 --- a/examples/notebook/linear_solver/assignment_mip.ipynb +++ b/examples/notebook/linear_solver/assignment_mip.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " # Data\n", " costs = [\n", diff --git a/examples/notebook/linear_solver/assignment_task_sizes_mip.ipynb b/examples/notebook/linear_solver/assignment_task_sizes_mip.ipynb index a4c4ca6dbf..7db62fc22d 100644 --- a/examples/notebook/linear_solver/assignment_task_sizes_mip.ipynb +++ b/examples/notebook/linear_solver/assignment_task_sizes_mip.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " # Data\n", " costs = [\n", diff --git a/examples/notebook/linear_solver/assignment_teams_mip.ipynb b/examples/notebook/linear_solver/assignment_teams_mip.ipynb index 6414ace0dd..90477f1955 100644 --- a/examples/notebook/linear_solver/assignment_teams_mip.ipynb +++ b/examples/notebook/linear_solver/assignment_teams_mip.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " # Data\n", " costs = [\n", diff --git a/examples/notebook/linear_solver/basic_example.ipynb b/examples/notebook/linear_solver/basic_example.ipynb index 525689e1a4..5176ae6070 100644 --- a/examples/notebook/linear_solver/basic_example.ipynb +++ b/examples/notebook/linear_solver/basic_example.ipynb @@ -87,6 +87,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " print(\"Google OR-Tools version:\", init.OrToolsVersion.version_string())\n", "\n", diff --git a/examples/notebook/linear_solver/bin_packing_mb.ipynb b/examples/notebook/linear_solver/bin_packing_mb.ipynb index 205acb33e2..8e21b6d545 100644 --- a/examples/notebook/linear_solver/bin_packing_mb.ipynb +++ b/examples/notebook/linear_solver/bin_packing_mb.ipynb @@ -90,6 +90,7 @@ "from ortools.linear_solver.python import model_builder\n", "\n", "\n", + "\n", "def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]:\n", " \"\"\"Create the data for the example.\"\"\"\n", "\n", diff --git a/examples/notebook/linear_solver/bin_packing_mip.ipynb b/examples/notebook/linear_solver/bin_packing_mip.ipynb index 53d68248b5..cefbfc700e 100644 --- a/examples/notebook/linear_solver/bin_packing_mip.ipynb +++ b/examples/notebook/linear_solver/bin_packing_mip.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def create_data_model():\n", " \"\"\"Create the data for the example.\"\"\"\n", " data = {}\n", @@ -98,6 +99,7 @@ "\n", "\n", "\n", + "\n", "def main():\n", " data = create_data_model()\n", "\n", diff --git a/examples/notebook/linear_solver/clone_model_mb.ipynb b/examples/notebook/linear_solver/clone_model_mb.ipynb index f64b365cf4..43f9cf98ab 100644 --- a/examples/notebook/linear_solver/clone_model_mb.ipynb +++ b/examples/notebook/linear_solver/clone_model_mb.ipynb @@ -88,6 +88,7 @@ "from ortools.linear_solver.python import model_builder\n", "\n", "\n", + "\n", "def main():\n", " # Create the model.\n", " model = model_builder.Model()\n", diff --git a/examples/notebook/linear_solver/integer_programming_example.ipynb b/examples/notebook/linear_solver/integer_programming_example.ipynb index 49a9a840ea..21270651a9 100644 --- a/examples/notebook/linear_solver/integer_programming_example.ipynb +++ b/examples/notebook/linear_solver/integer_programming_example.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def IntegerProgrammingExample():\n", " \"\"\"Integer programming sample.\"\"\"\n", " # Create the mip solver with the SCIP backend.\n", diff --git a/examples/notebook/linear_solver/linear_programming_example.ipynb b/examples/notebook/linear_solver/linear_programming_example.ipynb index 3db71f1df0..41e563d3d2 100644 --- a/examples/notebook/linear_solver/linear_programming_example.ipynb +++ b/examples/notebook/linear_solver/linear_programming_example.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def LinearProgrammingExample():\n", " \"\"\"Linear programming sample.\"\"\"\n", " # Instantiate a Glop solver, naming it LinearExample.\n", diff --git a/examples/notebook/linear_solver/mip_var_array.ipynb b/examples/notebook/linear_solver/mip_var_array.ipynb index 0a1575fc96..23f271c1e4 100644 --- a/examples/notebook/linear_solver/mip_var_array.ipynb +++ b/examples/notebook/linear_solver/mip_var_array.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def create_data_model():\n", " \"\"\"Stores the data for the problem.\"\"\"\n", " data = {}\n", @@ -103,6 +104,7 @@ "\n", "\n", "\n", + "\n", "def main():\n", " data = create_data_model()\n", " # Create the mip solver with the SCIP backend.\n", diff --git a/examples/notebook/linear_solver/multiple_knapsack_mip.ipynb b/examples/notebook/linear_solver/multiple_knapsack_mip.ipynb index ab752d2a99..4412da1565 100644 --- a/examples/notebook/linear_solver/multiple_knapsack_mip.ipynb +++ b/examples/notebook/linear_solver/multiple_knapsack_mip.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " data = {}\n", " data[\"weights\"] = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36]\n", diff --git a/examples/notebook/linear_solver/simple_lp_program.ipynb b/examples/notebook/linear_solver/simple_lp_program.ipynb index 8878896a77..b86ba489a2 100644 --- a/examples/notebook/linear_solver/simple_lp_program.ipynb +++ b/examples/notebook/linear_solver/simple_lp_program.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " # Create the linear solver with the GLOP backend.\n", " solver = pywraplp.Solver.CreateSolver(\"GLOP\")\n", diff --git a/examples/notebook/linear_solver/simple_lp_program_mb.ipynb b/examples/notebook/linear_solver/simple_lp_program_mb.ipynb index b974525215..9c18840229 100644 --- a/examples/notebook/linear_solver/simple_lp_program_mb.ipynb +++ b/examples/notebook/linear_solver/simple_lp_program_mb.ipynb @@ -88,6 +88,7 @@ "from ortools.linear_solver.python import model_builder\n", "\n", "\n", + "\n", "def main():\n", " # Create the model.\n", " model = model_builder.Model()\n", diff --git a/examples/notebook/linear_solver/simple_mip_program.ipynb b/examples/notebook/linear_solver/simple_mip_program.ipynb index 9ba26454b5..2993a83ee3 100644 --- a/examples/notebook/linear_solver/simple_mip_program.ipynb +++ b/examples/notebook/linear_solver/simple_mip_program.ipynb @@ -86,6 +86,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " # Create the mip solver with the CP-SAT backend.\n", " solver = pywraplp.Solver.CreateSolver(\"SAT\")\n", diff --git a/examples/notebook/linear_solver/simple_mip_program_mb.ipynb b/examples/notebook/linear_solver/simple_mip_program_mb.ipynb index 9feaf39005..577001dda5 100644 --- a/examples/notebook/linear_solver/simple_mip_program_mb.ipynb +++ b/examples/notebook/linear_solver/simple_mip_program_mb.ipynb @@ -88,6 +88,7 @@ "from ortools.linear_solver.python import model_builder\n", "\n", "\n", + "\n", "def main():\n", " # Create the model.\n", " model = model_builder.Model()\n", diff --git a/examples/notebook/linear_solver/stigler_diet.ipynb b/examples/notebook/linear_solver/stigler_diet.ipynb index 50bc390dbe..ba0e6e6616 100644 --- a/examples/notebook/linear_solver/stigler_diet.ipynb +++ b/examples/notebook/linear_solver/stigler_diet.ipynb @@ -89,6 +89,7 @@ "from ortools.linear_solver import pywraplp\n", "\n", "\n", + "\n", "def main():\n", " \"\"\"Entry point of the program.\"\"\"\n", " # Instantiate the data problem.\n", diff --git a/examples/notebook/sat/ranking_circuit_sample_sat.ipynb b/examples/notebook/sat/ranking_circuit_sample_sat.ipynb index 8f7be0d794..ea5d2909d3 100644 --- a/examples/notebook/sat/ranking_circuit_sample_sat.ipynb +++ b/examples/notebook/sat/ranking_circuit_sample_sat.ipynb @@ -73,7 +73,8 @@ "metadata": {}, "source": [ "\n", - "Code sample to demonstrates how to rank intervals using a circuit.\n" + "Code sample to demonstrates how to rank intervals using a circuit.\n", + "\n" ] }, { @@ -83,8 +84,7 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import List, Sequence\n", - "\n", + "from collections.abc import Sequence\n", "\n", "from ortools.sat.python import cp_model\n", "\n", @@ -125,7 +125,7 @@ " num_tasks = len(starts)\n", " all_tasks = range(num_tasks)\n", "\n", - " arcs: List[cp_model.ArcT] = []\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", diff --git a/examples/notebook/sat/sequences_in_no_overlap_sample_sat.ipynb b/examples/notebook/sat/sequences_in_no_overlap_sample_sat.ipynb index 0cf40020dc..8bf5e64d4f 100644 --- a/examples/notebook/sat/sequences_in_no_overlap_sample_sat.ipynb +++ b/examples/notebook/sat/sequences_in_no_overlap_sample_sat.ipynb @@ -83,7 +83,7 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Dict, List, Sequence, Tuple\n", + "from collections.abc import Sequence\n", "\n", "from ortools.sat.python import cp_model\n", "\n", @@ -95,9 +95,9 @@ " task_types: Sequence[str],\n", " lengths: Sequence[cp_model.IntVar],\n", " cumuls: Sequence[cp_model.IntVar],\n", - " sequence_length_constraints: Dict[str, Tuple[int, int]],\n", - " sequence_cumul_constraints: Dict[str, Tuple[int, int, int]],\n", - ") -> Sequence[Tuple[cp_model.IntVar, int]]:\n", + " sequence_length_constraints: dict[str, tuple[int, int]],\n", + " sequence_cumul_constraints: dict[str, tuple[int, int, int]],\n", + ") -> Sequence[tuple[cp_model.IntVar, int]]:\n", " \"\"\"This method enforces constraints on sequences of tasks of the same type.\n", "\n", " This method assumes that all durations are strictly positive.\n", @@ -133,7 +133,7 @@ " num_tasks = len(starts)\n", " all_tasks = range(num_tasks)\n", "\n", - " arcs: List[cp_model.ArcT] = []\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", diff --git a/examples/notebook/sat/soft_constraints_sat.ipynb b/examples/notebook/sat/soft_constraints_sat.ipynb new file mode 100644 index 0000000000..b0bfadb918 --- /dev/null +++ b/examples/notebook/sat/soft_constraints_sat.ipynb @@ -0,0 +1,249 @@ +{ + "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": [ + "# soft_constraints_sat" + ] + }, + { + "cell_type": "markdown", + "id": "link", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "
\n", + "Run in Google Colab\n", + "\n", + "View source on GitHub\n", + "
" + ] + }, + { + "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", + "The sample shows multiple ways to model soft constraints in CP-SAT.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "code", + "metadata": {}, + "outputs": [], + "source": [ + "from ortools.sat.python import cp_model\n", + "\n", + "\n", + "\n", + "def infeasible_model() -> None:\n", + " \"\"\"Base model that is infeasible.\"\"\"\n", + " # Creates the model.\n", + " model = cp_model.CpModel()\n", + "\n", + " # Creates the variables.\n", + " x = model.new_int_var(0, 10, \"x\")\n", + " y = model.new_int_var(0, 10, \"y\")\n", + " z = model.new_int_var(0, 10, \"z\")\n", + "\n", + " # Creates the constraints.\n", + " model.add(x > y)\n", + " model.add(y > z)\n", + " model.add(z > x)\n", + "\n", + " # Creates a solver and solves.\n", + " solver = cp_model.CpSolver()\n", + " status = solver.solve(model)\n", + "\n", + " # Print solution.\n", + " print(f\" Status = {solver.status_name(status)}\")\n", + "\n", + "\n", + "def model_with_enforcement_literals() -> None:\n", + " \"\"\"Adds fixed costs to violated constraints.\"\"\"\n", + " # Creates the model.\n", + " model = cp_model.CpModel()\n", + "\n", + " # Creates the variables.\n", + " x = model.new_int_var(0, 10, \"x\")\n", + " y = model.new_int_var(0, 10, \"y\")\n", + " z = model.new_int_var(0, 10, \"z\")\n", + " a = model.new_bool_var(\"a\")\n", + " b = model.new_bool_var(\"b\")\n", + "\n", + " # Creates the constraints. Adds enforcement literals to the first two\n", + " # constraints, we assume the third constraint is always enforced.\n", + " model.add(x > y).only_enforce_if(a)\n", + " model.add(y > z).only_enforce_if(b)\n", + " model.add(z > x)\n", + "\n", + " # Adds an objective to maximize the number of enforced constraints.\n", + " model.maximize(a + 2 * b)\n", + "\n", + " # Creates a solver and solves.\n", + " solver = cp_model.CpSolver()\n", + " status = solver.solve(model)\n", + "\n", + " # Print solution.\n", + " print(f\" Status = {solver.status_name(status)}\")\n", + " if status == cp_model.OPTIMAL:\n", + " print(f\" Objective value = {solver.objective_value}\")\n", + " print(f\" Value of x = {solver.value(x)}\")\n", + " print(f\" Value of y = {solver.value(y)}\")\n", + " print(f\" Value of z = {solver.value(z)}\")\n", + " print(f\" Value of a = {solver.boolean_value(a)}\")\n", + " print(f\" Value of b = {solver.boolean_value(b)}\")\n", + "\n", + "\n", + "def model_with_linear_violations() -> None:\n", + " \"\"\"Adds fixed costs to violated constraints.\"\"\"\n", + " # Creates the model.\n", + " model = cp_model.CpModel()\n", + "\n", + " # Creates the variables.\n", + " x = model.new_int_var(0, 10, \"x\")\n", + " y = model.new_int_var(0, 10, \"y\")\n", + " z = model.new_int_var(0, 10, \"z\")\n", + " a = model.new_int_var(0, 10, \"a\")\n", + " b = model.new_int_var(0, 10, \"b\")\n", + "\n", + " # Creates the constraints. Adds enforcement literals to the first two\n", + " # constraints, we assume the third constraint is always enforced.\n", + " model.add(x > y - a)\n", + " model.add(y > z - b)\n", + " model.add(z > x)\n", + "\n", + " # Adds an objective to minimize the added slacks.\n", + " model.minimize(a + 2 * b)\n", + "\n", + " # Creates a solver and solves.\n", + " solver = cp_model.CpSolver()\n", + " status = solver.solve(model)\n", + "\n", + " # Print solution.\n", + " print(f\" Status = {solver.status_name(status)}\")\n", + " if status == cp_model.OPTIMAL:\n", + " print(f\" Objective value = {solver.objective_value}\")\n", + " print(f\" Value of x = {solver.value(x)}\")\n", + " print(f\" Value of y = {solver.value(y)}\")\n", + " print(f\" Value of z = {solver.value(z)}\")\n", + " print(f\" Value of a = {solver.value(a)}\")\n", + " print(f\" Value of b = {solver.value(b)}\")\n", + "\n", + "\n", + "def model_with_quadratic_violations() -> None:\n", + " \"\"\"Adds fixed costs to violated constraints.\"\"\"\n", + " # Creates the model.\n", + " model = cp_model.CpModel()\n", + "\n", + " # Creates the variables.\n", + " x = model.new_int_var(0, 10, \"x\")\n", + " y = model.new_int_var(0, 10, \"y\")\n", + " z = model.new_int_var(0, 10, \"z\")\n", + " a = model.new_int_var(0, 10, \"a\")\n", + " b = model.new_int_var(0, 10, \"b\")\n", + " square_a = model.new_int_var(0, 100, \"square_a\")\n", + " square_b = model.new_int_var(0, 100, \"square_b\")\n", + "\n", + " # Creates the constraints. Adds enforcement literals to the first two\n", + " # constraints, we assume the third constraint is always enforced.\n", + " model.add(x > y - a)\n", + " model.add(y > z - b)\n", + " model.add(z > x)\n", + "\n", + " model.add_multiplication_equality(square_a, a, a)\n", + " model.add_multiplication_equality(square_b, b, b)\n", + "\n", + " # Adds an objective to minimize the added slacks.\n", + " model.minimize(square_a + 2 * square_b)\n", + "\n", + " # Creates a solver and solves.\n", + " solver = cp_model.CpSolver()\n", + " status = solver.solve(model)\n", + "\n", + " # Print solution.\n", + " print(f\" Status = {solver.status_name(status)}\")\n", + " if status == cp_model.OPTIMAL:\n", + " print(f\" Objective value = {solver.objective_value}\")\n", + " print(f\" Value of x = {solver.value(x)}\")\n", + " print(f\" Value of y = {solver.value(y)}\")\n", + " print(f\" Value of z = {solver.value(z)}\")\n", + " print(f\" Value of a = {solver.value(a)}\")\n", + " print(f\" Value of b = {solver.value(b)}\")\n", + "\n", + "\n", + "def main() -> None:\n", + " print(\"Infeasible model:\")\n", + " infeasible_model()\n", + " print(\"Model with enforcement literals:\")\n", + " model_with_enforcement_literals()\n", + " print(\"Model with linear violations:\")\n", + " model_with_linear_violations()\n", + " print(\"Model with quadratic violations:\")\n", + " model_with_quadratic_violations()\n", + "\n", + "\n", + "main()\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebook/sat/transitions_in_no_overlap_sample_sat.ipynb b/examples/notebook/sat/transitions_in_no_overlap_sample_sat.ipynb index 05f0a4ea3e..ba7b105464 100644 --- a/examples/notebook/sat/transitions_in_no_overlap_sample_sat.ipynb +++ b/examples/notebook/sat/transitions_in_no_overlap_sample_sat.ipynb @@ -83,7 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Dict, List, Sequence, Tuple, Union\n", + "from collections.abc import Sequence\n", + "from typing import Union\n", "\n", "from ortools.sat.python import cp_model\n", "\n", @@ -93,9 +94,9 @@ " starts: Sequence[cp_model.IntVar],\n", " durations: Sequence[int],\n", " presences: Sequence[Union[cp_model.IntVar, bool]],\n", - " penalties: Dict[Tuple[int, int], int],\n", - " delays: Dict[Tuple[int, int], int],\n", - ") -> Sequence[Tuple[cp_model.IntVar, int]]:\n", + " penalties: dict[tuple[int, int], int],\n", + " delays: dict[tuple[int, int], int],\n", + ") -> Sequence[tuple[cp_model.IntVar, int]]:\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", @@ -132,7 +133,7 @@ " num_tasks = len(starts)\n", " all_tasks = range(num_tasks)\n", "\n", - " arcs: List[cp_model.ArcT] = []\n", + " arcs: list[cp_model.ArcT] = []\n", " penalty_terms = []\n", " for i in all_tasks:\n", " # if node i is first.\n", diff --git a/examples/notebook/set_cover/set_cover.ipynb b/examples/notebook/set_cover/set_cover.ipynb index 3bc66a08eb..d41780de5e 100644 --- a/examples/notebook/set_cover/set_cover.ipynb +++ b/examples/notebook/set_cover/set_cover.ipynb @@ -86,6 +86,7 @@ "from ortools.set_cover.python import set_cover\n", "\n", "\n", + "\n", "def main():\n", " model = set_cover.SetCoverModel()\n", " model.add_empty_subset(2.0)\n",