Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
routing_sat.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14#include <algorithm>
15#include <atomic>
16#include <cstdint>
17#include <functional>
18#include <limits>
19#include <memory>
20#include <ostream>
21#include <utility>
22#include <vector>
23
24#include "absl/container/btree_map.h"
25#include "absl/container/flat_hash_map.h"
26#include "absl/log/check.h"
27#include "absl/time/time.h"
28#include "absl/types/span.h"
32#include "ortools/constraint_solver/routing_parameters.pb.h"
34#include "ortools/sat/cp_model.pb.h"
36#include "ortools/sat/integer.h"
37#include "ortools/sat/model.h"
38#include "ortools/sat/sat_parameters.pb.h"
39#include "ortools/util/bitset.h"
40#include "ortools/util/optional_boolean.pb.h"
43
44namespace operations_research {
45namespace sat {
46namespace {
47
48using operations_research::sat::BoolArgumentProto;
49using operations_research::sat::CircuitConstraintProto;
50using operations_research::sat::ConstraintProto;
51using operations_research::sat::CpModelProto;
52using operations_research::sat::CpObjectiveProto;
53using operations_research::sat::CpSolverResponse;
54using operations_research::sat::CpSolverStatus;
55using operations_research::sat::IntegerVariableProto;
58using operations_research::sat::LinearConstraintProto;
61using operations_research::sat::PartialVariableAssignment;
62using operations_research::sat::RoutesConstraintProto;
63using operations_research::sat::SatParameters;
64
65// As of 07/2019, TSPs and VRPs with homogeneous fleets of vehicles are
66// supported.
67// TODO(user): Support any type of constraints.
68// TODO(user): Make VRPs properly support optional nodes.
69bool RoutingModelCanBeSolvedBySat(const RoutingModel& model) {
70 return model.GetVehicleClassesCount() == 1;
71}
72
73// Adds an integer variable to a CpModelProto, returning its index in the proto.
74int AddVariable(CpModelProto* cp_model, int64_t lb, int64_t ub) {
75 const int index = cp_model->variables_size();
76 IntegerVariableProto* const var = cp_model->add_variables();
77 var->add_domain(lb);
78 var->add_domain(ub);
79 return index;
80}
81
82// Adds a linear constraint, enforcing
83// enforcement_literals -> lower_bound <= sum variable * coeff <= upper_bound.
84void AddLinearConstraint(
85 CpModelProto* cp_model, int64_t lower_bound, int64_t upper_bound,
86 absl::Span<const std::pair<int, double>> variable_coeffs,
87 absl::Span<const int> enforcement_literals) {
88 CHECK_LE(lower_bound, upper_bound);
89 ConstraintProto* ct = cp_model->add_constraints();
90 for (const int enforcement_literal : enforcement_literals) {
91 ct->add_enforcement_literal(enforcement_literal);
92 }
93 LinearConstraintProto* arg = ct->mutable_linear();
94 arg->add_domain(lower_bound);
95 arg->add_domain(upper_bound);
96 for (const auto [var, coeff] : variable_coeffs) {
97 arg->add_vars(var);
98 arg->add_coeffs(coeff);
99 }
100}
101
102// Adds a linear constraint, enforcing
103// lower_bound <= sum variable * coeff <= upper_bound.
104void AddLinearConstraint(
105 CpModelProto* cp_model, int64_t lower_bound, int64_t upper_bound,
106 absl::Span<const std::pair<int, double>> variable_coeffs) {
107 AddLinearConstraint(cp_model, lower_bound, upper_bound, variable_coeffs, {});
108}
109
110// Returns the unique depot node used in the CP-SAT models (as of 01/2020).
111int64_t GetDepotFromModel(const RoutingModel& model) { return model.Start(0); }
112
113// Structure to keep track of arcs created.
114struct Arc {
115 int tail;
116 int head;
117
118 friend bool operator==(const Arc& a, const Arc& b) {
119 return a.tail == b.tail && a.head == b.head;
120 }
121 friend bool operator!=(const Arc& a, const Arc& b) { return !(a == b); }
122 friend bool operator<(const Arc& a, const Arc& b) {
123 return a.tail == b.tail ? a.head < b.head : a.tail < b.tail;
124 }
125 friend std::ostream& operator<<(std::ostream& strm, const Arc& arc) {
126 return strm << "{" << arc.tail << ", " << arc.head << "}";
127 }
128 template <typename H>
129 friend H AbslHashValue(H h, const Arc& a) {
130 return H::combine(std::move(h), a.tail, a.head);
131 }
132};
133
134using ArcVarMap =
135 absl::btree_map<Arc, int>; // needs to be stable when iterating
136
137void AddSoftCumulBounds(const RoutingDimension* dimension, int index, int cumul,
138 int64_t cumul_min, int64_t cumul_max,
139 CpModelProto* cp_model) {
140 {
141 const int64_t soft_ub_coef =
142 dimension->GetCumulVarSoftUpperBoundCoefficient(index);
143 if (soft_ub_coef != 0) {
144 const int64_t soft_ub = dimension->GetCumulVarSoftUpperBound(index);
145 const int soft_ub_var =
146 AddVariable(cp_model, 0, CapSub(cumul_max, soft_ub));
147 // soft_ub_var >= cumul - soft_ub
148 AddLinearConstraint(cp_model, std::numeric_limits<int64_t>::min(),
149 soft_ub, {{cumul, 1}, {soft_ub_var, -1}});
150 cp_model->mutable_objective()->add_vars(soft_ub_var);
151 cp_model->mutable_objective()->add_coeffs(soft_ub_coef);
152 }
153 }
154 {
155 const int64_t soft_lb_coef =
156 dimension->GetCumulVarSoftLowerBoundCoefficient(index);
157 if (soft_lb_coef != 0) {
158 const int64_t soft_lb = dimension->GetCumulVarSoftLowerBound(index);
159 const int soft_lb_var =
160 AddVariable(cp_model, 0, CapSub(soft_lb, cumul_min));
161 // soft_lb_var >= soft_lb - cumul
162 AddLinearConstraint(cp_model, soft_lb,
163 std::numeric_limits<int64_t>::max(),
164 {{cumul, 1}, {soft_lb_var, 1}});
165 cp_model->mutable_objective()->add_vars(soft_lb_var);
166 cp_model->mutable_objective()->add_coeffs(soft_lb_coef);
167 }
168 }
169}
170
171// Adds all dimensions to a CpModelProto. Only adds path cumul constraints and
172// cumul bounds.
173void AddDimensions(const RoutingModel& model, const ArcVarMap& arc_vars,
174 CpModelProto* cp_model) {
175 for (const RoutingDimension* dimension : model.GetDimensions()) {
176 // Only a single vehicle class.
177 const RoutingModel::TransitCallback2& transit =
178 dimension->transit_evaluator(0);
179 std::vector<int> cumuls(dimension->cumuls().size(), -1);
180 const int64_t min_start = dimension->cumuls()[model.Start(0)]->Min();
181 const int64_t max_end = std::min(dimension->cumuls()[model.End(0)]->Max(),
182 dimension->vehicle_capacities()[0]);
183 for (int i = 0; i < cumuls.size(); ++i) {
184 if (model.IsStart(i) || model.IsEnd(i)) continue;
185 // Reducing bounds supposing the triangular inequality.
186 const int64_t cumul_min =
187 std::max(sat::kMinIntegerValue.value(),
188 std::max(dimension->cumuls()[i]->Min(),
189 CapAdd(transit(model.Start(0), i), min_start)));
190 const int64_t cumul_max =
191 std::min(sat::kMaxIntegerValue.value(),
192 std::min(dimension->cumuls()[i]->Max(),
193 CapSub(max_end, transit(i, model.End(0)))));
194 cumuls[i] = AddVariable(cp_model, cumul_min, cumul_max);
195 AddSoftCumulBounds(dimension, i, cumuls[i], cumul_min, cumul_max,
196 cp_model);
197 }
198 for (const auto arc_var : arc_vars) {
199 const int tail = arc_var.first.tail;
200 const int head = arc_var.first.head;
201 if (tail == head || model.IsStart(tail) || model.IsStart(head)) continue;
202 // arc[tail][head] -> cumuls[head] >= cumuls[tail] + transit.
203 // This is a relaxation of the model as it does not consider slack max.
204 AddLinearConstraint(
205 cp_model, transit(tail, head), std::numeric_limits<int64_t>::max(),
206 {{cumuls[head], 1}, {cumuls[tail], -1}}, {arc_var.second});
207 }
208 }
209}
210
211std::vector<int> CreateRanks(const RoutingModel& model,
212 const ArcVarMap& arc_vars,
213 CpModelProto* cp_model) {
214 const int depot = GetDepotFromModel(model);
215 const int size = model.Size() + model.vehicles();
216 const int rank_size = model.Size() - model.vehicles();
217 std::vector<int> ranks(size, -1);
218 for (int i = 0; i < size; ++i) {
219 if (model.IsStart(i) || model.IsEnd(i)) continue;
220 ranks[i] = AddVariable(cp_model, 0, rank_size);
221 }
222 ranks[depot] = AddVariable(cp_model, 0, 0);
223 for (const auto arc_var : arc_vars) {
224 const int tail = arc_var.first.tail;
225 const int head = arc_var.first.head;
226 if (tail == head || head == depot) continue;
227 // arc[tail][head] -> ranks[head] == ranks[tail] + 1.
228 AddLinearConstraint(cp_model, 1, 1, {{ranks[head], 1}, {ranks[tail], -1}},
229 {arc_var.second});
230 }
231 return ranks;
232}
233
234// Vehicle variables do not actually represent the index of the vehicle
235// performing a node, but we ensure that the values of two vehicle variables
236// are the same if and only if the corresponding nodes are served by the same
237// vehicle.
238std::vector<int> CreateVehicleVars(const RoutingModel& model,
239 const ArcVarMap& arc_vars,
240 CpModelProto* cp_model) {
241 const int depot = GetDepotFromModel(model);
242 const int size = model.Size() + model.vehicles();
243 std::vector<int> vehicles(size, -1);
244 for (int i = 0; i < size; ++i) {
245 if (model.IsStart(i) || model.IsEnd(i)) continue;
246 vehicles[i] = AddVariable(cp_model, 0, size - 1);
247 }
248 for (const auto arc_var : arc_vars) {
249 const int tail = arc_var.first.tail;
250 const int head = arc_var.first.head;
251 if (tail == head || head == depot) continue;
252 if (tail == depot) {
253 // arc[depot][head] -> vehicles[head] == head.
254 AddLinearConstraint(cp_model, head, head, {{vehicles[head], 1}},
255 {arc_var.second});
256 continue;
257 }
258 // arc[tail][head] -> vehicles[head] == vehicles[tail].
259 AddLinearConstraint(cp_model, 0, 0,
260 {{vehicles[head], 1}, {vehicles[tail], -1}},
261 {arc_var.second});
262 }
263 return vehicles;
264}
265
266void AddPickupDeliveryConstraints(const RoutingModel& model,
267 const ArcVarMap& arc_vars,
268 CpModelProto* cp_model) {
269 if (model.GetPickupAndDeliveryPairs().empty()) return;
270 const std::vector<int> ranks = CreateRanks(model, arc_vars, cp_model);
271 const std::vector<int> vehicles =
272 CreateVehicleVars(model, arc_vars, cp_model);
273 for (const auto& [pickups, deliveries] : model.GetPickupAndDeliveryPairs()) {
274 const int64_t pickup = pickups[0];
275 const int64_t delivery = deliveries[0];
276 // ranks[pickup] + 1 <= ranks[delivery].
277 AddLinearConstraint(cp_model, 1, std::numeric_limits<int64_t>::max(),
278 {{ranks[delivery], 1}, {ranks[pickup], -1}});
279 // vehicles[pickup] == vehicles[delivery]
280 AddLinearConstraint(cp_model, 0, 0,
281 {{vehicles[delivery], 1}, {vehicles[pickup], -1}});
282 }
283}
284
285// Converts a RoutingModel to CpModelProto for models with multiple vehicles.
286// All non-start/end nodes have the same index in both models. Start/end nodes
287// map to a single depot index; its value is arbitrarly the index of the start
288// node of the first vehicle in the RoutingModel.
289// The map between CPModelProto arcs and their corresponding arc variable is
290// returned.
291ArcVarMap PopulateMultiRouteModelFromRoutingModel(const RoutingModel& model,
292 CpModelProto* cp_model) {
293 ArcVarMap arc_vars;
294 const int num_nodes = model.Nexts().size();
295 const int depot = GetDepotFromModel(model);
296
297 // Create "arc" variables and set their cost.
298 for (int tail = 0; tail < num_nodes; ++tail) {
299 const int tail_index = model.IsStart(tail) ? depot : tail;
300 std::unique_ptr<IntVarIterator> iter(
301 model.NextVar(tail)->MakeDomainIterator(false));
302 for (int head : InitAndGetValues(iter.get())) {
303 // Vehicle start and end nodes are represented as a single node in the
304 // CP-SAT model. We choose the start index of the first vehicle to
305 // represent both. We can also skip any head representing a vehicle start
306 // as the CP solver will reject those.
307 if (model.IsStart(head)) continue;
308 const int head_index = model.IsEnd(head) ? depot : head;
309 if (head_index == tail_index && head_index == depot) continue;
310 const int64_t cost = tail != head ? model.GetHomogeneousCost(tail, head)
311 : model.UnperformedPenalty(tail);
312 if (cost == std::numeric_limits<int64_t>::max()) continue;
313 const Arc arc = {tail_index, head_index};
314 if (arc_vars.contains(arc)) continue;
315 const int index = AddVariable(cp_model, 0, 1);
316 gtl::InsertOrDie(&arc_vars, arc, index);
317 cp_model->mutable_objective()->add_vars(index);
318 cp_model->mutable_objective()->add_coeffs(cost);
319 }
320 }
321
322 // Limit the number of routes to the maximum number of vehicles.
323 {
324 std::vector<std::pair<int, double>> variable_coeffs;
325 for (int node = 0; node < num_nodes; ++node) {
326 if (model.IsStart(node) || model.IsEnd(node)) continue;
327 int* const var = gtl::FindOrNull(arc_vars, {depot, node});
328 if (var == nullptr) continue;
329 variable_coeffs.push_back({*var, 1});
330 }
331 AddLinearConstraint(
332 cp_model, 0,
333 std::min(model.vehicles(), model.GetMaximumNumberOfActiveVehicles()),
334 variable_coeffs);
335 }
336
337 AddPickupDeliveryConstraints(model, arc_vars, cp_model);
338
339 AddDimensions(model, arc_vars, cp_model);
340
341 // Create Routes constraint, ensuring circuits from and to the depot.
342 // This one is a bit tricky, because we need to remap the depot to zero.
343 // TODO(user): Make Routes constraints support optional nodes.
344 RoutesConstraintProto* routes_ct =
345 cp_model->add_constraints()->mutable_routes();
346 for (const auto arc_var : arc_vars) {
347 const int tail = arc_var.first.tail;
348 const int head = arc_var.first.head;
349 routes_ct->add_tails(tail == 0 ? depot : tail == depot ? 0 : tail);
350 routes_ct->add_heads(head == 0 ? depot : head == depot ? 0 : head);
351 routes_ct->add_literals(arc_var.second);
352 }
353
354 // Add demands and capacities to improve the LP relaxation and cuts. These are
355 // based on the first "unary" dimension in the model if it exists.
356 // TODO(user): We might want to try to get demand lower bounds from
357 // non-unary dimensions if no unary exist.
358 const RoutingDimension* primary_dimension = nullptr;
359 for (const RoutingDimension* dimension : model.GetDimensions()) {
360 // Only a single vehicle class is supported.
361 if (dimension->GetUnaryTransitEvaluator(0) != nullptr) {
362 primary_dimension = dimension;
363 break;
364 }
365 }
366 if (primary_dimension != nullptr) {
367 const RoutingModel::TransitCallback1& transit =
368 primary_dimension->GetUnaryTransitEvaluator(0);
369 for (int node = 0; node < num_nodes; ++node) {
370 // Tricky: demand is added for all nodes in the sat model; this means
371 // start/end nodes other than the one used for the depot must be ignored.
372 if (!model.IsEnd(node) && (!model.IsStart(node) || node == depot)) {
373 routes_ct->add_demands(transit(node));
374 }
375 }
376 DCHECK_EQ(routes_ct->demands_size(), num_nodes + 1 - model.vehicles());
377 routes_ct->set_capacity(primary_dimension->vehicle_capacities()[0]);
378 }
379 return arc_vars;
380}
381
382// Converts a RoutingModel with a single vehicle to a CpModelProto.
383// The mapping between CPModelProto arcs and their corresponding arc variables
384// is returned.
385ArcVarMap PopulateSingleRouteModelFromRoutingModel(const RoutingModel& model,
386 CpModelProto* cp_model) {
387 ArcVarMap arc_vars;
388 const int num_nodes = model.Nexts().size();
389 CircuitConstraintProto* circuit =
390 cp_model->add_constraints()->mutable_circuit();
391 for (int tail = 0; tail < num_nodes; ++tail) {
392 std::unique_ptr<IntVarIterator> iter(
393 model.NextVar(tail)->MakeDomainIterator(false));
394 for (int head : InitAndGetValues(iter.get())) {
395 // Vehicle start and end nodes are represented as a single node in the
396 // CP-SAT model. We choose the start index to represent both. We can also
397 // skip any head representing a vehicle start as the CP solver will reject
398 // those.
399 if (model.IsStart(head)) continue;
400 if (model.IsEnd(head)) head = model.Start(0);
401 const int64_t cost = tail != head ? model.GetHomogeneousCost(tail, head)
402 : model.UnperformedPenalty(tail);
403 if (cost == std::numeric_limits<int64_t>::max()) continue;
404 const int index = AddVariable(cp_model, 0, 1);
405 circuit->add_literals(index);
406 circuit->add_tails(tail);
407 circuit->add_heads(head);
408 cp_model->mutable_objective()->add_vars(index);
409 cp_model->mutable_objective()->add_coeffs(cost);
410 gtl::InsertOrDie(&arc_vars, {tail, head}, index);
411 }
412 }
413 AddPickupDeliveryConstraints(model, arc_vars, cp_model);
414 AddDimensions(model, arc_vars, cp_model);
415 return arc_vars;
416}
417
418// Converts a RoutingModel to a CpModelProto.
419// The mapping between CPModelProto arcs and their corresponding arc variables
420// is returned.
421ArcVarMap PopulateModelFromRoutingModel(const RoutingModel& model,
422 CpModelProto* cp_model) {
423 if (model.vehicles() == 1) {
424 return PopulateSingleRouteModelFromRoutingModel(model, cp_model);
425 }
426 return PopulateMultiRouteModelFromRoutingModel(model, cp_model);
427}
428
429void ConvertObjectiveToSolution(const CpSolverResponse& response,
430 const CpObjectiveProto& objective,
431 const RoutingModel& model,
433 if (response.status() == CpSolverStatus::OPTIMAL) {
434 // If the solution was proven optimal by CP-SAT, add the objective value to
435 // the solution; it will be a proper lower bound of the routing objective.
436 // Recomputing the objective value to avoid rounding errors due to scaling.
437 // Note: We could use inner_objective_lower_bound if we were sure
438 // absolute_gap_limit was 0 (which is not guaranteed).
439 int64_t cost_value = 0;
440 for (int i = 0; i < objective.coeffs_size(); ++i) {
441 cost_value = CapAdd(
442 cost_value,
443 CapProd(objective.coeffs(i), response.solution(objective.vars(i))));
444 }
445 solution->AddObjective(model.CostVar());
446 solution->SetObjectiveValue(cost_value);
447 } else if (response.status() == CpSolverStatus::FEASIBLE) {
448 // If the solution is feasible only, add the lower bound of the objective to
449 // the solution; it will be a proper lower bound of the routing objective.
450 solution->AddObjective(model.CostVar());
451 solution->SetObjectiveValue(response.inner_objective_lower_bound());
452 }
453}
454
455// Converts a CpSolverResponse to an Assignment containing next variables.
456bool ConvertToSolution(const CpSolverResponse& response,
457 const CpObjectiveProto& objective,
458 const RoutingModel& model, const ArcVarMap& arc_vars,
460 solution->Clear();
461 if (response.status() != CpSolverStatus::OPTIMAL &&
462 response.status() != CpSolverStatus::FEASIBLE) {
463 return false;
464 }
465 const int depot = GetDepotFromModel(model);
466 int vehicle = 0;
467 for (const auto& arc_var : arc_vars) {
468 if (response.solution(arc_var.second) != 0) {
469 const int tail = arc_var.first.tail;
470 const int head = arc_var.first.head;
471 if (head == depot) continue;
472 if (tail != depot) {
473 solution->Add(model.NextVar(tail))->SetValue(head);
474 } else {
475 solution->Add(model.NextVar(model.Start(vehicle)))->SetValue(head);
476 ++vehicle;
477 }
478 }
479 }
480 // Close open routes.
481 for (int v = 0; v < model.vehicles(); ++v) {
482 int current = model.Start(v);
483 while (!model.IsEnd(current) &&
484 solution->Contains(model.NextVar(current))) {
485 current = solution->Value(model.NextVar(current));
486 }
487 if (model.IsEnd(current)) continue;
488 solution->Add(model.NextVar(current))->SetValue(model.End(v));
489 }
490 ConvertObjectiveToSolution(response, objective, model, solution);
491 return true;
492}
493
494// Adds dimensions to a CpModelProto for heterogeneous fleet. Adds path
495// cumul constraints and cumul bounds.
496void AddGeneralizedDimensions(
497 const RoutingModel& model, const ArcVarMap& arc_vars,
498 absl::Span<const absl::flat_hash_map<int, int>> vehicle_performs_node,
499 absl::Span<const absl::flat_hash_map<int, int>> vehicle_class_performs_arc,
500 CpModelProto* cp_model) {
501 const int num_cp_nodes = model.Nexts().size() + model.vehicles() + 1;
502 for (const RoutingDimension* dimension : model.GetDimensions()) {
503 // Initialize cumuls.
504 std::vector<int> cumuls(num_cp_nodes, -1);
505 for (int cp_node = 1; cp_node < num_cp_nodes; ++cp_node) {
506 const int node = cp_node - 1;
507 int64_t cumul_min = dimension->cumuls()[node]->Min();
508 int64_t cumul_max = dimension->cumuls()[node]->Max();
509 if (model.IsStart(node) || model.IsEnd(node)) {
510 const int vehicle = model.VehicleIndex(node);
511 cumul_max =
512 std::min(cumul_max, dimension->vehicle_capacities()[vehicle]);
513 }
514 cumuls[cp_node] = AddVariable(cp_model, cumul_min, cumul_max);
515 AddSoftCumulBounds(dimension, node, cumuls[cp_node], cumul_min, cumul_max,
516 cp_model);
517 }
518
519 // Constrain cumuls with vehicle capacities.
520 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
521 for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
522 if (!vehicle_performs_node[vehicle].contains(cp_node)) continue;
523 const int64_t vehicle_capacity =
524 dimension->vehicle_capacities()[vehicle];
525 AddLinearConstraint(cp_model, std::numeric_limits<int64_t>::min(),
526 vehicle_capacity, {{cumuls[cp_node], 1}},
527 {vehicle_performs_node[vehicle].at(cp_node)});
528 }
529 }
530
531 for (auto vehicle_class = RoutingVehicleClassIndex(0);
532 vehicle_class < model.GetVehicleClassesCount(); vehicle_class++) {
533 std::vector<int> slack(num_cp_nodes, -1);
534 const int64_t slack_cost = CapAdd(
535 dimension->GetSpanCostCoefficientForVehicleClass(vehicle_class),
536 dimension->GetSlackCostCoefficientForVehicleClass(vehicle_class));
537 for (const auto [arc, arc_var] : arc_vars) {
538 const auto [cp_tail, cp_head] = arc;
539 if (cp_tail == cp_head || cp_tail == 0 || cp_head == 0) continue;
540 if (!vehicle_class_performs_arc[vehicle_class.value()].contains(
541 arc_var)) {
542 continue;
543 }
544 // Create slack variable and add span cost to the objective.
545 if (slack[cp_tail] == -1) {
546 const int64_t slack_max =
547 cp_tail - 1 < dimension->slacks().size()
548 ? dimension->slacks()[cp_tail - 1]->Max()
549 : 0;
550 slack[cp_tail] = AddVariable(cp_model, 0, slack_max);
551 if (slack_max > 0 && slack_cost > 0) {
552 cp_model->mutable_objective()->add_vars(slack[cp_tail]);
553 cp_model->mutable_objective()->add_coeffs(slack_cost);
554 }
555 }
556 const int64_t transit = dimension->class_transit_evaluator(
557 vehicle_class)(cp_tail - 1, cp_head - 1);
558 // vehicle_class_performs_arc[vehicle][arc_var] = 1 ->
559 // cumuls[cp_head] - cumuls[cp_tail] - slack[cp_tail] = transit
560 AddLinearConstraint(
561 cp_model, transit, transit,
562 {{cumuls[cp_head], 1}, {cumuls[cp_tail], -1}, {slack[cp_tail], -1}},
563 {vehicle_class_performs_arc[vehicle_class.value()].at(arc_var)});
564 }
565 }
566
567 // Constrain cumuls with span limits.
568 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
569 const int64_t span_limit =
570 dimension->vehicle_span_upper_bounds()[vehicle];
571 if (span_limit == std::numeric_limits<int64_t>::max()) continue;
572 int cp_start = model.Start(vehicle) + 1;
573 int cp_end = model.End(vehicle) + 1;
574 AddLinearConstraint(cp_model, std::numeric_limits<int64_t>::min(),
575 span_limit,
576 {{cumuls[cp_end], 1}, {cumuls[cp_start], -1}});
577 }
578
579 // Set soft span upper bound costs.
580 if (dimension->HasSoftSpanUpperBounds()) {
581 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
582 const auto [bound, cost] =
583 dimension->GetSoftSpanUpperBoundForVehicle(vehicle);
584 const int cp_start = model.Start(vehicle) + 1;
585 const int cp_end = model.End(vehicle) + 1;
586 const int extra =
587 AddVariable(cp_model, 0,
588 std::min(dimension->cumuls()[model.End(vehicle)]->Max(),
589 dimension->vehicle_capacities()[vehicle]));
590 // -inf <= cumuls[cp_end] - cumuls[cp_start] - extra <= bound
591 AddLinearConstraint(
592 cp_model, std::numeric_limits<int64_t>::min(), bound,
593 {{cumuls[cp_end], 1}, {cumuls[cp_start], -1}, {extra, -1}});
594 // Add extra * cost to objective.
595 cp_model->mutable_objective()->add_vars(extra);
596 cp_model->mutable_objective()->add_coeffs(cost);
597 }
598 }
599 }
600}
601
602std::vector<int> CreateGeneralizedRanks(const RoutingModel& model,
603 const ArcVarMap& arc_vars,
604 absl::Span<const int> is_unperformed,
605 CpModelProto* cp_model) {
606 const int depot = 0;
607 const int num_cp_nodes = model.Nexts().size() + model.vehicles() + 1;
608 // Maximum length of a single route (excluding the depot & vehicle end nodes).
609 const int max_rank = num_cp_nodes - 2 * model.vehicles();
610 std::vector<int> ranks(num_cp_nodes, -1);
611 ranks[depot] = AddVariable(cp_model, 0, 0);
612 for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
613 if (model.IsEnd(cp_node - 1)) continue;
614 ranks[cp_node] = AddVariable(cp_model, 0, max_rank);
615 // For unperformed nodes rank is 0.
616 AddLinearConstraint(cp_model, 0, 0, {{ranks[cp_node], 1}},
617 {is_unperformed[cp_node]});
618 }
619 for (const auto [arc, arc_var] : arc_vars) {
620 const auto [cp_tail, cp_head] = arc;
621 if (cp_head == 0 || model.IsEnd(cp_head - 1)) continue;
622 if (cp_tail == cp_head || cp_head == depot) continue;
623 // arc[tail][head] -> ranks[head] == ranks[tail] + 1.
624 AddLinearConstraint(cp_model, 1, 1,
625 {{ranks[cp_head], 1}, {ranks[cp_tail], -1}}, {arc_var});
626 }
627 return ranks;
628}
629
630void AddGeneralizedPickupDeliveryConstraints(
631 const RoutingModel& model, const ArcVarMap& arc_vars,
632 absl::Span<const absl::flat_hash_map<int, int>> vehicle_performs_node,
633 absl::Span<const int> is_unperformed, CpModelProto* cp_model) {
634 if (model.GetPickupAndDeliveryPairs().empty()) return;
635 const std::vector<int> ranks =
636 CreateGeneralizedRanks(model, arc_vars, is_unperformed, cp_model);
637 for (const auto& [pickups, deliveries] : model.GetPickupAndDeliveryPairs()) {
638 for (const int delivery : deliveries) {
639 const int cp_delivery = delivery + 1;
640 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
641 const Arc vehicle_start_delivery_arc = {
642 static_cast<int>(model.Start(vehicle) + 1), cp_delivery};
643 if (arc_vars.contains(vehicle_start_delivery_arc)) {
644 // Forbid vehicle_start -> delivery arc.
645 AddLinearConstraint(cp_model, 0, 0,
646 {{arc_vars.at(vehicle_start_delivery_arc), 1}});
647 }
648 }
649
650 for (const int pickup : pickups) {
651 const int cp_pickup = pickup + 1;
652 const Arc delivery_pickup_arc = {cp_delivery, cp_pickup};
653 if (arc_vars.contains(delivery_pickup_arc)) {
654 // Forbid delivery -> pickup arc.
655 AddLinearConstraint(cp_model, 0, 0,
656 {{arc_vars.at(delivery_pickup_arc), 1}});
657 }
658
659 DCHECK_GE(is_unperformed[cp_delivery], 0);
660 DCHECK_GE(is_unperformed[cp_pickup], 0);
661 // A negative index i refers to NOT the literal at index -i - 1.
662 // -i - 1 ~ NOT i, if value of i in [0, 1] (boolean).
663 const int delivery_performed = -is_unperformed[cp_delivery] - 1;
664 const int pickup_performed = -is_unperformed[cp_pickup] - 1;
665 // The same vehicle performs pickup and delivery.
666 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
667 // delivery_performed & pickup_performed ->
668 // vehicle_performs_node[vehicle][cp_delivery] -
669 // vehicle_performs_node[vehicle][cp_pickup] = 0
670 AddLinearConstraint(
671 cp_model, 0, 0,
672 {{vehicle_performs_node[vehicle].at(cp_delivery), 1},
673 {vehicle_performs_node[vehicle].at(cp_pickup), -1}},
674 {delivery_performed, pickup_performed});
675 }
676 }
677 }
678
679 std::vector<std::pair<int, double>> ranks_difference;
680 // -SUM(pickup)ranks[pickup].
681 for (const int pickup : pickups) {
682 const int cp_pickup = pickup + 1;
683 ranks_difference.push_back({ranks[cp_pickup], -1});
684 }
685 // SUM(delivery)ranks[delivery].
686 for (const int delivery : deliveries) {
687 const int cp_delivery = delivery + 1;
688 ranks_difference.push_back({ranks[cp_delivery], 1});
689 }
690 // SUM(delivery)ranks[delivery] - SUM(pickup)ranks[pickup] >= 1
691 AddLinearConstraint(cp_model, 1, std::numeric_limits<int64_t>::max(),
692 ranks_difference);
693 }
694}
695
696// Converts a RoutingModel to CpModelProto for models with multiple
697// vehicles. The node 0 is depot. All nodes in CpModel have index increased
698// by 1 in comparison to the RoutingModel. Each start node has only 1
699// incoming arc (from depot), each end node has only 1 outgoing arc (to
700// depot). The mapping from CPModelProto arcs to their corresponding arc
701// variable is returned.
702ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel(
703 const RoutingModel& model, CpModelProto* cp_model) {
704 ArcVarMap arc_vars;
705 const int depot = 0;
706 const int num_nodes = model.Nexts().size();
707 const int num_cp_nodes = num_nodes + model.vehicles() + 1;
708 // vehicle_performs_node[vehicle][node] equals to 1 if the vehicle performs
709 // the node, and 0 otherwise.
710 std::vector<absl::flat_hash_map<int, int>> vehicle_performs_node(
711 model.vehicles());
712 // Connect vehicles start and end nodes to depot.
713 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
714 const int cp_start = model.Start(vehicle) + 1;
715 const Arc start_arc = {depot, cp_start};
716 const int start_arc_var = AddVariable(cp_model, 1, 1);
717 DCHECK(!arc_vars.contains(start_arc));
718 arc_vars.insert({start_arc, start_arc_var});
719
720 const int cp_end = model.End(vehicle) + 1;
721 const Arc end_arc = {cp_end, depot};
722 const int end_arc_var = AddVariable(cp_model, 1, 1);
723 DCHECK(!arc_vars.contains(end_arc));
724 arc_vars.insert({end_arc, end_arc_var});
725
726 vehicle_performs_node[vehicle][cp_start] = start_arc_var;
727 vehicle_performs_node[vehicle][cp_end] = end_arc_var;
728 }
729
730 // is_unperformed[node] variable equals to 1 if visit is unperformed, and 0
731 // otherwise.
732 std::vector<int> is_unperformed(num_cp_nodes, -1);
733 // Initialize is_unperformed variables for nodes that must be performed.
734 for (int node = 0; node < num_nodes; node++) {
735 const int cp_node = node + 1;
736 // Forced active and nodes that are not involved in any disjunctions are
737 // always performed.
738 const std::vector<RoutingDisjunctionIndex>& disjunction_indices =
739 model.GetDisjunctionIndices(node);
740 if (disjunction_indices.empty() || model.ActiveVar(node)->Min() == 1) {
741 is_unperformed[cp_node] = AddVariable(cp_model, 0, 0);
742 continue;
743 }
744 // Check if the node is in a forced active disjunction.
745 for (RoutingDisjunctionIndex disjunction_index : disjunction_indices) {
746 const int num_nodes =
747 model.GetDisjunctionNodeIndices(disjunction_index).size();
748 const int64_t penalty = model.GetDisjunctionPenalty(disjunction_index);
749 const int64_t max_cardinality =
750 model.GetDisjunctionMaxCardinality(disjunction_index);
751 if (num_nodes == max_cardinality &&
752 (penalty < 0 || penalty == std::numeric_limits<int64_t>::max())) {
753 // Nodes in this disjunction are forced active.
754 is_unperformed[cp_node] = AddVariable(cp_model, 0, 0);
755 break;
756 }
757 }
758 }
759 // Add alternative visits. Create self-looped arc variables. Set penalty for
760 // not performing disjunctions.
761 for (RoutingDisjunctionIndex disjunction_index(0);
762 disjunction_index < model.GetNumberOfDisjunctions();
763 disjunction_index++) {
764 const std::vector<int64_t>& disjunction_indices =
765 model.GetDisjunctionNodeIndices(disjunction_index);
766 const int disjunction_size = disjunction_indices.size();
767 const int64_t penalty = model.GetDisjunctionPenalty(disjunction_index);
768 const int64_t max_cardinality =
769 model.GetDisjunctionMaxCardinality(disjunction_index);
770 // Case when disjunction involves only 1 node, the node is only present in
771 // this disjunction, and the node can be unperformed.
772 if (disjunction_size == 1 &&
773 model.GetDisjunctionIndices(disjunction_indices[0]).size() == 1 &&
774 is_unperformed[disjunction_indices[0] + 1] == -1) {
775 const int cp_node = disjunction_indices[0] + 1;
776 const Arc arc = {cp_node, cp_node};
777 DCHECK(!arc_vars.contains(arc));
778 is_unperformed[cp_node] = AddVariable(cp_model, 0, 1);
779 arc_vars.insert({arc, is_unperformed[cp_node]});
780 cp_model->mutable_objective()->add_vars(is_unperformed[cp_node]);
781 cp_model->mutable_objective()->add_coeffs(penalty);
782 continue;
783 }
784 // num_performed + SUM(node)is_unperformed[node] = disjunction_size
785 const int num_performed = AddVariable(cp_model, 0, max_cardinality);
786 std::vector<std::pair<int, double>> var_coeffs;
787 var_coeffs.push_back({num_performed, 1});
788 for (const int node : disjunction_indices) {
789 const int cp_node = node + 1;
790 // Node can be unperformed.
791 if (is_unperformed[cp_node] == -1) {
792 const Arc arc = {cp_node, cp_node};
793 DCHECK(!arc_vars.contains(arc));
794 is_unperformed[cp_node] = AddVariable(cp_model, 0, 1);
795 arc_vars.insert({arc, is_unperformed[cp_node]});
796 }
797 var_coeffs.push_back({is_unperformed[cp_node], 1});
798 }
799 AddLinearConstraint(cp_model, disjunction_size, disjunction_size,
800 var_coeffs);
801 // When penalty is negative or max int64_t (forced active), num_violated is
802 // 0.
803 if (penalty < 0 || penalty == std::numeric_limits<int64_t>::max()) {
804 AddLinearConstraint(cp_model, max_cardinality, max_cardinality,
805 {{num_performed, 1}});
806 continue;
807 }
808 // If number of active indices is less than max_cardinality, then for each
809 // violated index 'penalty' is paid.
810 const int num_violated = AddVariable(cp_model, 0, max_cardinality);
811 cp_model->mutable_objective()->add_vars(num_violated);
812 cp_model->mutable_objective()->add_coeffs(penalty);
813 // num_performed + num_violated = max_cardinality
814 AddLinearConstraint(cp_model, max_cardinality, max_cardinality,
815 {{num_performed, 1}, {num_violated, 1}});
816 }
817 // Create "arc" variables.
818 for (int tail = 0; tail < num_nodes; ++tail) {
819 const int cp_tail = tail + 1;
820 std::unique_ptr<IntVarIterator> iter(
821 model.NextVar(tail)->MakeDomainIterator(false));
822 for (int head : InitAndGetValues(iter.get())) {
823 const int cp_head = head + 1;
824 if (model.IsStart(head)) continue;
825 // Arcs for unperformed visits have already been created.
826 if (tail == head) continue;
827 // Direct arcs from start to end nodes should exist only if they are
828 // for the same vehicle.
829 if (model.IsStart(tail) && model.IsEnd(head) &&
830 model.VehicleIndex(tail) != model.VehicleIndex(head)) {
831 continue;
832 }
833
834 bool feasible = false;
835 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
836 if (model.GetArcCostForVehicle(tail, head, vehicle) !=
837 std::numeric_limits<int64_t>::max()) {
838 feasible = true;
839 break;
840 }
841 }
842 if (!feasible) continue;
843
844 const Arc arc = {cp_tail, cp_head};
845 DCHECK(!arc_vars.contains(arc));
846 const int arc_var = AddVariable(cp_model, 0, 1);
847 arc_vars.insert({arc, arc_var});
848 }
849 }
850
851 // Set literals for vehicle performing node.
852 for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
853 const int routing_index = cp_node - 1;
854 // For starts and ends nodes vehicle_performs_node variables already set.
855 if (model.IsStart(routing_index) || model.IsEnd(routing_index)) continue;
856 // Each node should be performed by 1 vehicle, or be unperformed.
857 // SUM(vehicle)(vehicle_performs_node[vehicle][cp_node]) + loop(cp_node) = 1
858 std::vector<std::pair<int, double>> var_coeffs;
859 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
860 vehicle_performs_node[vehicle][cp_node] =
861 model.VehicleVar(routing_index)->Contains(vehicle)
862 ? AddVariable(cp_model, 0, 1)
863 : AddVariable(cp_model, 0, 0);
864 var_coeffs.push_back({vehicle_performs_node[vehicle][cp_node], 1});
865 }
866 var_coeffs.push_back({is_unperformed[cp_node], 1});
867 AddLinearConstraint(cp_model, 1, 1, var_coeffs);
868 }
869 const int num_vehicle_classes = model.GetVehicleClassesCount();
870 // vehicle_class_performs_node[vehicle_class][node] equals to 1 if the
871 // vehicle of vehicle_class performs the node, and 0 otherwise.
872 std::vector<absl::flat_hash_map<int, int>> vehicle_class_performs_node(
873 num_vehicle_classes);
874 for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
875 const int node = cp_node - 1;
876 for (int vehicle_class = 0; vehicle_class < num_vehicle_classes;
877 vehicle_class++) {
878 if (model.IsStart(node) || model.IsEnd(node)) {
879 const int vehicle = model.VehicleIndex(node);
880 vehicle_class_performs_node[vehicle_class][cp_node] =
881 vehicle_class ==
882 model.GetVehicleClassIndexOfVehicle(vehicle).value()
883 ? AddVariable(cp_model, 1, 1)
884 : AddVariable(cp_model, 0, 0);
885 continue;
886 }
887 vehicle_class_performs_node[vehicle_class][cp_node] =
888 AddVariable(cp_model, 0, 1);
889 std::vector<std::pair<int, double>> var_coeffs;
890 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
891 if (model.GetVehicleClassIndexOfVehicle(vehicle).value() ==
892 vehicle_class) {
893 var_coeffs.push_back({vehicle_performs_node[vehicle][cp_node], 1});
894 // vehicle_performs_node -> vehicle_class_performs_node
895 AddLinearConstraint(
896 cp_model, 1, 1,
897 {{vehicle_class_performs_node[vehicle_class][cp_node], 1}},
898 {vehicle_performs_node[vehicle][cp_node]});
899 }
900 }
901 // vehicle_class_performs_node -> exactly one vehicle from this class
902 // performs node.
903 AddLinearConstraint(
904 cp_model, 1, 1, var_coeffs,
905 {vehicle_class_performs_node[vehicle_class][cp_node]});
906 }
907 }
908 // vehicle_class_performs_arc[vehicle_class][arc_var] equals to 1 if the
909 // vehicle of vehicle_class performs the arc, and 0 otherwise.
910 std::vector<absl::flat_hash_map<int, int>> vehicle_class_performs_arc(
911 num_vehicle_classes);
912 // Set "arc" costs.
913 for (const auto [arc, arc_var] : arc_vars) {
914 const auto [cp_tail, cp_head] = arc;
915 if (cp_tail == depot || cp_head == depot) continue;
916 const int tail = cp_tail - 1;
917 const int head = cp_head - 1;
918 // Costs for unperformed arcs have already been set.
919 if (tail == head) continue;
920 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
921 // The arc can't be performed by the vehicle when vehicle can't perform
922 // arc nodes.
923 if (!vehicle_performs_node[vehicle].contains(cp_tail) ||
924 !vehicle_performs_node[vehicle].contains(cp_head)) {
925 continue;
926 }
927 int64_t cost = model.GetArcCostForVehicle(tail, head, vehicle);
928 // Arcs with int64_t's max cost are infeasible.
929 if (cost == std::numeric_limits<int64_t>::max()) continue;
930 const int vehicle_class =
931 model.GetVehicleClassIndexOfVehicle(vehicle).value();
932 if (!vehicle_class_performs_arc[vehicle_class].contains(arc_var)) {
933 vehicle_class_performs_arc[vehicle_class][arc_var] =
934 AddVariable(cp_model, 0, 1);
935 // Create constraints to set vehicle_class_performs_arc.
936 // vehicle_class_performs_arc ->
937 // vehicle_class_performs_tail & vehicle_class_performs_head &
938 // arc_is_performed
939 ConstraintProto* ct = cp_model->add_constraints();
940 ct->add_enforcement_literal(
941 vehicle_class_performs_arc[vehicle_class][arc_var]);
942 BoolArgumentProto* bool_and = ct->mutable_bool_and();
943 bool_and->add_literals(
944 vehicle_class_performs_node[vehicle_class][cp_tail]);
945 bool_and->add_literals(
946 vehicle_class_performs_node[vehicle_class][cp_head]);
947 bool_and->add_literals(arc_var);
948 // Don't add arcs with zero cost to the objective.
949 if (cost != 0) {
950 cp_model->mutable_objective()->add_vars(
951 vehicle_class_performs_arc[vehicle_class][arc_var]);
952 cp_model->mutable_objective()->add_coeffs(cost);
953 }
954 }
955 // (arc_is_performed & vehicle_performs_tail) ->
956 // (vehicle_class_performs_arc & vehicle_performs_head)
957 ConstraintProto* ct_arc_tail = cp_model->add_constraints();
958 ct_arc_tail->add_enforcement_literal(arc_var);
959 ct_arc_tail->add_enforcement_literal(
960 vehicle_performs_node[vehicle][cp_tail]);
961 ct_arc_tail->mutable_bool_and()->add_literals(
962 vehicle_class_performs_arc[vehicle_class][arc_var]);
963 ct_arc_tail->mutable_bool_and()->add_literals(
964 vehicle_performs_node[vehicle][cp_head]);
965 // (arc_is_performed & vehicle_performs_head) ->
966 // (vehicle_class_performs_arc & vehicle_performs_tail)
967 ConstraintProto* ct_arc_head = cp_model->add_constraints();
968 ct_arc_head->add_enforcement_literal(arc_var);
969 ct_arc_head->add_enforcement_literal(
970 vehicle_performs_node[vehicle][cp_head]);
971 ct_arc_head->mutable_bool_and()->add_literals(
972 vehicle_class_performs_arc[vehicle_class][arc_var]);
973 ct_arc_head->mutable_bool_and()->add_literals(
974 vehicle_performs_node[vehicle][cp_tail]);
975 }
976 }
977
978 AddGeneralizedPickupDeliveryConstraints(
979 model, arc_vars, vehicle_performs_node, is_unperformed, cp_model);
980
981 AddGeneralizedDimensions(model, arc_vars, vehicle_performs_node,
982 vehicle_class_performs_arc, cp_model);
983
984 // Create Routes constraint, ensuring circuits from and to the depot.
985 RoutesConstraintProto* routes_ct =
986 cp_model->add_constraints()->mutable_routes();
987 for (const auto [arc, arc_var] : arc_vars) {
988 const int tail = arc.tail;
989 const int head = arc.head;
990 routes_ct->add_tails(tail);
991 routes_ct->add_heads(head);
992 routes_ct->add_literals(arc_var);
993 }
994
995 // Add demands and capacities to improve the LP relaxation and cuts. These
996 // are based on the first "unary" dimension in the model if it exists.
997 // TODO(user): We might want to try to get demand lower bounds from
998 // non-unary dimensions if no unary exist.
999 const RoutingDimension* primary_dimension = nullptr;
1000 for (const RoutingDimension* dimension : model.GetDimensions()) {
1001 bool is_unary = true;
1002 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
1003 if (dimension->GetUnaryTransitEvaluator(vehicle) == nullptr) {
1004 is_unary = false;
1005 break;
1006 }
1007 }
1008 if (is_unary) {
1009 primary_dimension = dimension;
1010 break;
1011 }
1012 }
1013 if (primary_dimension != nullptr) {
1014 for (int cp_node = 0; cp_node < num_cp_nodes; ++cp_node) {
1015 int64_t min_transit = std::numeric_limits<int64_t>::max();
1016 if (cp_node != 0 && !model.IsEnd(cp_node - 1)) {
1017 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
1018 const RoutingModel::TransitCallback1& transit =
1019 primary_dimension->GetUnaryTransitEvaluator(vehicle);
1020 min_transit = std::min(min_transit, transit(cp_node - 1));
1021 }
1022 } else {
1023 min_transit = 0;
1024 }
1025 routes_ct->add_demands(min_transit);
1026 }
1027 DCHECK_EQ(routes_ct->demands_size(), num_cp_nodes);
1028 int64_t max_capacity = std::numeric_limits<int64_t>::min();
1029 for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
1030 max_capacity = std::max(max_capacity,
1031 primary_dimension->vehicle_capacities()[vehicle]);
1032 }
1033 routes_ct->set_capacity(max_capacity);
1034 }
1035 return arc_vars;
1036}
1037
1038// Converts a CpSolverResponse to an Assignment containing next variables.
1039bool ConvertGeneralizedResponseToSolution(const CpSolverResponse& response,
1040 const CpObjectiveProto& objective,
1041 const RoutingModel& model,
1042 const ArcVarMap& arc_vars,
1044 solution->Clear();
1045 if (response.status() != CpSolverStatus::OPTIMAL &&
1046 response.status() != CpSolverStatus::FEASIBLE) {
1047 return false;
1048 }
1049 const int depot = 0;
1050 for (const auto [arc, arc_var] : arc_vars) {
1051 if (response.solution(arc_var) == 0) continue;
1052 const auto [tail, head] = arc;
1053 if (head == depot || tail == depot) continue;
1054 solution->Add(model.NextVar(tail - 1))->SetValue(head - 1);
1055 }
1056 ConvertObjectiveToSolution(response, objective, model, solution);
1057 return true;
1058}
1059
1060// Uses CP solution as hint for CP-SAT.
1061void AddSolutionAsHintToGeneralizedModel(const Assignment* solution,
1062 const RoutingModel& model,
1063 const ArcVarMap& arc_vars,
1064 CpModelProto* cp_model) {
1065 if (solution == nullptr) return;
1066 PartialVariableAssignment* const hint = cp_model->mutable_solution_hint();
1067 hint->Clear();
1068 const int num_nodes = model.Nexts().size();
1069 for (int tail = 0; tail < num_nodes; ++tail) {
1070 const int cp_tail = tail + 1;
1071 const int cp_head = solution->Value(model.NextVar(tail)) + 1;
1072 const int* const arc_var = gtl::FindOrNull(arc_vars, {cp_tail, cp_head});
1073 // Arcs with a cost of max int64_t are not added to the model (considered as
1074 // infeasible). In some rare cases CP solutions might contain such arcs in
1075 // which case they are skipped here and a partial solution is used as a
1076 // hint.
1077 if (arc_var == nullptr) continue;
1078 hint->add_vars(*arc_var);
1079 hint->add_values(1);
1080 }
1081}
1082
1083void AddSolutionAsHintToModel(const Assignment* solution,
1084 const RoutingModel& model,
1085 const ArcVarMap& arc_vars,
1086 CpModelProto* cp_model) {
1087 if (solution == nullptr) return;
1088 PartialVariableAssignment* const hint = cp_model->mutable_solution_hint();
1089 hint->Clear();
1090 const int depot = GetDepotFromModel(model);
1091 const int num_nodes = model.Nexts().size();
1092 for (int tail = 0; tail < num_nodes; ++tail) {
1093 const int tail_index = model.IsStart(tail) ? depot : tail;
1094 const int head = solution->Value(model.NextVar(tail));
1095 const int head_index = model.IsEnd(head) ? depot : head;
1096 if (tail_index == depot && head_index == depot) continue;
1097 const int* const var_index =
1098 gtl::FindOrNull(arc_vars, {tail_index, head_index});
1099 // Arcs with a cost of kint64max are not added to the model (considered as
1100 // infeasible). In some rare cases CP solutions might contain such arcs in
1101 // which case they are skipped here and a partial solution is used as a
1102 // hint.
1103 if (var_index == nullptr) continue;
1104 hint->add_vars(*var_index);
1105 hint->add_values(1);
1106 }
1107}
1108
1109// Configures a CP-SAT solver and solves the given (routing) model using it.
1110// Returns the response of the search.
1111CpSolverResponse SolveRoutingModel(
1112 const CpModelProto& cp_model, absl::Duration remaining_time,
1113 std::atomic<bool>* interrupt_solve,
1114 const RoutingSearchParameters& search_parameters,
1115 const std::function<void(const CpSolverResponse& response)>& observer) {
1116 // Copying to set remaining time.
1117 SatParameters sat_parameters = search_parameters.sat_parameters();
1118 if (!sat_parameters.has_max_time_in_seconds()) {
1119 sat_parameters.set_max_time_in_seconds(
1120 absl::ToDoubleSeconds(remaining_time));
1121 } else {
1122 sat_parameters.set_max_time_in_seconds(
1123 std::min(absl::ToDoubleSeconds(remaining_time),
1124 sat_parameters.max_time_in_seconds()));
1125 }
1126 Model model;
1127 model.Add(NewSatParameters(sat_parameters));
1128 model.GetOrCreate<TimeLimit>()->RegisterExternalBooleanAsLimit(
1129 interrupt_solve);
1130 if (observer != nullptr) {
1131 model.Add(NewFeasibleSolutionObserver(observer));
1132 }
1133 // TODO(user): Add an option to dump the CP-SAT model or check if the
1134 // cp_model_dump_file flag in cp_model_solver.cc is good enough.
1135 return SolveCpModel(cp_model, &model);
1136}
1137
1138// Check if all the nodes are present in arcs. Otherwise, CP-SAT solver may
1139// fail.
1140bool IsFeasibleArcVarMap(const ArcVarMap& arc_vars, int max_node_index) {
1141 Bitset64<> present_in_arcs(max_node_index + 1);
1142 for (const auto [arc, _] : arc_vars) {
1143 present_in_arcs.Set(arc.head);
1144 present_in_arcs.Set(arc.tail);
1145 }
1146 for (int i = 0; i <= max_node_index; i++) {
1147 if (!present_in_arcs[i]) return false;
1148 }
1149 return true;
1150}
1151
1152} // namespace
1153} // namespace sat
1154
1155// Solves a RoutingModel using the CP-SAT solver. Returns false if no solution
1156// was found.
1157bool SolveModelWithSat(RoutingModel* model,
1158 const RoutingSearchParameters& search_parameters,
1159 const Assignment* initial_solution,
1161 const absl::Duration remaining_time = model->RemainingTime();
1162 const absl::Time deadline = model->solver()->Now() + remaining_time;
1163 sat::CpModelProto cp_model;
1164 cp_model.mutable_objective()->set_scaling_factor(
1165 search_parameters.log_cost_scaling_factor());
1166 cp_model.mutable_objective()->set_offset(search_parameters.log_cost_offset());
1167 const sat::CpObjectiveProto& objective = cp_model.objective();
1168 const std::function<void(const sat::CpSolverResponse& response)>
1169 null_observer;
1170 if (search_parameters.use_generalized_cp_sat() == BOOL_TRUE) {
1171 const sat::ArcVarMap arc_vars =
1172 sat::PopulateGeneralizedRouteModelFromRoutingModel(*model, &cp_model);
1173 const int max_node_index = model->Nexts().size() + model->vehicles();
1174 if (!sat::IsFeasibleArcVarMap(arc_vars, max_node_index)) return false;
1175 sat::AddSolutionAsHintToGeneralizedModel(initial_solution, *model, arc_vars,
1176 &cp_model);
1177 const std::function<void(const sat::CpSolverResponse& response)> observer =
1178 search_parameters.report_intermediate_cp_sat_solutions() ?
1179 [model, &objective, &arc_vars, solution, deadline]
1180 (const sat::CpSolverResponse& response) {
1181 // TODO(user): Check that performance is acceptable.
1182 sat::ConvertGeneralizedResponseToSolution(
1183 response, objective, *model, arc_vars, solution);
1184 const absl::Duration remaining_time =
1185 deadline - model->solver()->Now();
1186 if (remaining_time < absl::ZeroDuration()) return;
1187 model->UpdateTimeLimit(remaining_time);
1188 model->CheckIfAssignmentIsFeasible(
1189 *solution,
1190 /*call_at_solution_monitors=*/true);
1191 } : null_observer;
1192 return sat::ConvertGeneralizedResponseToSolution(
1193 sat::SolveRoutingModel(cp_model, remaining_time,
1194 model->GetMutableCPSatInterrupt(),
1195 search_parameters, observer),
1196 objective, *model, arc_vars, solution);
1197 }
1198 if (!sat::RoutingModelCanBeSolvedBySat(*model)) return false;
1199 const sat::ArcVarMap arc_vars =
1200 sat::PopulateModelFromRoutingModel(*model, &cp_model);
1201 sat::AddSolutionAsHintToModel(initial_solution, *model, arc_vars, &cp_model);
1202 const std::function<void(const sat::CpSolverResponse& response)> observer =
1203 search_parameters.report_intermediate_cp_sat_solutions() ?
1204 [model, &objective, &arc_vars, solution, deadline]
1205 (const sat::CpSolverResponse& response) {
1206 // TODO(user): Check that performance is acceptable.
1207 sat::ConvertToSolution(response, objective, *model, arc_vars, solution);
1208 const absl::Duration remaining_time = deadline - model->solver()->Now();
1209 if (remaining_time < absl::ZeroDuration()) return;
1210 model->UpdateTimeLimit(remaining_time);
1211
1212 model->CheckIfAssignmentIsFeasible(*solution,
1213 /*call_at_solution_monitors=*/true);
1214 } : null_observer;
1215 return sat::ConvertToSolution(
1216 sat::SolveRoutingModel(cp_model, remaining_time,
1217 model->GetMutableCPSatInterrupt(),
1218 search_parameters, observer),
1219 objective, *model, arc_vars, solution);
1220}
1221
1222} // namespace operations_research
void Set(IndexType i)
Sets the bit at position i to 1.
Definition bitset.h:544
T Add(std::function< T(Model *)> f)
Definition model.h:87
void InsertOrDie(Collection *const collection, const typename Collection::value_type &value)
Definition map_util.h:159
const Collection::value_type::second_type * FindOrNull(const Collection &collection, const typename Collection::value_type::first_type &key)
Definition map_util.h:65
std::function< SatParameters(Model *)> NewSatParameters(const std::string &params)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
bool operator==(const BoolArgumentProto &lhs, const BoolArgumentProto &rhs)
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
std::ostream & operator<<(std::ostream &os, const BoolVar &var)
Definition cp_model.cc:89
CpSolverResponse SolveCpModel(const CpModelProto &model_proto, Model *model)
H AbslHashValue(H h, const IntVar &i)
– ABSL HASHING SUPPORT --------------------------------------------------—
Definition cp_model.h:515
std::function< void(Model *)> NewFeasibleSolutionObserver(const std::function< void(const CpSolverResponse &response)> &callback)
In SWIG mode, we don't want anything besides these top-level includes.
int64_t CapAdd(int64_t x, int64_t y)
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
int64_t CapSub(int64_t x, int64_t y)
int64_t CapProd(int64_t x, int64_t y)
bool SolveModelWithSat(RoutingModel *model, const RoutingSearchParameters &search_parameters, const Assignment *initial_solution, Assignment *solution)