diff --git a/ortools/routing/constraints.cc b/ortools/routing/constraints.cc index 6496346805..35bbe6d58e 100644 --- a/ortools/routing/constraints.cc +++ b/ortools/routing/constraints.cc @@ -183,9 +183,11 @@ class ResourceAssignmentConstraint : public Constraint { const util_intops::StrongVector> ignored_resources_per_class(resource_group_.GetResourceClassesCount()); std::vector> assignment_costs(model_.vehicles()); + // TODO(user): Adjust the 'solve_duration_ratio' parameter. for (int v : resource_group_.GetVehiclesRequiringAResource()) { if (!ComputeVehicleToResourceClassAssignmentCosts( - v, resource_group_, ignored_resources_per_class, next, transit, + v, /*solve_duration_ratio=*/1.0, resource_group_, + ignored_resources_per_class, next, transit, /*optimize_vehicle_costs*/ false, model_.GetMutableLocalCumulLPOptimizer(dimension), model_.GetMutableLocalCumulMPOptimizer(dimension), diff --git a/ortools/routing/decision_builders.cc b/ortools/routing/decision_builders.cc index 20ecd1ce0e..c08ec2de84 100644 --- a/ortools/routing/decision_builders.cc +++ b/ortools/routing/decision_builders.cc @@ -251,16 +251,24 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { // First look at vehicles that do not need resource assignment (fewer/faster // computations). + // NOTE(user): If it ever becomes an issue, we can consider leaving more + // 'shares' for the resource assignment calls since they're more expensive. + int solve_duration_shares = vehicles_without_resource_assignment.size() + + vehicles_with_resource_assignment.size(); for (int vehicle : vehicles_without_resource_assignment) { solver->TopPeriodicCheck(); std::vector cumul_values; std::vector break_start_end_values; // TODO(user): Distinguish between FEASIBLE and OPTIMAL statuses to - // keep track of the FEASIBLE-only cases. - if (!ComputeCumulAndBreakValuesForVehicle(vehicle, next, &cumul_values, - &break_start_end_values)) { + // keep track of the FEASIBLE-only cases, and resolve the feasible-only + // cases with the remaining time (if any) after all routes have been + // scheduled with the initial 'solve_duration_ratio'. + if (!ComputeCumulAndBreakValuesForVehicle( + vehicle, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + next, &cumul_values, &break_start_end_values)) { return false; } + solve_duration_shares--; AppendRouteCumulAndBreakVarAndValues(dimension_, vehicle, cumul_values, break_start_end_values, &cp_variables_, &cp_values_); @@ -353,7 +361,8 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { } bool ComputeCumulAndBreakValuesForVehicle( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, std::vector* cumul_values, std::vector* break_start_end_values) { cumul_values->clear(); @@ -379,29 +388,29 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { use_mp_optimizer ? mp_optimizer_ : lp_optimizer_; DCHECK_NE(optimizer, nullptr); DimensionSchedulingStatus status = - optimize_and_pack_ - ? optimizer->ComputePackedRouteCumuls( - vehicle, next_accessor, dimension_travel_info, resource, - cumul_values, break_start_end_values) - : optimizer->ComputeRouteCumuls( - vehicle, next_accessor, dimension_travel_info, resource, - cumul_values, break_start_end_values); + optimize_and_pack_ ? optimizer->ComputePackedRouteCumuls( + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values) + : optimizer->ComputeRouteCumuls( + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values); // If relaxation is not feasible, try the MP optimizer. if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { DCHECK(!use_mp_optimizer); DCHECK_NE(mp_optimizer_, nullptr); status = optimize_and_pack_ ? mp_optimizer_->ComputePackedRouteCumuls( - vehicle, next_accessor, dimension_travel_info, - resource, cumul_values, break_start_end_values) + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values) : mp_optimizer_->ComputeRouteCumuls( - vehicle, next_accessor, dimension_travel_info, - resource, cumul_values, break_start_end_values); + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values); } - if (status == DimensionSchedulingStatus::INFEASIBLE) { - return false; - } - return true; + return status != DimensionSchedulingStatus::INFEASIBLE; } bool ComputeVehicleResourceClassValuesAndIndices( @@ -414,17 +423,20 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { if (vehicles_to_assign.empty()) return true; DCHECK_NE(resource_group_, nullptr); + int solve_duration_shares = vehicles_to_assign.size(); for (int v : vehicles_to_assign) { DCHECK(resource_group_->VehicleRequiresAResource(v)); auto& [assignment_costs, cumul_values, break_values] = vehicle_resource_class_values_[v]; if (!ComputeVehicleToResourceClassAssignmentCosts( - v, *resource_group_, used_resources_per_class, next_accessor, + v, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + *resource_group_, used_resources_per_class, next_accessor, dimension_.transit_evaluator(v), /*optimize_vehicle_costs*/ true, lp_optimizer_, mp_optimizer_, &assignment_costs, &cumul_values, &break_values)) { return false; } + solve_duration_shares--; } return ComputeBestVehicleToResourceAssignment( diff --git a/ortools/routing/filters.cc b/ortools/routing/filters.cc index b04b3f1cdf..052f1c3c5f 100644 --- a/ortools/routing/filters.cc +++ b/ortools/routing/filters.cc @@ -1114,7 +1114,7 @@ bool ChainCumulFilter::AcceptPath(int64_t path_start, int64_t chain_start, bool PropagateLightweightVehicleBreaks( int path, DimensionValues& dimension_values, - const std::vector>& interbreaks) { + absl::Span> interbreaks) { using Interval = DimensionValues::Interval; using VehicleBreak = DimensionValues::VehicleBreak; const int64_t total_travel = dimension_values.TravelSums(path).back(); @@ -2001,6 +2001,9 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, if (may_use_optimizers_ && lp_optimizer_ != nullptr && accepted_objective_value_ <= objective_max) { std::vector paths_requiring_mp_optimizer; + // TODO(user): Further optimize the LPs when we find feasible-only + // solutions with the original time shares, if there's time left in the end. + int solve_duration_shares = dimension_values_.ChangedPaths().size(); for (const int vehicle : dimension_values_.ChangedPaths()) { if (!FilterWithDimensionCumulOptimizerForVehicle(vehicle)) { continue; @@ -2008,13 +2011,17 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, int64_t path_cost_with_lp = 0; const DimensionSchedulingStatus status = lp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, /*resource=*/nullptr, + vehicle, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + path_accessor_, /*resource=*/nullptr, filter_objective_cost_ ? &path_cost_with_lp : nullptr); + solve_duration_shares--; if (status == DimensionSchedulingStatus::INFEASIBLE) { return false; } // Replace previous path cost with the LP optimizer cost. if (filter_objective_cost_ && + (status == DimensionSchedulingStatus::OPTIMAL || + status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) && path_cost_with_lp > cost_of_path_.Get(vehicle)) { CapSubFrom(cost_of_path_.Get(vehicle), &accepted_objective_value_); CapAddTo(path_cost_with_lp, &accepted_objective_value_); @@ -2033,15 +2040,20 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, DCHECK_LE(accepted_objective_value_, objective_max); + solve_duration_shares = paths_requiring_mp_optimizer.size(); for (const int vehicle : paths_requiring_mp_optimizer) { int64_t path_cost_with_mp = 0; - if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, /*resource=*/nullptr, - filter_objective_cost_ ? &path_cost_with_mp : nullptr) == - DimensionSchedulingStatus::INFEASIBLE) { + const DimensionSchedulingStatus status = + mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( + vehicle, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + path_accessor_, /*resource=*/nullptr, + filter_objective_cost_ ? &path_cost_with_mp : nullptr); + solve_duration_shares--; + if (status == DimensionSchedulingStatus::INFEASIBLE) { return false; } if (filter_objective_cost_ && + status == DimensionSchedulingStatus::OPTIMAL && path_cost_with_mp > cost_of_path_.Get(vehicle)) { CapSubFrom(cost_of_path_.Get(vehicle), &accepted_objective_value_); CapAddTo(path_cost_with_mp, &accepted_objective_value_); @@ -2994,9 +3006,10 @@ bool ResourceGroupAssignmentFilter::FinalizeAcceptPath( continue; } if (!ComputeVehicleToResourceClassAssignmentCosts( - vehicle, resource_group_, ignored_resources_per_class_, - next_accessor, dimension_.transit_evaluator(vehicle), - filter_objective_cost_, lp_optimizer_, mp_optimizer_, + vehicle, /*solve_duration_ratio=*/1.0, resource_group_, + ignored_resources_per_class_, next_accessor, + dimension_.transit_evaluator(vehicle), filter_objective_cost_, + lp_optimizer_, mp_optimizer_, &delta_vehicle_to_resource_class_assignment_costs_[vehicle], nullptr, nullptr)) { return false; @@ -3068,8 +3081,10 @@ void ResourceGroupAssignmentFilter::OnSynchronizePathFromStart(int64_t start) { // vehicle requiring resource assignment to keep track of whether or not a // given vehicle-to-resource-class assignment is possible by storing 0 or -1 // in vehicle_to_resource_class_assignment_costs_. + // TODO(user): Adjust the 'solve_duration_ratio' below. if (!ComputeVehicleToResourceClassAssignmentCosts( - v, resource_group_, ignored_resources_per_class_, next_accessor, + v, /*solve_duration_ratio=*/1.0, resource_group_, + ignored_resources_per_class_, next_accessor, dimension_.transit_evaluator(v), filter_objective_cost_, lp_optimizer_, mp_optimizer_, &vehicle_to_resource_class_assignment_costs_[v], nullptr, nullptr)) { @@ -3152,14 +3167,14 @@ ResourceGroupAssignmentFilter::ComputeRouteCumulCostWithoutResourceAssignment( int64_t route_cost = 0; const DimensionSchedulingStatus status = lp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, next_accessor, resource, + vehicle, /*solve_duration_ratio=*/1.0, next_accessor, resource, filter_objective_cost_ ? &route_cost : nullptr); switch (status) { case DimensionSchedulingStatus::INFEASIBLE: return -1; case DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY: if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, next_accessor, resource, + vehicle, /*solve_duration_ratio=*/1.0, next_accessor, resource, filter_objective_cost_ ? &route_cost : nullptr) == DimensionSchedulingStatus::INFEASIBLE) { return -1; diff --git a/ortools/routing/filters.h b/ortools/routing/filters.h index e47f0de5dd..31acef2a8b 100644 --- a/ortools/routing/filters.h +++ b/ortools/routing/filters.h @@ -524,7 +524,7 @@ class DimensionValues { // This applies light reasoning, and runs in O(#breaks * #interbreak rules). bool PropagateLightweightVehicleBreaks( int path, DimensionValues& dimension_values, - const std::vector>& interbreaks); + absl::Span> interbreaks); /// Returns a filter tracking route constraints. IntVarLocalSearchFilter* MakeRouteConstraintFilter( diff --git a/ortools/routing/flow.cc b/ortools/routing/flow.cc index 01efa7c71c..a0ea1f297b 100644 --- a/ortools/routing/flow.cc +++ b/ortools/routing/flow.cc @@ -287,7 +287,7 @@ bool RoutingModel::SolveMatchingModel( // TODO(user): if the result is RELAXED_OPTIMAL_ONLY, do a // second pass with an MP solver. if (optimizer.ComputeRouteCumulCostWithoutFixedTransits( - vehicle, + vehicle, /*solve_duration_ratio=*/1.0, [&nexts](int64_t node) { return nexts.find(node)->second; }, @@ -312,7 +312,7 @@ bool RoutingModel::SolveMatchingModel( // TODO(user): if the result is RELAXED_OPTIMAL_ONLY, do a // second pass with an MP solver. if (optimizer.ComputeRouteCumulCostWithoutFixedTransits( - vehicle, + vehicle, /*solve_duration_ratio=*/1.0, [&nexts](int64_t node) { return nexts.find(node)->second; }, diff --git a/ortools/routing/lp_scheduling.cc b/ortools/routing/lp_scheduling.cc index aa6019d8ef..d85f6e4b25 100644 --- a/ortools/routing/lp_scheduling.cc +++ b/ortools/routing/lp_scheduling.cc @@ -212,12 +212,15 @@ LocalDimensionCumulOptimizer::LocalDimensionCumulOptimizer( } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCost( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, int64_t* optimal_cost) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); int64_t transit_cost = 0; const DimensionSchedulingStatus status = optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, + vehicle, solve_duration_ratio, next_accessor, /*dimension_travel_info=*/nullptr, /*resource=*/nullptr, /*optimize_vehicle_costs=*/optimal_cost != nullptr, @@ -233,11 +236,15 @@ DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCost( DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::ResourceGroup::Resource* resource, int64_t* optimal_cost_without_transits) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, /*dimension_travel_info=*/nullptr, resource, + vehicle, solve_duration_ratio, next_accessor, + /*dimension_travel_info=*/nullptr, resource, /*optimize_vehicle_costs=*/optimal_cost_without_transits != nullptr, solver_[vehicle].get(), /*cumul_values=*/nullptr, /*break_values=*/nullptr, optimal_cost_without_transits, nullptr); @@ -245,69 +252,85 @@ LocalDimensionCumulOptimizer::ComputeRouteCumulCostWithoutFixedTransits( std::vector LocalDimensionCumulOptimizer:: ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, absl::Span resources, absl::Span resource_indices, bool optimize_vehicle_costs, std::vector* optimal_costs_without_transits, std::vector>* optimal_cumuls, std::vector>* optimal_breaks) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResources( - vehicle, next_accessor, transit_accessor, nullptr, resources, - resource_indices, optimize_vehicle_costs, solver_[vehicle].get(), - optimal_cumuls, optimal_breaks, optimal_costs_without_transits, nullptr); + vehicle, solve_duration_ratio, next_accessor, transit_accessor, nullptr, + resources, resource_indices, optimize_vehicle_costs, + solver_[vehicle].get(), optimal_cumuls, optimal_breaks, + optimal_costs_without_transits, nullptr); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* optimal_cumuls, std::vector* optimal_breaks) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, dimension_travel_info, resource, - /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), optimal_cumuls, - optimal_breaks, /*cost_without_transit=*/nullptr, + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + resource, /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), + optimal_cumuls, optimal_breaks, /*cost_without_transit=*/nullptr, /*transit_cost=*/nullptr); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulsAndCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, std::vector* optimal_cumuls, std::vector* optimal_breaks, int64_t* optimal_cost_without_transits) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, dimension_travel_info, nullptr, - /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), optimal_cumuls, - optimal_breaks, optimal_cost_without_transits, nullptr); + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + nullptr, /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), + optimal_cumuls, optimal_breaks, optimal_cost_without_transits, nullptr); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, absl::Span solution_cumul_values, absl::Span solution_break_values, int64_t* solution_cost, int64_t* cost_offset, bool reuse_previous_model_if_possible, bool clear_lp, absl::Duration* solve_duration) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); RoutingLinearSolverWrapper* solver = solver_[vehicle].get(); return optimizer_core_.ComputeSingleRouteSolutionCostWithoutFixedTransits( - vehicle, next_accessor, dimension_travel_info, solver, - solution_cumul_values, solution_break_values, solution_cost, cost_offset, - reuse_previous_model_if_possible, clear_lp, + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + solver, solution_cumul_values, solution_break_values, solution_cost, + cost_offset, reuse_previous_model_if_possible, clear_lp, /*clear_solution_constraints=*/true, solve_duration); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputePackedRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* packed_cumuls, std::vector* packed_breaks) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeAndPackSingleRoute( - vehicle, next_accessor, dimension_travel_info, resource, - solver_[vehicle].get(), packed_cumuls, packed_breaks); + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + resource, solver_[vehicle].get(), packed_cumuls, packed_breaks); } const int CumulBoundsPropagator::kNoParent = -2; @@ -578,7 +601,8 @@ DimensionCumulOptimizerCore::DimensionCumulOptimizerCore( DimensionSchedulingStatus DimensionCumulOptimizerCore::ComputeSingleRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, RoutingLinearSolverWrapper* solver, absl::Span solution_cumul_values, @@ -608,7 +632,7 @@ DimensionCumulOptimizerCore::ComputeSingleRouteSolutionCostWithoutFixedTransits( if (model->CheckLimit()) { return DimensionSchedulingStatus::INFEASIBLE; } - solve_duration_value = model->RemainingTime(); + solve_duration_value = model->RemainingTime() * solve_duration_ratio; if (solve_duration != nullptr) *solve_duration = solve_duration_value; if (cost_offset != nullptr) *cost_offset = cost_offset_value; } else { @@ -617,7 +641,7 @@ DimensionCumulOptimizerCore::ComputeSingleRouteSolutionCostWithoutFixedTransits( cost_offset_value = *cost_offset; CHECK(solve_duration != nullptr) << "Cannot reuse model without the solve_duration"; - solve_duration_value = *solve_duration; + solve_duration_value = *solve_duration * solve_duration_ratio; } // Constrains the cumuls. @@ -689,7 +713,8 @@ void ClearIfNonNull(std::vector* v) { DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeSingleRouteWithResource( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, bool optimize_vehicle_costs, RoutingLinearSolverWrapper* solver, @@ -709,9 +734,9 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResource( std::vector> break_values_vec; const std::vector statuses = DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( - vehicle, next_accessor, dimension_->transit_evaluator(vehicle), - dimension_travel_info, resources, resource_indices, - optimize_vehicle_costs, solver, + vehicle, solve_duration_ratio, next_accessor, + dimension_->transit_evaluator(vehicle), dimension_travel_info, + resources, resource_indices, optimize_vehicle_costs, solver, cumul_values != nullptr ? &cumul_values_vec : nullptr, break_values != nullptr ? &break_values_vec : nullptr, cost_without_transits != nullptr ? &costs_without_transits : nullptr, @@ -827,7 +852,8 @@ bool TightenStartEndVariableBoundsWithAssignedResources( std::vector DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, const RouteDimensionTravelInfo* dimension_travel_info, absl::Span resources, @@ -870,9 +896,9 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( // NOTE: When there are no resources to optimize for, we still solve the // optimization problem for the route (without any added resource constraint). - const int num_solves = - std::max(static_cast(1UL), - resource_indices.size()); + // TODO(user): Consider taking 'num_solves' into account to re-adjust the + // solve_duration_ratio to make sure all sub-problems are given enough time. + const int num_solves = resource_indices.empty() ? 1 : resource_indices.size(); if (costs_without_transits != nullptr) { costs_without_transits->assign(num_solves, -1); } @@ -903,7 +929,8 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( continue; } - statuses.push_back(solver->Solve(model->RemainingTime())); + statuses.push_back( + solver->Solve(model->RemainingTime() * solve_duration_ratio)); if (statuses.back() == DimensionSchedulingStatus::INFEASIBLE) { continue; } @@ -1053,7 +1080,8 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack( std::iota(vehicles.begin(), vehicles.end(), 0); // Subtle: Even if the status was RELAXED_OPTIMAL_ONLY we try to pack just // in case packing manages to make the solution completely feasible. - status = PackRoutes(std::move(vehicles), solver, packing_parameters); + status = PackRoutes(std::move(vehicles), /*solve_duration_ratio=*/1.0, + solver, packing_parameters); } if (!solver->IsCPSATSolver()) { solver->SetParameters(original_params.SerializeAsString()); @@ -1073,7 +1101,8 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack( DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, RoutingLinearSolverWrapper* solver, std::vector* cumul_values, @@ -1086,15 +1115,20 @@ DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( packing_parameters.set_use_preprocessing(true); solver->SetParameters(packing_parameters.SerializeAsString()); } + // TODO(user): Since there are 3 separate solves for packing, divide the + // input solve_duration_ratio by 3 before passing to the below functions? Or + // maybe divide by 2 and let PackRoutes() divide by 2 itself (since the 2 + // solves in PackRoutes() should start with a very good first solution hint). DimensionSchedulingStatus status = OptimizeSingleRouteWithResource( - vehicle, next_accessor, dimension_travel_info, resource, - /*optimize_vehicle_costs=*/true, solver, + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + resource, /*optimize_vehicle_costs=*/true, solver, /*cumul_values=*/nullptr, /*break_values=*/nullptr, /*cost_without_transit=*/nullptr, /*transit_cost=*/nullptr, /*clear_lp=*/false); if (status != DimensionSchedulingStatus::INFEASIBLE) { - status = PackRoutes({vehicle}, solver, packing_parameters); + status = + PackRoutes({vehicle}, solve_duration_ratio, solver, packing_parameters); } if (!solver->IsCPSATSolver()) { solver->SetParameters(original_params.SerializeAsString()); @@ -1114,7 +1148,8 @@ DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( } DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( - std::vector vehicles, RoutingLinearSolverWrapper* solver, + std::vector vehicles, double solve_duration_ratio, + RoutingLinearSolverWrapper* solver, const glop::GlopParameters& packing_parameters) { const RoutingModel* model = dimension_->model(); @@ -1138,15 +1173,16 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( } glop::GlopParameters current_params; - const auto retry_solving = [¤t_params, model, solver]() { + const auto retry_solving = [¤t_params, model, solver, + solve_duration_ratio]() { // NOTE: To bypass some cases of false negatives due to imprecisions, we try // running Glop with a different use_dual_simplex parameter when running // into an infeasible status. current_params.set_use_dual_simplex(!current_params.use_dual_simplex()); solver->SetParameters(current_params.SerializeAsString()); - return solver->Solve(model->RemainingTime()); + return solver->Solve(model->RemainingTime() * solve_duration_ratio); }; - if (solver->Solve(model->RemainingTime()) == + if (solver->Solve(model->RemainingTime() * solve_duration_ratio) == DimensionSchedulingStatus::INFEASIBLE) { if (solver->IsCPSATSolver()) { return DimensionSchedulingStatus::INFEASIBLE; @@ -1173,7 +1209,8 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( index_to_cumul_variable_[model->Start(vehicle)], -1); } - DimensionSchedulingStatus status = solver->Solve(model->RemainingTime()); + DimensionSchedulingStatus status = + solver->Solve(model->RemainingTime() * solve_duration_ratio); if (!solver->IsCPSATSolver() && status == DimensionSchedulingStatus::INFEASIBLE) { status = retry_solving(); @@ -2147,77 +2184,96 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( } } - // TODO(user): find why adding these constraints make CPSAT slower. - if (!solver->IsCPSATSolver()) { - for (const auto& [limit, min_break_duration] : - dimension_->GetBreakDistanceDurationOfVehicle(vehicle)) { - int64_t min_num_breaks = 0; - if (limit > 0) { - min_num_breaks = - std::max(0, CapSub(total_fixed_transit, 1) / limit); - } - if (CapSub(current_route_min_cumuls_.back(), - current_route_max_cumuls_.front()) > limit) { - min_num_breaks = std::max(min_num_breaks, 1); - } - if (num_breaks < min_num_breaks) return false; - if (min_num_breaks == 0) continue; + for (const auto& [limit, min_break_duration] : + dimension_->GetBreakDistanceDurationOfVehicle(vehicle)) { + int64_t min_num_breaks = 0; + if (limit > 0) { + min_num_breaks = + std::max(0, CapSub(total_fixed_transit, 1) / limit); + } + if (CapSub(current_route_min_cumuls_.back(), + current_route_max_cumuls_.front()) > limit) { + min_num_breaks = std::max(min_num_breaks, 1); + } + if (num_breaks < min_num_breaks) return false; + if (min_num_breaks == 0) continue; - // Adds an LP relaxation of interbreak constraints. - // For all 0 <= pl < pr < path_size, for k > 0, - // if sum_{p in [pl, pr)} fixed_transit[p] > k * limit, - // then sum_{p in [pl, pr)} slack[p] >= k * min_break_duration. - // - // Moreover, if end_min[pr] - start_max[pl] > limit, - // the sum_{p in [pl, pr)} slack[p] >= min_break_duration. - // - // We want to apply the constraints above, without the ones that are - // dominated: - // - do not add the same constraint for k' < k, keep the largest k. - // - do not add the constraint for both (pl', pr') and (pl, pr) - // if [pl', pr') is a subset of [pl, pr), keep the smallest interval. - // TODO(user): reduce the number of constraints further; - // for instance if the constraint holds for (k, pl, pr) and (k', pl', pr') - // with pr <= pr', then no need to add the constraint for (k+k', pl, pr'). - // - // We need fast access to sum_{p in [pl, pr)} fixed_transit[p]. - // This will be sum_transits[pr] - sum_transits[pl]. Note that - // sum_transits[0] = 0, sum_transits[path_size-1] = total_fixed_transit. - std::vector sum_transits(path_size); - { - sum_transits[0] = 0; - for (int pos = 1; pos < path_size; ++pos) { - sum_transits[pos] = sum_transits[pos - 1] + fixed_transit[pos - 1]; - } + // Adds an LP relaxation of interbreak constraints. + // For all 0 <= pl < pr < path_size, for k > 0, + // if sum_{p in [pl, pr)} fixed_transit[p] > k * limit, + // then sum_{p in [pl, pr)} slack[p] >= k * min_break_duration. + // + // Moreover, if end_min[pr] - start_max[pl] > limit, + // the sum_{p in [pl, pr)} slack[p] >= min_break_duration. + // + // We want to apply the constraints above, without the ones that are + // dominated: + // - do not add the same constraint for k' < k, keep the largest k. + // - do not add the constraint for both (pl', pr') and (pl, pr) + // if [pl', pr') is a subset of [pl, pr), keep the smallest interval. + // TODO(user): reduce the number of constraints further; + // for instance if the constraint holds for (k, pl, pr) and (k', pl', pr') + // with pr <= pr', then no need to add the constraint for (k+k', pl, pr'). + // + // We need fast access to sum_{p in [pl, pr)} fixed_transit[p]. + // This will be sum_transits[pr] - sum_transits[pl]. Note that + // sum_transits[0] = 0, sum_transits[path_size-1] = total_fixed_transit. + std::vector sum_transits(path_size); + { + sum_transits[0] = 0; + for (int pos = 1; pos < path_size; ++pos) { + sum_transits[pos] = sum_transits[pos - 1] + fixed_transit[pos - 1]; } - // To add the slack sum constraints, we need slack sum variables. - // Those are created lazily in a sparse vector, then only those useful - // variables are linked to slack variables after slack sum constraints - // have been added. - std::vector slack_sum_vars(path_size, -1); - // Given a number of breaks k, an interval of path positions [pl, pr), - // returns true if the interbreak constraint triggers for k breaks. - // TODO(user): find tighter end_min/start_max conditions. - // Mind that a break may be longer than min_break_duration. - auto trigger = [&](int k, int pl, int pr) -> bool { - if (k == 1) { - const int64_t span_lb = - current_route_min_cumuls_[pr] - current_route_max_cumuls_[pl]; - if (span_lb > limit) return true; - } - return sum_transits[pr] - sum_transits[pl] > k * limit; - }; - int min_sum_var_index = path_size; - int max_sum_var_index = -1; - for (int k = 1; k <= min_num_breaks; ++k) { - int pr = 0; - for (int pl = 0; pl < path_size - 1; ++pl) { - pr = std::max(pr, pl + 1); - // Increase pr until transit(pl, pr) > k * limit. - while (pr < path_size && !trigger(k, pl, pr)) ++pr; - if (pr == path_size) break; - // Reduce [pl, pr) from the left. - while (pl < pr && trigger(k, pl + 1, pr)) ++pl; + } + // To add the slack sum constraints, we need slack sum variables. + // Those are created lazily in a sparse vector, then only those useful + // variables are linked to slack variables after slack sum constraints + // have been added. + std::vector slack_sum_vars(path_size, -1); + // Given a number of breaks k, an interval of path positions [pl, pr), + // returns true if the interbreak constraint triggers for k breaks. + // TODO(user): find tighter end_min/start_max conditions. + // Mind that a break may be longer than min_break_duration. + auto trigger = [&](int k, int pl, int pr) -> bool { + const int64_t r_pre_travel = pr < path_size - 1 ? pre_travel[pr] : 0; + const int64_t l_post_travel = pl >= 1 ? post_travel[pl - 1] : 0; + const int64_t extra_travel = CapAdd(r_pre_travel, l_post_travel); + if (k == 1) { + const int64_t span_lb = CapAdd(CapSub(current_route_min_cumuls_[pr], + current_route_max_cumuls_[pl]), + extra_travel); + if (span_lb > limit) return true; + } + return CapAdd(CapSub(sum_transits[pr], sum_transits[pl]), extra_travel) > + CapProd(k, limit); + }; + int min_sum_var_index = path_size; + int max_sum_var_index = -1; + for (int k = 1; k <= min_num_breaks; ++k) { + int pr = 0; + for (int pl = 0; pl < path_size - 1; ++pl) { + pr = std::max(pr, pl + 1); + // Increase pr until transit(pl, pr) > k * limit. + while (pr < path_size && !trigger(k, pl, pr)) ++pr; + if (pr == path_size) break; + // Reduce [pl, pr) from the left. + while (pl < pr && trigger(k, pl + 1, pr)) ++pl; + // If k is the largest for this interval, add the constraint. + // The call trigger(k', pl, pr) may hold for both k and k+1 with an + // irreducible interval [pl, pr] when there is a transit > limit at + // the beginning and at the end of the sub-route. + if (k < min_num_breaks && trigger(k + 1, pl, pr)) continue; + // If the solver is CPSAT, experimentation showed that adding slack sum + // constraints made solves worse than doing nothing, and that adding + // these lightweight constraints works better. + if (solver->IsCPSATSolver()) { + // lp_cumuls[pl] + transit(pl, pr) + k * min_break_duration <= + // lp_cumuls[pr]. + solver->AddLinearConstraint( + CapAdd(CapSub(sum_transits[pr], sum_transits[pl]), + CapProd(k, min_break_duration)), + kint64max, {{lp_cumuls[pr], 1}, {lp_cumuls[pl], -1}}); + } else { if (slack_sum_vars[pl] == -1) { slack_sum_vars[pl] = solver->CreateNewPositiveVariable(); min_sum_var_index = std::min(min_sum_var_index, pl); @@ -2226,32 +2282,26 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( slack_sum_vars[pr] = solver->CreateNewPositiveVariable(); max_sum_var_index = std::max(max_sum_var_index, pr); } - // If k is the largest for this interval, add the constraint. - // The call trigger(k', pl, pr) may hold for both k and k+1 with an - // irreducible interval [pl, pr] when there is a transit > limit at - // the beginning and at the end of the sub-route. sum_slacks[pr] - - // sum_slacks[pl] >= k * min_break_duration. - if (k < min_num_breaks && trigger(k + 1, pl, pr)) continue; + // sum_slacks[pr] - sum_slacks[pl] >= k * min_break_duration. solver->AddLinearConstraint( - k * min_break_duration, kint64max, + CapProd(k, min_break_duration), kint64max, {{slack_sum_vars[pr], 1}, {slack_sum_vars[pl], -1}}); } } - if (min_sum_var_index < max_sum_var_index) { - slack_sum_vars[min_sum_var_index] = solver->AddVariable(0, 0); - int prev_index = min_sum_var_index; - for (int pos = min_sum_var_index + 1; pos <= max_sum_var_index; ++pos) { - if (slack_sum_vars[pos] == -1) continue; - // slack_sum_var[pos] = - // slack_sum_var[prev_index] + sum_{p in [prev_index, pos)} slack[p]. - const int ct = solver->AddLinearConstraint( - 0, 0, - {{slack_sum_vars[pos], 1}, {slack_sum_vars[prev_index], -1}}); - for (int p = prev_index; p < pos; ++p) { - solver->SetCoefficient(ct, lp_slacks[p], -1); - } - prev_index = pos; + } + if (min_sum_var_index < max_sum_var_index) { + slack_sum_vars[min_sum_var_index] = solver->AddVariable(0, 0); + int prev_index = min_sum_var_index; + for (int pos = min_sum_var_index + 1; pos <= max_sum_var_index; ++pos) { + if (slack_sum_vars[pos] == -1) continue; + // slack_sum_var[pos] = + // slack_sum_var[prev_index] + sum_{p in [prev_index, pos)} slack[p]. + const int ct = solver->AddLinearConstraint( + 0, 0, {{slack_sum_vars[pos], 1}, {slack_sum_vars[prev_index], -1}}); + for (int p = prev_index; p < pos; ++p) { + solver->SetCoefficient(ct, lp_slacks[p], -1); } + prev_index = pos; } } } @@ -2779,7 +2829,8 @@ void MoveValuesToIndicesFrom(std::vector* out_values, } // namespace bool ComputeVehicleToResourceClassAssignmentCosts( - int v, const RoutingModel::ResourceGroup& resource_group, + int v, double solve_duration_ratio, + const RoutingModel::ResourceGroup& resource_group, const util_intops::StrongVector>& ignored_resources_per_class, @@ -2790,6 +2841,8 @@ bool ComputeVehicleToResourceClassAssignmentCosts( std::vector* assignment_costs, std::vector>* cumul_values, std::vector>* break_values) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); DCHECK(lp_optimizer != nullptr); DCHECK(mp_optimizer != nullptr); DCHECK_NE(assignment_costs, nullptr); @@ -2853,7 +2906,7 @@ bool ComputeVehicleToResourceClassAssignmentCosts( std::vector> considered_break_values; std::vector statuses = optimizer->ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - v, next_accessor, transit_accessor, resources, + v, solve_duration_ratio, next_accessor, transit_accessor, resources, considered_resource_indices, optimize_vehicle_costs, &considered_assignment_costs, cumul_values == nullptr ? nullptr : &considered_cumul_values, @@ -2904,8 +2957,8 @@ bool ComputeVehicleToResourceClassAssignmentCosts( std::vector> mp_cumul_values; std::vector> mp_break_values; mp_optimizer->ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - v, next_accessor, transit_accessor, resources, mp_resource_indices, - optimize_vehicle_costs, &mp_assignment_costs, + v, solve_duration_ratio, next_accessor, transit_accessor, resources, + mp_resource_indices, optimize_vehicle_costs, &mp_assignment_costs, cumul_values == nullptr ? nullptr : &mp_cumul_values, break_values == nullptr ? nullptr : &mp_break_values); if (!mp_resource_indices.empty() && mp_assignment_costs.empty()) { diff --git a/ortools/routing/lp_scheduling.h b/ortools/routing/lp_scheduling.h index 17e29b90f9..23d57bef2b 100644 --- a/ortools/routing/lp_scheduling.h +++ b/ortools/routing/lp_scheduling.h @@ -706,7 +706,8 @@ class DimensionCumulOptimizerCore { // Finds an optimal (or just feasible) solution for the route for the given // resource. If the resource is null, it is simply ignored. DimensionSchedulingStatus OptimizeSingleRouteWithResource( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const Resource* resource, bool optimize_vehicle_costs, RoutingLinearSolverWrapper* solver, std::vector* cumul_values, @@ -717,7 +718,8 @@ class DimensionCumulOptimizerCore { // same model as in OptimizeSingleRouteWithResource() with the addition of // constraints for cumuls and breaks. DimensionSchedulingStatus ComputeSingleRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, RoutingLinearSolverWrapper* solver, absl::Span solution_cumul_values, @@ -732,7 +734,8 @@ class DimensionCumulOptimizerCore { // If both 'resources' and 'resource_indices' are empty, computes the optimal // solution for the route itself (without added resource constraints). std::vector OptimizeSingleRouteWithResources( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, const RouteDimensionTravelInfo* dimension_travel_info, absl::Span resources, @@ -766,7 +769,8 @@ class DimensionCumulOptimizerCore { std::vector* break_values); DimensionSchedulingStatus OptimizeAndPackSingleRoute( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const Resource* resource, RoutingLinearSolverWrapper* solver, std::vector* cumul_values, std::vector* break_values); @@ -841,7 +845,8 @@ class DimensionCumulOptimizerCore { // ends' cumuls, and then maximizes the starts' cumuls without increasing the // ends. DimensionSchedulingStatus PackRoutes( - std::vector vehicles, RoutingLinearSolverWrapper* solver, + std::vector vehicles, double solve_duration_ratio, + RoutingLinearSolverWrapper* solver, const glop::GlopParameters& packing_parameters); std::unique_ptr propagator_; @@ -897,19 +902,22 @@ class LocalDimensionCumulOptimizer { // and stores it in "optimal_cost" (if not null). // Returns true iff the route respects all constraints. DimensionSchedulingStatus ComputeRouteCumulCost( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, int64_t* optimal_cost); // Same as ComputeRouteCumulCost, but the cost computed does not contain // the part of the vehicle span cost due to fixed transits. DimensionSchedulingStatus ComputeRouteCumulCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::ResourceGroup::Resource* resource, int64_t* optimal_cost_without_transits); std::vector ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, absl::Span resources, absl::Span resource_indices, bool optimize_vehicle_costs, @@ -923,7 +931,8 @@ class LocalDimensionCumulOptimizer { // (if not null), and optimal_breaks, and returns true. // Returns false if the route is not feasible. DimensionSchedulingStatus ComputeRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* optimal_cumuls, @@ -932,7 +941,8 @@ class LocalDimensionCumulOptimizer { // Simple combination of ComputeRouteCumulCostWithoutFixedTransits() and // ComputeRouteCumuls(). DimensionSchedulingStatus ComputeRouteCumulsAndCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, std::vector* optimal_cumuls, std::vector* optimal_breaks, @@ -941,7 +951,8 @@ class LocalDimensionCumulOptimizer { // If feasible, computes the cost of a given route performed by a vehicle // defined by its cumuls and breaks. DimensionSchedulingStatus ComputeRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, absl::Span solution_cumul_values, absl::Span solution_break_values, int64_t* solution_cost, @@ -955,7 +966,8 @@ class LocalDimensionCumulOptimizer { // If 'resource' is non-null, the packed route must also respect its start/end // time window. DimensionSchedulingStatus ComputePackedRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* packed_cumuls, std::vector* packed_breaks); @@ -1056,7 +1068,8 @@ int64_t ComputeBestVehicleToResourceAssignment( // The cumul and break values corresponding to the assignment of each resource // are also set in cumul_values and break_values, if non-null. bool ComputeVehicleToResourceClassAssignmentCosts( - int v, const RoutingModel::ResourceGroup& resource_group, + int v, double solve_duration_ratio, + const RoutingModel::ResourceGroup& resource_group, const util_intops::StrongVector>& ignored_resources_per_class,