diff --git a/ortools/constraint_solver/routing.cc b/ortools/constraint_solver/routing.cc index a863578cb3..dc03674a96 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/constraint_solver/routing.cc @@ -164,8 +164,8 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { public: SetCumulsFromLocalDimensionCosts( const std::vector& dimensions_for_local_optimizer, - SearchMonitor* monitor) - : monitor_(monitor) { + SearchMonitor* monitor, bool optimize_and_pack = false) + : monitor_(monitor), optimize_and_pack_(optimize_and_pack) { local_optimizers_.reserve(dimensions_for_local_optimizer.size()); for (RoutingDimension* const dimension : dimensions_for_local_optimizer) { local_optimizers_.emplace_back(dimension); @@ -178,17 +178,24 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { bool should_fail = false; for (LocalDimensionCumulOptimizer& optimizer : local_optimizers_) { const RoutingDimension* const dimension = optimizer.dimension(); + const bool dimension_has_breaks = dimension->HasBreakConstraints(); RoutingModel* const model = dimension->model(); const auto next = [model](int64 i) { return model->NextVar(i)->Value(); }; for (int vehicle = 0; vehicle < model->vehicles(); ++vehicle) { // TODO(user): Investigate if we should skip unused vehicles. - if (dimension->VehicleHasBreakIntervals(vehicle)) continue; - if (dimension->GetSpanCostCoefficientForVehicle(vehicle) <= 0) continue; + if (dimension_has_breaks && + !dimension->GetBreakIntervalsOfVehicle(vehicle).empty()) + continue; DCHECK(DimensionFixedTransitsEqualTransitEvaluatorForVehicle(*dimension, vehicle)); std::vector cumul_values; - if (!optimizer.ComputeRouteCumuls(vehicle, next, &cumul_values)) { + const bool cumuls_optimized = + optimize_and_pack_ + ? optimizer.ComputePackedRouteCumuls(vehicle, next, + &cumul_values) + : optimizer.ComputeRouteCumuls(vehicle, next, &cumul_values); + if (!cumuls_optimized) { should_fail = true; break; } @@ -222,6 +229,7 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { private: std::vector local_optimizers_; SearchMonitor* const monitor_; + const bool optimize_and_pack_; }; class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { @@ -280,21 +288,22 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { } // namespace -const Assignment* -RoutingModel::PackCumulsOfGlobalOptimizerDimensionsFromAssignment( +const Assignment* RoutingModel::PackCumulsOfOptimizerDimensionsFromAssignment( const Assignment* original_assignment, absl::Duration duration_limit) { - RegularLimit* const limit = GetOrCreateLimit(); + CHECK(closed_); const int64 time_limit_ms = absl::time_internal::IsInfiniteDuration(duration_limit) ? kint64max : absl::ToInt64Milliseconds(duration_limit); - limit->UpdateLimits(time_limit_ms, kint64max, kint64max, kint64max); - CHECK(closed_); - if (original_assignment == nullptr || - dimensions_for_global_optimizer_.empty()) { + if (time_limit_ms <= 0 || original_assignment == nullptr || + (dimensions_for_global_optimizer_.empty() && + dimensions_for_local_optimizer_.empty())) { return original_assignment; } + RegularLimit* const limit = GetOrCreateLimit(); + limit->UpdateLimits(time_limit_ms, kint64max, kint64max, kint64max); + // Initialize the packed_assignment with the Next values in the // original_assignment. Assignment* packed_assignment = solver_->MakeAssignment(); @@ -305,6 +314,11 @@ RoutingModel::PackCumulsOfGlobalOptimizerDimensionsFromAssignment( decision_builders.push_back(solver_->MakeRestoreAssignment(preassignment_)); decision_builders.push_back( solver_->MakeRestoreAssignment(packed_assignment)); + decision_builders.push_back( + solver_->RevAlloc(new SetCumulsFromLocalDimensionCosts( + dimensions_for_local_optimizer_, + GetOrCreateLargeNeighborhoodSearchLimit(), + /*optimize_and_pack=*/true))); decision_builders.push_back( solver_->RevAlloc(new SetCumulsFromGlobalDimensionCosts( dimensions_for_global_optimizer_, @@ -667,6 +681,7 @@ RoutingModel::RoutingModel(const RoutingIndexManager& index_manager, vehicle_pickup_delivery_policy_(vehicles_, PICKUP_AND_DELIVERY_NO_ORDER), has_hard_type_incompatibilities_(false), has_temporal_type_incompatibilities_(false), + has_same_vehicle_type_requirements_(false), has_temporal_type_requirements_(false), num_visit_types_(0), starts_(vehicles_), @@ -1951,38 +1966,70 @@ void RoutingModel::CloseModelWithParameters( for (const RoutingDimension* dimension : dimensions_) { dimension->SetupGlobalSpanCost(&cost_elements); dimension->SetupSlackAndDependentTransitCosts(); - bool has_span_constraint = false; - for (const int64 coeff : dimension->vehicle_span_cost_coefficients()) { - if (coeff != 0) { - has_span_constraint = true; - break; - } - } - if (!has_span_constraint) { - for (const int64 value : dimension->vehicle_span_upper_bounds()) { - if (value != 0) { - has_span_constraint = true; - break; - } - } - } + const std::vector& span_costs = + dimension->vehicle_span_cost_coefficients(); + const std::vector& span_ubs = dimension->vehicle_span_upper_bounds(); + const bool has_span_constraint = + std::any_of(span_costs.begin(), span_costs.end(), + [](int64 coeff) { return coeff != 0; }) || + std::any_of(span_ubs.begin(), span_ubs.end(), + [](int64 value) { return value < kint64max; }) || + dimension->HasSoftSpanUpperBounds(); if (has_span_constraint) { - std::vector total_slack(vehicles(), nullptr); + std::vector spans(vehicles(), nullptr); + std::vector total_slacks(vehicles(), nullptr); + // Generate variables only where needed. for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { - const int64 cost = dimension->vehicle_span_cost_coefficients()[vehicle]; - const int64 limit = dimension->vehicle_span_upper_bounds()[vehicle]; - if (cost != 0 || limit < kint64max) { - total_slack[vehicle] = solver_->MakeIntVar(0, limit, ""); - if (cost != 0) { - cost_elements.push_back( - solver_ - ->MakeProd(vehicle_costs_considered_[vehicle], - solver_->MakeProd(total_slack[vehicle], cost)) - ->Var()); - } + if (span_ubs[vehicle] < kint64max) { + spans[vehicle] = solver_->MakeIntVar(0, span_ubs[vehicle], ""); + } + if (span_costs[vehicle] != 0) { + total_slacks[vehicle] = solver_->MakeIntVar(0, span_ubs[vehicle], ""); + } + } + if (dimension->HasSoftSpanUpperBounds()) { + for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { + if (spans[vehicle]) continue; + const SimpleBoundCosts::BoundCost bound_cost = + dimension->GetSoftSpanUpperBoundForVehicle(vehicle); + if (bound_cost.cost == 0) continue; + spans[vehicle] = solver_->MakeIntVar(0, span_ubs[vehicle]); + } + } + solver_->AddConstraint( + MakePathSpansAndTotalSlacks(dimension, spans, total_slacks)); + // Add costs of variables. + for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { + if (span_costs[vehicle] != 0) { + DCHECK(total_slacks[vehicle] != nullptr); + cost_elements.push_back( + solver_ + ->MakeProd(vehicle_costs_considered_[vehicle], + solver_->MakeProd(total_slacks[vehicle], + span_costs[vehicle])) + ->Var()); + } + } + if (dimension->HasSoftSpanUpperBounds()) { + for (int vehicle = 0; vehicle < vehicles(); ++vehicle) { + const auto bound_cost = + dimension->GetSoftSpanUpperBoundForVehicle(vehicle); + if (bound_cost.cost == 0 || bound_cost.bound == kint64max) continue; + DCHECK(spans[vehicle] != nullptr); + // Additional cost is vehicle_cost_considered_[vehicle] * + // max(0, spans[vehicle] - bound_cost.bound) * bound_cost.cost. + cost_elements.push_back( + solver_ + ->MakeProd( + vehicle_costs_considered_[vehicle], + solver_->MakeProd( + solver_->MakeMax(solver_->MakeSum(spans[vehicle], + -bound_cost.bound), + 0), + bound_cost.cost)) + ->Var()); } } - solver_->AddConstraint(MakePathSlacks(dimension, total_slack)); } } // Penalty costs @@ -1992,7 +2039,7 @@ void RoutingModel::CloseModelWithParameters( cost_elements.push_back(penalty_var); } } - // Soft cumul upper bound costs + // Soft cumul lower/upper bound costs for (const RoutingDimension* dimension : dimensions_) { dimension->SetupCumulVarSoftLowerBoundCosts(&cost_elements); dimension->SetupCumulVarSoftUpperBoundCosts(&cost_elements); @@ -2885,16 +2932,27 @@ const Assignment* RoutingModel::SolveFromAssignmentWithParameters( LogSolution(parameters, "All Unperformed Solution", solution_pool.back()->ObjectiveValue(), start_time_ms); } - const int64 elapsed_time_ms = solver_->wall_time() - start_time_ms; - const int64 time_left_ms = - GetTimeLimitMs(parameters) != kint64max - ? GetTimeLimitMs(parameters) - elapsed_time_ms - : kint64max; - if (time_left_ms >= 0) { - limit_->UpdateLimits(time_left_ms, kint64max, kint64max, - parameters.solution_limit()); - ls_limit_->UpdateLimits(time_left_ms, kint64max, kint64max, 1); - solver_->Solve(solve_db_, monitors_); + if (parameters.use_cp_sat() == BOOL_TRUE) { + Assignment sat(solver_.get()); + if (SolveModelWithSat(this, &sat) && + AppendAssignmentIfFeasible(sat, &solution_pool) && + parameters.log_search()) { + LogSolution(parameters, "SAT", solution_pool.back()->ObjectiveValue(), + start_time_ms); + } + } + if (parameters.use_cp() == BOOL_TRUE) { + const int64 elapsed_time_ms = solver_->wall_time() - start_time_ms; + const int64 time_left_ms = + GetTimeLimitMs(parameters) != kint64max + ? GetTimeLimitMs(parameters) - elapsed_time_ms + : kint64max; + if (time_left_ms >= 0) { + limit_->UpdateLimits(time_left_ms, kint64max, kint64max, + parameters.solution_limit()); + ls_limit_->UpdateLimits(time_left_ms, kint64max, kint64max, 1); + solver_->Solve(solve_db_, monitors_); + } } } } else { @@ -3453,7 +3511,7 @@ void RoutingModel::AssignmentToRoutes( } int64 RoutingModel::GetArcCostForClassInternal( - int64 from_index, int64 to_index, CostClassIndex cost_class_index) { + int64 from_index, int64 to_index, CostClassIndex cost_class_index) const { DCHECK(closed_); DCHECK_GE(cost_class_index, 0); DCHECK_LT(cost_class_index, cost_classes_.size()); @@ -3514,7 +3572,7 @@ int64 RoutingModel::Next(const Assignment& assignment, int64 index) const { } int64 RoutingModel::GetArcCostForVehicle(int64 from_index, int64 to_index, - int64 vehicle) { + int64 vehicle) const { if (from_index != to_index && vehicle >= 0) { return GetArcCostForClassInternal(from_index, to_index, GetCostClassIndexOfVehicle(vehicle)); @@ -3525,7 +3583,7 @@ int64 RoutingModel::GetArcCostForVehicle(int64 from_index, int64 to_index, int64 RoutingModel::GetArcCostForClass( int64 from_index, int64 to_index, - int64 /*CostClassIndex*/ cost_class_index) { + int64 /*CostClassIndex*/ cost_class_index) const { if (from_index != to_index) { return GetArcCostForClassInternal(from_index, to_index, CostClassIndex(cost_class_index)); @@ -3535,7 +3593,7 @@ int64 RoutingModel::GetArcCostForClass( } int64 RoutingModel::GetArcCostForFirstSolution(int64 from_index, - int64 to_index) { + int64 to_index) const { // Return high cost if connecting to an end (or bound-to-end) node; // this is used in the cost-based first solution strategies to avoid closing // routes too soon. @@ -3674,6 +3732,8 @@ int RoutingModel::GetVisitType(int64 index) const { void RoutingModel::CloseVisitTypes() { hard_incompatible_types_per_type_index_.resize(num_visit_types_); temporal_incompatible_types_per_type_index_.resize(num_visit_types_); + same_vehicle_required_type_alternatives_per_type_index_.resize( + num_visit_types_); temporal_required_type_alternatives_per_type_index_.resize(num_visit_types_); } @@ -3709,6 +3769,24 @@ RoutingModel::GetTemporalTypeIncompatibilitiesOfType(int type) const { return temporal_incompatible_types_per_type_index_[type]; } +// TODO(user): Consider if an empty "required_type_alternatives" should mean +// trivially feasible requirement, as there are no required type alternatives? +void RoutingModel::AddSameVehicleRequiredTypeAlternatives( + int dependent_type, absl::flat_hash_set required_type_alternatives) { + DCHECK_LT(dependent_type, + same_vehicle_required_type_alternatives_per_type_index_.size()); + + if (required_type_alternatives.empty()) { + // The dependent_type requires an infeasible (empty) set of types. + trivially_infeasible_visit_types_.insert(dependent_type); + return; + } + + has_same_vehicle_type_requirements_ = true; + same_vehicle_required_type_alternatives_per_type_index_[dependent_type] + .push_back(std::move(required_type_alternatives)); +} + void RoutingModel::AddTemporalRequiredTypeAlternatives( int dependent_type, absl::flat_hash_set required_type_alternatives) { DCHECK_LT(dependent_type, @@ -3725,6 +3803,14 @@ void RoutingModel::AddTemporalRequiredTypeAlternatives( std::move(required_type_alternatives)); } +const std::vector>& +RoutingModel::GetSameVehicleRequiredTypeAlternativesOfType(int type) const { + DCHECK_GE(type, 0); + DCHECK_LT(type, + same_vehicle_required_type_alternatives_per_type_index_.size()); + return same_vehicle_required_type_alternatives_per_type_index_[type]; +} + const std::vector>& RoutingModel::GetTemporalRequiredTypeAlternativesOfType(int type) const { DCHECK_GE(type, 0); @@ -4245,7 +4331,7 @@ RoutingModel::GetOrCreateLocalSearchFilters() { filters_.insert(filters_.end(), cumul_filters.begin(), cumul_filters.end()); for (const RoutingDimension* dimension : dimensions_) { - if (!dimension->vehicle_break_intervals_.empty()) { + if (dimension->HasBreakConstraints()) { IntVarLocalSearchFilter* breaks_filter = MakeVehicleBreaksFilter(*this, *dimension); filters_.push_back(breaks_filter); @@ -4291,7 +4377,7 @@ RoutingModel::GetOrCreateFeasibilityFilters() { } for (const RoutingDimension* dimension : dimensions_) { - if (!dimension->vehicle_break_intervals_.empty()) { + if (dimension->HasBreakConstraints()) { IntVarLocalSearchFilter* breaks_filter = MakeVehicleBreaksFilter(*this, *dimension); feasibility_filters_.push_back(breaks_filter); @@ -4349,16 +4435,22 @@ void RoutingModel::StoreDimensionsForDimensionCumulOptimizers() { dimension->CumulVar(Start(vehicle))->Min() - 1) : 0; } - if (has_span_cost) { - for (int i = 0; i < dimension->cumuls().size(); ++i) { - if (dimension->HasCumulVarSoftUpperBound(i) || - dimension->HasCumulVarSoftLowerBound(i)) { - dimension->SetVehicleOffsetsForLocalOptimizer( - std::move(vehicle_offsets)); - dimensions_for_local_optimizer_.push_back(dimension); - break; - } + bool has_soft_lower_bound = false; + bool has_soft_upper_bound = false; + for (int i = 0; i < dimension->cumuls().size(); ++i) { + if (dimension->HasCumulVarSoftLowerBound(i)) { + has_soft_lower_bound = true; } + if (dimension->HasCumulVarSoftUpperBound(i)) { + has_soft_upper_bound = true; + } + } + if (has_span_cost ^ has_soft_lower_bound ? has_soft_upper_bound + : has_span_cost) { + dimension->SetVehicleOffsetsForLocalOptimizer( + std::move(vehicle_offsets)); + dimensions_for_local_optimizer_.push_back(dimension); + packed_dimensions_collector_assignment->Add(dimension->cumuls()); } } } @@ -4897,25 +4989,31 @@ void RoutingModel::AddIntervalToAssignment(IntervalVar* const interval) { namespace { -class PathSlacks : public Constraint { +class PathSpansAndTotalSlacks : public Constraint { public: - PathSlacks(const RoutingModel* model, const RoutingDimension* dimension, - std::vector total_slacks) + PathSpansAndTotalSlacks(const RoutingModel* model, + const RoutingDimension* dimension, + std::vector spans, + std::vector total_slacks) : Constraint(model->solver()), model_(model), dimension_(dimension), + spans_(std::move(spans)), total_slacks_(std::move(total_slacks)) { - DCHECK_LE(total_slacks_.size(), model_->vehicles()); + CHECK_EQ(spans_.size(), model_->vehicles()); + CHECK_EQ(total_slacks_.size(), model_->vehicles()); vehicle_demons_.resize(model_->vehicles()); } + std::string DebugString() const override { return "PathSpansAndTotalSlacks"; } + void Post() override { const int num_nodes = model_->VehicleVars().size(); const int num_transits = model_->Nexts().size(); for (int node = 0; node < num_nodes; ++node) { - auto* demon = MakeConstraintDemon1(model_->solver(), this, - &PathSlacks::PropagateNode, - "PathSlacks::PropagateNode", node); + auto* demon = MakeConstraintDemon1( + model_->solver(), this, &PathSpansAndTotalSlacks::PropagateNode, + "PathSpansAndTotalSlacks::PropagateNode", node); dimension_->CumulVar(node)->WhenRange(demon); model_->VehicleVar(node)->WhenBound(demon); if (node < num_transits) { @@ -4924,17 +5022,17 @@ class PathSlacks : public Constraint { model_->NextVar(node)->WhenBound(demon); } } - for (int vehicle = 0; vehicle < total_slacks_.size(); ++vehicle) { - if (total_slacks_[vehicle] == nullptr) continue; + for (int vehicle = 0; vehicle < spans_.size(); ++vehicle) { + if (!spans_[vehicle] && !total_slacks_[vehicle]) continue; auto* demon = MakeDelayedConstraintDemon1( - solver(), this, &PathSlacks::PropagateVehicle, - "PathSlacks::PropagateVehicle", vehicle); + solver(), this, &PathSpansAndTotalSlacks::PropagateVehicle, + "PathSpansAndTotalSlacks::PropagateVehicle", vehicle); vehicle_demons_[vehicle] = demon; - total_slacks_[vehicle]->WhenRange(demon); - if (dimension_->VehicleHasBreakIntervals(vehicle)) { - for (IntervalVar* br : - dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - br->WhenAnything(demon); + if (spans_[vehicle]) spans_[vehicle]->WhenRange(demon); + if (total_slacks_[vehicle]) total_slacks_[vehicle]->WhenRange(demon); + if (dimension_->HasBreakConstraints()) { + for (IntervalVar* b : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { + b->WhenAnything(demon); } } } @@ -4942,7 +5040,8 @@ class PathSlacks : public Constraint { // Call propagator on all vehicles. void InitialPropagate() override { - for (int vehicle = 0; vehicle < total_slacks_.size(); ++vehicle) { + for (int vehicle = 0; vehicle < spans_.size(); ++vehicle) { + if (!spans_[vehicle] && !total_slacks_[vehicle]) continue; PropagateVehicle(vehicle); } } @@ -4958,59 +5057,60 @@ class PathSlacks : public Constraint { EnqueueDelayedDemon(vehicle_demons_[vehicle]); } + // In order to make reasoning on span and total_slack of a vehicle uniform, + // we rely on the fact that span == sum_fixed_transits + total_slack + // to present both span and total_slack in terms of span and fixed transit. + // This allows to use the same code whether there actually are variables + // for span and total_slack or not. + int64 SpanMin(int vehicle, int64 sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + const int64 span_min = spans_[vehicle] ? spans_[vehicle]->Min() : kint64max; + const int64 total_slack_min = + total_slacks_[vehicle] ? total_slacks_[vehicle]->Min() : kint64max; + return std::min(span_min, CapAdd(total_slack_min, sum_fixed_transits)); + } + int64 SpanMax(int vehicle, int64 sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + const int64 span_max = spans_[vehicle] ? spans_[vehicle]->Max() : kint64min; + const int64 total_slack_max = + total_slacks_[vehicle] ? total_slacks_[vehicle]->Max() : kint64min; + return std::max(span_max, CapAdd(total_slack_max, sum_fixed_transits)); + } + void SetSpanMin(int vehicle, int64 min, int64 sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + if (spans_[vehicle]) { + spans_[vehicle]->SetMin(min); + } + if (total_slacks_[vehicle]) { + total_slacks_[vehicle]->SetMin(CapSub(min, sum_fixed_transits)); + } + } + void SetSpanMax(int vehicle, int64 max, int64 sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + if (spans_[vehicle]) { + spans_[vehicle]->SetMax(max); + } + if (total_slacks_[vehicle]) { + total_slacks_[vehicle]->SetMax(CapSub(max, sum_fixed_transits)); + } + } + // Propagates span == sum_fixed_transits + total_slack. + // This should be called at least once during PropagateVehicle(). + void SynchronizeSpanAndTotalSlack(int vehicle, int64 sum_fixed_transits) { + DCHECK_GE(sum_fixed_transits, 0); + IntVar* span = spans_[vehicle]; + IntVar* total_slack = total_slacks_[vehicle]; + if (!span || !total_slack) return; + span->SetMin(CapAdd(total_slack->Min(), sum_fixed_transits)); + span->SetMax(CapAdd(total_slack->Max(), sum_fixed_transits)); + total_slack->SetMin(CapSub(span->Min(), sum_fixed_transits)); + total_slack->SetMax(CapSub(span->Max(), sum_fixed_transits)); + } + void PropagateVehicle(int vehicle) { - if (total_slacks_[vehicle] == nullptr) return; + DCHECK(spans_[vehicle] || total_slacks_[vehicle]); const int start = model_->Start(vehicle); const int end = model_->End(vehicle); - - // The amount of break time that must occur during the route must be smaller - // than total_slack_max. A break must occur on the route if it must be - // after the route's start and before the route's end. - // Propagate lower bound on total_slack, then filter out values - // that would force more breaks in route than possible. - if (dimension_->VehicleHasBreakIntervals(vehicle)) { - const int64 vehicle_start_max = dimension_->CumulVar(start)->Max(); - const int64 vehicle_end_min = dimension_->CumulVar(end)->Min(); - // Compute and propagate lower bound. - int64 min_break_duration = 0; - for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - if (!br->MustBePerformed()) continue; - if (vehicle_start_max < br->EndMin() && - br->StartMax() < vehicle_end_min) { - min_break_duration = CapAdd(min_break_duration, br->DurationMin()); - } - } - total_slacks_[vehicle]->SetMin(min_break_duration); - // If a break that is not inside the route may violate total_slack_max, - // we can propagate in some cases: when the break must be before or - // must be after the route. - // In the other cases, we cannot deduce a better bound on a CumulVar or - // on a break, so we do nothing. - const int64 total_slack_max = total_slacks_[vehicle]->Max(); - for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - if (!br->MustBePerformed()) continue; - // Break must be before end, detect whether it must be before start. - if (vehicle_start_max >= br->EndMin() && - br->StartMax() < vehicle_end_min) { - if (CapAdd(min_break_duration, br->DurationMin()) > total_slack_max) { - // Having the break inside would violate total_slack_max. - // Thus, it must be outside the route, in this case, before. - br->SetEndMax(vehicle_start_max); - dimension_->CumulVar(start)->SetMin(br->EndMin()); - } - } - // Break must be after start, detect whether it must be after end. - // Same reasoning, in the case where the break is after. - if (vehicle_start_max < br->EndMin() && - br->StartMax() >= vehicle_end_min) { - if (CapAdd(min_break_duration, br->DurationMin()) > total_slack_max) { - br->SetStartMin(vehicle_end_min); - dimension_->CumulVar(end)->SetMax(br->StartMax()); - } - } - } - } - // Record path, if it is not fixed from start to end, stop here. // TRICKY: do not put end node yet, we look only at transits in the next // reasonings, we will append the end when we look at cumuls. @@ -5033,126 +5133,182 @@ class PathSlacks : public Constraint { sum_fixed_transits = CapAdd(sum_fixed_transits, fixed_transit_var->Value()); } - if (dimension_->GetSpanUpperBoundForVehicle(vehicle) < kint64max) { - const int64 total_slack_ub = CapSub( - dimension_->GetSpanUpperBoundForVehicle(vehicle), sum_fixed_transits); - total_slacks_[vehicle]->SetMax(total_slack_ub); - } - // Propagate total_slack = cumul(end) - cumul(start) - sum_fixed_transits. - { - IntVar* start_cumul_var = dimension_->CumulVar(start); - IntVar* end_cumul_var = dimension_->CumulVar(end); - // Propagate from cumuls to total_slack. - const int64 total_slack_lb = - CapSub(CapSub(end_cumul_var->Min(), start_cumul_var->Max()), - sum_fixed_transits); - total_slacks_[vehicle]->SetMin(total_slack_lb); - const int64 total_slack_ub = - CapSub(CapSub(end_cumul_var->Max(), start_cumul_var->Min()), - sum_fixed_transits); - total_slacks_[vehicle]->SetMax(total_slack_ub); - // Propagate from total_slack to cumuls. - const int64 maximum_deviation = - CapSub(total_slacks_[vehicle]->Max(), total_slack_lb); - start_cumul_var->SetMin( - CapSub(start_cumul_var->Max(), maximum_deviation)); - end_cumul_var->SetMax(CapAdd(end_cumul_var->Min(), maximum_deviation)); - } + SynchronizeSpanAndTotalSlack(vehicle, sum_fixed_transits); - // Propagate total_slack == sum transits. - { - // Propagate from transits to total_slack. - int64 total_slack_lb = 0; - int64 total_slack_ub = 0; - { - for (const int curr_node : path_) { - total_slack_lb = - CapAdd(total_slack_lb, dimension_->TransitVar(curr_node)->Min()); - total_slack_ub = - CapAdd(total_slack_ub, dimension_->TransitVar(curr_node)->Max()); + // The amount of break time that must occur during the route must be smaller + // than span max - sum_fixed_transits. A break must occur on the route if it + // must be after the route's start and before the route's end. + // Propagate lower bound on span, then filter out values + // that would force more breaks in route than possible. + if (dimension_->HasBreakConstraints() && + !dimension_->GetBreakIntervalsOfVehicle(vehicle).empty()) { + const int64 vehicle_start_max = dimension_->CumulVar(start)->Max(); + const int64 vehicle_end_min = dimension_->CumulVar(end)->Min(); + // Compute and propagate lower bound. + int64 min_break_duration = 0; + for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { + if (!br->MustBePerformed()) continue; + if (vehicle_start_max < br->EndMin() && + br->StartMax() < vehicle_end_min) { + min_break_duration = CapAdd(min_break_duration, br->DurationMin()); } - total_slack_lb = CapSub(total_slack_lb, sum_fixed_transits); - total_slack_ub = CapSub(total_slack_ub, sum_fixed_transits); } - total_slacks_[vehicle]->SetMin(total_slack_lb); - total_slacks_[vehicle]->SetMax(total_slack_ub); - // Propagate from total_slack to transits. - const int64 maximum_deviation = - CapSub(total_slacks_[vehicle]->Max(), total_slack_lb); - for (const int curr_node : path_) { - IntVar* transit_var = dimension_->TransitVar(curr_node); - transit_var->SetMax(CapAdd(transit_var->Min(), maximum_deviation)); + SetSpanMin(vehicle, CapAdd(min_break_duration, sum_fixed_transits), + sum_fixed_transits); + // If a break that is not inside the route may violate slack_max, + // we can propagate in some cases: when the break must be before or + // must be after the route. + // In the other cases, we cannot deduce a better bound on a CumulVar or + // on a break, so we do nothing. + const int64 slack_max = + CapSub(SpanMax(vehicle, sum_fixed_transits), sum_fixed_transits); + const int64 max_additional_slack = CapSub(slack_max, min_break_duration); + for (IntervalVar* br : dimension_->GetBreakIntervalsOfVehicle(vehicle)) { + if (!br->MustBePerformed()) continue; + // Break must be before end, detect whether it must be before start. + if (vehicle_start_max >= br->EndMin() && + br->StartMax() < vehicle_end_min) { + if (br->DurationMin() > max_additional_slack) { + // Having the break inside would violate max_additional_slack.. + // Thus, it must be outside the route, in this case, before. + br->SetEndMax(vehicle_start_max); + dimension_->CumulVar(start)->SetMin(br->EndMin()); + } + } + // Break must be after start, detect whether it must be after end. + // Same reasoning, in the case where the break is after. + if (vehicle_start_max < br->EndMin() && + br->StartMax() >= vehicle_end_min) { + if (br->DurationMin() > max_additional_slack) { + br->SetStartMin(vehicle_end_min); + dimension_->CumulVar(end)->SetMax(br->StartMax()); + } + } + } + } + + // Propagate span == cumul(end) - cumul(start). + { + IntVar* start_cumul = dimension_->CumulVar(start); + IntVar* end_cumul = dimension_->CumulVar(end); + const int64 start_min = start_cumul->Min(); + const int64 start_max = start_cumul->Max(); + const int64 end_min = end_cumul->Min(); + const int64 end_max = end_cumul->Max(); + // Propagate from cumuls to span. + const int64 span_lb = CapSub(end_min, start_max); + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + const int64 span_ub = CapSub(end_max, start_min); + SetSpanMax(vehicle, span_ub, sum_fixed_transits); + // Propagate from span to cumuls. + const int64 span_min = SpanMin(vehicle, sum_fixed_transits); + const int64 span_max = SpanMax(vehicle, sum_fixed_transits); + const int64 slack_from_lb = CapSub(span_max, span_lb); + const int64 slack_from_ub = CapSub(span_ub, span_min); + // start >= start_max - (span_max - span_lb). + start_cumul->SetMin(CapSub(start_max, slack_from_lb)); + // end <= end_min + (span_max - span_lb). + end_cumul->SetMax(CapAdd(end_min, slack_from_lb)); + // // start <= start_min + (span_ub - span_min) + start_cumul->SetMax(CapAdd(start_min, slack_from_ub)); + // // end >= end_max - (span_ub - span_min) + end_cumul->SetMin(CapSub(end_max, slack_from_ub)); + } + + // Propagate sum transits == span. + { + // Propagate from transits to span. + int64 span_lb = 0; + int64 span_ub = 0; + for (const int node : path_) { + span_lb = CapAdd(span_lb, dimension_->TransitVar(node)->Min()); + span_ub = CapAdd(span_ub, dimension_->TransitVar(node)->Max()); + } + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + SetSpanMax(vehicle, span_ub, sum_fixed_transits); + // Propagate from span to transits. + // transit[i] <= transit_i_min + (span_max - span_lb) + // transit[i] >= transit_i_max - (span_ub - span_min) + const int64 span_min = SpanMin(vehicle, sum_fixed_transits); + const int64 span_max = SpanMax(vehicle, sum_fixed_transits); + const int64 slack_from_lb = CapSub(span_max, span_lb); + const int64 slack_from_ub = + span_ub < kint64max ? CapSub(span_ub, span_min) : kint64max; + for (const int node : path_) { + IntVar* transit_var = dimension_->TransitVar(node); + const int64 transit_i_min = transit_var->Min(); + const int64 transit_i_max = transit_var->Max(); + // TRICKY: the first propagation might change transit_var->Max(), + // but we must use the same value of transit_i_max in the computation + // of transit[i]'s lower bound that was used for span_ub. + transit_var->SetMax(CapAdd(transit_i_min, slack_from_lb)); + transit_var->SetMin(CapSub(transit_i_max, slack_from_ub)); } } // TRICKY: add end node now, we will look at cumuls. path_.push_back(end); - // A stronger bound: from start min of the route, - // go to next node with time max(prev_cumul + fixed_transit, - // curr_cumul.Min()) Record arrival time (should be the same as end cumul - // min). Then do the reverse route, going to time min(cumul - fixed_transit, - // prev_cumul.Max()) Record final time as departure time. arrival time - - // departure time is a valid lower bound of total_slack. + // A stronger bound: from start min of the route, go to node i+1 with time + // max(cumul[i] + fixed_transit, cumul[i+1].Min()). + // Record arrival time (should be the same as end cumul min). + // Then do the reverse route, going to time + // min(cumul[i+1] - fixed_transit, cumul[i].Max()) + // Record final time as departure time. + // Then arrival time - departure time is a valid lower bound of span. + // First reasoning: start - end - start { - // start - end - start - { - int64 arrival_time = dimension_->CumulVar(start)->Min(); - for (int i = 1; i < path_.size(); ++i) { - arrival_time = - std::max(CapAdd(arrival_time, - dimension_->FixedTransitVar(path_[i - 1])->Min()), - dimension_->CumulVar(path_[i])->Min()); - } - int64 departure_time = arrival_time; - for (int i = path_.size() - 2; i >= 0; --i) { - departure_time = - std::min(CapSub(departure_time, - dimension_->FixedTransitVar(path_[i])->Min()), - dimension_->CumulVar(path_[i])->Max()); - } - const int64 total_slack_lb = - CapSub(CapSub(arrival_time, departure_time), sum_fixed_transits); - total_slacks_[vehicle]->SetMin(total_slack_lb); - const int64 maximum_deviation = - CapSub(total_slacks_[vehicle]->Max(), total_slack_lb); - const int64 start_lb = CapSub(departure_time, maximum_deviation); - dimension_->CumulVar(start)->SetMin(start_lb); + int64 arrival_time = dimension_->CumulVar(start)->Min(); + for (int i = 1; i < path_.size(); ++i) { + arrival_time = + std::max(CapAdd(arrival_time, + dimension_->FixedTransitVar(path_[i - 1])->Min()), + dimension_->CumulVar(path_[i])->Min()); } - - // end - start - end - { - int64 departure_time = dimension_->CumulVar(end)->Max(); - for (int i = path_.size() - 2; i >= 0; --i) { - const int curr_node = path_[i]; - departure_time = - std::min(CapSub(departure_time, - dimension_->FixedTransitVar(curr_node)->Min()), - dimension_->CumulVar(curr_node)->Max()); - } - int arrival_time = departure_time; - for (int i = 1; i < path_.size(); ++i) { - arrival_time = - std::max(CapAdd(arrival_time, - dimension_->FixedTransitVar(path_[i - 1])->Min()), - dimension_->CumulVar(path_[i])->Min()); - } - - // total_slack_lb will be the same value as in the start - end - start - // scenario, we don't need to propagate it. - const int64 total_slack_lb = - CapSub(CapSub(arrival_time, departure_time), sum_fixed_transits); - const int64 maximum_deviation = - CapSub(total_slacks_[vehicle]->Max(), total_slack_lb); - dimension_->CumulVar(end)->SetMax( - CapAdd(arrival_time, maximum_deviation)); + int64 departure_time = arrival_time; + for (int i = path_.size() - 2; i >= 0; --i) { + departure_time = + std::min(CapSub(departure_time, + dimension_->FixedTransitVar(path_[i])->Min()), + dimension_->CumulVar(path_[i])->Max()); } + const int64 span_lb = CapSub(arrival_time, departure_time); + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + const int64 maximum_deviation = + CapSub(SpanMax(vehicle, sum_fixed_transits), span_lb); + const int64 start_lb = CapSub(departure_time, maximum_deviation); + dimension_->CumulVar(start)->SetMin(start_lb); + } + // Second reasoning: end - start - end + { + int64 departure_time = dimension_->CumulVar(end)->Max(); + for (int i = path_.size() - 2; i >= 0; --i) { + const int curr_node = path_[i]; + departure_time = + std::min(CapSub(departure_time, + dimension_->FixedTransitVar(curr_node)->Min()), + dimension_->CumulVar(curr_node)->Max()); + } + int arrival_time = departure_time; + for (int i = 1; i < path_.size(); ++i) { + arrival_time = + std::max(CapAdd(arrival_time, + dimension_->FixedTransitVar(path_[i - 1])->Min()), + dimension_->CumulVar(path_[i])->Min()); + } + const int64 span_lb = CapSub(arrival_time, departure_time); + SetSpanMin(vehicle, span_lb, sum_fixed_transits); + const int64 maximum_deviation = + CapSub(SpanMax(vehicle, sum_fixed_transits), span_lb); + dimension_->CumulVar(end)->SetMax( + CapAdd(arrival_time, maximum_deviation)); } } const RoutingModel* const model_; const RoutingDimension* const dimension_; + std::vector spans_; std::vector total_slacks_; std::vector path_; std::vector vehicle_demons_; @@ -5160,10 +5316,13 @@ class PathSlacks : public Constraint { } // namespace -Constraint* RoutingModel::MakePathSlacks(const RoutingDimension* dimension, - std::vector total_slacks) { +Constraint* RoutingModel::MakePathSpansAndTotalSlacks( + const RoutingDimension* dimension, std::vector spans, + std::vector total_slacks) { + CHECK_EQ(vehicles_, spans.size()); CHECK_EQ(vehicles_, total_slacks.size()); - return solver()->RevAlloc(new PathSlacks(this, dimension, total_slacks)); + return solver()->RevAlloc( + new PathSpansAndTotalSlacks(this, dimension, spans, total_slacks)); } const char RoutingModelVisitor::kLightElement[] = "LightElement"; @@ -5442,500 +5601,83 @@ void RoutingDimension::InitializeTransits( InitializeTransitVariables(slack_max); } +void AppendTasksFromPath(const std::vector& path, + const std::vector& min_travels, + const std::vector& max_travels, + const std::vector& pre_travels, + const std::vector& post_travels, + const RoutingDimension& dimension, + DisjunctivePropagator::Tasks* tasks) { + const int num_nodes = path.size(); + DCHECK_EQ(pre_travels.size(), num_nodes - 1); + DCHECK_EQ(post_travels.size(), num_nodes - 1); + for (int i = 0; i < num_nodes; ++i) { + const int64 cumul_min = dimension.CumulVar(path[i])->Min(); + const int64 cumul_max = dimension.CumulVar(path[i])->Max(); + // Add task associated to visit i. + // Visits start at Cumul(path[i]) - before_visit + // and end at Cumul(path[i]) + after_visit + { + const int64 before_visit = (i == 0) ? 0 : post_travels[i - 1]; + const int64 after_visit = (i == num_nodes - 1) ? 0 : pre_travels[i]; -GlobalVehicleBreaksConstraint::GlobalVehicleBreaksConstraint( - const RoutingDimension* dimension) - : Constraint(dimension->model()->solver()), - model_(dimension->model()), - dimension_(dimension) { - vehicle_demons_.resize(model_->vehicles()); -} - -void GlobalVehicleBreaksConstraint::Post() { - for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { - if (!dimension_->VehicleHasBreakIntervals(vehicle)) continue; - vehicle_demons_[vehicle] = MakeDelayedConstraintDemon1( - solver(), this, &GlobalVehicleBreaksConstraint::PropagateVehicle, - "PropagateVehicle", vehicle); - for (IntervalVar* interval : - dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - interval->WhenAnything(vehicle_demons_[vehicle]); + tasks->start_min.push_back(CapSub(cumul_min, before_visit)); + tasks->start_max.push_back(CapSub(cumul_max, before_visit)); + tasks->duration_min.push_back(CapAdd(before_visit, after_visit)); + tasks->duration_max.push_back(CapAdd(before_visit, after_visit)); + tasks->end_min.push_back(CapAdd(cumul_min, after_visit)); + tasks->end_max.push_back(CapAdd(cumul_max, after_visit)); + tasks->is_preemptible.push_back(false); } - } - const int num_cumuls = dimension_->cumuls().size(); - const int num_nexts = model_->Nexts().size(); - for (int node = 0; node < num_cumuls; node++) { - Demon* dimension_demon = MakeConstraintDemon1( - solver(), this, &GlobalVehicleBreaksConstraint::PropagateNode, - "PropagateNode", node); - if (node < num_nexts) { - model_->NextVar(node)->WhenBound(dimension_demon); - dimension_->SlackVar(node)->WhenRange(dimension_demon); - } - model_->VehicleVar(node)->WhenBound(dimension_demon); - dimension_->CumulVar(node)->WhenRange(dimension_demon); - } -} + if (i == num_nodes - 1) break; -void GlobalVehicleBreaksConstraint::InitialPropagate() { - for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { - if (dimension_->VehicleHasBreakIntervals(vehicle)) { - PropagateVehicle(vehicle); + // Tasks from travels. + // A travel task starts at Cumul(path[i]) + pre_travel, + // last for FixedTransitVar(path[i]) - pre_travel - post_travel, + // and must end at the latest at Cumul(path[i+1]) - post_travel. + { + const int64 pre_travel = pre_travels[i]; + const int64 post_travel = post_travels[i]; + tasks->start_min.push_back(CapAdd(cumul_min, pre_travel)); + tasks->start_max.push_back(CapAdd(cumul_max, pre_travel)); + tasks->duration_min.push_back(std::max( + 0, CapSub(min_travels[i], CapAdd(pre_travel, post_travel)))); + tasks->duration_max.push_back( + max_travels[i] == kint64max + ? kint64max + : std::max(0, CapSub(max_travels[i], + CapAdd(pre_travel, post_travel)))); + tasks->end_min.push_back( + CapSub(dimension.CumulVar(path[i + 1])->Min(), post_travel)); + tasks->end_max.push_back( + CapSub(dimension.CumulVar(path[i + 1])->Max(), post_travel)); + tasks->is_preemptible.push_back(true); } } } -// This dispatches node events to the right vehicle propagator. -// It also filters out a part of uninteresting events, on which the vehicle -// propagator will not find anything new. -void GlobalVehicleBreaksConstraint::PropagateNode(int node) { - if (!model_->VehicleVar(node)->Bound()) return; - const int vehicle = model_->VehicleVar(node)->Min(); - if (vehicle < 0 || vehicle_demons_[vehicle] == nullptr) return; - EnqueueDelayedDemon(vehicle_demons_[vehicle]); -} - -// Naive filtering algorithm: for every arc on the path from start of vehicle, -// for every break B, either do pairwise disjunctive reasoning between -// [CumulVar(node), CumulVar(node) + node_transits(node)) and B -// or [CumulVar(node), CumulVar(Next(node))) and B, -// depending on whether B can fit inside the arc's slack or not. -// The pairwise disjunctive reasoning rule for two performed intervals is: -// StartMax(A) < EndMin(B) => End(A) < StartMax(B) and -// StartMax(A) < EndMin(B) => EndMin(A) < Start(B). -void GlobalVehicleBreaksConstraint::PropagateVehicle(int vehicle) { - const std::vector& break_intervals = - dimension_->GetBreakIntervalsOfVehicle(vehicle); - const std::vector& node_visit_transit = - dimension_->GetNodeVisitTransitsOfVehicle(vehicle); - // External loop: browse all arcs from start of vehicle. - int64 current = model_->Start(vehicle); - while (!model_->IsEnd(current)) { - if (!model_->NextVar(current)->Bound()) break; - const int64 next = model_->NextVar(current)->Min(); - const int64 current_start_max = dimension_->CumulVar(current)->Max(); - const int64 current_end_min = - dimension_->CumulVar(current)->Min() + node_visit_transit[current]; - const int64 next_start_min = dimension_->CumulVar(next)->Min(); - const int64 current_slack_max = dimension_->SlackVar(current)->Max(); - - int64 total_break_inside_arc = 0; - for (IntervalVar* interval : break_intervals) { - if (!interval->MayBePerformed()) continue; - - const bool interval_is_performed = interval->MustBePerformed(); - const int64 interval_start_max = interval->StartMax(); - const int64 interval_end_min = interval->EndMin(); - const int64 interval_duration_min = interval->DurationMin(); - - // When interval cannot fit inside current arc, - // do disjunctive reasoning on full arc. - if (interval_duration_min > current_slack_max) { - if (current_start_max < interval_end_min) { - interval->SetStartMin(next_start_min); - if (interval_is_performed) { - dimension_->CumulVar(next)->SetMax(interval_start_max); - } - } - if (interval_start_max < next_start_min) { - interval->SetEndMax(current_start_max); - if (interval_is_performed) { - dimension_->CumulVar(current)->SetMin(interval_end_min); - } - } - continue; - } - - // Interval could fit inside arc: do disjunctive reasoning between - // interval and service task only. - if (current_start_max < interval_end_min) { - interval->SetStartMin(current_end_min); - if (interval_is_performed) { - dimension_->CumulVar(current)->SetMax(interval_start_max); - } - } - if (interval_start_max < current_end_min) { - interval->SetEndMax(current_start_max); - if (interval_is_performed) { - dimension_->CumulVar(current)->SetMin(interval_end_min); - } - } - - const int64 actual_transit = dimension_->FixedTransitVar(current)->Min(); - if (interval_start_max < next_start_min) { // interval not after. - if (interval_is_performed) { - const int64 bound_interval_before = interval_end_min + actual_transit; - const int64 bound_interval_inside = - dimension_->CumulVar(current)->Min() + actual_transit + - interval_duration_min; - dimension_->CumulVar(next)->SetMin( - std::min(bound_interval_before, bound_interval_inside)); - } - } - - if (interval_end_min > current_start_max) { // interval not before - if (interval_is_performed) { - const int64 bound_interval_after = - interval_start_max - actual_transit; - const int64 bound_interval_inside = - dimension_->CumulVar(next)->Max() - actual_transit - - interval_duration_min; - dimension_->CumulVar(current)->SetMax( - std::max(bound_interval_after, bound_interval_inside)); - } - } - - // If interval must be inside the arc, the slack must contain it. - if (current_start_max < interval_end_min && - interval_start_max < next_start_min) { - if (interval_is_performed) { - total_break_inside_arc += interval_duration_min; - } - } - } - dimension_->SlackVar(current)->SetMin(total_break_inside_arc); - current = next; - } - - // Energy-based reasoning. - // Fill internal vectors with route and breaks information. - tasks_.start_min.clear(); - tasks_.start_max.clear(); - tasks_.duration_min.clear(); - tasks_.duration_max.clear(); - tasks_.end_min.clear(); - tasks_.end_max.clear(); - tasks_.is_preemptible.clear(); - tasks_.forbidden_intervals.clear(); - task_translators_.clear(); - // Translate route to tasks: visits are nonpreemptible, transits are. - int64 group_delay = 0; - current = model_->Start(vehicle); - while (true) { - const int64 cumul_min = dimension_->CumulVar(current)->Min(); - const int64 cumul_max = dimension_->CumulVar(current)->Max(); - // Tasks from visits. Visits start before the group_delay at Cumul(current), - // end at Cumul() + visit_duration. - const bool node_is_last = model_->IsEnd(current); - const int64 visit_duration = - node_is_last ? 0LL : node_visit_transit[current]; - tasks_.start_min.push_back(CapSub(cumul_min, group_delay)); - tasks_.start_max.push_back(CapSub(cumul_max, group_delay)); - tasks_.duration_min.push_back(CapAdd(group_delay, visit_duration)); - tasks_.duration_max.push_back(CapAdd(group_delay, visit_duration)); - tasks_.end_min.push_back(CapAdd(cumul_min, visit_duration)); - tasks_.end_max.push_back(CapAdd(cumul_max, visit_duration)); - tasks_.is_preemptible.push_back(false); - task_translators_.emplace_back(dimension_->CumulVar(current), - visit_duration, group_delay); - if (node_is_last) break; - - // Tasks from transits. Transit starts after the visit, - // but before the next group_delay. - const bool next_is_bound = model_->NextVar(current)->Bound(); - const int next = - next_is_bound ? model_->NextVar(current)->Min() : model_->End(vehicle); - group_delay = - next_is_bound ? dimension_->GetGroupDelay(vehicle, current, next) : 0; - tasks_.start_min.push_back(CapAdd(cumul_min, visit_duration)); - tasks_.start_max.push_back(CapAdd(cumul_max, visit_duration)); - tasks_.duration_min.push_back(std::max( - 0, CapSub(CapSub(dimension_->FixedTransitVar(current)->Min(), - visit_duration), - group_delay))); - tasks_.duration_max.push_back(std::max( - 0, CapSub(CapSub(dimension_->FixedTransitVar(current)->Max(), - visit_duration), - group_delay))); - tasks_.end_min.push_back( - CapSub(dimension_->CumulVar(next)->Min(), group_delay)); - tasks_.end_max.push_back( - CapSub(dimension_->CumulVar(next)->Max(), group_delay)); - tasks_.is_preemptible.push_back(true); - task_translators_.emplace_back(); // Dummy translator, prunes nothing. - current = next; - } - tasks_.num_chain_tasks = tasks_.start_min.size(); - // Tasks from breaks. - for (IntervalVar* interval : break_intervals) { +void AppendTasksFromIntervals(const std::vector& intervals, + DisjunctivePropagator::Tasks* tasks) { + for (IntervalVar* interval : intervals) { if (!interval->MustBePerformed()) continue; - tasks_.start_min.push_back(interval->StartMin()); - tasks_.start_max.push_back(interval->StartMax()); - tasks_.duration_min.push_back(interval->DurationMin()); - tasks_.duration_max.push_back(interval->DurationMax()); - tasks_.end_min.push_back(interval->EndMin()); - tasks_.end_max.push_back(interval->EndMax()); - tasks_.is_preemptible.push_back(false); - task_translators_.emplace_back(interval); - } - tasks_.distance_duration.clear(); - if (dimension_->HasBreakDistanceDurationOfVehicle(vehicle)) { - tasks_.distance_duration = - dimension_->GetBreakDistanceDurationOfVehicle(vehicle); - } - if (!disjunctive_propagator_.Propagate(&tasks_)) solver()->Fail(); - - // Push new bounds to variables. - const int num_tasks = tasks_.start_min.size(); - for (int task = 0; task < num_tasks; ++task) { - task_translators_[task].SetStartMin(tasks_.start_min[task]); - task_translators_[task].SetStartMax(tasks_.start_max[task]); - task_translators_[task].SetDurationMin(tasks_.duration_min[task]); - task_translators_[task].SetEndMin(tasks_.end_min[task]); - task_translators_[task].SetEndMax(tasks_.end_max[task]); + tasks->start_min.push_back(interval->StartMin()); + tasks->start_max.push_back(interval->StartMax()); + tasks->duration_min.push_back(interval->DurationMin()); + tasks->duration_max.push_back(interval->DurationMax()); + tasks->end_min.push_back(interval->EndMin()); + tasks->end_max.push_back(interval->EndMax()); + tasks->is_preemptible.push_back(false); } } -bool DisjunctivePropagator::Propagate(Tasks* tasks) { - DCHECK_LE(tasks->num_chain_tasks, tasks->start_min.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->start_max.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->duration_min.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->duration_max.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->end_min.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->end_max.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->is_preemptible.size()); - // Do forward deductions, then backward deductions. - // Interleave Precedences() that is O(n). - return Precedences(tasks) && EdgeFinding(tasks) && Precedences(tasks) && - DetectablePrecedencesWithChain(tasks) && ForbiddenIntervals(tasks) && - Precedences(tasks) && DistanceDuration(tasks) && Precedences(tasks) && - MirrorTasks(tasks) && EdgeFinding(tasks) && Precedences(tasks) && - DetectablePrecedencesWithChain(tasks) && MirrorTasks(tasks); -} - -bool DisjunctivePropagator::Precedences(Tasks* tasks) { - const int num_chain_tasks = tasks->num_chain_tasks; - if (num_chain_tasks == 0) return true; - // Propagate forwards. - int64 time = tasks->start_min[0]; - for (int task = 0; task < num_chain_tasks; ++task) { - time = std::max(tasks->start_min[task], time); - tasks->start_min[task] = time; - time = CapAdd(time, tasks->duration_min[task]); - if (tasks->end_max[task] < time) return false; +void FillPathEvaluation(const std::vector& path, + const RoutingModel::TransitCallback2& evaluator, + std::vector* values) { + const int num_nodes = path.size(); + values->resize(num_nodes - 1); + for (int i = 0; i < num_nodes - 1; ++i) { + (*values)[i] = evaluator(path[i], path[i + 1]); } - // Propagate backwards. - time = tasks->end_max[num_chain_tasks - 1]; - for (int task = num_chain_tasks - 1; task >= 0; --task) { - time = std::min(tasks->end_max[task], time); - tasks->end_max[task] = time; - time = CapSub(time, tasks->duration_min[task]); - if (time < tasks->start_min[task]) return false; - } - const int num_tasks = tasks->start_min.size(); - for (int task = 0; task < num_tasks; ++task) { - // Enforce start + duration <= end. - tasks->end_min[task] = - std::max(tasks->end_min[task], - CapAdd(tasks->start_min[task], tasks->duration_min[task])); - tasks->start_max[task] = - std::min(tasks->start_max[task], - CapSub(tasks->end_max[task], tasks->duration_min[task])); - tasks->duration_max[task] = - std::min(tasks->duration_max[task], - CapSub(tasks->end_max[task], tasks->start_min[task])); - if (!tasks->is_preemptible[task]) { - // Enforce start + duration == end for nonpreemptibles. - tasks->end_max[task] = - std::min(tasks->end_max[task], - CapAdd(tasks->start_max[task], tasks->duration_max[task])); - tasks->start_min[task] = - std::max(tasks->start_min[task], - CapSub(tasks->end_min[task], tasks->duration_max[task])); - tasks->duration_min[task] = - std::max(tasks->duration_min[task], - CapSub(tasks->end_min[task], tasks->start_max[task])); - } - if (tasks->duration_min[task] > tasks->duration_max[task]) return false; - if (tasks->end_min[task] > tasks->end_max[task]) return false; - if (tasks->start_min[task] > tasks->start_max[task]) return false; - } - return true; -} - -bool DisjunctivePropagator::MirrorTasks(Tasks* tasks) { - const int num_tasks = tasks->start_min.size(); - // For all tasks, start_min := -end_max and end_max := -start_min. - for (int task = 0; task < num_tasks; ++task) { - const int64 t = -tasks->start_min[task]; - tasks->start_min[task] = -tasks->end_max[task]; - tasks->end_max[task] = t; - } - // For all tasks, start_max := -end_min and end_min := -start_max. - for (int task = 0; task < num_tasks; ++task) { - const int64 t = -tasks->start_max[task]; - tasks->start_max[task] = -tasks->end_min[task]; - tasks->end_min[task] = t; - } - // In the mirror problem, tasks linked by precedences are in reversed order. - const int num_chain_tasks = tasks->num_chain_tasks; - if (num_chain_tasks > 0) { - std::reverse(tasks->start_min.begin(), - tasks->start_min.begin() + num_chain_tasks); - std::reverse(tasks->start_max.begin(), - tasks->start_max.begin() + num_chain_tasks); - std::reverse(tasks->duration_min.begin(), - tasks->duration_min.begin() + num_chain_tasks); - std::reverse(tasks->duration_max.begin(), - tasks->duration_max.begin() + num_chain_tasks); - std::reverse(tasks->end_min.begin(), - tasks->end_min.begin() + num_chain_tasks); - std::reverse(tasks->end_max.begin(), - tasks->end_max.begin() + num_chain_tasks); - std::reverse(tasks->is_preemptible.begin(), - tasks->is_preemptible.begin() + num_chain_tasks); - } - return true; -} - -bool DisjunctivePropagator::EdgeFinding(Tasks* tasks) { - const int num_tasks = tasks->start_min.size(); - // Prepare start_min events for tree. - tasks_by_start_min_.resize(num_tasks); - std::iota(tasks_by_start_min_.begin(), tasks_by_start_min_.end(), 0); - std::sort( - tasks_by_start_min_.begin(), tasks_by_start_min_.end(), - [&](int i, int j) { return tasks->start_min[i] < tasks->start_min[j]; }); - event_of_task_.resize(num_tasks); - for (int event = 0; event < num_tasks; ++event) { - event_of_task_[tasks_by_start_min_[event]] = event; - } - // Tasks will be browsed according to end_max order. - tasks_by_end_max_.resize(num_tasks); - std::iota(tasks_by_end_max_.begin(), tasks_by_end_max_.end(), 0); - std::sort( - tasks_by_end_max_.begin(), tasks_by_end_max_.end(), - [&](int i, int j) { return tasks->end_max[i] < tasks->end_max[j]; }); - - // Generic overload checking: insert tasks by end_max, - // fail if envelope > end_max. - theta_lambda_tree_.Reset(num_tasks); - for (const int task : tasks_by_end_max_) { - theta_lambda_tree_.AddOrUpdateEvent( - event_of_task_[task], tasks->start_min[task], tasks->duration_min[task], - tasks->duration_min[task]); - if (theta_lambda_tree_.GetEnvelope() > tasks->end_max[task]) { - return false; - } - } - - // Generic edge finding: from full set of tasks, at each end_max event in - // decreasing order, check lambda feasibility, then move end_max task from - // theta to lambda. - for (int i = num_tasks - 1; i >= 0; --i) { - const int task = tasks_by_end_max_[i]; - const int64 envelope = theta_lambda_tree_.GetEnvelope(); - // If a nonpreemptible optional would overload end_max, push to envelope. - while (theta_lambda_tree_.GetOptionalEnvelope() > tasks->end_max[task]) { - int critical_event; // Dummy value. - int optional_event; - int64 available_energy; // Dummy value. - theta_lambda_tree_.GetEventsWithOptionalEnvelopeGreaterThan( - tasks->end_max[task], &critical_event, &optional_event, - &available_energy); - const int optional_task = tasks_by_start_min_[optional_event]; - tasks->start_min[optional_task] = - std::max(tasks->start_min[optional_task], envelope); - theta_lambda_tree_.RemoveEvent(optional_event); - } - if (!tasks->is_preemptible[task]) { - theta_lambda_tree_.AddOrUpdateOptionalEvent(event_of_task_[task], - tasks->start_min[task], - tasks->duration_min[task]); - } else { - theta_lambda_tree_.RemoveEvent(event_of_task_[task]); - } - } - return true; -} - -bool DisjunctivePropagator::DetectablePrecedencesWithChain(Tasks* tasks) { - const int num_tasks = tasks->start_min.size(); - // Prepare start_min events for tree. - tasks_by_start_min_.resize(num_tasks); - std::iota(tasks_by_start_min_.begin(), tasks_by_start_min_.end(), 0); - std::sort( - tasks_by_start_min_.begin(), tasks_by_start_min_.end(), - [&](int i, int j) { return tasks->start_min[i] < tasks->start_min[j]; }); - event_of_task_.resize(num_tasks); - for (int event = 0; event < num_tasks; ++event) { - event_of_task_[tasks_by_start_min_[event]] = event; - } - theta_lambda_tree_.Reset(num_tasks); - - // Sort nonchain tasks by start max = end_max - duration_min. - const int num_chain_tasks = tasks->num_chain_tasks; - nonchain_tasks_by_start_max_.resize(num_tasks - num_chain_tasks); - std::iota(nonchain_tasks_by_start_max_.begin(), - nonchain_tasks_by_start_max_.end(), num_chain_tasks); - std::sort(nonchain_tasks_by_start_max_.begin(), - nonchain_tasks_by_start_max_.end(), [&tasks](int i, int j) { - return tasks->end_max[i] - tasks->duration_min[i] < - tasks->end_max[j] - tasks->duration_min[j]; - }); - - // Detectable precedences, specialized for routes: for every task on route, - // put all tasks before it in the tree, then push with envelope. - int index_nonchain = 0; - for (int i = 0; i < num_chain_tasks; ++i) { - if (!tasks->is_preemptible[i]) { - // Add all nonchain tasks detected before i. - while (index_nonchain < nonchain_tasks_by_start_max_.size()) { - const int task = nonchain_tasks_by_start_max_[index_nonchain]; - if (tasks->end_max[task] - tasks->duration_min[task] >= - tasks->start_min[i] + tasks->duration_min[i]) - break; - theta_lambda_tree_.AddOrUpdateEvent( - event_of_task_[task], tasks->start_min[task], - tasks->duration_min[task], tasks->duration_min[task]); - index_nonchain++; - } - } - // All chain and nonchain tasks before i are now in the tree, push i. - const int64 new_start_min = theta_lambda_tree_.GetEnvelope(); - // Add i to the tree before updating it. - theta_lambda_tree_.AddOrUpdateEvent(event_of_task_[i], tasks->start_min[i], - tasks->duration_min[i], - tasks->duration_min[i]); - tasks->start_min[i] = std::max(tasks->start_min[i], new_start_min); - } - return true; -} - -bool DisjunctivePropagator::ForbiddenIntervals(Tasks* tasks) { - if (tasks->forbidden_intervals.empty()) return true; - const int num_tasks = tasks->start_min.size(); - for (int task = 0; task < num_tasks; ++task) { - if (tasks->duration_min[task] == 0) continue; - if (tasks->forbidden_intervals[task] == nullptr) continue; - // If start_min forbidden, push to next feasible value. - { - const auto& interval = - tasks->forbidden_intervals[task]->FirstIntervalGreaterOrEqual( - tasks->start_min[task]); - if (interval == tasks->forbidden_intervals[task]->end()) continue; - if (interval->start <= tasks->start_min[task]) { - tasks->start_min[task] = CapAdd(interval->end, 1); - } - } - // If end_max forbidden, push to next feasible value. - { - const int64 start_max = - CapSub(tasks->end_max[task], tasks->duration_min[task]); - const auto& interval = - tasks->forbidden_intervals[task]->LastIntervalLessOrEqual(start_max); - if (interval == tasks->forbidden_intervals[task]->end()) continue; - if (interval->end >= start_max) { - tasks->end_max[task] = - CapAdd(interval->start, tasks->duration_min[task] - 1); - } - } - if (CapAdd(tasks->start_min[task], tasks->duration_min[task]) > - tasks->end_max[task]) { - return false; - } - } - return true; } TypeRegulationsChecker::TypeRegulationsChecker(const RoutingModel& model) @@ -5973,9 +5715,12 @@ bool TypeRegulationsChecker::CheckVehicle( return true; } + InitializeCheck(); + // Accumulates the count of types before the current node. + // {0, 0, 0} does not compile on or-tools. counts_of_type_.assign(model_.GetNumberOfVisitTypes(), - TypeRegulationsChecker::NodeCount()); + TypeRegulationsChecker::NodeCount()); for (int64 current = model_.Start(vehicle); !model_.IsEnd(current); current = next_accessor(current)) { @@ -6006,7 +5751,7 @@ bool TypeRegulationsChecker::CheckVehicle( : counts.pickup; count++; } - return true; + return FinalizeCheck(); } int TypeRegulationsChecker::GetNonDeliveryCount(int type) const { @@ -6031,7 +5776,10 @@ bool TypeIncompatibilityChecker::HasRegulationsToCheck() const { // TODO(user): Remove the check_hard_incompatibilities_ boolean and always // check both incompatibilities to simplify the code? -bool TypeIncompatibilityChecker::CheckTypeRegulations(int type) const { +// TODO(user): Improve algorithm by only checking a given type if necessary? +// - For temporal incompatibilities, only check if NonDeliveredType(count) == 1. +// - For hard incompatibilities, only if NonDeliveryType(type) == 1. +bool TypeIncompatibilityChecker::CheckTypeRegulations(int type) { for (int incompatible_type : model_.GetTemporalTypeIncompatibilitiesOfType(type)) { if (GetNonDeliveredCount(incompatible_type) > 0) { @@ -6050,10 +5798,11 @@ bool TypeIncompatibilityChecker::CheckTypeRegulations(int type) const { } bool TypeRequirementChecker::HasRegulationsToCheck() const { - return model_.HasTemporalTypeRequirements(); + return model_.HasTemporalTypeRequirements() || + model_.HasSameVehicleTypeRequirements(); } -bool TypeRequirementChecker::CheckTypeRegulations(int type) const { +bool TypeRequirementChecker::CheckTypeRegulations(int type) { for (const absl::flat_hash_set& requirement_alternatives : model_.GetTemporalRequiredTypeAlternativesOfType(type)) { bool has_one_of_alternatives = false; @@ -6067,6 +5816,28 @@ bool TypeRequirementChecker::CheckTypeRegulations(int type) const { return false; } } + if (!model_.GetSameVehicleRequiredTypeAlternativesOfType(type).empty()) { + types_with_same_vehicle_requirements_on_route_.insert(type); + } + return true; +} + +bool TypeRequirementChecker::FinalizeCheck() const { + for (int type : types_with_same_vehicle_requirements_on_route_) { + for (const absl::flat_hash_set& requirement_alternatives : + model_.GetSameVehicleRequiredTypeAlternativesOfType(type)) { + bool has_one_of_alternatives = false; + for (const int type_alternative : requirement_alternatives) { + if (GetNonDeliveryCount(type_alternative) > 0) { + has_one_of_alternatives = true; + break; + } + } + if (!has_one_of_alternatives) { + return false; + } + } + } return true; } @@ -6325,7 +6096,7 @@ void RoutingDimension::CloseModel(bool use_light_propagation) { } } } - if (!vehicle_break_intervals_.empty()) { + if (HasBreakConstraints()) { GlobalVehicleBreaksConstraint* constraint = model()->solver()->RevAlloc(new GlobalVehicleBreaksConstraint(this)); solver->AddConstraint(constraint); @@ -6344,11 +6115,6 @@ void RoutingDimension::SetSpanUpperBoundForVehicle(int64 upper_bound, CHECK_LT(vehicle, vehicle_span_upper_bounds_.size()); CHECK_GE(upper_bound, 0); vehicle_span_upper_bounds_[vehicle] = upper_bound; - Solver* const solver = model_->solver(); - IntVar* const start = cumuls_[model_->Start(vehicle)]; - IntVar* const end = cumuls_[model_->End(vehicle)]; - solver->AddConstraint( - solver->MakeLessOrEqual(solver->MakeDifference(end, start), upper_bound)); } void RoutingDimension::SetSpanCostCoefficientForVehicle(int64 coefficient, @@ -6478,8 +6244,8 @@ void RoutingDimension::SetupCumulVarSoftUpperBoundCosts( soft_bound.coefficient); IntVar* cost_var = BuildVarFromExprAndIndexActiveState(model_, expr, i); cost_elements->push_back(cost_var); - // TODO(user): Check if it wouldn't be better to minimize - // soft_bound.var here. + // NOTE: We minimize the cost here instead of minimizing the cumul + // variable, to avoid setting the cumul to earlier than necessary. model_->AddVariableMinimizedByFinalizer(cost_var); } } @@ -6526,9 +6292,11 @@ void RoutingDimension::SetupCumulVarSoftLowerBoundCosts( IntExpr* const expr = solver->MakeSemiContinuousExpr( solver->MakeDifference(soft_bound.bound, soft_bound.var), 0, soft_bound.coefficient); - cost_elements->push_back( - BuildVarFromExprAndIndexActiveState(model_, expr, i)); - model_->AddVariableMaximizedByFinalizer(soft_bound.var); + IntVar* cost_var = BuildVarFromExprAndIndexActiveState(model_, expr, i); + cost_elements->push_back(cost_var); + // NOTE: We minimize the cost here instead of maximizing the cumul + // variable, to avoid setting the cumul to later than necessary. + model_->AddVariableMinimizedByFinalizer(cost_var); } } } @@ -6588,41 +6356,66 @@ void RoutingDimension::SetupGlobalSpanCost( void RoutingDimension::SetBreakIntervalsOfVehicle( std::vector breaks, int vehicle, std::vector node_visit_transits) { - SetBreakIntervalsOfVehicle(std::move(breaks), vehicle, - std::move(node_visit_transits), - [](int64, int64) { return 0; }); + if (breaks.empty()) return; + const int visit_evaluator = model()->RegisterTransitCallback( + [node_visit_transits](int64 from, int64 to) { + return node_visit_transits[from]; + }); + SetBreakIntervalsOfVehicle(std::move(breaks), vehicle, visit_evaluator, -1); } void RoutingDimension::SetBreakIntervalsOfVehicle( std::vector breaks, int vehicle, std::vector node_visit_transits, - std::function group_delay) { - for (IntervalVar* const interval : breaks) { + std::function group_delays) { + if (breaks.empty()) return; + const int visit_evaluator = model()->RegisterTransitCallback( + [node_visit_transits](int64 from, int64 to) { + return node_visit_transits[from]; + }); + const int delay_evaluator = model()->RegisterTransitCallback(group_delays); + SetBreakIntervalsOfVehicle(std::move(breaks), vehicle, visit_evaluator, + delay_evaluator); +} + +void RoutingDimension::SetBreakIntervalsOfVehicle( + std::vector breaks, int vehicle, int pre_travel_evaluator, + int post_travel_evaluator) { + DCHECK_LE(0, vehicle); + DCHECK_LT(vehicle, model_->vehicles()); + if (breaks.empty()) return; + if (!break_constraints_are_initialized_) InitializeBreaks(); + vehicle_break_intervals_[vehicle] = std::move(breaks); + vehicle_pre_travel_evaluators_[vehicle] = pre_travel_evaluator; + vehicle_post_travel_evaluators_[vehicle] = post_travel_evaluator; + // Breaks intervals must be fixed by search. + for (IntervalVar* const interval : vehicle_break_intervals_[vehicle]) { model_->AddIntervalToAssignment(interval); + if (interval->MayBePerformed() && !interval->MustBePerformed()) { + model_->AddVariableMinimizedByFinalizer(interval->PerformedExpr()->Var()); + } model_->AddVariableMinimizedByFinalizer(interval->SafeStartExpr(0)->Var()); model_->AddVariableMinimizedByFinalizer( interval->SafeDurationExpr(0)->Var()); } + // When a vehicle has breaks, if its start and end are fixed, + // then propagation keeps the cumuls min and max on its path feasible. model_->AddVariableMinimizedByFinalizer(CumulVar(model_->End(vehicle))); model_->AddVariableMaximizedByFinalizer(CumulVar(model_->Start(vehicle))); - DCHECK_LE(0, vehicle); - DCHECK_LT(vehicle, model_->vehicles()); - if (vehicle_node_visit_transits_.empty()) { - vehicle_node_visit_transits_.resize(model_->vehicles()); - vehicle_break_intervals_.resize(model_->vehicles()); - vehicle_group_delays_.resize(model_->vehicles(), - [](int64, int64) { return 0; }); - } - DCHECK_EQ(0, vehicle_node_visit_transits_[vehicle].size()); - vehicle_node_visit_transits_[vehicle] = std::move(node_visit_transits); - vehicle_break_intervals_[vehicle] = std::move(breaks); - vehicle_group_delays_[vehicle] = std::move(group_delay); } -bool RoutingDimension::VehicleHasBreakIntervals(int vehicle) const { - DCHECK_LE(0, vehicle); - return (vehicle < vehicle_break_intervals_.size()) && - !vehicle_break_intervals_[vehicle].empty(); +void RoutingDimension::InitializeBreaks() { + DCHECK(!break_constraints_are_initialized_); + const int num_vehicles = model_->vehicles(); + vehicle_break_intervals_.resize(num_vehicles); + vehicle_pre_travel_evaluators_.resize(num_vehicles, -1); + vehicle_post_travel_evaluators_.resize(num_vehicles, -1); + vehicle_break_distance_duration_.resize(num_vehicles); + break_constraints_are_initialized_ = true; +} + +bool RoutingDimension::HasBreakConstraints() const { + return break_constraints_are_initialized_; } const std::vector& RoutingDimension::GetBreakIntervalsOfVehicle( @@ -6632,11 +6425,16 @@ const std::vector& RoutingDimension::GetBreakIntervalsOfVehicle( return vehicle_break_intervals_[vehicle]; } -const std::vector& RoutingDimension::GetNodeVisitTransitsOfVehicle( - int vehicle) const { +int RoutingDimension::GetPreTravelEvaluatorOfVehicle(int vehicle) const { DCHECK_LE(0, vehicle); - DCHECK_LT(vehicle, vehicle_node_visit_transits_.size()); - return vehicle_node_visit_transits_[vehicle]; + DCHECK_LT(vehicle, vehicle_pre_travel_evaluators_.size()); + return vehicle_pre_travel_evaluators_[vehicle]; +} + +int RoutingDimension::GetPostTravelEvaluatorOfVehicle(int vehicle) const { + DCHECK_LE(0, vehicle); + DCHECK_LT(vehicle, vehicle_post_travel_evaluators_.size()); + return vehicle_post_travel_evaluators_[vehicle]; } void RoutingDimension::SetBreakDistanceDurationOfVehicle(int64 distance, @@ -6644,9 +6442,7 @@ void RoutingDimension::SetBreakDistanceDurationOfVehicle(int64 distance, int vehicle) { DCHECK_LE(0, vehicle); DCHECK_LT(vehicle, model_->vehicles()); - if (vehicle_break_distance_duration_.empty()) { - vehicle_break_distance_duration_.resize(model_->vehicles()); - } + if (!break_constraints_are_initialized_) InitializeBreaks(); vehicle_break_distance_duration_[vehicle].emplace_back(distance, duration); } @@ -6657,11 +6453,6 @@ RoutingDimension::GetBreakDistanceDurationOfVehicle(int vehicle) const { return vehicle_break_distance_duration_[vehicle]; } -bool RoutingDimension::HasBreakDistanceDurationOfVehicle(int vehicle) const { - return (vehicle < vehicle_break_distance_duration_.size()) && - !vehicle_break_distance_duration_[vehicle].empty(); -} - void RoutingDimension::SetPickupToDeliveryLimitFunctionForPair( PickupToDeliveryLimitFunction limit_function, int pair_index) { CHECK_GE(pair_index, 0); diff --git a/ortools/constraint_solver/routing.h b/ortools/constraint_solver/routing.h index dc6dde9038..99b7ee48cd 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/constraint_solver/routing.h @@ -488,14 +488,18 @@ class RoutingModel { const std::function& f, int64 domain_start, int64 domain_end); - // For every vehicle of the routing model, if total_slacks[vehicle] is not - // nullptr, constrains it to be the sum of slacks on that vehicle, that is: - // dimension->CumulVar(end) - dimension->CumulVar(start) - - // sum_{node in path of vehicle} dimension->FixedTransitVar(node). + // For every vehicle of the routing model: + // - if total_slacks[vehicle] is not nullptr, constrains it to be the sum of + // slacks on that vehicle, that is, + // dimension->CumulVar(end) - dimension->CumulVar(start) - + // sum_{node in path of vehicle} dimension->FixedTransitVar(node). + // - if spans[vehicle] is not nullptr, constrains it to be + // dimension->CumulVar(end) - dimension->CumulVar(start) // This does stronger propagation than a decomposition, and takes breaks into // account. - Constraint* MakePathSlacks(const RoutingDimension* dimension, - std::vector total_slacks); + Constraint* MakePathSpansAndTotalSlacks(const RoutingDimension* dimension, + std::vector spans, + std::vector total_slacks); // Outputs the names of all dimensions added to the routing engine. // TODO(user): rename. @@ -696,6 +700,8 @@ class RoutingModel { int GetVisitType(int64 index) const; // This function should be called once all node visit types have been set and // prior to adding any incompatibilities/requirements. + // TODO(user): Reconsider the logic and potentially remove the need to + // "close" types. void CloseVisitTypes(); int GetNumberOfVisitTypes() const { return num_visit_types_; } // Incompatibilities: @@ -718,22 +724,35 @@ class RoutingModel { return has_temporal_type_incompatibilities_; } // Requirements: - // If type_D temporally depends on type_R, any non-delivery node_D of type_D - // requires at least one non-delivered node of type_R on its vehicle at the - // time node_D is visited. // NOTE: As of 2019-04, cycles in the requirement graph are not supported, // and lead to the dependent nodes being skipped if possible (otherwise // the model is considered infeasible). - // The following function specifies that "dependent_type" requires at least + // The following functions specify that "dependent_type" requires at least // one of the types in "required_type_alternatives". + // + // For same-vehicle requirements, a node of dependent type type_D requires at + // least one node of type type_R among the required alternatives on the same + // route. + void AddSameVehicleRequiredTypeAlternatives( + int dependent_type, absl::flat_hash_set required_type_alternatives); + // If type_D temporally depends on type_R, any non-delivery node_D of type_D + // requires at least one non-delivered node of type_R on its vehicle at the + // time node_D is visited. void AddTemporalRequiredTypeAlternatives( int dependent_type, absl::flat_hash_set required_type_alternatives); // clang-format off - // Returns all sets of requirement alternatives for the given type. + // Returns the sets of same-vehicle/temporal requirement alternatives for the + // given type. + const std::vector >& + GetSameVehicleRequiredTypeAlternativesOfType(int type) const; const std::vector >& GetTemporalRequiredTypeAlternativesOfType(int type) const; // clang-format on - // Returns true iff any type requirements have been added to the model. + // Returns true iff any same-route (resp. temporal) type requirements have + // been added to the model. + bool HasSameVehicleTypeRequirements() const { + return has_same_vehicle_type_requirements_; + } bool HasTemporalTypeRequirements() const { return has_temporal_type_requirements_; } @@ -742,7 +761,8 @@ class RoutingModel { // on node types. bool HasTypeRegulations() const { return HasTemporalTypeIncompatibilities() || - HasHardTypeIncompatibilities() || HasTemporalTypeRequirements(); + HasHardTypeIncompatibilities() || HasSameVehicleTypeRequirements() || + HasTemporalTypeRequirements(); } // Get the "unperformed" penalty of a node. This is only well defined if the @@ -982,8 +1002,8 @@ class RoutingModel { // Adds an extra variable to the vehicle routing assignment. void AddToAssignment(IntVar* const var); void AddIntervalToAssignment(IntervalVar* const interval); - // For every dimension in the model's dimensions_for_global_optimizer_, this - // method tries to pack the cumul values of the dimension, such that: + // For every dimension in the model's dimensions_for_local/global_optimizer_, + // this method tries to pack the cumul values of the dimension, such that: // - The cumul costs (span costs, soft lower and upper bound costs, etc) are // minimized. // - The cumuls of the ends of the routes are minimized for this given minimal @@ -991,7 +1011,7 @@ class RoutingModel { // - Given these minimal end cumuls, the route start cumuls are maximized. // Returns the assignment resulting from allocating these packed cumuls with // the solver, and nullptr if these cumuls could not be set by the solver. - const Assignment* PackCumulsOfGlobalOptimizerDimensionsFromAssignment( + const Assignment* PackCumulsOfOptimizerDimensionsFromAssignment( const Assignment* original_assignment, absl::Duration duration_limit); #ifndef SWIG // TODO(user): Revisit if coordinates are added to the RoutingModel class. @@ -1058,19 +1078,20 @@ class RoutingModel { IntVar* CostVar() const { return cost_; } // Returns the cost of the transit arc between two nodes for a given vehicle. // Input are variable indices of node. This returns 0 if vehicle < 0. - int64 GetArcCostForVehicle(int64 from_index, int64 to_index, int64 vehicle); + int64 GetArcCostForVehicle(int64 from_index, int64 to_index, + int64 vehicle) const; // Whether costs are homogeneous across all vehicles. bool CostsAreHomogeneousAcrossVehicles() const { return costs_are_homogeneous_across_vehicles_; } // Returns the cost of the segment between two nodes supposing all vehicle // costs are the same (returns the cost for the first vehicle otherwise). - int64 GetHomogeneousCost(int64 from_index, int64 to_index) { + int64 GetHomogeneousCost(int64 from_index, int64 to_index) const { return GetArcCostForVehicle(from_index, to_index, /*vehicle=*/0); } // Returns the cost of the arc in the context of the first solution strategy. // This is typically a simplification of the actual cost; see the .cc. - int64 GetArcCostForFirstSolution(int64 from_index, int64 to_index); + int64 GetArcCostForFirstSolution(int64 from_index, int64 to_index) const; // Returns the cost of the segment between two nodes for a given cost // class. Input are variable indices of nodes and the cost class. // Unlike GetArcCostForVehicle(), if cost_class is kNoCost, then the @@ -1078,7 +1099,7 @@ class RoutingModel { // of the cost that depend on the cost class will be omited. See the code // for details. int64 GetArcCostForClass(int64 from_index, int64 to_index, - int64 /*CostClassIndex*/ cost_class_index); + int64 /*CostClassIndex*/ cost_class_index) const; // Get the cost class index of the given vehicle. CostClassIndex GetCostClassIndexOfVehicle(int64 vehicle) const { DCHECK(closed_); @@ -1332,7 +1353,7 @@ class RoutingModel { void ComputeCostClasses(const RoutingSearchParameters& parameters); void ComputeVehicleClasses(); int64 GetArcCostForClassInternal(int64 from_index, int64 to_index, - CostClassIndex cost_class_index); + CostClassIndex cost_class_index) const; void AppendHomogeneousArcCosts(const RoutingSearchParameters& parameters, int node_index, std::vector* cost_elements); @@ -1468,7 +1489,7 @@ class RoutingModel { // - IsEnd(i) is true // - or nexts_[i] is bound and is_bound_to_end_[nexts_[i].Value()] is true. std::vector is_bound_to_end_; - RevSwitch is_bound_to_end_ct_added_; + mutable RevSwitch is_bound_to_end_ct_added_; // Dimensions absl::flat_hash_map dimension_name_to_index_; gtl::ITIVector dimensions_; @@ -1498,7 +1519,7 @@ class RoutingModel { #endif // SWIG bool costs_are_homogeneous_across_vehicles_; bool cache_callbacks_; - std::vector cost_cache_; // Index by source index. + mutable std::vector cost_cache_; // Index by source index. std::vector vehicle_class_index_of_vehicle_; #ifndef SWIG gtl::ITIVector vehicle_classes_; @@ -1542,10 +1563,13 @@ class RoutingModel { temporal_incompatible_types_per_type_index_; bool has_temporal_type_incompatibilities_; + std::vector > > + same_vehicle_required_type_alternatives_per_type_index_; + bool has_same_vehicle_type_requirements_; std::vector > > temporal_required_type_alternatives_per_type_index_; - absl::flat_hash_set trivially_infeasible_visit_types_; bool has_temporal_type_requirements_; + absl::flat_hash_set trivially_infeasible_visit_types_; // clang-format on int num_visit_types_; // Two indices are equivalent if they correspond to the same node (as given to @@ -1655,6 +1679,18 @@ class DisjunctivePropagator { std::vector is_preemptible; std::vector forbidden_intervals; std::vector> distance_duration; + + void Clear() { + start_min.clear(); + start_max.clear(); + duration_min.clear(); + duration_max.clear(); + end_min.clear(); + end_max.clear(); + is_preemptible.clear(); + forbidden_intervals.clear(); + distance_duration.clear(); + } }; // Computes new bounds for all tasks, returns false if infeasible. @@ -1685,6 +1721,19 @@ class DisjunctivePropagator { std::vector event_of_task_; std::vector nonchain_tasks_by_start_max_; }; + +void AppendTasksFromPath(const std::vector& path, + const std::vector& min_travels, + const std::vector& max_travels, + const std::vector& pre_travels, + const std::vector& post_travels, + const RoutingDimension& dimension, + DisjunctivePropagator::Tasks* tasks); +void AppendTasksFromIntervals(const std::vector& intervals, + DisjunctivePropagator::Tasks* tasks); +void FillPathEvaluation(const std::vector& path, + const RoutingModel::TransitCallback2& evaluator, + std::vector* values); #endif // !defined(SWIG) // GlobalVehicleBreaksConstraint ensures breaks constraints are enforced on @@ -1697,11 +1746,6 @@ class DisjunctivePropagator { // between two consecutive nodes of a route to do transit and vehicle breaks, // i.e. if Next(nodeA) = nodeB, CumulVar(nodeA) = tA and CumulVar(nodeB) = tB, // then SlackVar(nodeA) >= sum_{breaks \subseteq [tA, tB)} duration(break). -// TODO(user): This does not enforce vehicle breaks to be nonoverlapping, -// and supposes travel/service times to be feasible (e.g. with a PathCumul). -// This is probably the desired behaviour, because vehicle breaks will most -// likely be constrained with precedence relations that are stronger than -// a resource constraint. class GlobalVehicleBreaksConstraint : public Constraint { public: explicit GlobalVehicleBreaksConstraint(const RoutingDimension* dimension); @@ -1717,31 +1761,45 @@ class GlobalVehicleBreaksConstraint : public Constraint { const RoutingModel* model_; const RoutingDimension* const dimension_; std::vector vehicle_demons_; + std::vector path_; + + // Sets path_ to be the longest sequence such that + // _ path_[0] is the start of the vehicle + // _ Next(path_[i-1]) is Bound() and has value path_[i], + // followed by the end of the vehicle if the last node was not an end. + void FillPartialPathOfVehicle(int vehicle); + void FillPathTravels(const std::vector& path); // This translates pruning information to solver variables. + // If constructed with an IntervalVar*, it follows the usual semantics of + // IntervalVars. If constructed with an IntVar*, before_start and after_start, + // operations are translated to simulate an interval that starts at + // start - before_start and ends and start + after_start. + // If constructed with nothing, the TaskTranslator will do nothing. // This class should have been an interface + subclasses, // but that would force pointers in the user's task vector, - // which means dynamic allocation. Here such a vector's reserved size will - // adjust to usage and eventually no more dynamic allocation will be made. + // which means dynamic allocation. With this union-like structure, + // a vector's reserved size will adjust to usage and eventually no more + // dynamic allocation will be made. class TaskTranslator { public: - TaskTranslator(IntVar* start, int64 duration_min, int64 group_delay) + TaskTranslator(IntVar* start, int64 before_start, int64 after_start) : start_(start), - duration_min_(duration_min), - group_delay_(group_delay) {} + before_start_(before_start), + after_start_(after_start) {} explicit TaskTranslator(IntervalVar* interval) : interval_(interval) {} TaskTranslator() {} void SetStartMin(int64 value) { if (start_ != nullptr) { - start_->SetMin(CapAdd(group_delay_, value)); + start_->SetMin(CapAdd(before_start_, value)); } else if (interval_ != nullptr) { interval_->SetStartMin(value); } } void SetStartMax(int64 value) { if (start_ != nullptr) { - start_->SetMax(CapAdd(group_delay_, value)); + start_->SetMax(CapAdd(before_start_, value)); } else if (interval_ != nullptr) { interval_->SetStartMax(value); } @@ -1753,14 +1811,14 @@ class GlobalVehicleBreaksConstraint : public Constraint { } void SetEndMin(int64 value) { if (start_ != nullptr) { - start_->SetMin(CapSub(value, duration_min_)); + start_->SetMin(CapSub(value, after_start_)); } else if (interval_ != nullptr) { interval_->SetEndMin(value); } } void SetEndMax(int64 value) { if (start_ != nullptr) { - start_->SetMax(value - duration_min_); + start_->SetMax(CapSub(value, after_start_)); } else if (interval_ != nullptr) { interval_->SetEndMax(value); } @@ -1768,8 +1826,8 @@ class GlobalVehicleBreaksConstraint : public Constraint { private: IntVar* start_ = nullptr; - int64 duration_min_; - int64 group_delay_; + int64 before_start_; + int64 after_start_; IntervalVar* interval_ = nullptr; }; @@ -1780,8 +1838,11 @@ class GlobalVehicleBreaksConstraint : public Constraint { DisjunctivePropagator disjunctive_propagator_; DisjunctivePropagator::Tasks tasks_; - std::vector cumuls_; - std::vector fixed_transits_; + // Fields used to help build tasks_ at each propagation. + std::vector min_travel_; + std::vector max_travel_; + std::vector pre_travel_; + std::vector post_travel_; }; class TypeRegulationsChecker { @@ -1806,7 +1867,9 @@ class TypeRegulationsChecker { int GetNonDeliveredCount(int type) const; virtual bool HasRegulationsToCheck() const = 0; - virtual bool CheckTypeRegulations(int type) const = 0; + virtual void InitializeCheck() {} + virtual bool CheckTypeRegulations(int type) = 0; + virtual bool FinalizeCheck() const { return true; } const RoutingModel& model_; @@ -1824,7 +1887,7 @@ class TypeIncompatibilityChecker : public TypeRegulationsChecker { private: bool HasRegulationsToCheck() const override; - bool CheckTypeRegulations(int type) const override; + bool CheckTypeRegulations(int type) override; // NOTE(user): As temporal incompatibilities are always verified with this // checker, we only store 1 boolean indicating whether or not hard // incompatibilities are also verified. @@ -1840,7 +1903,13 @@ class TypeRequirementChecker : public TypeRegulationsChecker { private: bool HasRegulationsToCheck() const override; - bool CheckTypeRegulations(int type) const override; + void InitializeCheck() override { + types_with_same_vehicle_requirements_on_route_.clear(); + } + bool CheckTypeRegulations(int type) override; + bool FinalizeCheck() const override; + + absl::flat_hash_set types_with_same_vehicle_requirements_on_route_; }; // The following constraint ensures that incompatibilities and requirements @@ -1856,9 +1925,10 @@ class TypeRequirementChecker : public TypeRegulationsChecker { // p1 --> d1 --> n --> p2 --> d2 is acceptable, whereas any configurations // with p1 --> p2 --> d1 --> ..., or p1 --> n --> d1 --> ... is not feasible. // -// It also verifies temporal type requirements. -// In the above example, if T1 is a requirement for T2, p2 must be visited -// between p1 and d1. +// It also verifies same-vehicle and temporal type requirements. +// In the above example, if T1 is a requirement for T2: +// - For a same-vehicle requirement, p1/d1 must be on the same vehicle as p2/d2. +// - For a temporal requirement, p2 must be visited between p1 and d1. class TypeRegulationsConstraint : public Constraint { public: explicit TypeRegulationsConstraint(const RoutingModel& model); @@ -1875,6 +1945,37 @@ class TypeRegulationsConstraint : public Constraint { TypeRequirementChecker requirement_checker_; std::vector vehicle_demons_; }; +#if !defined SWIG +// A structure meant to store soft bounds and associated violation constants. +// It is 'Simple' because it has one BoundCost per element, +// in contrast to 'Multiple'. Design notes: +// - it is meant to store model information to be shared through pointers, +// so it disallows copy and assign to avoid accidental duplication. +// - it keeps soft bounds as an array of structs to help cache, +// because code that uses such bounds typically use both bound and cost. +// - soft bounds are named pairs, prevents some mistakes. +// - using operator[] to access elements is not interesting, +// because the structure will be accessed through pointers, moreover having +// to type bound_cost reminds the user of the order if they do a copy +// assignment of the element. +class SimpleBoundCosts { + public: + struct BoundCost { + int64 bound; + int64 cost; + }; + SimpleBoundCosts(int num_bounds, BoundCost default_bound_cost) + : bound_costs_(num_bounds, default_bound_cost) {} + BoundCost& bound_cost(int element) { return bound_costs_[element]; } + BoundCost bound_cost(int element) const { return bound_costs_[element]; } + int Size() { return bound_costs_.size(); } + SimpleBoundCosts(const SimpleBoundCosts&) = delete; + SimpleBoundCosts operator=(const SimpleBoundCosts&) = delete; + + private: + std::vector bound_costs_; +}; +#endif // !defined SWIG // Dimensions represent quantities accumulated at nodes along the routes. They // represent quantities such as weights or volumes carried along the route, or @@ -2030,36 +2131,51 @@ class RoutingDimension { int64 GetCumulVarSoftLowerBoundCoefficient(int64 index) const; // Sets the breaks for a given vehicle. Breaks are represented by // IntervalVars. They may interrupt transits between nodes and increase - // the value of corresponding slack variables. However a break interval cannot - // overlap the transit interval of a node, which is - // [CumulVar(node), CumulVar(node) + node_visit_transits[node]), i.e. the - // break interval must either end before CumulVar(node) or start after - // CumulVar(node) + node_visit_transits[node]. - // In the case where the route entails some group delay on a visit, the break - // cannot overlap that delay either, so the time window it cannot overlap is - // [CumulVar(node) - delay, CumulVar(node) + node_visit_transits[node]). + // the value of corresponding slack variables. + // A break may take place before the start of a vehicle, after the end of + // a vehicle, or during a travel i -> j. + // In that case, the interval [break.Start(), break.End()) must be a subset of + // [CumulVar(i) + pre_travel(i, j), CumulVar(j) - post_travel(i, j)). + // In other words, a break may not overlap any node n's visit, given by + // [CumulVar(n) - post_travel(_, n), CumulVar(n) + pre_travel(n, _)). + // This formula considers post_travel(_, start) and pre_travel(end, _) to be + // 0; pre_travel will never be called on any (_, start) and post_travel will + // never we called on any (end, _). If pre_travel_evaluator or + // post_travel_evaluator is -1, it will be taken as a function that always + // returns 0. + // TODO(user): Remove if !defined when routing.i is repaired. +#if !defined(SWIGPYTHON) + void SetBreakIntervalsOfVehicle(std::vector breaks, int vehicle, + int pre_travel_evaluator, + int post_travel_evaluator); +#endif // !defined(SWIGPYTHON) + + // Deprecated, sets pre_travel(i, j) = node_visit_transit[i]. void SetBreakIntervalsOfVehicle(std::vector breaks, int vehicle, std::vector node_visit_transits); + // With breaks supposed to be consecutive, this forces the distance between // breaks of size at least minimum_break_duration to be at least distance. // This supposes that the time until route start and after route end are // infinite breaks. void SetBreakDistanceDurationOfVehicle(int64 distance, int64 duration, int vehicle); + // Sets up vehicle_break_intervals_, vehicle_break_distance_duration_, + // pre_travel_evaluators and post_travel_evaluators. + void InitializeBreaks(); + // Returns true if any break interval or break distance was defined. + bool HasBreakConstraints() const; #if !defined(SWIGPYTHON) + // Deprecated, sets pre_travel(i, j) = node_visit_transit[i] + // and post_travel(i, j) = group_delays(i, j). void SetBreakIntervalsOfVehicle( std::vector breaks, int vehicle, std::vector node_visit_transits, - std::function group_delay); - // Returns true if the vehicle has break intervals. - bool VehicleHasBreakIntervals(int vehicle) const; - // Returns true iff some break distance constraint was set for this vehicle. - bool HasBreakDistanceDurationOfVehicle(int vehicle) const; + std::function group_delays); + // Returns the break intervals set by SetBreakIntervalsOfVehicle(). const std::vector& GetBreakIntervalsOfVehicle( int vehicle) const; - // Returns the amount of visit transit set by SetBreakIntervalsOfVehicle(). - const std::vector& GetNodeVisitTransitsOfVehicle(int vehicle) const; // Returns the pairs (distance, duration) specified by break distance // constraints. // clang-format off @@ -2067,6 +2183,8 @@ class RoutingDimension { GetBreakDistanceDurationOfVehicle(int vehicle) const; // clang-format on #endif // !defined(SWIGPYTHON) + int GetPreTravelEvaluatorOfVehicle(int vehicle) const; + int GetPostTravelEvaluatorOfVehicle(int vehicle) const; // Returns the parent in the dependency tree if any or nullptr otherwise. const RoutingDimension* base_dimension() const { return base_dimension_; } @@ -2146,11 +2264,6 @@ class RoutingDimension { return global_span_cost_coefficient_; } - int64 GetGroupDelay(int vehicle, int64 from_index, int64 to_index) const { - DCHECK_LT(vehicle, vehicle_group_delays_.size()); - return vehicle_group_delays_[vehicle](from_index, to_index); - } - int64 GetGlobalOptimizerOffset() const { DCHECK_GE(global_optimizer_offset_, 0); return global_optimizer_offset_; @@ -2162,6 +2275,26 @@ class RoutingDimension { DCHECK_GE(local_optimizer_offset_for_vehicle_[vehicle], 0); return local_optimizer_offset_for_vehicle_[vehicle]; } +#if !defined SWIG + // If the span of vehicle on this dimension is larger than bound, + // the cost will be increased by cost * (span - bound). + void SetSoftSpanUpperBoundForVehicle(SimpleBoundCosts::BoundCost bound_cost, + int vehicle) { + if (!HasSoftSpanUpperBounds()) { + vehicle_soft_span_upper_bound_ = absl::make_unique( + model_->vehicles(), SimpleBoundCosts::BoundCost{kint64max, 0}); + } + vehicle_soft_span_upper_bound_->bound_cost(vehicle) = bound_cost; + } + bool HasSoftSpanUpperBounds() const { + return vehicle_soft_span_upper_bound_ != nullptr; + } + SimpleBoundCosts::BoundCost GetSoftSpanUpperBoundForVehicle( + int vehicle) const { + DCHECK(HasSoftSpanUpperBounds()); + return vehicle_soft_span_upper_bound_->bound_cost(vehicle); + } +#endif // !defined SWIG private: struct SoftBound { @@ -2252,14 +2385,17 @@ class RoutingDimension { pickup_to_delivery_limits_per_pair_index_; // Used if some vehicle has breaks in this dimension, typically time. + bool break_constraints_are_initialized_ = false; // clang-format off std::vector > vehicle_break_intervals_; - std::vector > vehicle_node_visit_transits_; - std::vector > - vehicle_group_delays_; std::vector > > vehicle_break_distance_duration_; // clang-format on + // For each vehicle, stores the part of travel that is made directly + // after (before) the departure (arrival) node of the travel. + // These parts of the travel are non-interruptible, in particular by a break. + std::vector vehicle_pre_travel_evaluators_; + std::vector vehicle_post_travel_evaluators_; std::vector slacks_; std::vector dependent_transits_; @@ -2273,7 +2409,8 @@ class RoutingDimension { const std::string name_; int64 global_optimizer_offset_; std::vector local_optimizer_offset_for_vehicle_; - + // nullptr if not defined. + std::unique_ptr vehicle_soft_span_upper_bound_; friend class RoutingModel; friend class RoutingModelInspector; @@ -3001,6 +3138,12 @@ class ChristofidesFilteredDecisionBuilder } }; +// Attempts to solve the model using the cp-sat solver. As of 5/2019, will solve +// the TSP corresponding to the model if it has a single vehicle. Therefore the +// resulting solution might not actually be feasible. Will return false if a +// solution could not be found. +bool SolveModelWithSat(RoutingModel* model, Assignment* solution); + // Generic path-based filter class. class BasePathFilter : public IntVarLocalSearchFilter { @@ -3101,14 +3244,14 @@ IntVarLocalSearchFilter* MakeTypeRegulationsFilter( const RoutingModel& routing_model); std::vector MakeCumulFilters( const RoutingDimension& dimension, - Solver::ObjectiveWatcher objective_callback, bool filter_objective); + Solver::ObjectiveWatcher objective_callback, bool filter_objective_cost); IntVarLocalSearchFilter* MakePathCumulFilter( const RoutingDimension& dimension, Solver::ObjectiveWatcher objective_callback, - bool propagate_own_objective_value); + bool propagate_own_objective_value, bool filter_objective_cost); IntVarLocalSearchFilter* MakeGlobalLPCumulFilter( const RoutingDimension& dimension, - Solver::ObjectiveWatcher objective_callback); + Solver::ObjectiveWatcher objective_callback, bool filter_objective_cost); IntVarLocalSearchFilter* MakePickupDeliveryFilter( const RoutingModel& routing_model, const RoutingModel::IndexPairs& pairs, const std::vector& vehicle_policies); diff --git a/ortools/constraint_solver/routing_flags.cc b/ortools/constraint_solver/routing_flags.cc index aae39a00c2..874c7d0c00 100644 --- a/ortools/constraint_solver/routing_flags.cc +++ b/ortools/constraint_solver/routing_flags.cc @@ -253,6 +253,8 @@ void AddLocalSearchNeighborhoodOperatorsFromFlags( void SetSearchLimitsFromFlags(RoutingSearchParameters* parameters) { CHECK(parameters != nullptr); parameters->set_use_depth_first_search(FLAGS_routing_dfs); + parameters->set_use_cp(BOOL_TRUE); + parameters->set_use_cp_sat(BOOL_FALSE); parameters->set_optimization_step(FLAGS_routing_optimization_step); parameters->set_number_of_solutions_to_collect( FLAGS_routing_number_of_solutions_to_collect); diff --git a/ortools/constraint_solver/routing_lp_scheduling.cc b/ortools/constraint_solver/routing_lp_scheduling.cc index 57649b9fc0..4634f7d603 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.cc +++ b/ortools/constraint_solver/routing_lp_scheduling.cc @@ -56,6 +56,7 @@ bool SetVariableBounds(glop::LinearProgram* linear_program, // lp_min > lp_max case, so we must detect infeasibility here. return false; } + } // namespace LocalDimensionCumulOptimizer::LocalDimensionCumulOptimizer( @@ -106,10 +107,19 @@ bool LocalDimensionCumulOptimizer::ComputeRouteCumuls( lp_solver_[vehicle].get(), optimal_cumuls, nullptr, nullptr); } +bool LocalDimensionCumulOptimizer::ComputePackedRouteCumuls( + int vehicle, const std::function& next_accessor, + std::vector* packed_cumuls) { + return optimizer_core_.OptimizeAndPackSingleRoute( + vehicle, next_accessor, linear_program_[vehicle].get(), + lp_solver_[vehicle].get(), packed_cumuls); +} + bool DimensionCumulOptimizerCore::OptimizeSingleRoute( int vehicle, const std::function& next_accessor, glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, - std::vector* cumul_values, int64* cost, int64* transit_cost) { + std::vector* cumul_values, int64* cost, int64* transit_cost, + bool clear_lp) { InitOptimizer(linear_program); const RoutingModel* const model = dimension()->model(); @@ -133,14 +143,17 @@ bool DimensionCumulOptimizerCore::OptimizeSingleRoute( *cost = CapAdd(cost_offset, std::round(lp_solver->GetObjectiveValue())); } - linear_program->Clear(); + if (clear_lp) { + linear_program->Clear(); + } return true; } bool DimensionCumulOptimizerCore::Optimize( const std::function& next_accessor, glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, - std::vector* cumul_values, int64* cost, int64* transit_cost) { + std::vector* cumul_values, int64* cost, int64* transit_cost, + bool clear_lp) { InitOptimizer(linear_program); // If both "cumul_values" and "cost" parameters are null, we don't try to @@ -186,7 +199,9 @@ bool DimensionCumulOptimizerCore::Optimize( CapAdd(std::round(lp_solver->GetObjectiveValue()), total_cost_offset); } - linear_program->Clear(); + if (clear_lp) { + linear_program->Clear(); + } return true; } @@ -194,28 +209,54 @@ bool DimensionCumulOptimizerCore::OptimizeAndPack( const std::function& next_accessor, glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, std::vector* cumul_values) { - InitOptimizer(linear_program); - - const int64 cumul_offset = dimension_->GetGlobalOptimizerOffset(); - const RoutingModel* model = dimension()->model(); - bool has_vehicles_being_optimized = false; - for (int vehicle = 0; vehicle < model->vehicles(); vehicle++) { - const bool optimize_vehicle_costs = - !model->IsEnd(next_accessor(model->Start(vehicle))) || - model->AreEmptyRouteCostsConsideredForVehicle(vehicle); - if (!SetRouteCumulConstraints(vehicle, next_accessor, cumul_offset, - optimize_vehicle_costs, linear_program, - nullptr, nullptr)) { - return false; - } - has_vehicles_being_optimized |= optimize_vehicle_costs; - } - SetGlobalConstraints(has_vehicles_being_optimized, linear_program); - - if (!FinalizeAndSolve(linear_program, lp_solver)) { + // Note: We pass a non-nullptr cost to the Optimize() method so the costs are + // optimized by the LP. + int64 cost = 0; + if (!Optimize(next_accessor, linear_program, lp_solver, + /*cumul_values=*/nullptr, &cost, /*transit_cost=*/nullptr, + /*clear_lp=*/false)) { return false; } + std::vector vehicles(dimension()->model()->vehicles()); + std::iota(vehicles.begin(), vehicles.end(), 0); + if (!PackRoutes(std::move(vehicles), linear_program, lp_solver)) { + return false; + } + + SetCumulValuesFromLP(index_to_cumul_variable_, + dimension_->GetGlobalOptimizerOffset(), *lp_solver, + cumul_values); + linear_program->Clear(); + return true; +} + +bool DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( + int vehicle, const std::function& next_accessor, + glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, + std::vector* cumul_values) { + // Note: We pass a non-nullptr cost to the OptimizeSingleRoute() method so the + // costs are optimized by the LP. + int64 cost = 0; + if (!OptimizeSingleRoute(vehicle, next_accessor, linear_program, lp_solver, + /*cumul_values=*/nullptr, &cost, + /*transit_cost=*/nullptr, + /*clear_lp=*/false) || + !PackRoutes({vehicle}, linear_program, lp_solver)) { + return false; + } + SetCumulValuesFromLP(current_route_cumul_variables_, + dimension_->GetLocalOptimizerOffsetForVehicle(vehicle), + *lp_solver, cumul_values); + linear_program->Clear(); + return true; +} + +bool DimensionCumulOptimizerCore::PackRoutes( + std::vector vehicles, glop::LinearProgram* linear_program, + glop::LPSolver* lp_solver) { + const RoutingModel* model = dimension_->model(); + // Minimize the route end times without increasing the cost. glop::RowIndex objective_ct = linear_program->CreateNewConstraint(); linear_program->SetConstraintBounds(objective_ct, 0, @@ -231,7 +272,7 @@ bool DimensionCumulOptimizerCore::OptimizeAndPack( linear_program->SetObjectiveCoefficient(variable, 0); } } - for (int vehicle = 0; vehicle < model->vehicles(); vehicle++) { + for (int vehicle : vehicles) { linear_program->SetObjectiveCoefficient( index_to_cumul_variable_[model->End(vehicle)], 1); } @@ -242,7 +283,7 @@ bool DimensionCumulOptimizerCore::OptimizeAndPack( // Maximize the route start times without increasing the cost or the route end // times. - for (int vehicle = 0; vehicle < model->vehicles(); vehicle++) { + for (int vehicle : vehicles) { const glop::ColIndex end_cumul_var = index_to_cumul_variable_[model->End(vehicle)]; // end_cumul_var <= lp_solver.variable_values()[end_cumul_var] @@ -259,9 +300,6 @@ bool DimensionCumulOptimizerCore::OptimizeAndPack( return false; } - SetCumulValuesFromLP(index_to_cumul_variable_, cumul_offset, *lp_solver, - cumul_values); - linear_program->Clear(); return true; } diff --git a/ortools/constraint_solver/routing_lp_scheduling.h b/ortools/constraint_solver/routing_lp_scheduling.h index f8f9d14fe1..5b61168310 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.h +++ b/ortools/constraint_solver/routing_lp_scheduling.h @@ -40,18 +40,23 @@ class DimensionCumulOptimizerCore { glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, std::vector* cumul_values, int64* cost, - int64* transit_cost); + int64* transit_cost, bool clear_lp = true); bool Optimize(const std::function& next_accessor, glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, std::vector* cumul_values, int64* cost, - int64* transit_cost); + int64* transit_cost, bool clear_lp = true); bool OptimizeAndPack(const std::function& next_accessor, glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, std::vector* cumul_values); + bool OptimizeAndPackSingleRoute( + int vehicle, const std::function& next_accessor, + glop::LinearProgram* linear_program, glop::LPSolver* lp_solver, + std::vector* cumul_values); + const RoutingDimension* dimension() const { return dimension_; } private: @@ -85,6 +90,16 @@ class DimensionCumulOptimizerCore { int64 offset, const glop::LPSolver& lp_solver, std::vector* cumul_values); + // This function packs the routes of the given vehicles while keeping the cost + // of the LP lower than its current (supposed optimal) objective value. + // It does so by setting the current objective variables' coefficient to 0 and + // setting the coefficient of the route ends to 1, to first minimize the route + // ends' cumuls, and then maximizes the starts' cumuls without increasing the + // ends. + bool PackRoutes(std::vector vehicles, + glop::LinearProgram* linear_program, + glop::LPSolver* lp_solver); + const RoutingDimension* const dimension_; std::vector current_route_cumul_variables_; std::vector index_to_cumul_variable_; @@ -101,6 +116,7 @@ class DimensionCumulOptimizerCore { class LocalDimensionCumulOptimizer { public: explicit LocalDimensionCumulOptimizer(const RoutingDimension* dimension); + // If feasible, computes the optimal cost of the route performed by a vehicle, // minimizing cumul soft lower and upper bound costs and vehicle span costs, // and stores it in "optimal_cost" (if not null). @@ -108,11 +124,13 @@ class LocalDimensionCumulOptimizer { bool ComputeRouteCumulCost(int vehicle, const std::function& next_accessor, int64* optimal_cost); + // Same as ComputeRouteCumulCost, but the cost computed does not contain // the part of the vehicle span cost due to fixed transits. bool ComputeRouteCumulCostWithoutFixedTransits( int vehicle, const std::function& next_accessor, int64* optimal_cost_without_transits); + // If feasible, computes the optimal cumul values of the route performed by a // vehicle, minimizing cumul soft lower and upper bound costs and vehicle span // costs, stores them in "optimal_cumuls" (if not null), and returns true. @@ -121,6 +139,13 @@ class LocalDimensionCumulOptimizer { const std::function& next_accessor, std::vector* optimal_cumuls); + // Similar to ComputeRouteCumuls, but also tries to pack the cumul values on + // the route, such that the cost remains the same, the cumul of route end is + // minimized, and then the cumul of the start of the route is maximized. + bool ComputePackedRouteCumuls( + int vehicle, const std::function& next_accessor, + std::vector* packed_cumuls); + const RoutingDimension* dimension() const { return optimizer_core_.dimension(); } diff --git a/ortools/constraint_solver/routing_parameters.cc b/ortools/constraint_solver/routing_parameters.cc index fff703406c..74cdabc29f 100644 --- a/ortools/constraint_solver/routing_parameters.cc +++ b/ortools/constraint_solver/routing_parameters.cc @@ -85,6 +85,8 @@ RoutingSearchParameters DefaultRoutingSearchParameters() { "local_search_metaheuristic: AUTOMATIC " "guided_local_search_lambda_coefficient: 0.1 " "use_depth_first_search: false " + "use_cp: BOOL_TRUE " + "use_cp_sat: BOOL_FALSE " "optimization_step: 0.0 " "number_of_solutions_to_collect: 1 " // No "time_limit" by default. diff --git a/ortools/constraint_solver/routing_parameters.proto b/ortools/constraint_solver/routing_parameters.proto index 95847cfff1..d97a30eb3b 100644 --- a/ortools/constraint_solver/routing_parameters.proto +++ b/ortools/constraint_solver/routing_parameters.proto @@ -331,6 +331,15 @@ message RoutingSearchParameters { // If true, the solver should use depth-first search rather than local search // to solve the problem. bool use_depth_first_search = 6; + // If true, use the CP solver to find a solution. Either local or depth-first + // search will be used depending on the value of use_depth_first_search. Will + // be run after the CP-SAT solver if time is remaining (cf. use_cp_sat). + OptionalBoolean use_cp = 28; + // If true, use the CP-SAT solver to find a solution. If there is time + // remaining normal CP local or depth-first search will be run if use_cp is + // true. + // As of 5/2019, only TSP models can be solved. + OptionalBoolean use_cp_sat = 27; // Minimum step by which the solution must be improved in local search. 0 // means "unspecified". If this value is fractional, it will get rounded to // the nearest integer. diff --git a/ortools/constraint_solver/routing_search.cc b/ortools/constraint_solver/routing_search.cc index 8d6b8e9472..c7325c531e 100644 --- a/ortools/constraint_solver/routing_search.cc +++ b/ortools/constraint_solver/routing_search.cc @@ -16,6 +16,7 @@ // and local search filters. // TODO(user): Move all existing routing search code here. +#include #include #include #include @@ -717,7 +718,7 @@ class TypeRegulationsFilter : public BasePathFilter { // The following vector is used to keep track of the type counts for hard // incompatibilities. std::vector> hard_incompatibility_type_counts_per_vehicle_; - // Used to verify the temporal incompatibilities. + // Used to verify the temporal incompatibilities and requirements. TypeIncompatibilityChecker temporal_incompatibility_checker_; TypeRequirementChecker requirement_checker_; }; @@ -773,21 +774,23 @@ bool TypeRegulationsFilter::HardIncompatibilitiesRespected(int vehicle, int64 chain_end) { if (!routing_model_.HasHardTypeIncompatibilities()) return true; - std::vector new_type_counts = + const std::vector& previous_type_counts = hard_incompatibility_type_counts_per_vehicle_[vehicle]; - const int num_types = new_type_counts.size(); - std::vector types_to_check; + + absl::flat_hash_map new_type_counts; + absl::flat_hash_set types_to_check; // Go through the new nodes on the path and increment their type counts. int64 node = GetNext(chain_start); while (node != chain_end) { const int type = routing_model_.GetVisitType(node); if (type >= 0) { - CHECK_LT(type, num_types); - const int previous_count = new_type_counts[type]++; - if (previous_count == 0) { + DCHECK_LT(type, previous_type_counts.size()); + int& type_count = gtl::LookupOrInsert(&new_type_counts, type, + previous_type_counts[type]); + if (type_count++ == 0) { // New type on the route, mark to check its incompatibilities. - types_to_check.push_back(type); + types_to_check.insert(type); } } node = GetNext(node); @@ -799,8 +802,9 @@ bool TypeRegulationsFilter::HardIncompatibilitiesRespected(int vehicle, while (node != chain_end) { const int type = routing_model_.GetVisitType(node); if (type >= 0) { - CHECK_LT(type, num_types); - int& type_count = new_type_counts[type]; + DCHECK_LT(type, previous_type_counts.size()); + int& type_count = gtl::LookupOrInsert(&new_type_counts, type, + previous_type_counts[type]); CHECK_GE(type_count, 1); type_count--; } @@ -811,7 +815,8 @@ bool TypeRegulationsFilter::HardIncompatibilitiesRespected(int vehicle, for (int type : types_to_check) { for (int incompatible_type : routing_model_.GetHardTypeIncompatibilitiesOfType(type)) { - if (new_type_counts[incompatible_type] > 0) { + if (gtl::FindWithDefault(new_type_counts, incompatible_type, + previous_type_counts[incompatible_type]) > 0) { return false; } } @@ -975,7 +980,8 @@ class PathCumulFilter : public BasePathFilter { PathCumulFilter(const RoutingModel& routing_model, const RoutingDimension& dimension, Solver::ObjectiveWatcher objective_callback, - bool propagate_own_objective_value); + bool propagate_own_objective_value, + bool filter_objective_cost); ~PathCumulFilter() override {} std::string DebugString() const override { return "PathCumulFilter(" + name_ + ")"; @@ -1074,7 +1080,8 @@ class PathCumulFilter : public BasePathFilter { } bool FilterBreakCost(int vehicle) const { - return dimension_.VehicleHasBreakIntervals(vehicle); + return dimension_.HasBreakConstraints() && + !dimension_.GetBreakIntervalsOfVehicle(vehicle).empty(); } bool FilterCumulSoftBounds() const { return !cumul_soft_bounds_.empty(); } @@ -1089,15 +1096,17 @@ class PathCumulFilter : public BasePathFilter { // The DimensionCumulOptimizer is used to compute a more precise value of // the cost related to the cumul values (soft bounds and span costs). // Therefore, we only use the optimizer when the costs are actually used to - // filter the solutions, i.e. when we propagate the cost (during local - // search). - // NOTE: The optimizer might eventually catch an infeasibility not caught - // elsewhere in the filter. If that becomes the case, consider removing the - // "CanPropagateObjectiveValue() &&" from the clause. - return CanPropagateObjectiveValue() && - (dimension_.GetSpanCostCoefficientForVehicle(vehicle) > 0) && - !FilterCumulPiecewiseLinearCosts() && - (FilterCumulSoftBounds() || FilterCumulSoftLowerBounds()); + // filter the solutions, i.e. when filter_objective_cost_ is true. + // NOTE: If and when the optimizer is used to filter infeasibilities not + // caught elsewhere in the filter, we should use it regardless of + // filter_objective_cost_. + const bool has_span_cost = + dimension_.GetSpanCostCoefficientForVehicle(vehicle) > 0; + const bool has_soft_lower_bound = FilterCumulSoftLowerBounds(); + const bool has_soft_upper_bound = FilterCumulSoftBounds(); + return filter_objective_cost_ && !FilterCumulPiecewiseLinearCosts() && + (has_span_cost ^ has_soft_lower_bound ? has_soft_upper_bound + : has_span_cost); } int64 GetCumulPiecewiseLinearCost(int64 node, int64 cumul_value) const; @@ -1193,6 +1202,7 @@ class PathCumulFilter : public BasePathFilter { const std::string name_; LocalDimensionCumulOptimizer optimizer_; + const bool filter_objective_cost_; const bool propagate_own_objective_value_; bool lns_detected_; @@ -1201,7 +1211,8 @@ class PathCumulFilter : public BasePathFilter { PathCumulFilter::PathCumulFilter(const RoutingModel& routing_model, const RoutingDimension& dimension, Solver::ObjectiveWatcher objective_callback, - bool propagate_own_objective_value) + bool propagate_own_objective_value, + bool filter_objective_cost) : BasePathFilter(routing_model.Nexts(), dimension.cumuls().size(), std::move(objective_callback)), routing_model_(routing_model), @@ -1227,6 +1238,7 @@ PathCumulFilter::PathCumulFilter(const RoutingModel& routing_model, delta_nodes_with_precedences_and_changed_cumul_(routing_model.Size()), name_(dimension.name()), optimizer_(&dimension), + filter_objective_cost_(filter_objective_cost), propagate_own_objective_value_(propagate_own_objective_value), lns_detected_(false) { for (const int64 upper_bound : vehicle_span_upper_bounds_) { @@ -1889,26 +1901,25 @@ int64 PathCumulFilter::ComputePathMaxStartFromEndCumul( IntVarLocalSearchFilter* MakePathCumulFilter( const RoutingDimension& dimension, Solver::ObjectiveWatcher objective_callback, - bool propagate_own_objective_value) { + bool propagate_own_objective_value, bool filter_objective_cost) { RoutingModel& model = *dimension.model(); return model.solver()->RevAlloc(new PathCumulFilter( - model, dimension, objective_callback, propagate_own_objective_value)); + model, dimension, objective_callback, propagate_own_objective_value, + filter_objective_cost)); } std::vector MakeCumulFilters( const RoutingDimension& dimension, - Solver::ObjectiveWatcher objective_callback, bool filter_objective) { + Solver::ObjectiveWatcher objective_callback, bool filter_objective_cost) { std::vector filters; if (!dimension.GetNodePrecedences().empty() || - (filter_objective && dimension.global_span_cost_coefficient() > 0)) { - IntVarLocalSearchFilter* lp_cumul_filter = - MakeGlobalLPCumulFilter(dimension, objective_callback); + (filter_objective_cost && dimension.global_span_cost_coefficient() > 0)) { + IntVarLocalSearchFilter* lp_cumul_filter = MakeGlobalLPCumulFilter( + dimension, objective_callback, filter_objective_cost); filters.push_back(lp_cumul_filter); - if (filter_objective) { - objective_callback = [lp_cumul_filter](int64 value) { - return lp_cumul_filter->InjectObjectiveValue(value); - }; - } + objective_callback = [lp_cumul_filter](int64 value) { + return lp_cumul_filter->InjectObjectiveValue(value); + }; } // NOTE: We always add the PathCumulFilter to filter each route's feasibility // separately to try and cut bad decisions earlier in the search, but we don't @@ -1917,46 +1928,46 @@ std::vector MakeCumulFilters( for (const int64 upper_bound : dimension.vehicle_span_upper_bounds()) { if (upper_bound != kint64max) { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } } for (const int64 coefficient : dimension.vehicle_span_cost_coefficients()) { if (coefficient != 0) { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } } for (const IntVar* const slack : dimension.slacks()) { if (slack->Min() > 0) { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } } const std::vector& cumuls = dimension.cumuls(); for (int i = 0; i < cumuls.size(); ++i) { if (dimension.HasCumulVarSoftUpperBound(i)) { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } if (dimension.HasCumulVarSoftLowerBound(i)) { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } if (dimension.HasCumulVarPiecewiseLinearCost(i)) { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } IntVar* const cumul_var = cumuls[i]; @@ -1964,14 +1975,15 @@ std::vector MakeCumulFilters( if (!dimension.model()->IsEnd(i)) { filters.push_back( MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + propagate_path_cumul_filter_objective_value, + filter_objective_cost)); return filters; } } if (dimension.forbidden_intervals()[i].NumIntervals() > 0) { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } } @@ -1979,9 +1991,9 @@ std::vector MakeCumulFilters( return {dimension.model()->solver()->RevAlloc(new ChainCumulFilter( *dimension.model(), dimension, objective_callback))}; } else { - filters.push_back( - MakePathCumulFilter(dimension, objective_callback, - propagate_path_cumul_filter_objective_value)); + filters.push_back(MakePathCumulFilter( + dimension, objective_callback, + propagate_path_cumul_filter_objective_value, filter_objective_cost)); return filters; } } @@ -2266,156 +2278,14 @@ IntVarLocalSearchFilter* MakeVehicleVarFilter( return routing_model.solver()->RevAlloc(new VehicleVarFilter(routing_model)); } -namespace { -class VehicleBreaksFilter : public BasePathFilter { - public: - VehicleBreaksFilter(const RoutingModel& routing_model, - const RoutingDimension& dimension); - std::string DebugString() const override { return "VehicleBreaksFilter"; } - bool AcceptPath(int64 path_start, int64 chain_start, - int64 chain_end) override; - - private: - // Handles to model. - const RoutingModel& model_; - const RoutingDimension& dimension_; - // Strong energy-based filtering algorithm. - DisjunctivePropagator disjunctive_propagator_; - DisjunctivePropagator::Tasks tasks_; - // Used to check whether propagation changed a vector. - std::vector old_start_min_; - std::vector old_start_max_; - std::vector old_end_min_; - std::vector old_end_max_; - std::vector start_to_vehicle_; -}; - -VehicleBreaksFilter::VehicleBreaksFilter(const RoutingModel& routing_model, - const RoutingDimension& dimension) - : BasePathFilter(routing_model.Nexts(), - routing_model.Size() + routing_model.vehicles(), nullptr), - model_(routing_model), - dimension_(dimension) { - start_to_vehicle_.resize(Size(), -1); - for (int i = 0; i < routing_model.vehicles(); ++i) { - start_to_vehicle_[routing_model.Start(i)] = i; - } -} - -bool VehicleBreaksFilter::AcceptPath(int64 path_start, int64 chain_start, - int64 chain_end) { - // Translate path and breaks to tasks. - const int vehicle = start_to_vehicle_[path_start]; - if (!dimension_.VehicleHasBreakIntervals(vehicle)) return true; - tasks_.start_min.clear(); - tasks_.start_max.clear(); - tasks_.duration_min.clear(); - tasks_.duration_max.clear(); - tasks_.end_min.clear(); - tasks_.end_max.clear(); - tasks_.is_preemptible.clear(); - tasks_.forbidden_intervals.clear(); - bool has_forbidden_intervals = false; - int64 group_delay = 0LL; - int64 current = path_start; - while (true) { - const int64 cumul_min = dimension_.CumulVar(current)->Min(); - const int64 cumul_max = dimension_.CumulVar(current)->Max(); - // Tasks from visits. Visits start before the group_delay at Cumul(current), - // end at Cumul() + visit_duration. - const bool node_is_last = model_.IsEnd(current); - const int64 visit_duration = - node_is_last - ? 0LL - : dimension_.GetNodeVisitTransitsOfVehicle(vehicle)[current]; - tasks_.start_min.push_back(CapSub(cumul_min, group_delay)); - tasks_.start_max.push_back(CapSub(cumul_max, group_delay)); - tasks_.duration_min.push_back(CapAdd(group_delay, visit_duration)); - tasks_.duration_max.push_back(CapAdd(group_delay, visit_duration)); - tasks_.end_min.push_back(CapAdd(cumul_min, visit_duration)); - tasks_.end_max.push_back(CapAdd(cumul_max, visit_duration)); - tasks_.is_preemptible.push_back(false); - tasks_.forbidden_intervals.push_back( - &(dimension_.forbidden_intervals()[current])); - has_forbidden_intervals |= - tasks_.forbidden_intervals.back()->NumIntervals() > 0; - if (node_is_last) break; - - // Tasks from transits. Transit starts after the visit, - // but before the next group_delay. - const int next = GetNext(current); - group_delay = dimension_.GetGroupDelay(vehicle, current, next); - tasks_.start_min.push_back(CapAdd(cumul_min, visit_duration)); - tasks_.start_max.push_back(CapAdd(cumul_max, visit_duration)); - tasks_.duration_min.push_back( - CapSub(CapSub(dimension_.transit_evaluator(vehicle)(current, next), - visit_duration), - group_delay)); - tasks_.duration_max.push_back(kint64max); - tasks_.end_min.push_back( - CapSub(dimension_.CumulVar(next)->Min(), group_delay)); - tasks_.end_max.push_back( - CapSub(dimension_.CumulVar(next)->Max(), group_delay)); - tasks_.is_preemptible.push_back(true); - tasks_.forbidden_intervals.push_back(nullptr); - current = next; - } - tasks_.num_chain_tasks = tasks_.start_min.size(); - // Add tasks from breaks. - for (IntervalVar* interval : dimension_.GetBreakIntervalsOfVehicle(vehicle)) { - if (!interval->MustBePerformed()) continue; - tasks_.start_min.push_back(interval->StartMin()); - tasks_.start_max.push_back(interval->StartMax()); - tasks_.duration_min.push_back(interval->DurationMin()); - tasks_.duration_max.push_back(interval->DurationMax()); - tasks_.end_min.push_back(interval->EndMin()); - tasks_.end_max.push_back(interval->EndMax()); - tasks_.is_preemptible.push_back(false); - tasks_.forbidden_intervals.push_back(nullptr); - } - // Add side constraints. - if (!has_forbidden_intervals) tasks_.forbidden_intervals.clear(); - tasks_.distance_duration.clear(); - if (dimension_.HasBreakDistanceDurationOfVehicle(vehicle)) { - tasks_.distance_duration = - dimension_.GetBreakDistanceDurationOfVehicle(vehicle); - } - // Reduce bounds until failure or fixed point is reached. - // We set a maximum amount of iterations to avoid slow propagation. - bool is_feasible = true; - int maximum_num_iterations = 8; - while (--maximum_num_iterations >= 0) { - old_start_min_ = tasks_.start_min; - old_start_max_ = tasks_.start_max; - old_end_min_ = tasks_.end_min; - old_end_max_ = tasks_.end_max; - is_feasible = disjunctive_propagator_.Propagate(&tasks_); - if (!is_feasible) break; - // If fixed point reached, stop. - if ((old_start_min_ == tasks_.start_min) && - (old_start_max_ == tasks_.start_max) && - (old_end_min_ == tasks_.end_min) && (old_end_max_ == tasks_.end_max)) { - break; - } - } - return is_feasible; -} - -} // namespace - -IntVarLocalSearchFilter* MakeVehicleBreaksFilter( - const RoutingModel& routing_model, const RoutingDimension& dimension) { - return routing_model.solver()->RevAlloc( - new VehicleBreaksFilter(routing_model, dimension)); -} - namespace { class LPCumulFilter : public IntVarLocalSearchFilter { public: LPCumulFilter(const RoutingModel& routing_model, const RoutingDimension& dimension, - Solver::ObjectiveWatcher objective_callback); + Solver::ObjectiveWatcher objective_callback, + bool filter_objective_cost); bool Accept(Assignment* delta, Assignment* deltadelta) override; int64 GetAcceptedObjectiveValue() const override; void OnSynchronize(const Assignment* delta) override; @@ -2427,6 +2297,7 @@ class LPCumulFilter : public IntVarLocalSearchFilter { private: GlobalDimensionCumulOptimizer optimizer_; IntVar* const objective_; + const bool filter_objective_cost_; int64 synchronized_cost_without_transit_; int64 delta_cost_without_transit_; SparseBitset delta_touched_; @@ -2435,11 +2306,13 @@ class LPCumulFilter : public IntVarLocalSearchFilter { LPCumulFilter::LPCumulFilter(const RoutingModel& routing_model, const RoutingDimension& dimension, - Solver::ObjectiveWatcher objective_callback) + Solver::ObjectiveWatcher objective_callback, + bool filter_objective_cost) : IntVarLocalSearchFilter(routing_model.Nexts(), std::move(objective_callback)), optimizer_(&dimension), objective_(routing_model.CostVar()), + filter_objective_cost_(filter_objective_cost), synchronized_cost_without_transit_(-1), delta_cost_without_transit_(-1), delta_touched_(Size()), @@ -2463,12 +2336,7 @@ bool LPCumulFilter::Accept(Assignment* delta, Assignment* deltadelta) { return delta_touched_[index] ? delta_nexts_[index] : Value(index); }; - int64 objective_max = objective_->Max(); - if (delta->Objective() == objective_) { - objective_max = std::min(objective_max, delta->ObjectiveMax()); - } - - if (!CanPropagateObjectiveValue() && objective_max == kint64max) { + if (!filter_objective_cost_) { // No need to compute the cost of the LP, only verify its feasibility. delta_cost_without_transit_ = -1; return optimizer_.IsFeasible(next_accessor); @@ -2489,6 +2357,11 @@ bool LPCumulFilter::Accept(Assignment* delta, Assignment* deltadelta) { } delta->SetObjectiveMin(new_objective_value); + int64 objective_max = objective_->Max(); + if (delta->Objective() == objective_) { + objective_max = std::min(objective_max, delta->ObjectiveMax()); + } + return new_objective_value <= objective_max; } @@ -2518,10 +2391,10 @@ int64 LPCumulFilter::GetSynchronizedObjectiveValue() const { IntVarLocalSearchFilter* MakeGlobalLPCumulFilter( const RoutingDimension& dimension, - Solver::ObjectiveWatcher objective_callback) { + Solver::ObjectiveWatcher objective_callback, bool filter_objective_cost) { const RoutingModel* model = dimension.model(); - return model->solver()->RevAlloc( - new LPCumulFilter(*model, dimension, std::move(objective_callback))); + return model->solver()->RevAlloc(new LPCumulFilter( + *model, dimension, std::move(objective_callback), filter_objective_cost)); } const int64 CPFeasibilityFilter::kUnassigned = -1;