Export constraint_solver to github

This commit is contained in:
Corentin Le Molgat
2021-08-23 16:08:30 +02:00
parent 6165673203
commit 6a99deb2a3
15 changed files with 1340 additions and 319 deletions

View File

@@ -3479,7 +3479,8 @@ LocalSearchFilter* MakePathStateFilter(Solver* solver,
// - Synchronize() must be called before.
// - Revert() must be called before.
LocalSearchFilter* MakeUnaryDimensionFilter(
Solver* solver, std::unique_ptr<UnaryDimensionChecker> checker);
Solver* solver, std::unique_ptr<UnaryDimensionChecker> checker,
const std::string& dimension_name);
#endif // !defined(SWIG)

View File

@@ -3182,9 +3182,11 @@ namespace {
class UnaryDimensionFilter : public LocalSearchFilter {
public:
std::string DebugString() const override { return "UnaryDimensionFilter"; }
explicit UnaryDimensionFilter(std::unique_ptr<UnaryDimensionChecker> checker)
: checker_(std::move(checker)) {}
std::string DebugString() const override { return name_; }
UnaryDimensionFilter(std::unique_ptr<UnaryDimensionChecker> checker,
const std::string& dimension_name)
: checker_(std::move(checker)),
name_(absl::StrCat("UnaryDimensionFilter(", dimension_name, ")")) {}
bool Accept(const Assignment* delta, const Assignment* deltadelta,
int64_t objective_min, int64_t objective_max) override {
@@ -3198,13 +3200,16 @@ class UnaryDimensionFilter : public LocalSearchFilter {
private:
std::unique_ptr<UnaryDimensionChecker> checker_;
const std::string name_;
};
} // namespace
LocalSearchFilter* MakeUnaryDimensionFilter(
Solver* solver, std::unique_ptr<UnaryDimensionChecker> checker) {
UnaryDimensionFilter* filter = new UnaryDimensionFilter(std::move(checker));
Solver* solver, std::unique_ptr<UnaryDimensionChecker> checker,
const std::string& dimension_name) {
UnaryDimensionFilter* filter =
new UnaryDimensionFilter(std::move(checker), dimension_name);
return solver->RevAlloc(filter);
}

View File

@@ -100,6 +100,8 @@ class TwoOpt;
namespace operations_research {
namespace {
using ResourceGroup = RoutingModel::ResourceGroup;
// A decision builder which tries to assign values to variables as close as
// possible to target values first.
// TODO(user): Move to CP solver.
@@ -338,8 +340,11 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder {
SetCumulsFromGlobalDimensionCosts(
const std::vector<std::unique_ptr<GlobalDimensionCumulOptimizer>>*
global_optimizers,
const std::vector<std::unique_ptr<GlobalDimensionCumulOptimizer>>*
global_mp_optimizers,
SearchMonitor* monitor, bool optimize_and_pack = false)
: global_optimizers_(*global_optimizers),
global_mp_optimizers_(*global_mp_optimizers),
monitor_(monitor),
optimize_and_pack_(optimize_and_pack) {}
Decision* Next(Solver* const solver) override {
@@ -347,7 +352,8 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder {
// order to postpone the Fail() call until after the for loop, so there are
// no memory leaks related to the cumul_values vector.
bool should_fail = false;
for (const auto& global_optimizer : global_optimizers_) {
for (int i = 0; i < global_optimizers_.size(); ++i) {
const auto& global_optimizer = global_optimizers_[i];
const RoutingDimension* dimension = global_optimizer->dimension();
RoutingModel* const model = dimension->model();
@@ -359,16 +365,34 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder {
std::vector<int64_t> cumul_values;
std::vector<int64_t> break_start_end_values;
const bool cumuls_optimized =
const DimensionSchedulingStatus status =
optimize_and_pack_
? global_optimizer->ComputePackedCumuls(next, &cumul_values,
&break_start_end_values)
: global_optimizer->ComputeCumuls(next, &cumul_values,
&break_start_end_values);
if (!cumuls_optimized) {
if (status == DimensionSchedulingStatus::INFEASIBLE) {
should_fail = true;
break;
}
// If relaxation is not feasible, try the MILP optimizer.
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) {
cumul_values.clear();
break_start_end_values.clear();
DCHECK(global_mp_optimizers_[i] != nullptr);
const DimensionSchedulingStatus mp_status =
optimize_and_pack_
? global_mp_optimizers_[i]->ComputePackedCumuls(
next, &cumul_values, &break_start_end_values)
: global_mp_optimizers_[i]->ComputeCumuls(
next, &cumul_values, &break_start_end_values);
if (mp_status != DimensionSchedulingStatus::OPTIMAL) {
should_fail = true;
break;
}
} else {
DCHECK(status == DimensionSchedulingStatus::OPTIMAL);
}
// Concatenate cumul_values and break_start_end_values into cp_values,
// generate corresponding cp_variables vector.
std::vector<IntVar*> cp_variables = dimension->cumuls();
@@ -409,6 +433,8 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder {
private:
const std::vector<std::unique_ptr<GlobalDimensionCumulOptimizer>>&
global_optimizers_;
const std::vector<std::unique_ptr<GlobalDimensionCumulOptimizer>>&
global_mp_optimizers_;
SearchMonitor* const monitor_;
const bool optimize_and_pack_;
};
@@ -447,7 +473,7 @@ const Assignment* RoutingModel::PackCumulsOfOptimizerDimensionsFromAssignment(
/*optimize_and_pack=*/true)));
decision_builders.push_back(
solver_->RevAlloc(new SetCumulsFromGlobalDimensionCosts(
&global_dimension_optimizers_,
&global_dimension_optimizers_, &global_dimension_mp_optimizers_,
GetOrCreateLargeNeighborhoodSearchLimit(),
/*optimize_and_pack=*/true)));
decision_builders.push_back(
@@ -1181,6 +1207,18 @@ GlobalDimensionCumulOptimizer* RoutingModel::GetMutableGlobalCumulOptimizer(
return global_dimension_optimizers_[optimizer_index].get();
}
GlobalDimensionCumulOptimizer* RoutingModel::GetMutableGlobalCumulMPOptimizer(
const RoutingDimension& dimension) const {
const DimensionIndex dim_index = GetDimensionIndex(dimension.name());
if (dim_index < 0 || dim_index >= global_optimizer_index_.size() ||
global_optimizer_index_[dim_index] < 0) {
return nullptr;
}
const int optimizer_index = global_optimizer_index_[dim_index];
DCHECK_LT(optimizer_index, global_dimension_mp_optimizers_.size());
return global_dimension_mp_optimizers_[optimizer_index].get();
}
LocalDimensionCumulOptimizer* RoutingModel::GetMutableLocalCumulOptimizer(
const RoutingDimension& dimension) const {
const DimensionIndex dim_index = GetDimensionIndex(dimension.name());
@@ -1229,6 +1267,72 @@ RoutingDimension* RoutingModel::GetMutableDimension(
return nullptr;
}
// ResourceGroup
ResourceGroup::Attributes::Attributes()
: start_domain_(Domain::AllValues()), end_domain_(Domain::AllValues()) {
/// The default attributes have unconstrained start/end domains.
}
RoutingModel::ResourceGroup::Attributes::Attributes(Domain start_domain,
Domain end_domain)
: start_domain_(std::move(start_domain)),
end_domain_(std::move(end_domain)) {}
const ResourceGroup::Attributes&
ResourceGroup::Resource::GetDimensionAttributes(
const RoutingDimension* dimension) const {
DimensionIndex dimension_index = model_->GetDimensionIndex(dimension->name());
DCHECK_NE(dimension_index, kNoDimension);
return gtl::FindWithDefault(dimension_attributes_, dimension_index,
GetDefaultAttributes());
}
void ResourceGroup::Resource::SetDimensionAttributes(
Attributes attributes, const RoutingDimension* dimension) {
DCHECK(dimension_attributes_.empty())
<< "As of 2021/07, each resource can only constrain a single dimension.";
const DimensionIndex dimension_index =
model_->GetDimensionIndex(dimension->name());
DCHECK_NE(dimension_index, kNoDimension);
DCHECK(!dimension_attributes_.contains(dimension_index));
dimension_attributes_[dimension_index] = std::move(attributes);
}
const ResourceGroup::Attributes& ResourceGroup::Resource::GetDefaultAttributes()
const {
static const Attributes* const kAttributes = new Attributes();
return *kAttributes;
}
RoutingModel::ResourceGroup* const RoutingModel::AddResourceGroup() {
resource_groups_.push_back(absl::make_unique<ResourceGroup>(this));
return resource_groups_.back().get();
}
void RoutingModel::ResourceGroup::AddResource(
Attributes attributes, const RoutingDimension* dimension) {
resources_.push_back(Resource(model_));
resources_.back().SetDimensionAttributes(std::move(attributes), dimension);
const DimensionIndex dimension_index =
model_->GetDimensionIndex(dimension->name());
DCHECK_NE(dimension_index, kNoDimension);
affected_dimension_indices_.insert(dimension_index);
DCHECK_EQ(affected_dimension_indices_.size(), 1)
<< "As of 2021/07, each ResourceGroup can only affect a single "
"RoutingDimension at a time.";
}
const std::vector<int>& RoutingModel::GetDimensionResourceGroupIndices(
const RoutingDimension* dimension) const {
DCHECK(closed_);
const DimensionIndex dim = GetDimensionIndex(dimension->name());
DCHECK_NE(dim, kNoDimension);
return dimension_resource_group_indices_[dim];
}
void RoutingModel::SetArcCostEvaluatorOfAllVehicles(int evaluator_index) {
CHECK_LT(0, vehicles_);
for (int i = 0; i < vehicles_; ++i) {
@@ -1719,8 +1823,9 @@ void RoutingModel::AddPickupAndDelivery(int64_t pickup, int64_t delivery) {
void RoutingModel::AddPickupAndDeliverySets(
DisjunctionIndex pickup_disjunction,
DisjunctionIndex delivery_disjunction) {
AddPickupAndDeliverySetsInternal(GetDisjunctionIndices(pickup_disjunction),
GetDisjunctionIndices(delivery_disjunction));
AddPickupAndDeliverySetsInternal(
GetDisjunctionNodeIndices(pickup_disjunction),
GetDisjunctionNodeIndices(delivery_disjunction));
pickup_delivery_disjunctions_.push_back(
{pickup_disjunction, delivery_disjunction});
}
@@ -2124,6 +2229,15 @@ void RoutingModel::CloseModelWithParameters(
for (RoutingDimension* const dimension : dimensions_) {
dimension->CloseModel(UsesLightPropagation(parameters));
}
dimension_resource_group_indices_.resize(dimensions_.size());
for (int rg_index = 0; rg_index < resource_groups_.size(); rg_index++) {
for (DimensionIndex dim_index :
resource_groups_[rg_index]->GetAffectedDimensionIndices()) {
dimension_resource_group_indices_[dim_index].push_back(rg_index);
}
}
ComputeCostClasses(parameters);
ComputeVehicleClasses();
ComputeVehicleTypes();
@@ -2705,7 +2819,8 @@ const Assignment* RoutingModel::SolveFromAssignmentsWithParameters(
}
}
if (parameters.use_cp_sat() == BOOL_TRUE) {
if (parameters.use_cp_sat() == BOOL_TRUE ||
parameters.use_generalized_cp_sat() == BOOL_TRUE) {
const int solution_count = collect_assignments_->solution_count();
Assignment* const cp_solution =
solution_count >= 1 ? collect_assignments_->solution(solution_count - 1)
@@ -4354,7 +4469,11 @@ void RoutingModel::StoreDimensionCumulOptimizers(
// Use global optimizer.
global_optimizer_index_[dim] = global_dimension_optimizers_.size();
global_dimension_optimizers_.push_back(
absl::make_unique<GlobalDimensionCumulOptimizer>(dimension));
absl::make_unique<GlobalDimensionCumulOptimizer>(
dimension, parameters.continuous_scheduling_solver()));
global_dimension_mp_optimizers_.push_back(
absl::make_unique<GlobalDimensionCumulOptimizer>(
dimension, parameters.mixed_integer_scheduling_solver()));
packed_dimensions_collector_assignment->Add(dimension->cumuls());
if (!AllTransitsPositive(*dimension)) {
dimension->SetOffsetForGlobalOptimizer(0);
@@ -4514,7 +4633,8 @@ DecisionBuilder* RoutingModel::CreateSolutionFinalizer(SearchLimit* lns_limit) {
if (!global_dimension_optimizers_.empty()) {
decision_builders.push_back(
solver_->RevAlloc(new SetCumulsFromGlobalDimensionCosts(
&global_dimension_optimizers_, lns_limit)));
&global_dimension_optimizers_, &global_dimension_mp_optimizers_,
lns_limit)));
}
decision_builders.push_back(
CreateFinalizerForMinimizedAndMaximizedVariables());

View File

@@ -176,6 +176,7 @@
#include "ortools/base/integral_types.h"
#include "ortools/base/logging.h"
#include "ortools/base/macros.h"
#include "ortools/base/map_util.h"
#include "ortools/base/strong_vector.h"
#include "ortools/constraint_solver/constraint_solver.h"
#include "ortools/constraint_solver/constraint_solveri.h"
@@ -375,6 +376,75 @@ class RoutingModel {
// clang-format on
};
/// A ResourceGroup defines a set of available Resources with attributes on
/// one or multiple dimensions.
/// For every ResourceGroup in the model, each (used) vehicle in the solution
/// must be assigned to exactly 1 resource, and each resource can in turn be
/// assigned to at most 1 vehicle. This vehicle-to-resource assignment will
/// apply the corresponding Attributes to the dimensions affected by the
/// resource group.
/// NOTE: As of 2021/07, each ResourceGroup can only affect a single
/// RoutingDimension at a time, i.e. all Resources in a group must apply
/// attributes to the same single dimension.
class ResourceGroup {
public:
/// Attributes for a dimension.
class Attributes {
public:
Attributes();
Attributes(Domain start_domain, Domain end_domain);
const Domain& start_domain() const { return start_domain_; }
const Domain& end_domain() const { return end_domain_; }
private:
/// The following domains constrain the dimension start/end cumul of the
/// the vehicle assigned to this resource:
/// start_domain_.Min() <= cumul[Start(v)] <= start_domain_.Max()
Domain start_domain_;
/// end_domain_.Min() <= cumul[End(v)] <= end_domain_.Max()
Domain end_domain_;
};
/// A Resource sets attributes (costs/constraints) for a set of dimensions.
class Resource {
public:
const Attributes& GetDimensionAttributes(
const RoutingDimension* dimension) const;
private:
explicit Resource(const RoutingModel* model) : model_(model) {}
void SetDimensionAttributes(Attributes attributes,
const RoutingDimension* dimension);
const Attributes& GetDefaultAttributes() const;
const RoutingModel* const model_;
absl::flat_hash_map<DimensionIndex, Attributes> dimension_attributes_;
friend class ResourceGroup;
};
explicit ResourceGroup(const RoutingModel* model) : model_(model) {}
/// Add a Resource with the given attributes for the corresponding
/// dimension.
void AddResource(Attributes attributes, const RoutingDimension* dimension);
const std::vector<Resource>& GetResources() const { return resources_; }
const absl::flat_hash_set<DimensionIndex>& GetAffectedDimensionIndices()
const {
return affected_dimension_indices_;
}
int Size() const { return resources_.size(); }
private:
const RoutingModel* const model_;
std::vector<Resource> resources_;
/// All indices of dimensions affected by this resource group.
absl::flat_hash_set<DimensionIndex> affected_dimension_indices_;
};
/// Constant used to express a hard constraint instead of a soft penalty.
static const int64_t kNoPenalty;
@@ -563,6 +633,10 @@ class RoutingModel {
GetGlobalDimensionCumulOptimizers() const {
return global_dimension_optimizers_;
}
const std::vector<std::unique_ptr<GlobalDimensionCumulOptimizer> >&
GetGlobalDimensionCumulMPOptimizers() const {
return global_dimension_mp_optimizers_;
}
const std::vector<std::unique_ptr<LocalDimensionCumulOptimizer> >&
GetLocalDimensionCumulOptimizers() const {
return local_dimension_optimizers_;
@@ -577,6 +651,8 @@ class RoutingModel {
/// or nullptr if there is none.
GlobalDimensionCumulOptimizer* GetMutableGlobalCumulOptimizer(
const RoutingDimension& dimension) const;
GlobalDimensionCumulOptimizer* GetMutableGlobalCumulMPOptimizer(
const RoutingDimension& dimension) const;
LocalDimensionCumulOptimizer* GetMutableLocalCumulOptimizer(
const RoutingDimension& dimension) const;
LocalDimensionCumulOptimizer* GetMutableLocalCumulMPOptimizer(
@@ -603,6 +679,19 @@ class RoutingModel {
const std::string& GetPrimaryConstrainedDimension() const {
return primary_constrained_dimension_;
}
ResourceGroup* const AddResourceGroup();
// clang-format off
const std::vector<std::unique_ptr<ResourceGroup> >& GetResourceGroups()
const {
return resource_groups_;
}
// clang-format on
/// Returns the indices of resource groups for this dimension. This method can
/// only be called after the model has been closed.
const std::vector<int>& GetDimensionResourceGroupIndices(
const RoutingDimension* dimension) const;
/// Adds a disjunction constraint on the indices: exactly 'max_cardinality' of
/// the indices are active. Start and end indices of any vehicle cannot be
/// part of a disjunction.
@@ -644,7 +733,7 @@ class RoutingModel {
#if !defined(SWIGPYTHON)
/// Returns the variable indices of the nodes in the disjunction of index
/// 'index'.
const std::vector<int64_t>& GetDisjunctionIndices(
const std::vector<int64_t>& GetDisjunctionNodeIndices(
DisjunctionIndex index) const {
return disjunctions_[index].indices;
}
@@ -875,9 +964,9 @@ class RoutingModel {
}
/// Get the "unperformed" penalty of a node. This is only well defined if the
/// node is only part of a single Disjunction involving only itself, and that
/// disjunction has a penalty. In all other cases, including forced active
/// nodes, this returns 0.
/// node is only part of a single Disjunction, and that disjunction has a
/// penalty. For forced active nodes returns max int64_t. In all other cases,
/// this returns 0.
int64_t UnperformedPenalty(int64_t var_index) const;
/// Same as above except that it returns default_value instead of 0 when
/// penalty is not well defined (default value is passed as first argument to
@@ -1279,6 +1368,21 @@ class RoutingModel {
DCHECK(closed_);
return vehicle_class_index_of_vehicle_[vehicle];
}
/// Returns a vehicle of the given vehicle class, and -1 if there are no
/// vehicles for this class.
int GetVehicleOfClass(VehicleClassIndex vehicle_class) const {
DCHECK(closed_);
const RoutingModel::VehicleTypeContainer& vehicle_type_container =
GetVehicleTypeContainer();
if (vehicle_class.value() >= GetVehicleClassesCount() ||
vehicle_type_container.vehicles_per_vehicle_class[vehicle_class.value()]
.empty()) {
return -1;
}
return vehicle_type_container
.vehicles_per_vehicle_class[vehicle_class.value()]
.front();
}
/// Returns the number of different vehicle classes in the model.
int GetVehicleClassesCount() const { return vehicle_classes_.size(); }
/// Returns variable indices of nodes constrained to be on the same route.
@@ -1744,12 +1848,23 @@ class RoutingModel {
/// Dimensions
absl::flat_hash_map<std::string, DimensionIndex> dimension_name_to_index_;
absl::StrongVector<DimensionIndex, RoutingDimension*> dimensions_;
/// Resource Groups.
/// If resource_groups_ is not empty, then for each group of resources, each
/// (used) vehicle must be assigned to exactly 1 resource, and each resource
/// can in turn be assigned to at most 1 vehicle.
// clang-format off
std::vector<std::unique_ptr<ResourceGroup> > resource_groups_;
/// Stores the set of resource groups related to each dimension.
absl::StrongVector<DimensionIndex, std::vector<int> >
dimension_resource_group_indices_;
/// TODO(user): Define a new Dimension[Global|Local]OptimizerIndex type
/// and use it to define ITIVectors and for the dimension to optimizer index
/// mappings below.
std::vector<std::unique_ptr<GlobalDimensionCumulOptimizer> >
global_dimension_optimizers_;
std::vector<std::unique_ptr<GlobalDimensionCumulOptimizer> >
global_dimension_mp_optimizers_;
absl::StrongVector<DimensionIndex, int> global_optimizer_index_;
std::vector<std::unique_ptr<LocalDimensionCumulOptimizer> >
local_dimension_optimizers_;
@@ -1941,6 +2056,7 @@ class RoutingModel {
friend class RoutingDimension;
friend class RoutingModelInspector;
friend class ResourceGroup::Resource;
DISALLOW_COPY_AND_ASSIGN(RoutingModel);
};
@@ -2458,6 +2574,16 @@ class RoutingDimension {
return model_->TransitCallback(
class_evaluators_[vehicle_to_class_[vehicle]]);
}
/// Returns the callback evaluating the transit value between two node indices
/// for a given vehicle class.
const RoutingModel::TransitCallback2& class_transit_evaluator(
RoutingVehicleClassIndex vehicle_class) const {
const int vehicle = model_->GetVehicleOfClass(vehicle_class);
DCHECK_NE(vehicle, -1);
return transit_evaluator(vehicle);
}
/// Returns the unary callback evaluating the transit value between two node
/// indices for a given vehicle. If the corresponding callback is not unary,
/// returns a null callback.
@@ -2683,6 +2809,14 @@ class RoutingDimension {
int64_t GetSpanCostCoefficientForVehicle(int vehicle) const {
return vehicle_span_cost_coefficients_[vehicle];
}
#ifndef SWIG
int64_t GetSpanCostCoefficientForVehicleClass(
RoutingVehicleClassIndex vehicle_class) const {
const int vehicle = model_->GetVehicleOfClass(vehicle_class);
DCHECK_NE(vehicle, -1);
return GetSpanCostCoefficientForVehicle(vehicle);
}
#endif // SWIG
#ifndef SWIG
const std::vector<int64_t>& vehicle_span_cost_coefficients() const {
return vehicle_span_cost_coefficients_;

View File

@@ -334,7 +334,7 @@ bool DisjunctivePropagator::DistanceDuration(Tasks* tasks) {
// Skip breaks that cannot be performed after start.
int index_break_by_emax = tasks->num_chain_tasks;
while (index_break_by_emax < num_tasks &&
tasks->end_max[index_break_by_emax] <= tasks->end_max[route_start]) {
tasks->end_max[index_break_by_emax] <= tasks->end_min[route_start]) {
++index_break_by_emax;
}
// Special case: no breaks after start.

View File

@@ -212,7 +212,7 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter {
const int inactive_nodes =
current_inactive_nodes + disjunction_inactive_delta.second;
const int max_inactive_cardinality =
routing_model_.GetDisjunctionIndices(disjunction_index).size() -
routing_model_.GetDisjunctionNodeIndices(disjunction_index).size() -
routing_model_.GetDisjunctionMaxCardinality(disjunction_index);
// Too many inactive nodes.
if (inactive_nodes > max_inactive_cardinality) {
@@ -258,7 +258,7 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter {
active_per_disjunction_[i] = 0;
inactive_per_disjunction_[i] = 0;
const std::vector<int64_t>& disjunction_indices =
routing_model_.GetDisjunctionIndices(i);
routing_model_.GetDisjunctionNodeIndices(i);
for (const int64_t index : disjunction_indices) {
const bool index_synced = IsVarSynced(index);
if (index_synced) {
@@ -2199,7 +2199,7 @@ void AppendLightWeightDimensionFilters(
std::move(demands), std::move(node_capacity));
const auto kAccept = LocalSearchFilterManager::FilterEventType::kAccept;
LocalSearchFilter* filter = MakeUnaryDimensionFilter(
dimension->model()->solver(), std::move(checker));
dimension->model()->solver(), std::move(checker), dimension->name());
filters->push_back({filter, kAccept});
}
}
@@ -2275,6 +2275,7 @@ void AppendDimensionCumulFilters(
DCHECK(model.GetMutableGlobalCumulOptimizer(dimension) != nullptr);
filters->push_back({MakeGlobalLPCumulFilter(
model.GetMutableGlobalCumulOptimizer(dimension),
model.GetMutableGlobalCumulMPOptimizer(dimension),
filter_objective_cost),
kAccept});
} else if (use_cumul_bounds_propagator_filter[d]) {
@@ -2609,6 +2610,7 @@ class LPCumulFilter : public IntVarLocalSearchFilter {
public:
LPCumulFilter(const std::vector<IntVar*>& nexts,
GlobalDimensionCumulOptimizer* optimizer,
GlobalDimensionCumulOptimizer* mp_optimizer,
bool filter_objective_cost);
bool Accept(const Assignment* delta, const Assignment* deltadelta,
int64_t objective_min, int64_t objective_max) override;
@@ -2621,6 +2623,7 @@ class LPCumulFilter : public IntVarLocalSearchFilter {
private:
GlobalDimensionCumulOptimizer& optimizer_;
GlobalDimensionCumulOptimizer& mp_optimizer_;
const bool filter_objective_cost_;
int64_t synchronized_cost_without_transit_;
int64_t delta_cost_without_transit_;
@@ -2630,9 +2633,11 @@ class LPCumulFilter : public IntVarLocalSearchFilter {
LPCumulFilter::LPCumulFilter(const std::vector<IntVar*>& nexts,
GlobalDimensionCumulOptimizer* optimizer,
GlobalDimensionCumulOptimizer* mp_optimizer,
bool filter_objective_cost)
: IntVarLocalSearchFilter(nexts),
optimizer_(*optimizer),
mp_optimizer_(*mp_optimizer),
filter_objective_cost_(filter_objective_cost),
synchronized_cost_without_transit_(-1),
delta_cost_without_transit_(-1),
@@ -2662,12 +2667,30 @@ bool LPCumulFilter::Accept(const Assignment* delta,
if (!filter_objective_cost_) {
// No need to compute the cost of the LP, only verify its feasibility.
delta_cost_without_transit_ = 0;
return optimizer_.IsFeasible(next_accessor);
const DimensionSchedulingStatus status =
optimizer_.ComputeCumuls(next_accessor, nullptr, nullptr);
if (status == DimensionSchedulingStatus::OPTIMAL) return true;
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY &&
mp_optimizer_.ComputeCumuls(next_accessor, nullptr, nullptr) ==
DimensionSchedulingStatus::OPTIMAL) {
return true;
}
return false;
}
if (!optimizer_.ComputeCumulCostWithoutFixedTransits(
next_accessor, &delta_cost_without_transit_)) {
// Infeasible.
const DimensionSchedulingStatus status =
optimizer_.ComputeCumulCostWithoutFixedTransits(
next_accessor, &delta_cost_without_transit_);
if (status == DimensionSchedulingStatus::INFEASIBLE) {
delta_cost_without_transit_ = std::numeric_limits<int64_t>::max();
return false;
}
if (delta_cost_without_transit_ > objective_max) return false;
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY &&
mp_optimizer_.ComputeCumulCostWithoutFixedTransits(
next_accessor, &delta_cost_without_transit_) !=
DimensionSchedulingStatus::OPTIMAL) {
delta_cost_without_transit_ = std::numeric_limits<int64_t>::max();
return false;
}
@@ -2682,17 +2705,28 @@ void LPCumulFilter::OnSynchronize(const Assignment* delta) {
// TODO(user): Try to optimize this so the LP is not called when the last
// computed delta cost corresponds to the solution being synchronized.
const RoutingModel& model = *optimizer_.dimension()->model();
if (!optimizer_.ComputeCumulCostWithoutFixedTransits(
[this, &model](int64_t index) {
return IsVarSynced(index) ? Value(index)
: model.IsStart(index) ? model.End(model.VehicleIndex(index))
: index;
},
&synchronized_cost_without_transit_)) {
const auto& next_accessor = [this, &model](int64_t index) {
return IsVarSynced(index) ? Value(index)
: model.IsStart(index) ? model.End(model.VehicleIndex(index))
: index;
};
const DimensionSchedulingStatus status =
optimizer_.ComputeCumulCostWithoutFixedTransits(
next_accessor, &synchronized_cost_without_transit_);
if (status == DimensionSchedulingStatus::INFEASIBLE) {
// TODO(user): This should only happen if the LP solver times out.
// DCHECK the fail wasn't due to an infeasible model.
synchronized_cost_without_transit_ = 0;
}
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY &&
mp_optimizer_.ComputeCumulCostWithoutFixedTransits(
next_accessor, &synchronized_cost_without_transit_) !=
DimensionSchedulingStatus::OPTIMAL) {
// TODO(user): This should only happen if the MP solver times out.
// DCHECK the fail wasn't due to an infeasible model.
synchronized_cost_without_transit_ = 0;
}
}
int64_t LPCumulFilter::GetSynchronizedObjectiveValue() const {
@@ -2702,10 +2736,11 @@ int64_t LPCumulFilter::GetSynchronizedObjectiveValue() const {
} // namespace
IntVarLocalSearchFilter* MakeGlobalLPCumulFilter(
GlobalDimensionCumulOptimizer* optimizer, bool filter_objective_cost) {
GlobalDimensionCumulOptimizer* optimizer,
GlobalDimensionCumulOptimizer* mp_optimizer, bool filter_objective_cost) {
const RoutingModel& model = *optimizer->dimension()->model();
return model.solver()->RevAlloc(
new LPCumulFilter(model.Nexts(), optimizer, filter_objective_cost));
return model.solver()->RevAlloc(new LPCumulFilter(
model.Nexts(), optimizer, mp_optimizer, filter_objective_cost));
}
namespace {

View File

@@ -66,7 +66,8 @@ IntVarLocalSearchFilter* MakeCumulBoundsPropagatorFilter(
/// Returns a filter checking global linear constraints and costs.
IntVarLocalSearchFilter* MakeGlobalLPCumulFilter(
GlobalDimensionCumulOptimizer* optimizer, bool filter_objective_cost);
GlobalDimensionCumulOptimizer* optimizer,
GlobalDimensionCumulOptimizer* mp_optimizer, bool filter_objective_cost);
/// Returns a filter checking the current solution using CP propagation.
IntVarLocalSearchFilter* MakeCPFeasibilityFilter(RoutingModel* routing_model);

View File

@@ -58,7 +58,7 @@ bool RoutingModel::IsMatchingModel() const {
absl::flat_hash_set<int> disjunction_nodes;
for (DisjunctionIndex i(0); i < GetNumberOfDisjunctions(); ++i) {
if (GetDisjunctionMaxCardinality(i) > 1) return false;
for (int64_t node : GetDisjunctionIndices(i)) {
for (int64_t node : GetDisjunctionNodeIndices(i)) {
if (!disjunction_nodes.insert(node).second) return false;
}
}
@@ -225,7 +225,7 @@ bool RoutingModel::SolveMatchingModel(
disjunction_to_flow_nodes.back().push_back(num_flow_nodes);
num_flow_nodes++;
} else {
for (int n : GetDisjunctionIndices(disjunctions.back())) {
for (int n : GetDisjunctionNodeIndices(disjunctions.back())) {
in_disjunction[n] = true;
flow_to_non_pd[num_flow_nodes] = n;
disjunction_to_flow_nodes.back().push_back(num_flow_nodes);

View File

@@ -171,7 +171,10 @@ LocalDimensionCumulOptimizer::LocalDimensionCumulOptimizer(
case RoutingSearchParameters::GLOP: {
const glop::GlopParameters parameters = GetGlopParametersForLocalLP();
for (int vehicle = 0; vehicle < vehicles; ++vehicle) {
solver_[vehicle] = absl::make_unique<RoutingGlopWrapper>(parameters);
// TODO(user): Instead of passing false, detect if the relaxation
// will always violate the MIPL constraints.
solver_[vehicle] =
absl::make_unique<RoutingGlopWrapper>(false, parameters);
}
break;
}
@@ -516,7 +519,7 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeSingleRoute(
return status;
}
bool DimensionCumulOptimizerCore::Optimize(
DimensionSchedulingStatus DimensionCumulOptimizerCore::Optimize(
const std::function<int64_t(int64_t)>& next_accessor,
RoutingLinearSolverWrapper* solver, std::vector<int64_t>* cumul_values,
std::vector<int64_t>* break_values, int64_t* cost, int64_t* transit_cost,
@@ -532,23 +535,34 @@ bool DimensionCumulOptimizerCore::Optimize(
if (propagator_ != nullptr &&
!propagator_->PropagateCumulBounds(next_accessor, cumul_offset)) {
return false;
return DimensionSchedulingStatus::INFEASIBLE;
}
int64_t total_transit_cost = 0;
int64_t total_cost_offset = 0;
const RoutingModel* model = dimension()->model();
int num_needed_resources = 0;
int num_available_resources = model->vehicles();
const auto& resource_groups = model->GetResourceGroups();
for (int rg_index : model->GetDimensionResourceGroupIndices(dimension_)) {
num_available_resources =
std::min(num_available_resources, resource_groups[rg_index]->Size());
}
DCHECK_GT(num_available_resources, 0);
for (int vehicle = 0; vehicle < model->vehicles(); vehicle++) {
int64_t route_transit_cost = 0;
int64_t route_cost_offset = 0;
const bool optimize_vehicle_costs =
optimize_costs &&
(!model->IsEnd(next_accessor(model->Start(vehicle))) ||
model->AreEmptyRouteCostsConsideredForVehicle(vehicle));
const bool vehicle_is_used =
!model->IsEnd(next_accessor(model->Start(vehicle))) ||
model->AreEmptyRouteCostsConsideredForVehicle(vehicle);
if (vehicle_is_used && ++num_needed_resources > num_available_resources) {
return DimensionSchedulingStatus::INFEASIBLE;
}
const bool optimize_vehicle_costs = optimize_costs && vehicle_is_used;
if (!SetRouteCumulConstraints(vehicle, next_accessor, cumul_offset,
optimize_vehicle_costs, solver,
&route_transit_cost, &route_cost_offset)) {
return false;
return DimensionSchedulingStatus::INFEASIBLE;
}
total_transit_cost = CapAdd(total_transit_cost, route_transit_cost);
total_cost_offset = CapAdd(total_cost_offset, route_cost_offset);
@@ -558,13 +572,17 @@ bool DimensionCumulOptimizerCore::Optimize(
*transit_cost = total_transit_cost;
}
SetGlobalConstraints(has_vehicles_being_optimized, solver);
SetGlobalConstraints(next_accessor, cumul_offset,
has_vehicles_being_optimized, solver);
if (solver->Solve(model->RemainingTime()) ==
DimensionSchedulingStatus::INFEASIBLE) {
return false;
const DimensionSchedulingStatus status =
solver->Solve(model->RemainingTime());
if (status == DimensionSchedulingStatus::INFEASIBLE) {
return status;
}
// TODO(user): In case the status is RELAXED_OPTIMAL_ONLY, check we can
// safely avoid filling variable and cost values.
SetValuesFromLP(index_to_cumul_variable_, cumul_offset, solver, cumul_values);
SetValuesFromLP(all_break_variables_, cumul_offset, solver, break_values);
@@ -575,34 +593,38 @@ bool DimensionCumulOptimizerCore::Optimize(
if (clear_lp) {
solver->Clear();
}
return true;
return status;
}
bool DimensionCumulOptimizerCore::OptimizeAndPack(
DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack(
const std::function<int64_t(int64_t)>& next_accessor,
RoutingLinearSolverWrapper* solver, std::vector<int64_t>* cumul_values,
std::vector<int64_t>* break_values) {
// Note: We pass a non-nullptr cost to the Optimize() method so the costs are
// optimized by the LP.
// Note: We pass a non-nullptr cost to the Optimize() method so the costs
// are optimized by the solver.
int64_t cost = 0;
if (!Optimize(next_accessor, solver,
/*cumul_values=*/nullptr, /*break_values=*/nullptr, &cost,
/*transit_cost=*/nullptr, /*clear_lp=*/false)) {
return false;
if (Optimize(next_accessor, solver,
/*cumul_values=*/nullptr, /*break_values=*/nullptr, &cost,
/*transit_cost=*/nullptr,
/*clear_lp=*/false) == DimensionSchedulingStatus::INFEASIBLE) {
return DimensionSchedulingStatus::INFEASIBLE;
}
std::vector<int> vehicles(dimension()->model()->vehicles());
std::iota(vehicles.begin(), vehicles.end(), 0);
if (PackRoutes(std::move(vehicles), solver) ==
DimensionSchedulingStatus::INFEASIBLE) {
return false;
// Subtle: Even if the status was RELAXED_OPTIMAL_ONLY we try to pack just in
// case packing manages to make the solution completely feasible.
DimensionSchedulingStatus status = PackRoutes(vehicles, solver);
if (status == DimensionSchedulingStatus::INFEASIBLE) {
return status;
}
// TODO(user): In case the status is RELAXED_OPTIMAL_ONLY, check we can
// safely avoid filling variable values.
const int64_t global_offset = dimension_->GetGlobalOptimizerOffset();
SetValuesFromLP(index_to_cumul_variable_, global_offset, solver,
cumul_values);
SetValuesFromLP(all_break_variables_, global_offset, solver, break_values);
solver->Clear();
return true;
return status;
}
DimensionSchedulingStatus
@@ -650,15 +672,7 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes(
DCHECK(solver->SolutionIsInteger());
// Minimize the route end times without increasing the cost.
const int objective_ct =
solver->CreateNewConstraint(0, solver->GetObjectiveValue());
for (int variable = 0; variable < solver->NumVariables(); variable++) {
const double coefficient = solver->GetObjectiveCoefficient(variable);
if (coefficient != 0) {
solver->SetCoefficient(objective_ct, variable, coefficient);
}
}
solver->AddObjectiveConstraint();
solver->ClearObjective();
for (int vehicle : vehicles) {
solver->SetObjectiveCoefficient(
@@ -1286,6 +1300,7 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints(
}
void DimensionCumulOptimizerCore::SetGlobalConstraints(
const std::function<int64_t(int64_t)>& next_accessor, int64_t cumul_offset,
bool optimize_costs, RoutingLinearSolverWrapper* solver) {
// Global span cost =
// global_span_cost_coefficient * (max_end_cumul - min_start_cumul).
@@ -1316,6 +1331,95 @@ void DimensionCumulOptimizerCore::SetGlobalConstraints(
solver->SetCoefficient(ct, second_cumul_var, 1);
solver->SetCoefficient(ct, first_cumul_var, -1);
}
const RoutingModel& model = *dimension_->model();
if (!solver->IsCPSATSolver()) {
// The resource attributes conditional constraints can only be added with
// the CP-SAT MIP solver.
return;
}
using ResourceGroup = RoutingModel::ResourceGroup;
const auto& resource_groups = model.GetResourceGroups();
for (int rg_index : model.GetDimensionResourceGroupIndices(dimension_)) {
// Resource domain constraints:
// Every (used) vehicle must be assigned to exactly one resource in this
// group, and each resource must be assigned to at most 1 vehicle.
// For every resource r with Attributes A = resources[r].attributes(dim)
// and every vehicle v, assign(r, v) == 1 -->
// A.start_domain.Min() <= cumul[Start(v)] <= A.start_domain.Max(),
// and
// A.end_domain.Min() <= cumul[End(v)] <= A.end_domain.Max().
const std::vector<ResourceGroup::Resource>& resources =
resource_groups[rg_index]->GetResources();
static const int kNoConstraint = -1;
// Assignment constraints for vehicles: each (used) vehicle must have
// exactly one resource assigned to it.
std::vector<int> vehicle_constraints(model.vehicles(), kNoConstraint);
for (int v = 0; v < model.vehicles(); v++) {
if (model.IsEnd(next_accessor(model.Start(v))) &&
!model.AreEmptyRouteCostsConsideredForVehicle(v)) {
// We don't assign a driver to unused vehicles.
continue;
}
vehicle_constraints[v] = solver->CreateNewConstraint(1, 1);
}
// Assignment constraints for resources: each resource must be assigned to
// at most one (used) vehicle.
std::vector<int> resource_constraints(resources.size(), kNoConstraint);
for (int r = 0; r < resources.size(); r++) {
const ResourceGroup::Attributes& attributes =
resources[r].GetDimensionAttributes(dimension_);
if (attributes.start_domain().Max() < cumul_offset ||
attributes.end_domain().Max() < cumul_offset) {
// This resource's domain has a cumul max lower than the offset, so it's
// not possible to restrict any vehicle start/end to this domain; skip
// it.
continue;
}
resource_constraints[r] = solver->CreateNewConstraint(0, 1);
}
// Create assignment variables, add them to the corresponding constraints,
// and create the reified constraints assign(r, v) == 1 -->
// A(r).start_domain.Min() <= cumul[Start(v)] <= A(r).start_domain.Max(),
// and
// A(r).end_domain.Min() <= cumul[End(v)] <= A(r).end_domain.Max().
for (int r = 0; r < resources.size(); r++) {
if (resource_constraints[r] == kNoConstraint) continue;
const ResourceGroup::Attributes& attributes =
resources[r].GetDimensionAttributes(dimension_);
for (int v = 0; v < dimension_->model()->vehicles(); v++) {
if (vehicle_constraints[v] == kNoConstraint) continue;
const int assign_r_to_v = solver->AddVariable(0, 1);
solver->SetCoefficient(vehicle_constraints[v], assign_r_to_v, 1);
solver->SetCoefficient(resource_constraints[r], assign_r_to_v, 1);
const auto& add_domain_constraint =
[&solver, cumul_offset, assign_r_to_v](const Domain& domain,
int cumul_variable) {
if (domain == Domain::AllValues()) {
return;
}
const int64_t cumul_min =
std::max<int64_t>(CapSub(domain.Min(), cumul_offset), 0);
const int64_t cumul_max =
domain.Max() == std::numeric_limits<int64_t>::max()
? std::numeric_limits<int64_t>::max()
: CapSub(domain.Max(), cumul_offset);
DCHECK_GE(cumul_max, 0);
const int cumul_constraint = solver->AddLinearConstraint(
cumul_min, cumul_max, {{cumul_variable, 1}});
solver->SetEnforcementLiteral(cumul_constraint, assign_r_to_v);
};
add_domain_constraint(attributes.start_domain(),
index_to_cumul_variable_[model.Start(v)]);
add_domain_constraint(attributes.end_domain(),
index_to_cumul_variable_[model.End(v)]);
}
}
}
}
void DimensionCumulOptimizerCore::SetValuesFromLP(
@@ -1336,30 +1440,45 @@ void DimensionCumulOptimizerCore::SetValuesFromLP(
}
GlobalDimensionCumulOptimizer::GlobalDimensionCumulOptimizer(
const RoutingDimension* dimension)
const RoutingDimension* dimension,
RoutingSearchParameters::SchedulingSolver solver_type)
: optimizer_core_(dimension,
/*use_precedence_propagator=*/
!dimension->GetNodePrecedences().empty()) {
solver_ =
absl::make_unique<RoutingGlopWrapper>(GetGlopParametersForGlobalLP());
switch (solver_type) {
case RoutingSearchParameters::GLOP: {
solver_ = absl::make_unique<RoutingGlopWrapper>(
/*is_relaxation=*/!dimension->model()
->GetDimensionResourceGroupIndices(dimension)
.empty(),
GetGlopParametersForGlobalLP());
break;
}
case RoutingSearchParameters::CP_SAT: {
solver_ = absl::make_unique<RoutingCPSatWrapper>();
break;
}
default:
LOG(DFATAL) << "Unrecognized solver type: " << solver_type;
}
}
bool GlobalDimensionCumulOptimizer::ComputeCumulCostWithoutFixedTransits(
DimensionSchedulingStatus
GlobalDimensionCumulOptimizer::ComputeCumulCostWithoutFixedTransits(
const std::function<int64_t(int64_t)>& next_accessor,
int64_t* optimal_cost_without_transits) {
int64_t cost = 0;
int64_t transit_cost = 0;
if (optimizer_core_.Optimize(next_accessor, solver_.get(), nullptr, nullptr,
&cost, &transit_cost)) {
if (optimal_cost_without_transits != nullptr) {
*optimal_cost_without_transits = CapSub(cost, transit_cost);
}
return true;
DimensionSchedulingStatus status = optimizer_core_.Optimize(
next_accessor, solver_.get(), nullptr, nullptr, &cost, &transit_cost);
if (status != DimensionSchedulingStatus::INFEASIBLE &&
optimal_cost_without_transits != nullptr) {
*optimal_cost_without_transits = CapSub(cost, transit_cost);
}
return false;
return status;
}
bool GlobalDimensionCumulOptimizer::ComputeCumuls(
DimensionSchedulingStatus GlobalDimensionCumulOptimizer::ComputeCumuls(
const std::function<int64_t(int64_t)>& next_accessor,
std::vector<int64_t>* optimal_cumuls,
std::vector<int64_t>* optimal_breaks) {
@@ -1367,13 +1486,7 @@ bool GlobalDimensionCumulOptimizer::ComputeCumuls(
optimal_breaks, nullptr, nullptr);
}
bool GlobalDimensionCumulOptimizer::IsFeasible(
const std::function<int64_t(int64_t)>& next_accessor) {
return optimizer_core_.Optimize(next_accessor, solver_.get(), nullptr,
nullptr, nullptr, nullptr);
}
bool GlobalDimensionCumulOptimizer::ComputePackedCumuls(
DimensionSchedulingStatus GlobalDimensionCumulOptimizer::ComputePackedCumuls(
const std::function<int64_t(int64_t)>& next_accessor,
std::vector<int64_t>* packed_cumuls, std::vector<int64_t>* packed_breaks) {
return optimizer_core_.OptimizeAndPack(next_accessor, solver_.get(),

View File

@@ -174,6 +174,7 @@ class RoutingLinearSolverWrapper {
virtual int CreateNewConstraint(int64_t lower_bound, int64_t upper_bound) = 0;
virtual void SetCoefficient(int ct, int index, double coefficient) = 0;
virtual bool IsCPSATSolver() = 0;
virtual void AddObjectiveConstraint() = 0;
virtual void AddMaximumConstraint(int max_var, std::vector<int> vars) = 0;
virtual void AddProductConstraint(int product_var, std::vector<int> vars) = 0;
virtual void SetEnforcementLiteral(int ct, int condition) = 0;
@@ -236,7 +237,8 @@ class RoutingLinearSolverWrapper {
class RoutingGlopWrapper : public RoutingLinearSolverWrapper {
public:
explicit RoutingGlopWrapper(const glop::GlopParameters& parameters) {
RoutingGlopWrapper(bool is_relaxation, const glop::GlopParameters& parameters)
: is_relaxation_(is_relaxation) {
lp_solver_.SetParameters(parameters);
linear_program_.SetMaximizationProblem(false);
}
@@ -307,8 +309,17 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper {
coefficient);
}
bool IsCPSATSolver() override { return false; }
void AddMaximumConstraint(int max_var, std::vector<int> vars) override{};
void AddProductConstraint(int product_var, std::vector<int> vars) override{};
void AddObjectiveConstraint() override {
const int ct = CreateNewConstraint(0, GetObjectiveValue());
for (int variable = 0; variable < NumVariables(); variable++) {
const double coefficient = GetObjectiveCoefficient(variable);
if (coefficient != 0) {
SetCoefficient(ct, variable, coefficient);
}
}
}
void AddMaximumConstraint(int max_var, std::vector<int> vars) override {}
void AddProductConstraint(int product_var, std::vector<int> vars) override {}
void SetEnforcementLiteral(int ct, int condition) override{};
DimensionSchedulingStatus Solve(absl::Duration duration_limit) override {
lp_solver_.GetMutableParameters()->set_max_time_in_seconds(
@@ -327,6 +338,9 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper {
linear_program_.Clear();
return DimensionSchedulingStatus::INFEASIBLE;
}
if (is_relaxation_) {
return DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY;
}
for (const auto& allowed_interval : allowed_intervals_) {
const double value_double = GetValue(allowed_interval.first);
const int64_t value =
@@ -354,6 +368,7 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper {
}
private:
const bool is_relaxation_;
glop::LinearProgram linear_program_;
glop::LPSolver lp_solver_;
absl::flat_hash_map<int, std::unique_ptr<SortedDisjointIntervalList>>
@@ -362,7 +377,7 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper {
class RoutingCPSatWrapper : public RoutingLinearSolverWrapper {
public:
RoutingCPSatWrapper() : objective_offset_(0), first_constraint_to_offset_(0) {
RoutingCPSatWrapper() : first_constraint_to_offset_(0) {
parameters_.set_num_search_workers(1);
// Keeping presolve but with 0 iterations; as of 11/2019 it is
// significantly faster than both full presolve and no presolve.
@@ -377,7 +392,6 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper {
model_.Clear();
response_.Clear();
objective_coefficients_.clear();
objective_offset_ = 0;
variable_offset_.clear();
constraint_offset_.clear();
first_constraint_to_offset_ = 0;
@@ -436,17 +450,15 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper {
sat::CpObjectiveProto* const objective = model_.mutable_objective();
objective->add_vars(index);
objective->add_coeffs(coefficient);
objective_offset_ += coefficient * variable_offset_[index];
objective->set_offset(objective->offset() +
coefficient * variable_offset_[index]);
}
double GetObjectiveCoefficient(int index) const override {
return (index < objective_coefficients_.size())
? objective_coefficients_[index]
: 0;
}
void ClearObjective() override {
model_.mutable_objective()->Clear();
objective_offset_ = 0;
}
void ClearObjective() override { model_.mutable_objective()->Clear(); }
int NumVariables() const override { return model_.variables_size(); }
int CreateNewConstraint(int64_t lower_bound, int64_t upper_bound) override {
const int ct_index = model_.constraints_size();
@@ -471,6 +483,19 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper {
CapProd(variable_offset_[index], coefficient));
}
bool IsCPSATSolver() override { return true; }
void AddObjectiveConstraint() override {
const double scaling_factor =
model_.has_objective() && model_.objective().scaling_factor() != 0
? model_.objective().scaling_factor()
: 1;
const int ct =
CreateNewConstraint(0, response_.objective_value() / scaling_factor -
model_.objective().offset());
const sat::CpObjectiveProto& objective = model_.objective();
for (int i = 0; i < objective.vars_size(); ++i) {
SetCoefficient(ct, objective.vars(i), objective.coeffs(i));
}
}
void AddMaximumConstraint(int max_var, std::vector<int> vars) override {
sat::LinearArgumentProto* const ct =
model_.add_constraints()->mutable_lin_max();
@@ -505,6 +530,21 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper {
ct->set_domain(1, CapSub(ct->domain(1), constraint_offset_[ct_index]));
}
first_constraint_to_offset_ = constraint_offset_.size();
// Scale the objective
sat::CpObjectiveProto* const objective = model_.mutable_objective();
int64_t gcd(0);
for (int64_t coeff : objective->coeffs()) {
gcd = MathUtil::GCD64(gcd, std::abs(coeff));
}
if (gcd > 1) {
for (int i = 0; i < objective->coeffs_size(); ++i) {
objective->set_coeffs(i, objective->coeffs(i) / gcd);
}
objective->set_offset(objective->offset() / gcd);
objective->set_scaling_factor(gcd);
}
parameters_.set_max_time_in_seconds(absl::ToDoubleSeconds(duration_limit));
VLOG(2) << model_.DebugString();
if (hint_.vars_size() == model_.variables_size()) {
@@ -527,8 +567,7 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper {
return DimensionSchedulingStatus::INFEASIBLE;
}
int64_t GetObjectiveValue() const override {
return MathUtil::FastInt64Round(response_.objective_value() +
objective_offset_);
return MathUtil::FastInt64Round(response_.objective_value());
}
double GetValue(int index) const override {
return response_.solution(index) + variable_offset_[index];
@@ -540,7 +579,6 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper {
sat::CpSolverResponse response_;
sat::SatParameters parameters_;
std::vector<double> objective_coefficients_;
double objective_offset_;
std::vector<int64_t> variable_offset_;
std::vector<int64_t> constraint_offset_;
int first_constraint_to_offset_;
@@ -564,16 +602,16 @@ class DimensionCumulOptimizerCore {
std::vector<int64_t>* break_values, int64_t* cost, int64_t* transit_cost,
bool clear_lp = true);
bool Optimize(const std::function<int64_t(int64_t)>& next_accessor,
RoutingLinearSolverWrapper* solver,
std::vector<int64_t>* cumul_values,
std::vector<int64_t>* break_values, int64_t* cost,
int64_t* transit_cost, bool clear_lp = true);
DimensionSchedulingStatus Optimize(
const std::function<int64_t(int64_t)>& next_accessor,
RoutingLinearSolverWrapper* solver, std::vector<int64_t>* cumul_values,
std::vector<int64_t>* break_values, int64_t* cost, int64_t* transit_cost,
bool clear_lp = true);
bool OptimizeAndPack(const std::function<int64_t(int64_t)>& next_accessor,
RoutingLinearSolverWrapper* solver,
std::vector<int64_t>* cumul_values,
std::vector<int64_t>* break_values);
DimensionSchedulingStatus OptimizeAndPack(
const std::function<int64_t(int64_t)>& next_accessor,
RoutingLinearSolverWrapper* solver, std::vector<int64_t>* cumul_values,
std::vector<int64_t>* break_values);
DimensionSchedulingStatus OptimizeAndPackSingleRoute(
int vehicle, const std::function<int64_t(int64_t)>& next_accessor,
@@ -613,8 +651,10 @@ class DimensionCumulOptimizerCore {
// SetRouteCumulConstraints() has been called on all routes, so that
// index_to_cumul_variable_ and min_start/max_end_cumul_ are correctly
// initialized.
void SetGlobalConstraints(bool optimize_costs,
RoutingLinearSolverWrapper* solver);
void SetGlobalConstraints(
const std::function<int64_t(int64_t)>& next_accessor,
int64_t cumul_offset, bool optimize_costs,
RoutingLinearSolverWrapper* solver);
void SetValuesFromLP(const std::vector<int>& lp_variables, int64_t offset,
RoutingLinearSolverWrapper* solver,
@@ -709,13 +749,15 @@ class LocalDimensionCumulOptimizer {
class GlobalDimensionCumulOptimizer {
public:
explicit GlobalDimensionCumulOptimizer(const RoutingDimension* dimension);
GlobalDimensionCumulOptimizer(
const RoutingDimension* dimension,
RoutingSearchParameters::SchedulingSolver solver_type);
// If feasible, computes the optimal cost of the entire model with regards to
// the optimizer_core_'s dimension costs, minimizing cumul soft lower/upper
// bound costs and vehicle/global span costs, and stores it in "optimal_cost"
// (if not null).
// Returns true iff all the constraints can be respected.
bool ComputeCumulCostWithoutFixedTransits(
DimensionSchedulingStatus ComputeCumulCostWithoutFixedTransits(
const std::function<int64_t(int64_t)>& next_accessor,
int64_t* optimal_cost_without_transits);
// If feasible, computes the optimal values for cumul and break variables,
@@ -723,20 +765,17 @@ class GlobalDimensionCumulOptimizer {
// costs, stores them in "optimal_cumuls" (if not null) and optimal breaks,
// and returns true.
// Returns false if the routes are not feasible.
bool ComputeCumuls(const std::function<int64_t(int64_t)>& next_accessor,
std::vector<int64_t>* optimal_cumuls,
std::vector<int64_t>* optimal_breaks);
// Returns true iff the routes resulting from the next_accessor are feasible
// wrt the constraints on the optimizer_core_.dimension()'s cumuls.
bool IsFeasible(const std::function<int64_t(int64_t)>& next_accessor);
DimensionSchedulingStatus ComputeCumuls(
const std::function<int64_t(int64_t)>& next_accessor,
std::vector<int64_t>* optimal_cumuls,
std::vector<int64_t>* optimal_breaks);
// Similar to ComputeCumuls, but also tries to pack the cumul values on all
// routes, such that the cost remains the same, the cumuls of route ends are
// minimized, and then the cumuls of the starts of the routes are maximized.
bool ComputePackedCumuls(const std::function<int64_t(int64_t)>& next_accessor,
std::vector<int64_t>* packed_cumuls,
std::vector<int64_t>* packed_breaks);
DimensionSchedulingStatus ComputePackedCumuls(
const std::function<int64_t(int64_t)>& next_accessor,
std::vector<int64_t>* packed_cumuls, std::vector<int64_t>* packed_breaks);
const RoutingDimension* dimension() const {
return optimizer_core_.dimension();

View File

@@ -677,17 +677,24 @@ FilteredHeuristicLocalSearchOperator::FilteredHeuristicLocalSearchOperator(
bool keep_inverse_values)
: IntVarLocalSearchOperator(heuristic->model()->Nexts(),
keep_inverse_values),
model_(*heuristic->model()),
removed_nodes_(model_.Size()),
model_(heuristic->model()),
removed_nodes_(model_->Size()),
heuristic_(std::move(heuristic)),
consider_vehicle_vars_(!model_.CostsAreHomogeneousAcrossVehicles()) {
consider_vehicle_vars_(!model_->CostsAreHomogeneousAcrossVehicles()) {
if (consider_vehicle_vars_) {
AddVars(model_.VehicleVars());
AddVars(model_->VehicleVars());
}
}
bool FilteredHeuristicLocalSearchOperator::MakeOneNeighbor() {
while (IncrementPosition()) {
if (model_->CheckLimit()) {
// NOTE: Even though the limit is checked in the BuildSolutionFromRoutes()
// method of the heuristics, we still check it here to avoid calling
// IncrementPosition() and building a solution for every possible position
// if the time limit is reached.
return false;
}
// NOTE: No need to call RevertChanges() here as MakeChangeAndInsertNodes()
// will always return true if any change was made.
if (MakeChangesAndInsertNodes()) {
@@ -715,13 +722,13 @@ bool FilteredHeuristicLocalSearchOperator::MakeChangesAndInsertNodes() {
bool has_change = false;
const std::vector<IntVarElement>& elements =
result_assignment->IntVarContainer().elements();
for (int vehicle = 0; vehicle < model_.vehicles(); vehicle++) {
int64_t node_index = model_.Start(vehicle);
while (!model_.IsEnd(node_index)) {
for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) {
int64_t node_index = model_->Start(vehicle);
while (!model_->IsEnd(node_index)) {
// NOTE: When building the solution in the heuristic, Next vars are added
// to the assignment at the position corresponding to their index.
const IntVarElement& node_element = elements[node_index];
DCHECK_EQ(node_element.Var(), model_.NextVar(node_index));
DCHECK_EQ(node_element.Var(), model_->NextVar(node_index));
const int64_t new_node_value = node_element.Value();
DCHECK_NE(new_node_value, node_index);
@@ -742,7 +749,7 @@ bool FilteredHeuristicLocalSearchOperator::MakeChangesAndInsertNodes() {
// the heuristic.
for (int64_t node : removed_nodes_.PositionsSetAtLeastOnce()) {
const IntVarElement& node_element = elements[node];
DCHECK_EQ(node_element.Var(), model_.NextVar(node));
DCHECK_EQ(node_element.Var(), model_->NextVar(node));
if (node_element.Value() == node) {
DCHECK_NE(OldValue(node), node);
has_change = true;
@@ -786,11 +793,11 @@ bool FilteredHeuristicPathLNSOperator::IncrementPosition() {
}
bool FilteredHeuristicPathLNSOperator::CurrentRouteIsEmpty() const {
return model_.IsEnd(OldValue(model_.Start(current_route_)));
return model_->IsEnd(OldValue(model_->Start(current_route_)));
}
void FilteredHeuristicPathLNSOperator::IncrementCurrentRouteToNextNonEmpty() {
const int num_routes = model_.vehicles();
const int num_routes = model_->vehicles();
do {
++current_route_ %= num_routes;
if (current_route_ == last_route_) {
@@ -802,8 +809,8 @@ void FilteredHeuristicPathLNSOperator::IncrementCurrentRouteToNextNonEmpty() {
std::function<int64_t(int64_t)>
FilteredHeuristicPathLNSOperator::SetupNextAccessorForNeighbor() {
const int64_t start_node = model_.Start(current_route_);
const int64_t end_node = model_.End(current_route_);
const int64_t start_node = model_->Start(current_route_);
const int64_t end_node = model_->End(current_route_);
int64_t node = Value(start_node);
while (node != end_node) {
@@ -829,30 +836,30 @@ RelocatePathAndHeuristicInsertUnperformedOperator::
void RelocatePathAndHeuristicInsertUnperformedOperator::OnStart() {
has_unperformed_nodes_ = false;
last_node_on_route_.resize(model_.vehicles());
last_node_on_route_.resize(model_->vehicles());
routes_to_relocate_.clear();
empty_routes_.clear();
std::vector<bool> empty_vehicle_of_vehicle_class_added(
model_.GetVehicleClassesCount(), false);
for (int64_t node = 0; node < model_.Size(); node++) {
model_->GetVehicleClassesCount(), false);
for (int64_t node = 0; node < model_->Size(); node++) {
const int64_t next = OldValue(node);
if (next == node) {
has_unperformed_nodes_ = true;
continue;
}
if (model_.IsEnd(next)) {
last_node_on_route_[model_.VehicleIndex(next)] = node;
if (model_->IsEnd(next)) {
last_node_on_route_[model_->VehicleIndex(next)] = node;
}
}
for (int vehicle = 0; vehicle < model_.vehicles(); vehicle++) {
const int64_t next = OldValue(model_.Start(vehicle));
if (!model_.IsEnd(next)) {
for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) {
const int64_t next = OldValue(model_->Start(vehicle));
if (!model_->IsEnd(next)) {
routes_to_relocate_.push_back(vehicle);
continue;
}
const int vehicle_class =
model_.GetVehicleClassIndexOfVehicle(vehicle).value();
model_->GetVehicleClassIndexOfVehicle(vehicle).value();
if (!empty_vehicle_of_vehicle_class_added[vehicle_class]) {
empty_routes_.push_back(vehicle);
empty_vehicle_of_vehicle_class_added[vehicle_class] = true;
@@ -897,19 +904,19 @@ RelocatePathAndHeuristicInsertUnperformedOperator::
SetupNextAccessorForNeighbor() {
const int empty_route = empty_routes_[empty_route_index_];
const int relocated_route = routes_to_relocate_[route_to_relocate_index_];
if (model_.GetVehicleClassIndexOfVehicle(empty_route) ==
model_.GetVehicleClassIndexOfVehicle(relocated_route)) {
if (model_->GetVehicleClassIndexOfVehicle(empty_route) ==
model_->GetVehicleClassIndexOfVehicle(relocated_route)) {
// Don't try to relocate the route to an empty vehicle of the same class.
return nullptr;
}
const int64_t empty_start_node = model_.Start(empty_route);
const int64_t empty_end_node = model_.End(empty_route);
const int64_t empty_start_node = model_->Start(empty_route);
const int64_t empty_end_node = model_->End(empty_route);
const int64_t relocated_route_start = model_.Start(relocated_route);
const int64_t relocated_route_start = model_->Start(relocated_route);
const int64_t first_relocated_node = OldValue(relocated_route_start);
const int64_t last_relocated_node = last_node_on_route_[relocated_route];
const int64_t relocated_route_end = model_.End(relocated_route);
const int64_t relocated_route_end = model_->End(relocated_route);
return [this, empty_start_node, empty_end_node, first_relocated_node,
last_relocated_node, relocated_route_start,
@@ -927,33 +934,33 @@ FilteredHeuristicCloseNodesLNSOperator::FilteredHeuristicCloseNodesLNSOperator(
std::unique_ptr<RoutingFilteredHeuristic> heuristic, int num_close_nodes)
: FilteredHeuristicLocalSearchOperator(std::move(heuristic),
/*keep_inverse_values*/ true),
pickup_delivery_pairs_(model_.GetPickupAndDeliveryPairs()),
pickup_delivery_pairs_(model_->GetPickupAndDeliveryPairs()),
current_node_(0),
last_node_(0),
close_nodes_(model_.Size()),
new_nexts_(model_.Size()),
changed_nexts_(model_.Size()),
new_prevs_(model_.Size()),
changed_prevs_(model_.Size()) {
const int64_t size = model_.Size();
close_nodes_(model_->Size()),
new_nexts_(model_->Size()),
changed_nexts_(model_->Size()),
new_prevs_(model_->Size()),
changed_prevs_(model_->Size()) {
const int64_t size = model_->Size();
const int64_t max_num_neighbors =
std::max<int64_t>(0, size - 1 - model_.vehicles());
std::max<int64_t>(0, size - 1 - model_->vehicles());
const int64_t num_closest_neighbors =
std::min<int64_t>(num_close_nodes, max_num_neighbors);
DCHECK_GE(num_closest_neighbors, 0);
if (num_closest_neighbors == 0) return;
const int64_t num_cost_classes = model_.GetCostClassesCount();
const int64_t num_cost_classes = model_->GetCostClassesCount();
for (int64_t node = 0; node < size; node++) {
if (model_.IsStart(node) || model_.IsEnd(node)) continue;
if (model_->IsStart(node) || model_->IsEnd(node)) continue;
std::vector<std::pair</*cost*/ double, /*node*/ int64_t>>
costed_after_nodes;
costed_after_nodes.reserve(size);
for (int64_t after_node = 0; after_node < size; after_node++) {
if (model_.IsStart(after_node) || model_.IsEnd(after_node) ||
if (model_->IsStart(after_node) || model_->IsEnd(after_node) ||
after_node == node) {
continue;
}
@@ -961,7 +968,7 @@ FilteredHeuristicCloseNodesLNSOperator::FilteredHeuristicCloseNodesLNSOperator(
// NOTE: We don't consider the 'always-zero' cost class when searching for
// closest neighbors.
for (int cost_class = 1; cost_class < num_cost_classes; cost_class++) {
total_cost += model_.GetArcCostForClass(node, after_node, cost_class);
total_cost += model_->GetArcCostForClass(node, after_node, cost_class);
}
costed_after_nodes.emplace_back(total_cost, after_node);
}
@@ -987,12 +994,12 @@ bool FilteredHeuristicCloseNodesLNSOperator::IncrementPosition() {
just_started_ = false;
return true;
}
++current_node_ %= model_.Size();
++current_node_ %= model_->Size();
return current_node_ != last_node_;
}
void FilteredHeuristicCloseNodesLNSOperator::RemoveNode(int64_t node) {
DCHECK(!model_.IsEnd(node) && !model_.IsStart(node));
DCHECK(!model_->IsEnd(node) && !model_->IsStart(node));
DCHECK_NE(Value(node), node);
DCHECK(IsActive(node));
@@ -1001,7 +1008,7 @@ void FilteredHeuristicCloseNodesLNSOperator::RemoveNode(int64_t node) {
const int64_t next = Next(node);
changed_nexts_.Set(prev);
new_nexts_[prev] = next;
if (next < model_.Size()) {
if (next < model_->Size()) {
changed_prevs_.Set(next);
new_prevs_[next] = prev;
}
@@ -1013,7 +1020,7 @@ void FilteredHeuristicCloseNodesLNSOperator::RemoveNodeAndActiveSibling(
RemoveNode(node);
for (int64_t sibling_node : GetActiveSiblings(node)) {
if (!model_.IsStart(sibling_node) && !model_.IsEnd(sibling_node)) {
if (!model_->IsStart(sibling_node) && !model_->IsEnd(sibling_node)) {
RemoveNode(sibling_node);
}
}
@@ -1026,7 +1033,7 @@ std::vector<int64_t> FilteredHeuristicCloseNodesLNSOperator::GetActiveSiblings(
// alternative deliveries or pickups for this index pair.
std::vector<int64_t> active_siblings;
for (std::pair<int64_t, int64_t> index_pair :
model_.GetPickupIndexPairs(node)) {
model_->GetPickupIndexPairs(node)) {
for (int64_t sibling_delivery :
pickup_delivery_pairs_[index_pair.first].second) {
if (IsActive(sibling_delivery)) {
@@ -1036,7 +1043,7 @@ std::vector<int64_t> FilteredHeuristicCloseNodesLNSOperator::GetActiveSiblings(
}
}
for (std::pair<int64_t, int64_t> index_pair :
model_.GetDeliveryIndexPairs(node)) {
model_->GetDeliveryIndexPairs(node)) {
for (int64_t sibling_pickup :
pickup_delivery_pairs_[index_pair.first].first) {
if (IsActive(sibling_pickup)) {
@@ -1050,10 +1057,10 @@ std::vector<int64_t> FilteredHeuristicCloseNodesLNSOperator::GetActiveSiblings(
std::function<int64_t(int64_t)>
FilteredHeuristicCloseNodesLNSOperator::SetupNextAccessorForNeighbor() {
if (model_.IsStart(current_node_)) {
if (model_->IsStart(current_node_)) {
return nullptr;
}
DCHECK(!model_.IsEnd(current_node_));
DCHECK(!model_->IsEnd(current_node_));
changed_nexts_.SparseClearAll();
changed_prevs_.SparseClearAll();
@@ -1135,7 +1142,7 @@ FilteredHeuristicExpensiveChainLNSOperator::SetupNextAccessorForNeighbor() {
}
bool FilteredHeuristicExpensiveChainLNSOperator::IncrementRoute() {
++current_route_ %= model_.vehicles();
++current_route_ %= model_->vehicles();
return current_route_ != last_route_;
}
@@ -1219,9 +1226,9 @@ bool FilteredHeuristicExpensiveChainLNSOperator::
FindMostExpensiveChainsOnRemainingRoutes() {
do {
if (FindMostExpensiveArcsOnRoute(
num_arcs_to_consider_, model_.Start(current_route_),
num_arcs_to_consider_, model_->Start(current_route_),
[this](int64_t i) { return OldValue(i); },
[this](int64_t node) { return model_.IsEnd(node); },
[this](int64_t node) { return model_->IsEnd(node); },
arc_cost_for_route_start_, &most_expensive_arc_starts_and_ranks_,
&current_expensive_arc_indices_)) {
return true;

View File

@@ -385,7 +385,7 @@ class FilteredHeuristicLocalSearchOperator : public IntVarLocalSearchOperator {
// TODO(user): Remove the dependency from RoutingModel by storing an
// IntVarFilteredHeuristic here instead and storing information on path
// start/ends like PathOperator does (instead of relying on the model).
const RoutingModel& model_;
RoutingModel* const model_;
/// Keeps track of removed nodes when making a neighbor.
SparseBitset<> removed_nodes_;
@@ -393,7 +393,7 @@ class FilteredHeuristicLocalSearchOperator : public IntVarLocalSearchOperator {
bool MakeOneNeighbor() override;
bool MakeChangesAndInsertNodes();
int64_t VehicleVarIndex(int64_t node) const { return model_.Size() + node; }
int64_t VehicleVarIndex(int64_t node) const { return model_->Size() + node; }
const std::unique_ptr<RoutingFilteredHeuristic> heuristic_;
const bool consider_vehicle_vars_;
@@ -528,7 +528,7 @@ class FilteredHeuristicCloseNodesLNSOperator
void RemoveNodeAndActiveSibling(int64_t node);
bool IsActive(int64_t node) const {
DCHECK_LT(node, model_.Size());
DCHECK_LT(node, model_->Size());
return Value(node) != node && !removed_nodes_[node];
}
@@ -538,7 +538,7 @@ class FilteredHeuristicCloseNodesLNSOperator
return changed_prevs_[node] ? new_prevs_[node] : InverseValue(node);
}
int64_t Next(int64_t node) const {
DCHECK(!model_.IsEnd(node));
DCHECK(!model_->IsEnd(node));
return changed_nexts_[node] ? new_nexts_[node] : Value(node);
}

View File

@@ -26,6 +26,7 @@
#include "ortools/constraint_solver/constraint_solver.h"
#include "ortools/constraint_solver/routing_enums.pb.h"
#include "ortools/constraint_solver/solver_parameters.pb.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/util/optional_boolean.pb.h"
namespace operations_research {
@@ -107,6 +108,8 @@ RoutingSearchParameters DefaultRoutingSearchParameters() {
"use_depth_first_search: false "
"use_cp: BOOL_TRUE "
"use_cp_sat: BOOL_FALSE "
"use_generalized_cp_sat: BOOL_FALSE "
"sat_parameters { linearization_level: 2 num_search_workers: 1 } "
"continuous_scheduling_solver: GLOP "
"mixed_integer_scheduling_solver: CP_SAT "
"optimization_step: 0.0 "
@@ -370,6 +373,17 @@ std::string FindErrorInRoutingSearchParameters(
}
}
{
const sat::SatParameters& sat_parameters =
search_parameters.sat_parameters();
if (sat_parameters.enumerate_all_solutions() &&
(sat_parameters.num_search_workers() > 1 ||
sat_parameters.interleave_search())) {
return "sat_parameters.enumerate_all_solutions cannot be true in parallel"
" search";
}
}
return ""; // = Valid (No error).
}

View File

@@ -24,6 +24,7 @@ option csharp_namespace = "Google.OrTools.ConstraintSolver";
import "google/protobuf/duration.proto";
import "ortools/constraint_solver/routing_enums.proto";
import "ortools/constraint_solver/solver_parameters.proto";
import "ortools/sat/sat_parameters.proto";
import "ortools/util/optional_boolean.proto";
package operations_research;
@@ -34,7 +35,7 @@ package operations_research;
// then the routing library will pick its preferred value for that parameter
// automatically: this should be the case for most parameters.
// To see those "default" parameters, call GetDefaultRoutingSearchParameters().
// Next ID: 47
// Next ID: 49
message RoutingSearchParameters {
// First solution strategies, used as starting point of local search.
FirstSolutionStrategy.Value first_solution_strategy = 1;
@@ -423,6 +424,14 @@ message RoutingSearchParameters {
// remaining and will use the CP solution as a hint for the CP-SAT search.
// As of 5/2019, only TSP models can be solved.
OptionalBoolean use_cp_sat = 27;
// If true, use the CP-SAT solver to find a solution on generalized routing
// model. If use_cp is also true, the CP-SAT solver will be run after the CP
// solver if there is time remaining and will use the CP solution as a hint
// for the CP-SAT search.
OptionalBoolean use_generalized_cp_sat = 47;
// If use_cp_sat or use_generalized_cp_sat is true, contains the SAT algorithm
// parameters which will be used.
sat.SatParameters sat_parameters = 48;
// Underlying solver to use in dimension scheduling, respectively for
// continuous and mixed models.
enum SchedulingSolver {

View File

@@ -20,6 +20,7 @@
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/time/time.h"
#include "ortools/base/integral_types.h"
#include "ortools/base/logging.h"
@@ -27,11 +28,13 @@
#include "ortools/constraint_solver/constraint_solver.h"
#include "ortools/constraint_solver/routing.h"
#include "ortools/constraint_solver/routing_parameters.pb.h"
#include "ortools/constraint_solver/routing_types.h"
#include "ortools/sat/cp_model.pb.h"
#include "ortools/sat/cp_model_solver.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/model.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/util/optional_boolean.pb.h"
#include "ortools/util/saturated_arithmetic.h"
namespace operations_research {
@@ -55,6 +58,34 @@ int AddVariable(CpModelProto* cp_model, int64_t lb, int64_t ub) {
return index;
}
// Adds a linear constraint, enforcing
// enforcement_literals -> lower_bound <= sum variable * coeff <= upper_bound.
void AddLinearConstraint(
CpModelProto* cp_model, int64_t lower_bound, int64_t upper_bound,
const std::vector<std::pair<int, double>>& variable_coeffs,
const std::vector<int>& enforcement_literals) {
CHECK_LE(lower_bound, upper_bound);
ConstraintProto* ct = cp_model->add_constraints();
for (const int enforcement_literal : enforcement_literals) {
ct->add_enforcement_literal(enforcement_literal);
}
LinearConstraintProto* arg = ct->mutable_linear();
arg->add_domain(lower_bound);
arg->add_domain(upper_bound);
for (const auto [var, coeff] : variable_coeffs) {
arg->add_vars(var);
arg->add_coeffs(coeff);
}
}
// Adds a linear constraint, enforcing
// lower_bound <= sum variable * coeff <= upper_bound.
void AddLinearConstraint(
CpModelProto* cp_model, int64_t lower_bound, int64_t upper_bound,
const std::vector<std::pair<int, double>>& variable_coeffs) {
AddLinearConstraint(cp_model, lower_bound, upper_bound, variable_coeffs, {});
}
// Returns the unique depot node used in the CP-SAT models (as of 01/2020).
int64_t GetDepotFromModel(const RoutingModel& model) { return model.Start(0); }
@@ -112,15 +143,9 @@ void AddDimensions(const RoutingModel& model, const ArcVarMap& arc_vars,
if (tail == head || model.IsStart(tail) || model.IsStart(head)) continue;
// arc[tail][head] -> cumuls[head] >= cumuls[tail] + transit.
// This is a relaxation of the model as it does not consider slack max.
ConstraintProto* ct = cp_model->add_constraints();
ct->add_enforcement_literal(arc_var.second);
LinearConstraintProto* arg = ct->mutable_linear();
arg->add_domain(transit(tail, head));
arg->add_domain(std::numeric_limits<int64_t>::max());
arg->add_vars(cumuls[tail]);
arg->add_coeffs(-1);
arg->add_vars(cumuls[head]);
arg->add_coeffs(1);
AddLinearConstraint(
cp_model, transit(tail, head), std::numeric_limits<int64_t>::max(),
{{cumuls[head], 1}, {cumuls[tail], -1}}, {arc_var.second});
}
}
}
@@ -142,15 +167,8 @@ std::vector<int> CreateRanks(const RoutingModel& model,
const int head = arc_var.first.head;
if (tail == head || head == depot) continue;
// arc[tail][head] -> ranks[head] == ranks[tail] + 1.
ConstraintProto* ct = cp_model->add_constraints();
ct->add_enforcement_literal(arc_var.second);
LinearConstraintProto* arg = ct->mutable_linear();
arg->add_domain(1);
arg->add_domain(1);
arg->add_vars(ranks[tail]);
arg->add_coeffs(-1);
arg->add_vars(ranks[head]);
arg->add_coeffs(1);
AddLinearConstraint(cp_model, 1, 1, {{ranks[head], 1}, {ranks[tail], -1}},
{arc_var.second});
}
return ranks;
}
@@ -175,25 +193,14 @@ std::vector<int> CreateVehicleVars(const RoutingModel& model,
if (tail == head || head == depot) continue;
if (tail == depot) {
// arc[depot][head] -> vehicles[head] == head.
ConstraintProto* ct = cp_model->add_constraints();
ct->add_enforcement_literal(arc_var.second);
LinearConstraintProto* arg = ct->mutable_linear();
arg->add_domain(head);
arg->add_domain(head);
arg->add_vars(vehicles[head]);
arg->add_coeffs(1);
AddLinearConstraint(cp_model, head, head, {{vehicles[head], 1}},
{arc_var.second});
continue;
}
// arc[tail][head] -> vehicles[head] == vehicles[tail].
ConstraintProto* ct = cp_model->add_constraints();
ct->add_enforcement_literal(arc_var.second);
LinearConstraintProto* arg = ct->mutable_linear();
arg->add_domain(0);
arg->add_domain(0);
arg->add_vars(vehicles[tail]);
arg->add_coeffs(-1);
arg->add_vars(vehicles[head]);
arg->add_coeffs(1);
AddLinearConstraint(cp_model, 0, 0,
{{vehicles[head], 1}, {vehicles[tail], -1}},
{arc_var.second});
}
return vehicles;
}
@@ -208,28 +215,12 @@ void AddPickupDeliveryConstraints(const RoutingModel& model,
for (const auto& pairs : model.GetPickupAndDeliveryPairs()) {
const int64_t pickup = pairs.first[0];
const int64_t delivery = pairs.second[0];
{
// ranks[pickup] + 1 <= ranks[delivery].
ConstraintProto* ct = cp_model->add_constraints();
LinearConstraintProto* arg = ct->mutable_linear();
arg->add_domain(1);
arg->add_domain(std::numeric_limits<int64_t>::max());
arg->add_vars(ranks[delivery]);
arg->add_coeffs(1);
arg->add_vars(ranks[pickup]);
arg->add_coeffs(-1);
}
{
// vehicles[pickup] == vehicles[delivery]
ConstraintProto* ct = cp_model->add_constraints();
LinearConstraintProto* arg = ct->mutable_linear();
arg->add_domain(0);
arg->add_domain(0);
arg->add_vars(vehicles[delivery]);
arg->add_coeffs(1);
arg->add_vars(vehicles[pickup]);
arg->add_coeffs(-1);
}
// ranks[pickup] + 1 <= ranks[delivery].
AddLinearConstraint(cp_model, 1, std::numeric_limits<int64_t>::max(),
{{ranks[delivery], 1}, {ranks[pickup], -1}});
// vehicles[pickup] == vehicles[delivery]
AddLinearConstraint(cp_model, 0, 0,
{{vehicles[delivery], 1}, {vehicles[pickup], -1}});
}
}
@@ -257,7 +248,7 @@ ArcVarMap PopulateMultiRouteModelFromRoutingModel(const RoutingModel& model,
// as the CP solver will reject those.
if (model.IsStart(head)) continue;
const int head_index = model.IsEnd(head) ? depot : head;
if (head_index == tail_index) continue;
if (head_index == tail_index && head_index == depot) continue;
const int64_t cost = tail != head ? model.GetHomogeneousCost(tail, head)
: model.UnperformedPenalty(tail);
if (cost == std::numeric_limits<int64_t>::max()) continue;
@@ -270,80 +261,19 @@ ArcVarMap PopulateMultiRouteModelFromRoutingModel(const RoutingModel& model,
}
}
// The following flow constraints seem to be necessary with the Route
// constraint, greatly improving performance due to stronger LP relaxation
// (supposedly).
// TODO(user): Remove these constraints when the Route constraint handles
// LP relaxations properly.
// Limit the number of routes to the maximum number of vehicles.
{
LinearConstraintProto* ct = cp_model->add_constraints()->mutable_linear();
ct->add_domain(0);
ct->add_domain(0);
for (int node = 0; node < num_nodes; ++node) {
if (model.IsStart(node) || model.IsEnd(node)) continue;
int* const depot_node_var = gtl::FindOrNull(arc_vars, {depot, node});
if (depot_node_var == nullptr) continue;
ct->add_vars(*depot_node_var);
ct->add_coeffs(1);
int* const node_depot_var = gtl::FindOrNull(arc_vars, {node, depot});
if (node_depot_var == nullptr) continue;
ct->add_vars(*node_depot_var);
ct->add_coeffs(-1);
}
}
{
LinearConstraintProto* ct = cp_model->add_constraints()->mutable_linear();
ct->add_domain(0);
// Taking the min since as of 04/2020 fleet is homogeneous.
ct->add_domain(
std::min(model.vehicles(), model.GetMaximumNumberOfActiveVehicles()));
std::vector<std::pair<int, double>> variable_coeffs;
for (int node = 0; node < num_nodes; ++node) {
if (model.IsStart(node) || model.IsEnd(node)) continue;
int* const var = gtl::FindOrNull(arc_vars, {depot, node});
if (var == nullptr) continue;
ct->add_vars(*var);
ct->add_coeffs(1);
}
}
for (int tail = 0; tail < num_nodes; ++tail) {
if (model.IsStart(tail) || model.IsEnd(tail)) continue;
LinearConstraintProto* ct = cp_model->add_constraints()->mutable_linear();
ct->add_domain(1);
ct->add_domain(1);
std::unique_ptr<IntVarIterator> iter(
model.NextVar(tail)->MakeDomainIterator(false));
bool depot_added = false;
for (int head : InitAndGetValues(iter.get())) {
if (model.IsStart(head)) continue;
if (tail == head) continue;
if (model.IsEnd(head)) {
if (depot_added) continue;
head = depot;
depot_added = true;
}
int* const var = gtl::FindOrNull(arc_vars, {tail, head});
if (var == nullptr) continue;
ct->add_vars(*var);
ct->add_coeffs(1);
}
}
for (int head = 0; head < num_nodes; ++head) {
if (model.IsStart(head) || model.IsEnd(head)) continue;
LinearConstraintProto* ct = cp_model->add_constraints()->mutable_linear();
ct->add_domain(1);
ct->add_domain(1);
for (int tail = 0; tail < num_nodes; ++tail) {
if (model.IsEnd(head)) continue;
if (tail == head) continue;
if (model.IsStart(tail) && tail != depot) continue;
int* const var = gtl::FindOrNull(arc_vars, {tail, head});
if (var == nullptr) continue;
ct->add_vars(*var);
ct->add_coeffs(1);
variable_coeffs.push_back({*var, 1});
}
AddLinearConstraint(
cp_model, 0,
std::min(model.vehicles(), model.GetMaximumNumberOfActiveVehicles()),
variable_coeffs);
}
AddPickupDeliveryConstraints(model, arc_vars, cp_model);
@@ -471,6 +401,586 @@ bool ConvertToSolution(const CpSolverResponse& response,
return true;
}
// Adds dimensions to a CpModelProto for heterogeneous fleet. Adds path
// cumul constraints and cumul bounds.
void AddGeneralizedDimensions(
const RoutingModel& model, const ArcVarMap& arc_vars,
const std::vector<absl::flat_hash_map<int, int>>& vehicle_performs_node,
const std::vector<absl::flat_hash_map<int, int>>&
vehicle_class_performs_arc,
CpModelProto* cp_model) {
const int num_cp_nodes = model.Nexts().size() + model.vehicles() + 1;
for (const RoutingDimension* dimension : model.GetDimensions()) {
// Initialize cumuls.
std::vector<int> cumuls(num_cp_nodes, -1);
for (int cp_node = 1; cp_node < num_cp_nodes; ++cp_node) {
const int node = cp_node - 1;
int64_t cumul_min = dimension->cumuls()[node]->Min();
int64_t cumul_max = dimension->cumuls()[node]->Max();
if (model.IsStart(node) || model.IsEnd(node)) {
const int vehicle = model.VehicleIndex(node);
cumul_max =
std::min(cumul_max, dimension->vehicle_capacities()[vehicle]);
}
cumuls[cp_node] = AddVariable(cp_model, cumul_min, cumul_max);
}
// Constrain cumuls with vehicle capacities.
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
if (!vehicle_performs_node[vehicle].contains(cp_node)) continue;
const int64_t vehicle_capacity =
dimension->vehicle_capacities()[vehicle];
AddLinearConstraint(cp_model, std::numeric_limits<int64_t>::min(),
vehicle_capacity, {{cumuls[cp_node], 1}},
{vehicle_performs_node[vehicle].at(cp_node)});
}
}
for (auto vehicle_class = RoutingVehicleClassIndex(0);
vehicle_class < model.GetVehicleClassesCount(); vehicle_class++) {
std::vector<int> slack(num_cp_nodes, -1);
const int64_t span_cost =
dimension->GetSpanCostCoefficientForVehicleClass(vehicle_class);
for (const auto [arc, arc_var] : arc_vars) {
const auto [cp_tail, cp_head] = arc;
if (cp_tail == cp_head || cp_tail == 0 || cp_head == 0) continue;
if (!vehicle_class_performs_arc[vehicle_class.value()].contains(
arc_var)) {
continue;
}
// Create slack variable and add span cost to the objective.
if (slack[cp_tail] == -1) {
const int64_t slack_max =
cp_tail - 1 < dimension->slacks().size()
? dimension->slacks()[cp_tail - 1]->Max()
: 0;
slack[cp_tail] = AddVariable(cp_model, 0, slack_max);
if (slack_max > 0 && span_cost > 0) {
cp_model->mutable_objective()->add_vars(slack[cp_tail]);
cp_model->mutable_objective()->add_coeffs(span_cost);
}
}
const int64_t transit = dimension->class_transit_evaluator(
vehicle_class)(cp_tail - 1, cp_head - 1);
// vehicle_class_performs_arc[vehicle][arc_var] = 1 ->
// cumuls[cp_head] - cumuls[cp_tail] - slack[cp_tail] = transit
AddLinearConstraint(
cp_model, transit, transit,
{{cumuls[cp_head], 1}, {cumuls[cp_tail], -1}, {slack[cp_tail], -1}},
{vehicle_class_performs_arc[vehicle_class.value()].at(arc_var)});
}
}
// Constrain cumuls with span limits.
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
const int64_t span_limit =
dimension->vehicle_span_upper_bounds()[vehicle];
if (span_limit == std::numeric_limits<int64_t>::max()) continue;
int cp_start = model.Start(vehicle) + 1;
int cp_end = model.End(vehicle) + 1;
AddLinearConstraint(cp_model, std::numeric_limits<int64_t>::min(),
span_limit,
{{cumuls[cp_end], 1}, {cumuls[cp_start], -1}});
}
// Set soft span upper bound costs.
if (dimension->HasSoftSpanUpperBounds()) {
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
const auto [bound, cost] =
dimension->GetSoftSpanUpperBoundForVehicle(vehicle);
const int cp_start = model.Start(vehicle) + 1;
const int cp_end = model.End(vehicle) + 1;
const int extra =
AddVariable(cp_model, 0,
std::min(dimension->cumuls()[model.End(vehicle)]->Max(),
dimension->vehicle_capacities()[vehicle]));
// -inf <= cumuls[cp_end] - cumuls[cp_start] - extra <= bound
AddLinearConstraint(
cp_model, std::numeric_limits<int64_t>::min(), bound,
{{cumuls[cp_end], 1}, {cumuls[cp_start], -1}, {extra, -1}});
// Add extra * cost to objective.
cp_model->mutable_objective()->add_vars(extra);
cp_model->mutable_objective()->add_coeffs(cost);
}
}
}
}
std::vector<int> CreateGeneralizedRanks(const RoutingModel& model,
const ArcVarMap& arc_vars,
const std::vector<int>& is_unperformed,
CpModelProto* cp_model) {
const int depot = 0;
const int num_cp_nodes = model.Nexts().size() + model.vehicles() + 1;
// Maximum length of a single route (excluding the depot & vehicle end nodes).
const int max_rank = num_cp_nodes - 2 * model.vehicles();
std::vector<int> ranks(num_cp_nodes, -1);
ranks[depot] = AddVariable(cp_model, 0, 0);
for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
if (model.IsEnd(cp_node - 1)) continue;
ranks[cp_node] = AddVariable(cp_model, 0, max_rank);
// For unperformed nodes rank is 0.
AddLinearConstraint(cp_model, 0, 0, {{ranks[cp_node], 1}},
{is_unperformed[cp_node]});
}
for (const auto [arc, arc_var] : arc_vars) {
const auto [cp_tail, cp_head] = arc;
if (model.IsEnd(cp_head - 1)) continue;
if (cp_tail == cp_head || cp_head == depot) continue;
// arc[tail][head] -> ranks[head] == ranks[tail] + 1.
AddLinearConstraint(cp_model, 1, 1,
{{ranks[cp_head], 1}, {ranks[cp_tail], -1}}, {arc_var});
}
return ranks;
}
void AddGeneralizedPickupDeliveryConstraints(
const RoutingModel& model, const ArcVarMap& arc_vars,
const std::vector<absl::flat_hash_map<int, int>>& vehicle_performs_node,
const std::vector<int>& is_unperformed, CpModelProto* cp_model) {
if (model.GetPickupAndDeliveryPairs().empty()) return;
const std::vector<int> ranks =
CreateGeneralizedRanks(model, arc_vars, is_unperformed, cp_model);
for (const auto& pairs : model.GetPickupAndDeliveryPairs()) {
for (const int delivery : pairs.second) {
const int cp_delivery = delivery + 1;
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
const Arc vehicle_start_delivery_arc = {
static_cast<int>(model.Start(vehicle) + 1), cp_delivery};
if (gtl::ContainsKey(arc_vars, vehicle_start_delivery_arc)) {
// Forbid vehicle_start -> delivery arc.
AddLinearConstraint(cp_model, 0, 0,
{{arc_vars.at(vehicle_start_delivery_arc), 1}});
}
}
for (const int pickup : pairs.first) {
const int cp_pickup = pickup + 1;
const Arc delivery_pickup_arc = {cp_delivery, cp_pickup};
if (gtl::ContainsKey(arc_vars, delivery_pickup_arc)) {
// Forbid delivery -> pickup arc.
AddLinearConstraint(cp_model, 0, 0,
{{arc_vars.at(delivery_pickup_arc), 1}});
}
DCHECK_GE(is_unperformed[cp_delivery], 0);
DCHECK_GE(is_unperformed[cp_pickup], 0);
// A negative index i refers to NOT the literal at index -i - 1.
// -i - 1 ~ NOT i, if value of i in [0, 1] (boolean).
const int delivery_performed = -is_unperformed[cp_delivery] - 1;
const int pickup_performed = -is_unperformed[cp_pickup] - 1;
// The same vehicle performs pickup and delivery.
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
// delivery_performed & pickup_performed ->
// vehicle_performs_node[vehicle][cp_delivery] -
// vehicle_performs_node[vehicle][cp_pickup] = 0
AddLinearConstraint(
cp_model, 0, 0,
{{vehicle_performs_node[vehicle].at(cp_delivery), 1},
{vehicle_performs_node[vehicle].at(cp_pickup), -1}},
{delivery_performed, pickup_performed});
}
}
}
std::vector<std::pair<int, double>> ranks_difference;
// -SUM(pickup)ranks[pickup].
for (const int pickup : pairs.first) {
const int cp_pickup = pickup + 1;
ranks_difference.push_back({ranks[cp_pickup], -1});
}
// SUM(delivery)ranks[delivery].
for (const int delivery : pairs.second) {
const int cp_delivery = delivery + 1;
ranks_difference.push_back({ranks[cp_delivery], 1});
}
// SUM(delivery)ranks[delivery] - SUM(pickup)ranks[pickup] >= 1
AddLinearConstraint(cp_model, 1, std::numeric_limits<int64_t>::max(),
ranks_difference);
}
}
// Converts a RoutingModel to CpModelProto for models with multiple
// vehicles. The node 0 is depot. All nodes in CpModel have index increased
// by 1 in comparison to the RoutingModel. Each start node has only 1
// incoming arc (from depot), each end node has only 1 outgoing arc (to
// depot). The mapping from CPModelProto arcs to their corresponding arc
// variable is returned.
ArcVarMap PopulateGeneralizedRouteModelFromRoutingModel(
const RoutingModel& model, CpModelProto* cp_model) {
ArcVarMap arc_vars;
const int depot = 0;
const int num_nodes = model.Nexts().size();
const int num_cp_nodes = num_nodes + model.vehicles() + 1;
// vehicle_performs_node[vehicle][node] equals to 1 if the vehicle performs
// the node, and 0 otherwise.
std::vector<absl::flat_hash_map<int, int>> vehicle_performs_node(
model.vehicles());
// Connect vehicles start and end nodes to depot.
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
const int cp_start = model.Start(vehicle) + 1;
const Arc start_arc = {depot, cp_start};
const int start_arc_var = AddVariable(cp_model, 1, 1);
DCHECK(!gtl::ContainsKey(arc_vars, start_arc));
arc_vars.insert({start_arc, start_arc_var});
const int cp_end = model.End(vehicle) + 1;
const Arc end_arc = {cp_end, depot};
const int end_arc_var = AddVariable(cp_model, 1, 1);
DCHECK(!gtl::ContainsKey(arc_vars, end_arc));
arc_vars.insert({end_arc, end_arc_var});
vehicle_performs_node[vehicle][cp_start] = start_arc_var;
vehicle_performs_node[vehicle][cp_end] = end_arc_var;
}
// is_unperformed[node] variable equals to 1 if visit is unperformed, and 0
// otherwise.
std::vector<int> is_unperformed(num_cp_nodes, -1);
// Initialize is_unperformed variables for nodes that must be performed.
for (int node = 0; node < num_nodes; node++) {
const int cp_node = node + 1;
// Forced active and nodes that are not involved in any disjunctions are
// always performed.
const std::vector<RoutingDisjunctionIndex>& disjunction_indices =
model.GetDisjunctionIndices(node);
if (disjunction_indices.empty() || model.ActiveVar(node)->Min() == 1) {
is_unperformed[cp_node] = AddVariable(cp_model, 0, 0);
continue;
}
// Check if the node is in a forced active disjunction.
for (RoutingDisjunctionIndex disjunction_index : disjunction_indices) {
const int num_nodes =
model.GetDisjunctionNodeIndices(disjunction_index).size();
const int64_t penalty = model.GetDisjunctionPenalty(disjunction_index);
const int64_t max_cardinality =
model.GetDisjunctionMaxCardinality(disjunction_index);
if (num_nodes == max_cardinality &&
(penalty < 0 || penalty == std::numeric_limits<int64_t>::max())) {
// Nodes in this disjunction are forced active.
is_unperformed[cp_node] = AddVariable(cp_model, 0, 0);
break;
}
}
}
// Add alternative visits. Create self-looped arc variables. Set penalty for
// not performing disjunctions.
for (RoutingDisjunctionIndex disjunction_index(0);
disjunction_index < model.GetNumberOfDisjunctions();
disjunction_index++) {
const std::vector<int64_t>& disjunction_indices =
model.GetDisjunctionNodeIndices(disjunction_index);
const int disjunction_size = disjunction_indices.size();
const int64_t penalty = model.GetDisjunctionPenalty(disjunction_index);
const int64_t max_cardinality =
model.GetDisjunctionMaxCardinality(disjunction_index);
// Case when disjunction involves only 1 node, the node is only present in
// this disjunction, and the node can be unperformed.
if (disjunction_size == 1 &&
model.GetDisjunctionIndices(disjunction_indices[0]).size() == 1 &&
is_unperformed[disjunction_indices[0] + 1] == -1) {
const int cp_node = disjunction_indices[0] + 1;
const Arc arc = {cp_node, cp_node};
DCHECK(!gtl::ContainsKey(arc_vars, arc));
is_unperformed[cp_node] = AddVariable(cp_model, 0, 1);
arc_vars.insert({arc, is_unperformed[cp_node]});
cp_model->mutable_objective()->add_vars(is_unperformed[cp_node]);
cp_model->mutable_objective()->add_coeffs(penalty);
continue;
}
// num_performed + SUM(node)is_unperformed[node] = disjunction_size
const int num_performed = AddVariable(cp_model, 0, max_cardinality);
std::vector<std::pair<int, double>> var_coeffs;
var_coeffs.push_back({num_performed, 1});
for (const int node : disjunction_indices) {
const int cp_node = node + 1;
// Node can be unperformed.
if (is_unperformed[cp_node] == -1) {
const Arc arc = {cp_node, cp_node};
DCHECK(!gtl::ContainsKey(arc_vars, arc));
is_unperformed[cp_node] = AddVariable(cp_model, 0, 1);
arc_vars.insert({arc, is_unperformed[cp_node]});
}
var_coeffs.push_back({is_unperformed[cp_node], 1});
}
AddLinearConstraint(cp_model, disjunction_size, disjunction_size,
var_coeffs);
// When penalty is negative or max int64_t (forced active), num_violated is
// 0.
if (penalty < 0 || penalty == std::numeric_limits<int64_t>::max()) {
AddLinearConstraint(cp_model, max_cardinality, max_cardinality,
{{num_performed, 1}});
continue;
}
// If number of active indices is less than max_cardinality, then for each
// violated index 'penalty' is paid.
const int num_violated = AddVariable(cp_model, 0, max_cardinality);
cp_model->mutable_objective()->add_vars(num_violated);
cp_model->mutable_objective()->add_coeffs(penalty);
// num_performed + num_violated = max_cardinality
AddLinearConstraint(cp_model, max_cardinality, max_cardinality,
{{num_performed, 1}, {num_violated, 1}});
}
// Create "arc" variables.
for (int tail = 0; tail < num_nodes; ++tail) {
const int cp_tail = tail + 1;
std::unique_ptr<IntVarIterator> iter(
model.NextVar(tail)->MakeDomainIterator(false));
for (int head : InitAndGetValues(iter.get())) {
const int cp_head = head + 1;
if (model.IsStart(head)) continue;
// Arcs for unperformed visits have already been created.
if (tail == head) continue;
// Direct arcs from start to end nodes should exist only if they are
// for the same vehicle.
if (model.IsStart(tail) && model.IsEnd(head) &&
model.VehicleIndex(tail) != model.VehicleIndex(head)) {
continue;
}
bool feasible = false;
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
if (model.GetArcCostForVehicle(tail, head, vehicle) !=
std::numeric_limits<int64_t>::max()) {
feasible = true;
break;
}
}
if (!feasible) continue;
const Arc arc = {cp_tail, cp_head};
DCHECK(!gtl::ContainsKey(arc_vars, arc));
const int arc_var = AddVariable(cp_model, 0, 1);
arc_vars.insert({arc, arc_var});
}
}
// Set literals for vehicle performing node.
for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
// For starts and ends nodes vehicle_performs_node variables already set.
if (model.IsStart(cp_node - 1) || model.IsEnd(cp_node - 1)) continue;
// Each node should be performed by 1 vehicle, or be unperformed.
// SUM(vehicle)(vehicle_performs_node[vehicle][cp_node]) + loop(cp_node) = 1
std::vector<std::pair<int, double>> var_coeffs;
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
vehicle_performs_node[vehicle][cp_node] = AddVariable(cp_model, 0, 1);
var_coeffs.push_back({vehicle_performs_node[vehicle][cp_node], 1});
}
var_coeffs.push_back({is_unperformed[cp_node], 1});
AddLinearConstraint(cp_model, 1, 1, var_coeffs);
}
const int num_vehicle_classes = model.GetVehicleClassesCount();
// vehicle_class_performs_node[vehicle_class][node] equals to 1 if the
// vehicle of vehicle_class performs the node, and 0 otherwise.
std::vector<absl::flat_hash_map<int, int>> vehicle_class_performs_node(
num_vehicle_classes);
for (int cp_node = 1; cp_node < num_cp_nodes; cp_node++) {
const int node = cp_node - 1;
for (int vehicle_class = 0; vehicle_class < num_vehicle_classes;
vehicle_class++) {
if (model.IsStart(node) || model.IsEnd(node)) {
const int vehicle = model.VehicleIndex(node);
vehicle_class_performs_node[vehicle_class][cp_node] =
vehicle_class ==
model.GetVehicleClassIndexOfVehicle(vehicle).value()
? AddVariable(cp_model, 1, 1)
: AddVariable(cp_model, 0, 0);
continue;
}
vehicle_class_performs_node[vehicle_class][cp_node] =
AddVariable(cp_model, 0, 1);
std::vector<std::pair<int, double>> var_coeffs;
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
if (model.GetVehicleClassIndexOfVehicle(vehicle).value() ==
vehicle_class) {
var_coeffs.push_back({vehicle_performs_node[vehicle][cp_node], 1});
// vehicle_performs_node -> vehicle_class_performs_node
AddLinearConstraint(
cp_model, 1, 1,
{{vehicle_class_performs_node[vehicle_class][cp_node], 1}},
{vehicle_performs_node[vehicle][cp_node]});
}
}
// vehicle_class_performs_node -> exactly one vehicle from this class
// performs node.
AddLinearConstraint(
cp_model, 1, 1, var_coeffs,
{vehicle_class_performs_node[vehicle_class][cp_node]});
}
}
// vehicle_class_performs_arc[vehicle_class][arc_var] equals to 1 if the
// vehicle of vehicle_class performs the arc, and 0 otherwise.
std::vector<absl::flat_hash_map<int, int>> vehicle_class_performs_arc(
num_vehicle_classes);
// Set "arc" costs.
for (const auto [arc, arc_var] : arc_vars) {
const auto [cp_tail, cp_head] = arc;
if (cp_tail == depot || cp_head == depot) continue;
const int tail = cp_tail - 1;
const int head = cp_head - 1;
// Costs for unperformed arcs have already been set.
if (tail == head) continue;
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
// The arc can't be performed by the vehicle when vehicle can't perform
// arc nodes.
if (!vehicle_performs_node[vehicle].contains(cp_tail) ||
!vehicle_performs_node[vehicle].contains(cp_head)) {
continue;
}
int64_t cost = model.GetArcCostForVehicle(tail, head, vehicle);
// Arcs with int64_t's max cost are infeasible.
if (cost == std::numeric_limits<int64_t>::max()) continue;
const int vehicle_class =
model.GetVehicleClassIndexOfVehicle(vehicle).value();
if (!vehicle_class_performs_arc[vehicle_class].contains(arc_var)) {
vehicle_class_performs_arc[vehicle_class][arc_var] =
AddVariable(cp_model, 0, 1);
// Create constraints to set vehicle_class_performs_arc.
// vehicle_class_performs_arc ->
// vehicle_class_performs_tail & vehicle_class_performs_head &
// arc_is_performed
ConstraintProto* ct = cp_model->add_constraints();
ct->add_enforcement_literal(
vehicle_class_performs_arc[vehicle_class][arc_var]);
BoolArgumentProto* bool_and = ct->mutable_bool_and();
bool_and->add_literals(
vehicle_class_performs_node[vehicle_class][cp_tail]);
bool_and->add_literals(
vehicle_class_performs_node[vehicle_class][cp_head]);
bool_and->add_literals(arc_var);
// Don't add arcs with zero cost to the objective.
if (cost != 0) {
cp_model->mutable_objective()->add_vars(
vehicle_class_performs_arc[vehicle_class][arc_var]);
cp_model->mutable_objective()->add_coeffs(cost);
}
}
// (arc_is_performed & vehicle_performs_tail) ->
// (vehicle_class_performs_arc & vehicle_performs_head)
ConstraintProto* ct_arc_tail = cp_model->add_constraints();
ct_arc_tail->add_enforcement_literal(arc_var);
ct_arc_tail->add_enforcement_literal(
vehicle_performs_node[vehicle][cp_tail]);
ct_arc_tail->mutable_bool_and()->add_literals(
vehicle_class_performs_arc[vehicle_class][arc_var]);
ct_arc_tail->mutable_bool_and()->add_literals(
vehicle_performs_node[vehicle][cp_head]);
// (arc_is_performed & vehicle_performs_head) ->
// (vehicle_class_performs_arc & vehicle_performs_tail)
ConstraintProto* ct_arc_head = cp_model->add_constraints();
ct_arc_head->add_enforcement_literal(arc_var);
ct_arc_head->add_enforcement_literal(
vehicle_performs_node[vehicle][cp_head]);
ct_arc_head->mutable_bool_and()->add_literals(
vehicle_class_performs_arc[vehicle_class][arc_var]);
ct_arc_head->mutable_bool_and()->add_literals(
vehicle_performs_node[vehicle][cp_tail]);
}
}
AddGeneralizedPickupDeliveryConstraints(
model, arc_vars, vehicle_performs_node, is_unperformed, cp_model);
AddGeneralizedDimensions(model, arc_vars, vehicle_performs_node,
vehicle_class_performs_arc, cp_model);
// Create Routes constraint, ensuring circuits from and to the depot.
RoutesConstraintProto* routes_ct =
cp_model->add_constraints()->mutable_routes();
for (const auto [arc, arc_var] : arc_vars) {
const int tail = arc.tail;
const int head = arc.head;
routes_ct->add_tails(tail);
routes_ct->add_heads(head);
routes_ct->add_literals(arc_var);
}
// Add demands and capacities to improve the LP relaxation and cuts. These
// are based on the first "unary" dimension in the model if it exists.
// TODO(user): We might want to try to get demand lower bounds from
// non-unary dimensions if no unary exist.
const RoutingDimension* master_dimension = nullptr;
for (const RoutingDimension* dimension : model.GetDimensions()) {
bool is_unary = true;
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
if (dimension->GetUnaryTransitEvaluator(vehicle) == nullptr) {
is_unary = false;
break;
}
}
if (is_unary) {
master_dimension = dimension;
break;
}
}
if (master_dimension != nullptr) {
for (int cp_node = 0; cp_node < num_cp_nodes; ++cp_node) {
int64_t min_transit = std::numeric_limits<int64_t>::max();
if (cp_node != 0 && !model.IsEnd(cp_node - 1)) {
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
const RoutingModel::TransitCallback1& transit =
master_dimension->GetUnaryTransitEvaluator(vehicle);
min_transit = std::min(min_transit, transit(cp_node - 1));
}
} else {
min_transit = 0;
}
routes_ct->add_demands(min_transit);
}
DCHECK_EQ(routes_ct->demands_size(), num_cp_nodes);
int64_t max_capacity = std::numeric_limits<int64_t>::min();
for (int vehicle = 0; vehicle < model.vehicles(); vehicle++) {
max_capacity = std::max(max_capacity,
master_dimension->vehicle_capacities()[vehicle]);
}
routes_ct->set_capacity(max_capacity);
}
return arc_vars;
}
// Converts a CpSolverResponse to an Assignment containing next variables.
bool ConvertGeneralizedResponseToSolution(const CpSolverResponse& response,
const RoutingModel& model,
const ArcVarMap& arc_vars,
Assignment* solution) {
if (response.status() != CpSolverStatus::OPTIMAL &&
response.status() != CpSolverStatus::FEASIBLE) {
return false;
}
const int depot = 0;
for (const auto [arc, arc_var] : arc_vars) {
if (response.solution(arc_var) == 0) continue;
const auto [tail, head] = arc;
if (head == depot || tail == depot) continue;
solution->Add(model.NextVar(tail - 1))->SetValue(head - 1);
}
return true;
}
// Uses CP solution as hint for CP-SAT.
void AddSolutionAsHintToGeneralizedModel(const Assignment* solution,
const RoutingModel& model,
const ArcVarMap& arc_vars,
CpModelProto* cp_model) {
if (solution == nullptr) return;
PartialVariableAssignment* const hint = cp_model->mutable_solution_hint();
hint->Clear();
const int num_nodes = model.Nexts().size();
for (int tail = 0; tail < num_nodes; ++tail) {
const int cp_tail = tail + 1;
const int cp_head = solution->Value(model.NextVar(tail)) + 1;
const int* const arc_var = gtl::FindOrNull(arc_vars, {cp_tail, cp_head});
// Arcs with a cost of max int64_t are not added to the model (considered as
// infeasible). In some rare cases CP solutions might contain such arcs in
// which case they are skipped here and a partial solution is used as a
// hint.
if (arc_var == nullptr) continue;
hint->add_vars(*arc_var);
hint->add_values(1);
}
}
void AddSolutionAsHintToModel(const Assignment* solution,
const RoutingModel& model,
const ArcVarMap& arc_vars,
@@ -501,14 +1011,20 @@ void AddSolutionAsHintToModel(const Assignment* solution,
// Returns the response of the search.
CpSolverResponse SolveRoutingModel(
const CpModelProto& cp_model, absl::Duration remaining_time,
const RoutingSearchParameters& search_parameters,
const std::function<void(const CpSolverResponse& response)>& observer) {
// TODO(user): Add CP-SAT parameters to routing parameters.
SatParameters parameters;
parameters.set_linearization_level(2);
parameters.set_max_time_in_seconds(absl::ToDoubleSeconds(remaining_time));
parameters.set_num_search_workers(1);
// Copying to set remaining time.
SatParameters sat_parameters = search_parameters.sat_parameters();
if (!sat_parameters.has_max_time_in_seconds()) {
sat_parameters.set_max_time_in_seconds(
absl::ToDoubleSeconds(remaining_time));
} else {
sat_parameters.set_max_time_in_seconds(
std::min(absl::ToDoubleSeconds(remaining_time),
sat_parameters.max_time_in_seconds()));
}
Model model;
model.Add(NewSatParameters(parameters));
model.Add(NewSatParameters(sat_parameters));
if (observer != nullptr) {
model.Add(NewFeasibleSolutionObserver(observer));
}
@@ -517,6 +1033,20 @@ CpSolverResponse SolveRoutingModel(
return SolveCpModel(cp_model, &model);
}
// Check if all the nodes are present in arcs. Otherwise, CP-SAT solver may
// fail.
bool IsFeasibleArcVarMap(const ArcVarMap& arc_vars, int max_node_index) {
Bitset64<> present_in_arcs(max_node_index + 1);
for (const auto [arc, _] : arc_vars) {
present_in_arcs.Set(arc.head);
present_in_arcs.Set(arc.tail);
}
for (int i = 0; i <= max_node_index; i++) {
if (!present_in_arcs[i]) return false;
}
return true;
}
} // namespace
} // namespace sat
@@ -526,17 +1056,30 @@ bool SolveModelWithSat(const RoutingModel& model,
const RoutingSearchParameters& search_parameters,
const Assignment* initial_solution,
Assignment* solution) {
if (!sat::RoutingModelCanBeSolvedBySat(model)) return false;
sat::CpModelProto cp_model;
cp_model.mutable_objective()->set_scaling_factor(
search_parameters.log_cost_scaling_factor());
cp_model.mutable_objective()->set_offset(search_parameters.log_cost_offset());
if (search_parameters.use_generalized_cp_sat() == BOOL_TRUE) {
const sat::ArcVarMap arc_vars =
sat::PopulateGeneralizedRouteModelFromRoutingModel(model, &cp_model);
const int max_node_index = model.Nexts().size() + model.vehicles();
if (!sat::IsFeasibleArcVarMap(arc_vars, max_node_index)) return false;
sat::AddSolutionAsHintToGeneralizedModel(initial_solution, model, arc_vars,
&cp_model);
return sat::ConvertGeneralizedResponseToSolution(
sat::SolveRoutingModel(cp_model, model.RemainingTime(),
search_parameters, nullptr),
model, arc_vars, solution);
}
if (!sat::RoutingModelCanBeSolvedBySat(model)) return false;
const sat::ArcVarMap arc_vars =
sat::PopulateModelFromRoutingModel(model, &cp_model);
sat::AddSolutionAsHintToModel(initial_solution, model, arc_vars, &cp_model);
return sat::ConvertToSolution(
sat::SolveRoutingModel(cp_model, model.RemainingTime(), nullptr), model,
arc_vars, solution);
sat::SolveRoutingModel(cp_model, model.RemainingTime(), search_parameters,
nullptr),
model, arc_vars, solution);
}
} // namespace operations_research