Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
routing_lp_scheduling.cc
Go to the documentation of this file.
1// Copyright 2010-2024 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
15
16#include <algorithm>
17#include <cmath>
18#include <cstdint>
19#include <cstdlib>
20#include <deque>
21#include <functional>
22#include <iterator>
23#include <limits>
24#include <memory>
25#include <numeric>
26#include <string>
27#include <utility>
28#include <vector>
29
30#include "absl/algorithm/container.h"
31#include "absl/container/flat_hash_map.h"
32#include "absl/container/flat_hash_set.h"
33#include "absl/log/check.h"
34#include "absl/strings/str_format.h"
35#include "absl/strings/string_view.h"
36#include "absl/time/time.h"
37#include "absl/types/span.h"
42#include "ortools/base/types.h"
45#include "ortools/constraint_solver/routing_parameters.pb.h"
46#include "ortools/glop/parameters.pb.h"
49#include "ortools/sat/cp_model.pb.h"
54
55namespace operations_research {
56
57namespace {
58
59// The following sets of parameters give the fastest response time without
60// impacting solutions found negatively.
61glop::GlopParameters GetGlopParametersForLocalLP() {
62 glop::GlopParameters parameters;
63 parameters.set_use_dual_simplex(true);
64 parameters.set_use_preprocessing(false);
65 return parameters;
66}
67
68glop::GlopParameters GetGlopParametersForGlobalLP() {
69 glop::GlopParameters parameters;
70 parameters.set_use_dual_simplex(true);
71 return parameters;
72}
73
74bool GetCumulBoundsWithOffset(const RoutingDimension& dimension,
75 int64_t node_index, int64_t cumul_offset,
76 int64_t* lower_bound, int64_t* upper_bound) {
77 DCHECK(lower_bound != nullptr);
78 DCHECK(upper_bound != nullptr);
79
80 const IntVar& cumul_var = *dimension.CumulVar(node_index);
81 *upper_bound = cumul_var.Max();
82 if (*upper_bound < cumul_offset) {
83 return false;
84 }
85
86 const int64_t first_after_offset =
87 std::max(dimension.GetFirstPossibleGreaterOrEqualValueForNode(
88 node_index, cumul_offset),
89 cumul_var.Min());
90 DCHECK_LT(first_after_offset, std::numeric_limits<int64_t>::max());
91 *lower_bound = CapSub(first_after_offset, cumul_offset);
92 DCHECK_GE(*lower_bound, 0);
93
94 if (*upper_bound == std::numeric_limits<int64_t>::max()) {
95 return true;
96 }
97 *upper_bound = CapSub(*upper_bound, cumul_offset);
98 DCHECK_GE(*upper_bound, *lower_bound);
99 return true;
100}
101
102int64_t GetFirstPossibleValueForCumulWithOffset(
103 const RoutingDimension& dimension, int64_t node_index,
104 int64_t lower_bound_without_offset, int64_t cumul_offset) {
105 return CapSub(
106 dimension.GetFirstPossibleGreaterOrEqualValueForNode(
107 node_index, CapAdd(lower_bound_without_offset, cumul_offset)),
108 cumul_offset);
109}
110
111int64_t GetLastPossibleValueForCumulWithOffset(
112 const RoutingDimension& dimension, int64_t node_index,
113 int64_t upper_bound_without_offset, int64_t cumul_offset) {
114 return CapSub(
115 dimension.GetLastPossibleLessOrEqualValueForNode(
116 node_index, CapAdd(upper_bound_without_offset, cumul_offset)),
117 cumul_offset);
118}
119
120// Finds the pickup/delivery pairs of nodes on a given vehicle's route.
121// Returns the vector of visited pair indices, and stores the corresponding
122// pickup/delivery indices in visited_pickup_delivery_indices_for_pair_.
123// NOTE: Supposes that visited_pickup_delivery_indices_for_pair is correctly
124// sized and initialized to {-1, -1} for all pairs.
125void StoreVisitedPickupDeliveryPairsOnRoute(
126 const RoutingDimension& dimension, int vehicle,
127 const std::function<int64_t(int64_t)>& next_accessor,
128 std::vector<int>* visited_pairs,
129 std::vector<std::pair<int64_t, int64_t>>*
130 visited_pickup_delivery_indices_for_pair) {
131 // visited_pickup_delivery_indices_for_pair must be all {-1, -1}.
132 DCHECK_EQ(visited_pickup_delivery_indices_for_pair->size(),
133 dimension.model()->GetPickupAndDeliveryPairs().size());
134 DCHECK(std::all_of(visited_pickup_delivery_indices_for_pair->begin(),
135 visited_pickup_delivery_indices_for_pair->end(),
136 [](std::pair<int64_t, int64_t> p) {
137 return p.first == -1 && p.second == -1;
138 }));
139 visited_pairs->clear();
140 if (!dimension.HasPickupToDeliveryLimits()) {
141 return;
142 }
143 const RoutingModel& model = *dimension.model();
144
145 int64_t node_index = model.Start(vehicle);
146 while (!model.IsEnd(node_index)) {
147 const std::vector<RoutingModel::PickupDeliveryPosition>& pickup_positions =
148 model.GetPickupPositions(node_index);
149 const std::vector<RoutingModel::PickupDeliveryPosition>&
150 delivery_positions = model.GetDeliveryPositions(node_index);
151 if (!pickup_positions.empty()) {
152 // The current node is a pickup. We verify that it belongs to a single
153 // pickup index pair and that it's not a delivery, and store the index.
154 DCHECK(delivery_positions.empty());
155 DCHECK_EQ(pickup_positions.size(), 1);
156 const int pair_index = pickup_positions[0].pd_pair_index;
157 (*visited_pickup_delivery_indices_for_pair)[pair_index].first =
158 node_index;
159 visited_pairs->push_back(pair_index);
160 } else if (!delivery_positions.empty()) {
161 // The node is a delivery. We verify that it belongs to a single
162 // delivery pair, and set the limit with its pickup if one has been
163 // visited for this pair.
164 DCHECK_EQ(delivery_positions.size(), 1);
165 const int pair_index = delivery_positions[0].pd_pair_index;
166 std::pair<int64_t, int64_t>& pickup_delivery_index =
167 (*visited_pickup_delivery_indices_for_pair)[pair_index];
168 if (pickup_delivery_index.first < 0) {
169 // This case should not happen, as a delivery must have its pickup
170 // on the route, but we ignore it here.
171 node_index = next_accessor(node_index);
172 continue;
173 }
174 pickup_delivery_index.second = node_index;
175 }
176 node_index = next_accessor(node_index);
177 }
178}
179
180} // namespace
181
182// LocalDimensionCumulOptimizer
183
185 const RoutingDimension* dimension,
186 RoutingSearchParameters::SchedulingSolver solver_type)
187 : optimizer_core_(dimension, /*use_precedence_propagator=*/false) {
188 // Using one solver per vehicle in the hope that if routes don't change this
189 // will be faster.
190 const int vehicles = dimension->model()->vehicles();
191 solver_.resize(vehicles);
192 switch (solver_type) {
193 case RoutingSearchParameters::SCHEDULING_GLOP: {
194 const glop::GlopParameters parameters = GetGlopParametersForLocalLP();
195 for (int vehicle = 0; vehicle < vehicles; ++vehicle) {
196 // TODO(user): Instead of passing false, detect if the relaxation
197 // will always violate the MIPL constraints.
198 solver_[vehicle] =
199 std::make_unique<RoutingGlopWrapper>(false, parameters);
200 }
201 break;
202 }
203 case RoutingSearchParameters::SCHEDULING_CP_SAT: {
204 for (int vehicle = 0; vehicle < vehicles; ++vehicle) {
205 solver_[vehicle] = std::make_unique<RoutingCPSatWrapper>();
206 }
207 break;
208 }
209 default:
210 LOG(DFATAL) << "Unrecognized solver type: " << solver_type;
211 }
212}
213
215 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
216 int64_t* optimal_cost) {
217 int64_t transit_cost = 0;
219 optimizer_core_.OptimizeSingleRouteWithResource(
220 vehicle, next_accessor,
221 /*dimension_travel_info=*/{},
222 /*resource=*/nullptr,
223 /*optimize_vehicle_costs=*/optimal_cost != nullptr,
224 solver_[vehicle].get(), /*cumul_values=*/nullptr,
225 /*break_values=*/nullptr, optimal_cost, &transit_cost);
227 optimal_cost != nullptr) {
228 DCHECK_GE(*optimal_cost, 0);
229 *optimal_cost = CapAdd(*optimal_cost, transit_cost);
230 }
231 return status;
232}
233
236 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
237 int64_t* optimal_cost_without_transits) {
238 return optimizer_core_.OptimizeSingleRouteWithResource(
239 vehicle, next_accessor,
240 /*dimension_travel_info=*/{},
241 /*resource=*/nullptr,
242 /*optimize_vehicle_costs=*/optimal_cost_without_transits != nullptr,
243 solver_[vehicle].get(), /*cumul_values=*/nullptr,
244 /*break_values=*/nullptr, optimal_cost_without_transits, nullptr);
245}
246
247std::vector<DimensionSchedulingStatus> LocalDimensionCumulOptimizer::
249 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
250 const std::function<int64_t(int64_t, int64_t)>& transit_accessor,
251 const std::vector<RoutingModel::ResourceGroup::Resource>& resources,
252 const std::vector<int>& resource_indices, bool optimize_vehicle_costs,
253 std::vector<int64_t>* optimal_costs_without_transits,
254 std::vector<std::vector<int64_t>>* optimal_cumuls,
255 std::vector<std::vector<int64_t>>* optimal_breaks) {
256 return optimizer_core_.OptimizeSingleRouteWithResources(
257 vehicle, next_accessor, transit_accessor, {}, resources, resource_indices,
258 optimize_vehicle_costs, solver_[vehicle].get(), optimal_cumuls,
259 optimal_breaks, optimal_costs_without_transits, nullptr);
260}
261
263 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
264 const RoutingModel::RouteDimensionTravelInfo& dimension_travel_info,
265 const RoutingModel::ResourceGroup::Resource* resource,
266 std::vector<int64_t>* optimal_cumuls,
267 std::vector<int64_t>* optimal_breaks) {
268 return optimizer_core_.OptimizeSingleRouteWithResource(
269 vehicle, next_accessor, dimension_travel_info, resource,
270 /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), optimal_cumuls,
271 optimal_breaks, /*cost=*/nullptr, /*transit_cost=*/nullptr);
272}
273
276 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
277 const RoutingModel::RouteDimensionTravelInfo& dimension_travel_info,
278 std::vector<int64_t>* optimal_cumuls, std::vector<int64_t>* optimal_breaks,
279 int64_t* optimal_cost_without_transits) {
280 return optimizer_core_.OptimizeSingleRouteWithResource(
281 vehicle, next_accessor, dimension_travel_info, nullptr,
282 /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), optimal_cumuls,
283 optimal_breaks, optimal_cost_without_transits, nullptr);
284}
285
288 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
289 const RoutingModel::RouteDimensionTravelInfo& dimension_travel_info,
290 const std::vector<int64_t>& solution_cumul_values,
291 const std::vector<int64_t>& solution_break_values, int64_t* solution_cost,
292 int64_t* cost_offset, bool reuse_previous_model_if_possible, bool clear_lp,
293 absl::Duration* solve_duration) {
294 RoutingLinearSolverWrapper* solver = solver_[vehicle].get();
296 vehicle, next_accessor, dimension_travel_info, solver,
297 solution_cumul_values, solution_break_values, solution_cost, cost_offset,
298 reuse_previous_model_if_possible, clear_lp,
299 /*clear_solution_constraints=*/true, solve_duration);
300}
301
304 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
305 const RoutingModel::RouteDimensionTravelInfo& dimension_travel_info,
306 const RoutingModel::ResourceGroup::Resource* resource,
307 std::vector<int64_t>* packed_cumuls, std::vector<int64_t>* packed_breaks) {
308 return optimizer_core_.OptimizeAndPackSingleRoute(
309 vehicle, next_accessor, dimension_travel_info, resource,
310 solver_[vehicle].get(), packed_cumuls, packed_breaks);
311}
312
313const int CumulBoundsPropagator::kNoParent = -2;
314const int CumulBoundsPropagator::kParentToBePropagated = -1;
315
316CumulBoundsPropagator::CumulBoundsPropagator(const RoutingDimension* dimension)
317 : dimension_(*dimension), num_nodes_(2 * dimension->cumuls().size()) {
318 outgoing_arcs_.resize(num_nodes_);
319 node_in_queue_.resize(num_nodes_, false);
320 tree_parent_node_of_.resize(num_nodes_, kNoParent);
321 propagated_bounds_.resize(num_nodes_);
322 visited_pickup_delivery_indices_for_pair_.resize(
323 dimension->model()->GetPickupAndDeliveryPairs().size(), {-1, -1});
324}
325
326void CumulBoundsPropagator::AddArcs(int first_index, int second_index,
327 int64_t offset) {
328 // Add arc first_index + offset <= second_index
329 outgoing_arcs_[PositiveNode(first_index)].push_back(
330 {PositiveNode(second_index), offset});
331 AddNodeToQueue(PositiveNode(first_index));
332 // Add arc -second_index + transit <= -first_index
333 outgoing_arcs_[NegativeNode(second_index)].push_back(
334 {NegativeNode(first_index), offset});
335 AddNodeToQueue(NegativeNode(second_index));
336}
337
338bool CumulBoundsPropagator::InitializeArcsAndBounds(
339 const std::function<int64_t(int64_t)>& next_accessor, int64_t cumul_offset,
340 const std::vector<RoutingModel::RouteDimensionTravelInfo>*
341 dimension_travel_info_per_route) {
342 propagated_bounds_.assign(num_nodes_, std::numeric_limits<int64_t>::min());
343
344 for (std::vector<ArcInfo>& arcs : outgoing_arcs_) {
345 arcs.clear();
346 }
347
348 RoutingModel* const model = dimension_.model();
349 std::vector<int64_t>& lower_bounds = propagated_bounds_;
350
351 for (int vehicle = 0; vehicle < model->vehicles(); vehicle++) {
352 const std::function<int64_t(int64_t, int64_t)>& transit_accessor =
353 dimension_.transit_evaluator(vehicle);
354
355 int node = model->Start(vehicle);
356 int index_on_route = 0;
357 while (true) {
358 int64_t cumul_lb, cumul_ub;
359 if (!GetCumulBoundsWithOffset(dimension_, node, cumul_offset, &cumul_lb,
360 &cumul_ub)) {
361 return false;
362 }
363 lower_bounds[PositiveNode(node)] = cumul_lb;
364 if (cumul_ub < std::numeric_limits<int64_t>::max()) {
365 lower_bounds[NegativeNode(node)] = -cumul_ub;
366 }
367
368 if (model->IsEnd(node)) {
369 break;
370 }
371
372 const int next = next_accessor(node);
373 int64_t transit = transit_accessor(node, next);
374 if (dimension_travel_info_per_route != nullptr &&
375 !dimension_travel_info_per_route->empty()) {
376 const RoutingModel::RouteDimensionTravelInfo::TransitionInfo&
377 transition_info = (*dimension_travel_info_per_route)[vehicle]
378 .transition_info[index_on_route];
379 transit = transition_info.compressed_travel_value_lower_bound +
380 transition_info.pre_travel_transit_value +
381 transition_info.post_travel_transit_value;
382 ++index_on_route;
383 }
384 const IntVar& slack_var = *dimension_.SlackVar(node);
385 // node + transit + slack_var == next
386 // Add arcs for node + transit + slack_min <= next
387 AddArcs(node, next, CapAdd(transit, slack_var.Min()));
388 if (slack_var.Max() < std::numeric_limits<int64_t>::max()) {
389 // Add arcs for node + transit + slack_max >= next.
390 AddArcs(next, node, CapSub(-slack_var.Max(), transit));
391 }
392
393 node = next;
394 }
395
396 // Add vehicle span upper bound: end - span_ub <= start.
397 const int64_t span_ub = dimension_.GetSpanUpperBoundForVehicle(vehicle);
398 if (span_ub < std::numeric_limits<int64_t>::max()) {
399 AddArcs(model->End(vehicle), model->Start(vehicle), -span_ub);
400 }
401
402 // Set pickup/delivery limits on route.
403 std::vector<int> visited_pairs;
404 StoreVisitedPickupDeliveryPairsOnRoute(
405 dimension_, vehicle, next_accessor, &visited_pairs,
406 &visited_pickup_delivery_indices_for_pair_);
407 for (int pair_index : visited_pairs) {
408 const int64_t pickup_index =
409 visited_pickup_delivery_indices_for_pair_[pair_index].first;
410 const int64_t delivery_index =
411 visited_pickup_delivery_indices_for_pair_[pair_index].second;
412 visited_pickup_delivery_indices_for_pair_[pair_index] = {-1, -1};
413
414 DCHECK_GE(pickup_index, 0);
415 if (delivery_index < 0) {
416 // We didn't encounter a delivery for this pickup.
417 continue;
418 }
419
420 const int64_t limit = dimension_.GetPickupToDeliveryLimitForPair(
421 pair_index,
422 model->GetPickupPositions(pickup_index)[0].alternative_index,
423 model->GetDeliveryPositions(delivery_index)[0].alternative_index);
424 if (limit < std::numeric_limits<int64_t>::max()) {
425 // delivery_cumul - limit <= pickup_cumul.
426 AddArcs(delivery_index, pickup_index, -limit);
427 }
428 }
429 }
430
431 for (const RoutingDimension::NodePrecedence& precedence :
432 dimension_.GetNodePrecedences()) {
433 const int first_index = precedence.first_node;
434 const int second_index = precedence.second_node;
435 if (lower_bounds[PositiveNode(first_index)] ==
436 std::numeric_limits<int64_t>::min() ||
437 lower_bounds[PositiveNode(second_index)] ==
438 std::numeric_limits<int64_t>::min()) {
439 // One of the nodes is unperformed, so the precedence rule doesn't apply.
440 continue;
441 }
442 AddArcs(first_index, second_index, precedence.offset);
443 }
444
445 return true;
446}
447
448bool CumulBoundsPropagator::UpdateCurrentLowerBoundOfNode(int node,
449 int64_t new_lb,
450 int64_t offset) {
451 const int cumul_var_index = node / 2;
452
453 if (node == PositiveNode(cumul_var_index)) {
454 // new_lb is a lower bound of the cumul of variable 'cumul_var_index'.
455 propagated_bounds_[node] = GetFirstPossibleValueForCumulWithOffset(
456 dimension_, cumul_var_index, new_lb, offset);
457 } else {
458 // -new_lb is an upper bound of the cumul of variable 'cumul_var_index'.
459 const int64_t new_ub = CapSub(0, new_lb);
460 propagated_bounds_[node] =
461 CapSub(0, GetLastPossibleValueForCumulWithOffset(
462 dimension_, cumul_var_index, new_ub, offset));
463 }
464
465 // Test that the lower/upper bounds do not cross each other.
466 const int64_t cumul_lower_bound =
467 propagated_bounds_[PositiveNode(cumul_var_index)];
468
469 const int64_t negated_cumul_upper_bound =
470 propagated_bounds_[NegativeNode(cumul_var_index)];
471
472 return CapAdd(negated_cumul_upper_bound, cumul_lower_bound) <= 0;
473}
474
475bool CumulBoundsPropagator::DisassembleSubtree(int source, int target) {
476 tmp_dfs_stack_.clear();
477 tmp_dfs_stack_.push_back(source);
478 while (!tmp_dfs_stack_.empty()) {
479 const int tail = tmp_dfs_stack_.back();
480 tmp_dfs_stack_.pop_back();
481 for (const ArcInfo& arc : outgoing_arcs_[tail]) {
482 const int child_node = arc.head;
483 if (tree_parent_node_of_[child_node] != tail) continue;
484 if (child_node == target) return false;
485 tree_parent_node_of_[child_node] = kParentToBePropagated;
486 tmp_dfs_stack_.push_back(child_node);
487 }
488 }
489 return true;
490}
491
493 const std::function<int64_t(int64_t)>& next_accessor, int64_t cumul_offset,
494 const std::vector<RoutingModel::RouteDimensionTravelInfo>*
495 dimension_travel_info_per_route) {
496 tree_parent_node_of_.assign(num_nodes_, kNoParent);
497 DCHECK(std::none_of(node_in_queue_.begin(), node_in_queue_.end(),
498 [](bool b) { return b; }));
499 DCHECK(bf_queue_.empty());
500
501 if (!InitializeArcsAndBounds(next_accessor, cumul_offset,
502 dimension_travel_info_per_route)) {
503 return CleanupAndReturnFalse();
504 }
505
506 std::vector<int64_t>& current_lb = propagated_bounds_;
507
508 // Bellman-Ford-Tarjan algorithm.
509 while (!bf_queue_.empty()) {
510 const int node = bf_queue_.front();
511 bf_queue_.pop_front();
512 node_in_queue_[node] = false;
513
514 if (tree_parent_node_of_[node] == kParentToBePropagated) {
515 // The parent of this node is still in the queue, so no need to process
516 // node now, since it will be re-enqueued when its parent is processed.
517 continue;
518 }
519
520 const int64_t lower_bound = current_lb[node];
521 for (const ArcInfo& arc : outgoing_arcs_[node]) {
522 // NOTE: kint64min as a lower bound means no lower bound at all, so we
523 // don't use this value to propagate.
524 const int64_t induced_lb =
525 (lower_bound == std::numeric_limits<int64_t>::min())
526 ? std::numeric_limits<int64_t>::min()
527 : CapAdd(lower_bound, arc.offset);
528
529 const int head_node = arc.head;
530 if (induced_lb <= current_lb[head_node]) {
531 // No update necessary for the head_node, continue to next children of
532 // node.
533 continue;
534 }
535 if (!UpdateCurrentLowerBoundOfNode(head_node, induced_lb, cumul_offset) ||
536 !DisassembleSubtree(head_node, node)) {
537 // The new lower bound is infeasible, or a positive cycle was detected
538 // in the precedence graph by DisassembleSubtree().
539 return CleanupAndReturnFalse();
540 }
541
542 tree_parent_node_of_[head_node] = node;
543 AddNodeToQueue(head_node);
544 }
545 }
546 return true;
547}
548
550 const RoutingDimension* dimension, bool use_precedence_propagator)
551 : dimension_(dimension),
552 visited_pickup_delivery_indices_for_pair_(
553 dimension->model()->GetPickupAndDeliveryPairs().size(), {-1, -1}) {
554 if (use_precedence_propagator) {
555 propagator_ = std::make_unique<CumulBoundsPropagator>(dimension);
556 }
557 const RoutingModel& model = *dimension_->model();
558 if (dimension_->HasBreakConstraints()) {
559 // Initialize vehicle_to_first_index_ so the variables of the breaks of
560 // vehicle v are stored from vehicle_to_first_index_[v] to
561 // vehicle_to_first_index_[v+1] - 1.
562 const int num_vehicles = model.vehicles();
563 vehicle_to_all_break_variables_offset_.reserve(num_vehicles);
564 int num_break_vars = 0;
565 for (int vehicle = 0; vehicle < num_vehicles; ++vehicle) {
566 vehicle_to_all_break_variables_offset_.push_back(num_break_vars);
567 const auto& intervals = dimension_->GetBreakIntervalsOfVehicle(vehicle);
568 num_break_vars += 2 * intervals.size(); // 2 variables per break.
569 }
570 all_break_variables_.resize(num_break_vars, -1);
571 }
572 if (!model.GetDimensionResourceGroupIndices(dimension_).empty()) {
573 resource_group_to_resource_to_vehicle_assignment_variables_.resize(
574 model.GetResourceGroups().size());
575 }
576}
577
580 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
581 const RouteDimensionTravelInfo& dimension_travel_info,
583 absl::Span<const int64_t> solution_cumul_values,
584 absl::Span<const int64_t> solution_break_values,
585 int64_t* cost_without_transits, int64_t* cost_offset,
586 bool reuse_previous_model_if_possible, bool clear_lp,
587 bool clear_solution_constraints, absl::Duration* const solve_duration) {
588 absl::Duration solve_duration_value;
589 int64_t cost_offset_value;
590 if (!reuse_previous_model_if_possible || solver->ModelIsEmpty()) {
591 InitOptimizer(solver);
592 // Make sure SetRouteCumulConstraints will properly set the cumul bounds by
593 // looking at this route only.
594 DCHECK_EQ(propagator_.get(), nullptr);
595
596 RoutingModel* const model = dimension_->model();
597 const bool optimize_vehicle_costs =
598 !model->IsEnd(next_accessor(model->Start(vehicle))) ||
599 model->IsVehicleUsedWhenEmpty(vehicle);
600 if (!SetRouteCumulConstraints(
601 vehicle, next_accessor, dimension_->transit_evaluator(vehicle),
602 dimension_travel_info,
603 dimension_->GetLocalOptimizerOffsetForVehicle(vehicle),
604 optimize_vehicle_costs, solver, nullptr, &cost_offset_value)) {
606 }
607 if (model->CheckLimit()) {
609 }
610 solve_duration_value = model->RemainingTime();
611 if (solve_duration != nullptr) *solve_duration = solve_duration_value;
612 if (cost_offset != nullptr) *cost_offset = cost_offset_value;
613 } else {
614 CHECK(cost_offset != nullptr)
615 << "Cannot reuse model without the cost_offset";
616 cost_offset_value = *cost_offset;
617 CHECK(solve_duration != nullptr)
618 << "Cannot reuse model without the solve_duration";
619 solve_duration_value = *solve_duration;
620 }
621
622 // Constrains the cumuls.
623 DCHECK_EQ(solution_cumul_values.size(),
624 current_route_cumul_variables_.size());
625 for (int i = 0; i < current_route_cumul_variables_.size(); ++i) {
626 if (solution_cumul_values[i] < current_route_min_cumuls_[i] ||
627 solution_cumul_values[i] > current_route_max_cumuls_[i]) {
629 }
630 solver->SetVariableBounds(current_route_cumul_variables_[i],
631 /*lower_bound=*/solution_cumul_values[i],
632 /*upper_bound=*/solution_cumul_values[i]);
633 }
634
635 // Constrains the breaks.
636 DCHECK_EQ(solution_break_values.size(),
637 current_route_break_variables_.size());
638 std::vector<int64_t> current_route_min_breaks(
639 current_route_break_variables_.size());
640 std::vector<int64_t> current_route_max_breaks(
641 current_route_break_variables_.size());
642 for (int i = 0; i < current_route_break_variables_.size(); ++i) {
643 current_route_min_breaks[i] =
644 solver->GetVariableLowerBound(current_route_break_variables_[i]);
645 current_route_max_breaks[i] =
646 solver->GetVariableUpperBound(current_route_break_variables_[i]);
647 solver->SetVariableBounds(current_route_break_variables_[i],
648 /*lower_bound=*/solution_break_values[i],
649 /*upper_bound=*/solution_break_values[i]);
650 }
651
652 const DimensionSchedulingStatus status = solver->Solve(solve_duration_value);
654 solver->Clear();
655 return status;
656 }
657
658 if (cost_without_transits != nullptr) {
659 *cost_without_transits =
660 CapAdd(cost_offset_value, solver->GetObjectiveValue());
661 }
662
663 if (clear_lp) {
664 solver->Clear();
665 } else if (clear_solution_constraints) {
666 for (int i = 0; i < current_route_cumul_variables_.size(); ++i) {
667 solver->SetVariableBounds(current_route_cumul_variables_[i],
668 /*lower_bound=*/current_route_min_cumuls_[i],
669 /*upper_bound=*/current_route_max_cumuls_[i]);
670 }
671 for (int i = 0; i < current_route_break_variables_.size(); ++i) {
672 solver->SetVariableBounds(current_route_break_variables_[i],
673 /*lower_bound=*/current_route_min_breaks[i],
674 /*upper_bound=*/current_route_max_breaks[i]);
675 }
676 }
677 return status;
678}
679
680namespace {
681template <typename T>
682void ClearIfNonNull(std::vector<T>* v) {
683 if (v != nullptr) {
684 v->clear();
685 }
686}
687} // namespace
688
691 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
692 const RouteDimensionTravelInfo& dimension_travel_info,
693 const RoutingModel::ResourceGroup::Resource* resource,
694 bool optimize_vehicle_costs, RoutingLinearSolverWrapper* solver,
695 std::vector<int64_t>* cumul_values, std::vector<int64_t>* break_values,
696 int64_t* cost_without_transits, int64_t* transit_cost, bool clear_lp) {
697 if (cost_without_transits != nullptr) *cost_without_transits = -1;
698 ClearIfNonNull(cumul_values);
699 ClearIfNonNull(break_values);
700
701 const std::vector<Resource> resources =
702 resource == nullptr ? std::vector<Resource>()
703 : std::vector<Resource>({*resource});
704 const std::vector<int> resource_indices =
705 resource == nullptr ? std::vector<int>() : std::vector<int>({0});
706 std::vector<int64_t> costs_without_transits;
707 std::vector<std::vector<int64_t>> cumul_values_vec;
708 std::vector<std::vector<int64_t>> break_values_vec;
709 const std::vector<DimensionSchedulingStatus> statuses =
711 vehicle, next_accessor, dimension_->transit_evaluator(vehicle),
712 dimension_travel_info, resources, resource_indices,
713 optimize_vehicle_costs, solver,
714 cumul_values != nullptr ? &cumul_values_vec : nullptr,
715 break_values != nullptr ? &break_values_vec : nullptr,
716 cost_without_transits != nullptr ? &costs_without_transits : nullptr,
717 transit_cost, clear_lp);
718
719 if (dimension()->model()->CheckLimit()) {
721 }
722 DCHECK_EQ(statuses.size(), 1);
723 const DimensionSchedulingStatus status = statuses[0];
724
726
727 if (cost_without_transits != nullptr) {
728 *cost_without_transits = costs_without_transits[0];
729 }
730 if (cumul_values != nullptr) *cumul_values = std::move(cumul_values_vec[0]);
731 if (break_values != nullptr) *break_values = std::move(break_values_vec[0]);
732
733 return status;
734}
735
736namespace {
737
738using ResourceGroup = RoutingModel::ResourceGroup;
739
740bool GetDomainOffsetBounds(const Domain& domain, int64_t offset,
742 const int64_t lower_bound =
743 std::max<int64_t>(CapSub(domain.Min(), offset), 0);
744 const int64_t upper_bound =
745 domain.Max() == std::numeric_limits<int64_t>::max()
746 ? std::numeric_limits<int64_t>::max()
747 : CapSub(domain.Max(), offset);
748 if (lower_bound > upper_bound) return false;
749
751 return true;
752}
753
754bool GetIntervalIntersectionWithOffsetDomain(const ClosedInterval& interval,
755 const Domain& domain,
756 int64_t offset,
757 ClosedInterval* intersection) {
758 ClosedInterval domain_bounds;
759 if (!GetDomainOffsetBounds(domain, offset, &domain_bounds)) {
760 return false;
761 }
762 const int64_t intersection_lb = std::max(interval.start, domain_bounds.start);
763 const int64_t intersection_ub = std::min(interval.end, domain_bounds.end);
764 if (intersection_lb > intersection_ub) return false;
765
766 *intersection = ClosedInterval(intersection_lb, intersection_ub);
767 return true;
768}
769
770ClosedInterval GetVariableBounds(int index,
771 const RoutingLinearSolverWrapper& solver) {
772 return ClosedInterval(solver.GetVariableLowerBound(index),
773 solver.GetVariableUpperBound(index));
774}
775
776bool TightenStartEndVariableBoundsWithResource(
777 const RoutingDimension& dimension, const ResourceGroup::Resource& resource,
778 const ClosedInterval& start_bounds, int start_index,
779 const ClosedInterval& end_bounds, int end_index, int64_t offset,
780 RoutingLinearSolverWrapper* solver) {
781 const ResourceGroup::Attributes& attributes =
782 resource.GetDimensionAttributes(&dimension);
783 ClosedInterval new_start_bounds;
784 ClosedInterval new_end_bounds;
785 return GetIntervalIntersectionWithOffsetDomain(start_bounds,
786 attributes.start_domain(),
787 offset, &new_start_bounds) &&
788 solver->SetVariableBounds(start_index, new_start_bounds.start,
789 new_start_bounds.end) &&
790 GetIntervalIntersectionWithOffsetDomain(
791 end_bounds, attributes.end_domain(), offset, &new_end_bounds) &&
792 solver->SetVariableBounds(end_index, new_end_bounds.start,
793 new_end_bounds.end);
794}
795
796bool TightenStartEndVariableBoundsWithAssignedResources(
797 const RoutingDimension& dimension, int v, int start_index, int end_index,
798 int64_t offset, RoutingLinearSolverWrapper* solver) {
799 const RoutingModel* model = dimension.model();
800 for (int rg_index : model->GetDimensionResourceGroupIndices(&dimension)) {
801 const IntVar* res_var = model->ResourceVars(rg_index)[v];
802 DCHECK(res_var->Bound());
803 const int res_index = res_var->Value();
804 if (res_index < 0) {
805 // No resource from this group assigned to the vehicle.
806 DCHECK(!model->GetResourceGroup(rg_index)->VehicleRequiresAResource(v));
807 continue;
808 }
809 const ResourceGroup::Resource& resource =
810 model->GetResourceGroup(rg_index)->GetResource(res_index);
811 if (!TightenStartEndVariableBoundsWithResource(
812 dimension, resource, GetVariableBounds(start_index, *solver),
813 start_index, GetVariableBounds(end_index, *solver), end_index,
814 offset, solver)) {
815 return false;
816 }
817 }
818 return true;
819}
820
821} // namespace
822
823std::vector<DimensionSchedulingStatus>
825 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
826 const std::function<int64_t(int64_t, int64_t)>& transit_accessor,
827 const RouteDimensionTravelInfo& dimension_travel_info,
828 const std::vector<RoutingModel::ResourceGroup::Resource>& resources,
829 const std::vector<int>& resource_indices, bool optimize_vehicle_costs,
831 std::vector<std::vector<int64_t>>* cumul_values,
832 std::vector<std::vector<int64_t>>* break_values,
833 std::vector<int64_t>* costs_without_transits, int64_t* transit_cost,
834 bool clear_lp) {
835 ClearIfNonNull(costs_without_transits);
836 const bool optimize_with_resources = !resource_indices.empty();
837 if (!optimize_with_resources && !resources.empty()) return {};
838
839 InitOptimizer(solver);
840 // Make sure SetRouteCumulConstraints will properly set the cumul bounds by
841 // looking at this route only.
842 DCHECK_EQ(propagator_.get(), nullptr);
843
844 RoutingModel* const model = dimension()->model();
845 if (model->IsEnd(next_accessor(model->Start(vehicle))) &&
846 !model->IsVehicleUsedWhenEmpty(vehicle)) {
847 // An unused empty vehicle doesn't require resources.
848 DCHECK(!optimize_with_resources);
849 optimize_vehicle_costs = false;
850 }
851
852 const int64_t cumul_offset =
853 dimension_->GetLocalOptimizerOffsetForVehicle(vehicle);
854 int64_t cost_offset = 0;
855 if (!SetRouteCumulConstraints(vehicle, next_accessor, transit_accessor,
856 dimension_travel_info, cumul_offset,
857 optimize_vehicle_costs, solver, transit_cost,
858 &cost_offset)) {
859 if (costs_without_transits != nullptr) {
860 costs_without_transits->assign(1, -1);
861 }
863 }
864 DCHECK_GE(current_route_cumul_variables_.size(), 2);
865
866 // NOTE: When there are no resources to optimize for, we still solve the
867 // optimization problem for the route (without any added resource constraint).
868 const int num_solves =
869 std::max(static_cast<decltype(resource_indices.size())>(1UL),
870 resource_indices.size());
871 if (costs_without_transits != nullptr) {
872 costs_without_transits->assign(num_solves, -1);
873 }
874 if (cumul_values != nullptr) cumul_values->assign(num_solves, {});
875 if (break_values != nullptr) break_values->assign(num_solves, {});
876
877 const int start_cumul = current_route_cumul_variables_[0];
878 const ClosedInterval start_bounds = GetVariableBounds(start_cumul, *solver);
879 const int end_cumul = current_route_cumul_variables_.back();
880 const ClosedInterval end_bounds = GetVariableBounds(end_cumul, *solver);
881 std::vector<DimensionSchedulingStatus> statuses;
882 statuses.reserve(num_solves);
883 for (int i = 0; i < num_solves; i++) {
884 if (model->CheckLimit()) {
885 // The model's deadline has been reached, stop.
886 ClearIfNonNull(costs_without_transits);
887 ClearIfNonNull(cumul_values);
888 ClearIfNonNull(break_values);
889 solver->Clear();
890 return {};
891 }
892 if (optimize_with_resources &&
893 !TightenStartEndVariableBoundsWithResource(
894 *dimension_, resources[resource_indices[i]], start_bounds,
895 start_cumul, end_bounds, end_cumul, cumul_offset, solver)) {
896 // The resource attributes don't match this vehicle.
897 statuses.push_back(DimensionSchedulingStatus::INFEASIBLE);
898 continue;
899 }
900
901 statuses.push_back(solver->Solve(model->RemainingTime()));
902 if (statuses.back() == DimensionSchedulingStatus::INFEASIBLE) {
903 continue;
904 }
905 if (costs_without_transits != nullptr) {
906 costs_without_transits->at(i) =
907 optimize_vehicle_costs
908 ? CapAdd(cost_offset, solver->GetObjectiveValue())
909 : 0;
910 }
911
912 if (cumul_values != nullptr) {
913 SetValuesFromLP(current_route_cumul_variables_, cumul_offset, solver,
914 &cumul_values->at(i));
915 }
916 if (break_values != nullptr) {
917 SetValuesFromLP(current_route_break_variables_, cumul_offset, solver,
918 &break_values->at(i));
919 }
920 }
921
922 if (clear_lp) {
923 solver->Clear();
924 }
925 return statuses;
926}
927
929 const std::function<int64_t(int64_t)>& next_accessor,
930 const std::vector<RouteDimensionTravelInfo>&
931 dimension_travel_info_per_route,
932 RoutingLinearSolverWrapper* solver, std::vector<int64_t>* cumul_values,
933 std::vector<int64_t>* break_values,
934 std::vector<std::vector<int>>* resource_indices_per_group,
935 int64_t* cost_without_transits, int64_t* transit_cost, bool clear_lp,
936 bool optimize_resource_assignment) {
937 InitOptimizer(solver);
938 if (!optimize_resource_assignment) {
939 DCHECK_EQ(resource_indices_per_group, nullptr);
940 }
941
942 // If both "cumul_values" and "costs_without_transits" parameters are null,
943 // we don't try to optimize the cost and stop at the first feasible solution.
944 const bool optimize_costs =
945 (cumul_values != nullptr) || (cost_without_transits != nullptr);
946 bool has_vehicles_being_optimized = false;
947
948 const int64_t cumul_offset = dimension_->GetGlobalOptimizerOffset();
949
950 if (propagator_ != nullptr &&
951 !propagator_->PropagateCumulBounds(next_accessor, cumul_offset,
952 &dimension_travel_info_per_route)) {
954 }
955
956 int64_t total_transit_cost = 0;
957 int64_t total_cost_offset = 0;
958 const RoutingModel* model = dimension_->model();
959 for (int vehicle = 0; vehicle < model->vehicles(); vehicle++) {
960 int64_t route_transit_cost = 0;
961 int64_t route_cost_offset = 0;
962 const bool vehicle_is_used =
963 !model->IsEnd(next_accessor(model->Start(vehicle))) ||
964 model->IsVehicleUsedWhenEmpty(vehicle);
965 const bool optimize_vehicle_costs = optimize_costs && vehicle_is_used;
966 const RouteDimensionTravelInfo& dimension_travel_info =
967 dimension_travel_info_per_route.empty()
968 ? RouteDimensionTravelInfo()
969 : dimension_travel_info_per_route[vehicle];
970 if (!SetRouteCumulConstraints(
971 vehicle, next_accessor, dimension_->transit_evaluator(vehicle),
972 dimension_travel_info, cumul_offset, optimize_vehicle_costs, solver,
973 &route_transit_cost, &route_cost_offset)) {
975 }
976 DCHECK_GE(current_route_cumul_variables_.size(), 2);
977 if (vehicle_is_used && !optimize_resource_assignment) {
978 // Tighten the route start/end cumul wrt. resources assigned to it.
979 if (!TightenStartEndVariableBoundsWithAssignedResources(
980 *dimension_, vehicle, current_route_cumul_variables_[0],
981 current_route_cumul_variables_.back(), cumul_offset, solver)) {
983 }
984 }
985 total_transit_cost = CapAdd(total_transit_cost, route_transit_cost);
986 total_cost_offset = CapAdd(total_cost_offset, route_cost_offset);
987 has_vehicles_being_optimized |= optimize_vehicle_costs;
988 }
989 if (transit_cost != nullptr) {
990 *transit_cost = total_transit_cost;
991 }
992
993 if (!SetGlobalConstraints(next_accessor, cumul_offset,
994 has_vehicles_being_optimized,
995 optimize_resource_assignment, solver)) {
997 }
998
1000 solver->Solve(model->RemainingTime());
1002 solver->Clear();
1003 return status;
1004 }
1005
1006 // TODO(user): In case the status is RELAXED_OPTIMAL_ONLY, check we can
1007 // safely avoid filling variable and cost values.
1008 SetValuesFromLP(index_to_cumul_variable_, cumul_offset, solver, cumul_values);
1009 SetValuesFromLP(all_break_variables_, cumul_offset, solver, break_values);
1010 SetResourceIndices(solver, resource_indices_per_group);
1011
1012 if (cost_without_transits != nullptr) {
1013 *cost_without_transits =
1014 CapAdd(solver->GetObjectiveValue(), total_cost_offset);
1015 }
1016
1017 if (clear_lp) {
1018 solver->Clear();
1019 }
1020 return status;
1021}
1022
1024 const std::function<int64_t(int64_t)>& next_accessor,
1025 const std::vector<RouteDimensionTravelInfo>&
1026 dimension_travel_info_per_route,
1027 RoutingLinearSolverWrapper* solver, std::vector<int64_t>* cumul_values,
1028 std::vector<int64_t>* break_values) {
1029 // Note: We pass a non-nullptr cost to the Optimize() method so the costs
1030 // are optimized by the solver.
1031 int64_t cost = 0;
1032 const glop::GlopParameters original_params = GetGlopParametersForGlobalLP();
1033 glop::GlopParameters packing_parameters;
1034 if (!solver->IsCPSATSolver()) {
1035 packing_parameters = original_params;
1036 packing_parameters.set_use_dual_simplex(false);
1037 packing_parameters.set_use_preprocessing(true);
1038 solver->SetParameters(packing_parameters.SerializeAsString());
1039 }
1041 if (Optimize(next_accessor, dimension_travel_info_per_route, solver,
1042 /*cumul_values=*/nullptr, /*break_values=*/nullptr,
1043 /*resource_indices_per_group=*/nullptr, &cost,
1044 /*transit_cost=*/nullptr,
1045 /*clear_lp=*/false, /*optimize_resource_assignment=*/false) ==
1048 }
1050 std::vector<int> vehicles(dimension()->model()->vehicles());
1051 std::iota(vehicles.begin(), vehicles.end(), 0);
1052 // Subtle: Even if the status was RELAXED_OPTIMAL_ONLY we try to pack just
1053 // in case packing manages to make the solution completely feasible.
1054 status = PackRoutes(vehicles, solver, packing_parameters);
1055 }
1056 if (!solver->IsCPSATSolver()) {
1057 solver->SetParameters(original_params.SerializeAsString());
1058 }
1060 return status;
1061 }
1062 // TODO(user): In case the status is RELAXED_OPTIMAL_ONLY, check we can
1063 // safely avoid filling variable values.
1064 const int64_t global_offset = dimension_->GetGlobalOptimizerOffset();
1065 SetValuesFromLP(index_to_cumul_variable_, global_offset, solver,
1066 cumul_values);
1067 SetValuesFromLP(all_break_variables_, global_offset, solver, break_values);
1068 solver->Clear();
1069 return status;
1070}
1071
1074 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
1075 const RouteDimensionTravelInfo& dimension_travel_info,
1076 const RoutingModel::ResourceGroup::Resource* resource,
1077 RoutingLinearSolverWrapper* solver, std::vector<int64_t>* cumul_values,
1078 std::vector<int64_t>* break_values) {
1079 const glop::GlopParameters original_params = GetGlopParametersForLocalLP();
1080 glop::GlopParameters packing_parameters;
1081 if (!solver->IsCPSATSolver()) {
1082 packing_parameters = original_params;
1083 packing_parameters.set_use_dual_simplex(false);
1084 packing_parameters.set_use_preprocessing(true);
1085 solver->SetParameters(packing_parameters.SerializeAsString());
1086 }
1088 vehicle, next_accessor, dimension_travel_info, resource,
1089 /*optimize_vehicle_costs=*/true, solver,
1090 /*cumul_values=*/nullptr, /*break_values=*/nullptr,
1091 /*cost=*/nullptr, /*transit_cost=*/nullptr, /*clear_lp=*/false);
1092
1094 status = PackRoutes({vehicle}, solver, packing_parameters);
1095 }
1096 if (!solver->IsCPSATSolver()) {
1097 solver->SetParameters(original_params.SerializeAsString());
1098 }
1099
1102 }
1103 const int64_t local_offset =
1104 dimension_->GetLocalOptimizerOffsetForVehicle(vehicle);
1105 SetValuesFromLP(current_route_cumul_variables_, local_offset, solver,
1106 cumul_values);
1107 SetValuesFromLP(current_route_break_variables_, local_offset, solver,
1108 break_values);
1109 solver->Clear();
1110 return status;
1111}
1112
1113DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes(
1114 std::vector<int> vehicles, RoutingLinearSolverWrapper* solver,
1115 const glop::GlopParameters& packing_parameters) {
1116 const RoutingModel* model = dimension_->model();
1117
1118 // NOTE(user): Given our constraint matrix, our problem *should* always
1119 // have an integer optimal solution, in which case we can round to the nearest
1120 // integer both for the objective constraint bound (returned by
1121 // GetObjectiveValue()) and the end cumul variable bound after minimizing
1122 // (see b/154381899 showcasing an example where std::ceil leads to an
1123 // "imperfect" packing due to rounding precision errors).
1124 // If this DCHECK ever fails, it can be removed but the code below should be
1125 // adapted to have a 2-phase approach, solving once with the rounded value as
1126 // bound and if this fails, solve again using std::ceil.
1127 DCHECK(solver->SolutionIsInteger());
1128
1129 // Minimize the route end times without increasing the cost.
1130 solver->AddObjectiveConstraint();
1131 solver->ClearObjective();
1132 for (int vehicle : vehicles) {
1134 index_to_cumul_variable_[model->End(vehicle)], 1);
1135 }
1136
1137 glop::GlopParameters current_params;
1138 const auto retry_solving = [&current_params, model, solver]() {
1139 // NOTE: To bypass some cases of false negatives due to imprecisions, we try
1140 // running Glop with a different use_dual_simplex parameter when running
1141 // into an infeasible status.
1142 current_params.set_use_dual_simplex(!current_params.use_dual_simplex());
1143 solver->SetParameters(current_params.SerializeAsString());
1144 return solver->Solve(model->RemainingTime());
1145 };
1146 if (solver->Solve(model->RemainingTime()) ==
1148 if (solver->IsCPSATSolver()) {
1150 }
1151
1152 current_params = packing_parameters;
1153 if (retry_solving() == DimensionSchedulingStatus::INFEASIBLE) {
1155 }
1156 }
1157
1158 // Maximize the route start times without increasing the cost or the route end
1159 // times.
1160 solver->ClearObjective();
1161 for (int vehicle : vehicles) {
1162 const int end_cumul_var = index_to_cumul_variable_[model->End(vehicle)];
1163 // end_cumul_var <= solver.GetValue(end_cumul_var)
1164 solver->SetVariableBounds(
1165 end_cumul_var, solver->GetVariableLowerBound(end_cumul_var),
1166 MathUtil::FastInt64Round(solver->GetValue(end_cumul_var)));
1167
1168 // Maximize the starts of the routes.
1170 index_to_cumul_variable_[model->Start(vehicle)], -1);
1171 }
1172
1173 DimensionSchedulingStatus status = solver->Solve(model->RemainingTime());
1174 if (!solver->IsCPSATSolver() &&
1176 status = retry_solving();
1177 }
1178 return status;
1179}
1180
1181#define SET_DEBUG_VARIABLE_NAME(solver, var, name) \
1182 do { \
1183 if (DEBUG_MODE) { \
1184 solver->SetVariableName(var, name); \
1185 } \
1186 } while (false)
1187
1188void DimensionCumulOptimizerCore::InitOptimizer(
1189 RoutingLinearSolverWrapper* solver) {
1190 solver->Clear();
1191 index_to_cumul_variable_.assign(dimension_->cumuls().size(), -1);
1192 max_end_cumul_ = solver->CreateNewPositiveVariable();
1193 SET_DEBUG_VARIABLE_NAME(solver, max_end_cumul_, "max_end_cumul");
1194 min_start_cumul_ = solver->CreateNewPositiveVariable();
1195 SET_DEBUG_VARIABLE_NAME(solver, min_start_cumul_, "min_start_cumul");
1196}
1197
1198bool DimensionCumulOptimizerCore::ExtractRouteCumulBounds(
1199 absl::Span<const int64_t> route, int64_t cumul_offset) {
1200 const int route_size = route.size();
1201 current_route_min_cumuls_.resize(route_size);
1202 current_route_max_cumuls_.resize(route_size);
1203
1204 // Extract cumul min/max and fixed transits from CP.
1205 for (int pos = 0; pos < route_size; ++pos) {
1206 if (!GetCumulBoundsWithOffset(*dimension_, route[pos], cumul_offset,
1207 &current_route_min_cumuls_[pos],
1208 &current_route_max_cumuls_[pos])) {
1209 return false;
1210 }
1211 }
1212 return true;
1213}
1214
1215bool DimensionCumulOptimizerCore::TightenRouteCumulBounds(
1216 const std::vector<int64_t>& route, const std::vector<int64_t>& min_transits,
1217 int64_t cumul_offset) {
1218 const int route_size = route.size();
1219 if (propagator_ != nullptr) {
1220 for (int pos = 0; pos < route_size; pos++) {
1221 const int64_t node = route[pos];
1222 current_route_min_cumuls_[pos] = propagator_->CumulMin(node);
1223 DCHECK_GE(current_route_min_cumuls_[pos], 0);
1224 current_route_max_cumuls_[pos] = propagator_->CumulMax(node);
1225 DCHECK_GE(current_route_max_cumuls_[pos], current_route_min_cumuls_[pos]);
1226 }
1227 return true;
1228 }
1229
1230 // Refine cumul bounds using
1231 // cumul[i+1] >= cumul[i] + fixed_transit[i] + slack[i].
1232 for (int pos = 1; pos < route_size; ++pos) {
1233 const int64_t slack_min = dimension_->SlackVar(route[pos - 1])->Min();
1234 current_route_min_cumuls_[pos] = std::max(
1235 current_route_min_cumuls_[pos],
1236 CapAdd(
1237 CapAdd(current_route_min_cumuls_[pos - 1], min_transits[pos - 1]),
1238 slack_min));
1239 current_route_min_cumuls_[pos] = GetFirstPossibleValueForCumulWithOffset(
1240 *dimension_, route[pos], current_route_min_cumuls_[pos], cumul_offset);
1241 if (current_route_min_cumuls_[pos] > current_route_max_cumuls_[pos]) {
1242 return false;
1243 }
1244 }
1245
1246 for (int pos = route_size - 2; pos >= 0; --pos) {
1247 // If cumul_max[pos+1] is kint64max, it will be translated to
1248 // double +infinity, so it must not constrain cumul_max[pos].
1249 if (current_route_max_cumuls_[pos + 1] <
1250 std::numeric_limits<int64_t>::max()) {
1251 const int64_t slack_min = dimension_->SlackVar(route[pos])->Min();
1252 current_route_max_cumuls_[pos] = std::min(
1253 current_route_max_cumuls_[pos],
1254 CapSub(CapSub(current_route_max_cumuls_[pos + 1], min_transits[pos]),
1255 slack_min));
1256 current_route_max_cumuls_[pos] = GetLastPossibleValueForCumulWithOffset(
1257 *dimension_, route[pos], current_route_max_cumuls_[pos],
1258 cumul_offset);
1259 if (current_route_max_cumuls_[pos] < current_route_min_cumuls_[pos]) {
1260 return false;
1261 }
1262 }
1263 }
1264 return true;
1265}
1266
1268 const std::vector<SlopeAndYIntercept>& slope_and_y_intercept) {
1269 CHECK(!slope_and_y_intercept.empty());
1270 std::vector<bool> convex(slope_and_y_intercept.size(), false);
1271 double previous_slope = std::numeric_limits<double>::max();
1272 for (int i = 0; i < slope_and_y_intercept.size(); ++i) {
1273 const auto& pair = slope_and_y_intercept[i];
1274 if (pair.slope < previous_slope) {
1275 convex[i] = true;
1276 }
1277 previous_slope = pair.slope;
1278 }
1279 return convex;
1280}
1281
1283 const RoutingModel::RouteDimensionTravelInfo::TransitionInfo::
1284 PiecewiseLinearFormulation& pwl_function,
1285 int index_start, int index_end) {
1286 if (index_end < 0) index_end = pwl_function.x_anchors.size() - 1;
1287 const int num_segments = index_end - index_start;
1288 DCHECK_GE(num_segments, 1);
1289 std::vector<SlopeAndYIntercept> slope_and_y_intercept(num_segments);
1290 for (int seg = index_start; seg < index_end; ++seg) {
1291 auto& [slope, y_intercept] = slope_and_y_intercept[seg - index_start];
1292 slope = (pwl_function.y_anchors[seg + 1] - pwl_function.y_anchors[seg]) /
1293 static_cast<double>(pwl_function.x_anchors[seg + 1] -
1294 pwl_function.x_anchors[seg]);
1295 y_intercept =
1296 pwl_function.y_anchors[seg] - slope * pwl_function.x_anchors[seg];
1297 }
1298 return slope_and_y_intercept;
1299}
1300
1301namespace {
1302
1303// Find a "good" scaling factor for constraints with non-integers coefficients.
1304// See sat::FindBestScalingAndComputeErrors() for more infos.
1305double FindBestScaling(const std::vector<double>& coefficients,
1306 const std::vector<double>& lower_bounds,
1307 const std::vector<double>& upper_bounds,
1308 int64_t max_absolute_activity,
1309 double wanted_absolute_activity_precision) {
1310 double unused_relative_coeff_error = 0;
1311 double unused_scaled_sum_error = 0;
1313 coefficients, lower_bounds, upper_bounds, max_absolute_activity,
1314 wanted_absolute_activity_precision, &unused_relative_coeff_error,
1315 &unused_scaled_sum_error);
1316}
1317
1318// Returns the value of pwl(x) with pwl a PiecewiseLinearFormulation, knowing
1319// that x ∈ [pwl.x[upper_segment_index-1], pwl.x[upper_segment_index]].
1320int64_t PieceWiseLinearFormulationValueKnownSegment(
1321 const RoutingModel::RouteDimensionTravelInfo::TransitionInfo::
1322 PiecewiseLinearFormulation& pwl,
1323 int64_t x, int upper_segment_index, double delta = 0) {
1324 DCHECK_GE(upper_segment_index, 1);
1325 DCHECK_LE(upper_segment_index, pwl.x_anchors.size() - 1);
1326 const double alpha =
1327 static_cast<double>(pwl.y_anchors[upper_segment_index] -
1328 pwl.y_anchors[upper_segment_index - 1]) /
1329 (pwl.x_anchors[upper_segment_index] -
1330 pwl.x_anchors[upper_segment_index - 1]);
1331 const double beta = pwl.y_anchors[upper_segment_index] -
1332 pwl.x_anchors[upper_segment_index] * alpha;
1333 return std::ceil(alpha * x + beta + delta);
1334}
1335
1336} // namespace
1337
1339 const RoutingModel::RouteDimensionTravelInfo::TransitionInfo::
1340 PiecewiseLinearFormulation& pwl,
1341 int64_t x, int64_t* value, double delta) {
1342 // Search for first element xi such that xi < x.
1343 const auto upper_segment =
1344 std::upper_bound(pwl.x_anchors.begin(), pwl.x_anchors.end(), x);
1345 const int upper_segment_index =
1346 std::distance(pwl.x_anchors.begin(), upper_segment);
1347
1348 // Checking bounds
1349 if (upper_segment_index == 0) {
1351 } else if (upper_segment == pwl.x_anchors.end()) {
1352 if (x == pwl.x_anchors.back()) {
1353 *value = std::ceil(pwl.y_anchors.back() + delta);
1355 }
1357 }
1358
1359 *value = PieceWiseLinearFormulationValueKnownSegment(
1360 pwl, x, upper_segment_index, delta);
1362}
1363
1365 const RoutingModel::RouteDimensionTravelInfo::TransitionInfo::
1366 PiecewiseLinearFormulation& pwl,
1367 int64_t x, double delta) {
1368 int64_t y_value;
1371 switch (status) {
1373 // The status should be specified.
1374 LOG(FATAL) << "Unspecified PiecewiseEvaluationStatus.";
1375 break;
1377 // x is in the bounds, therefore, simply return the computed value.
1378 return y_value;
1380 // In the convex case, if x <= lower_bound, the most restrictive
1381 // constraint will be the first one.
1382 return PieceWiseLinearFormulationValueKnownSegment(pwl, x, 1, delta);
1384 // In the convex case, if x >= upper_bound, the most restrictive
1385 // constraint will be the last one.
1386 return PieceWiseLinearFormulationValueKnownSegment(
1387 pwl, x, pwl.x_anchors.size() - 1, delta);
1388 }
1389}
1390
1391bool DimensionCumulOptimizerCore::SetRouteTravelConstraints(
1392 const RouteDimensionTravelInfo& dimension_travel_info,
1393 const std::vector<int>& lp_slacks,
1394 const std::vector<int64_t>& fixed_transit,
1395 RoutingLinearSolverWrapper* solver) {
1396 const std::vector<int>& lp_cumuls = current_route_cumul_variables_;
1397 const int path_size = lp_cumuls.size();
1398
1399 if (dimension_travel_info.transition_info.empty()) {
1400 // Travel is not travel-start dependent.
1401 // Add all path constraints to LP:
1402 // cumul[i] + fixed_transit[i] + slack[i] == cumul[i+1]
1403 // <=> fixed_transit[i] == cumul[i+1] - cumul[i] - slack[i].
1404 for (int pos = 0; pos < path_size - 1; ++pos) {
1405 const int ct =
1406 solver->CreateNewConstraint(fixed_transit[pos], fixed_transit[pos]);
1407 solver->SetCoefficient(ct, lp_cumuls[pos + 1], 1);
1408 solver->SetCoefficient(ct, lp_cumuls[pos], -1);
1409 solver->SetCoefficient(ct, lp_slacks[pos], -1);
1410 }
1411 return true;
1412 }
1413
1414 // See:
1415 // https://docs.google.com/document/d/1niFfbCNjh70VepvVk4Ft8QgQAcrA5hrye-Vu_k0VgWw/edit?usp=sharing&resourcekey=0-rUJUHuWPjn1wWZaA0uDdtg
1416 for (int pos = 0; pos < path_size - 1; ++pos) {
1417 // Add a traffic-aware compression cost, for every path.
1418 // compression_cost represents the cost of the ABSOLUTE compression of the
1419 // travel.
1420 const int compression_cost = solver->CreateNewPositiveVariable();
1421 SET_DEBUG_VARIABLE_NAME(solver, compression_cost,
1422 absl::StrFormat("compression_cost(%ld)", pos));
1423 // relative_compression_cost represents the cost of the RELATIVE compression
1424 // of the travel. This is the real cost used. In theory,
1425 // relative_compression_cost = compression_cost / Tᵣ, where Tᵣ is the travel
1426 // value (computed with the PWL). In practice, this requires a
1427 // multiplication which is slow, so several approximations are implemented
1428 // below.
1429 const int relative_compression_cost = solver->CreateNewPositiveVariable();
1431 solver, relative_compression_cost,
1432 absl::StrFormat("relative_compression_cost(%ld)", pos));
1433
1434 const RoutingModel::RouteDimensionTravelInfo::TransitionInfo&
1435 transition_info = dimension_travel_info.transition_info[pos];
1436 const RouteDimensionTravelInfo::TransitionInfo::PiecewiseLinearFormulation&
1437 travel_function = transition_info.travel_start_dependent_travel;
1438 const int num_pwl_anchors = travel_function.x_anchors.size();
1439 DCHECK_GE(num_pwl_anchors, 2)
1440 << "Travel value PWL must have at least 2 points";
1441 DCHECK_EQ(num_pwl_anchors, travel_function.y_anchors.size())
1442 << "Travel value PWL must have as many x anchors than y.";
1443
1444 // 1. Create the travel value variable and set its constraints.
1445 // 1.a. Create Variables for the start and value of a travel
1446 const int64_t pre_travel_transit = transition_info.pre_travel_transit_value;
1447 const int64_t post_travel_transit =
1448 transition_info.post_travel_transit_value;
1449 const int64_t compressed_travel_value_lower_bound =
1450 transition_info.compressed_travel_value_lower_bound;
1451 const int64_t travel_value_upper_bound =
1452 dimension_travel_info.transition_info[pos].travel_value_upper_bound;
1453 // The lower bound of travel_value is already implemented by constraints as
1454 // travel_value >= compressed_travel_value (defined below) and
1455 // compressed_travel_value has compressed_travel_value_lower_bound as a
1456 // lower bound. The bound is added for the sake of clarity and being
1457 // explicit.
1458 const int travel_value = solver->AddVariable(
1459 compressed_travel_value_lower_bound, travel_value_upper_bound);
1460 SET_DEBUG_VARIABLE_NAME(solver, travel_value,
1461 absl::StrFormat("travel_value(%ld)", pos));
1462 const int travel_start = solver->AddVariable(
1463 current_route_min_cumuls_[pos] + pre_travel_transit,
1464 current_route_max_cumuls_[pos + 1] - post_travel_transit -
1465 compressed_travel_value_lower_bound);
1466 SET_DEBUG_VARIABLE_NAME(solver, travel_start,
1467 absl::StrFormat("travel_start(%ld)", pos));
1468 // travel_start = cumul[pos] + pre_travel[pos]
1469 // This becomes: pre_travel[pos] = travel_start - cumul[pos]
1470 solver->AddLinearConstraint(pre_travel_transit, pre_travel_transit,
1471 {{travel_start, 1}, {lp_cumuls[pos], -1}});
1472
1473 // Find segments that are in bounds
1474 // Only the segments in [index_anchor_start, index_anchor_end[ are in
1475 // bounds, the others can therefore be discarded.
1476 int index_anchor_start = 0;
1477 while (index_anchor_start < num_pwl_anchors - 1 &&
1478 travel_function.x_anchors[index_anchor_start + 1] <=
1479 current_route_min_cumuls_[pos] + pre_travel_transit) {
1480 ++index_anchor_start;
1481 }
1482 int index_anchor_end = num_pwl_anchors - 1;
1483 while (index_anchor_end > 0 &&
1484 travel_function.x_anchors[index_anchor_end - 1] >=
1485 current_route_max_cumuls_[pos] + pre_travel_transit) {
1486 --index_anchor_end;
1487 }
1488 // Check that there is at least one segment.
1489 if (index_anchor_start >= index_anchor_end) return false;
1490
1491 // Precompute the slopes and y-intercept as they will be used to detect
1492 // convexities and in the constraints.
1493 const std::vector<SlopeAndYIntercept> slope_and_y_intercept =
1495 travel_function, index_anchor_start, index_anchor_end);
1496
1497 // Optimize binary variables by detecting convexities
1498 const std::vector<bool> convexities =
1499 SlopeAndYInterceptToConvexityRegions(slope_and_y_intercept);
1500
1501 int nb_bin_variables = 0;
1502 for (const bool convexity : convexities) {
1503 if (convexity) {
1504 ++nb_bin_variables;
1505 if (nb_bin_variables >= 2) break;
1506 }
1507 }
1508 const bool need_bins = (nb_bin_variables > 1);
1509 // 1.b. Create a constraint so that the start belongs to only one segment
1510 const int travel_start_in_one_segment_ct =
1511 need_bins ? solver->CreateNewConstraint(1, 1)
1512 : -1; // -1 is a placeholder value here
1513
1514 int belongs_to_this_segment_var;
1515 for (int seg = 0; seg < convexities.size(); ++seg) {
1516 if (need_bins && convexities[seg]) {
1517 belongs_to_this_segment_var = solver->AddVariable(0, 1);
1519 solver, belongs_to_this_segment_var,
1520 absl::StrFormat("travel_start(%ld)belongs_to_seg(%ld)", pos, seg));
1521 solver->SetCoefficient(travel_start_in_one_segment_ct,
1522 belongs_to_this_segment_var, 1);
1523
1524 // 1.c. Link the binary variable to the departure time
1525 // If the travel_start value is outside the PWL, the closest segment
1526 // will be used. This is why some bounds are infinite.
1527 const int64_t lower_bound_interval =
1528 seg > 0 ? travel_function.x_anchors[index_anchor_start + seg]
1529 : current_route_min_cumuls_[pos] + pre_travel_transit;
1530 int64_t end_of_seg = seg + 1;
1531 while (end_of_seg < num_pwl_anchors - 1 && !convexities[end_of_seg]) {
1532 ++end_of_seg;
1533 }
1534 const int64_t higher_bound_interval =
1535 end_of_seg < num_pwl_anchors - 1
1536 ? travel_function.x_anchors[index_anchor_start + end_of_seg]
1537 : current_route_max_cumuls_[pos] + pre_travel_transit;
1538 const int travel_start_in_segment_ct = solver->AddLinearConstraint(
1539 lower_bound_interval, higher_bound_interval, {{travel_start, 1}});
1540 solver->SetEnforcementLiteral(travel_start_in_segment_ct,
1541 belongs_to_this_segment_var);
1542 }
1543
1544 // 1.d. Compute the slope and y_intercept, the
1545 // coefficient used in the constraint, for each segment.
1546 const auto [slope, y_intercept] = slope_and_y_intercept[seg];
1547 // Starting later should always mean arriving later
1548 DCHECK_GE(slope, -1.0) << "Travel value PWL should have a slope >= -1";
1549
1550 // 1.e. Define the linearization of travel_value
1551 // travel_value - slope * travel_start[pos] = y_intercept, for each
1552 // segment. In order to have a softer constraint, we only impose:
1553 // travel_value - slope * travel_start[pos] >= y_intercept
1554 // and since the cost is increasing in the travel_value, it will
1555 // Minimize it. In addition, since we are working with integers, we
1556 // add a relaxation of 0.5 which gives:
1557 // travel_value - slope * travel_start[pos] >= y_intercept - 0.5.
1558 const double upper_bound = current_route_max_cumuls_[pos];
1559 const double factor = FindBestScaling(
1560 {1.0, -slope, y_intercept - 0.5}, /*lower_bounds=*/
1561 {static_cast<double>(compressed_travel_value_lower_bound), 0, 1},
1562 /*upper_bounds=*/
1563 {static_cast<double>(travel_value_upper_bound), upper_bound, 1},
1564 /*max_absolute_activity=*/(int64_t{1} << 62),
1565 /*wanted_absolute_activity_precision=*/1e-3);
1566 // If no correct scaling is found, factor can be equal to 0. This will be
1567 // translated as an unfeasible model as, it will not constraint the
1568 // travel_value with a factor of 0.
1569 if (factor <= 0) return false;
1570
1571 const int linearization_ct = solver->AddLinearConstraint(
1572 MathUtil::FastInt64Round(factor * (y_intercept - 0.5)),
1573 std::numeric_limits<int64_t>::max(),
1574 {{travel_value, MathUtil::FastInt64Round(factor)},
1575 {travel_start, MathUtil::FastInt64Round(-factor * slope)}});
1576 if (need_bins) {
1577 solver->SetEnforcementLiteral(linearization_ct,
1578 belongs_to_this_segment_var);
1579 }
1580
1581 // ====== UNCOMMENT TO USE ORDER0 ERROR APPROXIMATION ===== //
1582 // Normally cost_scaled = C₂×(Tᵣ - T)²/Tᵣ
1583 // but here we approximate it as cost_scaled = C₂×(Tᵣ - T)²/Tm
1584 // with Tm the average travel value of this segment.
1585 // Therefore, we compute cost_scaled = cost / Tm
1586 // So the cost_function must be defined as cost = C₂×(Tᵣ - T)²
1587 // const int64_t Tm = (transit_function.y_anchors[seg] +
1588 // transit_function.y_anchors[seg + 1]) / 2; The constraint is
1589 // implemented as: cost_scaled * Tm >= cost const int cost_ct =
1590 // solver->AddLinearConstraint(0, std::numeric_limits<int64_t>::max(),
1591 // {{cost_scaled, Tm}, {cost, -1}});
1592 // solver->SetEnforcementLiteral(cost_ct, belongs_to_this_segment_var);
1593 }
1594
1595 // 2. Create a variable for the compressed_travel_value.
1596 // cumul[pos + 1] = cumul[pos] + slack[pos] + pre_travel_transit[pos] +
1597 // compressed_travel_value[pos] + post_travel_transit[pos] This becomes:
1598 // post_travel_transit[pos] + pre_travel_transit[pos] = cumul[pos + 1] -
1599 // cumul[pos] - slack[pos] - compressed_travel_value[pos] The higher bound
1600 // of compressed_travel_value is already implemented by constraints as
1601 // travel_compression_absolute = travel_value - compressed_travel_value > 0
1602 // (see below) and travel_value has travel_value_upper_bound as an upper
1603 // bound. The bound is added for the sake of clarity and being explicit.
1604 const int compressed_travel_value = solver->AddVariable(
1605 compressed_travel_value_lower_bound, travel_value_upper_bound);
1607 solver, compressed_travel_value,
1608 absl::StrFormat("compressed_travel_value(%ld)", pos));
1609 solver->AddLinearConstraint(post_travel_transit + pre_travel_transit,
1610 post_travel_transit + pre_travel_transit,
1611 {{compressed_travel_value, -1},
1612 {lp_cumuls[pos + 1], 1},
1613 {lp_cumuls[pos], -1},
1614 {lp_slacks[pos], -1}});
1615
1616 // 2. Create the travel value compression variable
1617 // travel_compression_absolute == travel_value - compressed_travel_value
1618 // This becomes: 0 = travel_compression_absolute - travel_value +
1619 // compressed_travel_value travel_compression_absolute must be positive or
1620 // equal to 0.
1621 const int travel_compression_absolute = solver->AddVariable(
1622 0, travel_value_upper_bound - compressed_travel_value_lower_bound);
1624 solver, travel_compression_absolute,
1625 absl::StrFormat("travel_compression_absolute(%ld)", pos));
1626
1627 solver->AddLinearConstraint(0, 0,
1628 {{travel_compression_absolute, 1},
1629 {travel_value, -1},
1630 {compressed_travel_value, 1}});
1631
1632 // 3. Add a cost per unit of travel
1633 // The travel_cost_coefficient is set with the travel_value and not the
1634 // compressed_travel_value to not give the incentive to compress a little
1635 // bit in order to same some cost per travel.
1636 solver->SetObjectiveCoefficient(
1637 travel_value, dimension_travel_info.travel_cost_coefficient);
1638
1639 // 4. Adds a convex cost in epsilon
1640 // Here we DCHECK that the cost function is indeed convex
1641 const RouteDimensionTravelInfo::TransitionInfo::PiecewiseLinearFormulation&
1642 cost_function =
1643 dimension_travel_info.transition_info[pos].travel_compression_cost;
1644 const std::vector<SlopeAndYIntercept> cost_slope_and_y_intercept =
1646 const double cost_max = ComputeConvexPiecewiseLinearFormulationValue(
1647 cost_function,
1648 travel_value_upper_bound - compressed_travel_value_lower_bound);
1649 double previous_slope = 0;
1650 for (int seg = 0; seg < cost_function.x_anchors.size() - 1; ++seg) {
1651 const auto [slope, y_intercept] = cost_slope_and_y_intercept[seg];
1652 // Check convexity
1653 DCHECK_GE(slope, previous_slope)
1654 << "Compression error is not convex. Segment " << (1 + seg)
1655 << " out of " << (cost_function.x_anchors.size() - 1);
1656 previous_slope = slope;
1657 const double factor = FindBestScaling(
1658 {1.0, -slope, y_intercept}, /*lower_bounds=*/
1659 {0, static_cast<double>(compressed_travel_value_lower_bound), 1},
1660 /*upper_bounds=*/
1661 {cost_max, static_cast<double>(travel_value_upper_bound), 1},
1662 /*max_absolute_activity=*/(static_cast<int64_t>(1) << 62),
1663 /*wanted_absolute_activity_precision=*/1e-3);
1664 // If no correct scaling is found, factor can be equal to 0. This will be
1665 // translated as an unfeasible model as, it will not constraint the
1666 // compression_cost with a factor of 0.
1667 if (factor <= 0) return false;
1668
1669 solver->AddLinearConstraint(
1670 MathUtil::FastInt64Round(factor * y_intercept),
1671 std::numeric_limits<int64_t>::max(),
1672 {{compression_cost, std::round(factor)},
1673 {travel_compression_absolute,
1674 MathUtil::FastInt64Round(-factor * slope)}});
1675 }
1676 // ====== UNCOMMENT TO USE PRODUCT TO COMPUTE THE EXACT ERROR ===== //
1677 // Normally cost_scaled = C₂×(Tᵣ - T)²/Tᵣ
1678 // and here we compute cost_scaled = cost / Tᵣ
1679 // So the cost_function must be defined as cost = C₂×(Tᵣ - T)²
1680 // const int prod = solver->CreateNewPositiveVariable();
1681 // solver->AddProductConstraint(prod, {cost_scaled, travel_value});
1682 // The constraint is implemented as: cost_scaled * Tᵣ >= cost
1683 // solver->AddLinearConstraint(0, std::numeric_limits<int64_t>::max(),
1684 // {{prod, 1}, {cost, -1}});
1685
1686 // ====== UNCOMMENT TO USE AVERAGE ERROR APPROXIMATION ===== //
1687 // Normally cost_scaled = C₂×(Tᵣ - T)²/Tᵣ
1688 // but here we approximate it as cost_scaled = C₂×(Tᵣ - T)²/Tₐ
1689 // with Tₐ the average travel value (on all the segments).
1690 // Since we do not have access to Tₐ here, we define cost_scaled as
1691 // cost_scaled = cost. So the cost_function must be defined as cost =
1692 // C₂×(Tᵣ - T)²/Tₐ The constraint is implemented as: cost_scaled >= cost
1693 solver->AddLinearConstraint(
1694 0, std::numeric_limits<int64_t>::max(),
1695 {{relative_compression_cost, 1}, {compression_cost, -1}});
1696
1697 solver->SetObjectiveCoefficient(relative_compression_cost, 1.0);
1698 }
1699 return true;
1700}
1701
1702namespace {
1703bool RouteIsValid(const RoutingModel& model, int vehicle,
1704 const std::function<int64_t(int64_t)>& next_accessor) {
1705 absl::flat_hash_set<int64_t> visited;
1706 int node = model.Start(vehicle);
1707 visited.insert(node);
1708 while (!model.IsEnd(node)) {
1709 node = next_accessor(node);
1710 if (visited.contains(node)) return false;
1711 visited.insert(node);
1712 }
1713 return visited.size() >= 2;
1714}
1715} // namespace
1716
1717bool DimensionCumulOptimizerCore::SetRouteCumulConstraints(
1718 int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
1719 const std::function<int64_t(int64_t, int64_t)>& transit_accessor,
1720 const RouteDimensionTravelInfo& dimension_travel_info, int64_t cumul_offset,
1721 bool optimize_costs, RoutingLinearSolverWrapper* solver,
1722 int64_t* route_transit_cost, int64_t* route_cost_offset) {
1723 RoutingModel* const model = dimension_->model();
1724 // Extract the vehicle's path from next_accessor.
1725 std::vector<int64_t> path;
1726 {
1727 DCHECK(RouteIsValid(*model, vehicle, next_accessor));
1728 int node = model->Start(vehicle);
1729 path.push_back(node);
1730 while (!model->IsEnd(node)) {
1731 node = next_accessor(node);
1732 path.push_back(node);
1733 }
1734 DCHECK_GE(path.size(), 2);
1735 }
1736 const int path_size = path.size();
1737
1738 std::vector<int64_t> fixed_transit(path_size - 1);
1739 int64_t total_fixed_transit = 0;
1740 {
1741 for (int pos = 1; pos < path_size; ++pos) {
1742 int64_t& transit = fixed_transit[pos - 1];
1743 transit = transit_accessor(path[pos - 1], path[pos]);
1744 total_fixed_transit = CapAdd(total_fixed_transit, transit);
1745 }
1746 }
1747 if (!ExtractRouteCumulBounds(path, cumul_offset)) {
1748 return false;
1749 }
1750 if (dimension_travel_info.transition_info.empty()) {
1751 if (!TightenRouteCumulBounds(path, fixed_transit, cumul_offset)) {
1752 return false;
1753 }
1754 } else {
1755 // Tighten the bounds with the lower bound of the transit value
1756 std::vector<int64_t> min_transit(path_size - 1);
1757 for (int pos = 0; pos < path_size - 1; ++pos) {
1758 const RouteDimensionTravelInfo::TransitionInfo& transition =
1759 dimension_travel_info.transition_info[pos];
1760 min_transit[pos] = transition.pre_travel_transit_value +
1761 transition.compressed_travel_value_lower_bound +
1762 transition.post_travel_transit_value;
1763 }
1764 if (!TightenRouteCumulBounds(path, min_transit, cumul_offset)) {
1765 return false;
1766 }
1767 }
1768
1769 // LP Model variables, current_route_cumul_variables_ and lp_slacks.
1770 // Create LP variables for cumuls.
1771 std::vector<int>& lp_cumuls = current_route_cumul_variables_;
1772 lp_cumuls.assign(path_size, -1);
1773 for (int pos = 0; pos < path_size; ++pos) {
1774 const int lp_cumul = solver->CreateNewPositiveVariable();
1775 SET_DEBUG_VARIABLE_NAME(solver, lp_cumul,
1776 absl::StrFormat("lp_cumul(%ld)", pos));
1777 index_to_cumul_variable_[path[pos]] = lp_cumul;
1778 lp_cumuls[pos] = lp_cumul;
1779 if (!solver->SetVariableBounds(lp_cumul, current_route_min_cumuls_[pos],
1780 current_route_max_cumuls_[pos])) {
1781 return false;
1782 }
1783 const SortedDisjointIntervalList& forbidden =
1784 dimension_->forbidden_intervals()[path[pos]];
1785 if (forbidden.NumIntervals() > 0) {
1786 std::vector<int64_t> starts;
1787 std::vector<int64_t> ends;
1788 for (const ClosedInterval interval :
1789 dimension_->GetAllowedIntervalsInRange(
1790 path[pos], CapAdd(current_route_min_cumuls_[pos], cumul_offset),
1791 CapAdd(current_route_max_cumuls_[pos], cumul_offset))) {
1792 starts.push_back(CapSub(interval.start, cumul_offset));
1793 ends.push_back(CapSub(interval.end, cumul_offset));
1794 }
1795 solver->SetVariableDisjointBounds(lp_cumul, starts, ends);
1796 }
1797 }
1798 // Create LP variables for slacks.
1799 std::vector<int> lp_slacks(path_size - 1, -1);
1800 for (int pos = 0; pos < path_size - 1; ++pos) {
1801 const IntVar* cp_slack = dimension_->SlackVar(path[pos]);
1802 lp_slacks[pos] = solver->CreateNewPositiveVariable();
1803 SET_DEBUG_VARIABLE_NAME(solver, lp_slacks[pos],
1804 absl::StrFormat("lp_slacks(%ld)", pos));
1805 if (!solver->SetVariableBounds(lp_slacks[pos], cp_slack->Min(),
1806 cp_slack->Max())) {
1807 return false;
1808 }
1809 }
1810
1811 if (!SetRouteTravelConstraints(dimension_travel_info, lp_slacks,
1812 fixed_transit, solver)) {
1813 return false;
1814 }
1815
1816 if (route_cost_offset != nullptr) *route_cost_offset = 0;
1817 if (optimize_costs) {
1818 // Add soft upper bounds.
1819 for (int pos = 0; pos < path_size; ++pos) {
1820 if (!dimension_->HasCumulVarSoftUpperBound(path[pos])) continue;
1821 const int64_t coef =
1822 dimension_->GetCumulVarSoftUpperBoundCoefficient(path[pos]);
1823 if (coef == 0) continue;
1824 int64_t bound = dimension_->GetCumulVarSoftUpperBound(path[pos]);
1825 if (bound < cumul_offset && route_cost_offset != nullptr) {
1826 // Add coef * (cumul_offset - bound) to the cost offset.
1827 *route_cost_offset = CapAdd(*route_cost_offset,
1828 CapProd(CapSub(cumul_offset, bound), coef));
1829 }
1830 bound = std::max<int64_t>(0, CapSub(bound, cumul_offset));
1831 if (current_route_max_cumuls_[pos] <= bound) {
1832 // constraint is never violated.
1833 continue;
1834 }
1835 const int soft_ub_diff = solver->CreateNewPositiveVariable();
1836 SET_DEBUG_VARIABLE_NAME(solver, soft_ub_diff,
1837 absl::StrFormat("soft_ub_diff(%ld)", pos));
1838 solver->SetObjectiveCoefficient(soft_ub_diff, coef);
1839 // cumul - soft_ub_diff <= bound.
1840 const int ct = solver->CreateNewConstraint(
1841 std::numeric_limits<int64_t>::min(), bound);
1842 solver->SetCoefficient(ct, lp_cumuls[pos], 1);
1843 solver->SetCoefficient(ct, soft_ub_diff, -1);
1844 }
1845 // Add soft lower bounds.
1846 for (int pos = 0; pos < path_size; ++pos) {
1847 if (!dimension_->HasCumulVarSoftLowerBound(path[pos])) continue;
1848 const int64_t coef =
1849 dimension_->GetCumulVarSoftLowerBoundCoefficient(path[pos]);
1850 if (coef == 0) continue;
1851 const int64_t bound = std::max<int64_t>(
1852 0, CapSub(dimension_->GetCumulVarSoftLowerBound(path[pos]),
1853 cumul_offset));
1854 if (current_route_min_cumuls_[pos] >= bound) {
1855 // constraint is never violated.
1856 continue;
1857 }
1858 const int soft_lb_diff = solver->CreateNewPositiveVariable();
1859 SET_DEBUG_VARIABLE_NAME(solver, soft_lb_diff,
1860 absl::StrFormat("soft_lb_diff(%ld)", pos));
1861 solver->SetObjectiveCoefficient(soft_lb_diff, coef);
1862 // bound - cumul <= soft_lb_diff
1863 const int ct = solver->CreateNewConstraint(
1864 bound, std::numeric_limits<int64_t>::max());
1865 solver->SetCoefficient(ct, lp_cumuls[pos], 1);
1866 solver->SetCoefficient(ct, soft_lb_diff, 1);
1867 }
1868 }
1869 // Add pickup and delivery limits.
1870 std::vector<int> visited_pairs;
1871 StoreVisitedPickupDeliveryPairsOnRoute(
1872 *dimension_, vehicle, next_accessor, &visited_pairs,
1873 &visited_pickup_delivery_indices_for_pair_);
1874 for (int pair_index : visited_pairs) {
1875 const int64_t pickup_index =
1876 visited_pickup_delivery_indices_for_pair_[pair_index].first;
1877 const int64_t delivery_index =
1878 visited_pickup_delivery_indices_for_pair_[pair_index].second;
1879 visited_pickup_delivery_indices_for_pair_[pair_index] = {-1, -1};
1880
1881 DCHECK_GE(pickup_index, 0);
1882 if (delivery_index < 0) {
1883 // We didn't encounter a delivery for this pickup.
1884 continue;
1885 }
1886
1887 const int64_t limit = dimension_->GetPickupToDeliveryLimitForPair(
1888 pair_index,
1889 model->GetPickupPositions(pickup_index)[0].alternative_index,
1890 model->GetDeliveryPositions(delivery_index)[0].alternative_index);
1891 if (limit < std::numeric_limits<int64_t>::max()) {
1892 // delivery_cumul - pickup_cumul <= limit.
1893 const int ct = solver->CreateNewConstraint(
1894 std::numeric_limits<int64_t>::min(), limit);
1895 solver->SetCoefficient(ct, index_to_cumul_variable_[delivery_index], 1);
1896 solver->SetCoefficient(ct, index_to_cumul_variable_[pickup_index], -1);
1897 }
1898 }
1899
1900 // Add span bound constraint.
1901 const int64_t span_bound = dimension_->GetSpanUpperBoundForVehicle(vehicle);
1902 if (span_bound < std::numeric_limits<int64_t>::max()) {
1903 // end_cumul - start_cumul <= bound
1904 const int ct = solver->CreateNewConstraint(
1905 std::numeric_limits<int64_t>::min(), span_bound);
1906 solver->SetCoefficient(ct, lp_cumuls.back(), 1);
1907 solver->SetCoefficient(ct, lp_cumuls.front(), -1);
1908 }
1909 // Add span and slack costs.
1910 // NOTE: The fixed transit is removed from the span cost since it doesn't
1911 // affect the optimization of the scheduling of the route.
1912 const int64_t span_cost_coef =
1913 dimension_->GetSpanCostCoefficientForVehicle(vehicle);
1914 const int64_t slack_cost_coef = CapAdd(
1915 span_cost_coef, dimension_->GetSlackCostCoefficientForVehicle(vehicle));
1916 if (optimize_costs && slack_cost_coef > 0) {
1917 // span_without_fixed_transit_var =
1918 // end_cumul - start_cumul - total_fixed_transit
1919 const int span_without_fixed_transit_var =
1920 solver->CreateNewPositiveVariable();
1921 SET_DEBUG_VARIABLE_NAME(solver, span_without_fixed_transit_var,
1922 "span_without_fixed_transit_var");
1923 solver->AddLinearConstraint(total_fixed_transit, total_fixed_transit,
1924 {{lp_cumuls.back(), 1},
1925 {lp_cumuls.front(), -1},
1926 {span_without_fixed_transit_var, -1}});
1927 solver->SetObjectiveCoefficient(span_without_fixed_transit_var,
1928 slack_cost_coef);
1929 }
1930 // Add soft span cost.
1931 if (optimize_costs && dimension_->HasSoftSpanUpperBounds()) {
1932 const BoundCost bound_cost =
1933 dimension_->GetSoftSpanUpperBoundForVehicle(vehicle);
1934 if (bound_cost.bound < std::numeric_limits<int64_t>::max() &&
1935 bound_cost.cost > 0) {
1936 const int span_violation = solver->CreateNewPositiveVariable();
1937 SET_DEBUG_VARIABLE_NAME(solver, span_violation, "span_violation");
1938 // end - start <= bound + span_violation
1939 const int violation = solver->CreateNewConstraint(
1940 std::numeric_limits<int64_t>::min(), bound_cost.bound);
1941 solver->SetCoefficient(violation, lp_cumuls.back(), 1.0);
1942 solver->SetCoefficient(violation, lp_cumuls.front(), -1.0);
1943 solver->SetCoefficient(violation, span_violation, -1.0);
1944 // Add span_violation * cost to objective.
1945 solver->SetObjectiveCoefficient(span_violation, bound_cost.cost);
1946 }
1947 }
1948 // Add global span constraint.
1949 if (optimize_costs && dimension_->global_span_cost_coefficient() > 0) {
1950 // min_start_cumul_ <= cumuls[start]
1951 int ct =
1952 solver->CreateNewConstraint(std::numeric_limits<int64_t>::min(), 0);
1953 solver->SetCoefficient(ct, min_start_cumul_, 1);
1954 solver->SetCoefficient(ct, lp_cumuls.front(), -1);
1955 // max_end_cumul_ >= cumuls[end]
1956 ct = solver->CreateNewConstraint(0, std::numeric_limits<int64_t>::max());
1957 solver->SetCoefficient(ct, max_end_cumul_, 1);
1958 solver->SetCoefficient(ct, lp_cumuls.back(), -1);
1959 }
1960 // Fill transit cost if specified.
1961 if (route_transit_cost != nullptr) {
1962 if (optimize_costs && span_cost_coef > 0) {
1963 *route_transit_cost = CapProd(total_fixed_transit, span_cost_coef);
1964 } else {
1965 *route_transit_cost = 0;
1966 }
1967 }
1968 // For every break that must be inside the route, the duration of that break
1969 // must be flowed in the slacks of arcs that can intersect the break.
1970 // This LP modelization is correct but not complete:
1971 // can miss some cases where the breaks cannot fit.
1972 // TODO(user): remove the need for returns in the code below.
1973 current_route_break_variables_.clear();
1974 if (!dimension_->HasBreakConstraints()) return true;
1975 const std::vector<IntervalVar*>& breaks =
1976 dimension_->GetBreakIntervalsOfVehicle(vehicle);
1977 const int num_breaks = breaks.size();
1978 // When there are no breaks, only break distance needs to be modeled,
1979 // and it reduces to a span maximum.
1980 // TODO(user): Also add the case where no breaks can intersect the route.
1981 if (num_breaks == 0) {
1982 int64_t maximum_route_span = std::numeric_limits<int64_t>::max();
1983 for (const auto& distance_duration :
1984 dimension_->GetBreakDistanceDurationOfVehicle(vehicle)) {
1985 maximum_route_span =
1986 std::min(maximum_route_span, distance_duration.first);
1987 }
1988 if (maximum_route_span < std::numeric_limits<int64_t>::max()) {
1989 const int ct = solver->CreateNewConstraint(
1990 std::numeric_limits<int64_t>::min(), maximum_route_span);
1991 solver->SetCoefficient(ct, lp_cumuls.back(), 1);
1992 solver->SetCoefficient(ct, lp_cumuls.front(), -1);
1993 }
1994 return true;
1995 }
1996 // Gather visit information: the visit of node i has [start, end) =
1997 // [cumul[i] - post_travel[i-1], cumul[i] + pre_travel[i]).
1998 // Breaks cannot overlap those visit intervals.
1999 std::vector<int64_t> pre_travel(path_size - 1, 0);
2000 std::vector<int64_t> post_travel(path_size - 1, 0);
2001 {
2002 const int pre_travel_index =
2003 dimension_->GetPreTravelEvaluatorOfVehicle(vehicle);
2004 if (pre_travel_index != -1) {
2005 FillPathEvaluation(path, model->TransitCallback(pre_travel_index),
2006 &pre_travel);
2007 }
2008 const int post_travel_index =
2009 dimension_->GetPostTravelEvaluatorOfVehicle(vehicle);
2010 if (post_travel_index != -1) {
2011 FillPathEvaluation(path, model->TransitCallback(post_travel_index),
2012 &post_travel);
2013 }
2014 }
2015 // If the solver is CPSAT, it will need to represent the times at which
2016 // breaks are scheduled, those variables are used both in the pure breaks
2017 // part and in the break distance part of the model.
2018 // Otherwise, it doesn't need the variables and they are not created.
2019 std::vector<int> lp_break_start;
2020 std::vector<int> lp_break_duration;
2021 std::vector<int> lp_break_end;
2022 if (solver->IsCPSATSolver()) {
2023 lp_break_start.resize(num_breaks, -1);
2024 lp_break_duration.resize(num_breaks, -1);
2025 lp_break_end.resize(num_breaks, -1);
2026 }
2027
2028 std::vector<int> slack_exact_lower_bound_ct(path_size - 1, -1);
2029 std::vector<int> slack_linear_lower_bound_ct(path_size - 1, -1);
2030
2031 const int64_t vehicle_start_min = current_route_min_cumuls_.front();
2032 const int64_t vehicle_start_max = current_route_max_cumuls_.front();
2033 const int64_t vehicle_end_min = current_route_min_cumuls_.back();
2034 const int64_t vehicle_end_max = current_route_max_cumuls_.back();
2035 const int all_break_variables_offset =
2036 vehicle_to_all_break_variables_offset_[vehicle];
2037 for (int br = 0; br < num_breaks; ++br) {
2038 const IntervalVar& break_var = *breaks[br];
2039 if (!break_var.MustBePerformed()) continue;
2040 const int64_t break_start_min = CapSub(break_var.StartMin(), cumul_offset);
2041 const int64_t break_start_max = CapSub(break_var.StartMax(), cumul_offset);
2042 const int64_t break_end_min = CapSub(break_var.EndMin(), cumul_offset);
2043 const int64_t break_end_max = CapSub(break_var.EndMax(), cumul_offset);
2044 const int64_t break_duration_min = break_var.DurationMin();
2045 const int64_t break_duration_max = break_var.DurationMax();
2046 // The CPSAT solver encodes all breaks that can intersect the route,
2047 // the LP solver only encodes the breaks that must intersect the route.
2048 if (solver->IsCPSATSolver()) {
2049 if (break_end_max <= vehicle_start_min ||
2050 vehicle_end_max <= break_start_min) {
2051 all_break_variables_[all_break_variables_offset + 2 * br] = -1;
2052 all_break_variables_[all_break_variables_offset + 2 * br + 1] = -1;
2053 current_route_break_variables_.push_back(-1);
2054 current_route_break_variables_.push_back(-1);
2055 continue;
2056 }
2057 lp_break_start[br] =
2058 solver->AddVariable(break_start_min, break_start_max);
2059 SET_DEBUG_VARIABLE_NAME(solver, lp_break_start[br],
2060 absl::StrFormat("lp_break_start(%ld)", br));
2061 lp_break_end[br] = solver->AddVariable(break_end_min, break_end_max);
2062 SET_DEBUG_VARIABLE_NAME(solver, lp_break_end[br],
2063 absl::StrFormat("lp_break_end(%ld)", br));
2064 lp_break_duration[br] =
2065 solver->AddVariable(break_duration_min, break_duration_max);
2066 SET_DEBUG_VARIABLE_NAME(solver, lp_break_duration[br],
2067 absl::StrFormat("lp_break_duration(%ld)", br));
2068 // start + duration = end.
2069 solver->AddLinearConstraint(0, 0,
2070 {{lp_break_end[br], 1},
2071 {lp_break_start[br], -1},
2072 {lp_break_duration[br], -1}});
2073 // Record index of variables
2074 all_break_variables_[all_break_variables_offset + 2 * br] =
2075 lp_break_start[br];
2076 all_break_variables_[all_break_variables_offset + 2 * br + 1] =
2077 lp_break_end[br];
2078 current_route_break_variables_.push_back(lp_break_start[br]);
2079 current_route_break_variables_.push_back(lp_break_end[br]);
2080 } else {
2081 if (break_end_min <= vehicle_start_max ||
2082 vehicle_end_min <= break_start_max) {
2083 all_break_variables_[all_break_variables_offset + 2 * br] = -1;
2084 all_break_variables_[all_break_variables_offset + 2 * br + 1] = -1;
2085 current_route_break_variables_.push_back(-1);
2086 current_route_break_variables_.push_back(-1);
2087 continue;
2088 }
2089 }
2090
2091 // Create a constraint for every break, that forces it to be scheduled
2092 // in exactly one place, i.e. one slack or before/after the route.
2093 // sum_i break_in_slack_i == 1.
2094 const int break_in_one_slack_ct = solver->CreateNewConstraint(1, 1);
2095
2096 if (solver->IsCPSATSolver()) {
2097 // Break can be before route.
2098 if (break_end_min <= vehicle_start_max) {
2099 const int ct = solver->AddLinearConstraint(
2100 0, std::numeric_limits<int64_t>::max(),
2101 {{lp_cumuls.front(), 1}, {lp_break_end[br], -1}});
2102 const int break_is_before_route = solver->AddVariable(0, 1);
2104 solver, break_is_before_route,
2105 absl::StrFormat("break_is_before_route(%ld)", br));
2106 solver->SetEnforcementLiteral(ct, break_is_before_route);
2107 solver->SetCoefficient(break_in_one_slack_ct, break_is_before_route, 1);
2108 }
2109 // Break can be after route.
2110 if (vehicle_end_min <= break_start_max) {
2111 const int ct = solver->AddLinearConstraint(
2112 0, std::numeric_limits<int64_t>::max(),
2113 {{lp_break_start[br], 1}, {lp_cumuls.back(), -1}});
2114 const int break_is_after_route = solver->AddVariable(0, 1);
2116 solver, break_is_after_route,
2117 absl::StrFormat("break_is_after_route(%ld)", br));
2118 solver->SetEnforcementLiteral(ct, break_is_after_route);
2119 solver->SetCoefficient(break_in_one_slack_ct, break_is_after_route, 1);
2120 }
2121 }
2122
2123 // Add the possibility of fitting the break during each slack where it can.
2124 for (int pos = 0; pos < path_size - 1; ++pos) {
2125 // Pass on slacks that cannot start before, cannot end after,
2126 // or are not long enough to contain the break.
2127 const int64_t slack_start_min =
2128 CapAdd(current_route_min_cumuls_[pos], pre_travel[pos]);
2129 if (slack_start_min > break_start_max) break;
2130 const int64_t slack_end_max =
2131 CapSub(current_route_max_cumuls_[pos + 1], post_travel[pos]);
2132 if (break_end_min > slack_end_max) continue;
2133 const int64_t slack_duration_max =
2134 std::min(CapSub(CapSub(current_route_max_cumuls_[pos + 1],
2135 current_route_min_cumuls_[pos]),
2136 fixed_transit[pos]),
2137 dimension_->SlackVar(path[pos])->Max());
2138 if (slack_duration_max < break_duration_min) continue;
2139
2140 // Break can fit into slack: make LP variable, add to break and slack
2141 // constraints.
2142 // Make a linearized slack lower bound (lazily), that represents
2143 // sum_br break_duration_min(br) * break_in_slack(br, pos) <=
2144 // lp_slacks(pos).
2145 const int break_in_slack = solver->AddVariable(0, 1);
2147 solver, break_in_slack,
2148 absl::StrFormat("break_in_slack(%ld, %ld)", br, pos));
2149 if (slack_linear_lower_bound_ct[pos] == -1) {
2150 slack_linear_lower_bound_ct[pos] = solver->AddLinearConstraint(
2151 std::numeric_limits<int64_t>::min(), 0, {{lp_slacks[pos], -1}});
2152 }
2153 // To keep the model clean
2154 // (cf. glop::LinearProgram::NotifyThatColumnsAreClean), constraints on
2155 // break_in_slack need to be in ascending order.
2156 if (break_in_one_slack_ct < slack_linear_lower_bound_ct[pos]) {
2157 solver->SetCoefficient(break_in_one_slack_ct, break_in_slack, 1);
2158 solver->SetCoefficient(slack_linear_lower_bound_ct[pos], break_in_slack,
2159 break_duration_min);
2160 } else {
2161 solver->SetCoefficient(slack_linear_lower_bound_ct[pos], break_in_slack,
2162 break_duration_min);
2163 solver->SetCoefficient(break_in_one_slack_ct, break_in_slack, 1);
2164 }
2165
2166 if (solver->IsCPSATSolver()) {
2167 // Exact relation between breaks, slacks and cumul variables.
2168 // Make an exact slack lower bound (lazily), that represents
2169 // sum_br break_duration(br) * break_in_slack(br, pos) <=
2170 // lp_slacks(pos).
2171 const int break_duration_in_slack =
2172 solver->AddVariable(0, slack_duration_max);
2174 solver, break_duration_in_slack,
2175 absl::StrFormat("break_duration_in_slack(%ld, %ld)", br, pos));
2176 solver->AddProductConstraint(break_duration_in_slack,
2177 {break_in_slack, lp_break_duration[br]});
2178 if (slack_exact_lower_bound_ct[pos] == -1) {
2179 slack_exact_lower_bound_ct[pos] = solver->AddLinearConstraint(
2180 std::numeric_limits<int64_t>::min(), 0, {{lp_slacks[pos], -1}});
2181 }
2182 solver->SetCoefficient(slack_exact_lower_bound_ct[pos],
2183 break_duration_in_slack, 1);
2184 // If break_in_slack_i == 1, then
2185 // 1) break_start >= cumul[pos] + pre_travel[pos]
2186 const int break_start_after_current_ct = solver->AddLinearConstraint(
2187 pre_travel[pos], std::numeric_limits<int64_t>::max(),
2188 {{lp_break_start[br], 1}, {lp_cumuls[pos], -1}});
2189 solver->SetEnforcementLiteral(break_start_after_current_ct,
2190 break_in_slack);
2191 // 2) break_end <= cumul[pos+1] - post_travel[pos]
2192 const int break_ends_before_next_ct = solver->AddLinearConstraint(
2193 post_travel[pos], std::numeric_limits<int64_t>::max(),
2194 {{lp_cumuls[pos + 1], 1}, {lp_break_end[br], -1}});
2195 solver->SetEnforcementLiteral(break_ends_before_next_ct,
2196 break_in_slack);
2197 }
2198 }
2199 }
2200
2201 if (!solver->IsCPSATSolver()) return true;
2202 if (!dimension_->GetBreakDistanceDurationOfVehicle(vehicle).empty()) {
2203 // If there is an optional interval, the following model would be wrong.
2204 // TODO(user): support optional intervals.
2205 for (const IntervalVar* interval :
2206 dimension_->GetBreakIntervalsOfVehicle(vehicle)) {
2207 if (!interval->MustBePerformed()) return true;
2208 }
2209 // When this feature is used, breaks are in sorted order.
2210 for (int br = 1; br < num_breaks; ++br) {
2211 if (lp_break_start[br] == -1 || lp_break_start[br - 1] == -1) continue;
2212 solver->AddLinearConstraint(
2213 0, std::numeric_limits<int64_t>::max(),
2214 {{lp_break_end[br - 1], -1}, {lp_break_start[br], 1}});
2215 }
2216 }
2217 for (const auto& distance_duration :
2218 dimension_->GetBreakDistanceDurationOfVehicle(vehicle)) {
2219 const int64_t limit = distance_duration.first;
2220 const int64_t min_break_duration = distance_duration.second;
2221 // Interbreak limit constraint: breaks are interpreted as being in sorted
2222 // order, and the maximum duration between two consecutive
2223 // breaks of duration more than 'min_break_duration' is 'limit'. This
2224 // considers the time until start of route and after end of route to be
2225 // infinite breaks.
2226 // The model for this constraint adds some 'cover_i' variables, such that
2227 // the breaks up to i and the start of route allows to go without a break.
2228 // With s_i the start of break i and e_i its end:
2229 // - the route start covers time from start to start + limit:
2230 // cover_0 = route_start + limit
2231 // - the coverage up to a given break is the largest of the coverage of the
2232 // previous break and if the break is long enough, break end + limit:
2233 // cover_{i+1} = max(cover_i,
2234 // e_i - s_i >= min_break_duration ? e_i + limit : -inf)
2235 // - the coverage of the last break must be at least the route end,
2236 // to ensure the time point route_end-1 is covered:
2237 // cover_{num_breaks} >= route_end
2238 // - similarly, time point s_i-1 must be covered by breaks up to i-1,
2239 // but only if the cover has not reached the route end.
2240 // For instance, a vehicle could have a choice between two days,
2241 // with a potential break on day 1 and a potential break on day 2,
2242 // but the break of day 1 does not have to cover that of day 2!
2243 // cover_{i-1} < route_end => s_i <= cover_{i-1}
2244 // This is sufficient to ensure that the union of the intervals
2245 // (-infinity, route_start], [route_end, +infinity) and all
2246 // [s_i, e_i+limit) where e_i - s_i >= min_break_duration is
2247 // the whole timeline (-infinity, +infinity).
2248 int previous_cover = solver->AddVariable(CapAdd(vehicle_start_min, limit),
2249 CapAdd(vehicle_start_max, limit));
2250 SET_DEBUG_VARIABLE_NAME(solver, previous_cover, "previous_cover");
2251 solver->AddLinearConstraint(limit, limit,
2252 {{previous_cover, 1}, {lp_cumuls.front(), -1}});
2253 for (int br = 0; br < num_breaks; ++br) {
2254 if (lp_break_start[br] == -1) continue;
2255 const int64_t break_end_min = CapSub(breaks[br]->EndMin(), cumul_offset);
2256 const int64_t break_end_max = CapSub(breaks[br]->EndMax(), cumul_offset);
2257 // break_is_eligible <=>
2258 // break_end - break_start >= break_minimum_duration.
2259 const int break_is_eligible = solver->AddVariable(0, 1);
2260 SET_DEBUG_VARIABLE_NAME(solver, break_is_eligible,
2261 absl::StrFormat("break_is_eligible(%ld)", br));
2262 const int break_is_not_eligible = solver->AddVariable(0, 1);
2264 solver, break_is_not_eligible,
2265 absl::StrFormat("break_is_not_eligible(%ld)", br));
2266 {
2267 solver->AddLinearConstraint(
2268 1, 1, {{break_is_eligible, 1}, {break_is_not_eligible, 1}});
2269 const int positive_ct = solver->AddLinearConstraint(
2270 min_break_duration, std::numeric_limits<int64_t>::max(),
2271 {{lp_break_end[br], 1}, {lp_break_start[br], -1}});
2272 solver->SetEnforcementLiteral(positive_ct, break_is_eligible);
2273 const int negative_ct = solver->AddLinearConstraint(
2274 std::numeric_limits<int64_t>::min(), min_break_duration - 1,
2275 {{lp_break_end[br], 1}, {lp_break_start[br], -1}});
2276 solver->SetEnforcementLiteral(negative_ct, break_is_not_eligible);
2277 }
2278 // break_is_eligible => break_cover == break_end + limit.
2279 // break_is_not_eligible => break_cover == vehicle_start_min + limit.
2280 // break_cover's initial domain is the smallest interval that contains the
2281 // union of sets {vehicle_start_min+limit} and
2282 // [break_end_min+limit, break_end_max+limit).
2283 const int break_cover = solver->AddVariable(
2284 CapAdd(std::min(vehicle_start_min, break_end_min), limit),
2285 CapAdd(std::max(vehicle_start_min, break_end_max), limit));
2286 SET_DEBUG_VARIABLE_NAME(solver, break_cover,
2287 absl::StrFormat("break_cover(%ld)", br));
2288 const int limit_cover_ct = solver->AddLinearConstraint(
2289 limit, limit, {{break_cover, 1}, {lp_break_end[br], -1}});
2290 solver->SetEnforcementLiteral(limit_cover_ct, break_is_eligible);
2291 const int empty_cover_ct = solver->AddLinearConstraint(
2292 CapAdd(vehicle_start_min, limit), CapAdd(vehicle_start_min, limit),
2293 {{break_cover, 1}});
2294 solver->SetEnforcementLiteral(empty_cover_ct, break_is_not_eligible);
2295
2296 const int cover =
2297 solver->AddVariable(CapAdd(vehicle_start_min, limit),
2298 std::numeric_limits<int64_t>::max());
2299 SET_DEBUG_VARIABLE_NAME(solver, cover, absl::StrFormat("cover(%ld)", br));
2300 solver->AddMaximumConstraint(cover, {previous_cover, break_cover});
2301 // Cover chaining. If route end is not covered, break start must be:
2302 // cover_{i-1} < route_end => s_i <= cover_{i-1}
2303 const int route_end_is_not_covered = solver->AddReifiedLinearConstraint(
2304 1, std::numeric_limits<int64_t>::max(),
2305 {{lp_cumuls.back(), 1}, {previous_cover, -1}});
2306 const int break_start_cover_ct = solver->AddLinearConstraint(
2307 0, std::numeric_limits<int64_t>::max(),
2308 {{previous_cover, 1}, {lp_break_start[br], -1}});
2309 solver->SetEnforcementLiteral(break_start_cover_ct,
2310 route_end_is_not_covered);
2311
2312 previous_cover = cover;
2313 }
2314 solver->AddLinearConstraint(0, std::numeric_limits<int64_t>::max(),
2315 {{previous_cover, 1}, {lp_cumuls.back(), -1}});
2316 }
2317
2318 return true;
2319}
2320
2321bool DimensionCumulOptimizerCore::SetGlobalConstraints(
2322 const std::function<int64_t(int64_t)>& next_accessor, int64_t cumul_offset,
2323 bool optimize_costs, bool optimize_resource_assignment,
2324 RoutingLinearSolverWrapper* solver) {
2325 // Global span cost =
2326 // global_span_cost_coefficient * (max_end_cumul - min_start_cumul).
2327 const int64_t global_span_coeff = dimension_->global_span_cost_coefficient();
2328 if (optimize_costs && global_span_coeff > 0) {
2329 // global_span_var = max_end_cumul_ - min_start_cumul_
2330 const int global_span_var = solver->CreateNewPositiveVariable();
2331 SET_DEBUG_VARIABLE_NAME(solver, global_span_var, "global_span_var");
2332 solver->AddLinearConstraint(
2333 0, 0,
2334 {{global_span_var, 1}, {max_end_cumul_, -1}, {min_start_cumul_, 1}});
2335 // NOTE: Adding the global span cost to the objective as
2336 // global_span_coeff * global_span_var increases the precision of the solver
2337 // compared to adding two separate terms global_span_coeff * max_end_cumul
2338 // and -global_span_coeff * min_start_cumul.
2339 solver->SetObjectiveCoefficient(global_span_var, global_span_coeff);
2340 }
2341
2342 // Node precedence constraints, set when both nodes are visited.
2343 for (const RoutingDimension::NodePrecedence& precedence :
2344 dimension_->GetNodePrecedences()) {
2345 const int first_cumul_var = index_to_cumul_variable_[precedence.first_node];
2346 const int second_cumul_var =
2347 index_to_cumul_variable_[precedence.second_node];
2348 if (first_cumul_var < 0 || second_cumul_var < 0) {
2349 // At least one of the nodes is not on any route, skip this precedence
2350 // constraint.
2351 continue;
2352 }
2353 DCHECK_NE(first_cumul_var, second_cumul_var)
2354 << "Dimension " << dimension_->name()
2355 << " has a self-precedence on node " << precedence.first_node << ".";
2356
2357 // cumul[second_node] - cumul[first_node] >= offset.
2358 const int ct = solver->CreateNewConstraint(
2359 precedence.offset, std::numeric_limits<int64_t>::max());
2360 solver->SetCoefficient(ct, second_cumul_var, 1);
2361 solver->SetCoefficient(ct, first_cumul_var, -1);
2362 }
2363
2364 if (optimize_resource_assignment &&
2365 !SetGlobalConstraintsForResourceAssignment(next_accessor, cumul_offset,
2366 solver)) {
2367 return false;
2368 }
2369 return true;
2370}
2371
2372bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment(
2373 const std::function<int64_t(int64_t)>& next_accessor, int64_t cumul_offset,
2374 RoutingLinearSolverWrapper* solver) {
2375 if (!solver->IsCPSATSolver()) {
2376 // The resource attributes conditional constraints can only be added with
2377 // the CP-SAT MIP solver.
2378 return true;
2379 }
2380
2381 const RoutingModel& model = *dimension_->model();
2382 const int num_vehicles = model.vehicles();
2383 const auto& resource_groups = model.GetResourceGroups();
2384 for (int rg_index : model.GetDimensionResourceGroupIndices(dimension_)) {
2385 // Resource domain constraints:
2386 // Every (used) vehicle requiring a resource from this group must be
2387 // assigned to exactly one resource in this group, and each resource must
2388 // be assigned to at most 1 vehicle requiring it. For every resource r
2389 // with Attributes A = resources[r].attributes(dim) and every vehicle v,
2390 // assign(r, v) == 1 --> A.start_domain.Min() <= cumul[Start(v)] <=
2391 // A.start_domain.Max(), and A.end_domain.Min() <= cumul[End(v)] <=
2392 // A.end_domain.Max().
2393 const ResourceGroup& resource_group = *resource_groups[rg_index];
2394 DCHECK(!resource_group.GetVehiclesRequiringAResource().empty());
2395
2396 const std::vector<ResourceGroup::Resource>& resources =
2397 resource_group.GetResources();
2398 int num_required_resources = 0;
2399 static const int kNoConstraint = -1;
2400 // Assignment constraints for vehicles: each (used) vehicle must have
2401 // exactly one resource assigned to it.
2402 std::vector<int> vehicle_constraints(model.vehicles(), kNoConstraint);
2403 for (int v : resource_group.GetVehiclesRequiringAResource()) {
2404 if (model.IsEnd(next_accessor(model.Start(v))) &&
2405 !model.IsVehicleUsedWhenEmpty(v)) {
2406 // We don't assign a driver to unused vehicles.
2407 continue;
2408 }
2409 num_required_resources++;
2410 vehicle_constraints[v] = solver->CreateNewConstraint(1, 1);
2411 }
2412 // Assignment constraints for resources: each resource must be assigned to
2413 // at most one (used) vehicle requiring one.
2414 const int num_resources = resources.size();
2415 std::vector<int> resource_constraints(num_resources, kNoConstraint);
2416 int num_available_resources = 0;
2417 for (int r = 0; r < num_resources; r++) {
2418 const ResourceGroup::Attributes& attributes =
2419 resources[r].GetDimensionAttributes(dimension_);
2420 if (attributes.start_domain().Max() < cumul_offset ||
2421 attributes.end_domain().Max() < cumul_offset) {
2422 // This resource's domain has a cumul max lower than the offset, so
2423 // it's not possible to restrict any vehicle start/end to this domain;
2424 // skip it.
2425 continue;
2426 }
2427 num_available_resources++;
2428 resource_constraints[r] = solver->CreateNewConstraint(0, 1);
2429 }
2430
2431 if (num_required_resources > num_available_resources) {
2432 // There aren't enough resources in this group for vehicles requiring
2433 // one.
2434 return false;
2435 }
2436
2437 std::vector<int>& resource_to_vehicle_assignment_variables =
2438 resource_group_to_resource_to_vehicle_assignment_variables_[rg_index];
2439 resource_to_vehicle_assignment_variables.assign(
2440 num_resources * num_vehicles, -1);
2441 // Create assignment variables, add them to the corresponding constraints,
2442 // and create the reified constraints assign(r, v) == 1 -->
2443 // A(r).start_domain.Min() <= cumul[Start(v)] <= A(r).start_domain.Max(),
2444 // and
2445 // A(r).end_domain.Min() <= cumul[End(v)] <= A(r).end_domain.Max().
2446 DCHECK_EQ(resource_group.Index(), rg_index);
2447 for (int v : resource_group.GetVehiclesRequiringAResource()) {
2448 if (vehicle_constraints[v] == kNoConstraint) continue;
2449 IntVar* const resource_var = model.ResourceVar(v, rg_index);
2450 std::unique_ptr<IntVarIterator> it(
2451 resource_var->MakeDomainIterator(false));
2452 for (int r : InitAndGetValues(it.get())) {
2453 if (r < 0) continue;
2454 DCHECK_LT(r, num_resources);
2455 if (resource_constraints[r] == kNoConstraint) continue;
2456
2457 const int assign_r_to_v = solver->AddVariable(0, 1);
2459 solver, assign_r_to_v,
2460 absl::StrFormat("assign_r_to_v(%ld, %ld)", r, v));
2461 resource_to_vehicle_assignment_variables[r * num_vehicles + v] =
2462 assign_r_to_v;
2463 // To keep the model clean
2464 // (cf. glop::LinearProgram::NotifyThatColumnsAreClean), constraints
2465 // on assign_r_to_v need to be in ascending order.
2466 if (vehicle_constraints[v] < resource_constraints[r]) {
2467 solver->SetCoefficient(vehicle_constraints[v], assign_r_to_v, 1);
2468 solver->SetCoefficient(resource_constraints[r], assign_r_to_v, 1);
2469 } else {
2470 solver->SetCoefficient(resource_constraints[r], assign_r_to_v, 1);
2471 solver->SetCoefficient(vehicle_constraints[v], assign_r_to_v, 1);
2472 }
2473
2474 const auto& add_domain_constraint =
2475 [&solver, cumul_offset, assign_r_to_v](const Domain& domain,
2476 int cumul_variable) {
2477 if (domain == Domain::AllValues()) {
2478 return;
2479 }
2480 ClosedInterval cumul_bounds;
2481 if (!GetDomainOffsetBounds(domain, cumul_offset, &cumul_bounds)) {
2482 // This domain cannot be assigned to this vehicle.
2483 solver->SetVariableBounds(assign_r_to_v, 0, 0);
2484 return;
2485 }
2486 const int cumul_constraint = solver->AddLinearConstraint(
2487 cumul_bounds.start, cumul_bounds.end, {{cumul_variable, 1}});
2488 solver->SetEnforcementLiteral(cumul_constraint, assign_r_to_v);
2489 };
2490 const ResourceGroup::Attributes& attributes =
2491 resources[r].GetDimensionAttributes(dimension_);
2492 add_domain_constraint(attributes.start_domain(),
2493 index_to_cumul_variable_[model.Start(v)]);
2494 add_domain_constraint(attributes.end_domain(),
2495 index_to_cumul_variable_[model.End(v)]);
2496 }
2497 }
2498 }
2499 return true;
2500}
2501
2502#undef SET_DEBUG_VARIABLE_NAME
2503
2504void DimensionCumulOptimizerCore::SetValuesFromLP(
2505 const std::vector<int>& lp_variables, int64_t offset,
2506 RoutingLinearSolverWrapper* solver, std::vector<int64_t>* lp_values) const {
2507 if (lp_values == nullptr) return;
2508 lp_values->assign(lp_variables.size(), std::numeric_limits<int64_t>::min());
2509 for (int i = 0; i < lp_variables.size(); i++) {
2510 const int lp_var = lp_variables[i];
2511 if (lp_var < 0) continue; // Keep default value, kint64min.
2512 const double lp_value_double = solver->GetValue(lp_var);
2513 const int64_t lp_value_int64 =
2514 (lp_value_double >= std::numeric_limits<int64_t>::max())
2515 ? std::numeric_limits<int64_t>::max()
2516 : MathUtil::FastInt64Round(lp_value_double);
2517 (*lp_values)[i] = CapAdd(lp_value_int64, offset);
2518 }
2519}
2520
2521void DimensionCumulOptimizerCore::SetResourceIndices(
2522 RoutingLinearSolverWrapper* solver,
2523 std::vector<std::vector<int>>* resource_indices_per_group) const {
2524 if (resource_indices_per_group == nullptr ||
2525 resource_group_to_resource_to_vehicle_assignment_variables_.empty()) {
2526 return;
2527 }
2528 const RoutingModel& model = *dimension_->model();
2529 const int num_vehicles = model.vehicles();
2530 DCHECK(!model.GetDimensionResourceGroupIndices(dimension_).empty());
2531 const auto& resource_groups = model.GetResourceGroups();
2532 resource_indices_per_group->resize(resource_groups.size());
2533 for (int rg_index : model.GetDimensionResourceGroupIndices(dimension_)) {
2534 const ResourceGroup& resource_group = *resource_groups[rg_index];
2535 DCHECK(!resource_group.GetVehiclesRequiringAResource().empty());
2536
2537 const int num_resources = resource_group.Size();
2538 std::vector<int>& resource_indices =
2539 resource_indices_per_group->at(rg_index);
2540 resource_indices.assign(num_vehicles, -1);
2541 // Find the resource assigned to each vehicle.
2542 const std::vector<int>& resource_to_vehicle_assignment_variables =
2543 resource_group_to_resource_to_vehicle_assignment_variables_[rg_index];
2544 DCHECK_EQ(resource_to_vehicle_assignment_variables.size(),
2545 num_resources * num_vehicles);
2546 for (int v : resource_group.GetVehiclesRequiringAResource()) {
2547 for (int r = 0; r < num_resources; r++) {
2548 const int assignment_var =
2549 resource_to_vehicle_assignment_variables[r * num_vehicles + v];
2550 if (assignment_var >= 0 && solver->GetValue(assignment_var) == 1) {
2551 // This resource is assigned to this vehicle.
2552 resource_indices[v] = r;
2553 break;
2554 }
2555 }
2556 }
2557 }
2558}
2559
2560// GlobalDimensionCumulOptimizer
2561
2562GlobalDimensionCumulOptimizer::GlobalDimensionCumulOptimizer(
2563 const RoutingDimension* dimension,
2564 RoutingSearchParameters::SchedulingSolver solver_type)
2565 : optimizer_core_(dimension,
2566 /*use_precedence_propagator=*/
2567 !dimension->GetNodePrecedences().empty()) {
2568 switch (solver_type) {
2569 case RoutingSearchParameters::SCHEDULING_GLOP: {
2570 solver_ = std::make_unique<RoutingGlopWrapper>(
2571 /*is_relaxation=*/!dimension->model()
2572 ->GetDimensionResourceGroupIndices(dimension)
2573 .empty(),
2574 GetGlopParametersForGlobalLP());
2575 break;
2576 }
2577 case RoutingSearchParameters::SCHEDULING_CP_SAT: {
2578 solver_ = std::make_unique<RoutingCPSatWrapper>();
2579 break;
2580 }
2581 default:
2582 LOG(DFATAL) << "Unrecognized solver type: " << solver_type;
2583 }
2584}
2585
2588 const std::function<int64_t(int64_t)>& next_accessor,
2589 int64_t* optimal_cost_without_transits) {
2590 return optimizer_core_.Optimize(next_accessor, {}, solver_.get(), nullptr,
2591 nullptr, nullptr,
2592 optimal_cost_without_transits, nullptr);
2593}
2594
2596 const std::function<int64_t(int64_t)>& next_accessor,
2597 const std::vector<RoutingModel::RouteDimensionTravelInfo>&
2598 dimension_travel_info_per_route,
2599 std::vector<int64_t>* optimal_cumuls, std::vector<int64_t>* optimal_breaks,
2600 std::vector<std::vector<int>>* optimal_resource_indices) {
2601 return optimizer_core_.Optimize(next_accessor,
2602 dimension_travel_info_per_route,
2603 solver_.get(), optimal_cumuls, optimal_breaks,
2604 optimal_resource_indices, nullptr, nullptr);
2605}
2606
2608 const std::function<int64_t(int64_t)>& next_accessor,
2609 const std::vector<RoutingModel::RouteDimensionTravelInfo>&
2610 dimension_travel_info_per_route,
2611 std::vector<int64_t>* packed_cumuls, std::vector<int64_t>* packed_breaks) {
2612 return optimizer_core_.OptimizeAndPack(
2613 next_accessor, dimension_travel_info_per_route, solver_.get(),
2614 packed_cumuls, packed_breaks);
2615}
2616
2617namespace {
2618
2619bool AllValuesContainedExcept(const IntVar& var, const std::vector<int>& values,
2620 const absl::flat_hash_set<int>& ignored_values) {
2621 for (int val : values) {
2622 if (!ignored_values.contains(val) && !var.Contains(val)) return false;
2623 }
2624 return true;
2625}
2626
2627template <typename T>
2628void MoveValuesToIndicesFrom(std::vector<T>* out_values,
2629 const std::vector<int>& out_indices_to_evaluate,
2630 const std::function<int(int)>& index_evaluator,
2631 std::vector<T>& values_to_copy) {
2632 if (out_values == nullptr) {
2633 DCHECK(values_to_copy.empty());
2634 return;
2635 }
2636 DCHECK_EQ(values_to_copy.size(), out_indices_to_evaluate.size());
2637 for (int i = 0; i < out_indices_to_evaluate.size(); i++) {
2638 const int output_index = index_evaluator(out_indices_to_evaluate[i]);
2639 DCHECK_LT(output_index, out_values->size());
2640 out_values->at(output_index) = std::move(values_to_copy[i]);
2641 }
2642}
2643
2644} // namespace
2645
2647 int v, const RoutingModel::ResourceGroup& resource_group,
2648 const util_intops::StrongVector<RoutingModel::ResourceClassIndex,
2649 absl::flat_hash_set<int>>&
2650 ignored_resources_per_class,
2651 const std::function<int64_t(int64_t)>& next_accessor,
2652 const std::function<int64_t(int64_t, int64_t)>& transit_accessor,
2653 bool optimize_vehicle_costs, LocalDimensionCumulOptimizer* lp_optimizer,
2654 LocalDimensionCumulOptimizer* mp_optimizer,
2655 std::vector<int64_t>* assignment_costs,
2656 std::vector<std::vector<int64_t>>* cumul_values,
2657 std::vector<std::vector<int64_t>>* break_values) {
2658 DCHECK(lp_optimizer != nullptr);
2659 DCHECK(mp_optimizer != nullptr);
2660 DCHECK_NE(assignment_costs, nullptr);
2661 assignment_costs->clear();
2662 ClearIfNonNull(cumul_values);
2663 ClearIfNonNull(break_values);
2664
2665 const RoutingDimension* dimension = lp_optimizer->dimension();
2666 DCHECK_EQ(dimension, mp_optimizer->dimension());
2667 RoutingModel* const model = dimension->model();
2668 if (!resource_group.VehicleRequiresAResource(v) ||
2669 (!model->IsVehicleUsedWhenEmpty(v) &&
2670 next_accessor(model->Start(v)) == model->End(v))) {
2671 return true;
2672 }
2673 if (model->CheckLimit()) {
2674 // The model's time limit has been reached, stop everything.
2675 return false;
2676 }
2677
2678 IntVar* const resource_var = model->ResourceVar(v, resource_group.Index());
2679 const int num_resource_classes = resource_group.GetResourceClassesCount();
2680 std::vector<int> considered_resource_indices;
2681 considered_resource_indices.reserve(
2682 std::min<int>(resource_var->Size(), num_resource_classes));
2683 std::vector<bool> resource_class_considered(num_resource_classes, false);
2684 std::unique_ptr<IntVarIterator> it(resource_var->MakeDomainIterator(false));
2685 for (const int res : InitAndGetValues(it.get())) {
2686 if (res < 0) {
2687 continue;
2688 }
2689 const RoutingModel::ResourceClassIndex resource_class =
2690 resource_group.GetResourceClassIndex(res);
2691 const int rc_index = resource_class.value();
2692 const absl::flat_hash_set<int>& ignored_resources =
2693 ignored_resources_per_class[resource_class];
2694 if (resource_class_considered[rc_index] ||
2695 ignored_resources.contains(res)) {
2696 continue;
2697 }
2698 resource_class_considered[rc_index] = true;
2699 // NOTE(user): The resource class computation should allow us to catch
2700 // all incompatibility reasons between vehicles and resources. If the
2701 // following DCHECK fails, the resource classes should be adapted
2702 // accordingly.
2703 DCHECK(AllValuesContainedExcept(
2704 *resource_var, resource_group.GetResourceIndicesInClass(resource_class),
2705 ignored_resources));
2706 considered_resource_indices.push_back(res);
2707 }
2708 const bool use_mp_optimizer =
2709 dimension->HasBreakConstraints() &&
2710 !dimension->GetBreakIntervalsOfVehicle(v).empty();
2711 LocalDimensionCumulOptimizer* optimizer =
2712 use_mp_optimizer ? mp_optimizer : lp_optimizer;
2713
2714 const std::vector<ResourceGroup::Resource>& resources =
2715 resource_group.GetResources();
2716 std::vector<int64_t> considered_assignment_costs;
2717 std::vector<std::vector<int64_t>> considered_cumul_values;
2718 std::vector<std::vector<int64_t>> considered_break_values;
2719 std::vector<DimensionSchedulingStatus> statuses =
2721 v, next_accessor, transit_accessor, resources,
2722 considered_resource_indices, optimize_vehicle_costs,
2723 &considered_assignment_costs,
2724 cumul_values == nullptr ? nullptr : &considered_cumul_values,
2725 break_values == nullptr ? nullptr : &considered_break_values);
2726
2727 if (statuses.empty() ||
2728 (statuses.size() == 1 &&
2729 statuses[0] == DimensionSchedulingStatus::INFEASIBLE)) {
2730 // Couldn't assign any resource to this vehicle.
2731 return false;
2732 }
2733
2734 assignment_costs->resize(num_resource_classes, -1);
2735 if (cumul_values != nullptr) {
2736 cumul_values->resize(num_resource_classes, {});
2737 }
2738 if (break_values != nullptr) {
2739 break_values->resize(num_resource_classes, {});
2740 }
2741
2742 const auto resource_to_class_index = [&resource_group](int resource_index) {
2743 return resource_group.GetResourceClassIndex(resource_index).value();
2744 };
2745 MoveValuesToIndicesFrom(assignment_costs, considered_resource_indices,
2746 resource_to_class_index, considered_assignment_costs);
2747 MoveValuesToIndicesFrom(cumul_values, considered_resource_indices,
2748 resource_to_class_index, considered_cumul_values);
2749 MoveValuesToIndicesFrom(break_values, considered_resource_indices,
2750 resource_to_class_index, considered_break_values);
2751
2752 if (use_mp_optimizer) {
2753 // We already used the mp optimizer, so we don't need to recompute anything.
2754 // If all assignment costs are negative, it means no resource is feasible
2755 // for this vehicle.
2756 return absl::c_any_of(*assignment_costs,
2757 [](int64_t cost) { return cost >= 0; });
2758 }
2759
2760 std::vector<int> mp_resource_indices;
2761 DCHECK_EQ(statuses.size(), considered_resource_indices.size());
2762 for (int i = 0; i < considered_resource_indices.size(); i++) {
2764 mp_resource_indices.push_back(considered_resource_indices[i]);
2765 }
2766 }
2767
2768 std::vector<int64_t> mp_assignment_costs;
2769 std::vector<std::vector<int64_t>> mp_cumul_values;
2770 std::vector<std::vector<int64_t>> mp_break_values;
2772 v, next_accessor, transit_accessor, resources, mp_resource_indices,
2773 optimize_vehicle_costs, &mp_assignment_costs,
2774 cumul_values == nullptr ? nullptr : &mp_cumul_values,
2775 break_values == nullptr ? nullptr : &mp_break_values);
2776 if (!mp_resource_indices.empty() && mp_assignment_costs.empty()) {
2777 // A timeout was reached during optimization.
2778 return false;
2779 }
2780
2781 MoveValuesToIndicesFrom(assignment_costs, mp_resource_indices,
2782 resource_to_class_index, mp_assignment_costs);
2783 MoveValuesToIndicesFrom(cumul_values, mp_resource_indices,
2784 resource_to_class_index, mp_cumul_values);
2785 MoveValuesToIndicesFrom(break_values, mp_resource_indices,
2786 resource_to_class_index, mp_break_values);
2787
2788 return absl::c_any_of(*assignment_costs,
2789 [](int64_t cost) { return cost >= 0; });
2790}
2791
2793 const std::vector<int>& vehicles,
2794 const util_intops::StrongVector<RoutingModel::ResourceClassIndex,
2795 std::vector<int>>& resource_indices_per_class,
2796 const util_intops::StrongVector<RoutingModel::ResourceClassIndex,
2797 absl::flat_hash_set<int>>&
2798 ignored_resources_per_class,
2799 std::function<const std::vector<int64_t>*(int)>
2800 vehicle_to_resource_class_assignment_costs,
2801 std::vector<int>* resource_indices) {
2802 const int total_num_resources = absl::c_accumulate(
2803 resource_indices_per_class, 0,
2804 [](int acc, const std::vector<int>& res) { return acc + res.size(); });
2805 DCHECK_GE(total_num_resources, 1);
2806 const int num_ignored_resources =
2807 absl::c_accumulate(ignored_resources_per_class, 0,
2808 [](int acc, const absl::flat_hash_set<int>& res) {
2809 return acc + res.size();
2810 });
2811 const int num_resources = total_num_resources - num_ignored_resources;
2812 const int num_vehicles = vehicles.size();
2813 int num_total_vehicles = -1;
2814 if (resource_indices != nullptr) {
2815 num_total_vehicles = resource_indices->size();
2816 // When returning infeasible, 'resource_indices' must be cleared, so we do
2817 // it here preemptively.
2818 resource_indices->clear();
2819 DCHECK_GE(num_total_vehicles, num_vehicles);
2820 for (int v : vehicles) {
2821 DCHECK_GE(v, 0);
2822 DCHECK_LT(v, num_total_vehicles);
2823 }
2824 }
2825
2826 // Collect vehicle_to_resource_class_assignment_costs(v) for all v ∈ vehicles.
2827 // Then detect trivial infeasibility cases, before doing the min-cost-flow:
2828 // - There are not enough resources overall.
2829 // - There is no resource assignable to a vehicle that needs one.
2830 const int num_resource_classes = resource_indices_per_class.size();
2831 std::vector<const std::vector<int64_t>*> vi_to_rc_cost(num_vehicles);
2832 int num_vehicles_to_assign = 0;
2833 for (int i = 0; i < num_vehicles; ++i) {
2834 vi_to_rc_cost[i] = vehicle_to_resource_class_assignment_costs(vehicles[i]);
2835 if (!vi_to_rc_cost[i]->empty()) {
2836 DCHECK_EQ(vi_to_rc_cost[i]->size(), num_resource_classes);
2837 ++num_vehicles_to_assign;
2838 }
2839 }
2840 if (num_vehicles_to_assign > num_resources) {
2841 VLOG(3) << "Less resources (" << num_resources << ") than the vehicles"
2842 << " requiring one (" << num_vehicles_to_assign << ")";
2843 return -1; // Infeasible.
2844 }
2845 // Catch infeasibility cases where
2846 // ComputeVehicleToResourceClassAssignmentCosts() hasn't "properly"
2847 // initialized the vehicle to resource class assignment costs (this can happen
2848 // for instance in the ResourceGroupAssignmentFilter when routes are
2849 // synchronized with an impossible first solution).
2850 for (int i = 0; i < num_vehicles; ++i) {
2851 if (!vi_to_rc_cost[i]->empty() &&
2852 *absl::c_max_element(*vi_to_rc_cost[i]) < 0) {
2853 VLOG(3) << "Vehicle #" << vehicles[i] << " has no feasible resource";
2854 return -1;
2855 }
2856 }
2857
2858 // We may need to apply some cost scaling when using SimpleMinCostFlow.
2859 // With our graph it seems having 4 * max_arc_cost * num_nodes ≤ kint64max is
2860 // sufficient. To do that, we first find the maximum arc cost.
2861 int64_t max_arc_cost = 0;
2862 for (const std::vector<int64_t>* costs : vi_to_rc_cost) {
2863 if (costs->empty()) continue;
2864 max_arc_cost = std::max(max_arc_cost, *absl::c_max_element(*costs));
2865 }
2866 // To avoid potential int64_t overflows, we slightly tweak the above formula.
2867 // NOTE(user): SimpleMinCostFlow always adds a sink and source node (we
2868 // probably shouldn't add a sink/source node ourselves in the graph).
2869 const int real_num_nodes = 4 + num_vehicles + num_resource_classes;
2870 const int64_t max_acceptable_arc_cost = kint64max / (4 * real_num_nodes) - 1;
2871 // We use a power of 2 for the cost scaling factor, to have clean (in)accuracy
2872 // properties. Note also that we must round *down* the costs.
2873 int cost_right_shift = 0;
2874 while ((max_arc_cost >> cost_right_shift) > max_acceptable_arc_cost) {
2875 ++cost_right_shift;
2876 }
2877
2878 // Then, we create the SimpleMinCostFlow and run the assignment algorithm.
2879 // NOTE(user): We often don't create as many arcs as outlined below,
2880 // especially when num_vehicles_to_assign < vehicles.size(). But since we
2881 // want to eventually make this whole function incremental, we prefer sticking
2882 // with the whole 'vehicles' set.
2883 SimpleMinCostFlow flow(
2884 /*reserve_num_nodes*/ 2 + num_vehicles + num_resource_classes,
2885 /*reserve_num_arcs*/ num_vehicles + num_vehicles * num_resource_classes +
2886 num_resource_classes);
2887 const int source_index = num_vehicles + num_resource_classes;
2888 const int sink_index = source_index + 1;
2889 const auto flow_rc_index = [num_vehicles](int rc) {
2890 return num_vehicles + rc;
2891 };
2892
2893 // Used to store the arc indices, if we need to later recover the solution.
2894 FlatMatrix<ArcIndex> vehicle_to_rc_arc_index;
2895 if (resource_indices != nullptr) {
2896 vehicle_to_rc_arc_index =
2897 FlatMatrix<ArcIndex>(num_vehicles, num_resource_classes, -1);
2898 }
2899 for (int vi = 0; vi < num_vehicles; ++vi) {
2900 const std::vector<int64_t>& assignment_costs = *vi_to_rc_cost[vi];
2901 if (assignment_costs.empty()) continue; // Doesn't need resources.
2902
2903 // Add a source → vehicle arc to the min-cost-flow graph.
2904 flow.AddArcWithCapacityAndUnitCost(source_index, vi, 1, 0);
2905
2906 // Add vehicle → resource-class arcs to the min-cost-flow graph.
2907 for (int rc = 0; rc < num_resource_classes; rc++) {
2908 const int64_t assignment_cost = assignment_costs[rc];
2909 if (assignment_cost < 0) continue;
2911 vi, flow_rc_index(rc), 1, assignment_cost >> cost_right_shift);
2912 if (resource_indices != nullptr) {
2913 vehicle_to_rc_arc_index[vi][rc] = arc;
2914 }
2915 }
2916 }
2917
2918 // Add resource-class->sink arcs to the flow. The capacity on these arcs is
2919 // the number of available resources for the corresponding class.
2920 using RCIndex = RoutingModel::ResourceClassIndex;
2921 for (int rc = 0; rc < num_resource_classes; rc++) {
2922 const RCIndex rci(rc);
2923 const int num_available_res = resource_indices_per_class[rci].size() -
2924 ignored_resources_per_class[rci].size();
2925 DCHECK_GE(num_available_res, 0);
2926 flow.AddArcWithCapacityAndUnitCost(flow_rc_index(rc), sink_index,
2927 num_available_res, 0);
2928 }
2929
2930 // Set the flow supply.
2931 flow.SetNodeSupply(source_index, num_vehicles_to_assign);
2932 flow.SetNodeSupply(sink_index, -num_vehicles_to_assign);
2933
2934 // Solve the min-cost flow and return its cost.
2935 if (flow.Solve() != SimpleMinCostFlow::OPTIMAL) {
2936 VLOG(3) << "Non-OPTIMAL flow result";
2937 return -1;
2938 }
2939
2940 if (resource_indices != nullptr) {
2941 // Fill the resource indices corresponding to the min-cost assignment.
2942 resource_indices->assign(num_total_vehicles, -1);
2943 std::vector<int> current_resource_pos_for_class(num_resource_classes, 0);
2944 for (int vi = 0; vi < num_vehicles; ++vi) {
2945 if (vi_to_rc_cost[vi]->empty()) {
2946 // No resource needed for this vehicle.
2947 continue;
2948 }
2949 for (int rc = 0; rc < num_resource_classes; rc++) {
2950 const ArcIndex arc = vehicle_to_rc_arc_index[vi][rc];
2951 if (arc >= 0 && flow.Flow(arc) > 0) {
2952 const RCIndex rci(rc);
2953 const std::vector<int>& class_resource_indices =
2954 resource_indices_per_class[rci];
2955 const absl::flat_hash_set<int>& ignored_resources =
2956 ignored_resources_per_class[rci];
2957 int& pos = current_resource_pos_for_class[rc];
2958 DCHECK_LT(pos, class_resource_indices.size());
2959 while (ignored_resources.contains(class_resource_indices[pos])) {
2960 pos++;
2961 DCHECK_LT(pos, class_resource_indices.size());
2962 }
2963 resource_indices->at(vehicles[vi]) = class_resource_indices[pos++];
2964 break;
2965 }
2966 }
2967 }
2968 }
2969
2970 const int64_t cost = flow.OptimalCost();
2971 DCHECK_LE(cost, kint64max >> cost_right_shift);
2972 return cost << cost_right_shift;
2973}
2974
2975std::string Int64ToStr(int64_t number) {
2976 if (number == kint64min) return "-infty";
2977 if (number == kint64max) return "+infty";
2978 return std::to_string(number);
2979}
2980
2981std::string DomainToString(
2982 const ::google::protobuf::RepeatedField<int64_t>* domain) {
2983 if (domain->size() > 2 && domain->size() % 2 == 0) {
2984 std::string s = "∈ ";
2985 for (int i = 0; i < domain->size(); i += 2) {
2986 s += absl::StrFormat("[%s, %s]", Int64ToStr(domain->Get(i)),
2987 Int64ToStr(domain->Get(i + 1)));
2988 if (i < domain->size() - 2) s += " ∪ ";
2989 }
2990 return s;
2991 } else if (domain->size() == 2) {
2992 if (domain->Get(0) == domain->Get(1)) {
2993 return absl::StrFormat("= %s", Int64ToStr(domain->Get(0)));
2994 } else if (domain->Get(0) == 0 && domain->Get(1) == 1) {
2995 return "∈ Binary";
2996 } else if (domain->Get(0) == std::numeric_limits<int64_t>::min() &&
2997 domain->Get(1) == std::numeric_limits<int64_t>::max()) {
2998 return "∈ ℝ";
2999 } else if (domain->Get(0) == std::numeric_limits<int64_t>::min()) {
3000 return absl::StrFormat("≤ %s", Int64ToStr(domain->Get(1)));
3001 } else if (domain->Get(1) == std::numeric_limits<int64_t>::max()) {
3002 return absl::StrFormat("≥ %s", Int64ToStr(domain->Get(0)));
3003 }
3004 return absl::StrFormat("∈ [%ls, %s]", Int64ToStr(domain->Get(0)),
3005 Int64ToStr(domain->Get(1)));
3006 } else if (domain->size() == 1) {
3007 return absl::StrFormat("= %s", Int64ToStr(domain->Get(0)));
3008 } else {
3009 return absl::StrFormat("∈ Unknown domain (size=%ld)", domain->size());
3010 }
3011}
3012
3014 std::pair<sat::IntegerVariableProto, int>& variable_pair,
3015 const sat::CpSolverResponse& response_) {
3016 std::string s = "";
3017 sat::IntegerVariableProto& variable = variable_pair.first;
3018 const int index = variable_pair.second;
3019 if (response_.IsInitialized() && variable.IsInitialized() &&
3020 (response_.status() == sat::CpSolverStatus::OPTIMAL ||
3021 response_.status() == sat::CpSolverStatus::FEASIBLE)) {
3022 const double lp_value_double = response_.solution(index);
3023 const int64_t lp_value_int64 =
3024 (lp_value_double >= std::numeric_limits<int64_t>::max())
3025 ? std::numeric_limits<int64_t>::max()
3026 : MathUtil::FastInt64Round(lp_value_double);
3027 s += Int64ToStr(lp_value_int64) + " ";
3028 } else {
3029 s += "? ";
3030 }
3031 s += DomainToString(variable.mutable_domain());
3032 return s;
3033}
3034
3035std::string ConstraintToString(const sat::ConstraintProto& constraint,
3036 const sat::CpModelProto& model_,
3037 bool show_enforcement = true) {
3038 std::string s = "";
3039 if (constraint.has_linear()) {
3040 const auto& linear = constraint.linear();
3041 for (int j = 0; j < linear.vars().size(); ++j) {
3042 const std::string sign = linear.coeffs(j) > 0 ? "+" : "-";
3043 const std::string mult =
3044 std::abs(linear.coeffs(j)) != 1
3045 ? std::to_string(std::abs(linear.coeffs(j))) + " * "
3046 : "";
3047 if (j > 0 || sign != "+") s += sign + " ";
3048 s += mult + model_.variables(linear.vars(j)).name() + " ";
3049 }
3050 s += DomainToString(&linear.domain());
3051
3052 // Enforcement literal.
3053 if (show_enforcement) {
3054 for (int j = 0; j < constraint.enforcement_literal_size(); ++j) {
3055 s += (j == 0) ? "\t if " : " and ";
3056 s += model_.variables(constraint.enforcement_literal(j)).name();
3057 }
3058 }
3059 } else {
3060 s += constraint.ShortDebugString();
3061 }
3062 return s;
3063}
3064
3066 absl::flat_hash_map<std::string, std::pair<sat::IntegerVariableProto, int>>&
3067 variables,
3068 absl::flat_hash_map<std::string, std::vector<int>>& variable_instances,
3069 absl::flat_hash_map<std::string, absl::flat_hash_set<std::string>>&
3070 variable_childs,
3071 const sat::CpSolverResponse& response_, absl::string_view variable,
3072 std::string prefix = "") {
3073 if (variable.empty()) {
3074 std::string s = "";
3075 const auto& childs = variable_childs[""];
3076 for (const std::string& child : childs) {
3077 s += prefix +
3078 VariablesToString(variables, variable_instances, variable_childs,
3079 response_, child, prefix) +
3080 prefix + "\n";
3081 }
3082 return s;
3083 }
3084
3085 const auto& instances = variable_instances[variable];
3086 std::string variable_display(variable);
3087 std::size_t bracket_pos = variable.find_last_of(')');
3088 if (bracket_pos != std::string::npos) {
3089 variable_display = variable.substr(bracket_pos + 1);
3090 }
3091 std::string s = variable_display + " | ";
3092 prefix += std::string(variable_display.length(), ' ') + " | ";
3093 for (int i = 0; i < instances.size(); ++i) {
3094 const std::string instance_name =
3095 absl::StrFormat("%s(%ld)", variable, instances[i]);
3096 if (i > 0) s += prefix;
3097 s += absl::StrFormat("%ld: %s", instances[i],
3098 VariableToString(variables[instance_name], response_));
3099
3100 // Childs
3101 const auto& childs = variable_childs[instance_name];
3102 for (const std::string& child : childs) {
3103 s += "\n" + prefix + "| ";
3104 s += VariablesToString(variables, variable_instances, variable_childs,
3105 response_, child, prefix + "| ");
3106 }
3107 if (childs.empty()) s += "\n";
3108 }
3109 return s;
3110}
3111
3113 // Constraints you want to separate.
3114 std::vector<std::vector<std::string>> constraints_apart;
3115 constraints_apart.push_back(
3116 {"compression_cost", "travel_compression_absolute"});
3117
3118 // variable_instances links the lemma of a variable to the different number of
3119 // instantiation. For instance if you have in your model x(0), x(1) and x(4),
3120 // the key "x" will be associated to {0,1,4}.
3121 absl::flat_hash_map<std::string, std::vector<int>> variable_instances;
3122 // variable_children links a variable to its children. That is, if you have in
3123 // you model x(0), then typical childs would be {"x(0)in_segment(0)",
3124 // "x(0)in_segment(1)", "x(0)scaled", ...}
3125 absl::flat_hash_map<std::string, absl::flat_hash_set<std::string>>
3126 variable_children;
3127 // variables link the name of a variable to its Proto.
3128 absl::flat_hash_map<std::string, std::pair<sat::IntegerVariableProto, int>>
3129 variables;
3130 variable_children[""] = {};
3131
3132 const int num_constraints = model_.constraints_size();
3133 const int num_variables = model_.variables_size();
3134 int num_binary_variables = 0;
3135 for (int i = 0; i < num_variables; ++i) {
3136 const auto& variable = model_.variables(i);
3137 const auto& name = variable.name();
3138 const int pos_bracket = name.find_last_of('(');
3139 if (pos_bracket != std::string::npos) {
3140 const std::string lemma = name.substr(0, pos_bracket);
3141 const int pos_closing_bracket = name.find_last_of(')');
3142 CHECK_NE(pos_closing_bracket, std::string::npos);
3143 const int index =
3144 std::stoi(name.substr(pos_bracket + 1, pos_closing_bracket));
3145 std::vector<int>* instances = gtl::FindOrNull(variable_instances, lemma);
3146 if (instances != nullptr) {
3147 instances->push_back(index);
3148 } else {
3149 variable_instances[lemma] = {index};
3150 }
3151 variable_children[name] = {};
3152
3153 std::string parent = "";
3154 const int pos_parent_closing_bracket = lemma.find_last_of(')');
3155 if (pos_parent_closing_bracket != std::string::npos) {
3156 parent = lemma.substr(0, pos_parent_closing_bracket + 1);
3157 }
3158 variable_children[parent].emplace(lemma);
3159 variables[name] = std::make_pair(variable, i);
3160 if (variable.domain(0) == 0 & variable.domain(1) == 1) {
3161 ++num_binary_variables;
3162 }
3163 }
3164 }
3165
3166 // Preparing constraints
3167 // the constraints hashmap associate enforcement to constraints.
3168 // If the ket is "", then the constraint has no enforcement and if the key is
3169 // "multiple", then the constraint has several enforcement. If the constraint
3170 // has a single enforcement, then the key will be the variable name of the
3171 // enforcement.
3172 absl::flat_hash_map<std::string, std::vector<sat::ConstraintProto>>
3173 constraints;
3174 absl::flat_hash_map<std::vector<std::string>,
3175 std::vector<sat::ConstraintProto>>
3176 constraint_groups;
3177 for (int i = 0; i < num_constraints; ++i) {
3178 const auto& constraint = model_.constraints(i);
3179 std::string enforcement = "";
3180 if (constraint.enforcement_literal_size() == 1) {
3181 enforcement = model_.variables(constraint.enforcement_literal(0)).name();
3182 } else if (constraint.enforcement_literal_size() > 1) {
3183 enforcement = "multiple";
3184 } else {
3185 if (constraint.has_linear()) {
3186 const auto& linear = constraint.linear();
3187 std::vector<std::string> key;
3188 for (int j = 0; j < linear.vars().size(); ++j) {
3189 std::string var_name = model_.variables(linear.vars(j)).name();
3190 std::string lemma = var_name.substr(0, var_name.find_last_of('('));
3191 key.push_back(lemma);
3192 }
3193 auto* constraint_group = gtl::FindOrNull(constraint_groups, key);
3194 if (constraint_group != nullptr) {
3195 constraint_group->push_back(constraint);
3196 } else {
3197 constraint_groups[key] = {constraint};
3198 }
3199 }
3200 }
3201 auto* constraints_enforced = gtl::FindOrNull(constraints, enforcement);
3202 if (constraints_enforced != nullptr) {
3203 constraints[enforcement].push_back(constraint);
3204 } else {
3205 constraints[enforcement] = {constraint};
3206 }
3207 }
3208
3209 const std::string prefix_constraint = " • ";
3210 std::string s = "Using RoutingCPSatWrapper.\n";
3211 s += absl::StrFormat("\nObjective = %f\n", this->GetObjectiveValue());
3212
3213 for (int i = 0; i < objective_coefficients_.size(); ++i) {
3214 double coeff = objective_coefficients_[i];
3215 if (coeff != 0) {
3216 s += absl::StrFormat(" | %f * %s\n", coeff, model_.variables(i).name());
3217 }
3218 }
3219
3220 s += absl::StrFormat("\nVariables %d (%d Binary - %d Non Binary)\n",
3221 num_variables, num_binary_variables,
3222 num_variables - num_binary_variables);
3223 s += VariablesToString(variables, variable_instances, variable_children,
3224 response_, "", " | ");
3225 s += absl::StrFormat("\n\nConstraints (%d)\n", num_constraints);
3226
3227 // Constraints NOT enforced
3228 s += "\n- Not enforced\n";
3229 bool at_least_one_not_enforced = false;
3230 for (const auto& pair : constraint_groups) {
3231 if (!std::count(constraints_apart.begin(), constraints_apart.end(),
3232 pair.first)) {
3233 for (const auto& constraint : pair.second) {
3234 s += prefix_constraint + ConstraintToString(constraint, model_, true) +
3235 "\n";
3236 at_least_one_not_enforced = true;
3237 }
3238 }
3239 }
3240 if (!at_least_one_not_enforced) {
3241 s += prefix_constraint + "None\n";
3242 }
3243
3244 // Constraints with a SINGLE enforcement
3245 s += "\n- Single enforcement\n";
3246 bool at_least_one_single_enforced = false;
3247 for (const auto& pair : variable_instances) {
3248 const std::string lemma = pair.first;
3249 bool found_one_constraint = false;
3250 std::string prefix = "";
3251 for (int instance : pair.second) {
3252 const std::string enforcement =
3253 absl::StrFormat("%s(%d)", lemma, instance);
3254 auto* constraints_enforced = gtl::FindOrNull(constraints, enforcement);
3255 std::string prefix_instance = "";
3256 if (constraints_enforced != nullptr) {
3257 at_least_one_single_enforced = true;
3258 if (!found_one_constraint) {
3259 found_one_constraint = true;
3260 s += prefix_constraint + "if " + lemma + " | ";
3261 prefix =
3262 std::string(prefix_constraint.size() + 1 + lemma.size(), ' ') +
3263 " | ";
3264 } else {
3265 s += prefix;
3266 }
3267 s += absl::StrFormat("%d: | ", instance);
3268 prefix_instance = prefix + " | ";
3269 bool first = true;
3270 for (const auto& constraint : *constraints_enforced) {
3271 if (!first)
3272 s += prefix_instance;
3273 else
3274 first = false;
3275 s += ConstraintToString(constraint, model_, false) + "\n";
3276 }
3277 }
3278 }
3279 }
3280 if (!at_least_one_single_enforced) {
3281 s += prefix_constraint + "None\n";
3282 }
3283
3284 // Constraints with MULTIPLE enforcement
3285 s += "\n- Multiple enforcement\n";
3286 auto* constraints_multiple_enforced =
3287 gtl::FindOrNull(constraints, "multiple");
3288 if (constraints_multiple_enforced != nullptr) {
3289 for (const auto& constraint : *constraints_multiple_enforced) {
3290 s += prefix_constraint + ConstraintToString(constraint, model_, true) +
3291 "\n";
3292 }
3293 } else {
3294 s += prefix_constraint + "None\n";
3295 }
3296
3297 // Constraints apart
3298 s += "\n- Set apart\n";
3299 bool at_least_one_apart = false;
3300 for (const auto& pair : constraint_groups) {
3301 if (std::count(constraints_apart.begin(), constraints_apart.end(),
3302 pair.first)) {
3303 for (const auto& constraint : pair.second) {
3304 s += prefix_constraint + ConstraintToString(constraint, model_, true) +
3305 "\n";
3306 at_least_one_apart = true;
3307 }
3308 }
3309 }
3310 if (!at_least_one_apart) {
3311 s += prefix_constraint + "None\n";
3312 }
3313
3314 return s;
3315}
3316
3317} // namespace operations_research
IntegerValue size
CumulBoundsPropagator(const RoutingDimension *dimension)
bool PropagateCumulBounds(const std::function< int64_t(int64_t)> &next_accessor, int64_t cumul_offset, const std::vector< RoutingModel::RouteDimensionTravelInfo > *dimension_travel_info_per_route=nullptr)
const RoutingDimension & dimension() const
DimensionSchedulingStatus Optimize(const std::function< int64_t(int64_t)> &next_accessor, const std::vector< RouteDimensionTravelInfo > &dimension_travel_info_per_route, RoutingLinearSolverWrapper *solver, std::vector< int64_t > *cumul_values, std::vector< int64_t > *break_values, std::vector< std::vector< int > > *resource_indices_per_group, int64_t *cost_without_transits, int64_t *transit_cost, bool clear_lp=true, bool optimize_resource_assignment=true)
DimensionSchedulingStatus OptimizeAndPack(const std::function< int64_t(int64_t)> &next_accessor, const std::vector< RouteDimensionTravelInfo > &dimension_travel_info_per_route, RoutingLinearSolverWrapper *solver, std::vector< int64_t > *cumul_values, std::vector< int64_t > *break_values)
DimensionSchedulingStatus OptimizeSingleRouteWithResource(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const RouteDimensionTravelInfo &dimension_travel_info, const Resource *resource, bool optimize_vehicle_costs, RoutingLinearSolverWrapper *solver, std::vector< int64_t > *cumul_values, std::vector< int64_t > *break_values, int64_t *cost_without_transit, int64_t *transit_cost, bool clear_lp=true)
DimensionCumulOptimizerCore(const RoutingDimension *dimension, bool use_precedence_propagator)
std::vector< DimensionSchedulingStatus > OptimizeSingleRouteWithResources(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const std::function< int64_t(int64_t, int64_t)> &transit_accessor, const RouteDimensionTravelInfo &dimension_travel_info, const std::vector< Resource > &resources, const std::vector< int > &resource_indices, bool optimize_vehicle_costs, RoutingLinearSolverWrapper *solver, std::vector< std::vector< int64_t > > *cumul_values, std::vector< std::vector< int64_t > > *break_values, std::vector< int64_t > *costs_without_transits, int64_t *transit_cost, bool clear_lp=true)
DimensionSchedulingStatus OptimizeAndPackSingleRoute(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const RouteDimensionTravelInfo &dimension_travel_info, const Resource *resource, RoutingLinearSolverWrapper *solver, std::vector< int64_t > *cumul_values, std::vector< int64_t > *break_values)
DimensionSchedulingStatus ComputeSingleRouteSolutionCostWithoutFixedTransits(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const RouteDimensionTravelInfo &dimension_travel_info, RoutingLinearSolverWrapper *solver, absl::Span< const int64_t > solution_cumul_values, absl::Span< const int64_t > solution_break_values, int64_t *cost_without_transits, int64_t *cost_offset=nullptr, bool reuse_previous_model_if_possible=true, bool clear_lp=false, bool clear_solution_constraints=true, absl::Duration *solve_duration=nullptr)
NOTE(user): T=bool is not yet supported (the [] operator doesn't work).
Definition flat_matrix.h:32
DimensionSchedulingStatus ComputeCumulCostWithoutFixedTransits(const std::function< int64_t(int64_t)> &next_accessor, int64_t *optimal_cost_without_transits)
DimensionSchedulingStatus ComputeCumuls(const std::function< int64_t(int64_t)> &next_accessor, const std::vector< RoutingModel::RouteDimensionTravelInfo > &dimension_travel_info_per_route, std::vector< int64_t > *optimal_cumuls, std::vector< int64_t > *optimal_breaks, std::vector< std::vector< int > > *optimal_resource_indices_per_group)
DimensionSchedulingStatus ComputePackedCumuls(const std::function< int64_t(int64_t)> &next_accessor, const std::vector< RoutingModel::RouteDimensionTravelInfo > &dimension_travel_info_per_route, std::vector< int64_t > *packed_cumuls, std::vector< int64_t > *packed_breaks)
virtual IntVarIterator * MakeDomainIterator(bool reversible) const =0
virtual bool Contains(int64_t v) const =0
virtual uint64_t Size() const =0
This method returns the number of values in the domain of the variable.
DimensionSchedulingStatus ComputeRouteSolutionCostWithoutFixedTransits(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const RoutingModel::RouteDimensionTravelInfo &dimension_travel_info, const std::vector< int64_t > &solution_cumul_values, const std::vector< int64_t > &solution_break_values, int64_t *solution_cost, int64_t *cost_offset=nullptr, bool reuse_previous_model_if_possible=false, bool clear_lp=true, absl::Duration *solve_duration=nullptr)
DimensionSchedulingStatus ComputeRouteCumulsAndCostWithoutFixedTransits(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const RoutingModel::RouteDimensionTravelInfo &dimension_travel_info, std::vector< int64_t > *optimal_cumuls, std::vector< int64_t > *optimal_breaks, int64_t *optimal_cost_without_transits)
LocalDimensionCumulOptimizer(const RoutingDimension *dimension, RoutingSearchParameters::SchedulingSolver solver_type)
LocalDimensionCumulOptimizer.
DimensionSchedulingStatus ComputeRouteCumuls(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const RoutingModel::RouteDimensionTravelInfo &dimension_travel_info, const RoutingModel::ResourceGroup::Resource *resource, std::vector< int64_t > *optimal_cumuls, std::vector< int64_t > *optimal_breaks)
DimensionSchedulingStatus ComputeRouteCumulCost(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, int64_t *optimal_cost)
DimensionSchedulingStatus ComputeRouteCumulCostWithoutFixedTransits(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, int64_t *optimal_cost_without_transits)
std::vector< DimensionSchedulingStatus > ComputeRouteCumulCostsForResourcesWithoutFixedTransits(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const std::function< int64_t(int64_t, int64_t)> &transit_accessor, const std::vector< RoutingModel::ResourceGroup::Resource > &resources, const std::vector< int > &resource_indices, bool optimize_vehicle_costs, std::vector< int64_t > *optimal_costs_without_transits, std::vector< std::vector< int64_t > > *optimal_cumuls, std::vector< std::vector< int64_t > > *optimal_breaks)
DimensionSchedulingStatus ComputePackedRouteCumuls(int vehicle, const std::function< int64_t(int64_t)> &next_accessor, const RoutingModel::RouteDimensionTravelInfo &dimension_travel_info, const RoutingModel::ResourceGroup::Resource *resource, std::vector< int64_t > *packed_cumuls, std::vector< int64_t > *packed_breaks)
static int64_t FastInt64Round(double x)
Definition mathutil.h:272
std::string PrintModel() const override
Prints an understandable view of the model.
virtual int64_t GetObjectiveValue() const =0
virtual DimensionSchedulingStatus Solve(absl::Duration duration_limit)=0
virtual int64_t GetVariableUpperBound(int index) const =0
virtual double GetValue(int index) const =0
virtual void SetObjectiveCoefficient(int index, double coefficient)=0
virtual bool SetVariableBounds(int index, int64_t lower_bound, int64_t upper_bound)=0
virtual void SetParameters(const std::string &parameters)=0
This function is meant to override the parameters of the solver.
virtual bool ModelIsEmpty() const
Returns if the model is empty or not.
virtual int64_t GetVariableLowerBound(int index) const =0
ArcIndex AddArcWithCapacityAndUnitCost(NodeIndex tail, NodeIndex head, FlowQuantity capacity, CostValue unit_cost)
void SetNodeSupply(NodeIndex node, FlowQuantity supply)
FlowQuantity Flow(ArcIndex arc) const
int64_t b
Definition table.cc:45
Block * next
SatParameters parameters
const std::string name
A name for logging purposes.
const Constraint * ct
int64_t value
IntVar * var
int64_t coef
absl::Status status
Definition g_gurobi.cc:44
double upper_bound
double lower_bound
absl::Span< const double > coefficients
GRBmodel * model
int arc
int index
const Collection::value_type::second_type * FindOrNull(const Collection &collection, const typename Collection::value_type::first_type &key)
Definition map_util.h:65
double FindBestScalingAndComputeErrors(const std::vector< double > &coefficients, absl::Span< const double > lower_bounds, absl::Span< const double > upper_bounds, int64_t max_absolute_activity, double wanted_absolute_activity_precision, double *relative_coeff_error, double *scaled_sum_error)
Definition lp_utils.cc:872
In SWIG mode, we don't want anything besides these top-level includes.
int64_t CapAdd(int64_t x, int64_t y)
std::string ConstraintToString(const sat::ConstraintProto &constraint, const sat::CpModelProto &model_, bool show_enforcement=true)
std::string DomainToString(const ::google::protobuf::RepeatedField< int64_t > *domain)
@ INFEASIBLE
A solution could not be found.
@ OPTIMAL
An optimal solution was found respecting all constraints.
int64_t CapSub(int64_t x, int64_t y)
PiecewiseEvaluationStatus ComputePiecewiseLinearFormulationValue(const RoutingModel::RouteDimensionTravelInfo::TransitionInfo::PiecewiseLinearFormulation &pwl, int64_t x, int64_t *value, double delta)
std::vector< bool > SlopeAndYInterceptToConvexityRegions(const std::vector< SlopeAndYIntercept > &slope_and_y_intercept)
int64_t ComputeConvexPiecewiseLinearFormulationValue(const RoutingModel::RouteDimensionTravelInfo::TransitionInfo::PiecewiseLinearFormulation &pwl, int64_t x, double delta)
bool ComputeVehicleToResourceClassAssignmentCosts(int v, const RoutingModel::ResourceGroup &resource_group, const util_intops::StrongVector< RoutingModel::ResourceClassIndex, absl::flat_hash_set< int > > &ignored_resources_per_class, const std::function< int64_t(int64_t)> &next_accessor, const std::function< int64_t(int64_t, int64_t)> &transit_accessor, bool optimize_vehicle_costs, LocalDimensionCumulOptimizer *lp_optimizer, LocalDimensionCumulOptimizer *mp_optimizer, std::vector< int64_t > *assignment_costs, std::vector< std::vector< int64_t > > *cumul_values, std::vector< std::vector< int64_t > > *break_values)
int64_t CapProd(int64_t x, int64_t y)
void FillPathEvaluation(const std::vector< int64_t > &path, const RoutingModel::TransitCallback2 &evaluator, std::vector< int64_t > *values)
Definition routing.cc:6095
std::string VariablesToString(absl::flat_hash_map< std::string, std::pair< sat::IntegerVariableProto, int > > &variables, absl::flat_hash_map< std::string, std::vector< int > > &variable_instances, absl::flat_hash_map< std::string, absl::flat_hash_set< std::string > > &variable_childs, const sat::CpSolverResponse &response_, absl::string_view variable, std::string prefix="")
std::string Int64ToStr(int64_t number)
std::vector< SlopeAndYIntercept > PiecewiseLinearFormulationToSlopeAndYIntercept(const RoutingModel::RouteDimensionTravelInfo::TransitionInfo::PiecewiseLinearFormulation &pwl_function, int index_start, int index_end)
int64_t ComputeBestVehicleToResourceAssignment(const std::vector< int > &vehicles, const util_intops::StrongVector< RoutingModel::ResourceClassIndex, std::vector< int > > &resource_indices_per_class, const util_intops::StrongVector< RoutingModel::ResourceClassIndex, absl::flat_hash_set< int > > &ignored_resources_per_class, std::function< const std::vector< int64_t > *(int)> vehicle_to_resource_class_assignment_costs, std::vector< int > *resource_indices)
std::string VariableToString(std::pair< sat::IntegerVariableProto, int > &variable_pair, const sat::CpSolverResponse &response_)
const Variable x
Definition qp_tests.cc:127
int64_t delta
Definition resource.cc:1709
IntervalVar * interval
Definition resource.cc:101
std::vector< int64_t > assignment_costs
std::vector< std::vector< int64_t > > cumul_values
std::vector< std::vector< int64_t > > break_values
int64_t bound
#define SET_DEBUG_VARIABLE_NAME(solver, var, name)
int tail
std::vector< double > lower_bounds
Definition lp_utils.cc:746
std::vector< double > upper_bounds
Definition lp_utils.cc:747
static const int64_t kint64max
Definition types.h:32
static const int64_t kint64min
Definition types.h:31