From 1f4e76642aeea4e92f7e504383e453f4952aeaf8 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Wed, 3 Jul 2024 20:11:15 +0200 Subject: [PATCH] [CP-SAT] fix struct packing; add missing example --- ortools/sat/docs/scheduling.md | 12 +- ortools/sat/integer.h | 4 +- ortools/sat/linear_propagation.h | 5 +- ortools/sat/samples/BUILD.bazel | 2 +- .../transitions_in_no_overlap_sample_sat.py | 203 ++++++++++++++++++ ortools/sat/sat_parameters.proto | 2 +- 6 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 ortools/sat/samples/transitions_in_no_overlap_sample_sat.py diff --git a/ortools/sat/docs/scheduling.md b/ortools/sat/docs/scheduling.md index e435f263730..54f72d57a3e 100644 --- a/ortools/sat/docs/scheduling.md +++ b/ortools/sat/docs/scheduling.md @@ -2316,7 +2316,7 @@ def overlapping_interval_sample_sat(): overlapping_interval_sample_sat() ``` -## Transitions in a disjunctive resource +## Transitions in a no_overlap constraint In some scheduling problems, switching between certain type of tasks on a machine implies some penalty, and/or some delay. Implementing these @@ -2330,14 +2330,14 @@ successor literals to implement the penalties or the delays. ```python #!/usr/bin/env python3 -"""Code sample to demonstrates how to rank intervals using a circuit.""" +"""Implements transition times and costs in a no_overlap constraint.""" from typing import Dict, List, Sequence, Tuple, Union from ortools.sat.python import cp_model -def transitive_reduction_with_circuit( +def transitive_reduction_with_circuit_delays_and_penalties( model: cp_model.CpModel, starts: Sequence[cp_model.IntVar], durations: Sequence[int], @@ -2435,7 +2435,7 @@ def transitive_reduction_with_circuit( return penalty_terms -def transitions_in_disjunctive_sample_sat(): +def transitions_in_no_overlap_sample_sat(): """Implement transitions in a NoOverlap constraint.""" model = cp_model.CpModel() @@ -2490,7 +2490,7 @@ def transitions_in_disjunctive_sample_sat(): model.add_no_overlap(intervals) # Adds ranking constraint. - penalty_terms = transitive_reduction_with_circuit( + penalty_terms = transitive_reduction_with_circuit_delays_and_penalties( model, starts, durations, presences, penalties, delays ) @@ -2518,7 +2518,7 @@ def transitions_in_disjunctive_sample_sat(): print(f"Solver exited with nonoptimal status: {status}") -transitions_in_disjunctive_sample_sat() +transitions_in_no_overlap_sample_sat() ``` ## Precedences between intervals diff --git a/ortools/sat/integer.h b/ortools/sat/integer.h index 2f2895031a1..472522028a7 100644 --- a/ortools/sat/integer.h +++ b/ortools/sat/integer.h @@ -750,7 +750,7 @@ class IntegerEncoder { // This class maintains a set of integer variables with their current bounds. // Bounds can be propagated from an external "source" and this class helps // to maintain the reason for each propagation. -class IntegerTrail : public SatPropagator { +class IntegerTrail final : public SatPropagator { public: explicit IntegerTrail(Model* model) : SatPropagator("IntegerTrail"), @@ -1365,7 +1365,7 @@ class RevIntegerValueRepository : public RevRepository { // watched Literal or LbVar changes. // // TODO(user): Move this to its own file. Add unit tests! -class GenericLiteralWatcher : public SatPropagator { +class GenericLiteralWatcher final : public SatPropagator { public: explicit GenericLiteralWatcher(Model* model); diff --git a/ortools/sat/linear_propagation.h b/ortools/sat/linear_propagation.h index ce48fed98b7..1b2bd5214a7 100644 --- a/ortools/sat/linear_propagation.h +++ b/ortools/sat/linear_propagation.h @@ -323,7 +323,8 @@ class LinearPropagator : public PropagatorInterface, ReversibleInterface { // initial size and enf_id that are only needed when we push something. struct ConstraintInfo { unsigned int enf_status : 2; - bool all_coeffs_are_one : 1; + // With Visual Studio or minGW, using bool here breaks the struct packing. + unsigned int all_coeffs_are_one : 1; unsigned int initial_size : 29; // Const. The size including all terms. EnforcementId enf_id; // Const. The id in enforcement_propagator_. @@ -332,10 +333,8 @@ class LinearPropagator : public PropagatorInterface, ReversibleInterface { IntegerValue rev_rhs; // The current rhs, updated on fixed terms. }; -#if !defined(_MSC_VER) static_assert(sizeof(ConstraintInfo) == 24, "ERROR_ConstraintInfo_is_not_well_compacted"); -#endif // !defined(_MSC_VER) absl::Span GetCoeffs(const ConstraintInfo& info); absl::Span GetVariables(const ConstraintInfo& info); diff --git a/ortools/sat/samples/BUILD.bazel b/ortools/sat/samples/BUILD.bazel index 8ec43f16071..0c0537b6921 100644 --- a/ortools/sat/samples/BUILD.bazel +++ b/ortools/sat/samples/BUILD.bazel @@ -95,7 +95,7 @@ code_sample_cc_py(name = "solve_with_time_limit_sample_sat") code_sample_cc_py(name = "stop_after_n_solutions_sample_sat") -code_sample_py(name = "transitions_in_disjunctive_sample_sat") +code_sample_py(name = "transitions_in_no_overlap_sample_sat") code_sample_java(name = "AssignmentGroupsSat") diff --git a/ortools/sat/samples/transitions_in_no_overlap_sample_sat.py b/ortools/sat/samples/transitions_in_no_overlap_sample_sat.py new file mode 100644 index 00000000000..56e7d6302fd --- /dev/null +++ b/ortools/sat/samples/transitions_in_no_overlap_sample_sat.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# Copyright 2010-2024 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Implements transition times and costs in a no_overlap constraint.""" + +from typing import Dict, List, Sequence, Tuple, Union + +from ortools.sat.python import cp_model + + +def transitive_reduction_with_circuit_delays_and_penalties( + model: cp_model.CpModel, + starts: Sequence[cp_model.IntVar], + durations: Sequence[int], + presences: Sequence[Union[cp_model.IntVar, bool]], + penalties: Dict[Tuple[int, int], int], + delays: Dict[Tuple[int, int], int], +) -> Sequence[Tuple[cp_model.IntVar, int]]: + """This method uses a circuit constraint to rank tasks. + + This method assumes that all starts are disjoint, meaning that all tasks have + a strictly positive duration, and they appear in the same NoOverlap + constraint. + + The extra node (with id 0) will be used to decide which task is first with + its only outgoing arc, and which task is last with its only incoming arc. + Each task i will be associated with id i + 1, and an arc between i + 1 and j + + 1 indicates that j is the immediate successor of i. + + The circuit constraint ensures there is at most 1 hamiltonian cycle of + length > 1. If no such path exists, then no tasks are active. + We also need to enforce that any hamiltonian cycle of size > 1 must contain + the node 0. And thus, there is a self loop on node 0 iff the circuit is empty. + + Args: + model: The CpModel to add the constraints to. + starts: The array of starts variables of all tasks. + durations: the durations of all tasks. + presences: The array of presence variables of all tasks. + penalties: the array of tuple (`tail_index`, `head_index`, `penalty`) that + specifies that if task `tail_index` is the successor of the task + `head_index`, then `penalty` must be added to the cost. + delays: the array of tuple (`tail_index`, `head_index`, `delay`) that + specifies that if task `tail_index` is the successor of the task + `head_index`, then an extra `delay` must be added between the end of the + first task and the start of the second task. + + Returns: + The list of pairs (Boolean variables, penalty) to be added to the objective. + """ + + num_tasks = len(starts) + all_tasks = range(num_tasks) + + arcs: List[cp_model.ArcT] = [] + penalty_terms = [] + for i in all_tasks: + # if node i is first. + start_lit = model.new_bool_var(f"start_{i}") + arcs.append((0, i + 1, start_lit)) + + # As there are no other constraints on the problem, we can add this + # redundant constraint. + model.add(starts[i] == 0).only_enforce_if(start_lit) + + # if node i is last. + end_lit = model.new_bool_var(f"end_{i}") + arcs.append((i + 1, 0, end_lit)) + + for j in all_tasks: + if i == j: + arcs.append((i + 1, i + 1, ~presences[i])) + else: + literal = model.new_bool_var(f"arc_{i}_to_{j}") + arcs.append((i + 1, j + 1, literal)) + + # To perform the transitive reduction from precedences to successors, + # we need to tie the starts of the tasks with 'literal'. + # In a pure problem, the following inequality could be an equality. + # It is not true in general. + # + # Note that we could use this literal to penalize the transition, add an + # extra delay to the precedence. + min_delay = 0 + key = (i, j) + if key in delays: + min_delay = delays[key] + model.add( + starts[j] >= starts[i] + durations[i] + min_delay + ).only_enforce_if(literal) + + # Create the penalties. + if key in penalties: + penalty_terms.append((literal, penalties[key])) + + # Manage the empty circuit + empty = model.new_bool_var("empty") + arcs.append((0, 0, empty)) + + for i in all_tasks: + model.add_implication(empty, ~presences[i]) + + # Add the circuit constraint. + model.add_circuit(arcs) + + return penalty_terms + + +def transitions_in_no_overlap_sample_sat(): + """Implement transitions in a NoOverlap constraint.""" + + model = cp_model.CpModel() + horizon = 40 + num_tasks = 4 + + # Breaking the natural sequence induces a fixed penalty. + penalties = { + (1, 0): 10, + (2, 0): 10, + (3, 0): 10, + (2, 1): 10, + (3, 1): 10, + (3, 2): 10, + } + + # Switching from an odd to even or even to odd task indices induces a delay. + delays = { + (1, 0): 10, + (0, 1): 10, + (3, 0): 10, + (0, 3): 10, + (1, 2): 10, + (2, 1): 10, + (3, 2): 10, + (2, 3): 10, + } + + all_tasks = range(num_tasks) + + starts = [] + durations = [] + intervals = [] + presences = [] + + # Creates intervals, all present. But the cost is robust w.r.t. optional + # intervals. + for t in all_tasks: + start = model.new_int_var(0, horizon, f"start[{t}]") + duration = 5 + presence = True + interval = model.new_optional_fixed_size_interval_var( + start, duration, presence, f"opt_interval[{t}]" + ) + + starts.append(start) + durations.append(duration) + intervals.append(interval) + presences.append(presence) + + # Adds NoOverlap constraint. + model.add_no_overlap(intervals) + + # Adds ranking constraint. + penalty_terms = transitive_reduction_with_circuit_delays_and_penalties( + model, starts, durations, presences, penalties, delays + ) + + # Minimize the sum of penalties, + model.minimize(sum(var * penalty for var, penalty in penalty_terms)) + + # In practise, only one penalty can happen. Thus the two even tasks are + # together, same for the two odd tasks. + # Because of the penalties, the optimal sequence is 0 -> 2 -> 1 -> 3 + # which induces one penalty and one delay. + + # Solves the model model. + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + # Prints out the makespan and the start times and ranks of all tasks. + print(f"Optimal cost: {solver.objective_value}") + for t in all_tasks: + if solver.value(presences[t]): + print(f"Task {t} starts at {solver.value(starts[t])} ") + else: + print(f"Task {t} in not performed") + else: + print(f"Solver exited with nonoptimal status: {status}") + + +transitions_in_no_overlap_sample_sat() diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index a741dc483a9..3b89a6f1192 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -1096,7 +1096,7 @@ message SatParameters { // total number of nodes that may be generated in the shared tree. If the // shared tree runs out of unassigned leaves, workers act as portfolio // workers. Note: this limit includes interior nodes, not just leaves. - optional int32 shared_tree_max_nodes_per_worker = 238 [default = 128]; + optional int32 shared_tree_max_nodes_per_worker = 238 [default = 100000]; enum SharedTreeSplitStrategy { // Uses the default strategy, currently equivalent to