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