From bcb7b3073dd27779a1992f604f0c745adde1e191 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Sun, 18 Oct 2020 16:38:25 +0200 Subject: [PATCH] large synchro with internal code: linear solver: introduce intermediate SCIP layer called gscip; sat: more work on slow propagation detection; base: remove statusor, use abseil version; constraint solver: more work on internal local search --- ortools/base/BUILD | 18 +- ortools/base/protoutil.h | 2 +- ortools/base/status_builder.h | 2 + ortools/base/status_macros.h | 4 +- ortools/base/statusor.h | 58 -- ortools/base/sysinfo.cc | 6 +- ortools/bop/BUILD | 4 +- ortools/constraint_solver/BUILD | 19 +- ortools/constraint_solver/assignment.cc | 2 +- .../constraint_solver/constraint_solver.cc | 10 + ortools/constraint_solver/constraint_solver.h | 81 +- .../constraint_solver/constraint_solveri.h | 15 +- ortools/constraint_solver/local_search.cc | 256 ++++- .../local_search_stats.proto | 33 - .../constraint_solver/python/pywrapcp_util.h | 1 + ortools/constraint_solver/routing.cc | 323 +++++-- ortools/constraint_solver/routing.h | 89 +- ortools/constraint_solver/routing_flags.cc | 3 + .../routing_neighborhoods.cc | 443 +++++---- .../constraint_solver/routing_neighborhoods.h | 161 ++-- .../constraint_solver/routing_parameters.cc | 28 +- .../routing_parameters.proto | 30 +- ortools/constraint_solver/routing_search.cc | 353 ++++--- .../samples/tsp_cities_routes.cc | 134 +++ .../constraint_solver/samples/vrp_routes.cc | 157 +++ ortools/constraint_solver/search.cc | 156 ++- ortools/constraint_solver/search_stats.proto | 80 ++ ortools/data/jobshop_scheduling_parser.cc | 3 +- ortools/flatzinc/parser.tab.cc | 849 +++++++++-------- ortools/flatzinc/parser.tab.hh | 54 +- ortools/glop/parameters.proto | 25 +- ortools/glop/preprocessor.cc | 2 +- ortools/graph/BUILD | 26 +- ortools/graph/connected_components.cc | 25 + ortools/graph/connected_components.h | 13 +- ortools/graph/connectivity.h | 170 ---- ortools/graph/io.h | 6 +- ortools/graph/minimum_spanning_tree.h | 16 +- ortools/graph/topologicalsorter.h | 10 +- ortools/gscip/BUILD | 151 +++ ortools/gscip/gscip.cc | 896 ++++++++++++++++++ ortools/gscip/gscip.h | 537 +++++++++++ ortools/gscip/gscip.proto | 170 ++++ ortools/gscip/gscip_ext.cc | 204 ++++ ortools/gscip/gscip_ext.h | 104 ++ ortools/gscip/gscip_parameters.cc | 124 +++ ortools/gscip/gscip_parameters.h | 56 ++ ortools/gscip/legacy_scip_params.cc | 116 +++ ortools/gscip/legacy_scip_params.h | 29 + .../java/com/google/ortools/sat/CpModel.java | 2 +- ortools/linear_solver/BUILD | 13 +- ortools/linear_solver/gurobi_environment.cc | 9 + ortools/linear_solver/gurobi_environment.h | 10 +- ortools/linear_solver/gurobi_interface.cc | 574 +++++++---- ortools/linear_solver/gurobi_proto_solver.cc | 136 ++- ortools/linear_solver/gurobi_proto_solver.h | 26 +- ortools/linear_solver/java/linear_solver.i | 2 +- ortools/linear_solver/linear_solver.cc | 30 +- ortools/linear_solver/linear_solver.h | 2 +- ortools/linear_solver/linear_solver.proto | 7 +- ortools/linear_solver/model_exporter.cc | 244 +++-- ortools/linear_solver/model_exporter.h | 2 +- .../model_exporter_swig_helper.h | 4 +- ortools/linear_solver/model_validator.cc | 7 +- ortools/linear_solver/python/linear_solver.i | 2 - ortools/linear_solver/sat_interface.cc | 2 +- ortools/linear_solver/sat_proto_solver.cc | 2 +- ortools/linear_solver/sat_proto_solver.h | 2 +- ortools/linear_solver/scip_callback.cc | 452 +++++++++ ortools/linear_solver/scip_callback.h | 278 ++++++ ortools/linear_solver/scip_interface.cc | 229 ++++- ortools/linear_solver/scip_proto_solver.cc | 89 +- ortools/linear_solver/scip_proto_solver.h | 5 +- ortools/lp_data/BUILD | 2 + ortools/lp_data/lp_data.cc | 45 +- ortools/lp_data/lp_data.h | 2 +- ortools/lp_data/lp_data_utils.cc | 9 +- ortools/lp_data/lp_data_utils.h | 1 + ortools/lp_data/lp_utils.h | 4 +- ortools/lp_data/mps_reader.cc | 29 +- ortools/lp_data/mps_reader.h | 4 +- ortools/port/file_nonport.cc | 4 +- ortools/sat/BUILD | 5 +- ortools/sat/clause.cc | 4 + ortools/sat/cp_model_search.cc | 32 +- ortools/sat/cp_model_search.h | 7 +- ortools/sat/cp_model_solver.cc | 21 +- ortools/sat/csharp/CpModel.cs | 1 - ortools/sat/cuts.cc | 6 +- ortools/sat/encoding.h | 2 + ortools/sat/integer.h | 9 +- ortools/sat/integer_expr.cc | 69 ++ ortools/sat/integer_expr.h | 28 + ortools/sat/integer_search.cc | 407 ++++---- ortools/sat/integer_search.h | 109 ++- ortools/sat/linear_programming_constraint.cc | 84 +- ortools/sat/linear_programming_constraint.h | 18 +- ortools/sat/lp_utils.cc | 86 +- ortools/sat/python/cp_model.py | 2 +- ortools/sat/python/sat.i | 1 + ortools/sat/rins.cc | 9 +- ortools/sat/sat_parameters.proto | 13 +- ortools/sat/synchronization.cc | 19 +- ortools/sat/synchronization.h | 2 +- ortools/util/BUILD | 2 +- ortools/util/file_util.cc | 1 + ortools/util/file_util.h | 2 +- ortools/util/proto_tools.h | 46 + ortools/util/testing_utils.h | 23 + 109 files changed, 7011 insertions(+), 2280 deletions(-) delete mode 100644 ortools/base/statusor.h delete mode 100644 ortools/constraint_solver/local_search_stats.proto create mode 100644 ortools/constraint_solver/samples/tsp_cities_routes.cc create mode 100644 ortools/constraint_solver/samples/vrp_routes.cc create mode 100644 ortools/constraint_solver/search_stats.proto delete mode 100644 ortools/graph/connectivity.h create mode 100644 ortools/gscip/BUILD create mode 100644 ortools/gscip/gscip.cc create mode 100644 ortools/gscip/gscip.h create mode 100644 ortools/gscip/gscip.proto create mode 100644 ortools/gscip/gscip_ext.cc create mode 100644 ortools/gscip/gscip_ext.h create mode 100644 ortools/gscip/gscip_parameters.cc create mode 100644 ortools/gscip/gscip_parameters.h create mode 100644 ortools/gscip/legacy_scip_params.cc create mode 100644 ortools/gscip/legacy_scip_params.h create mode 100644 ortools/linear_solver/scip_callback.cc create mode 100644 ortools/linear_solver/scip_callback.h create mode 100644 ortools/util/testing_utils.h diff --git a/ortools/base/BUILD b/ortools/base/BUILD index 4baa04b780..de04108cd4 100644 --- a/ortools/base/BUILD +++ b/ortools/base/BUILD @@ -51,17 +51,6 @@ cc_library( ], ) -cc_library( - name = "statusor", - hdrs = [ - "statusor.h", - ], - deps = [ - ":base", - "@com_google_absl//absl/status", - ], -) - cc_library( name = "status_macros", hdrs = [ @@ -69,8 +58,8 @@ cc_library( ], deps = [ ":base", - ":statusor", "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", ], ) @@ -144,8 +133,7 @@ cc_library( deps = [ ":base", ":file", - ":statusor", - + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_protobuf//:protobuf", ], @@ -157,9 +145,9 @@ cc_library( "protoutil.h", ], deps = [ - ":statusor", ":timer", "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", "@com_google_protobuf//:protobuf", ], ) diff --git a/ortools/base/protoutil.h b/ortools/base/protoutil.h index 5f6975cd53..36af71590f 100644 --- a/ortools/base/protoutil.h +++ b/ortools/base/protoutil.h @@ -15,10 +15,10 @@ #define OR_TOOLS_BASE_PROTOUTIL_H_ #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "google/protobuf/duration.pb.h" -#include "ortools/base/statusor.h" namespace util_time { diff --git a/ortools/base/status_builder.h b/ortools/base/status_builder.h index 71fc285c4b..78fb8d124d 100644 --- a/ortools/base/status_builder.h +++ b/ortools/base/status_builder.h @@ -36,6 +36,8 @@ class StatusBuilder { return *this; } + StatusBuilder& SetAppend() { return *this; } + private: const absl::StatusCode code_; std::ostringstream ss_; diff --git a/ortools/base/status_macros.h b/ortools/base/status_macros.h index cd91965c26..c3b45009e6 100644 --- a/ortools/base/status_macros.h +++ b/ortools/base/status_macros.h @@ -15,7 +15,7 @@ #define OR_TOOLS_BASE_STATUS_MACROS_H_ #include "absl/status/status.h" -#include "ortools/base/statusor.h" +#include "absl/status/statusor.h" namespace absl { @@ -47,7 +47,7 @@ template ::absl::Status status = DoAssignOrReturn(lhs, (rexpr)); \ if (!status.ok()) return status; -// Executes an expression that returns a absl::StatusOr, extracting its value +// Executes an expression that returns an absl::StatusOr, extracting its value // into the variable defined by lhs (or returning on error). // // Example: Assigning to an existing value diff --git a/ortools/base/statusor.h b/ortools/base/statusor.h deleted file mode 100644 index 973d513259..0000000000 --- a/ortools/base/statusor.h +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2010-2018 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_BASE_STATUSOR_H_ -#define OR_TOOLS_BASE_STATUSOR_H_ - -#include "absl/status/status.h" -#include "ortools/base/logging.h" - -namespace absl { - -// WARNING: This makes a copy of its payload. Ugly. -template -struct StatusOr { - // Non-explicit constructors, by design. - StatusOr(T value) : value_(value) {} // NOLINT - StatusOr(const Status& status) : status_(status) { // NOLINT - CHECK(!status_.ok()) << status.ToString(); - } - - // Copy constructor. - StatusOr(const StatusOr& other) - : value_(other.value_), status_(other.status_) {} - - bool ok() const { return status_.ok(); } - const T& value() const { - CHECK(ok()); - return value_; - } - - Status status() const { return status_; } - - template - T value_or(U&& default_value) const& { - if (ok()) { - return value_; - } - return std::forward(default_value); - } - - private: - T value_; - Status status_; -}; - -} // namespace absl - -#endif // OR_TOOLS_BASE_STATUSOR_H_ diff --git a/ortools/base/sysinfo.cc b/ortools/base/sysinfo.cc index 2dd9d6ba46..786297e13f 100644 --- a/ortools/base/sysinfo.cc +++ b/ortools/base/sysinfo.cc @@ -18,8 +18,8 @@ #include #include #elif defined(__FreeBSD__) // FreeBSD -#include #include +#include #elif defined(_MSC_VER) // WINDOWS // clang-format off #include @@ -47,7 +47,7 @@ int64 GetProcessMemoryUsage() { int64 resident_memory = t_info.resident_size; return resident_memory; } -#elif defined(__GNUC__) && !defined(__FreeBSD__) // LINUX +#elif defined(__GNUC__) && !defined(__FreeBSD__) // LINUX int64 GetProcessMemoryUsage() { unsigned size = 0; char buf[30]; @@ -81,7 +81,7 @@ int64 GetProcessMemoryUsage() { } return memory; } -#else // Unknown, returning 0. +#else // Unknown, returning 0. int64 GetProcessMemoryUsage() { return 0; } #endif diff --git a/ortools/bop/BUILD b/ortools/bop/BUILD index 5cf2f201e5..8916a866a1 100644 --- a/ortools/bop/BUILD +++ b/ortools/bop/BUILD @@ -168,12 +168,12 @@ cc_library( ":bop_solution", ":bop_types", ":bop_util", + "@com_google_absl//absl/status:statusor", "//ortools/base", "//ortools/base:hash", "//ortools/base:int_type", "//ortools/base:int_type_indexed_vector", "//ortools/base:random", - "//ortools/base:statusor", "//ortools/sat:boolean_problem", "//ortools/sat:boolean_problem_cc_proto", "//ortools/sat:sat_solver", @@ -194,11 +194,11 @@ cc_library( ":bop_types", ":bop_util", ":complete_optimizer", + "@com_google_absl//absl/status:statusor", "//ortools/base", "//ortools/base:hash", "//ortools/base:int_type", "//ortools/base:int_type_indexed_vector", - "//ortools/base:statusor", "//ortools/base:stl_util", "//ortools/glop:lp_solver", #"//ortools/glop", diff --git a/ortools/constraint_solver/BUILD b/ortools/constraint_solver/BUILD index ae094b580f..22a657d768 100644 --- a/ortools/constraint_solver/BUILD +++ b/ortools/constraint_solver/BUILD @@ -17,18 +17,18 @@ cc_proto_library( ) proto_library( - name = "local_search_stats_proto", - srcs = ["local_search_stats.proto"], + name = "search_stats_proto", + srcs = ["search_stats.proto"], ) cc_proto_library( - name = "local_search_stats_cc_proto", - deps = [":local_search_stats_proto"], + name = "search_stats_cc_proto", + deps = [":search_stats_proto"], ) java_proto_library( - name = "local_search_stats_java_proto", - deps = [":local_search_stats_proto"], + name = "search_stats_java_proto", + deps = [":search_stats_proto"], ) proto_library( @@ -135,9 +135,10 @@ cc_library( deps = [ ":assignment_cc_proto", ":demon_profiler_cc_proto", - ":local_search_stats_cc_proto", + ":search_stats_cc_proto", ":search_limit_cc_proto", ":solver_parameters_cc_proto", + ":routing_parameters_cc_proto", "//ortools/base", "//ortools/base:file", "//ortools/base:recordio", @@ -236,8 +237,8 @@ cc_library( ":solver_parameters_cc_proto", "//ortools/base", "//ortools/base:protoutil", - "//ortools/base:statusor", "//ortools/util:optional_boolean_cc_proto", + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", "@com_google_protobuf//:protobuf", @@ -333,7 +334,7 @@ cc_library( "//ortools/base:murmur", "//ortools/glop:lp_solver", "//ortools/graph:christofides", - "//ortools/graph:connectivity", + "//ortools/graph:connected_components", "//ortools/graph:linear_assignment", "//ortools/graph:min_cost_flow", "//ortools/graph:topologicalsorter", diff --git a/ortools/constraint_solver/assignment.cc b/ortools/constraint_solver/assignment.cc index 69f89cfab0..8b521b849f 100644 --- a/ortools/constraint_solver/assignment.cc +++ b/ortools/constraint_solver/assignment.cc @@ -539,7 +539,7 @@ void Assignment::Load(const AssignmentProto& assignment_proto) { &AssignmentProto::sequence_var_assignment); if (assignment_proto.has_objective()) { const IntVarAssignment& objective = assignment_proto.objective(); - const std::string objective_id = objective.var_id(); + const std::string& objective_id = objective.var_id(); CHECK(!objective_id.empty()); if (HasObjective() && objective_id == Objective()->name()) { const int64 obj_min = objective.min(); diff --git a/ortools/constraint_solver/constraint_solver.cc b/ortools/constraint_solver/constraint_solver.cc index 8a46bdd223..6e810d4620 100644 --- a/ortools/constraint_solver/constraint_solver.cc +++ b/ortools/constraint_solver/constraint_solver.cc @@ -1536,6 +1536,16 @@ void Solver::TopPeriodicCheck() { TopLevelSearch()->PeriodicCheck(); } int Solver::TopProgressPercent() { return TopLevelSearch()->ProgressPercent(); } +ConstraintSolverStatistics Solver::GetConstraintSolverStatistics() const { + ConstraintSolverStatistics stats; + stats.set_num_branches(branches()); + stats.set_num_failures(failures()); + stats.set_num_solutions(solutions()); + stats.set_bytes_used(MemoryUsage()); + stats.set_duration_seconds(absl::ToDoubleSeconds(timer_->GetDuration())); + return stats; +} + void Solver::PushState() { StateInfo info; PushState(SIMPLE_MARKER, info); diff --git a/ortools/constraint_solver/constraint_solver.h b/ortools/constraint_solver/constraint_solver.h index cb36b0029e..d9768e2e6e 100644 --- a/ortools/constraint_solver/constraint_solver.h +++ b/ortools/constraint_solver/constraint_solver.h @@ -85,7 +85,8 @@ #include "ortools/base/map_util.h" #include "ortools/base/sysinfo.h" #include "ortools/base/timer.h" -#include "ortools/constraint_solver/local_search_stats.pb.h" +#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/constraint_solver/search_stats.pb.h" #include "ortools/constraint_solver/solver_parameters.pb.h" #include "ortools/util/piecewise_linear_function.h" #include "ortools/util/sorted_interval_list.h" @@ -144,6 +145,7 @@ class RevBitSet; class RegularLimit; class RegularLimitParameters; class Search; +class ImprovementSearchLimit; class SearchLimit; class SearchMonitor; class SequenceVar; @@ -810,8 +812,8 @@ class Solver { /// /// - the most common use case is modeling: the given constraint is really /// part of the problem that the user is trying to solve. In this use case, - /// AddConstraint is called outside of search (i.e., with @code state() == - /// OUTSIDE_SEARCH @endcode). Most users should only use AddConstraint in this + /// AddConstraint is called outside of search (i.e., with state() == + /// OUTSIDE_SEARCH). Most users should only use AddConstraint in this /// way. In this case, the constraint will belong to the model forever: it /// cannot not be removed by backtracking. /// @@ -2246,6 +2248,15 @@ class Solver { SearchLimit* MakeLimit(SearchLimit* const limit_1, SearchLimit* const limit_2); + /// Limits the search based on the improvements of 'objective_var'. Stops the + /// search when the improvement rate gets lower than a threshold value. This + /// threshold value is computed based on the improvement rate during the first + /// phase of the search. + ImprovementSearchLimit* MakeImprovementLimit( + IntVar* objective_var, bool maximize, double objective_scaling_factor, + double objective_offset, double improvement_rate_coefficient, + int improvement_rate_solutions_distance); + /// Callback-based search limit. Search stops when limiter returns true; if /// this happens at a leaf the corresponding solution will be rejected. SearchLimit* MakeCustomLimit(std::function limiter); @@ -2293,8 +2304,12 @@ class Solver { double scaling_factor = 1.0; double offset = 0; /// SearchMonitors will display the result of display_callback at each new - /// solution found. + /// solution found and when the search finishes if + /// display_on_new_solutions_only is false. std::function display_callback; + /// To be used to protect from cases where display_callback assumes + /// variables are instantiated, which only happens in AtSolution(). + bool display_on_new_solutions_only = true; }; SearchMonitor* MakeSearchLog(SearchLogParameters parameters); @@ -2652,6 +2667,16 @@ class Solver { LocalSearchOperator* RandomConcatenateOperators( const std::vector& ops, int32 seed); + /// Creates a local search operator which concatenates a vector of operators. + /// Each operator from the vector is called sequentially. When a neighbor is + /// found the neighborhood exploration restarts from the last active operator + /// (the one which produced the neighbor). + /// + /// Moving to the next operator when improvement rate of the current operator + /// gets lower than a threshold or the operator makes no new neighbors. + LocalSearchOperator* StagnationLimitedConcatenateOperators( + const std::vector& ops); + /// Creates a local search operator that wraps another local search /// operator and limits the number of neighbors explored (i.e., calls /// to MakeNextNeighbor from the current solution (between two calls @@ -2743,8 +2768,6 @@ class Solver { const std::vector& vars, const std::vector& secondary_vars, IndexEvaluator3 values, Solver::LocalSearchFilterBound filter_enum); - LocalSearchFilterManager* MakeLocalSearchFilterManager( - std::vector filters); /// Performs PeriodicCheck on the top-level search; for instance, can be /// called from a nested solve to check top-level limits. @@ -2821,6 +2844,8 @@ class Solver { std::string LocalSearchProfile() const; #if !defined(SWIG) + /// Returns detailed cp search statistics. + ConstraintSolverStatistics GetConstraintSolverStatistics() const; /// Returns detailed local search statistics. LocalSearchStatistics GetLocalSearchStatistics() const; #endif // !defined(SWIG) @@ -3117,8 +3142,8 @@ inline int64 Zero() { return 0; } inline int64 One() { return 1; } /// A BaseObject is the root of all reversibly allocated objects. -/// A DebugString method and the associated @code operator<< @endcode -/// are implemented as a convenience. +/// A DebugString method and the associated << operator are implemented +/// as a convenience. class BaseObject { public: BaseObject() {} @@ -4311,6 +4336,46 @@ class RegularLimit : public SearchLimit { bool cumulative_; }; +// Limit based on the improvement rate of 'objective_var'. +// This limit proceeds in two stages: +// 1) During the phase of the search in which the objective_var is strictly +// improving, a threshold value is computed as the minimum improvement rate of +// the objective, based on the 'improvement_rate_coefficient' and +// 'improvement_rate_solutions_distance' parameters. +// 2) Then, if the search continues beyond this phase of strict improvement, the +// limit stops the search when the improvement rate of the objective gets below +// this threshold value. +class ImprovementSearchLimit : public SearchLimit { + public: + ImprovementSearchLimit(Solver* const s, IntVar* objective_var, bool maximize, + double objective_scaling_factor, + double objective_offset, + double improvement_rate_coefficient, + int improvement_rate_solutions_distance); + ~ImprovementSearchLimit() override; + void Copy(const SearchLimit* const limit) override; + SearchLimit* MakeClone() const override; + bool Check() override; + bool AtSolution() override; + void Init() override; + + private: + IntVar* objective_var_; + bool maximize_; + double objective_scaling_factor_; + double objective_offset_; + double improvement_rate_coefficient_; + int improvement_rate_solutions_distance_; + + double best_objective_; + // clang-format off + std::deque > improvements_; + // clang-format on + double threshold_; + bool objective_updated_; + bool gradient_stage_; +}; + /// Interval variables are often used in scheduling. The main characteristics /// of an IntervalVar are the start position, duration, and end /// date. All these characteristics can be queried and set, and demons can diff --git a/ortools/constraint_solver/constraint_solveri.h b/ortools/constraint_solver/constraint_solveri.h index c1c6d81b93..22e0a88e57 100644 --- a/ortools/constraint_solver/constraint_solveri.h +++ b/ortools/constraint_solver/constraint_solveri.h @@ -1753,6 +1753,9 @@ class LocalSearchFilter : public BaseObject { /// Cancels the changes made by the last Relax()/Accept() calls. virtual void Revert() {} + /// Sets the filter to empty solution. + virtual void Reset() {} + /// Objective value from last time Synchronize() was called. virtual int64 GetSynchronizedObjectiveValue() const { return 0LL; } /// Objective value from the last time Accept() was called and returned true. @@ -2027,7 +2030,8 @@ class SearchLog : public SearchMonitor { public: SearchLog(Solver* const s, OptimizeVar* const obj, IntVar* const var, double scaling_factor, double offset, - std::function display_callback, int period); + std::function display_callback, + bool display_on_new_solutions_only, int period); ~SearchLog() override; void EnterSearch() override; void ExitSearch() override; @@ -2057,6 +2061,7 @@ class SearchLog : public SearchMonitor { const double scaling_factor_; const double offset_; std::function display_callback_; + const bool display_on_new_solutions_only_; int nsol_; int64 tick_; int64 objective_min_; @@ -3121,6 +3126,11 @@ class PathState { // more formally, changes the state from (P0 |> D, D) to (P0, \emptyset). void Revert(); + // LNS Operators may not fix variables, + // in which case we mark the candidate invalid. + void SetInvalid() { is_invalid_ = true; } + bool IsInvalid() const { return is_invalid_; } + private: // Most structs below are named pairs of ints, for typing purposes. @@ -3219,6 +3229,9 @@ class PathState { std::vector arcs_by_tail_index_; std::vector arcs_by_head_index_; std::vector next_arc_; + + // See IsInvalid() and SetInvalid(). + bool is_invalid_ = false; }; // A Chain is a range of committed nodes. diff --git a/ortools/constraint_solver/local_search.cc b/ortools/constraint_solver/local_search.cc index 8823672fd4..8ad42a8f98 100644 --- a/ortools/constraint_solver/local_search.cc +++ b/ortools/constraint_solver/local_search.cc @@ -1919,10 +1919,13 @@ class CompoundOperator : public LocalSearchOperator { bool HoldsDelta() const override { return true; } std::string DebugString() const override { - return operators_[operator_indices_[index_]]->DebugString(); + return operators_.empty() + ? "" + : operators_[operator_indices_[index_]]->DebugString(); } const LocalSearchOperator* Self() const override { - return operators_[operator_indices_[index_]]->Self(); + return operators_.empty() ? this + : operators_[operator_indices_[index_]]->Self(); } private: @@ -2133,6 +2136,99 @@ LocalSearchOperator* Solver::RandomConcatenateOperators( return RevAlloc(new RandomCompoundOperator(ops, seed)); } +namespace { +class StagnationLimitingCompoundOperator : public LocalSearchOperator { + public: + explicit StagnationLimitingCompoundOperator( + std::vector operators); + ~StagnationLimitingCompoundOperator() override {} + void Reset() override; + void Start(const Assignment* assignment) override; + bool MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) override; + bool HoldsDelta() const override { return true; } + + std::string DebugString() const override { + return operators_.empty() ? "" : operators_[index_]->DebugString(); + } + const LocalSearchOperator* Self() const override { + return operators_.empty() ? this : operators_[index_]->Self(); + } + + private: + bool StopCurrentOperator() { return false; } + void IncrementOperatorIndex(); + int64 index_; + std::vector operators_; + Bitset64<> started_; + const Assignment* start_assignment_; + bool has_fragments_; +}; + +StagnationLimitingCompoundOperator::StagnationLimitingCompoundOperator( + std::vector operators) + : index_(0), + operators_(std::move(operators)), + started_(operators_.size()), + start_assignment_(nullptr), + has_fragments_(false) { + operators_.erase(std::remove(operators_.begin(), operators_.end(), nullptr), + operators_.end()); + for (LocalSearchOperator* const op : operators_) { + if (op->HasFragments()) { + has_fragments_ = true; + break; + } + } +} + +void StagnationLimitingCompoundOperator::Reset() { + for (LocalSearchOperator* const op : operators_) { + op->Reset(); + } +} + +void StagnationLimitingCompoundOperator::IncrementOperatorIndex() { + ++index_; + if (index_ == operators_.size()) { + index_ = 0; + } +} + +void StagnationLimitingCompoundOperator::Start(const Assignment* assignment) { + start_assignment_ = assignment; + started_.ClearAll(); + if (StopCurrentOperator()) { + IncrementOperatorIndex(); + } +} + +bool StagnationLimitingCompoundOperator::MakeNextNeighbor( + Assignment* delta, Assignment* deltadelta) { + if (operators_.empty()) return false; + const int last_index = index_; + do { + if (!started_[index_]) { + operators_[index_]->Start(start_assignment_); + started_.Set(index_); + } + if (!operators_[index_]->HoldsDelta()) { + delta->Clear(); + } + if (operators_[index_]->MakeNextNeighbor(delta, deltadelta)) { + return true; + } + IncrementOperatorIndex(); + delta->Clear(); + } while (index_ != last_index); + return false; +} +} // namespace + +LocalSearchOperator* Solver::StagnationLimitedConcatenateOperators( + const std::vector& ops) { + return RevAlloc(new StagnationLimitingCompoundOperator(ops)); +} + // ----- Operator factory ----- template @@ -2469,6 +2565,7 @@ PathState::NodeRange PathState::Nodes(int path) const { } void PathState::CutChains() { + if (is_invalid_) return; // Filter out unchanged arcs from changed_arcs_, // translate changed arcs to changed arc indices. // Fill changed_paths_ while we hold node_path. @@ -2555,6 +2652,7 @@ void PathState::CutChains() { } void PathState::Commit() { + DCHECK(!IsInvalid()); if (committed_nodes_.size() < num_nodes_threshold_) { IncrementalCommit(); } else { @@ -2563,6 +2661,7 @@ void PathState::Commit() { } void PathState::Revert() { + is_invalid_ = false; chains_.resize(num_paths_ + 1); // One per path + sentinel. for (const int path : changed_paths_) { paths_[path] = {path, path + 1}; @@ -2659,52 +2758,93 @@ class PathStateFilter : public LocalSearchFilter { return true; } void Synchronize(const Assignment* delta, - const Assignment* deltadelta) override; + const Assignment* deltadelta) override{}; + void Commit(const Assignment* assignment, const Assignment* delta) override; void Revert() override; + void Reset() override; private: const std::unique_ptr path_state_; // Map IntVar* index to node, offset by the min index in nexts. std::vector variable_index_to_node_; int index_offset_; + // Used only in Reset(), this is a member variable to avoid reallocation. + std::vector node_is_assigned_; }; PathStateFilter::PathStateFilter(std::unique_ptr path_state, const std::vector& nexts) : path_state_(std::move(path_state)) { - index_offset_ = std::numeric_limits::max(); - for (const IntVar* next : nexts) { - index_offset_ = std::min(index_offset_, next->index()); + { + int min_index = std::numeric_limits::max(); + int max_index = std::numeric_limits::min(); + for (const IntVar* next : nexts) { + const int index = next->index(); + min_index = std::min(min_index, index); + max_index = std::max(max_index, index); + } + variable_index_to_node_.resize(max_index - min_index + 1, -1); + index_offset_ = min_index; } - variable_index_to_node_.resize(nexts.size(), -1); + for (int node = 0; node < nexts.size(); ++node) { const int index = nexts[node]->index() - index_offset_; - if (variable_index_to_node_.size() <= index) { - variable_index_to_node_.resize(index + 1, -1); - } variable_index_to_node_[index] = node; } } void PathStateFilter::Relax(const Assignment* delta, const Assignment* deltadelta) { + path_state_->Revert(); for (const IntVarElement& var_value : delta->IntVarContainer().elements()) { + if (var_value.Var() == nullptr) continue; const int index = var_value.Var()->index() - index_offset_; if (index < 0 || variable_index_to_node_.size() <= index) continue; const int node = variable_index_to_node_[index]; if (node == -1) continue; - path_state_->ChangeNext(node, var_value.Value()); + if (var_value.Bound()) { + path_state_->ChangeNext(node, var_value.Value()); + } else { + path_state_->Revert(); + path_state_->SetInvalid(); + break; + } } path_state_->CutChains(); } -// The solver does not guarantee that a given Synchronize() corresponds to +void PathStateFilter::Reset() { + path_state_->Revert(); + // Set all paths of path state to empty start -> end paths, + // and all nonstart/nonend nodes to node -> node loops. + const int num_nodes = path_state_->NumNodes(); + node_is_assigned_.assign(num_nodes, false); + const int num_paths = path_state_->NumPaths(); + for (int path = 0; path < num_paths; ++path) { + const int start = path_state_->Start(path); + const int end = path_state_->End(path); + path_state_->ChangeNext(start, end); + node_is_assigned_[start] = true; + node_is_assigned_[end] = true; + } + for (int node = 0; node < num_nodes; ++node) { + if (!node_is_assigned_[node]) path_state_->ChangeNext(node, node); + } + path_state_->CutChains(); + path_state_->Commit(); +} + +// The solver does not guarantee that a given Commit() corresponds to // the previous Relax() (or that there has been a call to Relax()), // so we replay the full change call sequence. -void PathStateFilter::Synchronize(const Assignment* delta, - const Assignment* deltadelta) { +void PathStateFilter::Commit(const Assignment* assignment, + const Assignment* delta) { path_state_->Revert(); - Relax(delta, deltadelta); + if (delta == nullptr || delta->Empty()) { + Relax(assignment, nullptr); + } else { + Relax(delta, nullptr); + } path_state_->Commit(); } @@ -2738,14 +2878,12 @@ UnaryDimensionChecker::UnaryDimensionChecker( DCHECK_EQ(num_paths, path_class_.size()); const int maximum_rmq_exponent = MostSignificantBitPosition32(num_nodes); partial_demand_sums_rmq_.resize(maximum_rmq_exponent + 1); - for (auto& sums : partial_demand_sums_rmq_) { - sums.resize(num_nodes + num_paths); - } - previous_nontrivial_index_.reserve(num_nodes + num_paths); + previous_nontrivial_index_.reserve(maximum_partial_demand_layer_size_); FullCommit(); } bool UnaryDimensionChecker::Check() const { + if (path_state_->IsInvalid()) return true; for (const int path : path_state_->ChangedPaths()) { const Interval path_capacity = path_capacity_[path]; if (path_capacity.min == kint64min && path_capacity.max == kint64max) { @@ -2910,7 +3048,9 @@ void UnaryDimensionChecker::UpdateRMQStructure(int begin_index, int end_index) { UnaryDimensionChecker::Interval UnaryDimensionChecker::GetMinMaxPartialDemandSum(int first_node_index, int last_node_index) const { + DCHECK_LE(0, first_node_index); DCHECK_LT(first_node_index, last_node_index); + DCHECK_LT(last_node_index, partial_demand_sums_rmq_[0].size()); // Find largest window_size = 2^layer such that // first_node_index < last_node_index - window_size + 1. const int layer = @@ -2925,7 +3065,9 @@ UnaryDimensionChecker::GetMinMaxPartialDemandSum(int first_node_index, bool UnaryDimensionChecker::SubpathOnlyHasTrivialNodes( int first_node_index, int last_node_index) const { - DCHECK_LE(first_node_index, last_node_index); + DCHECK_LE(0, first_node_index); + DCHECK_LT(first_node_index, last_node_index); + DCHECK_LT(last_node_index, previous_nontrivial_index_.size()); return first_node_index > previous_nontrivial_index_[last_node_index]; } @@ -3394,9 +3536,18 @@ class LocalSearchProfiler : public LocalSearchMonitor { } LocalSearchStatistics ExportToLocalSearchStatistics() const { LocalSearchStatistics statistics_proto; - for (const auto& operator_stats : operator_stats_) { - const LocalSearchOperator* const op = operator_stats.first; - const OperatorStats& stats = operator_stats.second; + std::vector operators; + for (const auto& stat : operator_stats_) { + operators.push_back(stat.first); + } + std::sort( + operators.begin(), operators.end(), + [this](const LocalSearchOperator* op1, const LocalSearchOperator* op2) { + return gtl::FindOrDie(operator_stats_, op1).neighbors > + gtl::FindOrDie(operator_stats_, op2).neighbors; + }); + for (const LocalSearchOperator* const op : operators) { + const OperatorStats& stats = gtl::FindOrDie(operator_stats_, op); LocalSearchStatistics::LocalSearchOperatorStatistics* const local_search_operator_statistics = statistics_proto.add_local_search_operator_statistics(); @@ -3409,6 +3560,32 @@ class LocalSearchProfiler : public LocalSearchMonitor { stats.accepted_neighbors); local_search_operator_statistics->set_duration_seconds(stats.seconds); } + std::vector filters; + for (const auto& stat : filter_stats_) { + filters.push_back(stat.first); + } + std::sort(filters.begin(), filters.end(), + [this](const LocalSearchFilter* filter1, + const LocalSearchFilter* filter2) { + return gtl::FindOrDie(filter_stats_, filter1).calls > + gtl::FindOrDie(filter_stats_, filter2).calls; + }); + for (const LocalSearchFilter* const filter : filters) { + const FilterStats& stats = gtl::FindOrDie(filter_stats_, filter); + LocalSearchStatistics::LocalSearchFilterStatistics* const + local_search_filter_statistics = + statistics_proto.add_local_search_filter_statistics(); + local_search_filter_statistics->set_local_search_filter( + filter->DebugString()); + local_search_filter_statistics->set_num_calls(stats.calls); + local_search_filter_statistics->set_num_rejects(stats.rejects); + local_search_filter_statistics->set_duration_seconds(stats.seconds); + } + statistics_proto.set_total_num_neighbors(solver()->neighbors()); + statistics_proto.set_total_num_filtered_neighbors( + solver()->filtered_neighbors()); + statistics_proto.set_total_num_accepted_neighbors( + solver()->accepted_neighbors()); return statistics_proto; } std::string PrintOverview() const { @@ -3677,6 +3854,31 @@ bool LocalSearchFilterManager::Accept(LocalSearchMonitor* const monitor, void LocalSearchFilterManager::Synchronize(const Assignment* assignment, const Assignment* delta) { + // If delta is nullptr or empty, then assignment may be a partial solution. + // Send a signal to Relaxing filters to inform them, + // so they can show the partial solution as a change from the empty solution. + const bool reset_to_assignment = delta == nullptr || delta->Empty(); + // Relax in the forward direction. + for (auto [filter, event_type] : filter_events_) { + switch (event_type) { + case FilterEventType::kAccept: { + break; + } + case FilterEventType::kRelax: { + if (reset_to_assignment) { + filter->Reset(); + filter->Relax(assignment, nullptr); + } else { + filter->Relax(delta, nullptr); + } + break; + } + default: + LOG(FATAL) << "Unknown filter event type."; + } + } + // Synchronize/Commit backwards, so filters can read changes from their + // dependencies before those are synchronized/committed. synchronized_value_ = 0; for (auto [filter, event_type] : ::gtl::reversed_view(filter_events_)) { switch (event_type) { @@ -3696,11 +3898,6 @@ void LocalSearchFilterManager::Synchronize(const Assignment* assignment, } } -LocalSearchFilterManager* Solver::MakeLocalSearchFilterManager( - std::vector filters) { - return RevAlloc(new LocalSearchFilterManager(std::move(filters))); -} - // ----- Finds a neighbor of the assignment passed ----- class FindOneNeighbor : public DecisionBuilder { @@ -4408,8 +4605,9 @@ class SynchronizeFiltersDecisionBuilder : public DecisionBuilder { : assignment_(assignment), filter_manager_(filter_manager) {} Decision* Next(Solver* const solver) override { - if (filter_manager_ != nullptr) + if (filter_manager_ != nullptr) { filter_manager_->Synchronize(assignment_, nullptr); + } return nullptr; } diff --git a/ortools/constraint_solver/local_search_stats.proto b/ortools/constraint_solver/local_search_stats.proto deleted file mode 100644 index 4240d1bf3b..0000000000 --- a/ortools/constraint_solver/local_search_stats.proto +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2010-2018 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Protocol buffer used to store statistics on local search. - -syntax = "proto3"; - -option java_package = "com.google.ortools.constraintsolver"; -option java_multiple_files = true; -option csharp_namespace = "Google.OrTools.ConstraintSolver"; - -package operations_research; - -message LocalSearchStatistics { - message LocalSearchOperatorStatistics { - string local_search_operator = 1; - int64 num_neighbors = 2; - int64 num_filtered_neighbors = 3; - int64 num_accepted_neighbors = 4; - double duration_seconds = 5; - } - repeated LocalSearchOperatorStatistics local_search_operator_statistics = 1; -} diff --git a/ortools/constraint_solver/python/pywrapcp_util.h b/ortools/constraint_solver/python/pywrapcp_util.h index 78454a0e8b..783020bcfb 100644 --- a/ortools/constraint_solver/python/pywrapcp_util.h +++ b/ortools/constraint_solver/python/pywrapcp_util.h @@ -92,4 +92,5 @@ class CallPyDecisionBuilder : public operations_research::DecisionBuilder { PyObject* str_func_; }; + #endif // OR_TOOLS_CONSTRAINT_SOLVER_PYTHON_PYWRAPCP_UTIL_H_ diff --git a/ortools/constraint_solver/routing.cc b/ortools/constraint_solver/routing.cc index cb14a13ffb..7f2aa9a06b 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/constraint_solver/routing.cc @@ -48,7 +48,7 @@ #include "ortools/constraint_solver/routing_lp_scheduling.h" #include "ortools/constraint_solver/routing_neighborhoods.h" #include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/graph/connectivity.h" +#include "ortools/graph/connected_components.h" #include "ortools/graph/linear_assignment.h" #include "ortools/graph/min_cost_flow.h" #include "ortools/graph/topologicalsorter.h" @@ -221,7 +221,7 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { LocalDimensionCumulOptimizer* const optimizer = vehicle_has_break_constraint ? local_mp_optimizers_[i].get() : local_optimizer.get(); - // DCHECK_NE(optimizer, nullptr); + DCHECK_NE(optimizer, nullptr); std::vector cumul_values; std::vector break_start_end_values; const DimensionSchedulingStatus status = compute_cumul_values( @@ -1471,7 +1471,9 @@ void RoutingModel::ComputeVehicleClasses() { nodes_unvisitability_num_bytes); for (int index = 0; index < vehicle_vars_.size(); ++index) { IntVar* const vehicle_var = vehicle_vars_[index]; - if (!IsStart(index) && !IsEnd(index) && !vehicle_var->Contains(vehicle)) { + if (!IsStart(index) && !IsEnd(index) && + (!vehicle_var->Contains(vehicle) || + !IsVehicleAllowedForIndex(vehicle, index))) { nodes_unvisitability_bitmask[index / CHAR_BIT] |= 1U << (index % CHAR_BIT); } @@ -1571,8 +1573,23 @@ void RoutingModel::FinalizeVisitTypes() { } } - std::vector> requirement_arcs; + TopologicallySortVisitTypes(); +} + +void RoutingModel::TopologicallySortVisitTypes() { + if (!has_same_vehicle_type_requirements_ && + !has_temporal_type_requirements_) { + return; + } + std::vector> type_requirement_tightness( + num_visit_types_, {0, 0}); + std::vector> type_to_dependent_types( + num_visit_types_); + SparseBitset<> types_in_requirement_graph(num_visit_types_); + std::vector in_degree(num_visit_types_, 0); for (int type = 0; type < num_visit_types_; type++) { + int num_alternative_required_types = 0; + int num_required_sets = 0; for (const std::vector>* required_type_alternatives : {&required_type_alternatives_when_adding_type_index_[type], @@ -1580,15 +1597,78 @@ void RoutingModel::FinalizeVisitTypes() { &same_vehicle_required_type_alternatives_per_type_index_[type]}) { for (const absl::flat_hash_set& alternatives : *required_type_alternatives) { + types_in_requirement_graph.Set(type); + num_required_sets++; for (int required_type : alternatives) { - requirement_arcs.emplace_back(required_type, type); + type_requirement_tightness[required_type].second += + 1.0 / alternatives.size(); + types_in_requirement_graph.Set(required_type); + num_alternative_required_types++; + if (type_to_dependent_types[required_type].insert(type).second) { + in_degree[type]++; + } } } } + if (num_alternative_required_types > 0) { + type_requirement_tightness[type].first += 1.0 * num_required_sets * + num_required_sets / + num_alternative_required_types; + } } - if (requirement_arcs.empty()) return; - if (!util::DenseIntTopologicalSort(num_visit_types_, requirement_arcs, - &topologically_sorted_visit_types_)) { + + // Compute topological order of visit types. + topologically_sorted_visit_types_.clear(); + std::vector current_types_with_zero_indegree; + for (int type : types_in_requirement_graph.PositionsSetAtLeastOnce()) { + DCHECK(type_requirement_tightness[type].first > 0 || + type_requirement_tightness[type].second > 0); + if (in_degree[type] == 0) { + current_types_with_zero_indegree.push_back(type); + } + } + + int num_types_added = 0; + while (!current_types_with_zero_indegree.empty()) { + // Add all zero-degree nodes to the same topological order group, while + // also marking their dependent types that become part of the next group. + topologically_sorted_visit_types_.push_back({}); + std::vector& topological_group = + topologically_sorted_visit_types_.back(); + std::vector next_types_with_zero_indegree; + for (int type : current_types_with_zero_indegree) { + topological_group.push_back(type); + num_types_added++; + for (int dependent_type : type_to_dependent_types[type]) { + DCHECK_GT(in_degree[dependent_type], 0); + if (--in_degree[dependent_type] == 0) { + next_types_with_zero_indegree.push_back(dependent_type); + } + } + } + // Sort the types in the current topological group based on their + // requirement tightness. + // NOTE: For a deterministic order, types with equal tightness are sorted by + // increasing type. + // TODO(user): Put types of the same topological order and same + // requirement tightness in a single group (so that they all get inserted + // simultaneously by the GlobalCheapestInsertion heuristic, for instance). + std::sort(topological_group.begin(), topological_group.end(), + [&type_requirement_tightness](int type1, int type2) { + const auto& tightness1 = type_requirement_tightness[type1]; + const auto& tightness2 = type_requirement_tightness[type2]; + return tightness1 > tightness2 || + (tightness1 == tightness2 && type1 < type2); + }); + // Swap the current types with zero in-degree with the next ones. + current_types_with_zero_indegree.swap(next_types_with_zero_indegree); + } + + const int num_types_in_requirement_graph = + types_in_requirement_graph.NumberOfSetCallsWithDifferentArguments(); + DCHECK_LE(num_types_added, num_types_in_requirement_graph); + if (num_types_added < num_types_in_requirement_graph) { + // Requirement graph is cyclic, no topological order. topologically_sorted_visit_types_.clear(); } } @@ -1909,7 +1989,7 @@ void RoutingModel::CloseModel() { class RoutingModelInspector : public ModelVisitor { public: explicit RoutingModelInspector(RoutingModel* model) : model_(model) { - same_vehicle_components_.Init(model->Size()); + same_vehicle_components_.SetNumberOfNodes(model->Size()); for (const std::string& name : model->GetAllDimensionNames()) { RoutingDimension* const dimension = model->GetMutableDimension(name); const std::vector& cumuls = dimension->cumuls(); @@ -1925,24 +2005,13 @@ class RoutingModelInspector : public ModelVisitor { } ~RoutingModelInspector() override {} void EndVisitModel(const std::string& solver_name) override { - // Compact same vehicle component indices. - absl::flat_hash_map component_indices; - int component_index = 0; + const std::vector node_to_same_vehicle_component_id = + same_vehicle_components_.GetComponentIds(); + model_->InitSameVehicleGroups( + same_vehicle_components_.GetNumberOfComponents()); for (int node = 0; node < model_->Size(); ++node) { - const int component = - same_vehicle_components_.GetClassRepresentative(node); - if (gtl::InsertIfNotPresent(&component_indices, component, - component_index)) { - ++component_index; - } - } - model_->InitSameVehicleGroups(component_indices.size()); - for (int node = 0; node < model_->Size(); ++node) { - const int component = - same_vehicle_components_.GetClassRepresentative(node); - DCHECK(gtl::ContainsKey(component_indices, component)); - model_->SetSameVehicleGroup( - node, gtl::FindWithDefault(component_indices, component, 0)); + model_->SetSameVehicleGroup(node, + node_to_same_vehicle_component_id[node]); } // TODO(user): Perform transitive closure of dimension precedence graphs. // TODO(user): Have a single annotated precedence graph. @@ -2006,7 +2075,7 @@ class RoutingModelInspector : public ModelVisitor { gtl::FindCopy(vehicle_var_to_indices_, right_, &right_index)) { VLOG(2) << "Vehicle variables for " << left_index << " and " << right_index << " are equal."; - same_vehicle_components_.AddArc(left_index, right_index); + same_vehicle_components_.AddEdge(left_index, right_index); } left_ = nullptr; right_ = nullptr; @@ -2031,7 +2100,7 @@ class RoutingModelInspector : public ModelVisitor { } RoutingModel* const model_; - ConnectedComponents same_vehicle_components_; + DenseConnectedComponentsFinder same_vehicle_components_; absl::flat_hash_map> cumul_to_dim_indices_; absl::flat_hash_map vehicle_var_to_indices_; @@ -3130,7 +3199,8 @@ absl::Duration GetTimeLimit(const RoutingSearchParameters& parameters) { absl::Duration GetLnsTimeLimit(const RoutingSearchParameters& parameters) { if (!parameters.has_lns_time_limit()) return absl::InfiniteDuration(); - return util_time::DecodeGoogleApiProto(parameters.lns_time_limit()).value(); + return util_time::DecodeGoogleApiProto(parameters.lns_time_limit()) + .value(); } } // namespace @@ -4500,7 +4570,8 @@ void RoutingModel::CreateNeighborhoodOperators( /* is_sequential */ false, /* farthest_seeds_ratio */ 0.0, parameters.cheapest_insertion_ls_operator_neighbors_ratio(), - /* use_neighbors_ratio_for_initialization */ true}; + /* use_neighbors_ratio_for_initialization */ true, + parameters.cheapest_insertion_add_unperformed_entries()}; local_search_operators_[GLOBAL_CHEAPEST_INSERTION_CLOSE_NODES_LNS] = solver_->RevAlloc(new FilteredHeuristicCloseNodesLNSOperator( absl::make_unique( @@ -4542,6 +4613,22 @@ void RoutingModel::CreateNeighborhoodOperators( return GetArcCostForVehicle(i, j, vehicle); }, GetOrCreateFeasibilityFilterManager(parameters)))); + + local_search_operators_ + [RELOCATE_PATH_GLOBAL_CHEAPEST_INSERTION_INSERT_UNPERFORMED] = + solver_->RevAlloc( + new RelocatePathAndHeuristicInsertUnperformedOperator( + absl::make_unique( + this, + [this](int64 i, int64 j, int64 vehicle) { + return GetArcCostForVehicle(i, j, vehicle); + }, + [this](int64 i) { + return UnperformedPenaltyOrValue(0, i); + }, + GetOrCreateFeasibilityFilterManager(parameters), + ls_gci_parameters))); + local_search_operators_[GLOBAL_CHEAPEST_INSERTION_EXPENSIVE_CHAIN_LNS] = solver_->RevAlloc(new FilteredHeuristicExpensiveChainLNSOperator( absl::make_unique( @@ -4686,6 +4773,9 @@ LocalSearchOperator* RoutingModel::GetNeighborhoodOperators( global_cheapest_insertion_path_lns, operators); CP_ROUTING_PUSH_OPERATOR(LOCAL_CHEAPEST_INSERTION_PATH_LNS, local_cheapest_insertion_path_lns, operators); + CP_ROUTING_PUSH_OPERATOR( + RELOCATE_PATH_GLOBAL_CHEAPEST_INSERTION_INSERT_UNPERFORMED, + relocate_path_global_cheapest_insertion_insert_unperformed, operators); } CP_ROUTING_PUSH_OPERATOR(GLOBAL_CHEAPEST_INSERTION_EXPENSIVE_CHAIN_LNS, global_cheapest_insertion_expensive_chain_lns, @@ -4728,8 +4818,34 @@ LocalSearchOperator* RoutingModel::GetNeighborhoodOperators( #undef CP_ROUTING_PUSH_OPERATOR -std::vector RoutingModel::GetOrCreateLocalSearchFilters( - const RoutingSearchParameters& parameters) { +bool HasUnaryDimension(const std::vector& dimensions) { + for (const RoutingDimension* dimension : dimensions) { + if (dimension->GetUnaryTransitEvaluator(0) != nullptr) return true; + } + return false; +} + +namespace { + +void ConvertVectorInt64ToVectorInt(const std::vector& input, + std::vector* output) { + const int n = input.size(); + output->resize(n); + int* data = output->data(); + for (int i = 0; i < n; ++i) { + const int element = static_cast(input[i]); + DCHECK_EQ(input[i], static_cast(element)); + data[i] = element; + } +} + +} // namespace + +std::vector +RoutingModel::GetOrCreateLocalSearchFilters( + const RoutingSearchParameters& parameters, bool filter_cost) { + const auto kAccept = LocalSearchFilterManager::FilterEventType::kAccept; + const auto kRelax = LocalSearchFilterManager::FilterEventType::kRelax; // As of 2013/01, three filters evaluate sub-parts of the objective // function: // - NodeDisjunctionFilter: takes disjunction penalty costs into account, @@ -4739,54 +4855,77 @@ std::vector RoutingModel::GetOrCreateLocalSearchFilters( // related to amortized linear and quadratic vehicle cost factors. // - LocalSearchObjectiveFilter, which takes dimension "arc" costs into // account. - std::vector filters; + std::vector filters; // VehicleAmortizedCostFilter can have a negative value, so it must be first. - if (vehicle_amortized_cost_factors_set_) { - filters.push_back(MakeVehicleAmortizedCostFilter(*this)); + if (filter_cost && vehicle_amortized_cost_factors_set_) { + filters.push_back({MakeVehicleAmortizedCostFilter(*this), kAccept}); } // The SumObjectiveFilter has the best reject/second ratio in practice, // so it is the earliest. - if (CostsAreHomogeneousAcrossVehicles()) { - filters.push_back(solver_->MakeSumObjectiveFilter( - nexts_, [this](int64 i, int64 j) { return GetHomogeneousCost(i, j); }, - Solver::LE)); - } else { - filters.push_back(solver_->MakeSumObjectiveFilter( - nexts_, vehicle_vars_, - [this](int64 i, int64 j, int64 k) { - return GetArcCostForVehicle(i, j, k); - }, - Solver::LE)); + if (filter_cost) { + if (CostsAreHomogeneousAcrossVehicles()) { + LocalSearchFilter* sum = solver_->MakeSumObjectiveFilter( + nexts_, [this](int64 i, int64 j) { return GetHomogeneousCost(i, j); }, + Solver::LE); + filters.push_back({sum, kAccept}); + } else { + LocalSearchFilter* sum = solver_->MakeSumObjectiveFilter( + nexts_, vehicle_vars_, + [this](int64 i, int64 j, int64 k) { + return GetArcCostForVehicle(i, j, k); + }, + Solver::LE); + filters.push_back({sum, kAccept}); + } } - filters.push_back(solver_->MakeVariableDomainFilter()); + filters.push_back({solver_->MakeVariableDomainFilter(), kAccept}); if (vehicles_ > max_active_vehicles_) { - filters.push_back(MakeMaxActiveVehiclesFilter(*this)); + filters.push_back({MakeMaxActiveVehiclesFilter(*this), kAccept}); } if (!disjunctions_.empty()) { - filters.push_back(MakeNodeDisjunctionFilter(*this)); + filters.push_back({MakeNodeDisjunctionFilter(*this), kAccept}); } if (!pickup_delivery_pairs_.empty()) { - filters.push_back(MakePickupDeliveryFilter( - *this, pickup_delivery_pairs_, vehicle_pickup_delivery_policy_)); + filters.push_back( + {MakePickupDeliveryFilter(*this, pickup_delivery_pairs_, + vehicle_pickup_delivery_policy_), + kAccept}); } if (HasTypeRegulations()) { - filters.push_back(MakeTypeRegulationsFilter(*this)); + filters.push_back({MakeTypeRegulationsFilter(*this), kAccept}); } - filters.push_back(MakeVehicleVarFilter(*this)); + filters.push_back({MakeVehicleVarFilter(*this), kAccept}); - AppendDimensionCumulFilters(GetDimensions(), parameters, - /*filter_objective_cost*/ true, &filters); + const PathState* path_state_reference = nullptr; + if (HasUnaryDimension(GetDimensions())) { + std::vector path_starts; + std::vector path_ends; + ConvertVectorInt64ToVectorInt(starts_, &path_starts); + ConvertVectorInt64ToVectorInt(ends_, &path_ends); + + auto path_state = absl::make_unique( + Size() + vehicles(), std::move(path_starts), std::move(path_ends)); + path_state_reference = path_state.get(); + filters.push_back( + {MakePathStateFilter(solver_.get(), std::move(path_state), Nexts()), + kRelax}); + AppendLightWeightDimensionFilters(path_state_reference, GetDimensions(), + &filters); + } + + AppendDimensionCumulFilters(GetDimensions(), parameters, filter_cost, + &filters); for (const RoutingDimension* dimension : dimensions_) { if (!dimension->HasBreakConstraints()) continue; - filters.push_back(MakeVehicleBreaksFilter(*this, *dimension)); + filters.push_back({MakeVehicleBreaksFilter(*this, *dimension), kAccept}); } filters.insert(filters.end(), extra_filters_.begin(), extra_filters_.end()); return filters; @@ -4795,51 +4934,25 @@ std::vector RoutingModel::GetOrCreateLocalSearchFilters( LocalSearchFilterManager* RoutingModel::GetOrCreateLocalSearchFilterManager( const RoutingSearchParameters& parameters) { if (!local_search_filter_manager_) { - local_search_filter_manager_ = solver_->MakeLocalSearchFilterManager( - GetOrCreateLocalSearchFilters(parameters)); + local_search_filter_manager_ = + solver_->RevAlloc(new LocalSearchFilterManager( + GetOrCreateLocalSearchFilters(parameters))); } return local_search_filter_manager_; } -std::vector RoutingModel::GetOrCreateFeasibilityFilters( +std::vector +RoutingModel::GetOrCreateFeasibilityFilters( const RoutingSearchParameters& parameters) { - std::vector filters; - if (vehicles_ > max_active_vehicles_) { - filters.push_back(MakeMaxActiveVehiclesFilter(*this)); - } - if (!disjunctions_.empty()) { - filters.push_back(MakeNodeDisjunctionFilter(*this)); - } - filters.push_back(solver_->MakeVariableDomainFilter()); - if (!pickup_delivery_pairs_.empty()) { - filters.push_back(MakePickupDeliveryFilter( - *this, pickup_delivery_pairs_, vehicle_pickup_delivery_policy_)); - } - if (HasTypeRegulations()) { - filters.push_back(MakeTypeRegulationsFilter(*this)); - } - filters.push_back(MakeVehicleVarFilter(*this)); - - AppendDimensionCumulFilters(GetDimensions(), parameters, - /*filter_objective_cost*/ false, &filters); - - for (const RoutingDimension* dimension : dimensions_) { - if (dimension->HasBreakConstraints()) { - IntVarLocalSearchFilter* breaks_filter = - MakeVehicleBreaksFilter(*this, *dimension); - filters.push_back(breaks_filter); - } - } - - filters.insert(filters.end(), extra_filters_.begin(), extra_filters_.end()); - return filters; + return GetOrCreateLocalSearchFilters(parameters, false); } LocalSearchFilterManager* RoutingModel::GetOrCreateFeasibilityFilterManager( const RoutingSearchParameters& parameters) { if (!feasibility_filter_manager_) { - feasibility_filter_manager_ = solver_->MakeLocalSearchFilterManager( - GetOrCreateFeasibilityFilters(parameters)); + feasibility_filter_manager_ = + solver_->RevAlloc(new LocalSearchFilterManager( + GetOrCreateFeasibilityFilters(parameters))); } return feasibility_filter_manager_; } @@ -4848,11 +4961,12 @@ LocalSearchFilterManager* RoutingModel::GetOrCreateStrongFeasibilityFilterManager( const RoutingSearchParameters& parameters) { if (!strong_feasibility_filter_manager_) { - std::vector filters = + std::vector filters = GetOrCreateFeasibilityFilters(parameters); - filters.push_back(MakeCPFeasibilityFilter(this)); + filters.push_back({MakeCPFeasibilityFilter(this), + LocalSearchFilterManager::FilterEventType::kAccept}); strong_feasibility_filter_manager_ = - solver_->MakeLocalSearchFilterManager(std::move(filters)); + solver_->RevAlloc(new LocalSearchFilterManager(std::move(filters))); } return strong_feasibility_filter_manager_; } @@ -4872,6 +4986,7 @@ void RoutingModel::StoreDimensionCumulOptimizers( const RoutingSearchParameters& parameters) { Assignment* packed_dimensions_collector_assignment = solver_->MakeAssignment(); + packed_dimensions_collector_assignment->AddObjective(CostVar()); const int num_dimensions = dimensions_.size(); local_optimizer_index_.resize(num_dimensions, -1); global_optimizer_index_.resize(num_dimensions, -1); @@ -5159,7 +5274,8 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( /* is_sequential */ false, search_parameters.cheapest_insertion_farthest_seeds_ratio(), search_parameters.cheapest_insertion_first_solution_neighbors_ratio(), - /* use_neighbors_ratio_for_initialization */ false}; + /* use_neighbors_ratio_for_initialization */ false, + search_parameters.cheapest_insertion_add_unperformed_entries()}; for (bool is_sequential : {false, true}) { FirstSolutionStrategy::Value first_solution_strategy = is_sequential ? FirstSolutionStrategy::SEQUENTIAL_CHEAPEST_INSERTION @@ -5489,18 +5605,33 @@ void RoutingModel::SetupTrace( search_parameters.log_cost_scaling_factor(); search_log_parameters.offset = search_parameters.log_cost_offset(); if (!search_parameters.log_tag().empty()) { - const std::string tag = search_parameters.log_tag(); + const std::string& tag = search_parameters.log_tag(); search_log_parameters.display_callback = [tag]() { return tag; }; } else { search_log_parameters.display_callback = nullptr; } + search_log_parameters.display_on_new_solutions_only = false; monitors_.push_back(solver_->MakeSearchLog(search_log_parameters)); } } +void RoutingModel::SetupImprovementLimit( + const RoutingSearchParameters& search_parameters) { + if (search_parameters.has_improvement_limit_parameters()) { + monitors_.push_back(solver_->MakeImprovementLimit( + cost_, /*maximize=*/false, search_parameters.log_cost_scaling_factor(), + search_parameters.log_cost_offset(), + search_parameters.improvement_limit_parameters() + .improvement_rate_coefficient(), + search_parameters.improvement_limit_parameters() + .improvement_rate_solutions_distance())); + } +} + void RoutingModel::SetupSearchMonitors( const RoutingSearchParameters& search_parameters) { monitors_.push_back(GetOrCreateLimit()); + SetupImprovementLimit(search_parameters); SetupMetaheuristics(search_parameters); SetupAssignmentCollector(search_parameters); SetupTrace(search_parameters); diff --git a/ortools/constraint_solver/routing.h b/ortools/constraint_solver/routing.h index f3a88baa06..0679aa5698 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/constraint_solver/routing.h @@ -778,10 +778,13 @@ class RoutingModel { /// "close" types. void CloseVisitTypes(); int GetNumberOfVisitTypes() const { return num_visit_types_; } - const std::vector& GetTopologicallySortedVisitTypes() const { +#ifndef SWIG + const std::vector>& GetTopologicallySortedVisitTypes() + const { DCHECK(closed_); return topologically_sorted_visit_types_; } +#endif // SWIG /// Incompatibilities: /// Two nodes with "hard" incompatible types cannot share the same route at /// all, while with a "temporal" incompatibility they can't be on the same @@ -1147,7 +1150,8 @@ class RoutingModel { if (closed_) { LOG(WARNING) << "Model is closed, filter addition will be ignored."; } - extra_filters_.push_back(filter); + extra_filters_.push_back({filter, LocalSearchFilterManager::kRelax}); + extra_filters_.push_back({filter, LocalSearchFilterManager::kAccept}); } /// Model inspection. @@ -1225,6 +1229,9 @@ class RoutingModel { /// Get the cost class index of the given vehicle. CostClassIndex GetCostClassIndexOfVehicle(int64 vehicle) const { DCHECK(closed_); + DCHECK_GE(vehicle, 0); + DCHECK_LT(vehicle, cost_class_index_of_vehicle_.size()); + DCHECK_GE(cost_class_index_of_vehicle_[vehicle], 0); return cost_class_index_of_vehicle_[vehicle]; } /// Returns true iff the model contains a vehicle with the given @@ -1400,6 +1407,7 @@ class RoutingModel { LOCAL_CHEAPEST_INSERTION_CLOSE_NODES_LNS, GLOBAL_CHEAPEST_INSERTION_PATH_LNS, LOCAL_CHEAPEST_INSERTION_PATH_LNS, + RELOCATE_PATH_GLOBAL_CHEAPEST_INSERTION_INSERT_UNPERFORMED, GLOBAL_CHEAPEST_INSERTION_EXPENSIVE_CHAIN_LNS, LOCAL_CHEAPEST_INSERTION_EXPENSIVE_CHAIN_LNS, RELOCATE_EXPENSIVE_CHAIN, @@ -1518,6 +1526,8 @@ class RoutingModel { /// topological order based on required-->dependent arcs from the /// visit type requirements. void FinalizeVisitTypes(); + // Called by FinalizeVisitTypes() to setup topologically_sorted_visit_types_. + void TopologicallySortVisitTypes(); int64 GetArcCostForClassInternal(int64 from_index, int64 to_index, CostClassIndex cost_class_index) const; void AppendHomogeneousArcCosts(const RoutingSearchParameters& parameters, @@ -1607,12 +1617,13 @@ class RoutingModel { void CreateNeighborhoodOperators(const RoutingSearchParameters& parameters); LocalSearchOperator* GetNeighborhoodOperators( const RoutingSearchParameters& search_parameters) const; - std::vector GetOrCreateLocalSearchFilters( - const RoutingSearchParameters& parameters); + std::vector + GetOrCreateLocalSearchFilters(const RoutingSearchParameters& parameters, + bool filter_cost = true); LocalSearchFilterManager* GetOrCreateLocalSearchFilterManager( const RoutingSearchParameters& parameters); - std::vector GetOrCreateFeasibilityFilters( - const RoutingSearchParameters& parameters); + std::vector + GetOrCreateFeasibilityFilters(const RoutingSearchParameters& parameters); LocalSearchFilterManager* GetOrCreateFeasibilityFilterManager( const RoutingSearchParameters& parameters); LocalSearchFilterManager* GetOrCreateStrongFeasibilityFilterManager( @@ -1634,6 +1645,7 @@ class RoutingModel { void SetupAssignmentCollector( const RoutingSearchParameters& search_parameters); void SetupTrace(const RoutingSearchParameters& search_parameters); + void SetupImprovementLimit(const RoutingSearchParameters& search_parameters); void SetupSearchMonitors(const RoutingSearchParameters& search_parameters); bool UsesLightPropagation( const RoutingSearchParameters& search_parameters) const; @@ -1771,11 +1783,24 @@ class RoutingModel { bool has_temporal_type_requirements_; absl::flat_hash_map > trivially_infeasible_visit_types_to_policies_; - // clang-format on // Visit types sorted topologically based on required-->dependent requirement // arcs between the types (if the requirement/dependency graph is acyclic). - std::vector topologically_sorted_visit_types_; + // Visit types of the same topological level are sorted in each sub-vector + // by decreasing requirement "tightness", computed as the pair of the two + // following criteria: + // + // 1) How highly *dependent* this type is, determined by + // (total number of required alternative sets for that type) + // / (average number of types in the required alternative sets) + // 2) How highly *required* this type t is, computed as + // SUM_{S required set containing t} ( 1 / |S| ), + // i.e. the sum of reverse number of elements of all required sets + // containing the type t. + // + // The higher these two numbers, the tighter the type is wrt requirements. + std::vector > topologically_sorted_visit_types_; + // clang-format on int num_visit_types_; // Two indices are equivalent if they correspond to the same node (as given // to the constructors taking a RoutingIndexManager). @@ -1817,7 +1842,7 @@ class RoutingModel { LocalSearchFilterManager* local_search_filter_manager_ = nullptr; LocalSearchFilterManager* feasibility_filter_manager_ = nullptr; LocalSearchFilterManager* strong_feasibility_filter_manager_ = nullptr; - std::vector extra_filters_; + std::vector extra_filters_; #ifndef SWIG std::vector> finalizer_variable_cost_pairs_; std::vector> finalizer_variable_target_pairs_; @@ -2454,9 +2479,6 @@ class RoutingDimension { /// cumulVar). /// This is also handy to model earliness costs when the dimension represents /// time. - /// Note: Using soft lower and upper bounds or span costs together is, as of - /// 6/2014, not well supported in the sense that an optimal schedule is not - /// guaranteed. void SetCumulVarSoftLowerBound(int64 index, int64 lower_bound, int64 coefficient); /// Returns true if a soft lower bound has been set for a given variable @@ -2779,6 +2801,10 @@ class RoutingDimension { vehicle_quadratic_cost_soft_span_upper_bound_; friend class RoutingModel; friend class RoutingModelInspector; + friend void AppendDimensionCumulFilters( + const std::vector& dimensions, + const RoutingSearchParameters& parameters, bool filter_objective_cost, + std::vector* filters); DISALLOW_COPY_AND_ASSIGN(RoutingDimension); }; @@ -3118,6 +3144,11 @@ class GlobalCheapestInsertionFilteredHeuristic /// as insertion positions during initialization. Otherwise, all possible /// insertion positions are considered. bool use_neighbors_ratio_for_initialization; + /// If true, entries are created for making the nodes/pairs unperformed, and + /// when the cost of making a node unperformed is lower than all insertions, + /// the node/pair will be made unperformed. If false, only entries making + /// a node/pair performed are considered. + bool add_unperformed_entries; }; /// Takes ownership of evaluators. @@ -3138,22 +3169,21 @@ class GlobalCheapestInsertionFilteredHeuristic typedef absl::flat_hash_set PairEntries; typedef absl::flat_hash_set NodeEntries; - /// Inserts non-inserted single nodes which have a visit type in the - /// type requirement graph, i.e. required for or requiring another type for - /// insertions. + /// Inserts non-inserted single nodes or pickup/delivery pairs which have a + /// visit type in the type requirement graph, i.e. required for or requiring + /// another type for insertions. /// These nodes are inserted iff the requirement graph is acyclic, in which /// case nodes are inserted based on the topological order of their type, /// given by the routing model's GetTopologicallySortedVisitTypes() method. - /// TODO(user): Adapt this method to also insert pairs. - void InsertNodesByRequirementTopologicalOrder(); + void InsertPairsAndNodesByRequirementTopologicalOrder(); - /// Inserts all non-inserted pickup and delivery pairs. Maintains a priority + /// Inserts non-inserted pickup and delivery pairs. Maintains a priority /// queue of possible pair insertions, which is incrementally updated when a /// pair insertion is committed. Incrementality is obtained by updating pair /// insertion positions on the four newly modified route arcs: after the /// pickup insertion position, after the pickup position, after the delivery /// insertion position and after the delivery position. - void InsertPairs(); + void InsertPairs(const std::vector& pair_indices); /// Inserts non-inserted individual nodes on the given routes (or all routes /// if "vehicles" is an empty vector), by constructing routes in parallel. @@ -3198,9 +3228,10 @@ class GlobalCheapestInsertionFilteredHeuristic Queue* priority_queue, std::vector* is_vehicle_used); // clang-format on - /// Initializes the priority queue and the pair entries with the current state - /// of the solution. + /// Initializes the priority queue and the pair entries for the given pair + /// indices with the current state of the solution. void InitializePairPositions( + const std::vector& pair_indices, AdjustablePriorityQueue* priority_queue, std::vector* pickup_to_entries, std::vector* delivery_to_entries); @@ -3746,7 +3777,9 @@ class BasePathFilter : public IntVarLocalSearchFilter { std::vector new_nexts_; std::vector delta_touched_; SparseBitset<> touched_paths_; - SparseBitset<> touched_path_nodes_; + // clang-format off + std::vector > touched_path_chain_start_ends_; + // clang-format on std::vector ranks_; Status status_; @@ -3765,7 +3798,7 @@ class BasePathFilter : public IntVarLocalSearchFilter { // TODO(user): Avoid such false negatives. class CPFeasibilityFilter : public IntVarLocalSearchFilter { public: - explicit CPFeasibilityFilter(const RoutingModel* routing_model); + explicit CPFeasibilityFilter(RoutingModel* routing_model); ~CPFeasibilityFilter() override {} std::string DebugString() const override { return "CPFeasibilityFilter"; } bool Accept(const Assignment* delta, const Assignment* deltadelta, @@ -3781,6 +3814,7 @@ class CPFeasibilityFilter : public IntVarLocalSearchFilter { Assignment* const assignment_; Assignment* const temp_assignment_; DecisionBuilder* const restore_; + SearchLimit* const limit_; }; #if !defined(SWIG) @@ -3795,7 +3829,11 @@ IntVarLocalSearchFilter* MakeTypeRegulationsFilter( void AppendDimensionCumulFilters( const std::vector& dimensions, const RoutingSearchParameters& parameters, bool filter_objective_cost, - std::vector* filters); + std::vector* filters); +void AppendLightWeightDimensionFilters( + const PathState* path_state, + const std::vector& dimensions, + std::vector* filters); IntVarLocalSearchFilter* MakePathCumulFilter( const RoutingDimension& dimension, const RoutingSearchParameters& parameters, @@ -3812,8 +3850,7 @@ IntVarLocalSearchFilter* MakeVehicleVarFilter( const RoutingModel& routing_model); IntVarLocalSearchFilter* MakeVehicleBreaksFilter( const RoutingModel& routing_model, const RoutingDimension& dimension); -IntVarLocalSearchFilter* MakeCPFeasibilityFilter( - const RoutingModel* routing_model); +IntVarLocalSearchFilter* MakeCPFeasibilityFilter(RoutingModel* routing_model); #endif } // namespace operations_research diff --git a/ortools/constraint_solver/routing_flags.cc b/ortools/constraint_solver/routing_flags.cc index 5b0e6e4b91..dab2d07503 100644 --- a/ortools/constraint_solver/routing_flags.cc +++ b/ortools/constraint_solver/routing_flags.cc @@ -210,6 +210,9 @@ void AddLocalSearchNeighborhoodOperatorsFromFlags( local_search_operators->set_use_cross_exchange(BOOL_FALSE); local_search_operators->set_use_global_cheapest_insertion_path_lns(BOOL_TRUE); local_search_operators->set_use_local_cheapest_insertion_path_lns(BOOL_TRUE); + local_search_operators + ->set_use_relocate_path_global_cheapest_insertion_insert_unperformed( + BOOL_TRUE); local_search_operators->set_use_global_cheapest_insertion_expensive_chain_lns( BOOL_FALSE); local_search_operators->set_use_local_cheapest_insertion_expensive_chain_lns( diff --git a/ortools/constraint_solver/routing_neighborhoods.cc b/ortools/constraint_solver/routing_neighborhoods.cc index b5d291b8d9..527a80bba7 100644 --- a/ortools/constraint_solver/routing_neighborhoods.cc +++ b/ortools/constraint_solver/routing_neighborhoods.cc @@ -651,22 +651,102 @@ void IndexPairSwapActiveOperator::OnNodeInitialization() { inactive_node_ = Size(); } -// FilteredHeuristicPathLNSOperator +// FilteredHeuristicLocalSearchOperator -FilteredHeuristicPathLNSOperator::FilteredHeuristicPathLNSOperator( - std::unique_ptr heuristic) - : IntVarLocalSearchOperator(heuristic->model()->Nexts()), +FilteredHeuristicLocalSearchOperator::FilteredHeuristicLocalSearchOperator( + std::unique_ptr heuristic, + bool keep_inverse_values) + : IntVarLocalSearchOperator(heuristic->model()->Nexts(), + keep_inverse_values), + model_(*heuristic->model()), + removed_nodes_(model_.Size()), heuristic_(std::move(heuristic)), - model_(*heuristic_->model()), - consider_vehicle_vars_(!model_.CostsAreHomogeneousAcrossVehicles()), - current_route_(0), - last_route_(0), - just_started_(false) { + consider_vehicle_vars_(!model_.CostsAreHomogeneousAcrossVehicles()) { if (consider_vehicle_vars_) { AddVars(model_.VehicleVars()); } } +bool FilteredHeuristicLocalSearchOperator::MakeOneNeighbor() { + while (IncrementPosition()) { + // NOTE: No need to call RevertChanges() here as MakeChangeAndInsertNodes() + // will always return true if any change was made. + if (MakeChangesAndInsertNodes()) { + return true; + } + } + return false; +} + +bool FilteredHeuristicLocalSearchOperator::MakeChangesAndInsertNodes() { + removed_nodes_.SparseClearAll(); + + const std::function next_accessor = + SetupNextAccessorForNeighbor(); + if (next_accessor == nullptr) { + return false; + } + const Assignment* const result_assignment = + heuristic_->BuildSolutionFromRoutes(next_accessor); + + if (result_assignment == nullptr) { + return false; + } + + bool has_change = false; + const std::vector& elements = + result_assignment->IntVarContainer().elements(); + for (int vehicle = 0; vehicle < model_.vehicles(); vehicle++) { + int64 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)); + + const int64 new_node_value = node_element.Value(); + DCHECK_NE(new_node_value, node_index); + + const int64 vehicle_var_index = VehicleVarIndex(node_index); + if (OldValue(node_index) != new_node_value || + (consider_vehicle_vars_ && OldValue(vehicle_var_index) != vehicle)) { + has_change = true; + SetValue(node_index, new_node_value); + if (consider_vehicle_vars_) { + SetValue(vehicle_var_index, vehicle); + } + } + node_index = new_node_value; + } + } + // Check for newly unperformed nodes among the ones removed for insertion by + // the heuristic. + for (int64 node : removed_nodes_.PositionsSetAtLeastOnce()) { + const IntVarElement& node_element = elements[node]; + DCHECK_EQ(node_element.Var(), model_.NextVar(node)); + if (node_element.Value() == node) { + DCHECK_NE(OldValue(node), node); + has_change = true; + SetValue(node, node); + if (consider_vehicle_vars_) { + const int64 vehicle_var_index = VehicleVarIndex(node); + DCHECK_NE(OldValue(vehicle_var_index), -1); + SetValue(vehicle_var_index, -1); + } + } + } + return has_change; +} + +// FilteredHeuristicPathLNSOperator + +FilteredHeuristicPathLNSOperator::FilteredHeuristicPathLNSOperator( + std::unique_ptr heuristic) + : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), + current_route_(0), + last_route_(0), + just_started_(false) {} + void FilteredHeuristicPathLNSOperator::OnStart() { // NOTE: We set last_route_ to current_route_ here to make sure all routes // are scanned in IncrementCurrentRouteToNextNonEmpty(). @@ -677,19 +757,7 @@ void FilteredHeuristicPathLNSOperator::OnStart() { just_started_ = true; } -bool FilteredHeuristicPathLNSOperator::MakeOneNeighbor() { - while (IncrementRoute()) { - // NOTE: No need to call RevertChanges() here as - // DestroyRouteAndReinsertNodes() will always return true if any change was - // made. - if (DestroyRouteAndReinsertNodes()) { - return true; - } - } - return false; -} - -bool FilteredHeuristicPathLNSOperator::IncrementRoute() { +bool FilteredHeuristicPathLNSOperator::IncrementPosition() { if (just_started_) { just_started_ = false; return !CurrentRouteIsEmpty(); @@ -713,89 +781,140 @@ void FilteredHeuristicPathLNSOperator::IncrementCurrentRouteToNextNonEmpty() { } while (CurrentRouteIsEmpty()); } -bool FilteredHeuristicPathLNSOperator::DestroyRouteAndReinsertNodes() { +std::function +FilteredHeuristicPathLNSOperator::SetupNextAccessorForNeighbor() { const int64 start_node = model_.Start(current_route_); const int64 end_node = model_.End(current_route_); - const Assignment* const result_assignment = - heuristic_->BuildSolutionFromRoutes( - [this, start_node, end_node](int64 node) { - if (node == start_node) return end_node; - return Value(node); - }); + int64 node = Value(start_node); + while (node != end_node) { + removed_nodes_.Set(node); + node = Value(node); + } - if (result_assignment == nullptr) { + return [this, start_node, end_node](int64 node) { + if (node == start_node) return end_node; + return Value(node); + }; +} + +// RelocatePathAndHeuristicInsertUnperformedOperator + +RelocatePathAndHeuristicInsertUnperformedOperator:: + RelocatePathAndHeuristicInsertUnperformedOperator( + std::unique_ptr heuristic) + : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), + route_to_relocate_index_(0), + empty_route_index_(0), + just_started_(false) {} + +void RelocatePathAndHeuristicInsertUnperformedOperator::OnStart() { + has_unperformed_nodes_ = false; + last_node_on_route_.resize(model_.vehicles()); + routes_to_relocate_.clear(); + empty_routes_.clear(); + std::vector empty_vehicle_of_vehicle_class_added( + model_.GetVehicleClassesCount(), false); + for (int64 node = 0; node < model_.Size(); node++) { + const int64 next = OldValue(node); + if (next == node) { + has_unperformed_nodes_ = true; + continue; + } + if (model_.IsEnd(next)) { + last_node_on_route_[model_.VehicleIndex(next)] = node; + } + } + + for (int vehicle = 0; vehicle < model_.vehicles(); vehicle++) { + const int64 next = OldValue(model_.Start(vehicle)); + if (!model_.IsEnd(next)) { + routes_to_relocate_.push_back(vehicle); + continue; + } + const int vehicle_class = + 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; + } + } + + if (empty_route_index_ >= empty_routes_.size()) { + empty_route_index_ = 0; + } + if (route_to_relocate_index_ >= routes_to_relocate_.size()) { + route_to_relocate_index_ = 0; + } + last_empty_route_index_ = empty_route_index_; + last_route_to_relocate_index_ = route_to_relocate_index_; + + just_started_ = true; +} + +bool RelocatePathAndHeuristicInsertUnperformedOperator::IncrementPosition() { + if (!has_unperformed_nodes_ || empty_routes_.empty() || + routes_to_relocate_.empty()) { return false; } - - bool has_change = false; - std::vector node_performed(model_.Size(), false); - const std::vector& elements = - result_assignment->IntVarContainer().elements(); - for (int vehicle = 0; vehicle < model_.vehicles(); vehicle++) { - int64 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)); - - const int64 new_node_value = node_element.Value(); - DCHECK_NE(new_node_value, node_index); - node_performed[node_index] = true; - - const int64 vehicle_var_index = VehicleVarIndex(node_index); - if (OldValue(node_index) != new_node_value || - (consider_vehicle_vars_ && OldValue(vehicle_var_index) != vehicle)) { - has_change = true; - SetValue(node_index, new_node_value); - if (consider_vehicle_vars_) { - SetValue(vehicle_var_index, vehicle); - } - } - - node_index = new_node_value; - } + if (just_started_) { + just_started_ = false; + return true; } - for (int64 node = 0; node < model_.Size(); node++) { - if (node_performed[node]) continue; - const IntVarElement& node_element = elements[node]; - DCHECK_EQ(node_element.Var(), model_.NextVar(node)); - DCHECK_EQ(node_element.Value(), node); - if (OldValue(node) != node) { - has_change = true; - SetValue(node, node); - if (consider_vehicle_vars_) { - const int64 vehicle_var_index = VehicleVarIndex(node); - DCHECK_NE(OldValue(vehicle_var_index), -1); - SetValue(vehicle_var_index, -1); - } - } + return IncrementRoutes(); +} + +bool RelocatePathAndHeuristicInsertUnperformedOperator::IncrementRoutes() { + ++empty_route_index_ %= empty_routes_.size(); + if (empty_route_index_ != last_empty_route_index_) { + return true; } - return has_change; + ++route_to_relocate_index_ %= routes_to_relocate_.size(); + return route_to_relocate_index_ != last_route_to_relocate_index_; +} + +std::function 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)) { + // Don't try to relocate the route to an empty vehicle of the same class. + return nullptr; + } + + const int64 empty_start_node = model_.Start(empty_route); + const int64 empty_end_node = model_.End(empty_route); + + const int64 relocated_route_start = model_.Start(relocated_route); + const int64 first_relocated_node = OldValue(relocated_route_start); + const int64 last_relocated_node = last_node_on_route_[relocated_route]; + const int64 relocated_route_end = model_.End(relocated_route); + + return [this, empty_start_node, empty_end_node, first_relocated_node, + last_relocated_node, relocated_route_start, + relocated_route_end](int64 node) { + if (node == relocated_route_start) return relocated_route_end; + if (node == empty_start_node) return first_relocated_node; + if (node == last_relocated_node) return empty_end_node; + return Value(node); + }; } // FilteredHeuristicCloseNodesLNSOperator FilteredHeuristicCloseNodesLNSOperator::FilteredHeuristicCloseNodesLNSOperator( std::unique_ptr heuristic, int num_close_nodes) - : IntVarLocalSearchOperator(heuristic->model()->Nexts(), - /*keep_inverse_values*/ true), - heuristic_(std::move(heuristic)), - model_(*heuristic_->model()), + : FilteredHeuristicLocalSearchOperator(std::move(heuristic), + /*keep_inverse_values*/ true), pickup_delivery_pairs_(model_.GetPickupAndDeliveryPairs()), - consider_vehicle_vars_(!model_.CostsAreHomogeneousAcrossVehicles()), current_node_(0), last_node_(0), close_nodes_(model_.Size()), - removed_nodes_(model_.Size()), new_nexts_(model_.Size()), changed_nexts_(model_.Size()), new_prevs_(model_.Size()), changed_prevs_(model_.Size()) { - if (consider_vehicle_vars_) { - AddVars(model_.VehicleVars()); - } const int64 size = model_.Size(); const int64 max_num_neighbors = std::max(0, size - 1 - model_.vehicles()); @@ -842,19 +961,7 @@ void FilteredHeuristicCloseNodesLNSOperator::OnStart() { just_started_ = true; } -bool FilteredHeuristicCloseNodesLNSOperator::MakeOneNeighbor() { - while (IncrementNode()) { - // NOTE: No need to call RevertChanges() here as - // RemoveCloseNodesAndReinsert() will always return true if any change was - // made. - if (RemoveCloseNodesAndReinsert()) { - return true; - } - } - return false; -} - -bool FilteredHeuristicCloseNodesLNSOperator::IncrementNode() { +bool FilteredHeuristicCloseNodesLNSOperator::IncrementPosition() { if (just_started_) { just_started_ = false; return true; @@ -919,13 +1026,13 @@ std::vector FilteredHeuristicCloseNodesLNSOperator::GetActiveSiblings( return active_siblings; } -bool FilteredHeuristicCloseNodesLNSOperator::RemoveCloseNodesAndReinsert() { +std::function +FilteredHeuristicCloseNodesLNSOperator::SetupNextAccessorForNeighbor() { if (model_.IsStart(current_node_)) { - return false; + return nullptr; } DCHECK(!model_.IsEnd(current_node_)); - removed_nodes_.SparseClearAll(); changed_nexts_.SparseClearAll(); changed_prevs_.SparseClearAll(); @@ -935,57 +1042,7 @@ bool FilteredHeuristicCloseNodesLNSOperator::RemoveCloseNodesAndReinsert() { RemoveNodeAndActiveSibling(neighbor); } - const Assignment* const result_assignment = - heuristic_->BuildSolutionFromRoutes( - [this](int64 node) { return Next(node); }); - - if (result_assignment == nullptr) { - return false; - } - - bool has_change = false; - const std::vector& elements = - result_assignment->IntVarContainer().elements(); - for (int vehicle = 0; vehicle < model_.vehicles(); vehicle++) { - int64 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)); - - const int64 new_node_value = node_element.Value(); - DCHECK_NE(new_node_value, node_index); - - const int64 vehicle_var_index = VehicleVarIndex(node_index); - if (OldValue(node_index) != new_node_value || - (consider_vehicle_vars_ && OldValue(vehicle_var_index) != vehicle)) { - has_change = true; - SetValue(node_index, new_node_value); - if (consider_vehicle_vars_) { - SetValue(vehicle_var_index, vehicle); - } - } - node_index = new_node_value; - } - } - // Check for newly unperformed nodes among the ones removed for insertion by - // the heuristic. - for (int64 node : removed_nodes_.PositionsSetAtLeastOnce()) { - const IntVarElement& node_element = elements[node]; - DCHECK_EQ(node_element.Var(), model_.NextVar(node)); - if (node_element.Value() == node) { - DCHECK_NE(OldValue(node), node); - has_change = true; - SetValue(node, node); - if (consider_vehicle_vars_) { - const int64 vehicle_var_index = VehicleVarIndex(node); - DCHECK_NE(OldValue(vehicle_var_index), -1); - SetValue(vehicle_var_index, -1); - } - } - } - return has_change; + return [this](int64 node) { return Next(node); }; } // FilteredHeuristicExpensiveChainLNSOperator @@ -995,10 +1052,7 @@ FilteredHeuristicExpensiveChainLNSOperator:: std::unique_ptr heuristic, int num_arcs_to_consider, std::function arc_cost_for_route_start) - : IntVarLocalSearchOperator(heuristic->model()->Nexts()), - heuristic_(std::move(heuristic)), - model_(*heuristic_->model()), - consider_vehicle_vars_(!model_.CostsAreHomogeneousAcrossVehicles()), + : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), current_route_(0), last_route_(0), num_arcs_to_consider_(num_arcs_to_consider), @@ -1006,9 +1060,6 @@ FilteredHeuristicExpensiveChainLNSOperator:: arc_cost_for_route_start_(std::move(arc_cost_for_route_start)), just_started_(false) { DCHECK_GE(num_arcs_to_consider_, 2); - if (consider_vehicle_vars_) { - AddVars(model_.VehicleVars()); - } } void FilteredHeuristicExpensiveChainLNSOperator::OnStart() { @@ -1016,18 +1067,6 @@ void FilteredHeuristicExpensiveChainLNSOperator::OnStart() { just_started_ = true; } -bool FilteredHeuristicExpensiveChainLNSOperator::MakeOneNeighbor() { - while (IncrementPosition()) { - // NOTE: No need to call RevertChanges() here as - // DestroyChainAndReinsertNodes() will always return true if any change was - // made. - if (DestroyChainAndReinsertNodes()) { - return true; - } - } - return false; -} - bool FilteredHeuristicExpensiveChainLNSOperator::IncrementPosition() { if (just_started_) { just_started_ = false; @@ -1039,8 +1078,8 @@ bool FilteredHeuristicExpensiveChainLNSOperator::IncrementPosition() { return IncrementRoute() && FindMostExpensiveChainsOnRemainingRoutes(); } -bool FilteredHeuristicExpensiveChainLNSOperator:: - DestroyChainAndReinsertNodes() { +std::function +FilteredHeuristicExpensiveChainLNSOperator::SetupNextAccessorForNeighbor() { const int first_arc_index = current_expensive_arc_indices_.first; const int second_arc_index = current_expensive_arc_indices_.second; DCHECK_LE(0, first_arc_index); @@ -1060,62 +1099,16 @@ bool FilteredHeuristicExpensiveChainLNSOperator:: after_chain = OldValue(first_start_and_rank.first); } - const Assignment* const result_assignment = - heuristic_->BuildSolutionFromRoutes( - [this, before_chain, after_chain](int64 node) { - if (node == before_chain) return after_chain; - return OldValue(node); - }); - - if (result_assignment == nullptr) { - return false; + int node = Value(before_chain); + while (node != after_chain) { + removed_nodes_.Set(node); + node = Value(node); } - bool has_change = false; - std::vector node_performed(model_.Size(), false); - const std::vector& elements = - result_assignment->IntVarContainer().elements(); - for (int vehicle = 0; vehicle < model_.vehicles(); vehicle++) { - int64 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)); - - const int64 new_node_value = node_element.Value(); - DCHECK_NE(new_node_value, node_index); - node_performed[node_index] = true; - - const int64 vehicle_var_index = VehicleVarIndex(node_index); - if (OldValue(node_index) != new_node_value || - (consider_vehicle_vars_ && OldValue(vehicle_var_index) != vehicle)) { - has_change = true; - SetValue(node_index, new_node_value); - if (consider_vehicle_vars_) { - SetValue(vehicle_var_index, vehicle); - } - } - - node_index = new_node_value; - } - } - for (int64 node = 0; node < model_.Size(); node++) { - if (node_performed[node]) continue; - const IntVarElement& node_element = elements[node]; - DCHECK_EQ(node_element.Var(), model_.NextVar(node)); - DCHECK_EQ(node_element.Value(), node); - if (OldValue(node) != node) { - has_change = true; - SetValue(node, node); - if (consider_vehicle_vars_) { - const int64 vehicle_var_index = VehicleVarIndex(node); - DCHECK_NE(OldValue(vehicle_var_index), -1); - SetValue(vehicle_var_index, -1); - } - } - } - return has_change; + return [this, before_chain, after_chain](int64 node) { + if (node == before_chain) return after_chain; + return OldValue(node); + }; } bool FilteredHeuristicExpensiveChainLNSOperator::IncrementRoute() { diff --git a/ortools/constraint_solver/routing_neighborhoods.h b/ortools/constraint_solver/routing_neighborhoods.h index 3658fefe7d..6518757932 100644 --- a/ortools/constraint_solver/routing_neighborhoods.h +++ b/ortools/constraint_solver/routing_neighborhoods.h @@ -339,51 +339,120 @@ class IndexPairSwapActiveOperator : public PathOperator { int inactive_node_; }; +/// Class of operators using a RoutingFilteredHeuristic to insert unperformed +/// nodes after changes have been made to the current solution. +// TODO(user): Put these methods in an object with helper methods instead +// of adding a layer to the class hierarchy. +class FilteredHeuristicLocalSearchOperator : public IntVarLocalSearchOperator { + public: + explicit FilteredHeuristicLocalSearchOperator( + std::unique_ptr heuristic, + bool keep_inverse_values = false); + ~FilteredHeuristicLocalSearchOperator() override {} + + protected: + virtual bool IncrementPosition() = 0; + /// Virtual method to return the next_accessor to be passed to the heuristic + /// to build a new solution. This method should also correctly set the + /// nodes being removed (if any) in removed_nodes_. + virtual std::function SetupNextAccessorForNeighbor() = 0; + + std::string HeuristicName() const { + std::string heuristic_name = heuristic_->DebugString(); + const int erase_pos = heuristic_name.find("FilteredHeuristic"); + if (erase_pos != std::string::npos) { + const int expected_name_size = heuristic_name.size() - 17; + heuristic_name.erase(erase_pos); + // NOTE: Verify that the "FilteredHeuristic" string was at the end of the + // heuristic name. + DCHECK_EQ(heuristic_name.size(), expected_name_size); + } + return heuristic_name; + } + + // 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_; + /// Keeps track of removed nodes when making a neighbor. + SparseBitset<> removed_nodes_; + + private: + bool MakeOneNeighbor() override; + bool MakeChangesAndInsertNodes(); + + int64 VehicleVarIndex(int64 node) const { return model_.Size() + node; } + + const std::unique_ptr heuristic_; + const bool consider_vehicle_vars_; +}; + /// LNS-like operator based on a filtered first solution heuristic to rebuild /// the solution, after the destruction phase consisting of removing one route. -class FilteredHeuristicPathLNSOperator : public IntVarLocalSearchOperator { +class FilteredHeuristicPathLNSOperator + : public FilteredHeuristicLocalSearchOperator { public: explicit FilteredHeuristicPathLNSOperator( std::unique_ptr heuristic); ~FilteredHeuristicPathLNSOperator() override {} std::string DebugString() const override { - std::string heuristic_name = heuristic_->DebugString(); - const int erase_pos = heuristic_name.find("FilteredHeuristic"); - if (erase_pos != std::string::npos) { - heuristic_name.erase(erase_pos); - } - return absl::StrCat("HeuristicPathLNS(", heuristic_name, ")"); + return absl::StrCat("HeuristicPathLNS(", HeuristicName(), ")"); } private: void OnStart() override; - bool MakeOneNeighbor() override; - bool IncrementRoute(); + bool IncrementPosition() override; bool CurrentRouteIsEmpty() const; void IncrementCurrentRouteToNextNonEmpty(); - bool DestroyRouteAndReinsertNodes(); + std::function SetupNextAccessorForNeighbor() override; - int64 VehicleVarIndex(int64 node) const { return model_.Size() + node; } - - // 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 std::unique_ptr heuristic_; - const RoutingModel& model_; - const bool consider_vehicle_vars_; int current_route_; int last_route_; bool just_started_; }; -/// Similar to the move above, but instead of removing one route entirely, the -/// destruction phase consists of removing all nodes on an "expensive" chain -/// from a route. +/// Heuristic-based local search operator which relocates an entire route to +/// an empty vehicle of different vehicle class and then tries to insert +/// unperformed nodes using the heuristic. +class RelocatePathAndHeuristicInsertUnperformedOperator + : public FilteredHeuristicLocalSearchOperator { + public: + explicit RelocatePathAndHeuristicInsertUnperformedOperator( + std::unique_ptr heuristic); + ~RelocatePathAndHeuristicInsertUnperformedOperator() override {} + + std::string DebugString() const override { + return absl::StrCat("RelocatePathAndHeuristicInsertUnperformed(", + HeuristicName(), ")"); + } + + private: + void OnStart() override; + + bool IncrementPosition() override; + bool IncrementRoutes(); + + std::function SetupNextAccessorForNeighbor() override; + + int route_to_relocate_index_; + int last_route_to_relocate_index_; + int empty_route_index_; + int last_empty_route_index_; + std::vector routes_to_relocate_; + std::vector empty_routes_; + std::vector last_node_on_route_; + bool has_unperformed_nodes_; + bool just_started_; +}; + +/// Similar to the heuristic path LNS above, but instead of removing one route +/// entirely, the destruction phase consists of removing all nodes on an +/// "expensive" chain from a route. class FilteredHeuristicExpensiveChainLNSOperator - : public IntVarLocalSearchOperator { + : public FilteredHeuristicLocalSearchOperator { public: FilteredHeuristicExpensiveChainLNSOperator( std::unique_ptr heuristic, @@ -392,30 +461,19 @@ class FilteredHeuristicExpensiveChainLNSOperator ~FilteredHeuristicExpensiveChainLNSOperator() override {} std::string DebugString() const override { - std::string heuristic_name = heuristic_->DebugString(); - const int erase_pos = heuristic_name.find("FilteredHeuristic"); - if (erase_pos != std::string::npos) { - heuristic_name.erase(erase_pos); - } - return absl::StrCat("HeuristicExpensiveChainLNS(", heuristic_name, ")"); + return absl::StrCat("HeuristicExpensiveChainLNS(", HeuristicName(), ")"); } private: void OnStart() override; - bool MakeOneNeighbor() override; - bool IncrementPosition(); + bool IncrementPosition() override; bool IncrementRoute(); bool IncrementCurrentArcIndices(); bool FindMostExpensiveChainsOnRemainingRoutes(); - bool DestroyChainAndReinsertNodes(); + std::function SetupNextAccessorForNeighbor() override; - int64 VehicleVarIndex(int64 node) const { return model_.Size() + node; } - - const std::unique_ptr heuristic_; - const RoutingModel& model_; - const bool consider_vehicle_vars_; int current_route_; int last_route_; @@ -432,41 +490,30 @@ class FilteredHeuristicExpensiveChainLNSOperator bool just_started_; }; -// Filtered heuristic LNS operator, where the destruction phase consists of -// removing a node and the 'num_close_nodes' nodes closest to it, along with -// each of their corresponding sibling pickup/deliveries that are performed. -// TODO(user): Factor out MakeOneNeighbor() and the common parts of the -// destruction/reconstruction methods in a parent class for the three heuristic -// LNS operators. +/// Filtered heuristic LNS operator, where the destruction phase consists of +/// removing a node and the 'num_close_nodes' nodes closest to it, along with +/// each of their corresponding sibling pickup/deliveries that are performed. class FilteredHeuristicCloseNodesLNSOperator - : public IntVarLocalSearchOperator { + : public FilteredHeuristicLocalSearchOperator { public: FilteredHeuristicCloseNodesLNSOperator( std::unique_ptr heuristic, int num_close_nodes); ~FilteredHeuristicCloseNodesLNSOperator() override {} std::string DebugString() const override { - std::string heuristic_name = heuristic_->DebugString(); - const int erase_pos = heuristic_name.find("FilteredHeuristic"); - if (erase_pos != std::string::npos) { - heuristic_name.erase(erase_pos); - } - return absl::StrCat("HeuristicCloseNodesLNS(", heuristic_name, ")"); + return absl::StrCat("HeuristicCloseNodesLNS(", HeuristicName(), ")"); } private: void OnStart() override; - bool MakeOneNeighbor() override; - bool IncrementNode(); + bool IncrementPosition() override; - bool RemoveCloseNodesAndReinsert(); + std::function SetupNextAccessorForNeighbor() override; void RemoveNode(int64 node); void RemoveNodeAndActiveSibling(int64 node); - int64 VehicleVarIndex(int64 node) const { return model_.Size() + node; } - bool IsActive(int64 node) const { DCHECK_LT(node, model_.Size()); return Value(node) != node && !removed_nodes_[node]; @@ -484,19 +531,15 @@ class FilteredHeuristicCloseNodesLNSOperator std::vector GetActiveSiblings(int64 node) const; - const std::unique_ptr heuristic_; - const RoutingModel& model_; const std::vector, std::vector>>& pickup_delivery_pairs_; - const bool consider_vehicle_vars_; int current_node_; int last_node_; bool just_started_; std::vector> close_nodes_; - // Keep track of changes when making a neighbor. - SparseBitset<> removed_nodes_; + /// Keep track of changes when making a neighbor. std::vector new_nexts_; SparseBitset<> changed_nexts_; std::vector new_prevs_; diff --git a/ortools/constraint_solver/routing_parameters.cc b/ortools/constraint_solver/routing_parameters.cc index 0b9763878f..f8efe139fb 100644 --- a/ortools/constraint_solver/routing_parameters.cc +++ b/ortools/constraint_solver/routing_parameters.cc @@ -21,7 +21,6 @@ #include "google/protobuf/text_format.h" #include "ortools/base/logging.h" #include "ortools/base/protoutil.h" -#include "ortools/base/statusor.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/routing_enums.pb.h" #include "ortools/constraint_solver/solver_parameters.pb.h" @@ -54,6 +53,7 @@ RoutingSearchParameters DefaultRoutingSearchParameters() { "cheapest_insertion_farthest_seeds_ratio: 0 " "cheapest_insertion_first_solution_neighbors_ratio: 1 " "cheapest_insertion_ls_operator_neighbors_ratio: 1 " + "cheapest_insertion_add_unperformed_entries: false " "local_search_operators {" " use_relocate: BOOL_TRUE" " use_relocate_pair: BOOL_TRUE" @@ -83,6 +83,8 @@ RoutingSearchParameters DefaultRoutingSearchParameters() { " use_inactive_lns: BOOL_FALSE" " use_global_cheapest_insertion_path_lns: BOOL_TRUE" " use_local_cheapest_insertion_path_lns: BOOL_TRUE" + " use_relocate_path_global_cheapest_insertion_insert_unperformed: " + "BOOL_TRUE" " use_global_cheapest_insertion_expensive_chain_lns: BOOL_FALSE" " use_local_cheapest_insertion_expensive_chain_lns: BOOL_FALSE" " use_global_cheapest_insertion_close_nodes_lns: BOOL_FALSE" @@ -296,6 +298,30 @@ std::string FindErrorInRoutingSearchParameters( mixed_integer_scheduling_solver)); } + if (search_parameters.has_improvement_limit_parameters()) { + const double improvement_rate_coefficient = + search_parameters.improvement_limit_parameters() + .improvement_rate_coefficient(); + if (std::isnan(improvement_rate_coefficient) || + improvement_rate_coefficient <= 0) { + return StrCat( + "Invalid value for " + "improvement_limit_parameters.improvement_rate_coefficient: ", + improvement_rate_coefficient); + } + + const int32 improvement_rate_solutions_distance = + search_parameters.improvement_limit_parameters() + .improvement_rate_solutions_distance(); + if (std::isnan(improvement_rate_solutions_distance) || + improvement_rate_solutions_distance <= 0) { + return StrCat( + "Invalid value for " + "improvement_limit_parameters.improvement_rate_solutions_distance: ", + improvement_rate_solutions_distance); + } + } + return ""; // = Valid (No error). } diff --git a/ortools/constraint_solver/routing_parameters.proto b/ortools/constraint_solver/routing_parameters.proto index 0803618790..90f33270d2 100644 --- a/ortools/constraint_solver/routing_parameters.proto +++ b/ortools/constraint_solver/routing_parameters.proto @@ -34,7 +34,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: 37 +// Next ID: 41 message RoutingSearchParameters { // First solution strategies, used as starting point of local search. FirstSolutionStrategy.Value first_solution_strategy = 1; @@ -84,12 +84,16 @@ message RoutingSearchParameters { // local_search_operators.use_global_cheapest_insertion_chain_lns below). double cheapest_insertion_ls_operator_neighbors_ratio = 31; + // Whether or not to consider entries making the nodes/pairs unperformed in + // the GlobalCheapestInsertion heuristic. + bool cheapest_insertion_add_unperformed_entries = 40; + // If true use minimum matching instead of minimal matching in the // Christofides algorithm. bool christofides_use_minimum_matching = 30; // Local search neighborhood operators used to build a solutions neighborhood. - // Next ID: 33 + // Next ID: 34 message LocalSearchNeighborhoodOperators { // --- Inter-route operators --- // Operator which moves a single node to another position. @@ -323,6 +327,11 @@ message RoutingSearchParameters { OptionalBoolean use_global_cheapest_insertion_path_lns = 27; // Same as above but using LocalCheapestInsertion as a heuristic. OptionalBoolean use_local_cheapest_insertion_path_lns = 28; + // The following operator relocates an entire route to an empty path and + // then tries to insert the unperformed nodes using the global cheapest + // insertion heuristic. + OptionalBoolean + use_relocate_path_global_cheapest_insertion_insert_unperformed = 33; // This operator finds heuristic_expensive_chain_lns_num_arcs_to_consider // most expensive arcs on a route, makes the nodes in between pairs of these // expensive arcs unperformed, and reinserts them using the @@ -407,6 +416,23 @@ message RoutingSearchParameters { // neighbor. google.protobuf.Duration lns_time_limit = 10; + // Parameters required for the improvement search limit. + message ImprovementSearchLimitParameters { + // Parameter that regulates exchange rate between objective improvement and + // number of neighbors spent. The smaller the value, the sooner the limit + // stops the search. Must be positive. + double improvement_rate_coefficient = 38; + // Parameter that specifies the distance between improvements taken into + // consideration for calculating the improvement rate. + // Example: For 5 objective improvements = (10, 8, 6, 4, 2), and the + // solutions_distance parameter of 2, then the improvement_rate will be + // computed for (10, 6), (8, 4), and (6, 2). + int32 improvement_rate_solutions_distance = 39; + } + // The improvement search limit is added to the solver if the following + // parameters are set. + ImprovementSearchLimitParameters improvement_limit_parameters = 37; + // --- Propagation control --- // These are advanced settings which should not be modified unless you know // what you are doing. diff --git a/ortools/constraint_solver/routing_search.cc b/ortools/constraint_solver/routing_search.cc index b7c5a1cf69..7754183fc3 100644 --- a/ortools/constraint_solver/routing_search.cc +++ b/ortools/constraint_solver/routing_search.cc @@ -293,7 +293,7 @@ BasePathFilter::BasePathFilter(const std::vector& nexts, new_synchronized_unperformed_nodes_(nexts.size()), new_nexts_(nexts.size(), kUnassigned), touched_paths_(nexts.size()), - touched_path_nodes_(next_domain_size), + touched_path_chain_start_ends_(nexts.size(), {kUnassigned, kUnassigned}), ranks_(next_domain_size, -1), status_(BasePathFilter::UNKNOWN) {} @@ -308,11 +308,33 @@ bool BasePathFilter::Accept(const Assignment* delta, const Assignment::IntContainer& container = delta->IntVarContainer(); const int delta_size = container.Size(); delta_touched_.reserve(delta_size); - // Determining touched paths and touched nodes (a node is touched if it - // corresponds to an element of delta or that an element of delta points to - // it. + // Determining touched paths and their touched chain start and ends (a node is + // touched if it corresponds to an element of delta or that an element of + // delta points to it). + // The start and end of a touched path subchain will have remained on the same + // path and will correspond to the min and max ranks of touched nodes in the + // current assignment. + for (int64 touched_path : touched_paths_.PositionsSetAtLeastOnce()) { + touched_path_chain_start_ends_[touched_path] = {kUnassigned, kUnassigned}; + } touched_paths_.SparseClearAll(); - touched_path_nodes_.SparseClearAll(); + + const auto update_touched_path_chain_start_end = [this](int64 index) { + const int64 start = node_path_starts_[index]; + if (start == kUnassigned) return; + touched_paths_.Set(start); + + int64& chain_start = touched_path_chain_start_ends_[start].first; + if (chain_start == kUnassigned || ranks_[index] < ranks_[chain_start]) { + chain_start = index; + } + + int64& chain_end = touched_path_chain_start_ends_[start].second; + if (chain_end == kUnassigned || ranks_[index] > ranks_[chain_end]) { + chain_end = index; + } + }; + for (int i = 0; i < delta_size; ++i) { const IntVarElement& new_element = container.Element(i); IntVar* const var = new_element.Var(); @@ -324,44 +346,17 @@ bool BasePathFilter::Accept(const Assignment* delta, } new_nexts_[index] = new_element.Value(); delta_touched_.push_back(index); - const int64 start = node_path_starts_[index]; - touched_path_nodes_.Set(index); - touched_path_nodes_.Set(new_nexts_[index]); - if (start != kUnassigned) { - touched_paths_.Set(start); - } + update_touched_path_chain_start_end(index); + update_touched_path_chain_start_end(new_nexts_[index]); } } // Checking feasibility of touched paths. InitializeAcceptPath(); bool accept = true; - // Finding touched subchains from ranks of touched nodes in paths; the first - // and last node of a subchain will have remained on the same path and will - // correspond to the min and max ranks of touched nodes in the current - // assignment. for (const int64 touched_start : touched_paths_.PositionsSetAtLeastOnce()) { - int min_rank = kint32max; - int64 start = kUnassigned; - int max_rank = kint32min; - int64 end = kUnassigned; - // Linear search on touched nodes is ok since there shouldn't be many of - // them. - // TODO(user): Remove the linear loop. - for (const int64 touched_path_node : - touched_path_nodes_.PositionsSetAtLeastOnce()) { - if (node_path_starts_[touched_path_node] == touched_start) { - const int rank = ranks_[touched_path_node]; - if (rank < min_rank) { - min_rank = rank; - start = touched_path_node; - } - if (rank > max_rank) { - max_rank = rank; - end = touched_path_node; - } - } - } - if (!AcceptPath(touched_start, start, end)) { + const std::pair start_end = + touched_path_chain_start_ends_[touched_start]; + if (!AcceptPath(touched_start, start_end.first, start_end.second)) { accept = false; break; } @@ -1350,7 +1345,7 @@ PathCumulFilter::PathCumulFilter(const RoutingModel& routing_model, // null because the finalizer is using a global optimizer, so we create a // separate optimizer for the PathCumulFilter if we need it. if (can_use_lp_ && optimizer_ == nullptr) { - // DCHECK_EQ(mp_optimizer_, nullptr); + DCHECK_EQ(mp_optimizer_, nullptr); for (int vehicle = 0; vehicle < routing_model.vehicles(); vehicle++) { if (!FilterWithDimensionCumulOptimizerForVehicle(vehicle)) { continue; @@ -1531,7 +1526,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { int64 lp_cumul_cost_value = 0; LocalDimensionCumulOptimizer* const optimizer = FilterBreakCost(vehicle) ? mp_optimizer_ : optimizer_; - // DCHECK_NE(optimizer, nullptr); + DCHECK_NE(optimizer, nullptr); const DimensionSchedulingStatus status = optimizer->ComputeRouteCumulCostWithoutFixedTransits( vehicle, [this](int64 node) { return Value(node); }, @@ -1541,7 +1536,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { lp_cumul_cost_value = 0; break; case DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY: - // DCHECK_NE(mp_optimizer_, nullptr); + DCHECK_NE(mp_optimizer_, nullptr); if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( vehicle, [this](int64 node) { return Value(node); }, &lp_cumul_cost_value) == @@ -1661,36 +1656,47 @@ bool PathCumulFilter::AcceptPath(int64 path_start, int64 chain_start, } if (FilterSlackCost() || FilterBreakCost(vehicle) || FilterSoftSpanCost(vehicle) || FilterSoftSpanQuadraticCost(vehicle)) { + int64 slack_max = kint64max; + if (vehicle_span_upper_bounds_[vehicle] < kint64max) { + const int64 span_max = vehicle_span_upper_bounds_[vehicle]; + slack_max = std::min(slack_max, CapSub(span_max, total_transit)); + } const int64 max_start_from_min_end = ComputePathMaxStartFromEndCumul( delta_path_transits_, path, path_start, min_end); - int64 min_total_slack = - CapSub(CapSub(min_end, max_start_from_min_end), total_transit); - if (FilterBreakCost(vehicle)) { - // Copy path of vehicle. - { - current_path_.clear(); - int64 node = path_start; - while (node < Size()) { - current_path_.push_back(node); - node = GetNext(node); + const int64 span_lb = CapSub(min_end, max_start_from_min_end); + int64 min_total_slack = CapSub(span_lb, total_transit); + if (min_total_slack > slack_max) return false; + + if (dimension_.HasBreakConstraints()) { + for (const auto [limit, min_break_duration] : + dimension_.GetBreakDistanceDurationOfVehicle(vehicle)) { + // Minimal number of breaks depends on total transit: + // 0 breaks for 0 <= total transit <= limit, + // 1 break for limit + 1 <= total transit <= 2 * limit, + // i breaks for i * limit + 1 <= total transit <= (i+1) * limit, ... + if (limit == 0 || total_transit == 0) continue; + const int num_breaks_lb = (total_transit - 1) / limit; + const int64 slack_lb = CapProd(num_breaks_lb, min_break_duration); + if (slack_lb > slack_max) return false; + min_total_slack = std::max(min_total_slack, slack_lb); + } + // Compute a lower bound of the amount of break that must be made inside + // the route. We compute a mandatory interval (might be empty) + // [max_start, min_end[ during which the route will have to happen, + // then the duration of break that must happen during this interval. + int64 min_total_break = 0; + int64 max_path_end = cumuls_[routing_model_.End(vehicle)]->Max(); + const int64 max_start = ComputePathMaxStartFromEndCumul( + delta_path_transits_, path, path_start, max_path_end); + for (const IntervalVar* br : + dimension_.GetBreakIntervalsOfVehicle(vehicle)) { + if (!br->MustBePerformed()) continue; + if (max_start < br->EndMin() && br->StartMax() < min_end) { + min_total_break = CapAdd(min_total_break, br->DurationMin()); } - current_path_.push_back(node); } - FillTravelBoundsOfVehicle(vehicle, current_path_, dimension_, - &travel_bounds_); - tasks_.Clear(); - AppendTasksFromPath(current_path_, travel_bounds_, dimension_, &tasks_); - tasks_.num_chain_tasks = tasks_.start_min.size(); - AppendTasksFromIntervals(dimension_.GetBreakIntervalsOfVehicle(vehicle), - &tasks_); - tasks_.distance_duration = - dimension_.GetBreakDistanceDurationOfVehicle(vehicle); - if (!disjunctive_propagator_.Precedences(&tasks_) || - !disjunctive_propagator_.ChainSpanMin(&tasks_)) { - return false; - } - min_total_slack = - std::max(min_total_slack, CapSub(tasks_.span_min, total_transit)); + if (min_total_break > slack_max) return false; + min_total_slack = std::max(min_total_slack, min_total_break); } if (filter_vehicle_costs) { cumul_cost_delta = CapAdd( @@ -2109,10 +2115,73 @@ bool DimensionHasCumulConstraint(const RoutingDimension& dimension) { } // namespace +void AppendLightWeightDimensionFilters( + const PathState* path_state, + const std::vector& dimensions, + std::vector* filters) { + // For every dimension that fits, add a UnaryDimensionChecker. + for (const RoutingDimension* dimension : dimensions) { + // Skip dimension if not unary. + if (dimension->GetUnaryTransitEvaluator(0) == nullptr) continue; + + using Intervals = std::vector; + // Fill path capacities and classes. + const int num_vehicles = dimension->model()->vehicles(); + Intervals path_capacity(num_vehicles); + std::vector path_class(num_vehicles); + for (int v = 0; v < num_vehicles; ++v) { + const auto& vehicle_capacities = dimension->vehicle_capacities(); + path_capacity[v] = {0, vehicle_capacities[v]}; + path_class[v] = dimension->vehicle_to_class(v); + } + // For each class, retrieve the demands of each node. + // Dimension store evaluators with a double indirection for compacity: + // vehicle -> vehicle_class -> evaluator_index. + // We replicate this in UnaryDimensionChecker, + // except we expand evaluator_index to an array of values for all nodes. + const int num_vehicle_classes = + 1 + *std::max_element(path_class.begin(), path_class.end()); + std::vector demands(num_vehicle_classes); + const int num_cumuls = dimension->cumuls().size(); + const int num_slacks = dimension->slacks().size(); + for (int vehicle = 0; vehicle < num_vehicles; ++vehicle) { + const int vehicle_class = path_class[vehicle]; + if (!demands[vehicle_class].empty()) continue; + const auto& evaluator = dimension->GetUnaryTransitEvaluator(vehicle); + Intervals class_demands(num_cumuls); + for (int node = 0; node < num_cumuls; ++node) { + if (node < num_slacks) { + const int64 demand_min = evaluator(node); + const int64 slack_max = dimension->SlackVar(node)->Max(); + class_demands[node] = {demand_min, CapAdd(demand_min, slack_max)}; + } else { + class_demands[node] = {0, 0}; + } + } + demands[vehicle_class] = std::move(class_demands); + } + // Fill node capacities. + Intervals node_capacity(num_cumuls); + for (int node = 0; node < num_cumuls; ++node) { + const IntVar* cumul = dimension->CumulVar(node); + node_capacity[node] = {cumul->Min(), cumul->Max()}; + } + // Make the dimension checker and pass ownership to the filter. + auto checker = absl::make_unique( + path_state, std::move(path_capacity), std::move(path_class), + std::move(demands), std::move(node_capacity)); + const auto kAccept = LocalSearchFilterManager::FilterEventType::kAccept; + LocalSearchFilter* filter = MakeUnaryDimensionFilter( + dimension->model()->solver(), std::move(checker)); + filters->push_back({filter, kAccept}); + } +} + void AppendDimensionCumulFilters( const std::vector& dimensions, const RoutingSearchParameters& parameters, bool filter_objective_cost, - std::vector* filters) { + std::vector* filters) { + const auto kAccept = LocalSearchFilterManager::FilterEventType::kAccept; // NOTE: We first sort the dimensions by increasing complexity of filtering: // - Dimensions without any cumul-related costs or constraints will have a // ChainCumulFilter. @@ -2165,20 +2234,24 @@ void AppendDimensionCumulFilters( // already doing it. const bool use_global_lp = use_global_lp_filter[d]; if (use_path_cumul_filter[d]) { - filters->push_back(MakePathCumulFilter( - dimension, parameters, !use_global_lp, filter_objective_cost)); + filters->push_back( + {MakePathCumulFilter(dimension, parameters, !use_global_lp, + filter_objective_cost), + kAccept}); } else { filters->push_back( - model.solver()->RevAlloc(new ChainCumulFilter(model, dimension))); + {model.solver()->RevAlloc(new ChainCumulFilter(model, dimension)), + kAccept}); } if (use_global_lp) { DCHECK(model.GetMutableGlobalCumulOptimizer(dimension) != nullptr); - filters->push_back(MakeGlobalLPCumulFilter( - model.GetMutableGlobalCumulOptimizer(dimension), - filter_objective_cost)); + filters->push_back({MakeGlobalLPCumulFilter( + model.GetMutableGlobalCumulOptimizer(dimension), + filter_objective_cost), + kAccept}); } else if (use_cumul_bounds_propagator_filter[d]) { - filters->push_back(MakeCumulBoundsPropagatorFilter(dimension)); + filters->push_back({MakeCumulBoundsPropagatorFilter(dimension), kAccept}); } } } @@ -2581,8 +2654,13 @@ int64 LPCumulFilter::GetAcceptedObjectiveValue() const { 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](int64 index) { return Value(index); }, + [this, &model](int64 index) { + return IsVarSynced(index) ? Value(index) + : model.IsStart(index) ? model.End(model.VehicleIndex(index)) + : index; + }, &synchronized_cost_without_transit_)) { // TODO(user): This should only happen if the LP solver times out. // DCHECK the fail wasn't due to an infeasible model. @@ -2605,13 +2683,15 @@ IntVarLocalSearchFilter* MakeGlobalLPCumulFilter( const int64 CPFeasibilityFilter::kUnassigned = -1; -CPFeasibilityFilter::CPFeasibilityFilter(const RoutingModel* routing_model) +CPFeasibilityFilter::CPFeasibilityFilter(RoutingModel* routing_model) : IntVarLocalSearchFilter(routing_model->Nexts()), model_(routing_model), solver_(routing_model->solver()), assignment_(solver_->MakeAssignment()), temp_assignment_(solver_->MakeAssignment()), - restore_(solver_->MakeRestoreAssignment(temp_assignment_)) { + restore_(solver_->MakeRestoreAssignment(temp_assignment_)), + limit_(solver_->MakeCustomLimit( + [routing_model]() { return routing_model->CheckLimit(); })) { assignment_->Add(routing_model->Nexts()); } @@ -2620,7 +2700,8 @@ bool CPFeasibilityFilter::Accept(const Assignment* delta, int64 objective_min, int64 objective_max) { temp_assignment_->Copy(assignment_); AddDeltaToAssignment(delta, temp_assignment_); - return solver_->Solve(restore_); + + return solver_->Solve(restore_, limit_); } void CPFeasibilityFilter::OnSynchronize(const Assignment* delta) { @@ -2658,8 +2739,7 @@ void CPFeasibilityFilter::AddDeltaToAssignment(const Assignment* delta, } } -IntVarLocalSearchFilter* MakeCPFeasibilityFilter( - const RoutingModel* routing_model) { +IntVarLocalSearchFilter* MakeCPFeasibilityFilter(RoutingModel* routing_model) { return routing_model->solver()->RevAlloc( new CPFeasibilityFilter(routing_model)); } @@ -2793,9 +2873,10 @@ const Assignment* RoutingFilteredHeuristic::BuildSolutionFromRoutes( SetVehicleIndex(node, v); node = next; } - // All vehicles have full routes from start to end here. - start_chain_ends_[v] = model()->End(v); - end_chain_starts_[v] = model()->Start(v); + // We relax all routes from start to end, so routes can now be extended + // by inserting nodes between the start and end. + start_chain_ends_[v] = model()->Start(v); + end_chain_starts_[v] = model()->End(v); } if (!Commit()) { return nullptr; @@ -3337,9 +3418,12 @@ bool GlobalCheapestInsertionFilteredHeuristic::CheckVehicleIndices() const { bool GlobalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { ComputeNeighborhoods(); // Insert partially inserted pairs. + const RoutingModel::IndexPairs& pickup_delivery_pairs = + model()->GetPickupAndDeliveryPairs(); + std::vector pairs_to_insert; absl::flat_hash_map> vehicle_to_pair_nodes; - for (const RoutingModel::IndexPair& index_pair : - model()->GetPickupAndDeliveryPairs()) { + for (int index = 0; index < pickup_delivery_pairs.size(); index++) { + const RoutingModel::IndexPair& index_pair = pickup_delivery_pairs[index]; int pickup_vehicle = -1; for (int64 pickup : index_pair.first) { if (Contains(pickup)) { @@ -3354,6 +3438,9 @@ bool GlobalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { break; } } + if (pickup_vehicle < 0 && delivery_vehicle < 0) { + pairs_to_insert.push_back(index); + } if (pickup_vehicle >= 0 && delivery_vehicle < 0) { std::vector& pair_nodes = vehicle_to_pair_nodes[pickup_vehicle]; for (int64 delivery : index_pair.second) { @@ -3371,14 +3458,15 @@ bool GlobalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { InsertNodesOnRoutes(vehicle_and_nodes.second, {vehicle_and_nodes.first}); } - InsertNodesByRequirementTopologicalOrder(); + InsertPairsAndNodesByRequirementTopologicalOrder(); // TODO(user): Adapt the pair insertions to also support seed and // sequential insertion. - InsertPairs(); + InsertPairs(pairs_to_insert); std::vector nodes; for (int node = 0; node < model()->Size(); ++node) { - if (!Contains(node)) { + if (!Contains(node) && model()->GetPickupIndexPairs(node).empty() && + model()->GetDeliveryIndexPairs(node).empty()) { nodes.push_back(node); } } @@ -3394,17 +3482,22 @@ bool GlobalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { } void GlobalCheapestInsertionFilteredHeuristic:: - InsertNodesByRequirementTopologicalOrder() { - for (int type : model()->GetTopologicallySortedVisitTypes()) { - InsertNodesOnRoutes(model()->GetSingleNodesOfType(type), {}); + InsertPairsAndNodesByRequirementTopologicalOrder() { + for (const std::vector& types : + model()->GetTopologicallySortedVisitTypes()) { + for (int type : types) { + InsertPairs(model()->GetPairIndicesOfType(type)); + InsertNodesOnRoutes(model()->GetSingleNodesOfType(type), {}); + } } } -void GlobalCheapestInsertionFilteredHeuristic::InsertPairs() { +void GlobalCheapestInsertionFilteredHeuristic::InsertPairs( + const std::vector& pair_indices) { AdjustablePriorityQueue priority_queue; std::vector pickup_to_entries; std::vector delivery_to_entries; - InitializePairPositions(&priority_queue, &pickup_to_entries, + InitializePairPositions(pair_indices, &priority_queue, &pickup_to_entries, &delivery_to_entries); while (!priority_queue.IsEmpty()) { if (StopSearch()) { @@ -3661,6 +3754,7 @@ int GlobalCheapestInsertionFilteredHeuristic::InsertSeedNode( } void GlobalCheapestInsertionFilteredHeuristic::InitializePairPositions( + const std::vector& pair_indices, AdjustablePriorityQueue< GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue, std::vector* @@ -3672,34 +3766,31 @@ void GlobalCheapestInsertionFilteredHeuristic::InitializePairPositions( pickup_to_entries->resize(model()->Size()); delivery_to_entries->clear(); delivery_to_entries->resize(model()->Size()); - for (const RoutingModel::IndexPair& index_pair : - model()->GetPickupAndDeliveryPairs()) { + const RoutingModel::IndexPairs& pickup_delivery_pairs = + model()->GetPickupAndDeliveryPairs(); + for (int index : pair_indices) { + const RoutingModel::IndexPair& index_pair = pickup_delivery_pairs[index]; for (int64 pickup : index_pair.first) { + if (Contains(pickup)) continue; for (int64 delivery : index_pair.second) { - if (Contains(pickup) || Contains(delivery)) { - continue; - } - int64 penalty = - FLAGS_routing_shift_insertion_cost_by_penalty ? kint64max : 0; + if (Contains(delivery)) continue; + const int64 pickup_penalty = GetUnperformedValue(pickup); + const int64 delivery_penalty = GetUnperformedValue(delivery); + const int64 penalty = FLAGS_routing_shift_insertion_cost_by_penalty + ? CapAdd(pickup_penalty, delivery_penalty) + : 0; // Add insertion entry making pair unperformed. When the pair is part // of a disjunction we do not try to make any of its pairs unperformed // as it requires having an entry with all pairs being unperformed. // TODO(user): Adapt the code to make pair disjunctions unperformed. - if (index_pair.first.size() == 1 && index_pair.second.size() == 1) { - const int64 pickup_penalty = GetUnperformedValue(pickup); - const int64 delivery_penalty = GetUnperformedValue(delivery); - if (pickup_penalty != kint64max && delivery_penalty != kint64max) { - PairEntry* const entry = - new PairEntry(pickup, -1, delivery, -1, -1); - if (FLAGS_routing_shift_insertion_cost_by_penalty) { - entry->set_value(0); - penalty = CapAdd(pickup_penalty, delivery_penalty); - } else { - entry->set_value(CapAdd(pickup_penalty, delivery_penalty)); - penalty = 0; - } - priority_queue->Add(entry); - } + if (gci_params_.add_unperformed_entries && + index_pair.first.size() == 1 && index_pair.second.size() == 1 && + pickup_penalty != kint64max && delivery_penalty != kint64max) { + PairEntry* const entry = new PairEntry(pickup, -1, delivery, -1, -1); + entry->set_value(FLAGS_routing_shift_insertion_cost_by_penalty + ? 0 + : CapAdd(pickup_penalty, delivery_penalty)); + priority_queue->Add(entry); } // Add all other insertion entries with pair performed. InitializeInsertionEntriesPerformingPair( @@ -4071,21 +4162,16 @@ void GlobalCheapestInsertionFilteredHeuristic::InitializePositions( continue; } const int64 node_penalty = GetUnperformedValue(node); - int64 penalty = - FLAGS_routing_shift_insertion_cost_by_penalty ? kint64max : 0; + // In the case where we're not considering all routes simultaneously, + // always shift insertion costs by penalty. + const bool shift_insertion_cost_by_penalty = + FLAGS_routing_shift_insertion_cost_by_penalty || + num_vehicles < model()->vehicles(); + const int64 penalty = shift_insertion_cost_by_penalty ? node_penalty : 0; // Add insertion entry making node unperformed. - if (node_penalty != kint64max) { + if (gci_params_.add_unperformed_entries && node_penalty != kint64max) { NodeEntry* const node_entry = new NodeEntry(node, -1, -1); - if (FLAGS_routing_shift_insertion_cost_by_penalty || - num_vehicles < model()->vehicles()) { - // In the case where we're not considering all routes simultaneously, - // always shift insertion costs by penalty. - node_entry->set_value(0); - penalty = node_penalty; - } else { - node_entry->set_value(node_penalty); - penalty = 0; - } + node_entry->set_value(shift_insertion_cost_by_penalty ? 0 : node_penalty); priority_queue->Add(node_entry); } // Add all insertion entries making node performed. @@ -4142,7 +4228,8 @@ void GlobalCheapestInsertionFilteredHeuristic:: continue; } const int vehicle = node_index_to_vehicle_[insert_after]; - if (!insert_on_vehicle_for_cost_class(vehicle, cost_class)) { + if (vehicle == -1 || + !insert_on_vehicle_for_cost_class(vehicle, cost_class)) { continue; } NodeEntry* const node_entry = @@ -4277,11 +4364,9 @@ bool LocalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { for (const int64 delivery_insertion : delivery_insertion_positions) { InsertBetween(pickup, pickup_insertion, pickup_insertion_next); const int64 delivery_insertion_next = - (delivery_insertion == pickup_insertion) - ? pickup - : (delivery_insertion == pickup) - ? pickup_insertion_next - : Value(delivery_insertion); + (delivery_insertion == pickup_insertion) ? pickup + : (delivery_insertion == pickup) ? pickup_insertion_next + : Value(delivery_insertion); InsertBetween(delivery, delivery_insertion, delivery_insertion_next); if (Commit()) { diff --git a/ortools/constraint_solver/samples/tsp_cities_routes.cc b/ortools/constraint_solver/samples/tsp_cities_routes.cc new file mode 100644 index 0000000000..e77f48ec2c --- /dev/null +++ b/ortools/constraint_solver/samples/tsp_cities_routes.cc @@ -0,0 +1,134 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// [START program] +// [START import] +#include +#include + +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_enums.pb.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +// [END import] + +namespace operations_research { +// [START data_model] +struct DataModel { + const std::vector> distance_matrix{ + {0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972}, + {2451, 0, 1745, 1524, 831, 1240, 959, 2596, 403, 1589, 1374, 357, 579}, + {713, 1745, 0, 355, 920, 803, 1737, 851, 1858, 262, 940, 1453, 1260}, + {1018, 1524, 355, 0, 700, 862, 1395, 1123, 1584, 466, 1056, 1280, 987}, + {1631, 831, 920, 700, 0, 663, 1021, 1769, 949, 796, 879, 586, 371}, + {1374, 1240, 803, 862, 663, 0, 1681, 1551, 1765, 547, 225, 887, 999}, + {2408, 959, 1737, 1395, 1021, 1681, 0, 2493, 678, 1724, 1891, 1114, 701}, + {213, 2596, 851, 1123, 1769, 1551, 2493, 0, 2699, 1038, 1605, 2300, 2099}, + {2571, 403, 1858, 1584, 949, 1765, 678, 2699, 0, 1744, 1645, 653, 600}, + {875, 1589, 262, 466, 796, 547, 1724, 1038, 1744, 0, 679, 1272, 1162}, + {1420, 1374, 940, 1056, 879, 225, 1891, 1605, 1645, 679, 0, 1017, 1200}, + {2145, 357, 1453, 1280, 586, 887, 1114, 2300, 653, 1272, 1017, 0, 504}, + {1972, 579, 1260, 987, 371, 999, 701, 2099, 600, 1162, 1200, 504, 0}, + }; + const int num_vehicles = 1; + const RoutingIndexManager::NodeIndex depot{0}; +}; +// [END data_model] + +// [START solution_printer] +void PrintSolution( + const std::vector>& routes) { + // Print routes. + DataModel data; + int64 total_distance = 0; + for (int i = 0; i < routes.size(); ++i) { + std::vector route = routes[i]; + int64 route_distance{0}; + std::stringstream route_text; + LOG(INFO) << "Route for Vehicle " << i << ":"; + route_text << route[0]; + for (int j = 1; j < route.size(); ++j) { + route_text << " -> " << route[j]; + route_distance += + data.distance_matrix[route[j - 1].value()][route[j].value()]; + } + LOG(INFO) << route_text.str(); + LOG(INFO) << "Distance of the route: " << route_distance << "m"; + total_distance += route_distance; + } + LOG(INFO) << "Total distance of all routes: " << total_distance << "m"; +} +// [END solution_printer] + +void Tsp() { + // Instantiate the data problem. + // [START data] + DataModel data; + // [END data] + + // Create Routing Index Manager + // [START index_manager] + RoutingIndexManager manager(data.distance_matrix.size(), data.num_vehicles, + data.depot); + // [END index_manager] + + // Create Routing Model. + // [START routing_model] + RoutingModel routing(manager); + // [END routing_model] + + // Define cost of each arc. + // [START arc_cost] + const int transit_callback_index = routing.RegisterTransitCallback( + [&data, &manager](int64 from_index, int64 to_index) -> int64 { + // Convert from routing variable Index to distance matrix NodeIndex. + auto from_node = manager.IndexToNode(from_index).value(); + auto to_node = manager.IndexToNode(to_index).value(); + return data.distance_matrix[from_node][to_node]; + }); + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index); + // [END arc_cost] + + // Setting first solution heuristic. + // [START parameters] + RoutingSearchParameters searchParameters = DefaultRoutingSearchParameters(); + searchParameters.set_first_solution_strategy( + FirstSolutionStrategy::PATH_CHEAPEST_ARC); + // [END parameters] + + // Solve the problem. + // [START solve] + const Assignment* solution = routing.SolveWithParameters(searchParameters); + // [END solve] + + // [START get_routes] + // Get the routes and convert indices to nodes. + std::vector> routes; + for (const std::vector& + route_indices : routing.GetRoutesFromAssignment(*solution)) { + routes.push_back(manager.IndicesToNodes(route_indices)); + } + // [END get_routes] + + // Print solution on console. + PrintSolution(routes); + // [END print_solution] +} + +} // namespace operations_research + +int main(int argc, char** argv) { + operations_research::Tsp(); + return EXIT_SUCCESS; +} +// [END program] diff --git a/ortools/constraint_solver/samples/vrp_routes.cc b/ortools/constraint_solver/samples/vrp_routes.cc new file mode 100644 index 0000000000..4d32e4f87b --- /dev/null +++ b/ortools/constraint_solver/samples/vrp_routes.cc @@ -0,0 +1,157 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// [START program] +// [START import] +#include + +#include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_enums.pb.h" +#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/constraint_solver/routing_parameters.h" +// [END import] + +namespace operations_research { +// [START data_model] +struct DataModel { + const std::vector> distance_matrix{ + {0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, + 776, 662}, + {548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, + 1016, 868, 1210}, + {776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, + 788, 1552, 754}, + {696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, + 1164, 560, 1358}, + {582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, + 1050, 674, 1244}, + {274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, + 1050, 708}, + {502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, + 1278, 480}, + {194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, + 742, 856}, + {308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, + 1084, 514}, + {194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, + 810, 468}, + {536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, + 388, 1152, 354}, + {502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, + 650, 274, 844}, + {388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, + 388, 730}, + {354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, + 422, 536}, + {468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, + 0, 764, 194}, + {776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, + 422, 764, 0, 798}, + {662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, + 194, 798, 0}, + }; + const int num_vehicles = 4; + const RoutingIndexManager::NodeIndex depot{0}; +}; +// [END data_model] + +// [START solution_printer] +void PrintSolution( + const std::vector>& routes) { + // Print routes. + DataModel data; + int64 total_distance = 0; + for (int i = 0; i < routes.size(); ++i) { + std::vector route = routes[i]; + int64 route_distance{0}; + std::stringstream route_text; + LOG(INFO) << "Route for Vehicle " << i << ":"; + route_text << route[0]; + for (int j = 1; j < route.size(); ++j) { + route_text << " -> " << route[j]; + route_distance += + data.distance_matrix[route[j - 1].value()][route[j].value()]; + } + LOG(INFO) << route_text.str(); + LOG(INFO) << "Distance of the route: " << route_distance << "m"; + total_distance += route_distance; + } + LOG(INFO) << "Total distance of all routes: " << total_distance << "m"; +} +// [END solution_printer] + +void Vrp() { + // Instantiate the data problem. + // [START data] + DataModel data; + // [END data] + + // Create Routing Index Manager + // [START index_manager] + RoutingIndexManager manager(data.distance_matrix.size(), data.num_vehicles, + data.depot); + // [END index_manager] + + // Create Routing Model. + // [START routing_model] + RoutingModel routing(manager); + // [END routing_model] + + // Create and register a transit callback. + // [START transit_callback] + const int transit_callback_index = routing.RegisterTransitCallback( + [&data, &manager](int64 from_index, int64 to_index) -> int64 { + // Convert from routing variable Index to distance matrix NodeIndex. + auto from_node = manager.IndexToNode(from_index).value(); + auto to_node = manager.IndexToNode(to_index).value(); + return data.distance_matrix[from_node][to_node]; + }); + // [END transit_callback] + + // Define cost of each arc. + // [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index); + // [END arc_cost] + + // Setting first solution heuristic. + // [START parameters] + RoutingSearchParameters searchParameters = DefaultRoutingSearchParameters(); + searchParameters.set_first_solution_strategy( + FirstSolutionStrategy::PATH_CHEAPEST_ARC); + // [END parameters] + + // Solve the problem. + // [START solve] + const Assignment* solution = routing.SolveWithParameters(searchParameters); + // [END solve] + + // [START get_routes] + // Get the routes and convert indices to nodes. + std::vector> routes; + for (const std::vector& + route_indices : routing.GetRoutesFromAssignment(*solution)) { + routes.push_back(manager.IndicesToNodes(route_indices)); + } + // [END get_routes] + + // Print solution on console. + PrintSolution(routes); + // [END print_solution] +} +} // namespace operations_research + +int main(int argc, char** argv) { + operations_research::Vrp(); + return EXIT_SUCCESS; +} +// [END program] diff --git a/ortools/constraint_solver/search.cc b/ortools/constraint_solver/search.cc index 4cf1f2cebd..44e9058df3 100644 --- a/ortools/constraint_solver/search.cc +++ b/ortools/constraint_solver/search.cc @@ -55,7 +55,8 @@ namespace operations_research { SearchLog::SearchLog(Solver* const s, OptimizeVar* const obj, IntVar* const var, double scaling_factor, double offset, - std::function display_callback, int period) + std::function display_callback, + bool display_on_new_solutions_only, int period) : SearchMonitor(s), period_(period), timer_(new WallTimer), @@ -64,6 +65,7 @@ SearchLog::SearchLog(Solver* const s, OptimizeVar* const obj, IntVar* const var, scaling_factor_(scaling_factor), offset_(offset), display_callback_(std::move(display_callback)), + display_on_new_solutions_only_(display_on_new_solutions_only), nsol_(0), tick_(0), objective_min_(kint64max), @@ -187,7 +189,11 @@ void SearchLog::NoMoreSolutions() { solver()->neighbors(), solver()->filtered_neighbors(), solver()->accepted_neighbors()); } - absl::StrAppendFormat(&buffer, ", %s)", MemoryUsage()); + absl::StrAppendFormat(&buffer, ", %s", MemoryUsage()); + if (!display_on_new_solutions_only_ && display_callback_) { + absl::StrAppendFormat(&buffer, ", %s", display_callback_()); + } + buffer.append(")"); OutputLine(buffer); } @@ -276,45 +282,45 @@ std::string SearchLog::MemoryUsage() { } SearchMonitor* Solver::MakeSearchLog(int branch_period) { - return RevAlloc( - new SearchLog(this, nullptr, nullptr, 1.0, 0.0, nullptr, branch_period)); + return MakeSearchLog(branch_period, static_cast(nullptr)); } SearchMonitor* Solver::MakeSearchLog(int branch_period, IntVar* const var) { - return RevAlloc( - new SearchLog(this, nullptr, var, 1.0, 0.0, nullptr, branch_period)); + return MakeSearchLog(branch_period, var, nullptr); } SearchMonitor* Solver::MakeSearchLog( int branch_period, std::function display_callback) { - return RevAlloc(new SearchLog(this, nullptr, nullptr, 1.0, 0.0, - std::move(display_callback), branch_period)); + return MakeSearchLog(branch_period, static_cast(nullptr), + std::move(display_callback)); } SearchMonitor* Solver::MakeSearchLog( int branch_period, IntVar* const var, std::function display_callback) { return RevAlloc(new SearchLog(this, nullptr, var, 1.0, 0.0, - std::move(display_callback), branch_period)); + std::move(display_callback), true, + branch_period)); } SearchMonitor* Solver::MakeSearchLog(int branch_period, OptimizeVar* const opt_var) { - return RevAlloc( - new SearchLog(this, opt_var, nullptr, 1.0, 0.0, nullptr, branch_period)); + return MakeSearchLog(branch_period, opt_var, nullptr); } SearchMonitor* Solver::MakeSearchLog( int branch_period, OptimizeVar* const opt_var, std::function display_callback) { return RevAlloc(new SearchLog(this, opt_var, nullptr, 1.0, 0.0, - std::move(display_callback), branch_period)); + std::move(display_callback), true, + branch_period)); } SearchMonitor* Solver::MakeSearchLog(SearchLogParameters parameters) { return RevAlloc(new SearchLog(this, parameters.objective, parameters.variable, parameters.scaling_factor, parameters.offset, std::move(parameters.display_callback), + parameters.display_on_new_solutions_only, parameters.branch_period)); } @@ -4133,6 +4139,132 @@ RegularLimitParameters Solver::MakeDefaultRegularLimitParameters() const { return proto; } +// ----- Improvement Search Limit ----- + +ImprovementSearchLimit::ImprovementSearchLimit( + Solver* const s, IntVar* objective_var, bool maximize, + double objective_scaling_factor, double objective_offset, + double improvement_rate_coefficient, + int improvement_rate_solutions_distance) + : SearchLimit(s), + objective_var_(objective_var), + maximize_(maximize), + objective_scaling_factor_(objective_scaling_factor), + objective_offset_(objective_offset), + improvement_rate_coefficient_(improvement_rate_coefficient), + improvement_rate_solutions_distance_( + improvement_rate_solutions_distance) { + Init(); +} + +ImprovementSearchLimit::~ImprovementSearchLimit() {} + +void ImprovementSearchLimit::Init() { + best_objective_ = maximize_ ? -std::numeric_limits::infinity() + : std::numeric_limits::infinity(); + threshold_ = std::numeric_limits::infinity(); + objective_updated_ = false; + gradient_stage_ = true; +} + +void ImprovementSearchLimit::Copy(const SearchLimit* const limit) { + const ImprovementSearchLimit* const improv = + reinterpret_cast(limit); + objective_var_ = improv->objective_var_; + maximize_ = improv->maximize_; + objective_scaling_factor_ = improv->objective_scaling_factor_; + objective_offset_ = improv->objective_offset_; + improvement_rate_coefficient_ = improv->improvement_rate_coefficient_; + improvement_rate_solutions_distance_ = + improv->improvement_rate_solutions_distance_; + improvements_ = improv->improvements_; + threshold_ = improv->threshold_; + best_objective_ = improv->best_objective_; + objective_updated_ = improv->objective_updated_; + gradient_stage_ = improv->gradient_stage_; +} + +SearchLimit* ImprovementSearchLimit::MakeClone() const { + Solver* const s = solver(); + return s->MakeImprovementLimit( + objective_var_, maximize_, objective_scaling_factor_, objective_offset_, + improvement_rate_coefficient_, improvement_rate_solutions_distance_); +} + +bool ImprovementSearchLimit::Check() { + if (!objective_updated_) { + return false; + } + objective_updated_ = false; + + if (improvements_.size() <= improvement_rate_solutions_distance_) { + return false; + } + + const std::pair cur = improvements_.back(); + const std::pair prev = improvements_.front(); + DCHECK_GT(cur.second, prev.second); + double improvement_rate = + std::abs(prev.first - cur.first) / (cur.second - prev.second); + if (gradient_stage_) { + threshold_ = fmin(threshold_, improvement_rate); + } else if (improvement_rate_coefficient_ * improvement_rate < threshold_) { + return true; + } + + return false; +} + +bool ImprovementSearchLimit::AtSolution() { + const int64 new_objective = + objective_var_ != nullptr && objective_var_->Bound() + ? objective_var_->Value() + : (maximize_ + ? solver()->GetOrCreateLocalSearchState()->ObjectiveMax() + : solver()->GetOrCreateLocalSearchState()->ObjectiveMin()); + + const double scaled_new_objective = + objective_scaling_factor_ * (new_objective + objective_offset_); + + const bool is_improvement = maximize_ + ? scaled_new_objective > best_objective_ + : scaled_new_objective < best_objective_; + + if (gradient_stage_ && !is_improvement) { + gradient_stage_ = false; + // In case we haven't got enough solutions during the first stage, the limit + // never stops the search. + if (threshold_ == std::numeric_limits::infinity()) { + threshold_ = -1; + } + } + + if (is_improvement) { + best_objective_ = scaled_new_objective; + objective_updated_ = true; + improvements_.push_back( + std::make_pair(scaled_new_objective, solver()->neighbors())); + // We need to have 'improvement_rate_solutions_distance_' + 1 element in the + // 'improvements_', so the distance between improvements is + // 'improvement_rate_solutions_distance_'. + if (improvements_.size() - 1 > improvement_rate_solutions_distance_) { + improvements_.pop_front(); + } + DCHECK_LE(improvements_.size() - 1, improvement_rate_solutions_distance_); + } + + return true; +} + +ImprovementSearchLimit* Solver::MakeImprovementLimit( + IntVar* objective_var, bool maximize, double objective_scaling_factor, + double objective_offset, double improvement_rate_coefficient, + int improvement_rate_solutions_distance) { + return RevAlloc(new ImprovementSearchLimit( + this, objective_var, maximize, objective_scaling_factor, objective_offset, + improvement_rate_coefficient, improvement_rate_solutions_distance)); +} + // A limit whose Check function is the OR of two underlying limits. namespace { class ORLimit : public SearchLimit { diff --git a/ortools/constraint_solver/search_stats.proto b/ortools/constraint_solver/search_stats.proto new file mode 100644 index 0000000000..34c9c81682 --- /dev/null +++ b/ortools/constraint_solver/search_stats.proto @@ -0,0 +1,80 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Protocol buffer used to store search statistics. + +syntax = "proto3"; + +option java_package = "com.google.ortools.constraintsolver"; +option java_multiple_files = true; +option csharp_namespace = "Google.OrTools.ConstraintSolver"; + +package operations_research; + +// Statistics on local search. +message LocalSearchStatistics { + // Statistics on local search operators called during the search. + message LocalSearchOperatorStatistics { + // Name of the operator. + string local_search_operator = 1; + // Number of neighbors generated by the operator. + int64 num_neighbors = 2; + // Number of neighbors which were filtered. + int64 num_filtered_neighbors = 3; + // Number of neighbors eventually accepted. + int64 num_accepted_neighbors = 4; + // Time spent in the operator. + double duration_seconds = 5; + } + // Statistics for each operator called during the search. + repeated LocalSearchOperatorStatistics local_search_operator_statistics = 1; + // Statistics on local search filters called during the search. + message LocalSearchFilterStatistics { + // Name of the filter. + string local_search_filter = 1; + // Number of times the filter was called. + int64 num_calls = 2; + // Number of times the filter rejected a neighbor. + int64 num_rejects = 3; + // Time spent in the filter. + double duration_seconds = 4; + } + // Statistics for each filter called during the search. + repeated LocalSearchFilterStatistics local_search_filter_statistics = 2; + // Total number of (filtered/accepted) neighbors created during the search. + int64 total_num_neighbors = 3; + int64 total_num_filtered_neighbors = 4; + int64 total_num_accepted_neighbors = 5; +} + +// Statistics on the search in the constraint solver. +message ConstraintSolverStatistics { + // Number of branches explored. + int64 num_branches = 1; + // Number of failures/backtracks. + int64 num_failures = 2; + // Number of solutions found. + int64 num_solutions = 3; + // Memory usage of the solver. + int64 bytes_used = 4; + // Time spent in the filter. + double duration_seconds = 5; +} + +// Search statistics. +message SearchStatistics { + // Local search statistics. + LocalSearchStatistics local_search_statistics = 1; + // Constraint solver statistics. + ConstraintSolverStatistics constraint_solver_statistics = 2; +} diff --git a/ortools/data/jobshop_scheduling_parser.cc b/ortools/data/jobshop_scheduling_parser.cc index 257a42ec68..4f0f9107e0 100644 --- a/ortools/data/jobshop_scheduling_parser.cc +++ b/ortools/data/jobshop_scheduling_parser.cc @@ -16,7 +16,6 @@ #include #include "absl/strings/numbers.h" -#include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "google/protobuf/wrappers.pb.h" #include "ortools/base/commandlineflags.h" @@ -356,7 +355,7 @@ void JsspParser::ProcessTardinessLine(const std::string& line) { job->mutable_earliest_start()->set_value(est); } job->set_late_due_date(strtoint64(words[1])); - const double weight = stod(words[2]); + const double weight = std::stod(words[2]); const int64 tardiness = static_cast(round(weight * FLAGS_jssp_scaling_up_factor)); job->set_lateness_cost_per_time_unit(tardiness); diff --git a/ortools/flatzinc/parser.tab.cc b/ortools/flatzinc/parser.tab.cc index aa1bf881d3..18c09d0957 100644 --- a/ortools/flatzinc/parser.tab.cc +++ b/ortools/flatzinc/parser.tab.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -/* A Bison parser, made by GNU Bison 3.5.1. */ +/* A Bison parser, made by GNU Bison 3.7.2. */ /* Bison implementation for Yacc-like parsers in C @@ -47,6 +47,10 @@ /* C LALR(1) parser skeleton written by Richard Stallman, by simplifying the original so-called "semantic" parser. */ +/* DO NOT RELY ON FEATURES THAT ARE NOT DOCUMENTED in the manual, + especially those whose name start with YY_ or yy_. They are + private implementation details that can be changed or removed. */ + /* All symbols defined below should begin with yy or YY, to avoid infringing on user name space. This should be done even for local variables, as they might otherwise be expanded by user macros. @@ -54,14 +58,11 @@ define necessary library symbols; they are noted "INFRINGES ON USER NAME SPACE" below. */ -/* Undocumented macros, especially those whose name start with YY_, - are private implementation details. Do not rely on them. */ - /* Identify Bison output. */ #define YYBISON 1 /* Bison version. */ -#define YYBISON_VERSION "3.5.1" +#define YYBISON_VERSION "3.7.2" /* Skeleton name. */ #define YYSKELETON_NAME "yacc.c" @@ -105,84 +106,78 @@ #endif #endif -/* Enabling verbose error messages. */ -#ifdef YYERROR_VERBOSE -#undef YYERROR_VERBOSE -#define YYERROR_VERBOSE 1 -#else -#define YYERROR_VERBOSE 1 -#endif - -/* Use api.header.include to #include this header - instead of duplicating it here. */ -#ifndef YY_ORFZ_ORTOOLS_FLATZINC_PARSER_TAB_HH_INCLUDED -#define YY_ORFZ_ORTOOLS_FLATZINC_PARSER_TAB_HH_INCLUDED -/* Debug traces. */ -#ifndef ORFZ_DEBUG -#if defined YYDEBUG -#if YYDEBUG -#define ORFZ_DEBUG 1 -#else -#define ORFZ_DEBUG 0 -#endif -#else /* ! defined YYDEBUG */ -#define ORFZ_DEBUG 1 -#endif /* ! defined YYDEBUG */ -#endif /* ! defined ORFZ_DEBUG */ -#if ORFZ_DEBUG -extern int orfz_debug; -#endif -/* "%code requires" blocks. */ -#line 19 "./ortools/flatzinc/parser.yy" - -#if !defined(OR_TOOLS_FLATZINC_FLATZINC_TAB_HH_) -#define OR_TOOLS_FLATZINC_FLATZINC_TAB_HH_ -#include "absl/strings/match.h" -#include "absl/strings/str_format.h" -#include "ortools/flatzinc/parser_util.h" - -// Tells flex to use the LexerInfo class to communicate with the bison parser. -typedef operations_research::fz::LexerInfo YYSTYPE; - -// Defines the parameter to the orfz_lex() call from the orfz_parse() method. -#define YYLEX_PARAM scanner - -#endif // OR_TOOLS_FLATZINC_FLATZINC_TAB_HH_ - -#line 141 "./ortools/flatzinc/parser.tab.cc" - -/* Token type. */ -#ifndef ORFZ_TOKENTYPE -#define ORFZ_TOKENTYPE -enum orfz_tokentype { - ARRAY = 258, - TOKEN_BOOL = 259, - CONSTRAINT = 260, - TOKEN_FLOAT = 261, - TOKEN_INT = 262, - MAXIMIZE = 263, - MINIMIZE = 264, - OF = 265, - PREDICATE = 266, - SATISFY = 267, - SET = 268, - SOLVE = 269, - VAR = 270, - DOTDOT = 271, - COLONCOLON = 272, - IVALUE = 273, - SVALUE = 274, - IDENTIFIER = 275, - DVALUE = 276 +#include "parser.tab.hh" +/* Symbol kind. */ +enum yysymbol_kind_t { + YYSYMBOL_YYEMPTY = -2, + YYSYMBOL_YYEOF = 0, /* "end of file" */ + YYSYMBOL_YYerror = 1, /* error */ + YYSYMBOL_YYUNDEF = 2, /* "invalid token" */ + YYSYMBOL_ARRAY = 3, /* ARRAY */ + YYSYMBOL_TOKEN_BOOL = 4, /* TOKEN_BOOL */ + YYSYMBOL_CONSTRAINT = 5, /* CONSTRAINT */ + YYSYMBOL_TOKEN_FLOAT = 6, /* TOKEN_FLOAT */ + YYSYMBOL_TOKEN_INT = 7, /* TOKEN_INT */ + YYSYMBOL_MAXIMIZE = 8, /* MAXIMIZE */ + YYSYMBOL_MINIMIZE = 9, /* MINIMIZE */ + YYSYMBOL_OF = 10, /* OF */ + YYSYMBOL_PREDICATE = 11, /* PREDICATE */ + YYSYMBOL_SATISFY = 12, /* SATISFY */ + YYSYMBOL_SET = 13, /* SET */ + YYSYMBOL_SOLVE = 14, /* SOLVE */ + YYSYMBOL_VAR = 15, /* VAR */ + YYSYMBOL_DOTDOT = 16, /* DOTDOT */ + YYSYMBOL_COLONCOLON = 17, /* COLONCOLON */ + YYSYMBOL_IVALUE = 18, /* IVALUE */ + YYSYMBOL_SVALUE = 19, /* SVALUE */ + YYSYMBOL_IDENTIFIER = 20, /* IDENTIFIER */ + YYSYMBOL_DVALUE = 21, /* DVALUE */ + YYSYMBOL_22_ = 22, /* ';' */ + YYSYMBOL_23_ = 23, /* '(' */ + YYSYMBOL_24_ = 24, /* ')' */ + YYSYMBOL_25_ = 25, /* ',' */ + YYSYMBOL_26_ = 26, /* ':' */ + YYSYMBOL_27_ = 27, /* '[' */ + YYSYMBOL_28_ = 28, /* ']' */ + YYSYMBOL_29_ = 29, /* '=' */ + YYSYMBOL_30_ = 30, /* '{' */ + YYSYMBOL_31_ = 31, /* '}' */ + YYSYMBOL_YYACCEPT = 32, /* $accept */ + YYSYMBOL_model = 33, /* model */ + YYSYMBOL_predicates = 34, /* predicates */ + YYSYMBOL_predicate = 35, /* predicate */ + YYSYMBOL_predicate_arguments = 36, /* predicate_arguments */ + YYSYMBOL_predicate_argument = 37, /* predicate_argument */ + YYSYMBOL_predicate_array_argument = 38, /* predicate_array_argument */ + YYSYMBOL_predicate_ints = 39, /* predicate_ints */ + YYSYMBOL_variable_or_constant_declarations = + 40, /* variable_or_constant_declarations */ + YYSYMBOL_variable_or_constant_declaration = + 41, /* variable_or_constant_declaration */ + YYSYMBOL_optional_var_or_value = 42, /* optional_var_or_value */ + YYSYMBOL_optional_var_or_value_array = 43, /* optional_var_or_value_array */ + YYSYMBOL_var_or_value_array = 44, /* var_or_value_array */ + YYSYMBOL_var_or_value = 45, /* var_or_value */ + YYSYMBOL_int_domain = 46, /* int_domain */ + YYSYMBOL_set_domain = 47, /* set_domain */ + YYSYMBOL_float_domain = 48, /* float_domain */ + YYSYMBOL_domain = 49, /* domain */ + YYSYMBOL_integers = 50, /* integers */ + YYSYMBOL_integer = 51, /* integer */ + YYSYMBOL_floats = 52, /* floats */ + YYSYMBOL_float = 53, /* float */ + YYSYMBOL_const_literal = 54, /* const_literal */ + YYSYMBOL_const_literals = 55, /* const_literals */ + YYSYMBOL_constraints = 56, /* constraints */ + YYSYMBOL_constraint = 57, /* constraint */ + YYSYMBOL_arguments = 58, /* arguments */ + YYSYMBOL_argument = 59, /* argument */ + YYSYMBOL_annotations = 60, /* annotations */ + YYSYMBOL_annotation_arguments = 61, /* annotation_arguments */ + YYSYMBOL_annotation = 62, /* annotation */ + YYSYMBOL_solve = 63 /* solve */ }; -#endif - -/* Value type. */ - -int orfz_parse(operations_research::fz::ParserContext* context, - operations_research::fz::Model* model, bool* ok, void* scanner); - -#endif /* !YY_ORFZ_ORTOOLS_FLATZINC_PARSER_TAB_HH_INCLUDED */ +typedef enum yysymbol_kind_t yysymbol_kind_t; /* Unqualified %code blocks. */ #line 36 "./ortools/flatzinc/parser.yy" @@ -204,7 +199,7 @@ using operations_research::fz::SolutionOutputSpecs; using operations_research::fz::VariableRefOrValue; using operations_research::fz::VariableRefOrValueArray; -#line 199 "./ortools/flatzinc/parser.tab.cc" +#line 192 "./ortools/flatzinc/parser.tab.cc" #ifdef short #undef short @@ -373,7 +368,7 @@ typedef int yy_state_fast_t; #define YY_ASSERT(E) ((void)(0 && (E))) -#if !defined yyoverflow || YYERROR_VERBOSE +#if 1 /* The parser invokes alloca or malloc; define the necessary symbols. */ @@ -437,11 +432,11 @@ void* malloc(YYSIZE_T); /* INFRINGES ON USER NAME SPACE */ #ifndef YYFREE #define YYFREE free #if !defined free && !defined EXIT_SUCCESS -void free(void*); /* INFRINGES ON USER NAME SPACE */ +void free(void*); /* INFRINGES ON USER NAME SPACE */ #endif #endif #endif -#endif /* ! defined yyoverflow || YYERROR_VERBOSE */ +#endif /* 1 */ #if (!defined yyoverflow && \ (!defined __cplusplus || \ @@ -510,13 +505,15 @@ union yyalloc { /* YYNSTATES -- Number of states. */ #define YYNSTATES 223 -#define YYUNDEFTOK 2 +/* YYMAXUTOK -- Last valid token kind. */ #define YYMAXUTOK 276 /* YYTRANSLATE(TOKEN-NUM) -- Symbol number corresponding to TOKEN-NUM as returned by yylex, with out-of-bounds checking. */ -#define YYTRANSLATE(YYX) \ - (0 <= (YYX) && (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK) +#define YYTRANSLATE(YYX) \ + (0 <= (YYX) && (YYX) <= YYMAXUTOK \ + ? YY_CAST(yysymbol_kind_t, yytranslate[YYX]) \ + : YYSYMBOL_YYUNDEF) /* YYTRANSLATE[TOKEN-NUM] -- Symbol number corresponding to TOKEN-NUM as returned by yylex. */ @@ -548,12 +545,19 @@ static const yytype_int16 yyrline[] = { 599, 602, 603, 606, 607, 608, 609, 619, 628, 634, 649, 657, 666}; #endif -#if ORFZ_DEBUG || YYERROR_VERBOSE || 1 +/** Accessing symbol of state STATE. */ +#define YY_ACCESSING_SYMBOL(State) YY_CAST(yysymbol_kind_t, yystos[State]) + +#if 1 +/* The user-facing name of the symbol whose (internal) number is + YYSYMBOL. No bounds checking. */ +static const char* yysymbol_name(yysymbol_kind_t yysymbol) YY_ATTRIBUTE_UNUSED; + /* YYTNAME[SYMBOL-NUM] -- String name of the symbol SYMBOL-NUM. First, the terminals, then, starting at YYNTOKENS, nonterminals. */ -static const char* const yytname[] = {"$end", +static const char* const yytname[] = {"\"end of file\"", "error", - "$undefined", + "\"invalid token\"", "ARRAY", "TOKEN_BOOL", "CONSTRAINT", @@ -616,6 +620,10 @@ static const char* const yytname[] = {"$end", "annotation", "solve", YY_NULLPTR}; + +static const char* yysymbol_name(yysymbol_kind_t yysymbol) { + return yytname[yysymbol]; +} #endif #ifdef YYPRINT @@ -762,10 +770,10 @@ static const yytype_int8 yyr2[] = { 1, 3, 3, 2, 1, 1, 4, 3, 1, 3, 0, 6, 3, 1, 1, 1, 1, 3, 3, 1, 4, 3, 2, 3, 0, 3, 1, 3, 1, 1, 1, 4, 4, 3, 3, 4, 4}; +enum { YYENOMEM = -2 }; + #define yyerrok (yyerrstatus = 0) -#define yyclearin (yychar = YYEMPTY) -#define YYEMPTY (-2) -#define YYEOF 0 +#define yyclearin (yychar = ORFZ_EMPTY) #define YYACCEPT goto yyacceptlab #define YYABORT goto yyabortlab @@ -775,7 +783,7 @@ static const yytype_int8 yyr2[] = { #define YYBACKUP(Token, Value) \ do \ - if (yychar == YYEMPTY) { \ + if (yychar == ORFZ_EMPTY) { \ yychar = (Token); \ yylval = (Value); \ YYPOPSTACK(yylen); \ @@ -788,9 +796,9 @@ static const yytype_int8 yyr2[] = { } \ while (0) -/* Error token number */ -#define YYTERROR 1 -#define YYERRCODE 256 +/* Backward compatibility with an undocumented macro. + Use ORFZ_error or ORFZ_UNDEF. */ +#define YYERRCODE ORFZ_UNDEF /* Enable debugging if requested. */ #if ORFZ_DEBUG @@ -810,11 +818,11 @@ static const yytype_int8 yyr2[] = { #define YY_LOCATION_PRINT(File, Loc) ((void)0) #endif -#define YY_SYMBOL_PRINT(Title, Type, Value, Location) \ +#define YY_SYMBOL_PRINT(Title, Kind, Value, Location) \ do { \ if (yydebug) { \ YYFPRINTF(stderr, "%s ", Title); \ - yy_symbol_print(stderr, Type, Value, context, model, ok, scanner); \ + yy_symbol_print(stderr, Kind, Value, context, model, ok, scanner); \ YYFPRINTF(stderr, "\n"); \ } \ } while (0) @@ -824,7 +832,7 @@ static const yytype_int8 yyr2[] = { `-----------------------------------*/ static void yy_symbol_value_print( - FILE* yyo, int yytype, YYSTYPE const* const yyvaluep, + FILE* yyo, yysymbol_kind_t yykind, YYSTYPE const* const yyvaluep, operations_research::fz::ParserContext* context, operations_research::fz::Model* model, bool* ok, void* scanner) { FILE* yyoutput = yyo; @@ -835,10 +843,10 @@ static void yy_symbol_value_print( YYUSE(scanner); if (!yyvaluep) return; #ifdef YYPRINT - if (yytype < YYNTOKENS) YYPRINT(yyo, yytoknum[yytype], *yyvaluep); + if (yykind < YYNTOKENS) YYPRINT(yyo, yytoknum[yykind], *yyvaluep); #endif YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN - YYUSE(yytype); + YYUSE(yykind); YY_IGNORE_MAYBE_UNINITIALIZED_END } @@ -846,15 +854,15 @@ static void yy_symbol_value_print( | Print this symbol on YYO. | `---------------------------*/ -static void yy_symbol_print(FILE* yyo, int yytype, +static void yy_symbol_print(FILE* yyo, yysymbol_kind_t yykind, YYSTYPE const* const yyvaluep, operations_research::fz::ParserContext* context, operations_research::fz::Model* model, bool* ok, void* scanner) { - YYFPRINTF(yyo, "%s %s (", yytype < YYNTOKENS ? "token" : "nterm", - yytname[yytype]); + YYFPRINTF(yyo, "%s %s (", yykind < YYNTOKENS ? "token" : "nterm", + yysymbol_name(yykind)); - yy_symbol_value_print(yyo, yytype, yyvaluep, context, model, ok, scanner); + yy_symbol_value_print(yyo, yykind, yyvaluep, context, model, ok, scanner); YYFPRINTF(yyo, ")"); } @@ -893,7 +901,7 @@ static void yy_reduce_print(yy_state_t* yyssp, YYSTYPE* yyvsp, int yyrule, /* The symbols being reduced. */ for (yyi = 0; yyi < yynrhs; yyi++) { YYFPRINTF(stderr, " $%d = ", yyi + 1); - yy_symbol_print(stderr, yystos[+yyssp[yyi + 1 - yynrhs]], + yy_symbol_print(stderr, YY_ACCESSING_SYMBOL(+yyssp[yyi + 1 - yynrhs]), &yyvsp[(yyi + 1) - (yynrhs)], context, model, ok, scanner); YYFPRINTF(stderr, "\n"); } @@ -909,8 +917,8 @@ static void yy_reduce_print(yy_state_t* yyssp, YYSTYPE* yyvsp, int yyrule, multiple parsers can coexist. */ int yydebug; #else /* !ORFZ_DEBUG */ -#define YYDPRINTF(Args) -#define YY_SYMBOL_PRINT(Title, Type, Value, Location) +#define YYDPRINTF(Args) ((void)0) +#define YY_SYMBOL_PRINT(Title, Kind, Value, Location) #define YY_STACK_PRINT(Bottom, Top) #define YY_REDUCE_PRINT(Rule) #endif /* !ORFZ_DEBUG */ @@ -931,7 +939,46 @@ int yydebug; #define YYMAXDEPTH 10000 #endif -#if YYERROR_VERBOSE +/* Context of a parse error. */ +typedef struct { + yy_state_t* yyssp; + yysymbol_kind_t yytoken; +} yypcontext_t; + +/* Put in YYARG at most YYARGN of the expected tokens given the + current YYCTX, and return the number of tokens stored in YYARG. If + YYARG is null, return the number of expected tokens (guaranteed to + be less than YYNTOKENS). Return YYENOMEM on memory exhaustion. + Return 0 if there are more than YYARGN expected tokens, yet fill + YYARG up to YYARGN. */ +static int yypcontext_expected_tokens(const yypcontext_t* yyctx, + yysymbol_kind_t yyarg[], int yyargn) { + /* Actual size of YYARG. */ + int yycount = 0; + int yyn = yypact[+*yyctx->yyssp]; + if (!yypact_value_is_default(yyn)) { + /* Start YYX at -YYN if negative to avoid negative indexes in + YYCHECK. In other words, skip the first -YYN actions for + this state because they are default actions. */ + int yyxbegin = yyn < 0 ? -yyn : 0; + /* Stay within bounds of both yycheck and yytname. */ + int yychecklim = YYLAST - yyn + 1; + int yyxend = yychecklim < YYNTOKENS ? yychecklim : YYNTOKENS; + int yyx; + for (yyx = yyxbegin; yyx < yyxend; ++yyx) + if (yycheck[yyx + yyn] == yyx && yyx != YYSYMBOL_YYerror && + !yytable_value_is_error(yytable[yyx + yyn])) { + if (!yyarg) + ++yycount; + else if (yycount == yyargn) + return 0; + else + yyarg[yycount++] = YY_CAST(yysymbol_kind_t, yyx); + } + } + if (yyarg && yycount == 0 && 0 < yyargn) yyarg[0] = YYSYMBOL_YYEMPTY; + return yycount; +} #ifndef yystrlen #if defined __GLIBC__ && defined _STRING_H @@ -975,7 +1022,6 @@ static YYPTRDIFF_T yytnamerr(char* yyres, const char* yystr) { if (*yystr == '"') { YYPTRDIFF_T yyn = 0; char const* yyp = yystr; - for (;;) switch (*++yyp) { case '\'': case ',': @@ -1007,27 +1053,10 @@ static YYPTRDIFF_T yytnamerr(char* yyres, const char* yystr) { } #endif -/* Copy into *YYMSG, which is of size *YYMSG_ALLOC, an error message - about the unexpected token YYTOKEN for the state stack whose top is - YYSSP. - - Return 0 if *YYMSG was successfully written. Return 1 if *YYMSG is - not large enough to hold the message. In that case, also set - *YYMSG_ALLOC to the required number of bytes. Return 2 if the - required number of bytes is too large to store. */ -static int yysyntax_error(YYPTRDIFF_T* yymsg_alloc, char** yymsg, - yy_state_t* yyssp, int yytoken) { - enum { YYERROR_VERBOSE_ARGS_MAXIMUM = 5 }; - /* Internationalized format string. */ - const char* yyformat = YY_NULLPTR; - /* Arguments of yyformat: reported tokens (one for the "unexpected", - one per "expected"). */ - char const* yyarg[YYERROR_VERBOSE_ARGS_MAXIMUM]; +static int yy_syntax_error_arguments(const yypcontext_t* yyctx, + yysymbol_kind_t yyarg[], int yyargn) { /* Actual size of YYARG. */ int yycount = 0; - /* Cumulated lengths of YYARG. */ - YYPTRDIFF_T yysize = 0; - /* There are many possibilities here to consider: - If this state is a consistent state with a default action, then the only way this function was invoked is if the default action @@ -1051,40 +1080,42 @@ static int yysyntax_error(YYPTRDIFF_T* yymsg_alloc, char** yymsg, one exception: it will still contain any token that will not be accepted due to an error action in a later state. */ - if (yytoken != YYEMPTY) { - int yyn = yypact[+*yyssp]; - YYPTRDIFF_T yysize0 = yytnamerr(YY_NULLPTR, yytname[yytoken]); - yysize = yysize0; - yyarg[yycount++] = yytname[yytoken]; - if (!yypact_value_is_default(yyn)) { - /* Start YYX at -YYN if negative to avoid negative indexes in - YYCHECK. In other words, skip the first -YYN actions for - this state because they are default actions. */ - int yyxbegin = yyn < 0 ? -yyn : 0; - /* Stay within bounds of both yycheck and yytname. */ - int yychecklim = YYLAST - yyn + 1; - int yyxend = yychecklim < YYNTOKENS ? yychecklim : YYNTOKENS; - int yyx; - - for (yyx = yyxbegin; yyx < yyxend; ++yyx) - if (yycheck[yyx + yyn] == yyx && yyx != YYTERROR && - !yytable_value_is_error(yytable[yyx + yyn])) { - if (yycount == YYERROR_VERBOSE_ARGS_MAXIMUM) { - yycount = 1; - yysize = yysize0; - break; - } - yyarg[yycount++] = yytname[yyx]; - { - YYPTRDIFF_T yysize1 = yysize + yytnamerr(YY_NULLPTR, yytname[yyx]); - if (yysize <= yysize1 && yysize1 <= YYSTACK_ALLOC_MAXIMUM) - yysize = yysize1; - else - return 2; - } - } - } + if (yyctx->yytoken != YYSYMBOL_YYEMPTY) { + int yyn; + if (yyarg) yyarg[yycount] = yyctx->yytoken; + ++yycount; + yyn = yypcontext_expected_tokens(yyctx, yyarg ? yyarg + 1 : yyarg, + yyargn - 1); + if (yyn == YYENOMEM) + return YYENOMEM; + else + yycount += yyn; } + return yycount; +} + +/* Copy into *YYMSG, which is of size *YYMSG_ALLOC, an error message + about the unexpected token YYTOKEN for the state stack whose top is + YYSSP. + + Return 0 if *YYMSG was successfully written. Return -1 if *YYMSG is + not large enough to hold the message. In that case, also set + *YYMSG_ALLOC to the required number of bytes. Return YYENOMEM if the + required number of bytes is too large to store. */ +static int yysyntax_error(YYPTRDIFF_T* yymsg_alloc, char** yymsg, + const yypcontext_t* yyctx) { + enum { YYARGS_MAX = 5 }; + /* Internationalized format string. */ + const char* yyformat = YY_NULLPTR; + /* Arguments of yyformat: reported tokens (one for the "unexpected", + one per "expected"). */ + yysymbol_kind_t yyarg[YYARGS_MAX]; + /* Cumulated lengths of YYARG. */ + YYPTRDIFF_T yysize = 0; + + /* Actual size of YYARG. */ + int yycount = yy_syntax_error_arguments(yyctx, yyarg, YYARGS_MAX); + if (yycount == YYENOMEM) return YYENOMEM; switch (yycount) { #define YYCASE_(N, S) \ @@ -1103,21 +1134,25 @@ static int yysyntax_error(YYPTRDIFF_T* yymsg_alloc, char** yymsg, #undef YYCASE_ } + /* Compute error message size. Don't count the "%s"s, but reserve + room for the terminator. */ + yysize = yystrlen(yyformat) - 2 * yycount + 1; { - /* Don't count the "%s"s in the final size, but reserve room for - the terminator. */ - YYPTRDIFF_T yysize1 = yysize + (yystrlen(yyformat) - 2 * yycount) + 1; - if (yysize <= yysize1 && yysize1 <= YYSTACK_ALLOC_MAXIMUM) - yysize = yysize1; - else - return 2; + int yyi; + for (yyi = 0; yyi < yycount; ++yyi) { + YYPTRDIFF_T yysize1 = yysize + yytnamerr(YY_NULLPTR, yytname[yyarg[yyi]]); + if (yysize <= yysize1 && yysize1 <= YYSTACK_ALLOC_MAXIMUM) + yysize = yysize1; + else + return YYENOMEM; + } } if (*yymsg_alloc < yysize) { *yymsg_alloc = 2 * yysize; if (!(yysize <= *yymsg_alloc && *yymsg_alloc <= YYSTACK_ALLOC_MAXIMUM)) *yymsg_alloc = YYSTACK_ALLOC_MAXIMUM; - return 1; + return -1; } /* Avoid sprintf, as that infringes on the user's name space. @@ -1128,7 +1163,7 @@ static int yysyntax_error(YYPTRDIFF_T* yymsg_alloc, char** yymsg, int yyi = 0; while ((*yyp = *yyformat) != '\0') if (*yyp == '%' && yyformat[1] == 's' && yyi < yycount) { - yyp += yytnamerr(yyp, yyarg[yyi++]); + yyp += yytnamerr(yyp, yytname[yyarg[yyi++]]); yyformat += 2; } else { ++yyp; @@ -1137,13 +1172,13 @@ static int yysyntax_error(YYPTRDIFF_T* yymsg_alloc, char** yymsg, } return 0; } -#endif /* YYERROR_VERBOSE */ /*-----------------------------------------------. | Release the memory associated to this symbol. | `-----------------------------------------------*/ -static void yydestruct(const char* yymsg, int yytype, YYSTYPE* yyvaluep, +static void yydestruct(const char* yymsg, yysymbol_kind_t yykind, + YYSTYPE* yyvaluep, operations_research::fz::ParserContext* context, operations_research::fz::Model* model, bool* ok, void* scanner) { @@ -1153,10 +1188,10 @@ static void yydestruct(const char* yymsg, int yytype, YYSTYPE* yyvaluep, YYUSE(ok); YYUSE(scanner); if (!yymsg) yymsg = "Deleting"; - YY_SYMBOL_PRINT(yymsg, yytype, yyvaluep, yylocationp); + YY_SYMBOL_PRINT(yymsg, yykind, yyvaluep, yylocationp); YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN - YYUSE(yytype); + YYUSE(yykind); YY_IGNORE_MAYBE_UNINITIALIZED_END } @@ -1166,7 +1201,7 @@ static void yydestruct(const char* yymsg, int yytype, YYSTYPE* yyvaluep, int yyparse(operations_research::fz::ParserContext* context, operations_research::fz::Model* model, bool* ok, void* scanner) { - /* The lookahead symbol. */ + /* Lookahead token kind. */ int yychar; /* The semantic value of the lookahead symbol. */ @@ -1176,45 +1211,41 @@ int yyparse(operations_research::fz::ParserContext* context, YYSTYPE yylval YY_INITIAL_VALUE(= yyval_default); /* Number of syntax errors so far. */ - int yynerrs; + int yynerrs = 0; - yy_state_fast_t yystate; + yy_state_fast_t yystate = 0; /* Number of tokens to shift before error messages enabled. */ - int yyerrstatus; + int yyerrstatus = 0; - /* The stacks and their tools: - 'yyss': related to states. - 'yyvs': related to semantic values. - - Refer to the stacks through separate pointers, to allow yyoverflow + /* Refer to the stacks through separate pointers, to allow yyoverflow to reallocate them elsewhere. */ - /* The state stack. */ + /* Their size. */ + YYPTRDIFF_T yystacksize = YYINITDEPTH; + + /* The state stack: array, bottom, top. */ yy_state_t yyssa[YYINITDEPTH]; - yy_state_t* yyss; - yy_state_t* yyssp; + yy_state_t* yyss = yyssa; + yy_state_t* yyssp = yyss; - /* The semantic value stack. */ + /* The semantic value stack: array, bottom, top. */ YYSTYPE yyvsa[YYINITDEPTH]; - YYSTYPE* yyvs; - YYSTYPE* yyvsp; - - YYPTRDIFF_T yystacksize; + YYSTYPE* yyvs = yyvsa; + YYSTYPE* yyvsp = yyvs; int yyn; + /* The return value of yyparse. */ int yyresult; - /* Lookahead token as an internal (translated) token number. */ - int yytoken = 0; + /* Lookahead symbol kind. */ + yysymbol_kind_t yytoken = YYSYMBOL_YYEMPTY; /* The variables used to return semantic value and location from the action routines. */ YYSTYPE yyval; -#if YYERROR_VERBOSE /* Buffer for error messages, and its allocated size. */ char yymsgbuf[128]; char* yymsg = yymsgbuf; YYPTRDIFF_T yymsg_alloc = sizeof yymsgbuf; -#endif #define YYPOPSTACK(N) (yyvsp -= (N), yyssp -= (N)) @@ -1222,16 +1253,9 @@ int yyparse(operations_research::fz::ParserContext* context, Keep to zero when no symbol should be popped. */ int yylen = 0; - yyssp = yyss = yyssa; - yyvsp = yyvs = yyvsa; - yystacksize = YYINITDEPTH; - YYDPRINTF((stderr, "Starting parse\n")); - yystate = 0; - yyerrstatus = 0; - yynerrs = 0; - yychar = YYEMPTY; /* Cause a token to be read. */ + yychar = ORFZ_EMPTY; /* Cause a token to be read. */ goto yysetstate; /*------------------------------------------------------------. @@ -1251,6 +1275,7 @@ yysetstate: YY_IGNORE_USELESS_CAST_BEGIN *yyssp = YY_CAST(yy_state_t, yystate); YY_IGNORE_USELESS_CAST_END + YY_STACK_PRINT(yyss, yyssp); if (yyss + yystacksize - 1 <= yyssp) #if !defined yyoverflow && !defined YYSTACK_RELOCATE @@ -1325,15 +1350,24 @@ yybackup: /* Not known => get a lookahead token if don't already have one. */ - /* YYCHAR is either YYEMPTY or YYEOF or a valid lookahead symbol. */ - if (yychar == YYEMPTY) { - YYDPRINTF((stderr, "Reading a token: ")); + /* YYCHAR is either empty, or end-of-input, or a valid lookahead. */ + if (yychar == ORFZ_EMPTY) { + YYDPRINTF((stderr, "Reading a token\n")); yychar = yylex(&yylval, scanner); } - if (yychar <= YYEOF) { - yychar = yytoken = YYEOF; + if (yychar <= ORFZ_EOF) { + yychar = ORFZ_EOF; + yytoken = YYSYMBOL_YYEOF; YYDPRINTF((stderr, "Now at end of input.\n")); + } else if (yychar == ORFZ_error) { + /* The scanner already issued an error message, process directly + to error recovery. But do not keep the error token as + lookahead, it is too special and may lead us to an endless + loop in error recovery. */ + yychar = ORFZ_UNDEF; + yytoken = YYSYMBOL_YYerror; + goto yyerrlab1; } else { yytoken = YYTRANSLATE(yychar); YY_SYMBOL_PRINT("Next token is", yytoken, &yylval, &yylloc); @@ -1362,7 +1396,7 @@ yybackup: YY_IGNORE_MAYBE_UNINITIALIZED_END /* Discard the shifted token. */ - yychar = YYEMPTY; + yychar = ORFZ_EMPTY; goto yynewstate; /*-----------------------------------------------------------. @@ -1392,15 +1426,16 @@ yyreduce: YY_REDUCE_PRINT(yyn); switch (yyn) { - case 4: + case 4: /* predicates: predicates error ';' */ #line 114 "./ortools/flatzinc/parser.yy" { yyerrok; } -#line 1528 "./ortools/flatzinc/parser.tab.cc" +#line 1581 "./ortools/flatzinc/parser.tab.cc" break; - case 19: + case 19: /* variable_or_constant_declaration: domain ':' IDENTIFIER + annotations '=' const_literal */ #line 149 "./ortools/flatzinc/parser.yy" { // Declaration of a (named) constant: we simply register it in the @@ -1420,10 +1455,12 @@ yyreduce: } delete annotations; } -#line 1552 "./ortools/flatzinc/parser.tab.cc" +#line 1605 "./ortools/flatzinc/parser.tab.cc" break; - case 20: + case 20: /* variable_or_constant_declaration: ARRAY '[' IVALUE DOTDOT IVALUE + ']' OF int_domain ':' IDENTIFIER annotations '=' '[' integers + ']' */ #line 169 "./ortools/flatzinc/parser.yy" { std::vector* const annotations = (yyvsp[-4].annotations); @@ -1440,10 +1477,11 @@ yyreduce: delete assignments; delete annotations; } -#line 1571 "./ortools/flatzinc/parser.tab.cc" +#line 1624 "./ortools/flatzinc/parser.tab.cc" break; - case 21: + case 21: /* variable_or_constant_declaration: ARRAY '[' IVALUE DOTDOT IVALUE + ']' OF int_domain ':' IDENTIFIER annotations '=' '[' ']' */ #line 184 "./ortools/flatzinc/parser.yy" { std::vector* const annotations = (yyvsp[-3].annotations); @@ -1456,10 +1494,12 @@ yyreduce: context->integer_array_map[identifier] = std::vector(); delete annotations; } -#line 1586 "./ortools/flatzinc/parser.tab.cc" +#line 1639 "./ortools/flatzinc/parser.tab.cc" break; - case 22: + case 22: /* variable_or_constant_declaration: ARRAY '[' IVALUE DOTDOT IVALUE + ']' OF float_domain ':' IDENTIFIER annotations '=' '[' floats + ']' */ #line 195 "./ortools/flatzinc/parser.yy" { std::vector* const annotations = (yyvsp[-4].annotations); @@ -1476,10 +1516,11 @@ yyreduce: delete assignments; delete annotations; } -#line 1605 "./ortools/flatzinc/parser.tab.cc" +#line 1658 "./ortools/flatzinc/parser.tab.cc" break; - case 23: + case 23: /* variable_or_constant_declaration: ARRAY '[' IVALUE DOTDOT IVALUE + ']' OF float_domain ':' IDENTIFIER annotations '=' '[' ']' */ #line 210 "./ortools/flatzinc/parser.yy" { std::vector* const annotations = (yyvsp[-3].annotations); @@ -1492,10 +1533,12 @@ yyreduce: context->float_array_map[identifier] = std::vector(); delete annotations; } -#line 1620 "./ortools/flatzinc/parser.tab.cc" +#line 1673 "./ortools/flatzinc/parser.tab.cc" break; - case 24: + case 24: /* variable_or_constant_declaration: ARRAY '[' IVALUE DOTDOT IVALUE + ']' OF set_domain ':' IDENTIFIER annotations '=' '[' + const_literals ']' */ #line 221 "./ortools/flatzinc/parser.yy" { // Declaration of a (named) constant array: See rule above. @@ -1524,10 +1567,11 @@ yyreduce: delete assignments; delete annotations; } -#line 1650 "./ortools/flatzinc/parser.tab.cc" +#line 1703 "./ortools/flatzinc/parser.tab.cc" break; - case 25: + case 25: /* variable_or_constant_declaration: VAR domain ':' IDENTIFIER + annotations optional_var_or_value */ #line 246 "./ortools/flatzinc/parser.yy" { // Declaration of a variable. If it's unassigned or assigned to a @@ -1561,10 +1605,12 @@ yyreduce: } delete annotations; } -#line 1688 "./ortools/flatzinc/parser.tab.cc" +#line 1741 "./ortools/flatzinc/parser.tab.cc" break; - case 26: + case 26: /* variable_or_constant_declaration: ARRAY '[' IVALUE DOTDOT IVALUE + ']' OF VAR domain ':' IDENTIFIER annotations + optional_var_or_value_array */ #line 280 "./ortools/flatzinc/parser.yy" { // Declaration of a "variable array": these is exactly like N simple @@ -1635,77 +1681,77 @@ yyreduce: delete annotations; } } -#line 1760 "./ortools/flatzinc/parser.tab.cc" +#line 1813 "./ortools/flatzinc/parser.tab.cc" break; - case 27: + case 27: /* optional_var_or_value: '=' var_or_value */ #line 349 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value) = (yyvsp[0].var_or_value); } -#line 1766 "./ortools/flatzinc/parser.tab.cc" +#line 1819 "./ortools/flatzinc/parser.tab.cc" break; - case 28: + case 28: /* optional_var_or_value: %empty */ #line 350 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value) = VariableRefOrValue::Undefined(); } -#line 1772 "./ortools/flatzinc/parser.tab.cc" +#line 1825 "./ortools/flatzinc/parser.tab.cc" break; - case 29: + case 29: /* optional_var_or_value_array: '=' '[' var_or_value_array ']' */ #line 353 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value_array) = (yyvsp[-1].var_or_value_array); } -#line 1778 "./ortools/flatzinc/parser.tab.cc" +#line 1831 "./ortools/flatzinc/parser.tab.cc" break; - case 30: + case 30: /* optional_var_or_value_array: '=' '[' ']' */ #line 354 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value_array) = nullptr; } -#line 1784 "./ortools/flatzinc/parser.tab.cc" +#line 1837 "./ortools/flatzinc/parser.tab.cc" break; - case 31: + case 31: /* optional_var_or_value_array: %empty */ #line 355 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value_array) = nullptr; } -#line 1790 "./ortools/flatzinc/parser.tab.cc" +#line 1843 "./ortools/flatzinc/parser.tab.cc" break; - case 32: + case 32: /* var_or_value_array: var_or_value_array ',' var_or_value */ #line 358 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value_array) = (yyvsp[-2].var_or_value_array); (yyval.var_or_value_array)->PushBack((yyvsp[0].var_or_value)); } -#line 1799 "./ortools/flatzinc/parser.tab.cc" +#line 1852 "./ortools/flatzinc/parser.tab.cc" break; - case 33: + case 33: /* var_or_value_array: var_or_value */ #line 362 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value_array) = new VariableRefOrValueArray(); (yyval.var_or_value_array)->PushBack((yyvsp[0].var_or_value)); } -#line 1808 "./ortools/flatzinc/parser.tab.cc" +#line 1861 "./ortools/flatzinc/parser.tab.cc" break; - case 34: + case 34: /* var_or_value: IVALUE */ #line 368 "./ortools/flatzinc/parser.yy" { (yyval.var_or_value) = VariableRefOrValue::Value((yyvsp[0].integer_value)); } -#line 1814 "./ortools/flatzinc/parser.tab.cc" +#line 1867 "./ortools/flatzinc/parser.tab.cc" break; - case 35: + case 35: /* var_or_value: IDENTIFIER */ #line 369 "./ortools/flatzinc/parser.yy" { // A reference to an existing integer constant or variable. @@ -1722,10 +1768,10 @@ yyreduce: *ok = false; } } -#line 1832 "./ortools/flatzinc/parser.tab.cc" +#line 1885 "./ortools/flatzinc/parser.tab.cc" break; - case 36: + case 36: /* var_or_value: IDENTIFIER '[' IVALUE ']' */ #line 382 "./ortools/flatzinc/parser.yy" { // A given element of an existing constant array or variable array. @@ -1743,70 +1789,70 @@ yyreduce: *ok = false; } } -#line 1853 "./ortools/flatzinc/parser.tab.cc" +#line 1906 "./ortools/flatzinc/parser.tab.cc" break; - case 37: + case 37: /* int_domain: TOKEN_BOOL */ #line 400 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::Boolean(); } -#line 1859 "./ortools/flatzinc/parser.tab.cc" +#line 1912 "./ortools/flatzinc/parser.tab.cc" break; - case 38: + case 38: /* int_domain: TOKEN_INT */ #line 401 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::AllInt64(); } -#line 1865 "./ortools/flatzinc/parser.tab.cc" +#line 1918 "./ortools/flatzinc/parser.tab.cc" break; - case 39: + case 39: /* int_domain: IVALUE DOTDOT IVALUE */ #line 402 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::Interval((yyvsp[-2].integer_value), (yyvsp[0].integer_value)); } -#line 1871 "./ortools/flatzinc/parser.tab.cc" +#line 1924 "./ortools/flatzinc/parser.tab.cc" break; - case 40: + case 40: /* int_domain: '{' integers '}' */ #line 403 "./ortools/flatzinc/parser.yy" { CHECK((yyvsp[-1].integers) != nullptr); (yyval.domain) = Domain::IntegerList(std::move(*(yyvsp[-1].integers))); delete (yyvsp[-1].integers); } -#line 1881 "./ortools/flatzinc/parser.tab.cc" +#line 1934 "./ortools/flatzinc/parser.tab.cc" break; - case 41: + case 41: /* set_domain: SET OF TOKEN_BOOL */ #line 410 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::SetOfBoolean(); } -#line 1887 "./ortools/flatzinc/parser.tab.cc" +#line 1940 "./ortools/flatzinc/parser.tab.cc" break; - case 42: + case 42: /* set_domain: SET OF TOKEN_INT */ #line 411 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::SetOfAllInt64(); } -#line 1893 "./ortools/flatzinc/parser.tab.cc" +#line 1946 "./ortools/flatzinc/parser.tab.cc" break; - case 43: + case 43: /* set_domain: SET OF IVALUE DOTDOT IVALUE */ #line 412 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::SetOfInterval((yyvsp[-2].integer_value), (yyvsp[0].integer_value)); } -#line 1899 "./ortools/flatzinc/parser.tab.cc" +#line 1952 "./ortools/flatzinc/parser.tab.cc" break; - case 44: + case 44: /* set_domain: SET OF '{' integers '}' */ #line 413 "./ortools/flatzinc/parser.yy" { CHECK((yyvsp[-1].integers) != nullptr); @@ -1814,224 +1860,225 @@ yyreduce: Domain::SetOfIntegerList(std::move(*(yyvsp[-1].integers))); delete (yyvsp[-1].integers); } -#line 1909 "./ortools/flatzinc/parser.tab.cc" +#line 1962 "./ortools/flatzinc/parser.tab.cc" break; - case 45: + case 45: /* float_domain: TOKEN_FLOAT */ #line 420 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::AllInt64(); } -#line 1915 "./ortools/flatzinc/parser.tab.cc" +#line 1968 "./ortools/flatzinc/parser.tab.cc" break; - case 46: + case 46: /* float_domain: DVALUE DOTDOT DVALUE */ #line 421 "./ortools/flatzinc/parser.yy" { const int64 lb = ConvertAsIntegerOrDie((yyvsp[-2].double_value)); const int64 ub = ConvertAsIntegerOrDie((yyvsp[0].double_value)); (yyval.domain) = Domain::Interval(lb, ub); } -#line 1925 "./ortools/flatzinc/parser.tab.cc" +#line 1978 "./ortools/flatzinc/parser.tab.cc" break; - case 47: + case 47: /* domain: int_domain */ #line 428 "./ortools/flatzinc/parser.yy" { (yyval.domain) = (yyvsp[0].domain); } -#line 1931 "./ortools/flatzinc/parser.tab.cc" +#line 1984 "./ortools/flatzinc/parser.tab.cc" break; - case 48: + case 48: /* domain: set_domain */ #line 429 "./ortools/flatzinc/parser.yy" { (yyval.domain) = (yyvsp[0].domain); } -#line 1937 "./ortools/flatzinc/parser.tab.cc" +#line 1990 "./ortools/flatzinc/parser.tab.cc" break; - case 49: + case 49: /* domain: float_domain */ #line 430 "./ortools/flatzinc/parser.yy" { (yyval.domain) = (yyvsp[0].domain); } -#line 1943 "./ortools/flatzinc/parser.tab.cc" +#line 1996 "./ortools/flatzinc/parser.tab.cc" break; - case 50: + case 50: /* integers: integers ',' integer */ #line 433 "./ortools/flatzinc/parser.yy" { (yyval.integers) = (yyvsp[-2].integers); (yyval.integers)->emplace_back((yyvsp[0].integer_value)); } -#line 1949 "./ortools/flatzinc/parser.tab.cc" +#line 2002 "./ortools/flatzinc/parser.tab.cc" break; - case 51: + case 51: /* integers: integer */ #line 434 "./ortools/flatzinc/parser.yy" { (yyval.integers) = new std::vector(); (yyval.integers)->emplace_back((yyvsp[0].integer_value)); } -#line 1955 "./ortools/flatzinc/parser.tab.cc" +#line 2008 "./ortools/flatzinc/parser.tab.cc" break; - case 52: + case 52: /* integer: IVALUE */ #line 437 "./ortools/flatzinc/parser.yy" { (yyval.integer_value) = (yyvsp[0].integer_value); } -#line 1961 "./ortools/flatzinc/parser.tab.cc" +#line 2014 "./ortools/flatzinc/parser.tab.cc" break; - case 53: + case 53: /* integer: IDENTIFIER */ #line 438 "./ortools/flatzinc/parser.yy" { (yyval.integer_value) = gtl::FindOrDie(context->integer_map, (yyvsp[0].string_value)); } -#line 1967 "./ortools/flatzinc/parser.tab.cc" +#line 2020 "./ortools/flatzinc/parser.tab.cc" break; - case 54: + case 54: /* integer: IDENTIFIER '[' IVALUE ']' */ #line 439 "./ortools/flatzinc/parser.yy" { (yyval.integer_value) = Lookup( gtl::FindOrDie(context->integer_array_map, (yyvsp[-3].string_value)), (yyvsp[-1].integer_value)); } -#line 1975 "./ortools/flatzinc/parser.tab.cc" +#line 2028 "./ortools/flatzinc/parser.tab.cc" break; - case 55: + case 55: /* floats: floats ',' float */ #line 444 "./ortools/flatzinc/parser.yy" { (yyval.doubles) = (yyvsp[-2].doubles); (yyval.doubles)->emplace_back((yyvsp[0].double_value)); } -#line 1981 "./ortools/flatzinc/parser.tab.cc" +#line 2034 "./ortools/flatzinc/parser.tab.cc" break; - case 56: + case 56: /* floats: float */ #line 445 "./ortools/flatzinc/parser.yy" { (yyval.doubles) = new std::vector(); (yyval.doubles)->emplace_back((yyvsp[0].double_value)); } -#line 1987 "./ortools/flatzinc/parser.tab.cc" +#line 2040 "./ortools/flatzinc/parser.tab.cc" break; - case 57: + case 57: /* float: DVALUE */ #line 448 "./ortools/flatzinc/parser.yy" { (yyval.double_value) = (yyvsp[0].double_value); } -#line 1993 "./ortools/flatzinc/parser.tab.cc" +#line 2046 "./ortools/flatzinc/parser.tab.cc" break; - case 58: + case 58: /* float: IDENTIFIER */ #line 449 "./ortools/flatzinc/parser.yy" { (yyval.double_value) = gtl::FindOrDie(context->float_map, (yyvsp[0].string_value)); } -#line 1999 "./ortools/flatzinc/parser.tab.cc" +#line 2052 "./ortools/flatzinc/parser.tab.cc" break; - case 59: + case 59: /* float: IDENTIFIER '[' IVALUE ']' */ #line 450 "./ortools/flatzinc/parser.yy" { (yyval.double_value) = Lookup( gtl::FindOrDie(context->float_array_map, (yyvsp[-3].string_value)), (yyvsp[-1].integer_value)); } -#line 2007 "./ortools/flatzinc/parser.tab.cc" +#line 2060 "./ortools/flatzinc/parser.tab.cc" break; - case 60: + case 60: /* const_literal: IVALUE */ #line 455 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::IntegerValue((yyvsp[0].integer_value)); } -#line 2013 "./ortools/flatzinc/parser.tab.cc" +#line 2066 "./ortools/flatzinc/parser.tab.cc" break; - case 61: + case 61: /* const_literal: IVALUE DOTDOT IVALUE */ #line 456 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::Interval((yyvsp[-2].integer_value), (yyvsp[0].integer_value)); } -#line 2019 "./ortools/flatzinc/parser.tab.cc" +#line 2072 "./ortools/flatzinc/parser.tab.cc" break; - case 62: + case 62: /* const_literal: '{' integers '}' */ #line 457 "./ortools/flatzinc/parser.yy" { CHECK((yyvsp[-1].integers) != nullptr); (yyval.domain) = Domain::IntegerList(std::move(*(yyvsp[-1].integers))); delete (yyvsp[-1].integers); } -#line 2029 "./ortools/flatzinc/parser.tab.cc" +#line 2082 "./ortools/flatzinc/parser.tab.cc" break; - case 63: + case 63: /* const_literal: '{' '}' */ #line 462 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::EmptyDomain(); } -#line 2035 "./ortools/flatzinc/parser.tab.cc" +#line 2088 "./ortools/flatzinc/parser.tab.cc" break; - case 64: + case 64: /* const_literal: DVALUE */ #line 463 "./ortools/flatzinc/parser.yy" { CHECK_EQ(std::round((yyvsp[0].double_value)), (yyvsp[0].double_value)); (yyval.domain) = Domain::IntegerValue(static_cast((yyvsp[0].double_value))); } -#line 2044 "./ortools/flatzinc/parser.tab.cc" +#line 2097 "./ortools/flatzinc/parser.tab.cc" break; - case 65: + case 65: /* const_literal: IDENTIFIER */ #line 467 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::IntegerValue( gtl::FindOrDie(context->integer_map, (yyvsp[0].string_value))); } -#line 2050 "./ortools/flatzinc/parser.tab.cc" +#line 2103 "./ortools/flatzinc/parser.tab.cc" break; - case 66: + case 66: /* const_literal: IDENTIFIER '[' IVALUE ']' */ #line 468 "./ortools/flatzinc/parser.yy" { (yyval.domain) = Domain::IntegerValue(Lookup( gtl::FindOrDie(context->integer_array_map, (yyvsp[-3].string_value)), (yyvsp[-1].integer_value))); } -#line 2059 "./ortools/flatzinc/parser.tab.cc" +#line 2112 "./ortools/flatzinc/parser.tab.cc" break; - case 67: + case 67: /* const_literals: const_literals ',' const_literal */ #line 474 "./ortools/flatzinc/parser.yy" { (yyval.domains) = (yyvsp[-2].domains); (yyval.domains)->emplace_back((yyvsp[0].domain)); } -#line 2068 "./ortools/flatzinc/parser.tab.cc" +#line 2121 "./ortools/flatzinc/parser.tab.cc" break; - case 68: + case 68: /* const_literals: const_literal */ #line 478 "./ortools/flatzinc/parser.yy" { (yyval.domains) = new std::vector(); (yyval.domains)->emplace_back((yyvsp[0].domain)); } -#line 2074 "./ortools/flatzinc/parser.tab.cc" +#line 2127 "./ortools/flatzinc/parser.tab.cc" break; - case 71: + case 71: /* constraint: CONSTRAINT IDENTIFIER '(' arguments ')' annotations + */ #line 488 "./ortools/flatzinc/parser.yy" { const std::string& identifier = (yyvsp[-4].string_value); @@ -2044,72 +2091,72 @@ yyreduce: delete annotations; delete (yyvsp[-2].args); } -#line 2089 "./ortools/flatzinc/parser.tab.cc" +#line 2142 "./ortools/flatzinc/parser.tab.cc" break; - case 72: + case 72: /* arguments: arguments ',' argument */ #line 500 "./ortools/flatzinc/parser.yy" { (yyval.args) = (yyvsp[-2].args); (yyval.args)->emplace_back((yyvsp[0].arg)); } -#line 2095 "./ortools/flatzinc/parser.tab.cc" +#line 2148 "./ortools/flatzinc/parser.tab.cc" break; - case 73: + case 73: /* arguments: argument */ #line 501 "./ortools/flatzinc/parser.yy" { (yyval.args) = new std::vector(); (yyval.args)->emplace_back((yyvsp[0].arg)); } -#line 2101 "./ortools/flatzinc/parser.tab.cc" +#line 2154 "./ortools/flatzinc/parser.tab.cc" break; - case 74: + case 74: /* argument: IVALUE */ #line 504 "./ortools/flatzinc/parser.yy" { (yyval.arg) = Argument::IntegerValue((yyvsp[0].integer_value)); } -#line 2107 "./ortools/flatzinc/parser.tab.cc" +#line 2160 "./ortools/flatzinc/parser.tab.cc" break; - case 75: + case 75: /* argument: DVALUE */ #line 505 "./ortools/flatzinc/parser.yy" { (yyval.arg) = Argument::IntegerValue( ConvertAsIntegerOrDie((yyvsp[0].double_value))); } -#line 2113 "./ortools/flatzinc/parser.tab.cc" +#line 2166 "./ortools/flatzinc/parser.tab.cc" break; - case 76: + case 76: /* argument: SVALUE */ #line 506 "./ortools/flatzinc/parser.yy" { (yyval.arg) = Argument::VoidArgument(); } -#line 2119 "./ortools/flatzinc/parser.tab.cc" +#line 2172 "./ortools/flatzinc/parser.tab.cc" break; - case 77: + case 77: /* argument: IVALUE DOTDOT IVALUE */ #line 507 "./ortools/flatzinc/parser.yy" { (yyval.arg) = Argument::Interval((yyvsp[-2].integer_value), (yyvsp[0].integer_value)); } -#line 2125 "./ortools/flatzinc/parser.tab.cc" +#line 2178 "./ortools/flatzinc/parser.tab.cc" break; - case 78: + case 78: /* argument: '{' integers '}' */ #line 508 "./ortools/flatzinc/parser.yy" { CHECK((yyvsp[-1].integers) != nullptr); (yyval.arg) = Argument::IntegerList(std::move(*(yyvsp[-1].integers))); delete (yyvsp[-1].integers); } -#line 2135 "./ortools/flatzinc/parser.tab.cc" +#line 2188 "./ortools/flatzinc/parser.tab.cc" break; - case 79: + case 79: /* argument: IDENTIFIER */ #line 513 "./ortools/flatzinc/parser.yy" { const std::string& id = (yyvsp[0].string_value); @@ -2148,10 +2195,10 @@ yyreduce: (yyval.arg) = Argument::DomainList(d); } } -#line 2171 "./ortools/flatzinc/parser.tab.cc" +#line 2224 "./ortools/flatzinc/parser.tab.cc" break; - case 80: + case 80: /* argument: IDENTIFIER '[' IVALUE ']' */ #line 544 "./ortools/flatzinc/parser.yy" { const std::string& id = (yyvsp[-3].string_value); @@ -2170,10 +2217,10 @@ yyreduce: (yyval.arg) = Argument::FromDomain(d); } } -#line 2193 "./ortools/flatzinc/parser.tab.cc" +#line 2246 "./ortools/flatzinc/parser.tab.cc" break; - case 81: + case 81: /* argument: '[' var_or_value_array ']' */ #line 561 "./ortools/flatzinc/parser.yy" { VariableRefOrValueArray* const arguments = (yyvsp[-1].var_or_value_array); @@ -2201,18 +2248,18 @@ yyreduce: } delete arguments; } -#line 2223 "./ortools/flatzinc/parser.tab.cc" +#line 2276 "./ortools/flatzinc/parser.tab.cc" break; - case 82: + case 82: /* argument: '[' ']' */ #line 586 "./ortools/flatzinc/parser.yy" { (yyval.arg) = Argument::VoidArgument(); } -#line 2231 "./ortools/flatzinc/parser.tab.cc" +#line 2284 "./ortools/flatzinc/parser.tab.cc" break; - case 83: + case 83: /* annotations: annotations COLONCOLON annotation */ #line 595 "./ortools/flatzinc/parser.yy" { (yyval.annotations) = (yyvsp[-2].annotations) != nullptr @@ -2220,61 +2267,61 @@ yyreduce: : new std::vector(); (yyval.annotations)->emplace_back((yyvsp[0].annotation)); } -#line 2240 "./ortools/flatzinc/parser.tab.cc" +#line 2293 "./ortools/flatzinc/parser.tab.cc" break; - case 84: + case 84: /* annotations: %empty */ #line 599 "./ortools/flatzinc/parser.yy" { (yyval.annotations) = nullptr; } -#line 2246 "./ortools/flatzinc/parser.tab.cc" +#line 2299 "./ortools/flatzinc/parser.tab.cc" break; - case 85: + case 85: /* annotation_arguments: annotation_arguments ',' annotation */ #line 602 "./ortools/flatzinc/parser.yy" { (yyval.annotations) = (yyvsp[-2].annotations); (yyval.annotations)->emplace_back((yyvsp[0].annotation)); } -#line 2252 "./ortools/flatzinc/parser.tab.cc" +#line 2305 "./ortools/flatzinc/parser.tab.cc" break; - case 86: + case 86: /* annotation_arguments: annotation */ #line 603 "./ortools/flatzinc/parser.yy" { (yyval.annotations) = new std::vector(); (yyval.annotations)->emplace_back((yyvsp[0].annotation)); } -#line 2258 "./ortools/flatzinc/parser.tab.cc" +#line 2311 "./ortools/flatzinc/parser.tab.cc" break; - case 87: + case 87: /* annotation: IVALUE DOTDOT IVALUE */ #line 606 "./ortools/flatzinc/parser.yy" { (yyval.annotation) = Annotation::Interval((yyvsp[-2].integer_value), (yyvsp[0].integer_value)); } -#line 2264 "./ortools/flatzinc/parser.tab.cc" +#line 2317 "./ortools/flatzinc/parser.tab.cc" break; - case 88: + case 88: /* annotation: IVALUE */ #line 607 "./ortools/flatzinc/parser.yy" { (yyval.annotation) = Annotation::IntegerValue((yyvsp[0].integer_value)); } -#line 2270 "./ortools/flatzinc/parser.tab.cc" +#line 2323 "./ortools/flatzinc/parser.tab.cc" break; - case 89: + case 89: /* annotation: SVALUE */ #line 608 "./ortools/flatzinc/parser.yy" { (yyval.annotation) = Annotation::String((yyvsp[0].string_value)); } -#line 2276 "./ortools/flatzinc/parser.tab.cc" +#line 2329 "./ortools/flatzinc/parser.tab.cc" break; - case 90: + case 90: /* annotation: IDENTIFIER */ #line 609 "./ortools/flatzinc/parser.yy" { const std::string& id = (yyvsp[0].string_value); @@ -2288,10 +2335,10 @@ yyreduce: (yyval.annotation) = Annotation::Identifier(id); } } -#line 2291 "./ortools/flatzinc/parser.tab.cc" +#line 2344 "./ortools/flatzinc/parser.tab.cc" break; - case 91: + case 91: /* annotation: IDENTIFIER '(' annotation_arguments ')' */ #line 619 "./ortools/flatzinc/parser.yy" { std::vector* const annotations = (yyvsp[-1].annotations); @@ -2303,10 +2350,10 @@ yyreduce: (yyval.annotation) = Annotation::FunctionCall((yyvsp[-3].string_value)); } } -#line 2305 "./ortools/flatzinc/parser.tab.cc" +#line 2358 "./ortools/flatzinc/parser.tab.cc" break; - case 92: + case 92: /* annotation: IDENTIFIER '[' IVALUE ']' */ #line 628 "./ortools/flatzinc/parser.yy" { CHECK(gtl::ContainsKey(context->variable_array_map, @@ -2316,10 +2363,10 @@ yyreduce: gtl::FindOrDie(context->variable_array_map, (yyvsp[-3].string_value)), (yyvsp[-1].integer_value))); } -#line 2316 "./ortools/flatzinc/parser.tab.cc" +#line 2369 "./ortools/flatzinc/parser.tab.cc" break; - case 93: + case 93: /* annotation: '[' annotation_arguments ']' */ #line 634 "./ortools/flatzinc/parser.yy" { std::vector* const annotations = (yyvsp[-1].annotations); @@ -2331,10 +2378,10 @@ yyreduce: (yyval.annotation) = Annotation::Empty(); } } -#line 2330 "./ortools/flatzinc/parser.tab.cc" +#line 2383 "./ortools/flatzinc/parser.tab.cc" break; - case 94: + case 94: /* solve: SOLVE annotations SATISFY */ #line 649 "./ortools/flatzinc/parser.yy" { if ((yyvsp[-1].annotations) != nullptr) { @@ -2344,10 +2391,10 @@ yyreduce: model->Satisfy(std::vector()); } } -#line 2343 "./ortools/flatzinc/parser.tab.cc" +#line 2396 "./ortools/flatzinc/parser.tab.cc" break; - case 95: + case 95: /* solve: SOLVE annotations MINIMIZE argument */ #line 657 "./ortools/flatzinc/parser.yy" { CHECK_EQ(Argument::INT_VAR_REF, (yyvsp[0].arg).type); @@ -2359,10 +2406,10 @@ yyreduce: model->Minimize((yyvsp[0].arg).Var(), std::vector()); } } -#line 2357 "./ortools/flatzinc/parser.tab.cc" +#line 2410 "./ortools/flatzinc/parser.tab.cc" break; - case 96: + case 96: /* solve: SOLVE annotations MAXIMIZE argument */ #line 666 "./ortools/flatzinc/parser.yy" { CHECK_EQ(Argument::INT_VAR_REF, (yyvsp[0].arg).type); @@ -2374,10 +2421,10 @@ yyreduce: model->Maximize((yyvsp[0].arg).Var(), std::vector()); } } -#line 2371 "./ortools/flatzinc/parser.tab.cc" +#line 2424 "./ortools/flatzinc/parser.tab.cc" break; -#line 2375 "./ortools/flatzinc/parser.tab.cc" +#line 2428 "./ortools/flatzinc/parser.tab.cc" default: break; @@ -2393,11 +2440,11 @@ yyreduce: case of YYERROR or YYBACKUP, subsequent parser actions might lead to an incorrect destructor call or verbose syntax error message before the lookahead is translated. */ - YY_SYMBOL_PRINT("-> $$ =", yyr1[yyn], &yyval, &yyloc); + YY_SYMBOL_PRINT("-> $$ =", YY_CAST(yysymbol_kind_t, yyr1[yyn]), &yyval, + &yyloc); YYPOPSTACK(yylen); yylen = 0; - YY_STACK_PRINT(yyss, yyssp); *++yyvsp = yyval; @@ -2420,51 +2467,45 @@ yyreduce: yyerrlab: /* Make sure we have latest lookahead translation. See comments at user semantic actions for why this is necessary. */ - yytoken = yychar == YYEMPTY ? YYEMPTY : YYTRANSLATE(yychar); - + yytoken = yychar == ORFZ_EMPTY ? YYSYMBOL_YYEMPTY : YYTRANSLATE(yychar); /* If not already recovering from an error, report this error. */ if (!yyerrstatus) { ++yynerrs; -#if !YYERROR_VERBOSE - yyerror(context, model, ok, scanner, YY_("syntax error")); -#else -#define YYSYNTAX_ERROR yysyntax_error(&yymsg_alloc, &yymsg, yyssp, yytoken) { + yypcontext_t yyctx = {yyssp, yytoken}; char const* yymsgp = YY_("syntax error"); int yysyntax_error_status; - yysyntax_error_status = YYSYNTAX_ERROR; + yysyntax_error_status = yysyntax_error(&yymsg_alloc, &yymsg, &yyctx); if (yysyntax_error_status == 0) yymsgp = yymsg; - else if (yysyntax_error_status == 1) { + else if (yysyntax_error_status == -1) { if (yymsg != yymsgbuf) YYSTACK_FREE(yymsg); yymsg = YY_CAST(char*, YYSTACK_ALLOC(YY_CAST(YYSIZE_T, yymsg_alloc))); - if (!yymsg) { + if (yymsg) { + yysyntax_error_status = yysyntax_error(&yymsg_alloc, &yymsg, &yyctx); + yymsgp = yymsg; + } else { yymsg = yymsgbuf; yymsg_alloc = sizeof yymsgbuf; - yysyntax_error_status = 2; - } else { - yysyntax_error_status = YYSYNTAX_ERROR; - yymsgp = yymsg; + yysyntax_error_status = YYENOMEM; } } yyerror(context, model, ok, scanner, yymsgp); - if (yysyntax_error_status == 2) goto yyexhaustedlab; + if (yysyntax_error_status == YYENOMEM) goto yyexhaustedlab; } -#undef YYSYNTAX_ERROR -#endif } if (yyerrstatus == 3) { /* If just tried and failed to reuse lookahead token after an error, discard it. */ - if (yychar <= YYEOF) { + if (yychar <= ORFZ_EOF) { /* Return failure if at end of input. */ - if (yychar == YYEOF) YYABORT; + if (yychar == ORFZ_EOF) YYABORT; } else { yydestruct("Error: discarding", yytoken, &yylval, context, model, ok, scanner); - yychar = YYEMPTY; + yychar = ORFZ_EMPTY; } } @@ -2494,11 +2535,12 @@ yyerrorlab: yyerrlab1: yyerrstatus = 3; /* Each real token shifted decrements this. */ + /* Pop stack until we find a state that shifts the error token. */ for (;;) { yyn = yypact[yystate]; if (!yypact_value_is_default(yyn)) { - yyn += YYTERROR; - if (0 <= yyn && yyn <= YYLAST && yycheck[yyn] == YYTERROR) { + yyn += YYSYMBOL_YYerror; + if (0 <= yyn && yyn <= YYLAST && yycheck[yyn] == YYSYMBOL_YYerror) { yyn = yytable[yyn]; if (0 < yyn) break; } @@ -2507,8 +2549,8 @@ yyerrlab1: /* Pop the current state because it cannot handle the error token. */ if (yyssp == yyss) YYABORT; - yydestruct("Error: popping", yystos[yystate], yyvsp, context, model, ok, - scanner); + yydestruct("Error: popping", YY_ACCESSING_SYMBOL(yystate), yyvsp, context, + model, ok, scanner); YYPOPSTACK(1); yystate = *yyssp; YY_STACK_PRINT(yyss, yyssp); @@ -2519,7 +2561,7 @@ yyerrlab1: YY_IGNORE_MAYBE_UNINITIALIZED_END /* Shift the error token. */ - YY_SYMBOL_PRINT("Shifting", yystos[yyn], yyvsp, yylsp); + YY_SYMBOL_PRINT("Shifting", YY_ACCESSING_SYMBOL(yyn), yyvsp, yylsp); yystate = yyn; goto yynewstate; @@ -2538,21 +2580,21 @@ yyabortlab: yyresult = 1; goto yyreturn; -#if !defined yyoverflow || YYERROR_VERBOSE +#if 1 /*-------------------------------------------------. | yyexhaustedlab -- memory exhaustion comes here. | `-------------------------------------------------*/ yyexhaustedlab: yyerror(context, model, ok, scanner, YY_("memory exhausted")); yyresult = 2; - /* Fall through. */ + goto yyreturn; #endif -/*-----------------------------------------------------. -| yyreturn -- parsing is finished, return the result. | -`-----------------------------------------------------*/ +/*-------------------------------------------------------. +| yyreturn -- parsing is finished, clean up and return. | +`-------------------------------------------------------*/ yyreturn: - if (yychar != YYEMPTY) { + if (yychar != ORFZ_EMPTY) { /* Make sure we have latest lookahead translation. See comments at user semantic actions for why this is necessary. */ yytoken = YYTRANSLATE(yychar); @@ -2564,16 +2606,15 @@ yyreturn: YYPOPSTACK(yylen); YY_STACK_PRINT(yyss, yyssp); while (yyssp != yyss) { - yydestruct("Cleanup: popping", yystos[+*yyssp], yyvsp, context, model, ok, - scanner); + yydestruct("Cleanup: popping", YY_ACCESSING_SYMBOL(+*yyssp), yyvsp, context, + model, ok, scanner); YYPOPSTACK(1); } #ifndef yyoverflow if (yyss != yyssa) YYSTACK_FREE(yyss); #endif -#if YYERROR_VERBOSE if (yymsg != yymsgbuf) YYSTACK_FREE(yymsg); -#endif return yyresult; } + #line 676 "./ortools/flatzinc/parser.yy" diff --git a/ortools/flatzinc/parser.tab.hh b/ortools/flatzinc/parser.tab.hh index 3a486f7c9c..b2139de949 100644 --- a/ortools/flatzinc/parser.tab.hh +++ b/ortools/flatzinc/parser.tab.hh @@ -1,4 +1,4 @@ -/* A Bison parser, made by GNU Bison 3.5.1. */ +/* A Bison parser, made by GNU Bison 3.7.2. */ /* Bison interface for Yacc-like parsers in C @@ -31,8 +31,9 @@ This special exception was added by the Free Software Foundation in version 2.2 of Bison. */ -/* Undocumented macros, especially those whose name start with YY_, - are private implementation details. Do not rely on them. */ +/* DO NOT RELY ON FEATURES THAT ARE NOT DOCUMENTED in the manual, + especially those whose name start with YY_ or yy_. They are + private implementation details that can be changed or removed. */ #ifndef YY_ORFZ_ORTOOLS_FLATZINC_PARSER_TAB_HH_INCLUDED #define YY_ORFZ_ORTOOLS_FLATZINC_PARSER_TAB_HH_INCLUDED @@ -68,32 +69,37 @@ typedef operations_research::fz::LexerInfo YYSTYPE; #endif // OR_TOOLS_FLATZINC_FLATZINC_TAB_HH_ -#line 72 "./ortools/flatzinc/parser.tab.hh" +#line 73 "./ortools/flatzinc/parser.tab.hh" -/* Token type. */ +/* Token kinds. */ #ifndef ORFZ_TOKENTYPE #define ORFZ_TOKENTYPE enum orfz_tokentype { - ARRAY = 258, - TOKEN_BOOL = 259, - CONSTRAINT = 260, - TOKEN_FLOAT = 261, - TOKEN_INT = 262, - MAXIMIZE = 263, - MINIMIZE = 264, - OF = 265, - PREDICATE = 266, - SATISFY = 267, - SET = 268, - SOLVE = 269, - VAR = 270, - DOTDOT = 271, - COLONCOLON = 272, - IVALUE = 273, - SVALUE = 274, - IDENTIFIER = 275, - DVALUE = 276 + ORFZ_EMPTY = -2, + ORFZ_EOF = 0, /* "end of file" */ + ORFZ_error = 256, /* error */ + ORFZ_UNDEF = 257, /* "invalid token" */ + ARRAY = 258, /* ARRAY */ + TOKEN_BOOL = 259, /* TOKEN_BOOL */ + CONSTRAINT = 260, /* CONSTRAINT */ + TOKEN_FLOAT = 261, /* TOKEN_FLOAT */ + TOKEN_INT = 262, /* TOKEN_INT */ + MAXIMIZE = 263, /* MAXIMIZE */ + MINIMIZE = 264, /* MINIMIZE */ + OF = 265, /* OF */ + PREDICATE = 266, /* PREDICATE */ + SATISFY = 267, /* SATISFY */ + SET = 268, /* SET */ + SOLVE = 269, /* SOLVE */ + VAR = 270, /* VAR */ + DOTDOT = 271, /* DOTDOT */ + COLONCOLON = 272, /* COLONCOLON */ + IVALUE = 273, /* IVALUE */ + SVALUE = 274, /* SVALUE */ + IDENTIFIER = 275, /* IDENTIFIER */ + DVALUE = 276 /* DVALUE */ }; +typedef enum orfz_tokentype orfz_token_kind_t; #endif /* Value type. */ diff --git a/ortools/glop/parameters.proto b/ortools/glop/parameters.proto index 476c6d4965..b4b65dc908 100644 --- a/ortools/glop/parameters.proto +++ b/ortools/glop/parameters.proto @@ -22,7 +22,7 @@ syntax = "proto2"; package operations_research.glop; -// next id = 60 +// next id = 61 message GlopParameters { // Supported algorithms for scaling: @@ -192,6 +192,29 @@ message GlopParameters { // each line and each column is 1.0. optional bool use_scaling = 16 [default = true]; + // This is only used if use_scaling is true. After the scaling is done, we + // also scale the objective by a constant factor. This is important because + // scaling the cost has a direct influence on the meaning of the + // dual_feasibility_tolerance. Because we usually use a fixed tolerance, the + // objective must be well scaled to make sense. + enum CostScalingAlgorithm { + // Leave the cost as is. + NO_COST_SCALING = 0; + + // This is the most defensive option. It makes sure that + // [min_cost_magnitude, max_cost_magnitude] contains 1.0, and if not, it + // makes the closest magnitude bound equal to one. + CONTAIN_ONE_COST_SCALING = 1; + + // Make the mean of the non-zero costs equals to one. + MEAN_COST_SCALING = 2; + + // Make the median of the non-zero costs equals to one. + MEDIAN_COST_SCALING = 3; + } + optional CostScalingAlgorithm cost_scaling = 60 + [default = CONTAIN_ONE_COST_SCALING]; + // What heuristic is used to try to replace the fixed slack columns in the // initial basis of the primal simplex. optional InitialBasisHeuristic initial_basis = 17 [default = TRIANGULAR]; diff --git a/ortools/glop/preprocessor.cc b/ortools/glop/preprocessor.cc index 712b8d83d5..14af39c1ae 100644 --- a/ortools/glop/preprocessor.cc +++ b/ortools/glop/preprocessor.cc @@ -3593,7 +3593,7 @@ bool ScalingPreprocessor::Run(LinearProgram* lp) { // See the doc of these functions for more details. // It is important to call Scale() before the other two. Scale(lp, &scaler_, parameters_.scaling_method()); - cost_scaling_factor_ = lp->ScaleObjective(); + cost_scaling_factor_ = lp->ScaleObjective(parameters_.cost_scaling()); bound_scaling_factor_ = lp->ScaleBounds(); return true; diff --git a/ortools/graph/BUILD b/ortools/graph/BUILD index 1fd1087d17..f3c1980380 100644 --- a/ortools/graph/BUILD +++ b/ortools/graph/BUILD @@ -44,8 +44,8 @@ cc_library( deps = [ ":graph", "//ortools/base:filelineiter", - "//ortools/base:statusor", "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", ], ) @@ -179,7 +179,7 @@ cc_library( name = "minimum_spanning_tree", hdrs = ["minimum_spanning_tree.h"], deps = [ - ":connectivity", + ":connected_components", ":graph", "//ortools/base", "//ortools/base:adjustable_priority_queue", @@ -242,14 +242,6 @@ cc_library( ], ) -cc_library( - name = "connectivity", - hdrs = ["connectivity.h"], - deps = [ - "//ortools/base", - ], -) - proto_library( name = "flow_problem_proto", srcs = ["flow_problem.proto"], @@ -281,7 +273,7 @@ cc_library( srcs = ["min_cost_flow.cc"], hdrs = ["min_cost_flow.h"], deps = [ - ":connectivity", + ":connected_components", ":ebert_graph", ":graph", ":graphs", @@ -329,6 +321,7 @@ cc_library( deps = [ "//ortools/base", "//ortools/base:map_util", + "//ortools/base:stl_util", "//ortools/base:ptr_util", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", @@ -375,14 +368,3 @@ cc_library( # srcs = ["hopcroft_karp.c"], # hdrs = ["hopcroft_karp.h"], #) - -#cc_library( -# name = "dag_connectivity", -# srcs = ["dag_connectivity.cc"], -# hdrs = ["dag_connectivity.h"], -# deps = [ -# "//ortools/base", -# "//ortools/util:bitset", -# "//ortools/util/graph:topologicalsorter", -# ], -#) diff --git a/ortools/graph/connected_components.cc b/ortools/graph/connected_components.cc index 9ab1b24dad..86f631e835 100644 --- a/ortools/graph/connected_components.cc +++ b/ortools/graph/connected_components.cc @@ -31,6 +31,8 @@ #include +#include "ortools/base/stl_util.h" + void DenseConnectedComponentsFinder::SetNumberOfNodes(int num_nodes) { const int old_num_nodes = GetNumberOfNodes(); if (num_nodes == old_num_nodes) { @@ -68,6 +70,29 @@ int DenseConnectedComponentsFinder::FindRoot(int node) { return root; } +const std::vector& DenseConnectedComponentsFinder::GetComponentRoots() { + const int num_nodes = GetNumberOfNodes(); + if (num_nodes != num_nodes_at_last_get_roots_call_) { + // Add potential roots for each new node that did not exist the last time + // GetComponentRoots() was called. The cost here is amortized against + // adding the nodes in the first place. + const int previous_num_roots = roots_.size(); + roots_.resize(previous_num_roots + num_nodes - + num_nodes_at_last_get_roots_call_); + std::iota(roots_.begin() + previous_num_roots, roots_.end(), + num_nodes_at_last_get_roots_call_); + } + + // Remove the roots that have been merged with other components. Each node + // only gets removed once from the roots vector, so the cost of FindRoot() is + // amortized against adding the edge. + gtl::STLEraseAllFromSequenceIf( + &roots_, [&](const int node) { return node != FindRoot(node); }); + + num_nodes_at_last_get_roots_call_ = num_nodes; + return roots_; +} + void DenseConnectedComponentsFinder::AddEdge(int node1, int node2) { // Grow if needed. const int min_num_nodes = std::max(node1, node2) + 1; diff --git a/ortools/graph/connected_components.h b/ortools/graph/connected_components.h index 8c14ce2fdd..a474c2e69a 100644 --- a/ortools/graph/connected_components.h +++ b/ortools/graph/connected_components.h @@ -95,6 +95,10 @@ class DenseConnectedComponentsFinder { int GetNumberOfComponents() const { return num_components_; } int GetNumberOfNodes() const { return parent_.size(); } + // Gets the current set of root nodes in sorted order. Runs in amortized + // O(#components) time. + const std::vector& GetComponentRoots(); + // Sets the number of nodes in the graph. The graph can only grow: this // dies if "num_nodes" is lower or equal to any of the values ever given // to AddEdge(), or lower than a previous value given to SetNumberOfNodes(). @@ -120,6 +124,11 @@ class DenseConnectedComponentsFinder { std::vector rank_; // Number of connected components. int num_components_ = 0; + // The current roots. This is maintained lazily by GetComponentRoots(). + std::vector roots_; + // The number of nodes that existed the last time GetComponentRoots() was + // called. + int num_nodes_at_last_get_roots_call_ = 0; }; namespace internal { @@ -141,8 +150,8 @@ struct ConnectedComponentsTypeHelper { // like a hash functor. template struct SelectContainer< - U, absl::enable_if_t()(std::declval()))>::value>> { + U, absl::enable_if_t()( + std::declval()))>::value>> { using Set = absl::flat_hash_set; using Map = absl::flat_hash_map; }; diff --git a/ortools/graph/connectivity.h b/ortools/graph/connectivity.h deleted file mode 100644 index 7acf32cba6..0000000000 --- a/ortools/graph/connectivity.h +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2010-2018 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Graph connectivity algorithm for undirected graphs. -// Memory consumption: O(n) where m is the number of arcs and n the number -// of nodes. -// TODO(user): add depth-first-search based connectivity for directed graphs. -// TODO(user): add depth-first-search based biconnectivity for directed graphs. - -#ifndef OR_TOOLS_GRAPH_CONNECTIVITY_H_ -#define OR_TOOLS_GRAPH_CONNECTIVITY_H_ - -#include - -#include "ortools/base/integral_types.h" -#include "ortools/base/logging.h" -#include "ortools/base/macros.h" - -namespace operations_research { - -// Template class implementing a Union-Find algorithm with path compression for -// maintaining the connected components of a graph. -// See Cormen et al. 2nd Edition. MIT Press, 2001. ISBN 0-262-03293-7. -// Chapter 21: Data structures for Disjoint Sets, pp. 498-524. -// and Tarjan (1975). Efficiency of a Good But Not Linear Set -// Union Algorithm. Journal of the ACM 22(2):215-225 -// It is implemented as a template so that the size of NodeIndex can be chosen -// depending on the size of the graphs considered. -// The main interest is that arcs do not need to be kept. Thus the memory -// complexity is O(n) where n is the number of nodes in the graph. -// The complexity of this algorithm is O(n . alpha(n)) where alpha(n) is -// the inverse Ackermann function. alpha(n) <= log(log(log(..log(log(n))..) -// In practice alpha(n) <= 5. -// See Tarjan and van Leeuwen (1984). Worst-case analysis of set union -// algorithms. Journal of the ACM 31(2):245-281. -// -// Usage example: -// ConnectedComponents components; -// components.Init(num_nodes); -// for (int arc = 0; arc < num_arcs; ++arc) { -// components.AddArc(tail[arc], head[arc]); -// } -// int num_connected_components = components.GetNumberOfConnectedComponents(); -// if (num_connected_components == 1) { -// // Graph is completely connected. -// } -// // Group the nodes in the same connected component together. -// // group[class_number][i] contains the i-th node in group class_number. -// hash_map > group(num_connected_components); -// for (int node = 0; node < num_nodes; ++node) { -// group[components.GetClassRepresentative(node)].push_back(node); -// } -// -// Keywords: graph, connected components. - -template -class ConnectedComponents { - public: - ConnectedComponents() : num_nodes_(0), class_(), class_size_() {} - - // Reserves memory for num_nodes and resets the data structures. - void Init(NodeIndex num_nodes) { - CHECK_GE(num_nodes, 0); - num_nodes_ = num_nodes; - class_.resize(num_nodes_); - class_size_.assign(num_nodes_, 1); - for (NodeIndex node = 0; node < num_nodes_; ++node) { - class_[node] = node; - } - } - - // Adds the information that NodeIndex tail and NodeIndex head are connected. - void AddArc(NodeIndex tail, NodeIndex head) { - const NodeIndex tail_class = CompressPath(tail); - const NodeIndex head_class = CompressPath(head); - if (tail_class != head_class) { - MergeClasses(tail_class, head_class); - } - } - - // Adds a complete StarGraph to the object. Note that Depth-First Search - // is a better algorithm for finding connected components on graphs. - // TODO(user): implement Depth-First Search-based connected components finder. - template - void AddGraph(const Graph& graph) { - Init(graph.num_nodes()); - for (NodeIndex tail = 0; tail < graph.num_nodes(); ++tail) { - for (typename Graph::OutgoingArcIterator it(graph, tail); it.Ok(); - it.Next()) { - AddArc(tail, graph.Head(it.Index())); - } - } - } - - // Compresses the path for node. - NodeIndex CompressPath(NodeIndex node) { - CheckNodeBounds(node); - NodeIndex parent = node; - while (parent != class_[parent]) { - CheckNodeBounds(class_[parent]); - CheckNodeBounds(class_[class_[parent]]); - parent = class_[parent]; - } - while (node != class_[node]) { - const NodeIndex old_parent = class_[node]; - class_[node] = parent; - node = old_parent; - } - return parent; - } - - // Returns the equivalence class representative for node. - NodeIndex GetClassRepresentative(NodeIndex node) { - return CompressPath(node); - } - - // Returns the number of connected components. Allocates num_nodes_ bits for - // the computation. - NodeIndex GetNumberOfConnectedComponents() { - NodeIndex number = 0; - for (NodeIndex node = 0; node < num_nodes_; ++node) { - if (class_[node] == node) ++number; - } - return number; - } - - // Merges the equivalence classes of node1 and node2. - void MergeClasses(NodeIndex node1, NodeIndex node2) { - // It's faster (~10%) to swap the two values and have a single piece of - // code for merging the classes. - CheckNodeBounds(node1); - CheckNodeBounds(node2); - if (class_size_[node1] < class_size_[node2]) { - std::swap(node1, node2); - } - class_[node2] = node1; - class_size_[node1] += class_size_[node2]; - } - - private: - void CheckNodeBounds(NodeIndex node_index) { - DCHECK_LE(0, node_index); - DCHECK_LT(node_index, num_nodes_); - } - // The exact number of nodes in the graph. - NodeIndex num_nodes_; - - // The equivalence class representative for each node. - std::vector class_; - - // The size of each equivalence class of each node. Used to compress the paths - // and therefore achieve better time complexity. - std::vector class_size_; - - DISALLOW_COPY_AND_ASSIGN(ConnectedComponents); -}; - -} // namespace operations_research - -#endif // OR_TOOLS_GRAPH_CONNECTIVITY_H_ diff --git a/ortools/graph/io.h b/ortools/graph/io.h index f7dedd4072..40c7deebab 100644 --- a/ortools/graph/io.h +++ b/ortools/graph/io.h @@ -23,13 +23,13 @@ #include #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "ortools/base/filelineiter.h" -#include "ortools/base/statusor.h" #include "ortools/graph/graph.h" namespace util { @@ -58,7 +58,7 @@ std::string GraphToString(const Graph& graph, GraphToStringFormat format); // . // // This returns a newly created graph upon success, which the user needs to take -// ownership of, or a failure status. See ortools/base/statusor.h. +// ownership of, or a failure status. See absl/status/statusor.h. // // If "num_nodes_with_color_or_null" is not nullptr, it will be filled with the // color information: num_nodes_with_color_or_null[i] will be the number of @@ -140,7 +140,7 @@ absl::StatusOr ReadGraphFile( ++num_lines_read; if (num_lines_read == 1) { std::vector header_ints; - // if (!SplitStringAndParse(line, " ", &absl::SimpleAtoi, + // if (!SplitStringAndParse(line, " ", &strings::safe_strto64, // &header_ints) || // header_ints.size() < 2 || header_ints[0] < 0 || header_ints[1] < 0) // { diff --git a/ortools/graph/minimum_spanning_tree.h b/ortools/graph/minimum_spanning_tree.h index 3e8973da3a..b92a8415b4 100644 --- a/ortools/graph/minimum_spanning_tree.h +++ b/ortools/graph/minimum_spanning_tree.h @@ -20,7 +20,7 @@ #include "ortools/base/adjustable_priority_queue-inl.h" #include "ortools/base/adjustable_priority_queue.h" #include "ortools/base/integral_types.h" -#include "ortools/graph/connectivity.h" +#include "ortools/graph/connected_components.h" #include "ortools/util/vector_or_function.h" namespace operations_research { @@ -60,16 +60,14 @@ BuildKruskalMinimumSpanningTreeFromSortedArcs( } const int expected_tree_size = graph.num_nodes() - 1; tree_arcs.reserve(expected_tree_size); - ConnectedComponents components; - components.Init(graph.num_nodes()); + DenseConnectedComponentsFinder components; + components.SetNumberOfNodes(graph.num_nodes()); while (tree_arcs.size() != expected_tree_size && arc_index < num_arcs) { const ArcIndex arc = sorted_arcs[arc_index]; - const NodeIndex tail_class = - components.GetClassRepresentative(graph.Tail(arc)); - const NodeIndex head_class = - components.GetClassRepresentative(graph.Head(arc)); - if (tail_class != head_class) { - components.MergeClasses(tail_class, head_class); + const auto tail = graph.Tail(arc); + const auto head = graph.Head(arc); + if (!components.Connected(tail, head)) { + components.AddEdge(tail, head); tree_arcs.push_back(arc); } ++arc_index; diff --git a/ortools/graph/topologicalsorter.h b/ortools/graph/topologicalsorter.h index 424cf7b942..8a3ef82c15 100644 --- a/ortools/graph/topologicalsorter.h +++ b/ortools/graph/topologicalsorter.h @@ -40,9 +40,10 @@ // non-dense integers), but slower, or the "dense int" versions which requires // nodes to be a dense interval [0..num_nodes-1]. Note that the type must // be compatible with LOG << T if you're using the OrDie() version. -// - The sorting can be either stable or not. Stable sorting means that if -// nodes A and B appear in that order and aren't ancestors of each other, -// they will remain in that order in the returned topological order. +// - The sorting can be either stable or not. "Stable" essentially means that it +// will preserve the order of nodes, if possible. More precisely, the returned +// topological order will be the lexicographically minimal valid order, where +// "lexicographic" applies to the indices of the nodes. // // TopologicalSort() // TopologicalSortOrDie() @@ -69,7 +70,6 @@ #include "ortools/base/macros.h" #include "ortools/base/map_util.h" #include "ortools/base/stl_util.h" -//#include "ortools/base/vector32.h" namespace util { @@ -141,7 +141,7 @@ template class DenseIntTopologicalSorterTpl { public: // To store the adjacency lists efficiently. - typedef ::std::vector AdjacencyList; + typedef std::vector AdjacencyList; // For efficiency, it is best to specify how many nodes are required // by using the next constructor. diff --git a/ortools/gscip/BUILD b/ortools/gscip/BUILD new file mode 100644 index 0000000000..defbccd398 --- /dev/null +++ b/ortools/gscip/BUILD @@ -0,0 +1,151 @@ +load("@rules_cc//cc:defs.bzl", "cc_proto_library") + +package( + default_visibility = ["//visibility:public"], +) + +proto_library( + name = "gscip_proto", + srcs = ["gscip.proto"], +) + +cc_proto_library( + name = "gscip_cc_proto", + deps = [":gscip_proto"], +) + +# NOTE(user): this file should ideally not have a compile time dependency on +# SCIP, so it can be used in client code. +cc_library( + name = "gscip_parameters", + srcs = ["gscip_parameters.cc"], + hdrs = ["gscip_parameters.h"], + deps = [ + ":gscip_cc_proto", + "//ortools/base:status_macros", + "@com_github_glog_glog//:glog", + "@com_google_absl//absl/time", + ], +) + +#cc_test( +# name = "gscip_parameters_test", +# srcs = ["gscip_parameters_test.cc"], +# deps = [ +# ":gscip_cc_proto", +# ":gscip_parameters", +# "@com_google_absl//absl/time", +# "@gtest//:main", +# ], +#) + +cc_library( + name = "legacy_scip_params", + srcs = ["legacy_scip_params.cc"], + hdrs = ["legacy_scip_params.h"], + deps = [ + "//ortools/linear_solver:scip_helper_macros", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@scip//:libscip", + ], +) + +cc_library( + name = "gscip", + srcs = ["gscip.cc"], + hdrs = ["gscip.h"], + deps = [ + ":gscip_cc_proto", + ":gscip_parameters", + ":legacy_scip_params", + "//ortools/base", + "//ortools/base:status_builder", + "//ortools/base:status_macros", + "//ortools/linear_solver:scip_helper_macros", + "//ortools/port:proto_utils", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@scip//:libscip", + ], +) + +#gmock not supported +#cc_library( +# name = "gscip_testing", +# testonly = 1, +# srcs = ["gscip_testing.cc"], +# hdrs = ["gscip_testing.h"], +# deps = [ +# ":gscip", +# ":gscip_parameters", +# "@gtest//:main", +# ], +#) + +# Status matchers not supported. +# cc_test( +# name = "gscip_test", +# size = "small", +# srcs = ["gscip_test.cc"], +# deps = [ +# ":gscip", +# ":gscip_cc_proto", +# ":gscip_parameters", +# ":gscip_testing", +# "@com_google_googletest//:gtest", +# "@com_google_googletest//:gtest_main", +# "@scip//:libscip", +# ], +#) + +#cc_test( +# name = "gscip_io_test", +# size = "medium", +# srcs = ["gscip_io_test.cc"], +# deps = [ +# ":gscip", +# ":gscip_cc_proto", +# ":gscip_testing", +# "//ortools/base/file", +# "//file/base:path", # Path is not supported? +# "@com_google_googletest//:gtest", +# "@com_google_googletest//:gtest_main", +# "@scip//:libscip", +# ], +#) + +cc_library( + name = "gscip_ext", + srcs = ["gscip_ext.cc"], + hdrs = ["gscip_ext.h"], + deps = [ + ":gscip", + "//ortools/base:status_macros", + "@com_github_glog_glog//:glog", + "@com_google_absl//absl/status", + "@scip//:libscip", + ], +) + +# Status matchers not supported. +#cc_test( +# name = "gscip_ext_test", +# size = "small", +# srcs = ["gscip_ext_test.cc"], +# deps = [ +# ":gscip", +# ":gscip_cc_proto", +# ":gscip_ext", +# ":gscip_parameters", +# ":gscip_testing", +# "//ortools/base:map_util", +# "@com_google_googletest//:gtest", +# "@com_google_googletest//:gtest_main", +# "@scip//:libscip", +# ], +#) diff --git a/ortools/gscip/gscip.cc b/ortools/gscip/gscip.cc new file mode 100644 index 0000000000..a53465877a --- /dev/null +++ b/ortools/gscip/gscip.cc @@ -0,0 +1,896 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/gscip/gscip.h" + +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/gscip/gscip_parameters.h" +#include "ortools/gscip/legacy_scip_params.h" +#include "ortools/linear_solver/scip_helper_macros.h" +#include "ortools/port/proto_utils.h" +#include "scip/cons_linear.h" +#include "scip/scip_general.h" +#include "scip/scip_param.h" +#include "scip/scip_solvingstats.h" +#include "scip/scipdefplugins.h" +#include "scip/type_cons.h" + +namespace operations_research { + +#define RETURN_ERROR_UNLESS(x) \ + if (!(x)) \ + return util::StatusBuilder(absl::InvalidArgumentError(absl::StrFormat( \ + "Condition violated at %s:%d: %s", __FILE__, __LINE__, #x))) + +namespace { + +constexpr absl::string_view kLinearConstraintHandlerName = "linear"; + +SCIP_VARTYPE ConvertVarType(const GScipVarType var_type) { + switch (var_type) { + case GScipVarType::kContinuous: + return SCIP_VARTYPE_CONTINUOUS; + case GScipVarType::kImpliedInteger: + return SCIP_VARTYPE_IMPLINT; + case GScipVarType::kInteger: + return SCIP_VARTYPE_INTEGER; + } +} + +GScipVarType ConvertVarType(const SCIP_VARTYPE var_type) { + switch (var_type) { + case SCIP_VARTYPE_CONTINUOUS: + return GScipVarType::kContinuous; + case SCIP_VARTYPE_IMPLINT: + return GScipVarType::kImpliedInteger; + case SCIP_VARTYPE_INTEGER: + case SCIP_VARTYPE_BINARY: + return GScipVarType::kInteger; + } +} + +GScipOutput::Status ConvertStatus(const SCIP_STATUS scip_status) { + switch (scip_status) { + case SCIP_STATUS_UNKNOWN: + return GScipOutput::UNKNOWN; + case SCIP_STATUS_USERINTERRUPT: + return GScipOutput::USER_INTERRUPT; + case SCIP_STATUS_BESTSOLLIMIT: + return GScipOutput::BEST_SOL_LIMIT; + case SCIP_STATUS_MEMLIMIT: + return GScipOutput::MEM_LIMIT; + case SCIP_STATUS_NODELIMIT: + return GScipOutput::NODE_LIMIT; + case SCIP_STATUS_RESTARTLIMIT: + return GScipOutput::RESTART_LIMIT; + case SCIP_STATUS_SOLLIMIT: + return GScipOutput::SOL_LIMIT; + case SCIP_STATUS_STALLNODELIMIT: + return GScipOutput::STALL_NODE_LIMIT; + case SCIP_STATUS_TIMELIMIT: + return GScipOutput::TIME_LIMIT; + case SCIP_STATUS_TOTALNODELIMIT: + return GScipOutput::TOTAL_NODE_LIMIT; + case SCIP_STATUS_OPTIMAL: + return GScipOutput::OPTIMAL; + case SCIP_STATUS_GAPLIMIT: + return GScipOutput::GAP_LIMIT; + case SCIP_STATUS_INFEASIBLE: + return GScipOutput::INFEASIBLE; + case SCIP_STATUS_UNBOUNDED: + return GScipOutput::UNBOUNDED; + case SCIP_STATUS_INFORUNBD: + return GScipOutput::INF_OR_UNBD; + case SCIP_STATUS_TERMINATE: + return GScipOutput::TERMINATE; + default: + LOG(FATAL) << "Unrecognized scip status: " << scip_status; + } +} + +SCIP_PARAMEMPHASIS ConvertEmphasis( + const GScipParameters::Emphasis gscip_emphasis) { + switch (gscip_emphasis) { + case GScipParameters::DEFAULT_EMPHASIS: + return SCIP_PARAMEMPHASIS_DEFAULT; + case GScipParameters::CP_SOLVER: + return SCIP_PARAMEMPHASIS_CPSOLVER; + case GScipParameters::EASY_CIP: + return SCIP_PARAMEMPHASIS_EASYCIP; + case GScipParameters::FEASIBILITY: + return SCIP_PARAMEMPHASIS_FEASIBILITY; + case GScipParameters::HARD_LP: + return SCIP_PARAMEMPHASIS_HARDLP; + case GScipParameters::OPTIMALITY: + return SCIP_PARAMEMPHASIS_OPTIMALITY; + case GScipParameters::COUNTER: + return SCIP_PARAMEMPHASIS_COUNTER; + case GScipParameters::PHASE_FEAS: + return SCIP_PARAMEMPHASIS_PHASEFEAS; + case GScipParameters::PHASE_IMPROVE: + return SCIP_PARAMEMPHASIS_PHASEIMPROVE; + case GScipParameters::PHASE_PROOF: + return SCIP_PARAMEMPHASIS_PHASEPROOF; + default: + LOG(FATAL) << "Unrecognized gscip_emphasis: " + << ProtoEnumToString(gscip_emphasis); + } +} + +SCIP_PARAMSETTING ConvertMetaParamValue( + const GScipParameters::MetaParamValue gscip_meta_param_value) { + switch (gscip_meta_param_value) { + case GScipParameters::DEFAULT_META_PARAM_VALUE: + return SCIP_PARAMSETTING_DEFAULT; + case GScipParameters::AGGRESSIVE: + return SCIP_PARAMSETTING_AGGRESSIVE; + case GScipParameters::FAST: + return SCIP_PARAMSETTING_FAST; + case GScipParameters::OFF: + return SCIP_PARAMSETTING_OFF; + default: + LOG(FATAL) << "Unrecognized gscip_meta_param_value: " + << ProtoEnumToString(gscip_meta_param_value); + } +} +} // namespace + +const GScipVariableOptions& DefaultGScipVariableOptions() { + static GScipVariableOptions var_options; + return var_options; +} + +const GScipConstraintOptions& DefaultGScipConstraintOptions() { + static GScipConstraintOptions constraint_options; + return constraint_options; +} + +absl::Status GScip::SetParams(const GScipParameters& params, + const std::string& legacy_params) { + SCIPsetMessagehdlrQuiet(scip_, params.silence_output()); + if (!params.search_logs_filename().empty()) { + SCIPsetMessagehdlrLogfile(scip_, params.search_logs_filename().c_str()); + } + const SCIP_Bool set_param_quiet = + static_cast(!params.silence_output()); + RETURN_IF_SCIP_ERROR(SCIPsetEmphasis( + scip_, ConvertEmphasis(params.emphasis()), set_param_quiet)); + RETURN_IF_SCIP_ERROR(SCIPsetHeuristics( + scip_, ConvertMetaParamValue(params.heuristics()), set_param_quiet)); + RETURN_IF_SCIP_ERROR(SCIPsetPresolving( + scip_, ConvertMetaParamValue(params.presolve()), set_param_quiet)); + RETURN_IF_SCIP_ERROR(SCIPsetSeparating( + scip_, ConvertMetaParamValue(params.separating()), set_param_quiet)); + for (const auto& bool_param : params.bool_params()) { + RETURN_IF_SCIP_ERROR( + (SCIPsetBoolParam(scip_, bool_param.first.c_str(), bool_param.second))); + } + for (const auto& int_param : params.int_params()) { + RETURN_IF_SCIP_ERROR( + (SCIPsetIntParam(scip_, int_param.first.c_str(), int_param.second))); + } + for (const auto& long_param : params.long_params()) { + RETURN_IF_SCIP_ERROR((SCIPsetLongintParam(scip_, long_param.first.c_str(), + long_param.second))); + } + for (const auto& char_param : params.char_params()) { + if (char_param.second.size() != 1) { + return absl::InvalidArgumentError( + absl::StrCat("Character parameters must be single character strings, " + "but parameter: ", + char_param.first, " was: ", char_param.second)); + } + RETURN_IF_SCIP_ERROR((SCIPsetCharParam(scip_, char_param.first.c_str(), + char_param.second[0]))); + } + for (const auto& string_param : params.string_params()) { + RETURN_IF_SCIP_ERROR((SCIPsetStringParam(scip_, string_param.first.c_str(), + string_param.second.c_str()))); + } + for (const auto& real_param : params.real_params()) { + RETURN_IF_SCIP_ERROR( + (SCIPsetRealParam(scip_, real_param.first.c_str(), real_param.second))); + } + if (!legacy_params.empty()) { + RETURN_IF_ERROR( + LegacyScipSetSolverSpecificParameters(legacy_params, scip_)); + } + return absl::OkStatus(); +} + +absl::StatusOr> GScip::Create( + const std::string& problem_name) { + SCIP* scip = nullptr; + RETURN_IF_SCIP_ERROR(SCIPcreate(&scip)); + RETURN_IF_SCIP_ERROR(SCIPincludeDefaultPlugins(scip)); + RETURN_IF_SCIP_ERROR(SCIPcreateProbBasic(scip, problem_name.c_str())); + // NOTE(user): the constructor is private, so we cannot call make_unique. + return absl::WrapUnique(new GScip(scip)); +} + +GScip::GScip(SCIP* scip) : scip_(scip) {} + +double GScip::ScipInf() { return SCIPinfinity(scip_); } + +absl::Status GScip::FreeTransform() { + return SCIP_TO_STATUS(SCIPfreeTransform(scip_)); +} + +std::string GScip::ScipVersion() { + return absl::StrFormat("SCIP %d.%d.%d [LP solver: %s]", SCIPmajorVersion(), + SCIPminorVersion(), SCIPtechVersion(), + SCIPlpiGetSolverName()); +} + +bool GScip::InterruptSolve() { + if (scip_ == nullptr) { + return true; + } + return SCIPinterruptSolve(scip_) == SCIP_OKAY; +} + +absl::Status GScip::CleanUp() { + if (scip_ != nullptr) { + for (SCIP_VAR* variable : variables_) { + if (variable != nullptr) { + RETURN_IF_SCIP_ERROR(SCIPreleaseVar(scip_, &variable)); + } + } + for (SCIP_CONS* constraint : constraints_) { + if (constraint != nullptr) { + RETURN_IF_SCIP_ERROR(SCIPreleaseCons(scip_, &constraint)); + } + } + RETURN_IF_SCIP_ERROR(SCIPfree(&scip_)); + } + return absl::OkStatus(); +} + +GScip::~GScip() { + const absl::Status clean_up_status = CleanUp(); + LOG_IF(DFATAL, !clean_up_status.ok()) << clean_up_status; +} + +absl::StatusOr GScip::AddVariable( + double lb, double ub, double obj_coef, GScipVarType var_type, + const std::string& var_name, const GScipVariableOptions& options) { + SCIP_VAR* var = nullptr; + lb = ScipInfClamp(lb); + ub = ScipInfClamp(ub); + RETURN_IF_SCIP_ERROR(SCIPcreateVarBasic(scip_, /*var=*/&var, + /*name=*/var_name.c_str(), + /*lb=*/lb, /*ub=*/ub, + /*obj=*/obj_coef, + ConvertVarType(var_type))); + RETURN_IF_SCIP_ERROR(SCIPvarSetInitial(var, options.initial)); + RETURN_IF_SCIP_ERROR(SCIPvarSetRemovable(var, options.removable)); + RETURN_IF_SCIP_ERROR(SCIPaddVar(scip_, var)); + if (options.keep_alive) { + variables_.insert(var); + } else { + RETURN_IF_SCIP_ERROR(SCIPreleaseVar(scip_, &var)); + } + return var; +} + +absl::Status GScip::MaybeKeepConstraintAlive( + SCIP_CONS* constraint, const GScipConstraintOptions& options) { + if (options.keep_alive) { + constraints_.insert(constraint); + } else { + RETURN_IF_SCIP_ERROR(SCIPreleaseCons(scip_, &constraint)); + } + return absl::OkStatus(); +} + +absl::StatusOr GScip::AddLinearConstraint( + const GScipLinearRange& range, const std::string& name, + const GScipConstraintOptions& options) { + SCIP_CONS* constraint = nullptr; + RETURN_ERROR_UNLESS(range.variables.size() == range.coefficients.size()) + << "Error adding constraint: " << name << "."; + RETURN_IF_SCIP_ERROR(SCIPcreateConsLinear( + scip_, &constraint, name.c_str(), range.variables.size(), + const_cast(range.variables.data()), + const_cast(range.coefficients.data()), + ScipInfClamp(range.lower_bound), ScipInfClamp(range.upper_bound), + /*initial=*/options.initial, + /*separate=*/options.separate, + /*enforce=*/options.enforce, + /*check=*/options.check, + /*propagate=*/options.propagate, + /*local=*/options.local, + /*modifiable=*/options.modifiable, + /*dynamic=*/options.dynamic, + /*removable=*/options.removable, + /*stickingatnode=*/options.sticking_at_node)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip_, constraint)); + RETURN_IF_ERROR(MaybeKeepConstraintAlive(constraint, options)); + return constraint; +} + +absl::StatusOr GScip::AddQuadraticConstraint( + const GScipQuadraticRange& range, const std::string& name, + const GScipConstraintOptions& options) { + SCIP_CONS* constraint = nullptr; + const int num_lin_vars = range.linear_variables.size(); + RETURN_ERROR_UNLESS(num_lin_vars == range.linear_coefficients.size()) + << "Error adding quadratic constraint: " << name << " in linear term."; + const int num_quad_vars = range.quadratic_variables1.size(); + RETURN_ERROR_UNLESS(num_quad_vars == range.quadratic_variables2.size()) + << "Error adding quadratic constraint: " << name << " in quadratic term."; + RETURN_ERROR_UNLESS(num_quad_vars == range.quadratic_coefficients.size()) + << "Error adding quadratic constraint: " << name << " in quadratic term."; + RETURN_IF_SCIP_ERROR(SCIPcreateConsQuadratic( + scip_, &constraint, name.c_str(), num_lin_vars, + const_cast(range.linear_variables.data()), + const_cast(range.linear_coefficients.data()), num_quad_vars, + const_cast(range.quadratic_variables1.data()), + const_cast(range.quadratic_variables2.data()), + const_cast(range.quadratic_coefficients.data()), + ScipInfClamp(range.lower_bound), ScipInfClamp(range.upper_bound), + /*initial=*/options.initial, + /*separate=*/options.separate, + /*enforce=*/options.enforce, + /*check=*/options.check, + /*propagate=*/options.propagate, + /*local=*/options.local, + /*modifiable=*/options.modifiable, + /*dynamic=*/options.dynamic, + /*removable=*/options.removable)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip_, constraint)); + RETURN_IF_ERROR(MaybeKeepConstraintAlive(constraint, options)); + return constraint; +} + +absl::StatusOr GScip::AddIndicatorConstraint( + const GScipIndicatorConstraint& indicator_constraint, + const std::string& name, const GScipConstraintOptions& options) { + SCIP_VAR* indicator = indicator_constraint.indicator_variable; + RETURN_ERROR_UNLESS(indicator != nullptr) + << "Error adding indicator constraint: " << name << "."; + if (indicator_constraint.negate_indicator) { + RETURN_IF_SCIP_ERROR(SCIPgetNegatedVar(scip_, indicator, &indicator)); + } + + SCIP_CONS* constraint = nullptr; + RETURN_ERROR_UNLESS(indicator_constraint.variables.size() == + indicator_constraint.coefficients.size()) + << "Error adding indicator constraint: " << name << "."; + RETURN_IF_SCIP_ERROR(SCIPcreateConsIndicator( + scip_, &constraint, name.c_str(), indicator, + indicator_constraint.variables.size(), + const_cast(indicator_constraint.variables.data()), + const_cast(indicator_constraint.coefficients.data()), + ScipInfClamp(indicator_constraint.upper_bound), + /*initial=*/options.initial, + /*separate=*/options.separate, + /*enforce=*/options.enforce, + /*check=*/options.check, + /*propagate=*/options.propagate, + /*local=*/options.local, + /*dynamic=*/options.dynamic, + /*removable=*/options.removable, + /*stickingatnode=*/options.sticking_at_node)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip_, constraint)); + RETURN_IF_ERROR(MaybeKeepConstraintAlive(constraint, options)); + return constraint; +} + +absl::StatusOr GScip::AddAndConstraint( + const GScipLogicalConstraintData& logical_data, const std::string& name, + const GScipConstraintOptions& options) { + RETURN_ERROR_UNLESS(logical_data.resultant != nullptr) + << "Error adding and constraint: " << name << "."; + SCIP_CONS* constraint = nullptr; + RETURN_IF_SCIP_ERROR( + SCIPcreateConsAnd(scip_, &constraint, name.c_str(), + logical_data.resultant, logical_data.operators.size(), + const_cast(logical_data.operators.data()), + /*initial=*/options.initial, + /*separate=*/options.separate, + /*enforce=*/options.enforce, + /*check=*/options.check, + /*propagate=*/options.propagate, + /*local=*/options.local, + /*modifiable=*/options.modifiable, + /*dynamic=*/options.dynamic, + /*removable=*/options.removable, + /*stickingatnode=*/options.sticking_at_node)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip_, constraint)); + RETURN_IF_ERROR(MaybeKeepConstraintAlive(constraint, options)); + return constraint; +} + +absl::StatusOr GScip::AddOrConstraint( + const GScipLogicalConstraintData& logical_data, const std::string& name, + const GScipConstraintOptions& options) { + RETURN_ERROR_UNLESS(logical_data.resultant != nullptr) + << "Error adding or constraint: " << name << "."; + SCIP_CONS* constraint = nullptr; + RETURN_IF_SCIP_ERROR( + SCIPcreateConsOr(scip_, &constraint, name.c_str(), logical_data.resultant, + logical_data.operators.size(), + const_cast(logical_data.operators.data()), + /*initial=*/options.initial, + /*separate=*/options.separate, + /*enforce=*/options.enforce, + /*check=*/options.check, + /*propagate=*/options.propagate, + /*local=*/options.local, + /*modifiable=*/options.modifiable, + /*dynamic=*/options.dynamic, + /*removable=*/options.removable, + /*stickingatnode=*/options.sticking_at_node)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip_, constraint)); + RETURN_IF_ERROR(MaybeKeepConstraintAlive(constraint, options)); + return constraint; +} + +namespace { + +absl::Status ValidateSOSData(const GScipSOSData& sos_data, + const std::string& name) { + RETURN_ERROR_UNLESS(!sos_data.variables.empty()) + << "Error adding SOS constraint: " << name << "."; + if (!sos_data.weights.empty()) { + RETURN_ERROR_UNLESS(sos_data.variables.size() == sos_data.weights.size()) + << " Error adding SOS constraint: " << name << "."; + } + absl::flat_hash_set distinct_weights; + for (const double w : sos_data.weights) { + RETURN_ERROR_UNLESS(!distinct_weights.contains(w)) + << "Error adding SOS constraint: " << name + << ", weights must be distinct, but found value " << w << " twice."; + distinct_weights.insert(w); + } + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr GScip::AddSOS1Constraint( + const GScipSOSData& sos_data, const std::string& name, + const GScipConstraintOptions& options) { + RETURN_IF_ERROR(ValidateSOSData(sos_data, name)); + SCIP_CONS* constraint = nullptr; + double* weights = nullptr; + if (!sos_data.weights.empty()) { + weights = const_cast(sos_data.weights.data()); + } + + RETURN_IF_SCIP_ERROR(SCIPcreateConsSOS1( + scip_, &constraint, name.c_str(), sos_data.variables.size(), + const_cast(sos_data.variables.data()), weights, + /*initial=*/options.initial, + /*separate=*/options.separate, + /*enforce=*/options.enforce, + /*check=*/options.check, + /*propagate=*/options.propagate, + /*local=*/options.local, + /*dynamic=*/options.dynamic, + /*removable=*/options.removable, + /*stickingatnode=*/options.sticking_at_node)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip_, constraint)); + RETURN_IF_ERROR(MaybeKeepConstraintAlive(constraint, options)); + return constraint; +} + +absl::StatusOr GScip::AddSOS2Constraint( + const GScipSOSData& sos_data, const std::string& name, + const GScipConstraintOptions& options) { + RETURN_IF_ERROR(ValidateSOSData(sos_data, name)); + SCIP_CONS* constraint = nullptr; + double* weights = nullptr; + if (!sos_data.weights.empty()) { + weights = const_cast(sos_data.weights.data()); + } + RETURN_IF_SCIP_ERROR(SCIPcreateConsSOS2( + scip_, &constraint, name.c_str(), sos_data.variables.size(), + const_cast(sos_data.variables.data()), weights, + /*initial=*/options.initial, + /*separate=*/options.separate, + /*enforce=*/options.enforce, + /*check=*/options.check, + /*propagate=*/options.propagate, + /*local=*/options.local, + /*dynamic=*/options.dynamic, + /*removable=*/options.removable, + /*stickingatnode=*/options.sticking_at_node)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip_, constraint)); + RETURN_IF_ERROR(MaybeKeepConstraintAlive(constraint, options)); + return constraint; +} + +absl::Status GScip::SetMaximize(bool is_maximize) { + RETURN_IF_SCIP_ERROR(SCIPsetObjsense( + scip_, is_maximize ? SCIP_OBJSENSE_MAXIMIZE : SCIP_OBJSENSE_MINIMIZE)); + return absl::OkStatus(); +} + +absl::Status GScip::SetObjectiveOffset(double offset) { + double old_offset = SCIPgetOrigObjoffset(scip_); + double delta_offset = offset - old_offset; + RETURN_IF_SCIP_ERROR(SCIPaddOrigObjoffset(scip_, delta_offset)); + return absl::OkStatus(); +} + +bool GScip::ObjectiveIsMaximize() { + return SCIPgetObjsense(scip_) == SCIP_OBJSENSE_MAXIMIZE; +} + +double GScip::ObjectiveOffset() { return SCIPgetOrigObjoffset(scip_); } + +absl::Status GScip::SetBranchingPriority(SCIP_VAR* var, int priority) { + RETURN_IF_SCIP_ERROR(SCIPchgVarBranchPriority(scip_, var, priority)); + return absl::OkStatus(); +} + +absl::Status GScip::SetLb(SCIP_VAR* var, double lb) { + lb = ScipInfClamp(lb); + RETURN_IF_SCIP_ERROR(SCIPchgVarLb(scip_, var, lb)); + return absl::OkStatus(); +} + +absl::Status GScip::SetUb(SCIP_VAR* var, double ub) { + ub = ScipInfClamp(ub); + RETURN_IF_SCIP_ERROR(SCIPchgVarUb(scip_, var, ub)); + return absl::OkStatus(); +} + +absl::Status GScip::SetObjCoef(SCIP_VAR* var, double obj_coef) { + RETURN_IF_SCIP_ERROR(SCIPchgVarObj(scip_, var, obj_coef)); + return absl::OkStatus(); +} + +absl::Status GScip::SetVarType(SCIP_VAR* var, GScipVarType var_type) { + SCIP_Bool infeasible; + RETURN_IF_SCIP_ERROR( + SCIPchgVarType(scip_, var, ConvertVarType(var_type), &infeasible)); + return absl::OkStatus(); +} + +absl::Status GScip::DeleteVariable(SCIP_VAR* var) { + SCIP_Bool did_delete; + RETURN_IF_SCIP_ERROR(SCIPdelVar(scip_, var, &did_delete)); + RETURN_ERROR_UNLESS(static_cast(did_delete)) + << "Failed to delete variable named: " << Name(var); + variables_.erase(var); + RETURN_IF_SCIP_ERROR(SCIPreleaseVar(scip_, &var)); + return absl::OkStatus(); +} + +absl::Status GScip::CanSafeBulkDelete( + const absl::flat_hash_set& vars) { + for (SCIP_CONS* constraint : constraints_) { + if (!IsConstraintLinear(constraint)) { + return absl::InvalidArgumentError(absl::StrCat( + "Model contains nonlinear constraint: ", Name(constraint))); + } + } + return absl::OkStatus(); +} + +absl::Status GScip::SafeBulkDelete(const absl::flat_hash_set& vars) { + RETURN_IF_ERROR(CanSafeBulkDelete(vars)); + // Now, we can assume that all constraints are linear. + for (SCIP_CONS* constraint : constraints_) { + const absl::Span nonzeros = + LinearConstraintVariables(constraint); + const std::vector nonzeros_copy(nonzeros.begin(), + nonzeros.end()); + for (SCIP_VAR* var : nonzeros_copy) { + if (vars.contains(var)) { + RETURN_IF_ERROR(SetLinearConstraintCoef(constraint, var, 0.0)); + } + } + } + for (SCIP_VAR* const var : vars) { + RETURN_IF_ERROR(DeleteVariable(var)); + } + return absl::OkStatus(); +} + +double GScip::Lb(SCIP_VAR* var) { + return ScipInfUnclamp(SCIPvarGetLbOriginal(var)); +} + +double GScip::Ub(SCIP_VAR* var) { + return ScipInfUnclamp(SCIPvarGetUbOriginal(var)); +} + +double GScip::ObjCoef(SCIP_VAR* var) { return SCIPvarGetObj(var); } + +GScipVarType GScip::VarType(SCIP_VAR* var) { + return ConvertVarType(SCIPvarGetType(var)); +} + +absl::string_view GScip::Name(SCIP_VAR* var) { return SCIPvarGetName(var); } + +absl::string_view GScip::ConstraintType(SCIP_CONS* constraint) { + return absl::string_view(SCIPconshdlrGetName(SCIPconsGetHdlr(constraint))); +} + +bool GScip::IsConstraintLinear(SCIP_CONS* constraint) { + return ConstraintType(constraint) == kLinearConstraintHandlerName; +} + +absl::Span GScip::LinearConstraintCoefficients( + SCIP_CONS* constraint) { + int num_vars = SCIPgetNVarsLinear(scip_, constraint); + return absl::MakeConstSpan(SCIPgetValsLinear(scip_, constraint), num_vars); +} + +absl::Span GScip::LinearConstraintVariables( + SCIP_CONS* constraint) { + int num_vars = SCIPgetNVarsLinear(scip_, constraint); + return absl::MakeConstSpan(SCIPgetVarsLinear(scip_, constraint), num_vars); +} + +double GScip::LinearConstraintLb(SCIP_CONS* constraint) { + return ScipInfUnclamp(SCIPgetLhsLinear(scip_, constraint)); +} + +double GScip::LinearConstraintUb(SCIP_CONS* constraint) { + return ScipInfUnclamp(SCIPgetRhsLinear(scip_, constraint)); +} + +absl::string_view GScip::Name(SCIP_CONS* constraint) { + return SCIPconsGetName(constraint); +} + +absl::Status GScip::SetLinearConstraintLb(SCIP_CONS* constraint, double lb) { + lb = ScipInfClamp(lb); + RETURN_IF_SCIP_ERROR(SCIPchgLhsLinear(scip_, constraint, lb)); + return absl::OkStatus(); +} + +absl::Status GScip::SetLinearConstraintUb(SCIP_CONS* constraint, double ub) { + ub = ScipInfClamp(ub); + RETURN_IF_SCIP_ERROR(SCIPchgRhsLinear(scip_, constraint, ub)); + return absl::OkStatus(); +} + +absl::Status GScip::DeleteConstraint(SCIP_CONS* constraint) { + RETURN_IF_SCIP_ERROR(SCIPdelCons(scip_, constraint)); + constraints_.erase(constraint); + RETURN_IF_SCIP_ERROR(SCIPreleaseCons(scip_, &constraint)); + return absl::OkStatus(); +} + +absl::Status GScip::SetLinearConstraintCoef(SCIP_CONS* constraint, + SCIP_VAR* var, double value) { + // TODO(user): this operation is slow (linear in the nnz in the constraint). + // It would be better to just use a bulk operation, but there doesn't appear + // to be any? + RETURN_IF_SCIP_ERROR(SCIPchgCoefLinear(scip_, constraint, var, value)); + return absl::OkStatus(); +} + +absl::StatusOr GScip::SuggestHint( + const GScipSolution& partial_solution) { + SCIP_SOL* solution; + const int scip_num_vars = SCIPgetNOrigVars(scip_); + const bool is_solution_partial = partial_solution.size() < scip_num_vars; + if (is_solution_partial) { + RETURN_IF_SCIP_ERROR(SCIPcreatePartialSol(scip_, &solution, nullptr)); + } else { + // This is actually a full solution + RETURN_ERROR_UNLESS(partial_solution.size() == scip_num_vars) + << "Error suggesting hint."; + RETURN_IF_SCIP_ERROR(SCIPcreateSol(scip_, &solution, nullptr)); + } + for (const auto& var_value_pair : partial_solution) { + RETURN_IF_SCIP_ERROR(SCIPsetSolVal(scip_, solution, var_value_pair.first, + var_value_pair.second)); + } + if (!is_solution_partial) { + SCIP_Bool is_feasible; + RETURN_IF_SCIP_ERROR(SCIPcheckSol( + scip_, solution, /*printreason=*/false, /*completely=*/true, + /*checkbounds=*/true, /*checkintegrality=*/true, /*checklprows=*/true, + &is_feasible)); + if (!static_cast(is_feasible)) { + RETURN_IF_SCIP_ERROR(SCIPfreeSol(scip_, &solution)); + return GScipHintResult::kInfeasible; + } + } + SCIP_Bool is_stored; + RETURN_IF_SCIP_ERROR(SCIPaddSolFree(scip_, &solution, &is_stored)); + if (static_cast(is_stored)) { + return GScipHintResult::kAccepted; + } else { + return GScipHintResult::kRejected; + } +} + +absl::StatusOr GScip::Solve(const GScipParameters& params, + const std::string& legacy_params) { + // A four step process: + // 1. Apply parameters. + // 2. Solve the problem. + // 3. Extract solution and solve statistics. + // 4. Prepare the solver for further modification/solves (reset parameters, + // free the solutions found). + GScipResult result; + + // Step 1: apply parameters. + const absl::Status param_status = SetParams(params, legacy_params); + if (!param_status.ok()) { + result.gscip_output.set_status(GScipOutput::INVALID_SOLVER_PARAMETERS); + // Conversion to std::string for open source build. + result.gscip_output.set_status_detail( + std::string(param_status.message())); // NOLINT + return result; + } + if (params.print_scip_model()) { + RETURN_IF_SCIP_ERROR(SCIPwriteOrigProblem(scip_, nullptr, "cip", FALSE)); + } + if (!params.scip_model_filename().empty()) { + RETURN_IF_SCIP_ERROR(SCIPwriteOrigProblem( + scip_, params.scip_model_filename().c_str(), "cip", FALSE)); + } + + // Step 2: Solve. + // NOTE(user): after solve, SCIP will either be in stage PRESOLVING, + // SOLVING, OR SOLVED. + if (MaxNumThreads(params) > 1) { + RETURN_IF_SCIP_ERROR(SCIPsolveConcurrent(scip_)); + } else { + RETURN_IF_SCIP_ERROR(SCIPsolve(scip_)); + } + const SCIP_STAGE stage = SCIPgetStage(scip_); + if (stage != SCIP_STAGE_PRESOLVING && stage != SCIP_STAGE_SOLVING && + stage != SCIP_STAGE_SOLVED) { + result.gscip_output.set_status(GScipOutput::UNKNOWN); + result.gscip_output.set_status_detail( + absl::StrCat("Unpexpected SCIP final stage= ", stage, + " was expected to be either SCIP_STAGE_PRESOLVING, " + "SCIP_STAGE_SOLVING, or SCIP_STAGE_SOLVED")); + return result; + } + if (params.print_detailed_solving_stats()) { + RETURN_IF_SCIP_ERROR(SCIPprintStatistics(scip_, nullptr)); + } + if (!params.detailed_solving_stats_filename().empty()) { + FILE* file = fopen(params.detailed_solving_stats_filename().c_str(), "w"); + if (file == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Could not open file: ", params.detailed_solving_stats_filename(), + " to write SCIP solve stats.")); + } + RETURN_IF_SCIP_ERROR(SCIPprintStatistics(scip_, file)); + int close_result = fclose(file); + if (close_result != 0) { + return absl::InvalidArgumentError(absl::StrCat( + "Error: ", close_result, + " closing file: ", params.detailed_solving_stats_filename(), + " when writing solve stats.")); + } + } + // Step 3: Extract solution information. + // Some outputs are available unconditionally, and some are only ready if at + // least presolve succeeded. + GScipSolvingStats* stats = result.gscip_output.mutable_stats(); + const int num_scip_solutions = SCIPgetNSols(scip_); + const int num_returned_solutions = + std::min(num_scip_solutions, std::max(1, params.num_solutions())); + SCIP_SOL** all_solutions = SCIPgetSols(scip_); + stats->set_best_objective(ScipInfUnclamp(SCIPgetPrimalbound(scip_))); + for (int i = 0; i < num_returned_solutions; ++i) { + SCIP_SOL* scip_sol = all_solutions[i]; + const double obj_value = ScipInfUnclamp(SCIPgetSolOrigObj(scip_, scip_sol)); + GScipSolution solution; + for (SCIP_VAR* v : variables_) { + solution[v] = SCIPgetSolVal(scip_, scip_sol, v); + } + result.solutions.push_back(solution); + result.objective_values.push_back(obj_value); + } + // Can only check for primal ray if we made it past presolve. + if (stage != SCIP_STAGE_PRESOLVING && SCIPhasPrimalRay(scip_)) { + for (SCIP_VAR* v : variables_) { + result.primal_ray[v] = SCIPgetPrimalRayVal(scip_, v); + } + } + // TODO(user): refactor this into a new method. + stats->set_best_bound(ScipInfUnclamp(SCIPgetDualbound(scip_))); + stats->set_node_count(SCIPgetNTotalNodes(scip_)); + stats->set_first_lp_relaxation_bound(SCIPgetFirstLPDualboundRoot(scip_)); + stats->set_root_node_bound(SCIPgetDualboundRoot(scip_)); + if (stage != SCIP_STAGE_PRESOLVING) { + stats->set_total_lp_iterations(SCIPgetNLPIterations(scip_)); + stats->set_primal_simplex_iterations(SCIPgetNPrimalLPIterations(scip_)); + stats->set_dual_simplex_iterations(SCIPgetNDualLPIterations(scip_)); + stats->set_deterministic_time(SCIPgetDeterministicTime(scip_)); + } + result.gscip_output.set_status(ConvertStatus(SCIPgetStatus(scip_))); + + // Step 4: clean up. + RETURN_IF_ERROR(FreeTransform()); + RETURN_IF_SCIP_ERROR(SCIPresetParams(scip_)); + return result; +} + +absl::StatusOr GScip::DefaultBoolParamValue( + const std::string& parameter_name) { + SCIP_Bool default_value; + RETURN_IF_SCIP_ERROR( + SCIPgetBoolParam(scip_, parameter_name.c_str(), &default_value)); + return static_cast(default_value); +} + +absl::StatusOr GScip::DefaultIntParamValue( + const std::string& parameter_name) { + int default_value; + RETURN_IF_SCIP_ERROR( + SCIPgetIntParam(scip_, parameter_name.c_str(), &default_value)); + return default_value; +} + +absl::StatusOr GScip::DefaultLongParamValue( + const std::string& parameter_name) { + SCIP_Longint result; + RETURN_IF_SCIP_ERROR( + SCIPgetLongintParam(scip_, parameter_name.c_str(), &result)); + return static_cast(result); +} + +absl::StatusOr GScip::DefaultRealParamValue( + const std::string& parameter_name) { + double result; + RETURN_IF_SCIP_ERROR( + SCIPgetRealParam(scip_, parameter_name.c_str(), &result)); + return result; +} + +absl::StatusOr GScip::DefaultCharParamValue( + const std::string& parameter_name) { + char result; + RETURN_IF_SCIP_ERROR( + SCIPgetCharParam(scip_, parameter_name.c_str(), &result)); + return result; +} + +absl::StatusOr GScip::DefaultStringParamValue( + const std::string& parameter_name) { + char* result; + RETURN_IF_SCIP_ERROR( + SCIPgetStringParam(scip_, parameter_name.c_str(), &result)); + return std::string(result); +} + +double GScip::ScipInfClamp(double d) { + const double kScipInf = ScipInf(); + if (d > kScipInf) return kScipInf; + if (d < -kScipInf) return -kScipInf; + return d; +} + +double GScip::ScipInfUnclamp(double d) { + const double kScipInf = ScipInf(); + if (d >= kScipInf) return std::numeric_limits::infinity(); + if (d <= -kScipInf) return -std::numeric_limits::infinity(); + return d; +} + +#undef RETURN_ERROR_UNLESS + +} // namespace operations_research diff --git a/ortools/gscip/gscip.h b/ortools/gscip/gscip.h new file mode 100644 index 0000000000..850ff2ef22 --- /dev/null +++ b/ortools/gscip/gscip.h @@ -0,0 +1,537 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Simplified bindings for the SCIP solver. This is not designed to be used +// directly by users, the API is not friendly to a modeler. For most common +// cases, use MPSolver instead. +// +// Notable differences between gSCIP and SCIP: +// * Unless callbacks are used, gSCIP only exposes the SCIP stage PROBLEM to +// the user through public APIs. +// * Instead of the stateful SCIP parameters API, parameters are passed in at +// Solve() time and cleared at the end of solve. Parameters that effect +// problem creation are thus not supported. +// * gSCIP uses std::numeric_limits::infinity(), rather than SCIPs +// infinity (a default value of 1e20). Doubles with absolute value >= 1e20 +// are automatically converting to std::numeric_limits::infinity() +// by gSCIP. Changing the underlying SCIP's infinity is not supported. +// * absl::Status and absl::StatusOr are used to propagate SCIP errors (and on +// a best effort basis, also filter out bad input to gSCIP functions). +// +// A note on error propagation and reliability: +// Many methods on SCIP return an error code. Errors can be triggered by +// both invalid input and bugs in SCIP. We propagate these errors back to the +// user through gSCIP through Status and StatusOr. If you are solving a single +// MIP and you have previously successfully solved similar MIPs, it is unlikely +// gSCIP would return any status errors. Depending on your application, CHECK +// failing on these errors may be appropriate (e.g. a benchmark that is run by +// hand). If you are solving a very large number of MIPs (e.g. in a flume job), +// your instances are numerically challenging, or the model/data are drawn from +// an unreliable source, or you are running a server that cannot crash, you may +// want to try and process these errors instead. Note that on bad instances, +// SCIP may still crash, so highly reliable systems should run SCIP in a +// separate process. +// +// NOTE(user): much of the API uses const std::string& instead of +// absl::string_view because the underlying SCIP API needs a null terminated +// char*. +#ifndef OR_TOOLS_GSCIP_GSCIP_H_ +#define OR_TOOLS_GSCIP_GSCIP_H_ + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/gscip/gscip.pb.h" +#include "scip/scip.h" +#include "scip/scip_prob.h" +#include "scip/type_cons.h" +#include "scip/type_scip.h" +#include "scip/type_var.h" + +namespace operations_research { + +using GScipSolution = absl::flat_hash_map; + +// The result of GScip::Solve(). Contains the solve status, statistics, and the +// solutions found. +struct GScipResult { + GScipOutput gscip_output; + // The number of solutions returned is at most GScipParameters::num_solutions. + // They are ordered from best objective value to worst. When + // gscip_output.status() is optimal, solutions will have at least one element. + std::vector solutions; + // Of the same size as solutions. + std::vector objective_values; + // Advanced use below + + // If the problem was unbounded, a primal ray in the unbounded direction of + // the LP relaxation should be produced. + absl::flat_hash_map primal_ray; + // TODO(user): add dual support: + // 1. The dual solution for LPs. + // 2. The dual ray for infeasible LP/MIPs. +}; + +// Models the constraint lb <= a*x <= ub. Members variables and coefficients +// must have the same size. +struct GScipLinearRange { + double lower_bound = -std::numeric_limits::infinity(); + std::vector variables; + std::vector coefficients; + double upper_bound = std::numeric_limits::infinity(); +}; + +// A variable is implied integer if the integrality constraint is not required +// for the model to be valid, but the variable takes an integer value in any +// optimal solution to the problem. +enum class GScipVarType { kContinuous, kInteger, kImpliedInteger }; + +// Some advanced features, defined at the end of the header file. +struct GScipQuadraticRange; +struct GScipSOSData; +struct GScipIndicatorConstraint; +struct GScipLogicalConstraintData; +struct GScipVariableOptions; +const GScipVariableOptions& DefaultGScipVariableOptions(); +struct GScipConstraintOptions; +const GScipConstraintOptions& DefaultGScipConstraintOptions(); +using GScipBranchingPriority = absl::flat_hash_map; +enum class GScipHintResult; + +// A thin wrapper around the SCIP solver that provides C++ bindings that are +// idiomatic for Google. Unless callbacks are used, the SCIP stage is always +// PROBLEM. +class GScip { + public: + // Create a new GScip (the constructor is private). The default objective + // direction is minimization. + static absl::StatusOr> Create( + const std::string& problem_name); + ~GScip(); + static std::string ScipVersion(); + + // After Solve() the parameters are reset and SCIP stage is restored to + // PROBLEM. "legacy_params" are in the format of legacy_scip_params.h and are + // applied after "params". Use of "legacy_params" is discouraged. + // + // The returned StatusOr will contain an error only if an: + // * An underlying function from SCIP fails. + // * There is an I/O error with managing SCIP output. + // The above cases are not mutually exclusive. If the problem is infeasible, + // this will be reflected in the value of GScipResult::gscip_output::status. + absl::StatusOr Solve( + const GScipParameters& params = GScipParameters(), + const std::string& legacy_params = ""); + + // /////////////////////////////////////////////////////////////////////////// + // Basic Model Construction + // /////////////////////////////////////////////////////////////////////////// + + // Use true for maximization, false for minimization. + absl::Status SetMaximize(bool is_maximize); + absl::Status SetObjectiveOffset(double offset); + + // The returned SCIP_VAR is owned by GScip. With default options, the + // returned variable will have the same lifetime as GScip (if instead, + // GScipVariableOptions::keep_alive is false, SCIP may free the variable at + // any time, see GScipVariableOptions::keep_alive for details). + absl::StatusOr AddVariable( + double lb, double ub, double obj_coef, GScipVarType var_type, + const std::string& var_name = "", + const GScipVariableOptions& options = DefaultGScipVariableOptions()); + + // The returned SCIP_CONS is owned by GScip. With default options, the + // returned variable will have the same lifetime as GScip (if instead, + // GScipConstraintOptions::keep_alive is false, SCIP may free the constraint + // at any time, see GScipConstraintOptions::keep_alive for details). + // + // Can be called while creating the model or in a callback (e.g. in a + // GScipConstraintHandler). + absl::StatusOr AddLinearConstraint( + const GScipLinearRange& range, const std::string& name = "", + const GScipConstraintOptions& options = DefaultGScipConstraintOptions()); + + // /////////////////////////////////////////////////////////////////////////// + // Model Queries + // /////////////////////////////////////////////////////////////////////////// + + bool ObjectiveIsMaximize(); + double ObjectiveOffset(); + + double Lb(SCIP_VAR* var); + double Ub(SCIP_VAR* var); + double ObjCoef(SCIP_VAR* var); + GScipVarType VarType(SCIP_VAR* var); + absl::string_view Name(SCIP_VAR* var); + const absl::flat_hash_set& variables() { return variables_; } + + // These methods works on all constraint types. + absl::string_view Name(SCIP_CONS* constraint); + bool IsConstraintLinear(SCIP_CONS* constraint); + const absl::flat_hash_set& constraints() { return constraints_; } + + // These methods will CHECK fail if constraint is not a linear constraint. + absl::Span LinearConstraintCoefficients(SCIP_CONS* constraint); + absl::Span LinearConstraintVariables(SCIP_CONS* constraint); + double LinearConstraintLb(SCIP_CONS* constraint); + double LinearConstraintUb(SCIP_CONS* constraint); + + // /////////////////////////////////////////////////////////////////////////// + // Model Updates (needed for incrementalism) + // /////////////////////////////////////////////////////////////////////////// + absl::Status SetLb(SCIP_VAR* var, double lb); + absl::Status SetUb(SCIP_VAR* var, double ub); + absl::Status SetObjCoef(SCIP_VAR* var, double obj_coef); + absl::Status SetVarType(SCIP_VAR* var, GScipVarType var_type); + + // Warning: you need to ensure that no constraint has a reference to this + // variable before deleting it, or undefined behavior will occur. For linear + // constraints, you can set the coefficient of this variable to zero to remove + // the variable from the constriant. + absl::Status DeleteVariable(SCIP_VAR* var); + + // Checks if SafeBulkDelete will succeed for vars, and returns a description + // the problematic variables/constraints on a failure (the returned status + // will not contain a propagated SCIP error). Will not modify the underyling + // SCIP, it is safe to continue using this if an error is returned. + absl::Status CanSafeBulkDelete(const absl::flat_hash_set& vars); + + // Attempts to remove vars from all constraints and then remove vars from + // the model. As of August 7, 2020, will fail if the model contains any + // constraints that are not linear. + // + // Will call CanSafeBulkDelete above, but can also return an error Status + // propagated from SCIP. Do not assume SCIP is in a valid state if this fails. + absl::Status SafeBulkDelete(const absl::flat_hash_set& vars); + + // These methods will CHECK fail if constraint is not a linear constraint. + absl::Status SetLinearConstraintLb(SCIP_CONS* constraint, double lb); + absl::Status SetLinearConstraintUb(SCIP_CONS* constraint, double ub); + absl::Status SetLinearConstraintCoef(SCIP_CONS* constraint, SCIP_VAR* var, + double value); + + // Works on all constraint types. Unlike DeleteVariable, no special action is + // required before deleting a constraint. + absl::Status DeleteConstraint(SCIP_CONS* constraint); + + // /////////////////////////////////////////////////////////////////////////// + // Nonlinear constraint types. + // For now, only basic support (adding to the model) is provided. Reading and + // updating support may be added in the future. + // /////////////////////////////////////////////////////////////////////////// + + // Adds a constraint of the form: + // if z then a * x <= b + // where z is a binary variable, x is a vector of decision variables, a is + // vector of constants, and b is a constant. z can be negated. + // + // NOTE(user): options.modifiable is ignored. + absl::StatusOr AddIndicatorConstraint( + const GScipIndicatorConstraint& indicator_constraint, + const std::string& name = "", + const GScipConstraintOptions& options = DefaultGScipConstraintOptions()); + + // Adds a constraint of form lb <= x * Q * x + a * x <= ub. + // + // NOTE(user): options.modifiable and options.sticking_at_node are ignored. + absl::StatusOr AddQuadraticConstraint( + const GScipQuadraticRange& range, const std::string& name = "", + const GScipConstraintOptions& options = DefaultGScipConstraintOptions()); + + // Adds the constraint: + // logical_data.resultant = AND_i logical_data.operators[i], + // where logical_data.resultant and logical_data.operators[i] are all binary + // variables. + absl::StatusOr AddAndConstraint( + const GScipLogicalConstraintData& logical_data, + const std::string& name = "", + const GScipConstraintOptions& options = DefaultGScipConstraintOptions()); + + // Adds the constraint: + // logical_data.resultant = OR_i logical_data.operators[i], + // where logical_data.resultant and logical_data.operators[i] must be binary + // variables. + absl::StatusOr AddOrConstraint( + const GScipLogicalConstraintData& logical_data, + const std::string& name = "", + const GScipConstraintOptions& options = DefaultGScipConstraintOptions()); + + // Adds the constraint that at most one of the variables in sos_data can be + // nonzero. The variables can be integer or continuous. See GScipSOSData for + // details. + // + // NOTE(user): options.modifiable is ignored (these constraints are not + // modifiable). + absl::StatusOr AddSOS1Constraint( + const GScipSOSData& sos_data, const std::string& name = "", + const GScipConstraintOptions& options = DefaultGScipConstraintOptions()); + + // Adds the constraint that at most two of the variables in sos_data can be + // nonzero, and they must be adjacent under the ordering for sos_data. See + // GScipSOSData for details. + // + // NOTE(user): options.modifiable is ignored (these constraints are not + // modifiable). + absl::StatusOr AddSOS2Constraint( + const GScipSOSData& sos_data, const std::string& name = "", + const GScipConstraintOptions& options = DefaultGScipConstraintOptions()); + + // /////////////////////////////////////////////////////////////////////////// + // Advanced use + // /////////////////////////////////////////////////////////////////////////// + + // Returns the name of the constraint handler for this constraint. + absl::string_view ConstraintType(SCIP_CONS* constraint); + + // The proposed solution can be partial (only specify some of the variables) + // or complete. Complete solutions will be checked for feasibility and + // objective quality, and might be unused for these reasons. Partial solutions + // will always be accepted. + absl::StatusOr SuggestHint( + const GScipSolution& partial_solution); + + // All variables have a default branching priority of zero. Variables are + // partitioned by their branching priority, and a fractional variable from the + // highest partition will always be branched on. + // + // TODO(user): Add support for BranchingFactor as well, this is typically + // more useful. + absl::Status SetBranchingPriority(SCIP_VAR* var, int priority); + + // Doubles with absolute value of at least this value are replaced by this + // value before giving them SCIP. SCIP considers values at least this large to + // be infinite. When querying gSCIP, if an absolute value exceeds ScipInf, it + // is replaced by std::numeric_limits::infinity(). + double ScipInf(); + + // WARNING(rander): no synchronization is provided between InterruptSolve() + // and ~GScip(). These methods require mutual exclusion, the user is + // responsible for ensuring this invariant. + // TODO(user): should we add a lock here? Seems a little dangerous to block + // in a destructor. + bool InterruptSolve(); + + // These should typically not be needed. + SCIP* scip() { return scip_; } + + absl::StatusOr DefaultBoolParamValue(const std::string& parameter_name); + absl::StatusOr DefaultIntParamValue(const std::string& parameter_name); + absl::StatusOr DefaultLongParamValue( + const std::string& parameter_name); + absl::StatusOr DefaultRealParamValue( + const std::string& parameter_name); + absl::StatusOr DefaultCharParamValue(const std::string& parameter_name); + absl::StatusOr DefaultStringParamValue( + const std::string& parameter_name); + + private: + explicit GScip(SCIP* scip); + // Releases SCIP memory. + absl::Status CleanUp(); + + absl::Status SetParams(const GScipParameters& params, + const std::string& legacy_params); + absl::Status FreeTransform(); + // Clamps d to [-ScipInf(), ScipInf()]. + double ScipInfClamp(double d); + // Returns +/- inf if |d| >= ScipInf(), otherwise returns d. + double ScipInfUnclamp(double d); + + absl::Status MaybeKeepConstraintAlive(SCIP_CONS* constraint, + const GScipConstraintOptions& options); + + SCIP* scip_; + absl::flat_hash_set variables_; + absl::flat_hash_set constraints_; +}; + +// Advanced features below + +// Models the constraint +// lb <= x * Q * x + a * x <= ub +struct GScipQuadraticRange { + // Models lb above. + double lower_bound = -std::numeric_limits::infinity(); + + // Models a * x above. linear_variables and linear_coefficients must have the + // same size. + std::vector linear_variables; + std::vector linear_coefficients; + + // These three vectors must have the same size. Models x * Q * x as + // sum_i quadratic_coefficients[i] * quadratic_variables1[i] + // * quadratic_variables2[i] + // + // Duplicate quadratic terms (e.g. i=3 encodes 4*x1*x3 and i=4 encodes + // 8*x3*x1) are added (as if you added a single entry 12*x1*x3). + // + // TODO(user): investigate, the documentation seems to suggest that when + // linear_variables[i] == quadratic_variables1[i] == quadratic_variables2[i] + // there is some advantage. + std::vector quadratic_variables1; + std::vector quadratic_variables2; + std::vector quadratic_coefficients; + + // Models ub above. + double upper_bound = std::numeric_limits::infinity(); +}; + +// Models special ordered set constraints (SOS1 and SOS2 constraints). Each +// contains a list of variables that are implicitly ordered by the provided +// weights, which must be distinct. +// SOS1: At most one of the variables can be nonzero. +// SOS2: At most two of the variables can be nonzero, and they must be +// consecutive. +// +// The weights are optional, and if not provided, the ordering in "variables" is +// used. +struct GScipSOSData { + // The list of variables where all but one or two must be zero. Can be integer + // or continuous variables, typically their domain will contain zero. Cannot + // be empty in a valid SOS constraint. + std::vector variables; + + // Optional, can be empty. Otherwise, must have size equal to variables, and + // values must be distinct. Determines an "ordering" over the variables + // (smallest weight to largest). Additionally, the numeric values of + // the weights are used to make branching decisions in a solver specific way, + // for details, see: + // * https://scip.zib.de/doc/html/cons__sos1_8c.php + // * https://scip.zib.de/doc/html/cons__sos2_8c.php. + std::vector weights; +}; + +// Models the constraint z = 1 => a * x <= b +// If negate_indicator, then instead: z = 0 => a * x <= b +struct GScipIndicatorConstraint { + // The z variable above. + SCIP_VAR* indicator_variable = nullptr; + bool negate_indicator = false; + // The x variable above. + std::vector variables; + // a above. Must have the same size as x. + std::vector coefficients; + // b above. + double upper_bound = std::numeric_limits::infinity(); +}; + +// Data for constraint of the form resultant = f(operators), e.g.: +// resultant = AND_i operators[i] +// For existing constraints (e.g. AND, OR) resultant and operators[i] should all +// be binary variables, this my change. See use in GScip for details. +struct GScipLogicalConstraintData { + SCIP_VAR* resultant = nullptr; + std::vector operators; +}; + +enum class GScipHintResult { + // Hint was not feasible. + kInfeasible, + // Hint was not good enough to keep. + kRejected, + // Hint was kept. Partial solutions are not checked for feasibility, they + // are always accepted. + kAccepted +}; + +// Advanced use. Options to use when creating a variable. +struct GScipVariableOptions { + // /////////////////////////////////////////////////////////////////////////// + // SCIP options. Descriptions are from the SCIP documentation, e.g. + // SCIPcreateVar: + // https://scip.zib.de/doc/html/group__PublicVariableMethods.php#ga7a37fe4dc702dadecc4186b9624e93fc + // /////////////////////////////////////////////////////////////////////////// + + // Should var's column be present in the initial root LP? + bool initial = true; + + // Is var's column removable from the LP (due to aging or cleanup)? + bool removable = false; + + // /////////////////////////////////////////////////////////////////////////// + // gSCIP options. + // /////////////////////////////////////////////////////////////////////////// + + // If keep_alive=true, the returned variable will not to be freed until after + // ~GScip() is called. Otherwise, the returned variable could be freed + // internally by SCIP at any point, and it is not safe to hold a reference to + // the returned variable. + // + // The primary reason to set keep_alive=false is if you are adding many + // variables in a callback (in branch and price), and you expect that most of + // them will be deleted. + bool keep_alive = true; +}; + +// Advanced use. Options to use when creating a constraint. +struct GScipConstraintOptions { + // /////////////////////////////////////////////////////////////////////////// + // SCIP options. Descriptions are from the SCIP documentation, e.g. + // SCIPcreateConsLinear: + // https://scip.zib.de/doc/html/group__CONSHDLRS.php#gaea3b4db21fe214be5db047e08b46b50e + // /////////////////////////////////////////////////////////////////////////// + + // Should the LP relaxation of constraint be in the initial LP? False for lazy + // constraints (true in callbacks). + bool initial = true; + // Should the constraint be separated during LP processing? + bool separate = true; + // Should the constraint be enforced during node processing? True for model + // constraints, false for redundant constraints. + bool enforce = true; + // Should the constraint be checked for feasibility? True for model + // constraints, false for redundant constraints. + bool check = true; + // Should the constraint be propagated during node processing? + bool propagate = true; + // Is constraint only valid locally? Must be true for branching constraints. + bool local = false; + // Is constraint modifiable (subject to column generation)? In column + // generation applications, set to true if pricing adds coefficients to this + // constraint. + bool modifiable = false; + // Is constraint subject to aging? Set to true for own cuts which are + // separated as constraints + bool dynamic = false; + // Should the relaxation be removed from the LP due to aging or cleanup? Set + // to true for 'lazy constraints' and 'user cuts'. + bool removable = false; + // Should the constraint always be kept at the node where it was added, even + // if it may be moved to a more global node? Usually set to false. Set to true + // for constraints that represent node data. + bool sticking_at_node = false; + + // /////////////////////////////////////////////////////////////////////////// + // gSCIP options. + // /////////////////////////////////////////////////////////////////////////// + + // If keep_alive=true, the returned constraint will not to be freed until + // after ~GScip() is called. Otherwise, the returned constraint could be freed + // internally by SCIP at any point, and it is not safe to hold a reference to + // the returned constraint. + // + // The primary reason to set keep_alive=false is if you are adding many + // constraints in a callback, and you expect that most of them will be + // deleted. + bool keep_alive = true; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_GSCIP_GSCIP_H_ diff --git a/ortools/gscip/gscip.proto b/ortools/gscip/gscip.proto new file mode 100644 index 0000000000..43b85f0bba --- /dev/null +++ b/ortools/gscip/gscip.proto @@ -0,0 +1,170 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package operations_research; + +// Contains both the "SCIP parameters" and gSCIP only configuration. For the +// SCIP parameters, the order of application is: +// 1. Emphasis +// 2. Meta parameters (heuristics, presolve, separating) +// 3. Individual SCIP parameters (e.g. an entry in bool_params) +// Note that 1. and 2. both apply a combination of parameters from 3. +message GScipParameters { + // See SCIP documentation for details: + // https://scip.zib.de/doc/html/type__paramset_8h.php#a2e51a867a8ea3ea16f15e7cc935c3f32 + enum Emphasis { + DEFAULT_EMPHASIS = 0; + COUNTER = 1; + CP_SOLVER = 2; + EASY_CIP = 3; + FEASIBILITY = 4; + HARD_LP = 5; + OPTIMALITY = 6; + PHASE_FEAS = 7; + PHASE_IMPROVE = 8; + PHASE_PROOF = 9; + } + + // See SCIP documentation for details: + // https://scip.zib.de/doc/html/type__paramset_8h.php#a083067d8e425d0d44e834095e82902ed + enum MetaParamValue { + DEFAULT_META_PARAM_VALUE = 0; + AGGRESSIVE = 1; + FAST = 2; + OFF = 3; + } + + Emphasis emphasis = 1; + + // See SCIPsetHeuristics() for details: + // https://scip.zib.de/doc/html/group__ParameterMethods.php#gaeccb7859066cadd01d0df7aca98e2c7d + MetaParamValue heuristics = 2; + + // See SCIPsetPresolving() for details: + // https://scip.zib.de/doc/html/group__ParameterMethods.php#ga8365de8ab5ec5961c005e2d77965b182 + MetaParamValue presolve = 3; + + // See SCIPsetSeparating() for details: + // https://scip.zib.de/doc/html/group__ParameterMethods.php#gad0c64e3e9b8def72fd8a7d3d9dce7729 + MetaParamValue separating = 4; + + // See https://scip.zib.de/doc/html/PARAMETERS.php for a list of all SCIP + // parameters. + map bool_params = 5; + map int_params = 6; + map long_params = 7; + map real_params = 8; + map char_params = 9; + map string_params = 10; + + // /////////////////////////////////////////////////////////////////////////// + // gSCIP only parameters + // /////////////////////////////////////////////////////////////////////////// + + // Disable all terminal output (override all logging parameters). To control + // only the search logs, see also the SCIP parameter display/verblevel and + // from gscip_parameters.h, SetLogLevel() and SetOutputEnabled(). + bool silence_output = 11; + + // Log solver metrics to terminal when finished solving (unless silenced). + bool print_detailed_solving_stats = 12; + + // Write out the model in SCIP's text format before solving to the terminal + // (unless silenced). + bool print_scip_model = 13; + + // If nonempty, search logs are written here INSTEAD OF out to terminal. See + // also the SCIP parameter display/verblevel and from gscip_parameters.h, the + // functions SetLogLevel() and SetOutputEnabled() for configuring the search + // logs. + // + // Does not use gfile, can only write to local disk. + string search_logs_filename = 14; + + // If non-empty, write detailed_solving_stats to a file. Can be set + // independently from print_detailed_solving_stats. + // + // Does not use gfile, can only write to local disk. + string detailed_solving_stats_filename = 15; + + // If nonempty, out the model in SCIP's text format to a file before solving. + // Can be set independently of print_scip_model. + // + // Does not use gfile, can only write to local disk. + string scip_model_filename = 16; + + // How many solutions to retrieve from the solution pool (if this many exist). + // At least one solution will always be returned, even if num_solutions < 1. + int32 num_solutions = 17; +} + +// TODO(user): this should be machine generated by script and contain all of +// https://scip.zib.de/doc/html/group__PublicSolvingStatsMethods.php +message GScipSolvingStats { + // The objective value of the best solution (or the cutoff). If no solution is + // found, returns +inf for minimization and -inf for maximization. Equivalent + // to SCIPgetPrimalbound(). + double best_objective = 1; + // The best proven bound on the object (e.g. through the LP relaxation). + // Returns +inf for maximization and -inf for minimization if no bound was + // found. Equivalent to SCIPgetDualBound(). + double best_bound = 2; + int64 primal_simplex_iterations = 3; + int64 dual_simplex_iterations = 4; + // nlp_iterations in SCIP. The total number of LP steps taken, i.e. primal + // simplex iterations + dual simplex iterations + barrier iterations. + int64 total_lp_iterations = 5; + // NTotalNodes in SCIP. + // This is the total number of nodes used in the solve, potentially across + // multiple branch-and-bound trees. Use limits/totalnodes (rather than + // limits/nodes) to control this value. + int64 node_count = 6; + // FirstLPDualboundRoot in SCIP. The bound obtained from the first LP solve + // at the root node. + double first_lp_relaxation_bound = 7; + // DualboundRoot in SCIP. The bound obtained at the root node, possibly after + // multiple rounds of cuts. + double root_node_bound = 8; + // A deterministic measure of work done during the solve. The units of this + // field are specific to SCIP. + double deterministic_time = 9; +} + +message GScipOutput { + // See https://scip.zib.de/doc/html/type__stat_8h.php + enum Status { + UNKNOWN = 0; + USER_INTERRUPT = 1; + NODE_LIMIT = 2; + TOTAL_NODE_LIMIT = 3; + STALL_NODE_LIMIT = 4; + TIME_LIMIT = 5; + MEM_LIMIT = 6; + GAP_LIMIT = 7; + SOL_LIMIT = 8; + BEST_SOL_LIMIT = 9; + RESTART_LIMIT = 10; + OPTIMAL = 11; + INFEASIBLE = 12; + UNBOUNDED = 13; + INF_OR_UNBD = 14; + TERMINATE = 15; + // WARNING(rander): we add some extra status values beyond SCIP here + INVALID_SOLVER_PARAMETERS = 16; + } + Status status = 1; + string status_detail = 2; + GScipSolvingStats stats = 3; +} diff --git a/ortools/gscip/gscip_ext.cc b/ortools/gscip/gscip_ext.cc new file mode 100644 index 0000000000..6ecd183f7d --- /dev/null +++ b/ortools/gscip/gscip_ext.cc @@ -0,0 +1,204 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/gscip/gscip_ext.h" + +#include "ortools/base/logging.h" +#include "ortools/base/status_macros.h" + +namespace operations_research { + +namespace { + +std::string MaybeExtendName(const std::string& base_name, + const std::string& extension) { + if (base_name.empty()) { + return ""; + } + return absl::StrCat(base_name, "/", extension); +} + +} // namespace + +GScipLinearExpr::GScipLinearExpr(SCIP_VAR* variable) { terms[variable] = 1.0; } + +GScipLinearExpr::GScipLinearExpr(double offset) : offset(offset) {} + +GScipLinearExpr Difference(GScipLinearExpr left, const GScipLinearExpr& right) { + left.offset -= right.offset; + for (const auto& term : right.terms) { + left.terms[term.first] -= term.second; + } + return left; +} + +GScipLinearExpr Negate(GScipLinearExpr expr) { + expr.offset = -expr.offset; + for (auto& term : expr.terms) { + term.second = -term.second; + } + return expr; +} + +// Returns the range -inf <= left.terms - right.terms <= right.offset - +// left.offset +GScipLinearRange Le(const GScipLinearExpr left, const GScipLinearExpr& right) { + GScipLinearExpr diff = Difference(left, right); + GScipLinearRange result; + result.lower_bound = -std::numeric_limits::infinity(); + result.upper_bound = -diff.offset; + for (const auto& term : diff.terms) { + result.variables.push_back(term.first); + result.coefficients.push_back(term.second); + } + return result; +} + +absl::Status CreateAbs(GScip* gscip, SCIP_Var* x, SCIP_Var* abs_x, + const std::string& name) { + return CreateMaximum(gscip, GScipLinearExpr(abs_x), + {GScipLinearExpr(x), Negate(GScipLinearExpr(x))}, name); +} + +absl::Status CreateMaximum(GScip* gscip, const GScipLinearExpr& resultant, + const std::vector& terms, + const std::string& name) { + // TODO(user): it may be better to write this in terms of the disjuntive + // constraint, we need to support disjunctions in gscip.h to do this. + // + // z_i in {0,1}, indicates if y = x_i + // + // x_i <= y + // z_i => y <= x_i + // \sum_i z_i == 1 + std::vector indicators; + for (int i = 0; i < terms.size(); ++i) { + auto z = gscip->AddVariable(0.0, 1.0, 0.0, GScipVarType::kInteger, + MaybeExtendName(name, absl::StrCat("z_", i))); + RETURN_IF_ERROR(z.status()); + indicators.push_back(*z); + } + + for (int i = 0; i < terms.size(); ++i) { + // x_i <= y + RETURN_IF_ERROR( + gscip + ->AddLinearConstraint( + Le(terms.at(i), resultant), + MaybeExtendName(name, absl::StrCat("x_", i, "_le_y"))) + .status()); + // z_i => y <= x_i + { + GScipLinearRange y_less_x = Le(resultant, terms.at(i)); + CHECK_EQ(y_less_x.lower_bound, -std::numeric_limits::infinity()); + GScipIndicatorConstraint ind; + ind.indicator_variable = indicators.at(i); + ind.variables = y_less_x.variables; + ind.coefficients = y_less_x.coefficients; + ind.upper_bound = y_less_x.upper_bound; + RETURN_IF_ERROR( + gscip + ->AddIndicatorConstraint( + ind, MaybeExtendName( + name, absl::StrCat("y_le__x_", i, "_if_z_", i))) + .status()); + } + } + + // sum_i z_i = 1. + GScipLinearRange z_use; + z_use.upper_bound = 1.0; + z_use.lower_bound = 1.0; + z_use.variables = indicators; + z_use.coefficients = std::vector(indicators.size(), 1.0); + + return gscip->AddLinearConstraint(z_use, MaybeExtendName(name, "one_z")) + .status(); +} + +absl::Status CreateMinimum(GScip* gscip, const GScipLinearExpr& resultant, + const std::vector& terms, + const std::string& name) { + std::vector negated_terms; + negated_terms.reserve(terms.size()); + for (const GScipLinearExpr& e : terms) { + negated_terms.push_back(Negate(e)); + } + return CreateMaximum(gscip, Negate(resultant), negated_terms, name); +} + +absl::Status AddQuadraticObjectiveTerm( + GScip* gscip, std::vector quadratic_variables1, + std::vector quadratic_variables2, + std::vector quadratic_coefficients, const std::string& name) { + constexpr double kInf = std::numeric_limits::infinity(); + auto obj_term = + gscip->AddVariable(-kInf, kInf, 1.0, GScipVarType::kContinuous, + MaybeExtendName(name, "obj")); + RETURN_IF_ERROR(obj_term.status()); + GScipQuadraticRange range; + range.quadratic_variables1 = quadratic_variables1; + range.quadratic_variables2 = quadratic_variables2; + range.quadratic_coefficients = quadratic_coefficients; + range.linear_coefficients = {-1.0}; + range.linear_variables = {*obj_term}; + if (gscip->ObjectiveIsMaximize()) { + // maximize z + // z <= Q(x, y) + // => 0 <= Q(x, y) - z <= inf + range.lower_bound = 0.0; + } else { + // minimize z + // z >= Q(x, y) + // => 0 >= Q(x, y) - z >= -inf + range.upper_bound = 0.0; + } + return gscip->AddQuadraticConstraint(range, MaybeExtendName(name, "cons")) + .status(); +} + +absl::Status CreateIndicatorRange( + GScip* gscip, const GScipIndicatorRangeConstraint& indicator_range, + const std::string& name, const GScipConstraintOptions& options) { + if (std::isfinite(indicator_range.range.upper_bound)) { + GScipIndicatorConstraint ub_constraint; + ub_constraint.upper_bound = indicator_range.range.upper_bound; + ub_constraint.variables = indicator_range.range.variables; + ub_constraint.coefficients = indicator_range.range.coefficients; + ub_constraint.indicator_variable = indicator_range.indicator_variable; + ub_constraint.negate_indicator = indicator_range.negate_indicator; + RETURN_IF_ERROR(gscip + ->AddIndicatorConstraint( + ub_constraint, MaybeExtendName(name, "ub"), options) + .status()); + } + if (std::isfinite(indicator_range.range.lower_bound)) { + // want z -> lb <= a * x + // <=> z -> -lb >= -a * x + GScipIndicatorConstraint lb_constraint; + lb_constraint.upper_bound = -indicator_range.range.lower_bound; + lb_constraint.variables = indicator_range.range.variables; + for (const double c : indicator_range.range.coefficients) { + lb_constraint.coefficients.push_back(-c); + } + lb_constraint.indicator_variable = indicator_range.indicator_variable; + lb_constraint.negate_indicator = indicator_range.negate_indicator; + RETURN_IF_ERROR(gscip + ->AddIndicatorConstraint( + lb_constraint, MaybeExtendName(name, "lb"), options) + .status()); + } + return absl::OkStatus(); +} + +} // namespace operations_research diff --git a/ortools/gscip/gscip_ext.h b/ortools/gscip/gscip_ext.h new file mode 100644 index 0000000000..5065da9949 --- /dev/null +++ b/ortools/gscip/gscip_ext.h @@ -0,0 +1,104 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Additional nonlinear constraints not supported directly by SCIP. +// +// The primary purpose of this file is to support the nonlinear constraints of +// MPSolver proto API. +// +// WARNING(rander): as these constraints are not natively supported in SCIP, +// they will generally not be a single SCIP_CONS* created, but will typically +// result in multiple SCIP_CONS* and SCIP_VAR* being created. Direct access to +// these intermediate variables and constraints is currently not provided. +// +// TODO(user): either implement with SCIP constraint handlers or use a solver +// independent implementation. +#ifndef OR_TOOLS_GSCIP_GSCIP_EXT_H_ +#define OR_TOOLS_GSCIP_GSCIP_EXT_H_ + +#include "absl/status/status.h" +#include "ortools/gscip/gscip.h" +#include "scip/scip.h" +#include "scip/scip_prob.h" +#include "scip/type_cons.h" +#include "scip/type_scip.h" +#include "scip/type_var.h" + +namespace operations_research { + +// Adds the constraint y = abs(x). May create auxiliary variables. Supports +// unbounded x. +absl::Status CreateAbs(GScip* gscip, SCIP_Var* x, SCIP_Var* abs_x, + const std::string& name = ""); + +// TODO(user): delete this type and the methods below, use a generic version +// templated on the variable type that supports operator overloads. +struct GScipLinearExpr { + absl::flat_hash_map terms; + double offset = 0.0; + + GScipLinearExpr() = default; + explicit GScipLinearExpr(SCIP_VAR* variable); + explicit GScipLinearExpr(double offset); +}; + +// Returns left - right. +GScipLinearExpr Difference(GScipLinearExpr left, const GScipLinearExpr& right); + +// Returns -expr. +GScipLinearExpr Negate(GScipLinearExpr expr); + +// Returns the range -inf <= left.terms - right.terms <= right.offset - +// left.offset +GScipLinearRange Le(const GScipLinearExpr left, const GScipLinearExpr& right); + +// Adds the constraint resultant = maximum(terms). Supports unbounded variables +// in terms. +absl::Status CreateMaximum(GScip* gscip, const GScipLinearExpr& resultant, + const std::vector& terms, + const std::string& name = ""); + +// Adds the constraint resultant = minimum(terms). Supports unbounded variables +// in terms. +absl::Status CreateMinimum(GScip* gscip, const GScipLinearExpr& resultant, + const std::vector& terms, + const std::string& name = ""); + +// Models the constraint z = 1 => lb <= ax <= ub +// If negate_indicator, then instead: z = 0 => lb <= ax <= ub +struct GScipIndicatorRangeConstraint { + SCIP_VAR* indicator_variable = nullptr; + bool negate_indicator = false; + GScipLinearRange range; +}; + +// Supports unbounded variables in indicator_range.range.variables. +absl::Status CreateIndicatorRange( + GScip* gscip, const GScipIndicatorRangeConstraint& indicator_range, + const std::string& name = "", + const GScipConstraintOptions& options = GScipConstraintOptions()); + +// WARNING: DO NOT CHANGE THE OBJECTIVE DIRECTION AFTER CALLING THIS METHOD. +// +// This is implemented by modeling the quadratic term with an an inequality +// constraint and a single extra variable, which is then added to the objective. +// The inequality will be in the wrong direction if you change the objective +// direction after calling this method. +absl::Status AddQuadraticObjectiveTerm( + GScip* gscip, std::vector quadratic_variables1, + std::vector quadratic_variables2, + std::vector quadratic_coefficients, const std::string& name = ""); + +} // namespace operations_research + +#endif // OR_TOOLS_GSCIP_GSCIP_EXT_H_ diff --git a/ortools/gscip/gscip_parameters.cc b/ortools/gscip/gscip_parameters.cc new file mode 100644 index 0000000000..583257637a --- /dev/null +++ b/ortools/gscip/gscip_parameters.cc @@ -0,0 +1,124 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/gscip/gscip_parameters.h" + +#include "ortools/base/logging.h" + +namespace operations_research { + +// NOTE(user): the open source build for proto is less accepting of +// absl::string_view than internally, so we do more conversions than would +// appear necessary. +namespace { +constexpr absl::string_view kLimitsTime = "limits/time"; +constexpr absl::string_view kParallelMaxNThreads = "parallel/maxnthreads"; +constexpr absl::string_view kDisplayVerbLevel = "display/verblevel"; +constexpr absl::string_view kRandomSeedParam = "randomization/randomseedshift"; +} // namespace + +void SetTimeLimit(const absl::Duration time_limit, + GScipParameters* parameters) { + if (time_limit < absl::Seconds(1e20) && time_limit > absl::Duration()) { + (*parameters->mutable_real_params())[std::string(kLimitsTime)] = + absl::ToDoubleSeconds(time_limit); + } else { + parameters->mutable_real_params()->erase(std::string(kLimitsTime)); + } +} + +absl::Duration TimeLimit(const GScipParameters& parameters) { + if (parameters.real_params().contains(std::string(kLimitsTime))) { + const double scip_limit = + parameters.real_params().at(std::string(kLimitsTime)); + if (scip_limit >= 1e20) { + return absl::InfiniteDuration(); + } else if (scip_limit <= 0.0) { + return absl::Duration(); + } else { + return absl::Seconds(scip_limit); + } + } + return absl::InfiniteDuration(); +} + +bool TimeLimitSet(const GScipParameters& parameters) { + return parameters.real_params().contains(std::string(kLimitsTime)); +} + +void SetMaxNumThreads(int num_threads, GScipParameters* parameters) { + CHECK_GE(num_threads, 1); + (*parameters->mutable_int_params())[std::string(kParallelMaxNThreads)] = + num_threads; +} + +int MaxNumThreads(const GScipParameters& parameters) { + if (parameters.int_params().contains(std::string(kParallelMaxNThreads))) { + return parameters.int_params().at(std::string(kParallelMaxNThreads)); + } + return 1; +} + +bool MaxNumThreadsSet(const GScipParameters& parameters) { + return parameters.int_params().contains(std::string(kParallelMaxNThreads)); +} + +void SetLogLevel(GScipParameters* parameters, int log_level) { + CHECK_GE(log_level, 0); + CHECK_LE(log_level, 5); + (*parameters->mutable_int_params())[std::string(kDisplayVerbLevel)] = + log_level; +} + +int LogLevel(const GScipParameters& parameters) { + return parameters.int_params().contains(std::string(kDisplayVerbLevel)) + ? parameters.int_params().at(std::string(kDisplayVerbLevel)) + : 4; +} +bool LogLevelSet(const GScipParameters& parameters) { + return parameters.int_params().contains(std::string(kDisplayVerbLevel)); +} + +void SetOutputEnabled(GScipParameters* parameters, bool output_enabled) { + if (output_enabled) { + parameters->mutable_int_params()->erase(std::string(kDisplayVerbLevel)); + } else { + (*parameters->mutable_int_params())[std::string(kDisplayVerbLevel)] = 0; + } +} +bool OutputEnabled(const GScipParameters& parameters) { + return !parameters.int_params().contains(std::string(kDisplayVerbLevel)) || + (parameters.int_params().at(std::string(kDisplayVerbLevel)) > 0); +} + +bool OutputEnabledSet(const GScipParameters& parameters) { + return LogLevelSet(parameters); +} + +void SetRandomSeed(GScipParameters* parameters, int random_seed) { + random_seed = std::max(0, random_seed); + (*parameters->mutable_int_params())[std::string(kRandomSeedParam)] = + random_seed; +} + +int RandomSeed(const GScipParameters& parameters) { + if (RandomSeedSet(parameters)) { + return parameters.int_params().at(std::string(kRandomSeedParam)); + } + return -1; // Unset value. +} + +bool RandomSeedSet(const GScipParameters& parameters) { + return parameters.int_params().contains(std::string(kRandomSeedParam)); +} +} // namespace operations_research diff --git a/ortools/gscip/gscip_parameters.h b/ortools/gscip/gscip_parameters.h new file mode 100644 index 0000000000..3f59c6832b --- /dev/null +++ b/ortools/gscip/gscip_parameters.h @@ -0,0 +1,56 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_GSCIP_GSCIP_PARAMETERS_H_ +#define OR_TOOLS_GSCIP_GSCIP_PARAMETERS_H_ + +#include "absl/time/time.h" +#include "ortools/gscip/gscip.pb.h" + +namespace operations_research { + +void SetTimeLimit(absl::Duration time_limit, GScipParameters* parameters); +absl::Duration TimeLimit(const GScipParameters& parameters); +bool TimeLimitSet(const GScipParameters& parameters); + +// CHECK fails if num_threads < 1. +void SetMaxNumThreads(int num_threads, GScipParameters* parameters); + +// Returns 1 if the number of threads it not specified. +int MaxNumThreads(const GScipParameters& parameters); +bool MaxNumThreadsSet(const GScipParameters& parameters); + +// log_level must be in [0, 5], where 0 is none, 5 is most verbose, and the +// default is 4. CHECK fails on bad log_level. Default level displays standard +// search logs. +void SetLogLevel(GScipParameters* parameters, int log_level); +int LogLevel(const GScipParameters& parameters); +bool LogLevelSet(const GScipParameters& parameters); + +// Sets the log level to 4 if enabled, 0 if disabled (see above). +void SetOutputEnabled(GScipParameters* parameters, bool output_enabled); +// Checks if the log level is equal to zero. +bool OutputEnabled(const GScipParameters& parameters); +bool OutputEnabledSet(const GScipParameters& parameters); + +// Sets an initial seed (shift) for all pseudo-random number generators used +// within SCIP. Valid values are [0:INT_MAX] i.e. [0:2^31-1]. If an invalid +// value is passed, 0 would be stored instead. +void SetRandomSeed(GScipParameters* parameters, int random_seed); +// Returns -1 if unset. +int RandomSeed(const GScipParameters& parameters); +bool RandomSeedSet(const GScipParameters& parameters); + +} // namespace operations_research + +#endif // OR_TOOLS_GSCIP_GSCIP_PARAMETERS_H_ diff --git a/ortools/gscip/legacy_scip_params.cc b/ortools/gscip/legacy_scip_params.cc new file mode 100644 index 0000000000..875bfb5848 --- /dev/null +++ b/ortools/gscip/legacy_scip_params.cc @@ -0,0 +1,116 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/gscip/legacy_scip_params.h" + +#include + +#include "absl/status/status.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" +#include "ortools/linear_solver/scip_helper_macros.h" +#include "scip/scip.h" +#include "scip/scip_numerics.h" +#include "scip/scip_param.h" +#include "scip/struct_paramset.h" +#include "scip/type_paramset.h" + +namespace operations_research { + +absl::Status LegacyScipSetSolverSpecificParameters( + const std::string& parameters, SCIP* scip) { + for (const auto parameter : absl::StrSplit(parameters, absl::ByAnyChar(",\n"), + absl::SkipWhitespace())) { + std::vector key_value = absl::StrSplit( + parameter, absl::ByAnyChar("= "), absl::SkipWhitespace()); + if (key_value.size() != 2) { + return absl::InvalidArgumentError( + absl::StrFormat("Cannot parse parameter '%s'. Expected format is " + "'parameter/name = value'", + parameter)); + } + + std::string name = key_value[0]; + absl::RemoveExtraAsciiWhitespace(&name); + std::string value = key_value[1]; + absl::RemoveExtraAsciiWhitespace(&value); + const double infinity = SCIPinfinity(scip); + + SCIP_PARAM* param = SCIPgetParam(scip, name.c_str()); + if (param == nullptr) { + return absl::InvalidArgumentError( + absl::StrFormat("Invalid parameter name '%s'", name)); + } + switch (param->paramtype) { + case SCIP_PARAMTYPE_BOOL: { + bool parsed_value; + if (absl::SimpleAtob(value, &parsed_value)) { + RETURN_IF_SCIP_ERROR( + SCIPsetBoolParam(scip, name.c_str(), parsed_value)); + continue; + } + break; + } + case SCIP_PARAMTYPE_INT: { + int parsed_value; + if (absl::SimpleAtoi(value, &parsed_value)) { + RETURN_IF_SCIP_ERROR( + SCIPsetIntParam(scip, name.c_str(), parsed_value)); + continue; + } + break; + } + case SCIP_PARAMTYPE_LONGINT: { + int64_t parsed_value; + if (absl::SimpleAtoi(value, &parsed_value)) { + RETURN_IF_SCIP_ERROR(SCIPsetLongintParam( + scip, name.c_str(), static_cast(parsed_value))); + continue; + } + break; + } + case SCIP_PARAMTYPE_REAL: { + double parsed_value; + if (absl::SimpleAtod(value, &parsed_value)) { + if (parsed_value > infinity) parsed_value = infinity; + RETURN_IF_SCIP_ERROR( + SCIPsetRealParam(scip, name.c_str(), parsed_value)); + continue; + } + break; + } + case SCIP_PARAMTYPE_CHAR: { + if (value.size() == 1) { + RETURN_IF_SCIP_ERROR(SCIPsetCharParam(scip, name.c_str(), value[0])); + continue; + } + break; + } + case SCIP_PARAMTYPE_STRING: { + if (value.front() == '"' && value.back() == '"') { + value.erase(value.begin()); + value.erase(value.end() - 1); + } + RETURN_IF_SCIP_ERROR( + SCIPsetStringParam(scip, name.c_str(), value.c_str())); + continue; + } + } + return absl::InvalidArgumentError( + absl::StrFormat("Invalid parameter value '%s'", parameter)); + } + return absl::OkStatus(); +} + +} // namespace operations_research diff --git a/ortools/gscip/legacy_scip_params.h b/ortools/gscip/legacy_scip_params.h new file mode 100644 index 0000000000..dc003c7815 --- /dev/null +++ b/ortools/gscip/legacy_scip_params.h @@ -0,0 +1,29 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_GSCIP_LEGACY_SCIP_PARAMS_H_ +#define OR_TOOLS_GSCIP_LEGACY_SCIP_PARAMS_H_ + +#include + +#include "absl/status/status.h" +#include "scip/scip.h" +#include "scip/type_scip.h" + +namespace operations_research { + +absl::Status LegacyScipSetSolverSpecificParameters( + const std::string& parameters, SCIP* scip); +} + +#endif // OR_TOOLS_GSCIP_LEGACY_SCIP_PARAMS_H_ diff --git a/ortools/java/com/google/ortools/sat/CpModel.java b/ortools/java/com/google/ortools/sat/CpModel.java index 6e7b50fe7f..ffb8817479 100644 --- a/ortools/java/com/google/ortools/sat/CpModel.java +++ b/ortools/java/com/google/ortools/sat/CpModel.java @@ -1004,7 +1004,7 @@ public final class CpModel { } /** Write the model as a ascii protocol buffer to 'file'.*/ - public Boolean ExportToFile(String file) { + public Boolean exportToFile(String file) { return SatHelper.writeModelToFile(model(), file); } diff --git a/ortools/linear_solver/BUILD b/ortools/linear_solver/BUILD index 3810fd4d60..b8c8aea7a5 100644 --- a/ortools/linear_solver/BUILD +++ b/ortools/linear_solver/BUILD @@ -65,6 +65,7 @@ cc_library( "sat_interface.cc", "sat_proto_solver.cc", "sat_solver_utils.cc", + "scip_callback.cc", "scip_interface.cc", "scip_proto_solver.cc", ] + select({ @@ -95,6 +96,7 @@ cc_library( "model_validator.h", "sat_proto_solver.h", "sat_solver_utils.h", + "scip_callback.h", "scip_helper_macros.h", "scip_proto_solver.h", ], @@ -118,6 +120,7 @@ cc_library( "//ortools/bop:integral_solver", "//ortools/glop:lp_solver", "//ortools/glop:parameters_cc_proto", + "//ortools/gscip:legacy_scip_params", "//ortools/port:file", "//ortools/port:proto_utils", "//ortools/sat:cp_model_cc_proto", @@ -131,10 +134,18 @@ cc_library( }), ) +cc_library( + name = "scip_helper_macros", + hdrs = ["scip_helper_macros.h"], + deps = [ + "//ortools/base:status_macros", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings:str_format", + ], +) copy_file( name = "lpi_glop", src = "@scip//:scip-7.0.1/src/lpi/lpi_glop.cpp", out = "lpi_glop.cpp", ) - diff --git a/ortools/linear_solver/gurobi_environment.cc b/ortools/linear_solver/gurobi_environment.cc index 07386dad17..872e65805f 100644 --- a/ortools/linear_solver/gurobi_environment.cc +++ b/ortools/linear_solver/gurobi_environment.cc @@ -39,9 +39,16 @@ absl::Status LoadGurobiEnvironment(GRBenv **env) { std::function GRBaddrangeconstr = nullptr; +std::function + GRBaddvar = nullptr; std::function GRBaddvars = nullptr; +std::function + GRBchgcoeffs = nullptr; std::function GRBfreeenv = nullptr; std::function GRBfreemodel = nullptr; std::function @@ -131,7 +138,9 @@ std::string gurobi_library_path; void LoadGurobiFunctions() { gurobi_dynamic_library->GetFunction(&GRBaddrangeconstr, NAMEOF(GRBaddrangeconstr)); + gurobi_dynamic_library->GetFunction(&GRBaddvar, NAMEOF(GRBaddvar)); gurobi_dynamic_library->GetFunction(&GRBaddvars, NAMEOF(GRBaddvars)); + gurobi_dynamic_library->GetFunction(&GRBchgcoeffs, NAMEOF(GRBchgcoeffs)); gurobi_dynamic_library->GetFunction(&GRBfreeenv, NAMEOF(GRBfreeenv)); gurobi_dynamic_library->GetFunction(&GRBfreemodel, NAMEOF(GRBfreemodel)); gurobi_dynamic_library->GetFunction(&GRBgetcharattrelement, diff --git a/ortools/linear_solver/gurobi_environment.h b/ortools/linear_solver/gurobi_environment.h index d9788ed9b9..1c508dffce 100644 --- a/ortools/linear_solver/gurobi_environment.h +++ b/ortools/linear_solver/gurobi_environment.h @@ -31,15 +31,23 @@ typedef struct _GRBenv GRBenv; } namespace operations_research { -absl::Status LoadGurobiEnvironment(GRBenv **env); +absl::Status LoadGurobiEnvironment(GRBenv** env); #define CB_ARGS GRBmodel *model, void *cbdata, int where, void *usrdata extern std::function GRBaddrangeconstr; + +extern std::function + GRBaddvar; + extern std::function GRBaddvars; + +extern std::function GRBchgcoeffs; extern std::function GRBfreeenv; extern std::function GRBfreemodel; extern std::function diff --git a/ortools/linear_solver/gurobi_interface.cc b/ortools/linear_solver/gurobi_interface.cc index 191ed9fb20..af446f9109 100644 --- a/ortools/linear_solver/gurobi_interface.cc +++ b/ortools/linear_solver/gurobi_interface.cc @@ -11,6 +11,37 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Gurobi backend to MPSolver. +// +// Implementation Notes: +// +// Incrementalism (last updated June 29, 2020): For solving both LPs and MIPs, +// Gurobi attempts to reuse information from previous solves, potentially +// giving a faster solve time. MPSolver supports this for the following problem +// modification types: +// * Adding a variable, +// * Adding a linear constraint, +// * Updating a variable bound, +// * Updating an objective coefficient or the objective offset (note that in +// Gurobi 7.5 LP solver, there is a bug if you update only the objective +// offset and nothing else). +// * Updating a coefficient in the constraint matrix. +// * Updating the type of variable (integer, continuous) +// * Changing the optimization direction. +// Updates of the following types will force a resolve from scratch: +// * Updating the upper or lower bounds of a linear constraint. Note that in +// MPSolver's model, this includes updating the sense (le, ge, eq, range) of +// a linear constraint. +// * Clearing a constraint +// Any model containing indicator constraints is considered "non-incremental" +// and will always solve from scratch. +// +// The above limitations are largely due MPSolver and this file, not Gurobi. +// +// Warning(rander): the interactions between callbacks and incrementalism are +// poorly tested, proceed with caution. +// + #include #include #include @@ -27,7 +58,6 @@ #include "ortools/base/integral_types.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" -#include "ortools/base/statusor.h" #include "ortools/base/timer.h" #include "ortools/linear_solver/gurobi_environment.h" #include "ortools/linear_solver/gurobi_proto_solver.h" @@ -155,6 +185,19 @@ class GurobiInterface : public MPSolverInterface { private: // Sets all parameters in the underlying solver. void SetParameters(const MPSolverParameters& param) override; + // Sets solver-specific parameters (avoiding using files). The previous + // implementations supported multi-line strings of the form: + // parameter_i value_i\n + // We extend support for strings of the form: + // parameter1=value1,....,parametern=valuen + // or for strings of the form: + // parameter1 value1, ... ,parametern valuen + // which are easier to set in the command line. + // This implementations relies on SetSolverSpecificParameters, which has the + // extra benefit of unifying the way we handle specific parameters for both + // proto-based solves and for MPModel solves. + bool SetSolverSpecificParametersAsString( + const std::string& parameters) override; // Sets each parameter in the underlying solver. void SetRelativeMipGap(double value) override; void SetPrimalTolerance(double value) override; @@ -171,6 +214,21 @@ class GurobiInterface : public MPSolverInterface { MPSolver::BasisStatus TransformGRBConstraintBasisStatus( int gurobi_basis_status, int constraint_index) const; + // See the implementation note at the top of file on incrementalism. + bool ModelIsNonincremental() const; + + void SetIntAttr(const char* name, int value); + int GetIntAttr(const char* name) const; + void SetDoubleAttr(const char* name, double value); + double GetDoubleAttr(const char* name) const; + void SetIntAttrElement(const char* name, int index, int value); + int GetIntAttrElement(const char* name, int index) const; + void SetDoubleAttrElement(const char* name, int index, double value); + double GetDoubleAttrElement(const char* name, int index) const; + std::vector GetDoubleAttrArray(const char* name, int elements); + void SetCharAttrElement(const char* name, int index, char value); + char GetCharAttrElement(const char* name, int index) const; + void CheckedGurobiCall(int err) const; int SolutionCount() const; @@ -181,6 +239,24 @@ class GurobiInterface : public MPSolverInterface { int current_solution_index_; MPCallback* callback_ = nullptr; bool update_branching_priorities_ = false; + // Has length equal to the number of MPVariables in + // MPSolverInterface::solver_. Values are the index of the corresponding + // Gurobi variable. Note that Gurobi may have additional auxiliary variables + // not represented by MPVariables, such as those created by two-sided range + // constraints. + std::vector mp_var_to_gurobi_var_; + // Has length equal to the number of MPConstraints in + // MPSolverInterface::solver_. Values are the index of the corresponding + // linear (or range) constraint in Gurobi, or -1 if no such constraint exists + // (e.g. for indicator constraints). + std::vector mp_cons_to_gurobi_linear_cons_; + // Should match the Gurobi model after it is updated. + int num_gurobi_vars_ = 0; + // Should match the Gurobi model after it is updated. + // NOTE(user): indicator constraints are not counted below. + int num_gurobi_linear_cons_ = 0; + // See the implementation note at the top of file on incrementalism. + bool had_nonincremental_change_ = false; }; namespace { @@ -199,7 +275,9 @@ struct GurobiInternalCallbackContext { class GurobiMPCallbackContext : public MPCallbackContext { public: - GurobiMPCallbackContext(GRBenv* env, bool might_add_cuts, + GurobiMPCallbackContext(GRBenv* env, + const std::vector* mp_var_to_gurobi_var, + int num_gurobi_vars, bool might_add_cuts, bool might_add_lazy_constraints); // Implementation of the interface. @@ -231,12 +309,9 @@ class GurobiMPCallbackContext : public MPCallbackContext { void AddGeneratedConstraint(const LinearRange& linear_range, GRBConstraintFunction grb_constraint_function); - // Returns the number of variables in the Gurobi model. - // WARNING(rander): This is not the same as solver_->variables_.size(), the - // use of range constraints adds new variables to the Gurobi model. - int NumGurobiVariables() const; - GRBenv* const env_; + const std::vector* const mp_var_to_gurobi_var_; + const int num_gurobi_vars_; const bool might_add_cuts_; const bool might_add_lazy_constraints_; @@ -244,7 +319,7 @@ class GurobiMPCallbackContext : public MPCallbackContext { // Stateful, updated before each call to the callback. GurobiInternalCallbackContext current_gurobi_internal_callback_context_; bool variable_values_extracted_ = false; - std::vector variable_values_; + std::vector gurobi_variable_values_; }; void GurobiMPCallbackContext::CheckedGurobiCall(int gurobi_error_code) const { @@ -252,8 +327,11 @@ void GurobiMPCallbackContext::CheckedGurobiCall(int gurobi_error_code) const { } GurobiMPCallbackContext::GurobiMPCallbackContext( - GRBenv* env, bool might_add_cuts, bool might_add_lazy_constraints) + GRBenv* env, const std::vector* mp_var_to_gurobi_var, + int num_gurobi_vars, bool might_add_cuts, bool might_add_lazy_constraints) : env_(ABSL_DIE_IF_NULL(env)), + mp_var_to_gurobi_var_(ABSL_DIE_IF_NULL(mp_var_to_gurobi_var)), + num_gurobi_vars_(num_gurobi_vars), might_add_cuts_(might_add_cuts), might_add_lazy_constraints_(might_add_lazy_constraints) {} @@ -345,14 +423,14 @@ double GurobiMPCallbackContext::VariableValue(const MPVariable* variable) { ? GRB_CB_MIPNODE_REL : GRB_CB_MIPSOL_SOL; - variable_values_.resize(NumGurobiVariables()); + gurobi_variable_values_.resize(num_gurobi_vars_); CheckedGurobiCall(GRBcbget( current_gurobi_internal_callback_context_.gurobi_internal_callback_data, current_gurobi_internal_callback_context_.where, gurobi_get_var_param, - static_cast(variable_values_.data()))); + static_cast(gurobi_variable_values_.data()))); variable_values_extracted_ = true; } - return variable_values_[variable->index()]; + return gurobi_variable_values_[mp_var_to_gurobi_var_->at(variable->index())]; } template @@ -365,7 +443,8 @@ void GurobiMPCallbackContext::AddGeneratedConstraint( variable_indices.reserve(num_terms); variable_coefficients.reserve(num_terms); for (const auto& var_coef_pair : linear_range.linear_expr().terms()) { - variable_indices.push_back(var_coef_pair.first->index()); + variable_indices.push_back( + mp_var_to_gurobi_var_->at(var_coef_pair.first->index())); variable_coefficients.push_back(var_coef_pair.second); } if (std::isfinite(linear_range.upper_bound())) { @@ -413,10 +492,11 @@ double GurobiMPCallbackContext::SuggestSolution( "solution at: " << ToString(where); - std::vector full_solution(NumGurobiVariables(), GRB_UNDEFINED); + std::vector full_solution(num_gurobi_vars_, GRB_UNDEFINED); for (const auto& variable_value : solution) { const MPVariable* var = variable_value.first; - full_solution[var->index()] = variable_value.second; + full_solution[mp_var_to_gurobi_var_->at(var->index())] = + variable_value.second; } double objval; @@ -427,14 +507,6 @@ double GurobiMPCallbackContext::SuggestSolution( return objval; } -int GurobiMPCallbackContext::NumGurobiVariables() const { - int num_gurobi_variables = 0; - CheckedGurobiCall( - GRBgetintattr(current_gurobi_internal_callback_context_.model, "NumVars", - &num_gurobi_variables)); - return num_gurobi_variables; -} - struct MPCallbackWithGurobiContext { GurobiMPCallbackContext* context; MPCallback* callback; @@ -442,8 +514,8 @@ struct MPCallbackWithGurobiContext { // NOTE(user): This function must have this exact API, because we are passing // it to Gurobi as a callback. -int STDCALL CallbackImpl(GRBmodel* model, void* gurobi_internal_callback_data, - int where, void* raw_model_and_callback) { +int __stdcall CallbackImpl(GRBmodel* model, void* gurobi_internal_callback_data, + int where, void* raw_model_and_callback) { MPCallbackWithGurobiContext* const callback_with_context = static_cast(raw_model_and_callback); CHECK(callback_with_context != nullptr); @@ -463,6 +535,66 @@ void GurobiInterface::CheckedGurobiCall(int err) const { ::operations_research::CheckedGurobiCall(err, env_); } +void GurobiInterface::SetIntAttr(const char* name, int value) { + CheckedGurobiCall(GRBsetintattr(model_, name, value)); +} + +int GurobiInterface::GetIntAttr(const char* name) const { + int value; + CheckedGurobiCall(GRBgetintattr(model_, name, &value)); + return value; +} + +void GurobiInterface::SetDoubleAttr(const char* name, double value) { + CheckedGurobiCall(GRBsetdblattr(model_, name, value)); +} + +double GurobiInterface::GetDoubleAttr(const char* name) const { + double value; + CheckedGurobiCall(GRBgetdblattr(model_, name, &value)); + return value; +} + +void GurobiInterface::SetIntAttrElement(const char* name, int index, + int value) { + CheckedGurobiCall(GRBsetintattrelement(model_, name, index, value)); +} + +int GurobiInterface::GetIntAttrElement(const char* name, int index) const { + int value; + CheckedGurobiCall(GRBgetintattrelement(model_, name, index, &value)); + return value; +} + +void GurobiInterface::SetDoubleAttrElement(const char* name, int index, + double value) { + CheckedGurobiCall(GRBsetdblattrelement(model_, name, index, value)); +} +double GurobiInterface::GetDoubleAttrElement(const char* name, + int index) const { + double value; + CheckedGurobiCall(GRBgetdblattrelement(model_, name, index, &value)); + return value; +} + +std::vector GurobiInterface::GetDoubleAttrArray(const char* name, + int elements) { + std::vector results(elements); + CheckedGurobiCall( + GRBgetdblattrarray(model_, name, 0, elements, results.data())); + return results; +} + +void GurobiInterface::SetCharAttrElement(const char* name, int index, + char value) { + CheckedGurobiCall(GRBsetcharattrelement(model_, name, index, value)); +} +char GurobiInterface::GetCharAttrElement(const char* name, int index) const { + char value; + CheckedGurobiCall(GRBgetcharattrelement(model_, name, index, &value)); + return value; +} + // Creates a LP/MIP instance with the specified name and minimization objective. GurobiInterface::GurobiInterface(MPSolver* const solver, bool mip) : MPSolverInterface(solver), @@ -478,9 +610,7 @@ GurobiInterface::GurobiInterface(MPSolver* const solver, bool mip) nullptr, // ub nullptr, // vtype nullptr)); // varnanes - CheckedGurobiCall( - GRBsetintattr(model_, GRB_INT_ATTR_MODELSENSE, maximize_ ? -1 : 1)); - + SetIntAttr(GRB_INT_ATTR_MODELSENSE, maximize_ ? GRB_MAXIMIZE : GRB_MINIMIZE); CheckedGurobiCall( GRBsetintparam(env_, GRB_INT_PAR_THREADS, FLAGS_num_gurobi_threads)); } @@ -502,43 +632,41 @@ void GurobiInterface::Reset() { nullptr, // vtype nullptr)); // varnames ResetExtractionInformation(); + mp_var_to_gurobi_var_.clear(); + mp_cons_to_gurobi_linear_cons_.clear(); + num_gurobi_vars_ = 0; + num_gurobi_linear_cons_ = 0; + had_nonincremental_change_ = false; } void GurobiInterface::SetOptimizationDirection(bool maximize) { - sync_status_ = MUST_RELOAD; - // TODO(user,user): Fix, not yet working. - // InvalidateSolutionSynchronization(); - // CheckedGurobiCall(GRBsetintattr(model_, - // GRB_INT_ATTR_MODELSENSE, - // maximize_ ? -1 : 1)); + InvalidateSolutionSynchronization(); + SetIntAttr(GRB_INT_ATTR_MODELSENSE, maximize_ ? GRB_MAXIMIZE : GRB_MINIMIZE); } void GurobiInterface::SetVariableBounds(int var_index, double lb, double ub) { - sync_status_ = MUST_RELOAD; + InvalidateSolutionSynchronization(); + if (!had_nonincremental_change_ && variable_is_extracted(var_index)) { + SetDoubleAttrElement(GRB_DBL_ATTR_LB, mp_var_to_gurobi_var_.at(var_index), + lb); + SetDoubleAttrElement(GRB_DBL_ATTR_UB, mp_var_to_gurobi_var_.at(var_index), + ub); + } else { + sync_status_ = MUST_RELOAD; + } } -// Modifies integrality of an extracted variable. void GurobiInterface::SetVariableInteger(int index, bool integer) { - char current_type; - CheckedGurobiCall( - GRBgetcharattrelement(model_, GRB_CHAR_ATTR_VTYPE, index, ¤t_type)); - - if ((integer && - (current_type == GRB_INTEGER || current_type == GRB_BINARY)) || - (!integer && current_type == GRB_CONTINUOUS)) { - return; - } - InvalidateSolutionSynchronization(); - if (sync_status_ == MODEL_SYNCHRONIZED) { + if (!had_nonincremental_change_ && variable_is_extracted(index)) { char type_var; if (integer) { type_var = GRB_INTEGER; } else { type_var = GRB_CONTINUOUS; } - CheckedGurobiCall( - GRBsetcharattrelement(model_, GRB_CHAR_ATTR_VTYPE, index, type_var)); + SetCharAttrElement(GRB_CHAR_ATTR_VTYPE, mp_var_to_gurobi_var_.at(index), + type_var); } else { sync_status_ = MUST_RELOAD; } @@ -546,6 +674,17 @@ void GurobiInterface::SetVariableInteger(int index, bool integer) { void GurobiInterface::SetConstraintBounds(int index, double lb, double ub) { sync_status_ = MUST_RELOAD; + if (constraint_is_extracted(index)) { + had_nonincremental_change_ = true; + } + // TODO(user): this is nontrivial to make incremental: + // 1. Make sure it is a linear constraint (not an indicator or indicator + // range constraint). + // 2. Check if the sense of the constraint changes. If it was previously a + // range constraint, we can do nothing, and if it becomes a range + // constraint, we can do nothing. We could support range constraints if + // we tracked the auxiliary variable that is added with range + // constraints. } void GurobiInterface::AddRowConstraint(MPConstraint* const ct) { @@ -553,6 +692,7 @@ void GurobiInterface::AddRowConstraint(MPConstraint* const ct) { } bool GurobiInterface::AddIndicatorConstraint(MPConstraint* const ct) { + had_nonincremental_change_ = true; sync_status_ = MUST_RELOAD; return !IsContinuous(); } @@ -564,29 +704,65 @@ void GurobiInterface::AddVariable(MPVariable* const ct) { void GurobiInterface::SetCoefficient(MPConstraint* const constraint, const MPVariable* const variable, double new_value, double old_value) { - sync_status_ = MUST_RELOAD; + InvalidateSolutionSynchronization(); + if (!had_nonincremental_change_ && variable_is_extracted(variable->index()) && + constraint_is_extracted(constraint->index())) { + // Cannot be const, GRBchgcoeffs needs non-const pointer. + int grb_var = mp_var_to_gurobi_var_.at(variable->index()); + int grb_cons = mp_cons_to_gurobi_linear_cons_.at(constraint->index()); + if (grb_cons < 0) { + had_nonincremental_change_ = true; + sync_status_ = MUST_RELOAD; + } else { + // TODO(user): investigate if this has bad performance. + CheckedGurobiCall( + GRBchgcoeffs(model_, 1, &grb_cons, &grb_var, &new_value)); + } + } else { + sync_status_ = MUST_RELOAD; + } } void GurobiInterface::ClearConstraint(MPConstraint* const constraint) { + had_nonincremental_change_ = true; sync_status_ = MUST_RELOAD; + // TODO(user): this is difficult to make incremental, like + // SetConstraintBounds(), because of the auxiliary Gurobi variables that + // range constraints introduce. } void GurobiInterface::SetObjectiveCoefficient(const MPVariable* const variable, double coefficient) { - sync_status_ = MUST_RELOAD; + InvalidateSolutionSynchronization(); + if (!had_nonincremental_change_ && variable_is_extracted(variable->index())) { + SetDoubleAttrElement(GRB_DBL_ATTR_OBJ, + mp_var_to_gurobi_var_.at(variable->index()), + coefficient); + } else { + sync_status_ = MUST_RELOAD; + } } void GurobiInterface::SetObjectiveOffset(double value) { - sync_status_ = MUST_RELOAD; - // TODO(user,user): make it work. - // InvalidateSolutionSynchronization(); - // CheckedGurobiCall(GRBsetdblattr(model_, - // GRB_DBL_ATTR_OBJCON, - // solver_->Objective().offset())); - // CheckedGurobiCall(GRBupdatemodel(model_)); + InvalidateSolutionSynchronization(); + if (!had_nonincremental_change_) { + SetDoubleAttr(GRB_DBL_ATTR_OBJCON, value); + } else { + sync_status_ = MUST_RELOAD; + } } -void GurobiInterface::ClearObjective() { sync_status_ = MUST_RELOAD; } +void GurobiInterface::ClearObjective() { + InvalidateSolutionSynchronization(); + if (!had_nonincremental_change_) { + SetObjectiveOffset(0.0); + for (const auto& entry : solver_->objective_->coefficients_) { + SetObjectiveCoefficient(entry.first, 0.0); + } + } else { + sync_status_ = MUST_RELOAD; + } +} void GurobiInterface::BranchingPriorityChangedForVariable(int var_index) { update_branching_priorities_ = true; @@ -604,9 +780,7 @@ int64 GurobiInterface::iterations() const { int64 GurobiInterface::nodes() const { if (mip_) { if (!CheckSolutionIsSynchronized()) return kUnknownNumberOfNodes; - double nodes = 0; - CheckedGurobiCall(GRBgetdblattr(model_, GRB_DBL_ATTR_NODECOUNT, &nodes)); - return static_cast(nodes); + return static_cast(GetDoubleAttr(GRB_DBL_ATTR_NODECOUNT)); } else { LOG(DFATAL) << "Number of nodes only available for discrete problems."; return kUnknownNumberOfNodes; @@ -664,20 +838,21 @@ MPSolver::BasisStatus GurobiInterface::TransformGRBVarBasisStatus( MPSolver::BasisStatus GurobiInterface::TransformGRBConstraintBasisStatus( int gurobi_basis_status, int constraint_index) const { + const int grb_index = mp_cons_to_gurobi_linear_cons_.at(constraint_index); + if (grb_index < 0) { + LOG(DFATAL) << "Basis status not available for nonlinear constraints."; + return MPSolver::FREE; + } switch (gurobi_basis_status) { case GRB_BASIC: return MPSolver::BASIC; default: { // Non basic. - double slack = 0.0; double tolerance = 0.0; CheckedGurobiCall(GRBgetdblparam(GRBgetenv(model_), GRB_DBL_PAR_FEASIBILITYTOL, &tolerance)); - CheckedGurobiCall(GRBgetdblattrelement(model_, GRB_DBL_ATTR_SLACK, - constraint_index, &slack)); - char sense; - CheckedGurobiCall(GRBgetcharattrelement(model_, GRB_CHAR_ATTR_SENSE, - constraint_index, &sense)); + const double slack = GetDoubleAttrElement(GRB_DBL_ATTR_SLACK, grb_index); + const char sense = GetCharAttrElement(GRB_CHAR_ATTR_SENSE, grb_index); VLOG(4) << "constraint " << constraint_index << " , slack = " << slack << " , sense = " << sense; if (fabs(slack) <= tolerance) { @@ -699,8 +874,7 @@ MPSolver::BasisStatus GurobiInterface::TransformGRBConstraintBasisStatus( // Returns the basis status of a row. MPSolver::BasisStatus GurobiInterface::row_status(int constraint_index) const { - int optim_status = 0; - CheckedGurobiCall(GRBgetintattr(model_, GRB_INT_ATTR_STATUS, &optim_status)); + const int optim_status = GetIntAttr(GRB_INT_ATTR_STATUS); if (optim_status != GRB_OPTIMAL && optim_status != GRB_SUBOPTIMAL) { LOG(DFATAL) << "Basis status only available after a solution has " << "been found."; @@ -710,17 +884,20 @@ MPSolver::BasisStatus GurobiInterface::row_status(int constraint_index) const { LOG(DFATAL) << "Basis status only available for continuous problems."; return MPSolver::FREE; } - int gurobi_basis_status = 0; - CheckedGurobiCall(GRBgetintattrelement( - model_, GRB_INT_ATTR_CBASIS, constraint_index, &gurobi_basis_status)); + const int grb_index = mp_cons_to_gurobi_linear_cons_.at(constraint_index); + if (grb_index < 0) { + LOG(DFATAL) << "Basis status not available for nonlinear constraints."; + return MPSolver::FREE; + } + const int gurobi_basis_status = + GetIntAttrElement(GRB_INT_ATTR_CBASIS, grb_index); return TransformGRBConstraintBasisStatus(gurobi_basis_status, constraint_index); } // Returns the basis status of a column. MPSolver::BasisStatus GurobiInterface::column_status(int variable_index) const { - int optim_status = 0; - CheckedGurobiCall(GRBgetintattr(model_, GRB_INT_ATTR_STATUS, &optim_status)); + const int optim_status = GetIntAttr(GRB_INT_ATTR_STATUS); if (optim_status != GRB_OPTIMAL && optim_status != GRB_SUBOPTIMAL) { LOG(DFATAL) << "Basis status only available after a solution has " << "been found."; @@ -730,129 +907,134 @@ MPSolver::BasisStatus GurobiInterface::column_status(int variable_index) const { LOG(DFATAL) << "Basis status only available for continuous problems."; return MPSolver::FREE; } - int gurobi_basis_status = 0; - CheckedGurobiCall(GRBgetintattrelement(model_, GRB_INT_ATTR_VBASIS, - variable_index, &gurobi_basis_status)); + const int grb_index = mp_var_to_gurobi_var_.at(variable_index); + const int gurobi_basis_status = + GetIntAttrElement(GRB_INT_ATTR_VBASIS, grb_index); return TransformGRBVarBasisStatus(gurobi_basis_status); } // Extracts new variables. void GurobiInterface::ExtractNewVariables() { - CHECK(last_variable_index_ == 0 || - last_variable_index_ == solver_->variables_.size()); - CHECK(last_constraint_index_ == 0 || - last_constraint_index_ == solver_->constraints_.size()); const int total_num_vars = solver_->variables_.size(); if (total_num_vars > last_variable_index_) { - int num_new_variables = total_num_vars - last_variable_index_; - std::unique_ptr obj_coeffs(new double[num_new_variables]); - std::unique_ptr lb(new double[num_new_variables]); - std::unique_ptr ub(new double[num_new_variables]); - std::unique_ptr ctype(new char[num_new_variables]); - std::unique_ptr colname(new const char*[num_new_variables]); - - for (int j = 0; j < num_new_variables; ++j) { - MPVariable* const var = solver_->variables_[last_variable_index_ + j]; + // Define new variables. + for (int j = last_variable_index_; j < total_num_vars; ++j) { + const MPVariable* const var = solver_->variables_.at(j); set_variable_as_extracted(var->index(), true); - lb[j] = var->lb(); - ub[j] = var->ub(); - ctype.get()[j] = var->integer() && mip_ ? GRB_INTEGER : GRB_CONTINUOUS; - if (!var->name().empty()) { - colname[j] = var->name().c_str(); - } - obj_coeffs[j] = solver_->objective_->GetCoefficient(var); + CheckedGurobiCall(GRBaddvar( + model_, 0, // numnz + nullptr, // vind + nullptr, // vval + solver_->objective_->GetCoefficient(var), var->lb(), var->ub(), + var->integer() && mip_ ? GRB_INTEGER : GRB_CONTINUOUS, + var->name().empty() ? nullptr : var->name().c_str())); + mp_var_to_gurobi_var_.push_back(num_gurobi_vars_++); } + CheckedGurobiCall(GRBupdatemodel(model_)); + // Add new variables to existing constraints. + std::vector grb_cons_ind; + std::vector grb_var_ind; + std::vector coef; + for (int i = 0; i < last_constraint_index_; ++i) { + // If there was a nonincremental change/the model is not incremental (e.g. + // there is an indicator constraint), we should never enter this loop, as + // last_variable_index_ will be reset to zero before ExtractNewVariables() + // is called. + MPConstraint* const ct = solver_->constraints_[i]; + const int grb_ct_idx = mp_cons_to_gurobi_linear_cons_.at(ct->index()); + DCHECK_GE(grb_ct_idx, 0); + DCHECK(ct->indicator_variable() == nullptr); + for (const auto& entry : ct->coefficients_) { + const int var_index = entry.first->index(); + DCHECK(variable_is_extracted(var_index)); - CheckedGurobiCall(GRBaddvars(model_, num_new_variables, 0, nullptr, nullptr, - nullptr, obj_coeffs.get(), lb.get(), ub.get(), - ctype.get(), - const_cast(colname.get()))); + if (var_index >= last_variable_index_) { + grb_cons_ind.push_back(grb_ct_idx); + grb_var_ind.push_back(mp_var_to_gurobi_var_.at(var_index)); + coef.push_back(entry.second); + } + } + } + if (!grb_cons_ind.empty()) { + CheckedGurobiCall(GRBchgcoeffs(model_, grb_cons_ind.size(), + grb_cons_ind.data(), grb_var_ind.data(), + coef.data())); + } } CheckedGurobiCall(GRBupdatemodel(model_)); + DCHECK_EQ(GetIntAttr(GRB_INT_ATTR_NUMVARS), num_gurobi_vars_); } void GurobiInterface::ExtractNewConstraints() { - CHECK(last_variable_index_ == 0 || - last_variable_index_ == solver_->variables_.size()); - CHECK(last_constraint_index_ == 0 || - last_constraint_index_ == solver_->constraints_.size()); int total_num_rows = solver_->constraints_.size(); if (last_constraint_index_ < total_num_rows) { - // Find the length of the longest row. - int max_row_length = 0; - for (int row = last_constraint_index_; row < total_num_rows; ++row) { - MPConstraint* const ct = solver_->constraints_[row]; - CHECK(!constraint_is_extracted(row)); - set_constraint_as_extracted(row, true); - if (ct->coefficients_.size() > max_row_length) { - max_row_length = ct->coefficients_.size(); - } - } - - max_row_length = std::max(1, max_row_length); - std::unique_ptr col_indices(new int[max_row_length]); - std::unique_ptr coeffs(new double[max_row_length]); - // Add each new constraint. for (int row = last_constraint_index_; row < total_num_rows; ++row) { MPConstraint* const ct = solver_->constraints_[row]; - CHECK(constraint_is_extracted(row)); + set_constraint_as_extracted(row, true); const int size = ct->coefficients_.size(); - int col = 0; + std::vector grb_vars; + std::vector coefs; + grb_vars.reserve(size); + coefs.reserve(size); for (const auto& entry : ct->coefficients_) { const int var_index = entry.first->index(); CHECK(variable_is_extracted(var_index)); - col_indices[col] = var_index; - coeffs[col] = entry.second; - col++; + grb_vars.push_back(mp_var_to_gurobi_var_.at(var_index)); + coefs.push_back(entry.second); } char* const name = ct->name().empty() ? nullptr : const_cast(ct->name().c_str()); if (ct->indicator_variable() != nullptr) { + const int grb_ind_var = + mp_var_to_gurobi_var_.at(ct->indicator_variable()->index()); if (ct->lb() > -std::numeric_limits::infinity()) { CheckedGurobiCall(GRBaddgenconstrIndicator( - model_, name, ct->indicator_variable()->index(), - ct->indicator_value(), size, col_indices.get(), coeffs.get(), + model_, name, grb_ind_var, ct->indicator_value(), size, + grb_vars.data(), coefs.data(), ct->ub() == ct->lb() ? GRB_EQUAL : GRB_GREATER_EQUAL, ct->lb())); } if (ct->ub() < std::numeric_limits::infinity() && ct->lb() != ct->ub()) { CheckedGurobiCall(GRBaddgenconstrIndicator( - model_, name, ct->indicator_variable()->index(), - ct->indicator_value(), size, col_indices.get(), coeffs.get(), - GRB_LESS_EQUAL, ct->ub())); + model_, name, grb_ind_var, ct->indicator_value(), size, + grb_vars.data(), coefs.data(), GRB_LESS_EQUAL, ct->ub())); } + mp_cons_to_gurobi_linear_cons_.push_back(-1); } else { // Using GRBaddrangeconstr for constraints that don't require it adds // a slack which is not always removed by presolve. if (ct->lb() == ct->ub()) { - CheckedGurobiCall(GRBaddconstr(model_, size, col_indices.get(), - coeffs.get(), GRB_EQUAL, ct->lb(), + CheckedGurobiCall(GRBaddconstr(model_, size, grb_vars.data(), + coefs.data(), GRB_EQUAL, ct->lb(), name)); } else if (ct->lb() == -std::numeric_limits::infinity()) { - CheckedGurobiCall(GRBaddconstr(model_, size, col_indices.get(), - coeffs.get(), GRB_LESS_EQUAL, ct->ub(), + CheckedGurobiCall(GRBaddconstr(model_, size, grb_vars.data(), + coefs.data(), GRB_LESS_EQUAL, ct->ub(), name)); } else if (ct->ub() == std::numeric_limits::infinity()) { - CheckedGurobiCall(GRBaddconstr(model_, size, col_indices.get(), - coeffs.get(), GRB_GREATER_EQUAL, + CheckedGurobiCall(GRBaddconstr(model_, size, grb_vars.data(), + coefs.data(), GRB_GREATER_EQUAL, ct->lb(), name)); } else { - CheckedGurobiCall(GRBaddrangeconstr(model_, size, col_indices.get(), - coeffs.get(), ct->lb(), ct->ub(), + CheckedGurobiCall(GRBaddrangeconstr(model_, size, grb_vars.data(), + coefs.data(), ct->lb(), ct->ub(), name)); + // NOTE(user): range constraints implicitly add an extra variable + // to the model. + num_gurobi_vars_++; } + mp_cons_to_gurobi_linear_cons_.push_back(num_gurobi_linear_cons_++); } } } CheckedGurobiCall(GRBupdatemodel(model_)); + DCHECK_EQ(GetIntAttr(GRB_INT_ATTR_NUMCONSTRS), num_gurobi_linear_cons_); } void GurobiInterface::ExtractObjective() { - CheckedGurobiCall( - GRBsetintattr(model_, GRB_INT_ATTR_MODELSENSE, maximize_ ? -1 : 1)); - CheckedGurobiCall(GRBsetdblattr(model_, GRB_DBL_ATTR_OBJCON, - solver_->Objective().offset())); + SetIntAttr(GRB_INT_ATTR_MODELSENSE, maximize_ ? GRB_MAXIMIZE : GRB_MINIMIZE); + SetDoubleAttr(GRB_DBL_ATTR_OBJCON, solver_->Objective().offset()); } // ------ Parameters ----- @@ -864,6 +1046,11 @@ void GurobiInterface::SetParameters(const MPSolverParameters& param) { } } +bool GurobiInterface::SetSolverSpecificParametersAsString( + const std::string& parameters) { + return SetSolverSpecificParameters(parameters, GRBgetenv(model_)).ok(); +} + void GurobiInterface::SetRelativeMipGap(double value) { if (mip_) { CheckedGurobiCall( @@ -956,10 +1143,16 @@ void GurobiInterface::SetLpAlgorithm(int value) { } int GurobiInterface::SolutionCount() const { - int solution_count = 0; - CheckedGurobiCall( - GRBgetintattr(model_, GRB_INT_ATTR_SOLCOUNT, &solution_count)); - return solution_count; + return GetIntAttr(GRB_INT_ATTR_SOLCOUNT); +} + +bool GurobiInterface::ModelIsNonincremental() const { + for (const MPConstraint* c : solver_->constraints()) { + if (c->indicator_variable() != nullptr) { + return true; + } + } + return false; } MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { @@ -967,12 +1160,8 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { timer.Start(); if (param.GetIntegerParam(MPSolverParameters::INCREMENTALITY) == - MPSolverParameters::INCREMENTALITY_OFF) { - Reset(); - } - - // TODO(user,user): Support incrementality. - if (sync_status_ == MUST_RELOAD) { + MPSolverParameters::INCREMENTALITY_OFF || + ModelIsNonincremental() || had_nonincremental_change_) { Reset(); } @@ -989,16 +1178,16 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { // Set solution hints if any. for (const std::pair& p : solver_->solution_hint_) { - CheckedGurobiCall( - GRBsetdblattrelement(model_, "Start", p.first->index(), p.second)); + SetDoubleAttrElement(GRB_DBL_ATTR_START, + mp_var_to_gurobi_var_.at(p.first->index()), p.second); } // Pass branching priority annotations if at least one has been updated. if (update_branching_priorities_) { for (const MPVariable* var : solver_->variables_) { - CheckedGurobiCall( - GRBsetintattrelement(model_, GRB_INT_ATTR_BRANCHPRIORITY, - var->index(), var->branching_priority())); + SetIntAttrElement(GRB_INT_ATTR_BRANCHPRIORITY, + mp_var_to_gurobi_var_.at(var->index()), + var->branching_priority()); } update_branching_priorities_ = false; } @@ -1027,8 +1216,8 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { CheckedGurobiCall(GRBsetcallbackfunc(model_, nullptr, nullptr)); } else { gurobi_context = absl::make_unique( - env_, callback_->might_add_cuts(), - callback_->might_add_lazy_constraints()); + env_, &mp_var_to_gurobi_var_, num_gurobi_vars_, + callback_->might_add_cuts(), callback_->might_add_lazy_constraints()); mp_callback_with_context.context = gurobi_context.get(); mp_callback_with_context.callback = callback_; CheckedGurobiCall(GRBsetcallbackfunc( @@ -1053,9 +1242,7 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { } // Get the status. - int optimization_status = 0; - CheckedGurobiCall( - GRBgetintattr(model_, GRB_INT_ATTR_STATUS, &optimization_status)); + const int optimization_status = GetIntAttr(GRB_INT_ATTR_STATUS); VLOG(1) << absl::StrFormat("Solution status %d.\n", optimization_status); const int solution_count = SolutionCount(); @@ -1088,44 +1275,40 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { result_status_ == MPSolver::OPTIMAL)) { current_solution_index_ = 0; // Get the results. - const int total_num_rows = solver_->constraints_.size(); - const int total_num_cols = solver_->variables_.size(); + objective_value_ = GetDoubleAttr(GRB_DBL_ATTR_OBJVAL); + VLOG(1) << "objective = " << objective_value_; { - std::vector variable_values(total_num_cols); - CheckedGurobiCall( - GRBgetdblattr(model_, GRB_DBL_ATTR_OBJVAL, &objective_value_)); - CheckedGurobiCall(GRBgetdblattrarray( - model_, GRB_DBL_ATTR_X, 0, total_num_cols, variable_values.data())); - - VLOG(1) << "objective = " << objective_value_; + const std::vector grb_variable_values = + GetDoubleAttrArray(GRB_DBL_ATTR_X, num_gurobi_vars_); for (int i = 0; i < solver_->variables_.size(); ++i) { MPVariable* const var = solver_->variables_[i]; - var->set_solution_value(variable_values[i]); - VLOG(3) << var->name() << ", value = " << variable_values[i]; + const double val = grb_variable_values.at(mp_var_to_gurobi_var_.at(i)); + var->set_solution_value(val); + VLOG(3) << var->name() << ", value = " << val; } } if (!mip_) { { - std::vector reduced_costs(total_num_cols); - CheckedGurobiCall(GRBgetdblattrarray( - model_, GRB_DBL_ATTR_RC, 0, total_num_cols, reduced_costs.data())); + const std::vector grb_reduced_costs = + GetDoubleAttrArray(GRB_DBL_ATTR_RC, num_gurobi_vars_); for (int i = 0; i < solver_->variables_.size(); ++i) { MPVariable* const var = solver_->variables_[i]; - var->set_reduced_cost(reduced_costs[i]); - VLOG(4) << var->name() << ", reduced cost = " << reduced_costs[i]; + const double rc = grb_reduced_costs.at(mp_var_to_gurobi_var_.at(i)); + var->set_reduced_cost(rc); + VLOG(4) << var->name() << ", reduced cost = " << rc; } } { - std::vector dual_values(total_num_rows); - CheckedGurobiCall(GRBgetdblattrarray( - model_, GRB_DBL_ATTR_PI, 0, total_num_rows, dual_values.data())); + std::vector grb_dual_values = + GetDoubleAttrArray(GRB_DBL_ATTR_PI, num_gurobi_linear_cons_); for (int i = 0; i < solver_->constraints_.size(); ++i) { MPConstraint* const ct = solver_->constraints_[i]; - ct->set_dual_value(dual_values[i]); - VLOG(4) << "row " << ct->index() - << ", dual value = " << dual_values[i]; + const double dual_value = + grb_dual_values.at(mp_cons_to_gurobi_linear_cons_.at(i)); + ct->set_dual_value(dual_value); + VLOG(4) << "row " << ct->index() << ", dual value = " << dual_value; } } } @@ -1138,7 +1321,9 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { absl::optional GurobiInterface::DirectlySolveProto( const MPModelRequest& request) { - const auto status_or = GurobiSolveProto(request); + // Here we reuse the Gurobi environment to support single-use license that + // forbids creating a second environment if one already exists. + const auto status_or = GurobiSolveProto(request, env_); if (status_or.ok()) return status_or.value(); // Special case: if something is not implemented yet, fall back to solving // through MPSolver. @@ -1150,7 +1335,7 @@ absl::optional GurobiInterface::DirectlySolveProto( MPSolutionResponse response; response.set_status(MPSOLVER_NOT_SOLVED); response.set_status_str(status_or.status().ToString()); - return std::move(response); + return response; } bool GurobiInterface::NextSolution() { @@ -1167,19 +1352,17 @@ bool GurobiInterface::NextSolution() { } current_solution_index_++; - const int total_num_cols = solver_->variables_.size(); - std::vector variable_values(total_num_cols); - CheckedGurobiCall(GRBsetintparam( GRBgetenv(model_), GRB_INT_PAR_SOLUTIONNUMBER, current_solution_index_)); - CheckedGurobiCall( - GRBgetdblattr(model_, GRB_DBL_ATTR_POOLOBJVAL, &objective_value_)); - CheckedGurobiCall(GRBgetdblattrarray(model_, GRB_DBL_ATTR_XN, 0, - total_num_cols, variable_values.data())); + objective_value_ = GetDoubleAttr(GRB_DBL_ATTR_POOLOBJVAL); + const std::vector grb_variable_values = + GetDoubleAttrArray(GRB_DBL_ATTR_XN, num_gurobi_vars_); + for (int i = 0; i < solver_->variables_.size(); ++i) { MPVariable* const var = solver_->variables_[i]; - var->set_solution_value(variable_values[i]); + var->set_solution_value( + grb_variable_values.at(mp_var_to_gurobi_var_.at(i))); } // TODO(user,user): This reset may not be necessary, investigate. GRBresetparams(GRBgetenv(model_)); @@ -1210,7 +1393,6 @@ std::string GurobiInterface::ValidFileExtensionForParameterFile() const { } MPSolverInterface* BuildGurobiInterface(bool mip, MPSolver* const solver) { - MPSolver::LoadGurobiSharedLibrary(); return new GurobiInterface(solver, mip); } diff --git a/ortools/linear_solver/gurobi_proto_solver.cc b/ortools/linear_solver/gurobi_proto_solver.cc index 2dde7394c2..73c5e72094 100644 --- a/ortools/linear_solver/gurobi_proto_solver.cc +++ b/ortools/linear_solver/gurobi_proto_solver.cc @@ -20,6 +20,7 @@ #include #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_split.h" @@ -47,34 +48,6 @@ inline absl::Status GurobiCodeToUtilStatus(int error_code, source_file, source_line, statement, GRBgeterrormsg(env))); } -absl::Status SetSolverSpecificParameters(const std::string& parameters, - GRBenv* gurobi) { - for (const auto parameter : - absl::StrSplit(parameters, '\n', absl::SkipWhitespace())) { - if (parameter.empty()) return absl::OkStatus(); - if (*parameter.begin() == '#') return absl::OkStatus(); - std::vector key_value = - absl::StrSplit(parameter, ' ', absl::SkipWhitespace()); - if (key_value.size() != 2) { - return absl::InvalidArgumentError( - absl::StrFormat("Cannot parse parameter '%s'. Expected format is " - "'ParameterName value'", - parameter)); - } - - std::string name = key_value[0]; - std::string value = key_value[1]; - - if (GRBsetparam(gurobi, name.c_str(), value.c_str()) != GRB_OK) { - return absl::InvalidArgumentError( - absl::StrFormat("Error setting parameter '%s' to value '%s': %s", - name, value, GRBgeterrormsg(gurobi))); - } - } - - return absl::OkStatus(); -} - int AddIndicatorConstraint(const MPGeneralConstraintProto& gen_cst, GRBmodel* gurobi_model, std::vector* tmp_variables, @@ -245,39 +218,95 @@ int AddMaxConstraint(const MPGeneralConstraintProto& gen_cst, } } // namespace +absl::Status SetSolverSpecificParameters(const std::string& parameters, + GRBenv* gurobi) { + std::string error_message(""); + for (const auto parameter : absl::StrSplit(parameters, absl::ByAnyChar("\n,"), + absl::SkipWhitespace())) { + // If the line is a comment, we skip it. + if (parameter.empty() || parameter[0] == '#') { + continue; + } + // This double creation of sub-strings is wasteful, but probably does not + // matter much in this context. Still better than going through a file. + std::vector key_value = absl::StrSplit( + parameter, absl::ByAnyChar("= "), absl::SkipWhitespace()); + // If one parameter fails, we keep processing the list of parameters. + if (key_value.size() != 2) { + const std::string current_message = + absl::StrCat("Cannot parse parameter '", parameter, + "'. Expected format is 'ParameterName value'"); + LOG(WARNING) << current_message; + if (error_message.empty()) { + error_message = current_message; + } else { + absl::StrAppend(&error_message, "\n", current_message); + } + continue; + } + // Again, if setting one parameter fails, we notify and keep moving down + // the list. + const int gurobi_code = + GRBsetparam(gurobi, key_value[0].c_str(), key_value[1].c_str()); + if (gurobi_code != 0) { + const std::string current_message = absl::StrCat( + "Error setting parameter '", key_value[0], "' to value '", + key_value[1], "': ", GRBgeterrormsg(gurobi)); + LOG(WARNING) << current_message; + if (error_message.empty()) { + error_message = current_message; + } else { + absl::StrAppend(&error_message, "\n", current_message); + } + continue; + } + VLOG(2) << absl::StrCat("Set parameter '", key_value[0], "' to value '", + key_value[1]); + } + + if (error_message.empty()) return absl::OkStatus(); + return absl::InvalidArgumentError(error_message); +} + absl::StatusOr GurobiSolveProto( - const MPModelRequest& request) { + const MPModelRequest& request, GRBenv* gurobi_env) { MPSolutionResponse response; const absl::optional> optional_model = ExtractValidMPModelOrPopulateResponseStatus(request, &response); if (!optional_model) return response; const MPModelProto& model = optional_model->get(); - if (model.has_solution_hint()) { - // TODO(user): Support solution hints. - return absl::UnimplementedError("Solution hint not supported."); + // We set `gurobi_env` to point to a new environment if no existing one is + // provided. We must make sure that we free this environment when we exit this + // function. + bool gurobi_env_was_created = false; + auto gurobi_env_deleter = absl::MakeCleanup([&]() { + if (gurobi_env_was_created && gurobi_env != nullptr) { + GRBfreeenv(gurobi_env); + } + }); + if (gurobi_env == nullptr) { + // We activate the deletion of `gurobi_env` before making the call to + // `LoadGurobiEnvironment()` since this function still returns a non null + // value even when it fails. + gurobi_env_was_created = true; + RETURN_IF_ERROR(LoadGurobiEnvironment(&gurobi_env)); } - GRBenv* gurobi = nullptr; GRBmodel* gurobi_model = nullptr; - -// `gurobi` references ther GRBenv variable defined above. -#define RETURN_IF_GUROBI_ERROR(x) \ - RETURN_IF_ERROR(GurobiCodeToUtilStatus(x, __FILE__, __LINE__, #x, gurobi)); - - auto delete_gurobi_objects = [&]() -> absl::Status { - // Release all created pointers. - if (gurobi_model) RETURN_IF_GUROBI_ERROR(GRBfreemodel(gurobi_model)); - if (gurobi) GRBfreeenv(gurobi); - return absl::OkStatus(); - }; - auto gurobi_deleter = absl::MakeCleanup([delete_gurobi_objects]() { - const absl::Status deleter_status = delete_gurobi_objects(); - LOG_IF(DFATAL, !deleter_status.ok()) << deleter_status; + auto gurobi_model_deleter = absl::MakeCleanup([&]() { + const int error_code = GRBfreemodel(gurobi_model); + LOG_IF(DFATAL, error_code != GRB_OK) + << "GRBfreemodel failed with error " << error_code << ": " + << GRBgeterrormsg(gurobi_env); }); - RETURN_IF_ERROR(LoadGurobiEnvironment(&gurobi)); - RETURN_IF_GUROBI_ERROR(GRBnewmodel(gurobi, &gurobi_model, +// `gurobi_env` references ther GRBenv argument. +#define RETURN_IF_GUROBI_ERROR(x) \ + RETURN_IF_ERROR( \ + GurobiCodeToUtilStatus(x, __FILE__, __LINE__, #x, gurobi_env)); + + RETURN_IF_GUROBI_ERROR(GRBnewmodel(gurobi_env, &gurobi_model, model.name().c_str(), /*numvars=*/0, /*obj=*/nullptr, @@ -291,7 +320,7 @@ absl::StatusOr GurobiSolveProto( request.solver_specific_parameters(), GRBgetenv(gurobi_model)); if (!parameters_status.ok()) { response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); - response.set_status_str(parameters_status.message().data()); + response.set_status_str(std::string(parameters_status.message())); return response; } } @@ -327,6 +356,13 @@ absl::StatusOr GurobiSolveProto( /*obj=*/obj_coeffs.data(), /*lb=*/lb.data(), /*ub=*/ub.data(), /*vtype=*/ctype.data(), /*varnames=*/const_cast(varnames.data()))); + + // Set solution hints if any. + for (int i = 0; i < model.solution_hint().var_index_size(); ++i) { + RETURN_IF_GUROBI_ERROR(GRBsetdblattrelement( + gurobi_model, GRB_DBL_ATTR_START, model.solution_hint().var_index(i), + model.solution_hint().var_value(i))); + } } { diff --git a/ortools/linear_solver/gurobi_proto_solver.h b/ortools/linear_solver/gurobi_proto_solver.h index bf08994fca..a6801cee21 100644 --- a/ortools/linear_solver/gurobi_proto_solver.h +++ b/ortools/linear_solver/gurobi_proto_solver.h @@ -14,13 +14,35 @@ #ifndef OR_TOOLS_LINEAR_SOLVER_GUROBI_PROTO_SOLVER_H_ #define OR_TOOLS_LINEAR_SOLVER_GUROBI_PROTO_SOLVER_H_ -#include "ortools/base/statusor.h" +#include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/gurobi_environment.h" namespace operations_research { +// Solves the input request. +// +// By default this function creates a new master Gurobi environment, but an +// existing one can be passed as parameter. This can be useful with single-use +// Gurobi licenses since it is not possible to create a second environment if +// one already exists with those licenses. +// +// Please note though that the provided environment should not be actively used +// by another thread at the same time. absl::StatusOr GurobiSolveProto( - const MPModelRequest& request); + const MPModelRequest& request, GRBenv* gurobi_env = nullptr); +// Set parameters specified in the string. The format of the string is a series +// of tokens separated by either '\n' or by ',' characters. +// Any token whose first character is a '#' or has zero length is skiped. +// Any other token has to has the form: +// parameter_name(separator)value +// where (separator) is either '=' or ' '. +// A valid string can look-like: +// "#\n# Gurobi-specific parameters\n\nThreads=1\nPresolve 2,SolutionLimit=100" +// This function will process each and every token, even if an intermediate +// token is unrecognized. +absl::Status SetSolverSpecificParameters(const std::string& parameters, + GRBenv* gurobi); } // namespace operations_research #endif // OR_TOOLS_LINEAR_SOLVER_GUROBI_PROTO_SOLVER_H_ diff --git a/ortools/linear_solver/java/linear_solver.i b/ortools/linear_solver/java/linear_solver.i index 9820ae0642..13f38895b7 100644 --- a/ortools/linear_solver/java/linear_solver.i +++ b/ortools/linear_solver/java/linear_solver.i @@ -342,7 +342,7 @@ PROTO2_RETURN( %rename (reset) operations_research::MPSolver::Reset; // no test %rename (infinity) operations_research::MPSolver::infinity; %rename (setTimeLimit) operations_research::MPSolver::set_time_limit; // no test -%rename (isMip) operations_research::MPSolver::IsMIP; +%rename (isMip) operations_research::MPSolver::IsMIP; // no test // Proto-based API of the MPSolver. Use is encouraged. // Note: the following proto-based methods aren't listed here, but are diff --git a/ortools/linear_solver/linear_solver.cc b/ortools/linear_solver/linear_solver.cc index 51bc7d0d98..f323f4e2aa 100644 --- a/ortools/linear_solver/linear_solver.cc +++ b/ortools/linear_solver/linear_solver.cc @@ -24,7 +24,9 @@ #include #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/ascii.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_replace.h" @@ -35,7 +37,6 @@ #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/status_macros.h" -#include "ortools/base/statusor.h" #include "ortools/base/stl_util.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_exporter.h" @@ -669,9 +670,8 @@ MPSolverResponseStatus MPSolver::LoadModelFromProtoInternal( << "Ignoring the model error(s) because of" << " --mpsolver_bypass_model_validation."; } else { - return error.find("Infeasible") == std::string::npos - ? MPSOLVER_MODEL_INVALID - : MPSOLVER_INFEASIBLE; + return absl::StrContains(error, "Infeasible") ? MPSOLVER_INFEASIBLE + : MPSOLVER_MODEL_INVALID; } } } @@ -875,10 +875,28 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request, solver.SetTimeLimit( absl::Seconds(model_request.solver_time_limit_seconds())); } - solver.SetSolverSpecificParametersAsString( - model_request.solver_specific_parameters()); + std::string warning_message; + if (model_request.has_solver_specific_parameters()) { + if (!solver.SetSolverSpecificParametersAsString( + model_request.solver_specific_parameters())) { + if (model_request.ignore_solver_specific_parameters_failure()) { + // We'll add a warning message in status_str after the solve. + warning_message = + "Warning: the solver specific parameters were not successfully " + "applied"; + } else { + response->set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); + return; + } + } + } solver.Solve(); solver.FillSolutionResponseProto(response); + if (!warning_message.empty()) { + response->set_status_str(absl::StrCat( + response->status_str(), (response->status_str().empty() ? "" : "\n"), + warning_message)); + } } void MPSolver::ExportModelToProto(MPModelProto* output_model) const { diff --git a/ortools/linear_solver/linear_solver.h b/ortools/linear_solver/linear_solver.h index 7f678a84ea..847a518a22 100644 --- a/ortools/linear_solver/linear_solver.h +++ b/ortools/linear_solver/linear_solver.h @@ -266,7 +266,7 @@ class MPSolver { /** * Parses the name of the solver and returns the correct optimization type or - * dies. + * dies. Invariant: ParseSolverTypeOrDie(ToString(type)) = type. */ static OptimizationProblemType ParseSolverTypeOrDie( const std::string& solver_id); diff --git a/ortools/linear_solver/linear_solver.proto b/ortools/linear_solver/linear_solver.proto index 32d04e696f..d78c1d83ab 100644 --- a/ortools/linear_solver/linear_solver.proto +++ b/ortools/linear_solver/linear_solver.proto @@ -388,7 +388,7 @@ message MPModelDeltaProto { // NOTE(user): We may also add more deltas, eg. objective offset. } -// Next id: 9. +// Next id: 10. message MPModelRequest { // The model to be optimized by the server. optional MPModelProto model = 1; @@ -452,8 +452,11 @@ message MPModelRequest { // // If the option format is not understood by the solver, the request will be // rejected and yield an RPC Application error with code - // MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS. + // MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS, unless you have set + // ignore_solver_specific_parameters_failure=true (in which case they are + // simply ignored). optional string solver_specific_parameters = 5; + optional bool ignore_solver_specific_parameters_failure = 9 [default = false]; // Advanced usage: model "delta". If used, "model" must be unset. See the diff --git a/ortools/linear_solver/model_exporter.cc b/ortools/linear_solver/model_exporter.cc index aaefa3c6eb..f63b4bf006 100644 --- a/ortools/linear_solver/model_exporter.cc +++ b/ortools/linear_solver/model_exporter.cc @@ -19,6 +19,7 @@ #include "absl/container/flat_hash_set.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" @@ -36,6 +37,44 @@ namespace { constexpr double kInfinity = std::numeric_limits::infinity(); +class LineBreaker { + public: + explicit LineBreaker(int max_line_size) + : max_line_size_(max_line_size), line_size_(0), output_() {} + // Lines are broken in such a way that: + // - Strings that are given to Append() are never split. + // - Lines are split so that their length doesn't exceed the max length; + // unless a single string given to Append() exceeds that length (in which + // case it will be put alone on a single unsplit line). + void Append(const std::string& s); + + // Returns true if string s will fit on the current line without adding a + // carriage return. + bool WillFit(const std::string& s) { + return line_size_ + s.size() < max_line_size_; + } + + // "Consumes" size characters on the line. Used when starting the constraint + // lines. + void Consume(int size) { line_size_ += size; } + + std::string GetOutput() const { return output_; } + + private: + int max_line_size_; + int line_size_; + std::string output_; +}; + +void LineBreaker::Append(const std::string& s) { + line_size_ += s.size(); + if (line_size_ > max_line_size_) { + line_size_ = s.size(); + absl::StrAppend(&output_, "\n "); + } + absl::StrAppend(&output_, s); +} + class MPModelProtoExporter { public: explicit MPModelProtoExporter(const MPModelProto& model); @@ -84,7 +123,17 @@ class MPModelProtoExporter { // term >= lhs and term <= rhs. void AppendComments(const std::string& separator, std::string* output) const; - // Clears "output" and writes a term to it, in "Lp" format. Returns false on + // Appends an MPConstraintProto to the output text. If the constraint has + // both an upper and lower bound that are not equal, it splits the constraint + // into two constraints, one for the left hand side (_lhs) and one for right + // hand side (_rhs). + bool AppendConstraint(const MPConstraintProto& ct_proto, + const std::string& name, + const MPModelExportOptions& options, + LineBreaker& line_breaker, + std::vector& show_variable, std::string* output); + + // Clears "output" and writes a term to it, in "LP" format. Returns false on // error (for example, var_index is out of range). bool WriteLpTerm(int var_index, double coefficient, std::string* output) const; @@ -138,6 +187,9 @@ class MPModelProtoExporter { // Vector of constraint names as they will be exported. std::vector exported_constraint_names_; + // Vector of general constraint names as they will be exported. + std::vector exported_general_constraint_names_; + // Number of integer variables in proto_. int num_integer_variables_; @@ -156,12 +208,17 @@ class MPModelProtoExporter { DISALLOW_COPY_AND_ASSIGN(MPModelProtoExporter); }; + } // namespace absl::StatusOr ExportModelAsLpFormat( const MPModelProto& model, const MPModelExportOptions& options) { - if (model.general_constraint_size() > 0) { - return absl::InvalidArgumentError("General constraints are not supported."); + for (const MPGeneralConstraintProto& general_constraint : + model.general_constraint()) { + if (!general_constraint.has_indicator_constraint()) { + return absl::InvalidArgumentError( + "Non-indicator general constraints are not supported."); + } } MPModelProtoExporter exporter(model); std::string output; @@ -293,8 +350,9 @@ void MPModelProtoExporter::AppendComments(const std::string& separator, absl::StrAppendFormat(output, "%s %-16s : %s\n", sep, "Name", proto_.has_name() ? proto_.name().c_str() : "NoName"); absl::StrAppendFormat(output, "%s %-16s : %s\n", sep, "Format", "Free"); - absl::StrAppendFormat(output, "%s %-16s : %d\n", sep, "Constraints", - proto_.constraint_size()); + absl::StrAppendFormat( + output, "%s %-16s : %d\n", sep, "Constraints", + proto_.constraint_size() + proto_.general_constraint_size()); absl::StrAppendFormat(output, "%s %-16s : %d\n", sep, "Variables", proto_.variable_size()); absl::StrAppendFormat(output, "%s %-14s : %d\n", sep, "Binary", @@ -306,43 +364,6 @@ void MPModelProtoExporter::AppendComments(const std::string& separator, } namespace { -class LineBreaker { - public: - explicit LineBreaker(int max_line_size) - : max_line_size_(max_line_size), line_size_(0), output_() {} - // Lines are broken in such a way that: - // - Strings that are given to Append() are never split. - // - Lines are split so that their length doesn't exceed the max length; - // unless a single string given to Append() exceeds that length (in which - // case it will be put alone on a single unsplit line). - void Append(const std::string& s); - - // Returns true if string s will fit on the current line without adding - // a carriage return. - bool WillFit(const std::string& s) { - return line_size_ + s.size() < max_line_size_; - } - - // "Consumes" size characters on the line. Used when starting the constraint - // lines. - void Consume(int size) { line_size_ += size; } - - std::string GetOutput() const { return output_; } - - private: - int max_line_size_; - int line_size_; - std::string output_; -}; - -void LineBreaker::Append(const std::string& s) { - line_size_ += s.size(); - if (line_size_ > max_line_size_) { - line_size_ = s.size(); - absl::StrAppend(&output_, "\n "); - } - absl::StrAppend(&output_, s); -} std::string DoubleToStringWithForcedSign(double d) { return absl::StrCat((d < 0 ? "" : "+"), (d)); @@ -352,6 +373,58 @@ std::string DoubleToString(double d) { return absl::StrCat((d)); } } // namespace +bool MPModelProtoExporter::AppendConstraint(const MPConstraintProto& ct_proto, + const std::string& name, + const MPModelExportOptions& options, + LineBreaker& line_breaker, + std::vector& show_variable, + std::string* output) { + for (int i = 0; i < ct_proto.var_index_size(); ++i) { + const int var_index = ct_proto.var_index(i); + const double coeff = ct_proto.coefficient(i); + std::string term; + if (!WriteLpTerm(var_index, coeff, &term)) { + return false; + } + line_breaker.Append(term); + show_variable[var_index] = coeff != 0.0 || options.show_unused_variables; + } + + const double lb = ct_proto.lower_bound(); + const double ub = ct_proto.upper_bound(); + if (lb == ub) { + line_breaker.Append(absl::StrCat(" = ", DoubleToString(ub), "\n")); + absl::StrAppend(output, " ", name, ": ", line_breaker.GetOutput()); + } else { + if (ub != +kInfinity) { + std::string rhs_name = name; + if (lb != -kInfinity) { + absl::StrAppend(&rhs_name, "_rhs"); + } + absl::StrAppend(output, " ", rhs_name, ": ", line_breaker.GetOutput()); + const std::string relation = + absl::StrCat(" <= ", DoubleToString(ub), "\n"); + // Here we have to make sure we do not add the relation to the contents + // of line_breaker, which may be used in the subsequent clause. + if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n "); + absl::StrAppend(output, relation); + } + if (lb != -kInfinity) { + std::string lhs_name = name; + if (ub != +kInfinity) { + absl::StrAppend(&lhs_name, "_lhs"); + } + absl::StrAppend(output, " ", lhs_name, ": ", line_breaker.GetOutput()); + const std::string relation = + absl::StrCat(" >= ", DoubleToString(lb), "\n"); + if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n "); + absl::StrAppend(output, relation); + } + } + + return true; +} + bool MPModelProtoExporter::WriteLpTerm(int var_index, double coefficient, std::string* output) const { output->clear(); @@ -453,6 +526,9 @@ bool MPModelProtoExporter::ExportModelAsLpFormat( exported_constraint_names_ = ExtractAndProcessNames( proto_.constraint(), "C", options.obfuscate, options.log_invalid_names, kForbiddenFirstChars, kForbiddenChars); + exported_general_constraint_names_ = ExtractAndProcessNames( + proto_.general_constraint(), "C", options.obfuscate, + options.log_invalid_names, kForbiddenFirstChars, kForbiddenChars); exported_variable_names_ = ExtractAndProcessNames( proto_.variable(), "V", options.obfuscate, options.log_invalid_names, kForbiddenFirstChars, kForbiddenChars); @@ -482,7 +558,7 @@ bool MPModelProtoExporter::ExportModelAsLpFormat( obj_line_breaker.Append(term); show_variable[var_index] = coeff != 0.0 || options.show_unused_variables; } - // Constraints + // Linear Constraints absl::StrAppend(output, obj_line_breaker.GetOutput(), "\nSubject to\n"); for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) { const MPConstraintProto& ct_proto = proto_.constraint(cst_index); @@ -492,46 +568,37 @@ bool MPModelProtoExporter::ExportModelAsLpFormat( // Account for the size of the constraint name + possibly "_rhs" + // the formatting characters here. line_breaker.Consume(kNumFormattingChars + name.size()); - for (int i = 0; i < ct_proto.var_index_size(); ++i) { - const int var_index = ct_proto.var_index(i); - const double coeff = ct_proto.coefficient(i); - std::string term; - if (!WriteLpTerm(var_index, coeff, &term)) { - return false; - } - line_breaker.Append(term); - show_variable[var_index] = coeff != 0.0 || options.show_unused_variables; + if (!AppendConstraint(ct_proto, name, options, line_breaker, show_variable, + output)) { + return false; } - const double lb = ct_proto.lower_bound(); - const double ub = ct_proto.upper_bound(); - if (lb == ub) { - line_breaker.Append(absl::StrCat(" = ", DoubleToString(ub), "\n")); - absl::StrAppend(output, " ", name, ": ", line_breaker.GetOutput()); - } else { - if (ub != +kInfinity) { - std::string rhs_name = name; - if (lb != -kInfinity) { - absl::StrAppend(&rhs_name, "_rhs"); - } - absl::StrAppend(output, " ", rhs_name, ": ", line_breaker.GetOutput()); - const std::string relation = - absl::StrCat(" <= ", DoubleToString(ub), "\n"); - // Here we have to make sure we do not add the relation to the contents - // of line_breaker, which may be used in the subsequent clause. - if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n "); - absl::StrAppend(output, relation); - } - if (lb != -kInfinity) { - std::string lhs_name = name; - if (ub != +kInfinity) { - absl::StrAppend(&lhs_name, "_lhs"); - } - absl::StrAppend(output, " ", lhs_name, ": ", line_breaker.GetOutput()); - const std::string relation = - absl::StrCat(" >= ", DoubleToString(lb), "\n"); - if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n "); - absl::StrAppend(output, relation); - } + } + + // General Constraints + for (int cst_index = 0; cst_index < proto_.general_constraint_size(); + ++cst_index) { + const MPGeneralConstraintProto& ct_proto = + proto_.general_constraint(cst_index); + const std::string& name = exported_general_constraint_names_[cst_index]; + LineBreaker line_breaker(options.max_line_length); + const int kNumFormattingChars = 10; // Overevaluated. + // Account for the size of the constraint name + possibly "_rhs" + + // the formatting characters here. + line_breaker.Consume(kNumFormattingChars + name.size()); + + if (!ct_proto.has_indicator_constraint()) return false; + const MPIndicatorConstraint& indicator_ct = ct_proto.indicator_constraint(); + const int binary_var_index = indicator_ct.var_index(); + const int binary_var_value = indicator_ct.var_value(); + if (binary_var_index < 0 || binary_var_index >= proto_.variable_size()) { + return false; + } + line_breaker.Append(absl::StrFormat( + "%s = %d -> ", exported_variable_names_[binary_var_index], + binary_var_value)); + if (!AppendConstraint(indicator_ct.constraint(), name, options, + line_breaker, show_variable, output)) { + return false; } } @@ -653,7 +720,7 @@ void MPModelProtoExporter::AppendMpsColumns( AppendMpsTermWithContext(var_name, "COST", var_proto.objective_coefficient(), output); } - for (const std::pair cst_index_and_coeff : + for (const std::pair& cst_index_and_coeff : transpose[var_index]) { const std::string& cst_name = exported_constraint_names_[cst_index_and_coeff.first]; @@ -678,13 +745,6 @@ bool MPModelProtoExporter::ExportModelAsMpsFormat( proto_.variable(), "V", options.obfuscate, options.log_invalid_names, kForbiddenFirstChars, kForbiddenChars); - // TODO(user): Support maximization via OBJSENSE section. See: - // http://www-eio.upc.es/lceio/manuals/cplex-11/html/reffileformatscplex/ - // reffileformatscplex10.html - if (proto_.maximize()) { - LOG(DFATAL) << "MPS cannot represent maximization objectives."; - return false; - } // Comments. AppendComments("*", output); @@ -692,6 +752,10 @@ bool MPModelProtoExporter::ExportModelAsMpsFormat( // TODO(user): Obfuscate the model name too if `obfuscate` is true. absl::StrAppendFormat(output, "%-14s%s\n", "NAME", proto_.name()); + if (proto_.maximize()) { + absl::StrAppendFormat(output, "OBJSENSE\n MAX\n"); + } + // ROWS section. current_mps_column_ = 0; std::string rows_section; diff --git a/ortools/linear_solver/model_exporter.h b/ortools/linear_solver/model_exporter.h index 09e0ecc82c..ed3c8e3af1 100644 --- a/ortools/linear_solver/model_exporter.h +++ b/ortools/linear_solver/model_exporter.h @@ -17,10 +17,10 @@ #include #include +#include "absl/status/statusor.h" #include "absl/strings/str_format.h" #include "ortools/base/hash.h" #include "ortools/base/macros.h" -#include "ortools/base/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" namespace operations_research { diff --git a/ortools/linear_solver/model_exporter_swig_helper.h b/ortools/linear_solver/model_exporter_swig_helper.h index 36353ce0e2..38ea20ce25 100644 --- a/ortools/linear_solver/model_exporter_swig_helper.h +++ b/ortools/linear_solver/model_exporter_swig_helper.h @@ -21,14 +21,14 @@ namespace operations_research { -std::string ExportModelAsLpFormatReturnString( +inline std::string ExportModelAsLpFormatReturnString( const MPModelProto& input_model, const MPModelExportOptions& options = MPModelExportOptions()) { return operations_research::ExportModelAsLpFormat(input_model, options) .value_or(""); } -std::string ExportModelAsMpsFormatReturnString( +inline std::string ExportModelAsMpsFormatReturnString( const MPModelProto& input_model, const MPModelExportOptions& options = MPModelExportOptions()) { return operations_research::ExportModelAsMpsFormat(input_model, options) diff --git a/ortools/linear_solver/model_validator.cc b/ortools/linear_solver/model_validator.cc index 11849d5eb6..5581ffde0b 100644 --- a/ortools/linear_solver/model_validator.cc +++ b/ortools/linear_solver/model_validator.cc @@ -20,6 +20,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/status/status.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/types/optional.h" @@ -559,9 +560,9 @@ ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request, if (request.enable_internal_solver_output()) { LOG(ERROR) << absl::StrCat("Invalid model: ", error); } - response->set_status(error.find("Infeasible") == std::string::npos - ? MPSOLVER_MODEL_INVALID - : MPSOLVER_INFEASIBLE); + response->set_status(absl::StrContains(error, "Infeasible") + ? MPSOLVER_INFEASIBLE + : MPSOLVER_MODEL_INVALID); response->set_status_str(error); return absl::nullopt; } diff --git a/ortools/linear_solver/python/linear_solver.i b/ortools/linear_solver/python/linear_solver.i index 79e14799db..835e1a38eb 100644 --- a/ortools/linear_solver/python/linear_solver.i +++ b/ortools/linear_solver/python/linear_solver.i @@ -112,7 +112,6 @@ __pdoc__['Variable.thisown'] = False return $self->LoadSolutionFromProto(response, tolerance).ok(); } - // Change the API of ExportModelAsLpFormat() to simply return the model. std::string ExportModelAsLpFormat(bool obfuscated) { operations_research::MPModelExportOptions options; options.obfuscate = obfuscated; @@ -121,7 +120,6 @@ __pdoc__['Variable.thisown'] = False return ExportModelAsLpFormat(model, options).value_or(""); } - // Change the API of ExportModelAsMpsFormat() to simply return the model. std::string ExportModelAsMpsFormat(bool fixed_format, bool obfuscated) { operations_research::MPModelExportOptions options; options.obfuscate = obfuscated; diff --git a/ortools/linear_solver/sat_interface.cc b/ortools/linear_solver/sat_interface.cc index 57d7a40774..7c60034fb8 100644 --- a/ortools/linear_solver/sat_interface.cc +++ b/ortools/linear_solver/sat_interface.cc @@ -16,10 +16,10 @@ #include #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "ortools/base/hash.h" #include "ortools/base/integral_types.h" #include "ortools/base/logging.h" -#include "ortools/base/statusor.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/sat_proto_solver.h" diff --git a/ortools/linear_solver/sat_proto_solver.cc b/ortools/linear_solver/sat_proto_solver.cc index d4eaf4f9e6..ff7485f649 100644 --- a/ortools/linear_solver/sat_proto_solver.cc +++ b/ortools/linear_solver/sat_proto_solver.cc @@ -15,7 +15,7 @@ #include -#include "ortools/base/statusor.h" +#include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_validator.h" #include "ortools/linear_solver/sat_solver_utils.h" diff --git a/ortools/linear_solver/sat_proto_solver.h b/ortools/linear_solver/sat_proto_solver.h index 7680723b6a..e19b456d4a 100644 --- a/ortools/linear_solver/sat_proto_solver.h +++ b/ortools/linear_solver/sat_proto_solver.h @@ -14,7 +14,7 @@ #ifndef OR_TOOLS_LINEAR_SOLVER_SAT_PROTO_SOLVER_H_ #define OR_TOOLS_LINEAR_SOLVER_SAT_PROTO_SOLVER_H_ -#include "ortools/base/statusor.h" +#include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" namespace operations_research { diff --git a/ortools/linear_solver/scip_callback.cc b/ortools/linear_solver/scip_callback.cc new file mode 100644 index 0000000000..666fdf478c --- /dev/null +++ b/ortools/linear_solver/scip_callback.cc @@ -0,0 +1,452 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/linear_solver/scip_callback.h" + +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "ortools/base/logging.h" +#include "ortools/linear_solver/scip_helper_macros.h" +#include "scip/cons_linear.h" +#include "scip/def.h" +#include "scip/pub_cons.h" +#include "scip/scip.h" +#include "scip/scip_cons.h" +#include "scip/scip_cut.h" +#include "scip/scip_general.h" +#include "scip/scip_lp.h" +#include "scip/scip_param.h" +#include "scip/scip_prob.h" +#include "scip/scip_sol.h" +#include "scip/scip_solvingstats.h" +#include "scip/scip_tree.h" +#include "scip/scipdefplugins.h" +#include "scip/struct_cons.h" +#include "scip/struct_tree.h" +#include "scip/struct_var.h" +#include "scip/type_cons.h" +#include "scip/type_lp.h" +#include "scip/type_result.h" +#include "scip/type_retcode.h" +#include "scip/type_scip.h" +#include "scip/type_sol.h" +#include "scip/type_tree.h" +#include "scip/type_var.h" + +struct SCIP_ConshdlrData { + std::unique_ptr runner; +}; + +struct SCIP_ConsData { + void* data; +}; + +namespace operations_research { + +namespace { +int ScipNumVars(SCIP* scip) { return SCIPgetNOrigVars(scip); } + +SCIP_VAR* ScipGetVar(SCIP* scip, int var_index) { + DCHECK_GE(var_index, 0); + DCHECK_LT(var_index, ScipNumVars(scip)); + return SCIPgetOrigVars(scip)[var_index]; +} + +} // namespace + +ScipConstraintHandlerContext::ScipConstraintHandlerContext( + SCIP* scip, SCIP_SOL* solution, bool is_pseudo_solution) + : scip_(scip), + solution_(solution), + is_pseudo_solution_(is_pseudo_solution) {} + +double ScipConstraintHandlerContext::VariableValue( + const MPVariable* variable) const { + return SCIPgetSolVal(scip_, solution_, ScipGetVar(scip_, variable->index())); +} + +int64 ScipConstraintHandlerContext::NumNodesProcessed() const { + return SCIPgetNNodes(scip_); +} + +int64 ScipConstraintHandlerContext::CurrentNodeId() const { + return SCIPgetCurrentNode(scip_)->number; +} + +enum class ScipSeparationResult { + kLazyConstraintAdded, + kCuttingPlaneAdded, + kDidNotFind +}; + +bool LinearConstraintIsViolated(const ScipConstraintHandlerContext& context, + const LinearRange& constraint) { + double a_times_x = 0; + for (const auto& coef_pair : constraint.linear_expr().terms()) { + a_times_x += coef_pair.second * context.VariableValue(coef_pair.first); + } + double violation = std::max(a_times_x - constraint.upper_bound(), + constraint.lower_bound() - a_times_x); + return violation > 0; +} + +// If any violated lazy constraint is found: +// returns kLazyConstraintAdded, +// else if any violated cutting plane is found: +// returns kCuttingPlaneAdded, +// else: +// returns kDidNotFind +ScipSeparationResult RunSeparation(internal::ScipCallbackRunner* runner, + const ScipConstraintHandlerContext& context, + absl::Span constraints, + bool is_integral) { + ScipSeparationResult result = ScipSeparationResult::kDidNotFind; + SCIP* scip = context.scip(); + for (SCIP_CONS* constraint : constraints) { + SCIP_CONSDATA* consdata = SCIPconsGetData(constraint); + CHECK(consdata != nullptr); + std::vector user_suggested_constraints; + if (is_integral) { + user_suggested_constraints = + runner->SeparateIntegerSolution(context, consdata->data); + } else { + user_suggested_constraints = + runner->SeparateFractionalSolution(context, consdata->data); + } + int num_constraints_added = 0; + for (const CallbackRangeConstraint& user_suggested_constraint : + user_suggested_constraints) { + if (!LinearConstraintIsViolated(context, + user_suggested_constraint.range)) { + continue; + } + num_constraints_added++; + // Two code paths, one for cuts, one for lazy constraints. Cuts first: + if (user_suggested_constraint.is_cut) { + SCIP_ROW* row = nullptr; + constexpr bool kModifiable = false; + constexpr bool kRemovable = true; + CHECK_OK(SCIP_TO_STATUS(SCIPcreateEmptyRowCons( + scip, &row, constraint, user_suggested_constraint.name.c_str(), + user_suggested_constraint.range.lower_bound(), + user_suggested_constraint.range.upper_bound(), + user_suggested_constraint.local, kModifiable, kRemovable))); + CHECK_OK(SCIP_TO_STATUS(SCIPcacheRowExtensions(scip, row))); + for (const auto& coef_pair : + user_suggested_constraint.range.linear_expr().terms()) { + // NOTE(user): the coefficients don't come out sorted. I don't + // think this matters. + SCIP_VAR* var = ScipGetVar(scip, coef_pair.first->index()); + const double coef = coef_pair.second; + CHECK_OK(SCIP_TO_STATUS(SCIPaddVarToRow(scip, row, var, coef))); + } + CHECK_OK(SCIP_TO_STATUS(SCIPflushRowExtensions(scip, row))); + SCIP_Bool infeasible; + constexpr bool kForceCut = false; + CHECK_OK(SCIP_TO_STATUS(SCIPaddRow(scip, row, kForceCut, &infeasible))); + CHECK_OK(SCIP_TO_STATUS(SCIPreleaseRow(scip, &row))); + // TODO(user): when infeasible is true, it better to have the scip + // return status be cutoff instead of cutting plane added (e.g. see + // cs/scip/cons_knapsack.c). However, as we use + // SCIPaddRow(), it isn't clear this will even happen. + if (result != ScipSeparationResult::kLazyConstraintAdded) { + // NOTE(user): if we have already found a violated lazy constraint, + // we want to return kLazyConstraintAdded, not kCuttingPlaneAdded, + // see function contract. + result = ScipSeparationResult::kCuttingPlaneAdded; + } + } else { + // Lazy constraint path: + std::vector vars; + std::vector coefs; + for (const auto& coef_pair : + user_suggested_constraint.range.linear_expr().terms()) { + // NOTE(user): the coefficients don't come out sorted. I don't + // think this matters. + vars.push_back(ScipGetVar(scip, coef_pair.first->index())); + coefs.push_back(coef_pair.second); + } + + const int num_vars = vars.size(); + SCIP_CONS* scip_cons; + // TODO(user): Maybe it is better to expose more of these options, + // potentially through user_suggested_constraint. + CHECK_OK(SCIP_TO_STATUS(SCIPcreateConsLinear( + scip, &scip_cons, user_suggested_constraint.name.c_str(), num_vars, + vars.data(), coefs.data(), + user_suggested_constraint.range.lower_bound(), + user_suggested_constraint.range.upper_bound(), /*initial=*/true, + /*separate=*/true, /*enforce=*/true, /*check=*/true, + /*propagate=*/true, /*local=*/user_suggested_constraint.local, + /*modifiable=*/false, /*dynamic=*/false, /*removable=*/true, + /*stickingatnode=*/false))); + if (user_suggested_constraint.local) { + CHECK_OK(SCIP_TO_STATUS(SCIPaddConsLocal(scip, scip_cons, nullptr))); + } else { + CHECK_OK(SCIP_TO_STATUS(SCIPaddCons(scip, scip_cons))); + } + CHECK_OK(SCIP_TO_STATUS(SCIPreleaseCons(scip, &scip_cons))); + result = ScipSeparationResult::kLazyConstraintAdded; + } + } + } + return result; +} + +struct CallbackSetup { + SCIP_CONSHDLRDATA* scip_handler_data; + internal::ScipCallbackRunner* callback_runner; + ScipConstraintHandlerContext context; + absl::Span useful_constraints; + absl::Span unlikely_useful_constraints; + + CallbackSetup(SCIP* scip, SCIP_CONSHDLR* scip_handler, SCIP_CONS** conss, + int nconss, int nusefulconss, SCIP_SOL* sol, + bool is_pseudo_solution) + : scip_handler_data(SCIPconshdlrGetData(scip_handler)), + callback_runner(scip_handler_data->runner.get()), + context(scip, sol, is_pseudo_solution), + useful_constraints(absl::MakeSpan(conss, nusefulconss)), + unlikely_useful_constraints( + absl::MakeSpan(conss, nconss).subspan(nusefulconss)) { + CHECK(scip_handler_data != nullptr); + CHECK(callback_runner != nullptr); + } +}; + +} // namespace operations_research + +extern "C" { +/** destructor of constraint handler to free user data (called when SCIP is + * exiting) */ +static SCIP_DECL_CONSFREE(ConstraintHandlerFreeC) { + VLOG(3) << "FreeC"; + CHECK(scip != nullptr); + SCIP_CONSHDLRDATA* scip_handler_data = SCIPconshdlrGetData(conshdlr); + CHECK(scip_handler_data != nullptr); + delete scip_handler_data; + SCIPconshdlrSetData(conshdlr, nullptr); + return SCIP_OKAY; +} + +static SCIP_DECL_CONSDELETE(ConstraintHandlerDeleteC) { + VLOG(3) << "DeleteC"; + CHECK(consdata != nullptr); + CHECK(*consdata != nullptr); + delete *consdata; + cons->consdata = nullptr; + return SCIP_OKAY; +} + +static SCIP_DECL_CONSENFOLP(EnforceLpC) { + VLOG(3) << "EnforceC"; + operations_research::CallbackSetup setup(scip, conshdlr, conss, nconss, + nusefulconss, nullptr, false); + operations_research::ScipSeparationResult separation_result = + operations_research::RunSeparation(setup.callback_runner, setup.context, + setup.useful_constraints, + /*is_integral=*/true); + if (separation_result == + operations_research::ScipSeparationResult::kDidNotFind) { + separation_result = operations_research::RunSeparation( + setup.callback_runner, setup.context, setup.unlikely_useful_constraints, + /*is_integral=*/true); + } + switch (separation_result) { + case operations_research::ScipSeparationResult::kLazyConstraintAdded: + *result = SCIP_CONSADDED; + break; + case operations_research::ScipSeparationResult::kCuttingPlaneAdded: + *result = SCIP_SEPARATED; + break; + case operations_research::ScipSeparationResult::kDidNotFind: + *result = SCIP_FEASIBLE; + break; + } + return SCIP_OKAY; +} + +static SCIP_DECL_CONSSEPALP(SeparateLpC) { + VLOG(3) << "SeparateLpC"; + operations_research::CallbackSetup setup(scip, conshdlr, conss, nconss, + nusefulconss, nullptr, false); + operations_research::ScipSeparationResult separation_result = + operations_research::RunSeparation(setup.callback_runner, setup.context, + setup.useful_constraints, + /*is_integral=*/false); + if (separation_result == + operations_research::ScipSeparationResult::kDidNotFind) { + separation_result = operations_research::RunSeparation( + setup.callback_runner, setup.context, setup.unlikely_useful_constraints, + /*is_integral=*/false); + } + switch (separation_result) { + case operations_research::ScipSeparationResult::kLazyConstraintAdded: + *result = SCIP_CONSADDED; + break; + case operations_research::ScipSeparationResult::kCuttingPlaneAdded: + *result = SCIP_SEPARATED; + break; + case operations_research::ScipSeparationResult::kDidNotFind: + *result = SCIP_DIDNOTFIND; + break; + } + return SCIP_OKAY; +} + +static SCIP_DECL_CONSSEPASOL(SeparatePrimalSolutionC) { + VLOG(3) << "SeparatePrimalC"; + operations_research::CallbackSetup setup(scip, conshdlr, conss, nconss, + nusefulconss, sol, false); + operations_research::ScipSeparationResult separation_result = + operations_research::RunSeparation(setup.callback_runner, setup.context, + setup.useful_constraints, + /*is_integral=*/true); + if (separation_result == + operations_research::ScipSeparationResult::kDidNotFind) { + separation_result = operations_research::RunSeparation( + setup.callback_runner, setup.context, setup.unlikely_useful_constraints, + /*is_integral=*/true); + } + switch (separation_result) { + case operations_research::ScipSeparationResult::kLazyConstraintAdded: + *result = SCIP_CONSADDED; + break; + case operations_research::ScipSeparationResult::kCuttingPlaneAdded: + LOG(ERROR) << "Cutting planes cannot be added on integer solutions, " + "treating as a constraint."; + *result = SCIP_CONSADDED; + break; + case operations_research::ScipSeparationResult::kDidNotFind: + *result = SCIP_DIDNOTFIND; + break; + } + return SCIP_OKAY; +} + +static SCIP_DECL_CONSCHECK(CheckFeasibilityC) { + VLOG(3) << "CheckFeasibilityC"; + operations_research::CallbackSetup setup(scip, conshdlr, conss, nconss, + nconss, sol, false); + // All constraints are "useful" for this callback. + for (SCIP_CONS* constraint : setup.useful_constraints) { + SCIP_CONSDATA* consdata = SCIPconsGetData(constraint); + CHECK(consdata != nullptr); + if (!setup.callback_runner->IntegerSolutionFeasible(setup.context, + consdata->data)) { + *result = SCIP_INFEASIBLE; + return SCIP_OKAY; + } + } + *result = SCIP_FEASIBLE; + return SCIP_OKAY; +} +static SCIP_DECL_CONSENFOPS(EnforcePseudoSolutionC) { + VLOG(3) << "EnforcePseudoSolutionC"; + // TODO(user): are we sure the pseudo solution is LP feasible? It seems like + // it doesn't need to be. The code in RunSeparation might assume this? + operations_research::CallbackSetup setup(scip, conshdlr, conss, nconss, + nusefulconss, nullptr, true); + operations_research::ScipSeparationResult separation_result = + operations_research::RunSeparation(setup.callback_runner, setup.context, + setup.useful_constraints, + /*is_integral=*/false); + if (separation_result == + operations_research::ScipSeparationResult::kDidNotFind) { + separation_result = operations_research::RunSeparation( + setup.callback_runner, setup.context, setup.unlikely_useful_constraints, + /*is_integral=*/false); + } + switch (separation_result) { + case operations_research::ScipSeparationResult::kLazyConstraintAdded: + *result = SCIP_CONSADDED; + break; + case operations_research::ScipSeparationResult::kCuttingPlaneAdded: + LOG(ERROR) << "Cutting planes cannot be added on pseudo solutions, " + "treating as a constraint."; + *result = SCIP_CONSADDED; + break; + case operations_research::ScipSeparationResult::kDidNotFind: + *result = SCIP_FEASIBLE; + break; + } + return SCIP_OKAY; +} +static SCIP_DECL_CONSLOCK(VariableRoundingLockC) { + // In this callback, we need to say, for a constraint class and an instance of + // the constraint, for which variables could an {increase,decrease,either} + // affect feasibility. As a conservative overestimate, we say that any + // change in any variable could cause an infeasibility for any instance of + // any callback constraint. + // TODO(user): this could be a little better, but we would need to add + // another method to override on ScipConstraintHandler. + + const int num_vars = operations_research::ScipNumVars(scip); + for (int i = 0; i < num_vars; ++i) { + SCIP_VAR* var = operations_research::ScipGetVar(scip, i); + SCIP_CALL(SCIPaddVarLocksType(scip, var, locktype, nlockspos + nlocksneg, + nlockspos + nlocksneg)); + } + return SCIP_OKAY; +} +} + +namespace operations_research { +namespace internal { + +void AddConstraintHandlerImpl( + const ScipConstraintHandlerDescription& description, + std::unique_ptr runner, SCIP* scip) { + SCIP_CONSHDLR* c_scip_handler; + SCIP_CONSHDLRDATA* scip_handler_data = new SCIP_CONSHDLRDATA; + scip_handler_data->runner = std::move(runner); + + CHECK_OK(SCIP_TO_STATUS(SCIPincludeConshdlrBasic( + scip, &c_scip_handler, description.name.c_str(), + description.description.c_str(), description.enforcement_priority, + description.feasibility_check_priority, description.eager_frequency, + description.needs_constraints, EnforceLpC, EnforcePseudoSolutionC, + CheckFeasibilityC, VariableRoundingLockC, scip_handler_data))); + CHECK(c_scip_handler != nullptr); + CHECK_OK(SCIP_TO_STATUS(SCIPsetConshdlrSepa( + scip, c_scip_handler, SeparateLpC, SeparatePrimalSolutionC, + description.separation_frequency, description.separation_priority, + /*delaysepa=*/false))); + CHECK_OK(SCIP_TO_STATUS( + SCIPsetConshdlrFree(scip, c_scip_handler, ConstraintHandlerFreeC))); + CHECK_OK(SCIP_TO_STATUS( + SCIPsetConshdlrDelete(scip, c_scip_handler, ConstraintHandlerDeleteC))); +} + +void AddCallbackConstraintImpl(SCIP* scip, const std::string& handler_name, + const std::string& constraint_name, + void* constraint_data, + const ScipCallbackConstraintOptions& options) { + SCIP_CONSHDLR* conshdlr = SCIPfindConshdlr(scip, handler_name.c_str()); + CHECK(conshdlr != nullptr) + << "Constraint handler " << handler_name << " not registered with scip."; + SCIP_ConsData* consdata = new SCIP_ConsData; + consdata->data = constraint_data; + SCIP_CONS* constraint = nullptr; + CHECK_OK(SCIP_TO_STATUS(SCIPcreateCons( + scip, &constraint, constraint_name.c_str(), conshdlr, consdata, + options.initial, options.separate, options.enforce, options.check, + options.propagate, options.local, options.modifiable, options.dynamic, + options.removable, options.stickingatnodes))); + CHECK(constraint != nullptr); + CHECK_OK(SCIP_TO_STATUS(SCIPaddCons(scip, constraint))); + CHECK_OK(SCIP_TO_STATUS(SCIPreleaseCons(scip, &constraint))); +} + +} // namespace internal +} // namespace operations_research diff --git a/ortools/linear_solver/scip_callback.h b/ortools/linear_solver/scip_callback.h new file mode 100644 index 0000000000..e4e3f529ab --- /dev/null +++ b/ortools/linear_solver/scip_callback.h @@ -0,0 +1,278 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// See go/scip-callbacks for documentation. +// +// This file provides a simplified C++ API for using callbacks with SCIP and +// MPSolver. It can be used directly by users, although in most cases, the +// mp_callback.h should be sufficient (in fact, SCIP's mp_callback.h +// implementation is built on top of this). See also go/mpsolver-callbacks. + +#ifndef OR_TOOLS_LINEAR_SOLVER_SCIP_CALLBACK_H_ +#define OR_TOOLS_LINEAR_SOLVER_SCIP_CALLBACK_H_ + +#include +#include + +#include "absl/memory/memory.h" +#include "ortools/linear_solver/linear_expr.h" +#include "ortools/linear_solver/linear_solver.h" +#include "scip/scip_sol.h" +#include "scip/type_cons.h" +#include "scip/type_scip.h" +#include "scip/type_sol.h" +#include "scip/type_var.h" + +namespace operations_research { + +// See https://scip.zib.de/doc-6.0.2/html/CONS.php#CONS_PROPERTIES for details. +// For member below, the corresponding SCIP constraint handler property name is +// provided. +// +// TODO(user): no effort has been made to optimize the default values of +// enforcement_priority, feasibility_check_priority, eager_frequency, or +// separation_priority. +struct ScipConstraintHandlerDescription { + // See CONSHDLR_NAME in SCIP documentation above. + std::string name; + + // See CONSHDLR_DESC in SCIP documentation above. + std::string description; + + // See CONSHDLR_ENFOPRIORITY in the SCIP documentation above. Determines the + // order this constraint class is checked at each LP node. + // + // WARNING(rander): Assumed that enforcement_priority < 0. (This enforcement + // runs after integrality enforcement, so CONSENFOLP always runs on integral + // solutions.) + int enforcement_priority = -100; + + // See CONSHDLR_CHECKPRIORITY: in the SCIP documentation above. Determines the + // order this constraint class runs in when testing solution feasibility. + // + // WARNING(rander): Assumed that feasibility_check_priority < 0. (This check + // runs after the integrality check, so CONSCHECK always runs on integral + // solutions.) + int feasibility_check_priority = -100; + + // See CONSHDLR_EAGERFREQ in SCIP documentation above. + int eager_frequency = 10; + + // See CONSHDLR_NEEDSCONS in SCIP documentation above. + bool needs_constraints = false; + + // See CONSHDLR_SEPAPRIORITY in SCIP documentation above. Determines the + // order this constraint class runs in the cut loop. + int separation_priority = 100; + + // See CONSHDLR_SEPAFREQ in the SCIP documentation above. + int separation_frequency = 1; +}; + +class ScipConstraintHandlerContext { + public: + // A value of nullptr for solution means to use the current LP solution. + ScipConstraintHandlerContext(SCIP* scip, SCIP_SOL* solution, + bool is_pseudo_solution); + double VariableValue(const MPVariable* variable) const; + + int64 CurrentNodeId() const; + int64 NumNodesProcessed() const; + + SCIP* scip() const { return scip_; } + + // Pseudo solutions may not be LP feasible. Duals/reduced costs are not + // available (the LP solver failed at this node). + // + // Do not add "user cuts" here (that strengthen LP solution but don't change + // feasible region), add only "lazy constraints" (cut off integer solutions). + // + // TODO(user): maybe this can be abstracted away. + bool is_pseudo_solution() const { return is_pseudo_solution_; } + + private: + SCIP* scip_; + SCIP_SOL* solution_; + bool is_pseudo_solution_; +}; + +struct CallbackRangeConstraint { + LinearRange range; + bool is_cut = false; // Does not remove any integer points. + std::string name; // can be empty + bool local = false; +}; + +// See go/scip-callbacks for additional documentation. +template +class ScipConstraintHandler { + public: + explicit ScipConstraintHandler( + const ScipConstraintHandlerDescription& description) + : description_(description) {} + virtual ~ScipConstraintHandler() {} + const ScipConstraintHandlerDescription& description() const { + return description_; + } + + // Unless SeparateIntegerSolution() below is overridden, this must find a + // violated lazy constraint if one exists when given an integral solution. + virtual std::vector SeparateFractionalSolution( + const ScipConstraintHandlerContext& context, + const Constraint& constraint) = 0; + + // This MUST find a violated lazy constraint if one exists. + // All constraints returned must have is_cut as false. + virtual std::vector SeparateIntegerSolution( + const ScipConstraintHandlerContext& context, + const Constraint& constraint) { + return SeparateFractionalSolution(context, constraint); + } + + // Returns true if no constraints are violated. + virtual bool FractionalSolutionFeasible( + const ScipConstraintHandlerContext& context, + const Constraint& constraint) { + return SeparateFractionalSolution(context, constraint).empty(); + } + + // This MUST find a violated constraint if one exists. + virtual bool IntegerSolutionFeasible( + const ScipConstraintHandlerContext& context, + const Constraint& constraint) { + return SeparateIntegerSolution(context, constraint).empty(); + } + + private: + ScipConstraintHandlerDescription description_; +}; + +// handler is not owned but held. +template +void RegisterConstraintHandler(ScipConstraintHandler* handler, + SCIP* scip); + +struct ScipCallbackConstraintOptions { + bool initial = true; + bool separate = true; + bool enforce = true; + bool check = true; + bool propagate = true; + bool local = false; + bool modifiable = false; + bool dynamic = false; + bool removable = true; + bool stickingatnodes = false; +}; + +// constraint_data is not owned but held. +template +void AddCallbackConstraint(SCIP* scip, + ScipConstraintHandler* handler, + const std::string& constraint_name, + const ConstraintData* constraint_data, + const ScipCallbackConstraintOptions& options); + +// Implementation details, here and below. + +namespace internal { + +class ScipCallbackRunner { + public: + virtual ~ScipCallbackRunner() {} + virtual std::vector SeparateFractionalSolution( + const ScipConstraintHandlerContext& context, void* constraint) = 0; + + virtual std::vector SeparateIntegerSolution( + const ScipConstraintHandlerContext& context, void* constraint) = 0; + + virtual bool FractionalSolutionFeasible( + const ScipConstraintHandlerContext& context, void* constraint) = 0; + + virtual bool IntegerSolutionFeasible( + const ScipConstraintHandlerContext& context, void* constraint) = 0; +}; + +template +class ScipCallbackRunnerImpl : public ScipCallbackRunner { + public: + explicit ScipCallbackRunnerImpl( + ScipConstraintHandler* handler) + : handler_(handler) {} + + std::vector SeparateFractionalSolution( + const ScipConstraintHandlerContext& context, + void* constraint_data) override { + return handler_->SeparateFractionalSolution( + context, *static_cast(constraint_data)); + } + + std::vector SeparateIntegerSolution( + const ScipConstraintHandlerContext& context, + void* constraint_data) override { + return handler_->SeparateIntegerSolution( + context, *static_cast(constraint_data)); + } + + bool FractionalSolutionFeasible(const ScipConstraintHandlerContext& context, + void* constraint_data) override { + return handler_->FractionalSolutionFeasible( + context, *static_cast(constraint_data)); + } + + bool IntegerSolutionFeasible(const ScipConstraintHandlerContext& context, + void* constraint_data) override { + return handler_->IntegerSolutionFeasible( + context, *static_cast(constraint_data)); + } + + private: + ScipConstraintHandler* handler_; +}; + +void AddConstraintHandlerImpl( + const ScipConstraintHandlerDescription& description, + std::unique_ptr runner, SCIP* scip); + +void AddCallbackConstraintImpl(SCIP* scip, const std::string& handler_name, + const std::string& constraint_name, + void* constraint_data, + const ScipCallbackConstraintOptions& options); + +} // namespace internal + +template +void RegisterConstraintHandler(ScipConstraintHandler* handler, + SCIP* scip) { + internal::AddConstraintHandlerImpl( + handler->description(), + absl::make_unique>( + handler), + scip); +} + +template +void AddCallbackConstraint(SCIP* scip, + ScipConstraintHandler* handler, + const std::string& constraint_name, + const ConstraintData* constraint_data, + const ScipCallbackConstraintOptions& options) { + internal::AddCallbackConstraintImpl( + scip, handler->description().name, constraint_name, + static_cast(const_cast(constraint_data)), + options); +} + +} // namespace operations_research + +#endif // OR_TOOLS_LINEAR_SOLVER_SCIP_CALLBACK_H_ diff --git a/ortools/linear_solver/scip_interface.cc b/ortools/linear_solver/scip_interface.cc index cb6e7c0048..a5b6713815 100644 --- a/ortools/linear_solver/scip_interface.cc +++ b/ortools/linear_solver/scip_interface.cc @@ -30,12 +30,16 @@ #include "ortools/base/logging.h" #include "ortools/base/status_macros.h" #include "ortools/base/timer.h" +#include "ortools/gscip/legacy_scip_params.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/linear_solver_callback.h" +#include "ortools/linear_solver/scip_callback.h" #include "ortools/linear_solver/scip_helper_macros.h" #include "ortools/linear_solver/scip_proto_solver.h" #include "scip/cons_indicator.h" #include "scip/scip.h" +#include "scip/scip_param.h" #include "scip/scip_prob.h" #include "scip/scipdefplugins.h" @@ -44,6 +48,12 @@ DEFINE_bool(scip_feasibility_emphasis, false, "may not result in speedups in some problems."); namespace operations_research { +namespace { +// See the class ScipConstraintHandlerForMPCallback below. +struct EmptyStruct {}; +} // namespace + +class ScipConstraintHandlerForMPCallback; class SCIPInterface : public MPSolverInterface { public: @@ -105,6 +115,33 @@ class SCIPInterface : public MPSolverInterface { void* underlying_solver() override { return reinterpret_cast(scip_); } + // MULTIPLE SOLUTIONS SUPPORT + // The default behavior of scip is to store the top incidentally generated + // integer solutions in the solution pool. The default maximum size is 100. + // This can be adjusted by setting the param limits/maxsol. There is no way + // to ensure that the pool will actually be full. + // + // You can also ask SCIP to enumerate all feasible solutions. Combined with + // an equality or inequality constraint on the objective (after solving once + // to find the optimal solution), you can use this to find all high quality + // solutions. See https://scip.zib.de/doc/html/COUNTER.php. This behavior is + // not supported directly through MPSolver, but in theory can be controlled + // entirely through scip parameters. + bool NextSolution() override; + + // CALLBACK SUPPORT: + // * We support MPSolver's callback API via MPCallback. + // See ./linear_solver_callback.h. + // * We also support SCIP's more general callback interface, built on + // 'constraint handlers'. See ./scip_callback.h and test, these are added + // directly to the underlying SCIP object, bypassing SCIPInterface. + // The former works by calling the latter. See go/scip-callbacks for + // a complete documentation of this design. + + // MPCallback API + void SetCallback(MPCallback* mp_callback) override; + bool SupportsCallbacks() const override { return true; } + private: void SetParameters(const MPSolverParameters& param) override; void SetRelativeMipGap(double value) override; @@ -131,8 +168,11 @@ class SCIPInterface : public MPSolverInterface { MPSolverParameters::IntegerParam param) override; void SetIntegerParamToUnsupportedValue(MPSolverParameters::IntegerParam param, int value) override; + // How many solutions SCIP found. + int SolutionCount(); // Copy sol from SCIP to MPSolver. void SetSolution(SCIP_SOL* solution); + absl::Status CreateSCIP(); void DeleteSCIP(); @@ -147,7 +187,32 @@ class SCIPInterface : public MPSolverInterface { SCIP* scip_; std::vector scip_variables_; std::vector scip_constraints_; + int current_solution_index_ = 0; + MPCallback* callback_ = nullptr; + std::unique_ptr scip_constraint_handler_; + // See ScipConstraintHandlerForMPCallback below. + EmptyStruct constraint_data_for_handler_; bool branching_priority_reset_ = false; + bool callback_reset_ = false; +}; + +class ScipConstraintHandlerForMPCallback + : public ScipConstraintHandler { + public: + explicit ScipConstraintHandlerForMPCallback(MPCallback* mp_callback); + + std::vector SeparateFractionalSolution( + const ScipConstraintHandlerContext& context, const EmptyStruct&) override; + + std::vector SeparateIntegerSolution( + const ScipConstraintHandlerContext& context, const EmptyStruct&) override; + + private: + std::vector SeparateSolution( + const ScipConstraintHandlerContext& context, + const bool at_integer_solution); + + MPCallback* mp_callback_; }; SCIPInterface::SCIPInterface(MPSolver* solver) @@ -159,6 +224,7 @@ SCIPInterface::~SCIPInterface() { DeleteSCIP(); } void SCIPInterface::Reset() { DeleteSCIP(); + scip_constraint_handler_.reset(); status_ = CreateSCIP(); ResetExtractionInformation(); } @@ -206,12 +272,12 @@ void SCIPInterface::DeleteSCIP() { scip_ = nullptr; } -#define RETURN_IF_ALREADY_IN_ERROR_STATE \ - do { \ - if (!status_.ok()) { \ - LOG_EVERY_N(INFO, 10) << "Early abort: SCIP is in error state."; \ - return; \ - } \ +#define RETURN_IF_ALREADY_IN_ERROR_STATE \ + do { \ + if (!status_.ok()) { \ + VLOG_EVERY_N(1, 10) << "Early abort: SCIP is in error state."; \ + return; \ + } \ } while (false) #define RETURN_AND_STORE_IF_SCIP_ERROR(x) \ @@ -576,9 +642,10 @@ MPSolver::ResultStatus SCIPInterface::Solve(const MPSolverParameters& param) { // TODO(user): Is that still true now (2018) ? if (param.GetIntegerParam(MPSolverParameters::INCREMENTALITY) == MPSolverParameters::INCREMENTALITY_OFF || - branching_priority_reset_) { + branching_priority_reset_ || callback_reset_) { Reset(); branching_priority_reset_ = false; + callback_reset_ = false; } // Set log level. @@ -595,6 +662,16 @@ MPSolver::ResultStatus SCIPInterface::Solve(const MPSolverParameters& param) { ExtractModel(); VLOG(1) << absl::StrFormat("Model built in %s.", absl::FormatDuration(timer.GetDuration())); + if (callback_ != nullptr) { + scip_constraint_handler_ = + absl::make_unique(callback_); + RegisterConstraintHandler(scip_constraint_handler_.get(), + scip_); + AddCallbackConstraint(scip_, scip_constraint_handler_.get(), + "mp_solver_callback_constraint_for_scip", + &constraint_data_for_handler_, + ScipCallbackConstraintOptions()); + } // Time limit. if (solver_->time_limit() != 0) { @@ -669,7 +746,7 @@ MPSolver::ResultStatus SCIPInterface::Solve(const MPSolverParameters& param) { : SCIPsolve(scip_)); VLOG(1) << absl::StrFormat("Solved in %s.", absl::FormatDuration(timer.GetDuration())); - + current_solution_index_ = 0; // Get the results. SCIP_SOL* const solution = SCIPgetBestSol(scip_); if (solution != nullptr) { @@ -751,6 +828,22 @@ absl::optional SCIPInterface::DirectlySolveProto( return response; } +int SCIPInterface::SolutionCount() { return SCIPgetNSols(scip_); } + +bool SCIPInterface::NextSolution() { + // Make sure we have successfully solved the problem and not modified it. + if (!CheckSolutionIsSynchronizedAndExists()) { + return false; + } + if (current_solution_index_ + 1 >= SolutionCount()) { + return false; + } + current_solution_index_++; + SCIP_SOL** all_solutions = SCIPgetSols(scip_); + SetSolution(all_solutions[current_solution_index_]); + return true; +} + int64 SCIPInterface::iterations() const { // NOTE(user): As of 2018-12 it doesn't run in the stubby server, and is // a specialized call, so it's ok to crash if the status is broken. @@ -802,19 +895,6 @@ void SCIPInterface::SetRelativeMipGap(double value) { } void SCIPInterface::SetPrimalTolerance(double value) { - // SCIP automatically updates numerics/lpfeastol if the primal tolerance is - // tighter. Doing that it unconditionally logs this modification to stderr. By - // setting numerics/lpfeastol first we avoid this unwanted log. - // double current_lpfeastol = 0.0; - // CHECK_EQ(SCIP_OKAY, - // SCIPgetRealParam(scip_, "numerics/lpfeastol", - // ¤t_lpfeastol)); - // if (value < current_lpfeastol) { - // // See the NOTE on SetRelativeMipGap(). - // const auto status = - // SCIP_TO_STATUS(SCIPsetRealParam(scip_, "numerics/lpfeastol", value)); - // if (status_.ok()) status_ = status; - // } // See the NOTE on SetRelativeMipGap(). const auto status = SCIP_TO_STATUS(SCIPsetRealParam(scip_, "numerics/feastol", value)); @@ -918,7 +998,7 @@ absl::Status SCIPInterface::SetNumThreads(int num_threads) { bool SCIPInterface::SetSolverSpecificParametersAsString( const std::string& parameters) { const absl::Status s = - operations_research::ScipSetSolverSpecificParameters(parameters, scip_); + LegacyScipSetSolverSpecificParameters(parameters, scip_); if (!s.ok()) { LOG(WARNING) << "Failed to set SCIP parameter string: " << parameters << ", error is: " << s; @@ -926,6 +1006,111 @@ bool SCIPInterface::SetSolverSpecificParametersAsString( return s.ok(); } +class ScipMPCallbackContext : public MPCallbackContext { + public: + ScipMPCallbackContext(const ScipConstraintHandlerContext* scip_context, + bool at_integer_solution) + : scip_context_(scip_context), + at_integer_solution_(at_integer_solution) {} + + MPCallbackEvent Event() override { + if (at_integer_solution_) { + return MPCallbackEvent::kMipSolution; + } + return MPCallbackEvent::kMipNode; + } + + bool CanQueryVariableValues() override { + return !scip_context_->is_pseudo_solution(); + } + + double VariableValue(const MPVariable* variable) override { + CHECK(CanQueryVariableValues()); + return scip_context_->VariableValue(variable); + } + + void AddCut(const LinearRange& cutting_plane) override { + CallbackRangeConstraint constraint; + constraint.is_cut = true; + constraint.range = cutting_plane; + constraint.local = false; + constraints_added_.push_back(std::move(constraint)); + } + + void AddLazyConstraint(const LinearRange& lazy_constraint) override { + CallbackRangeConstraint constraint; + constraint.is_cut = false; + constraint.range = lazy_constraint; + constraint.local = false; + constraints_added_.push_back(std::move(constraint)); + } + + double SuggestSolution( + const absl::flat_hash_map& solution) override { + LOG(FATAL) << "SuggestSolution() not currently supported for SCIP."; + } + + int64 NumExploredNodes() override { + // scip_context_->NumNodesProcessed() returns: + // 0 before the root node is solved, e.g. if a heuristic finds a solution. + // 1 at the root node + // > 1 after the root node. + // The NumExploredNodes spec requires that we return 0 at the root node, + // (this is consistent with gurobi). Below is a bandaid to try and make the + // behavior consistent, although some information is lost. + return std::max(int64{0}, scip_context_->NumNodesProcessed() - 1); + } + + const std::vector& constraints_added() { + return constraints_added_; + } + + private: + const ScipConstraintHandlerContext* scip_context_; + bool at_integer_solution_; + // second value of pair is true for cuts and false for lazy constraints. + std::vector constraints_added_; +}; + +ScipConstraintHandlerForMPCallback::ScipConstraintHandlerForMPCallback( + MPCallback* mp_callback) + : ScipConstraintHandler( + // MOE(begin-strip): + {/*name=*/"mp_solver_constraint_handler", + /*description=*/ + "A single constraint handler for all MPSolver models."} + // MOE(end-strip-and-replace): ScipConstraintHandlerDescription() + ), + mp_callback_(mp_callback) {} + +std::vector +ScipConstraintHandlerForMPCallback::SeparateFractionalSolution( + const ScipConstraintHandlerContext& context, const EmptyStruct&) { + return SeparateSolution(context, /*at_integer_solution=*/false); +} + +std::vector +ScipConstraintHandlerForMPCallback::SeparateIntegerSolution( + const ScipConstraintHandlerContext& context, const EmptyStruct&) { + return SeparateSolution(context, /*at_integer_solution=*/true); +} + +std::vector +ScipConstraintHandlerForMPCallback::SeparateSolution( + const ScipConstraintHandlerContext& context, + const bool at_integer_solution) { + ScipMPCallbackContext mp_context(&context, at_integer_solution); + mp_callback_->RunCallback(&mp_context); + return mp_context.constraints_added(); +} + +void SCIPInterface::SetCallback(MPCallback* mp_callback) { + if (callback_ != nullptr) { + callback_reset_ = true; + } + callback_ = mp_callback; +} + MPSolverInterface* BuildSCIPInterface(MPSolver* const solver) { return new SCIPInterface(solver); } diff --git a/ortools/linear_solver/scip_proto_solver.cc b/ortools/linear_solver/scip_proto_solver.cc index 2d0fd727cd..4364c896ad 100644 --- a/ortools/linear_solver/scip_proto_solver.cc +++ b/ortools/linear_solver/scip_proto_solver.cc @@ -24,6 +24,7 @@ #include #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" @@ -32,6 +33,7 @@ #include "ortools/base/cleanup.h" #include "ortools/base/commandlineflags.h" #include "ortools/base/status_macros.h" +#include "ortools/gscip/legacy_scip_params.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_validator.h" #include "ortools/linear_solver/scip_helper_macros.h" @@ -56,91 +58,6 @@ DEFINE_string(scip_proto_solver_output_cip_file, "", namespace operations_research { -absl::Status ScipSetSolverSpecificParameters(const std::string& parameters, - SCIP* scip) { - for (const auto parameter : - absl::StrSplit(parameters, '\n', absl::SkipWhitespace())) { - std::vector key_value = - absl::StrSplit(parameter, '=', absl::SkipWhitespace()); - if (key_value.size() != 2) { - return absl::InvalidArgumentError( - absl::StrFormat("Cannot parse parameter '%s'. Expected format is " - "'parameter/name = value'", - parameter)); - } - - std::string name = key_value[0]; - absl::RemoveExtraAsciiWhitespace(&name); - std::string value = key_value[1]; - absl::RemoveExtraAsciiWhitespace(&value); - const double infinity = SCIPinfinity(scip); - - SCIP_PARAM* param = SCIPgetParam(scip, name.c_str()); - if (param == nullptr) { - return absl::InvalidArgumentError( - absl::StrFormat("Invalid parameter name '%s'", name)); - } - switch (param->paramtype) { - case SCIP_PARAMTYPE_BOOL: { - bool parsed_value; - if (absl::SimpleAtob(value, &parsed_value)) { - RETURN_IF_SCIP_ERROR( - SCIPsetBoolParam(scip, name.c_str(), parsed_value)); - continue; - } - break; - } - case SCIP_PARAMTYPE_INT: { - int parsed_value; - if (absl::SimpleAtoi(value, &parsed_value)) { - RETURN_IF_SCIP_ERROR( - SCIPsetIntParam(scip, name.c_str(), parsed_value)); - continue; - } - break; - } - case SCIP_PARAMTYPE_LONGINT: { - int64 parsed_value; - if (absl::SimpleAtoi(value, &parsed_value)) { - RETURN_IF_SCIP_ERROR( - SCIPsetLongintParam(scip, name.c_str(), parsed_value)); - continue; - } - break; - } - case SCIP_PARAMTYPE_REAL: { - double parsed_value; - if (absl::SimpleAtod(value, &parsed_value)) { - if (parsed_value > infinity) parsed_value = infinity; - RETURN_IF_SCIP_ERROR( - SCIPsetRealParam(scip, name.c_str(), parsed_value)); - continue; - } - break; - } - case SCIP_PARAMTYPE_CHAR: { - if (value.size() == 1) { - RETURN_IF_SCIP_ERROR(SCIPsetCharParam(scip, name.c_str(), value[0])); - continue; - } - break; - } - case SCIP_PARAMTYPE_STRING: { - if (value.front() == '"' && value.back() == '"') { - value.erase(value.begin()); - value.erase(value.end() - 1); - } - RETURN_IF_SCIP_ERROR( - SCIPsetStringParam(scip, name.c_str(), value.c_str())); - continue; - } - } - return absl::InvalidArgumentError( - absl::StrFormat("Invalid parameter value '%s'", parameter)); - } - return absl::OkStatus(); -} - namespace { // This function will create a new constraint if the indicator constraint has // both a lower bound and an upper bound. @@ -781,7 +698,7 @@ absl::StatusOr ScipSolveProto( return response; } - const auto parameters_status = ScipSetSolverSpecificParameters( + const auto parameters_status = LegacyScipSetSolverSpecificParameters( request.solver_specific_parameters(), scip); if (!parameters_status.ok()) { response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); diff --git a/ortools/linear_solver/scip_proto_solver.h b/ortools/linear_solver/scip_proto_solver.h index d65b1a73db..75cfd51fce 100644 --- a/ortools/linear_solver/scip_proto_solver.h +++ b/ortools/linear_solver/scip_proto_solver.h @@ -14,15 +14,12 @@ #ifndef OR_TOOLS_LINEAR_SOLVER_SCIP_PROTO_SOLVER_H_ #define OR_TOOLS_LINEAR_SOLVER_SCIP_PROTO_SOLVER_H_ -#include "ortools/base/statusor.h" +#include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "scip/type_scip.h" namespace operations_research { -absl::Status ScipSetSolverSpecificParameters(const std::string& parameters, - SCIP* scip); - // Note, here we do not override any of SCIP default parameters. This behavior // *differs* from `MPSolver::Solve()` which sets the feasibility tolerance to // 1e-7, and the gap limit to 0.0001 (whereas SCIP defaults are 1e-6 and 0, diff --git a/ortools/lp_data/BUILD b/ortools/lp_data/BUILD index 78de5fa1b9..7b48581e99 100644 --- a/ortools/lp_data/BUILD +++ b/ortools/lp_data/BUILD @@ -272,7 +272,9 @@ cc_library( "//ortools/base:map_util", "//ortools/base:protobuf_util", "@com_google_absl//absl/status", + "@com_google_absl//absl/container:node_hash_set", "//ortools/base:status_macros", + "//ortools/base:status_builder", "//ortools/linear_solver:linear_solver_cc_proto", "@com_google_absl//absl/strings", ], diff --git a/ortools/lp_data/lp_data.cc b/ortools/lp_data/lp_data.cc index 84266956bd..e02a318ef7 100644 --- a/ortools/lp_data/lp_data.cc +++ b/ortools/lp_data/lp_data.cc @@ -1119,6 +1119,29 @@ void UpdateMinAndMaxMagnitude(const FractionalRange& range, } } +Fractional GetMedianScalingFactor(const DenseRow& range) { + std::vector median; + for (const Fractional value : range) { + if (value == 0.0) continue; + median.push_back(std::abs(value)); + } + if (median.empty()) return 1.0; + std::sort(median.begin(), median.end()); + return median[median.size() / 2]; +} + +Fractional GetMeanScalingFactor(const DenseRow& range) { + Fractional mean = 0.0; + int num_non_zeros = 0; + for (const Fractional value : range) { + if (value == 0.0) continue; + ++num_non_zeros; + mean += std::abs(value); + } + if (num_non_zeros == 0.0) return 1.0; + return mean / static_cast(num_non_zeros); +} + Fractional ComputeDivisorSoThatRangeContainsOne(Fractional min_magnitude, Fractional max_magnitude) { if (min_magnitude > 1.0 && min_magnitude < kInfinity) { @@ -1131,22 +1154,36 @@ Fractional ComputeDivisorSoThatRangeContainsOne(Fractional min_magnitude, } // namespace -Fractional LinearProgram::ScaleObjective() { +Fractional LinearProgram::ScaleObjective( + GlopParameters::CostScalingAlgorithm method) { Fractional min_magnitude = kInfinity; Fractional max_magnitude = 0.0; UpdateMinAndMaxMagnitude(objective_coefficients(), &min_magnitude, &max_magnitude); - const Fractional cost_scaling_factor = - ComputeDivisorSoThatRangeContainsOne(min_magnitude, max_magnitude); + Fractional cost_scaling_factor = 1.0; + switch (method) { + case GlopParameters::NO_COST_SCALING: + break; + case GlopParameters::CONTAIN_ONE_COST_SCALING: + cost_scaling_factor = + ComputeDivisorSoThatRangeContainsOne(min_magnitude, max_magnitude); + break; + case GlopParameters::MEAN_COST_SCALING: + cost_scaling_factor = GetMeanScalingFactor(objective_coefficients()); + break; + case GlopParameters::MEDIAN_COST_SCALING: + cost_scaling_factor = GetMedianScalingFactor(objective_coefficients()); + break; + } if (cost_scaling_factor != 1.0) { for (ColIndex col(0); col < num_variables(); ++col) { + if (objective_coefficients()[col] == 0.0) continue; SetObjectiveCoefficient( col, objective_coefficients()[col] / cost_scaling_factor); } SetObjectiveScalingFactor(objective_scaling_factor() * cost_scaling_factor); SetObjectiveOffset(objective_offset() / cost_scaling_factor); } - VLOG(1) << "Objective magnitude range is [" << min_magnitude << ", " << max_magnitude << "] (dividing by " << cost_scaling_factor << ")."; return cost_scaling_factor; diff --git a/ortools/lp_data/lp_data.h b/ortools/lp_data/lp_data.h index b3ac6885f8..4596ab160e 100644 --- a/ortools/lp_data/lp_data.h +++ b/ortools/lp_data/lp_data.h @@ -496,7 +496,7 @@ class LinearProgram { // multiplying the new ones by the returned factor. // - For ScaleBounds(), the old variable and constraint bounds can be // retrieved by multiplying the new ones by the returned factor. - Fractional ScaleObjective(); + Fractional ScaleObjective(GlopParameters::CostScalingAlgorithm method); Fractional ScaleBounds(); // Removes the given row indices from the LinearProgram. diff --git a/ortools/lp_data/lp_data_utils.cc b/ortools/lp_data/lp_data_utils.cc index b595a72128..280766354d 100644 --- a/ortools/lp_data/lp_data_utils.cc +++ b/ortools/lp_data/lp_data_utils.cc @@ -73,12 +73,13 @@ void Scale(LinearProgram* lp, SparseMatrixScaler* scaler, lp->transpose_matrix_is_consistent_ = false; } -void LpScalingHelper::Scale(LinearProgram* lp) { +void LpScalingHelper::Scale(LinearProgram* lp) { Scale(GlopParameters(), lp); } + +void LpScalingHelper::Scale(const GlopParameters& params, LinearProgram* lp) { scaler_.Clear(); - ::operations_research::glop::Scale( - lp, &scaler_, operations_research::glop::GlopParameters::DEFAULT); + ::operations_research::glop::Scale(lp, &scaler_, params.scaling_method()); bound_scaling_factor_ = 1.0 / lp->ScaleBounds(); - objective_scaling_factor_ = 1.0 / lp->ScaleObjective(); + objective_scaling_factor_ = 1.0 / lp->ScaleObjective(params.cost_scaling()); } void LpScalingHelper::Clear() { diff --git a/ortools/lp_data/lp_data_utils.h b/ortools/lp_data/lp_data_utils.h index 681098933e..127e7d67fc 100644 --- a/ortools/lp_data/lp_data_utils.h +++ b/ortools/lp_data/lp_data_utils.h @@ -52,6 +52,7 @@ class LpScalingHelper { public: // Scale the given LP. void Scale(LinearProgram* lp); + void Scale(const GlopParameters& params, LinearProgram* lp); // Clear all scaling coefficients. void Clear(); diff --git a/ortools/lp_data/lp_utils.h b/ortools/lp_data/lp_utils.h index 7d01af7f9d..4a0edad0db 100644 --- a/ortools/lp_data/lp_utils.h +++ b/ortools/lp_data/lp_utils.h @@ -81,7 +81,7 @@ Fractional ScalarProduct(const DenseRowOrColumn1& u, template Fractional ScalarProduct(const DenseRowOrColumn& u, const SparseColumn& v) { Fractional sum(0.0); - for (const SparseColumn::Entry e : v) { + for (const SparseColumn::Entry& e : v) { sum += u[typename DenseRowOrColumn::IndexType(e.row().value())] * e.coefficient(); } @@ -103,7 +103,7 @@ template Fractional PreciseScalarProduct(const DenseRowOrColumn& u, const SparseColumn& v) { KahanSum sum; - for (const SparseColumn::Entry e : v) { + for (const SparseColumn::Entry& e : v) { sum.Add(u[typename DenseRowOrColumn::IndexType(e.row().value())] * e.coefficient()); } diff --git a/ortools/lp_data/mps_reader.cc b/ortools/lp_data/mps_reader.cc index 7737f66e11..e9002f711b 100644 --- a/ortools/lp_data/mps_reader.cc +++ b/ortools/lp_data/mps_reader.cc @@ -14,9 +14,11 @@ #include "ortools/lp_data/mps_reader.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/match.h" #include "absl/strings/str_split.h" -//#include "ortools/base/status_builder.h" +#include "ortools/base/status_builder.h" +#include "ortools/lp_data/lp_types.h" namespace operations_research { namespace glop { @@ -423,7 +425,7 @@ class DataWrapper { absl::flat_hash_map variable_indices_by_name_; absl::flat_hash_map constraint_indices_by_name_; - std::set constraints_to_delete_; + absl::node_hash_set constraints_to_delete_; }; template @@ -596,14 +598,14 @@ absl::Status MPSReaderImpl::ProcessRowsSection(bool is_lazy, template absl::Status MPSReaderImpl::ProcessColumnsSection(DataWrapper* data) { // Take into account the INTORG and INTEND markers. - if (line_.find("'MARKER'") != std::string::npos) { - if (line_.find("'INTORG'") != std::string::npos) { + if (absl::StrContains(line_, "'MARKER'")) { + if (absl::StrContains(line_, "'INTORG'")) { VLOG(2) << "Entering integer marker.\n" << line_; if (in_integer_section_) { return InvalidArgumentError("Found INTORG inside the integer section."); } in_integer_section_ = true; - } else if (line_.find("'INTEND'") != std::string::npos) { + } else if (absl::StrContains(line_, "'INTEND'")) { VLOG(2) << "Leaving integer marker.\n" << line_; if (!in_integer_section_) { return InvalidArgumentError( @@ -722,11 +724,9 @@ absl::Status MPSReaderImpl::ProcessIndicatorsSection(DataWrapper* data) { data->SetVariableBounds(col, std::max(0.0, data->VariableLowerBound(col)), std::min(1.0, data->VariableUpperBound(col))); - RETURN_IF_ERROR(data->CreateIndicatorConstraint(row_name, col, value)); + RETURN_IF_ERROR( + AppendLineToError(data->CreateIndicatorConstraint(row_name, col, value))); - // RETURN_IF_ERROR( - // AppendLineToError(data->CreateIndicatorConstraint(row_name, col, - // value))); return absl::OkStatus(); } @@ -738,6 +738,7 @@ absl::Status MPSReaderImpl::StoreCoefficient(int col, if (row_name.empty() || row_name == "$") { return absl::OkStatus(); } + double value; ASSIGN_OR_RETURN(value, GetDoubleFromString(row_value)); if (value == kInfinity || value == -kInfinity) { @@ -1025,13 +1026,13 @@ absl::Status MPSReaderImpl::ProcessSosSection() { absl::Status MPSReaderImpl::InvalidArgumentError( const std::string& error_message) { - return absl::InvalidArgumentError(error_message); + return AppendLineToError(absl::InvalidArgumentError(error_message)); } -// absl::Status MPSReaderImpl::AppendLineToError(const absl::Status& status) { -// return absl::StatusBuilder(status, GTL_LOC).SetAppend() -// << " Line " << line_num_ << ": \"" << line_ << "\"."; -// } +absl::Status MPSReaderImpl::AppendLineToError(const absl::Status& status) { + return util::StatusBuilder(status).SetAppend() + << " Line " << line_num_ << ": \"" << line_ << "\"."; +} // Parses instance from a file. absl::Status MPSReader::ParseFile(const std::string& file_name, diff --git a/ortools/lp_data/mps_reader.h b/ortools/lp_data/mps_reader.h index de88fd6c3c..47f431fc95 100644 --- a/ortools/lp_data/mps_reader.h +++ b/ortools/lp_data/mps_reader.h @@ -32,9 +32,8 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/numbers.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_split.h" #include "ortools/base/commandlineflags.h" #include "ortools/base/filelineiter.h" #include "ortools/base/hash.h" @@ -45,7 +44,6 @@ #include "ortools/base/map_util.h" #include "ortools/base/protobuf_util.h" #include "ortools/base/status_macros.h" -#include "ortools/base/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/lp_data/lp_data.h" #include "ortools/lp_data/lp_types.h" diff --git a/ortools/port/file_nonport.cc b/ortools/port/file_nonport.cc index 4ff7ffbdb0..e04a3aa3b8 100644 --- a/ortools/port/file_nonport.cc +++ b/ortools/port/file_nonport.cc @@ -38,12 +38,12 @@ bool PortableTemporaryFile(const char* directory_prefix, std::string* filename_out) { #if defined(__linux) int32 tid = static_cast(pthread_self()); -#else // defined(__linux__) +#else // defined(__linux__) int32 tid = 123; #endif // defined(__linux__) #if !defined(_MSC_VER) int32 pid = static_cast(getpid()); -#else // _MSC_VER +#else // _MSC_VER int32 pid = 456; #endif // _MSC_VER int64 now = absl::GetCurrentTimeNanos(); diff --git a/ortools/sat/BUILD b/ortools/sat/BUILD index efcdf24905..0d3ec3e485 100644 --- a/ortools/sat/BUILD +++ b/ortools/sat/BUILD @@ -166,8 +166,8 @@ cc_library( ":cp_model_expand", ":cp_model_lns", ":cp_model_loader", - ":cp_model_presolve", ":cp_model_postsolve", + ":cp_model_presolve", ":cp_model_search", ":cp_model_utils", ":drat_checker", @@ -204,7 +204,7 @@ cc_library( "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/memory", - "@com_google_absl//absl/status:status", + "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/synchronization", @@ -326,7 +326,6 @@ cc_library( ], ) - cc_library( name = "cp_model_postsolve", srcs = [ diff --git a/ortools/sat/clause.cc b/ortools/sat/clause.cc index cc59961df0..e13bc85455 100644 --- a/ortools/sat/clause.cc +++ b/ortools/sat/clause.cc @@ -1580,8 +1580,12 @@ std::vector BinaryImplicationGraph::ExpandAtMostOneWithWeight( std::vector clique(at_most_one.begin(), at_most_one.end()); std::vector intersection; double clique_weight = 0.0; + const int64 old_work = work_done_in_mark_descendants_; for (const Literal l : clique) clique_weight += expanded_lp_values[l.Index()]; for (int i = 0; i < clique.size(); ++i) { + // Do not spend too much time here. + if (work_done_in_mark_descendants_ - old_work > 1e8) break; + is_marked_.ClearAndResize(LiteralIndex(implications_.size())); MarkDescendants(clique[i]); if (i == 0) { diff --git a/ortools/sat/cp_model_search.cc b/ortools/sat/cp_model_search.cc index e9d20cd03f..d205470231 100644 --- a/ortools/sat/cp_model_search.cc +++ b/ortools/sat/cp_model_search.cc @@ -39,7 +39,7 @@ struct VarValue { IntegerValue value; }; -const std::function ConstructSearchStrategyInternal( +const std::function ConstructSearchStrategyInternal( const absl::flat_hash_map>& var_to_coeff_offset_pair, const std::vector& strategies, Model* model) { @@ -164,18 +164,18 @@ const std::function ConstructSearchStrategyInternal( LOG(FATAL) << "Unknown DomainReductionStrategy " << strategy.domain_strategy; } - return integer_encoder->GetOrCreateAssociatedLiteral(literal).Index(); + return BooleanOrIntegerLiteral(literal); } - return kNoLiteralIndex; + return BooleanOrIntegerLiteral(); }; } -std::function ConstructSearchStrategy( +std::function ConstructSearchStrategy( const CpModelProto& cp_model_proto, const std::vector& variable_mapping, IntegerVariable objective_var, Model* model) { // Default strategy is to instantiate the IntegerVariable in order. - std::function default_search_strategy = nullptr; + std::function default_search_strategy = nullptr; const bool instantiate_all_variables = model->GetOrCreate()->instantiate_all_variables(); @@ -228,10 +228,11 @@ std::function ConstructSearchStrategy( } } -std::function InstrumentSearchStrategy( +std::function InstrumentSearchStrategy( const CpModelProto& cp_model_proto, const std::vector& variable_mapping, - const std::function& instrumented_strategy, Model* model) { + const std::function& instrumented_strategy, + Model* model) { std::vector ref_to_display; for (int i = 0; i < cp_model_proto.variables_size(); ++i) { if (variable_mapping[i] == kNoIntegerVariable) continue; @@ -246,13 +247,18 @@ std::function InstrumentSearchStrategy( std::vector> old_domains(variable_mapping.size()); return [instrumented_strategy, model, variable_mapping, cp_model_proto, old_domains, ref_to_display]() mutable { - const LiteralIndex decision = instrumented_strategy(); - if (decision == kNoLiteralIndex) return decision; + const BooleanOrIntegerLiteral decision = instrumented_strategy(); + if (!decision.HasValue()) return decision; - for (const IntegerLiteral i_lit : - model->Get()->GetAllIntegerLiterals( - Literal(decision))) { - LOG(INFO) << "decision " << i_lit; + if (decision.boolean_literal_index != kNoLiteralIndex) { + const Literal l = Literal(decision.boolean_literal_index); + LOG(INFO) << "Boolean decision " << l; + for (const IntegerLiteral i_lit : + model->Get()->GetAllIntegerLiterals(l)) { + LOG(INFO) << " - associated with " << i_lit; + } + } else { + LOG(INFO) << "Integer decision " << decision.integer_literal; } const int level = model->Get()->CurrentDecisionLevel(); std::string to_display = diff --git a/ortools/sat/cp_model_search.h b/ortools/sat/cp_model_search.h index fbc598cc81..01bfe934df 100644 --- a/ortools/sat/cp_model_search.h +++ b/ortools/sat/cp_model_search.h @@ -30,7 +30,7 @@ namespace sat { // positive variable ref in the proto is mapped to variable_mapping[ref] in the // model. All the variables referred in the search strategy must be correctly // mapped, the other entries can be set to kNoIntegerVariable. -std::function ConstructSearchStrategy( +std::function ConstructSearchStrategy( const CpModelProto& cp_model_proto, const std::vector& variable_mapping, IntegerVariable objective_var, Model* model); @@ -39,10 +39,11 @@ std::function ConstructSearchStrategy( // domain before taking each decision. Note that we copy the instrumented // stategy so it doesn't have to outlive the returned functions like the other // arguments. -std::function InstrumentSearchStrategy( +std::function InstrumentSearchStrategy( const CpModelProto& cp_model_proto, const std::vector& variable_mapping, - const std::function& instrumented_strategy, Model* model); + const std::function& instrumented_strategy, + Model* model); // Returns up to "num_workers" different parameters. We do not always return // num_worker parameters to leave room for strategies like LNS that do not diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index d4913b0f96..e6cb2b3865 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -153,6 +153,7 @@ std::string CpModelStats(const CpModelProto& model_proto) { std::map num_constraints_by_name; std::map num_reif_constraints_by_name; std::map name_to_num_literals; + std::map name_to_num_terms; for (const ConstraintProto& ct : model_proto.constraints()) { std::string name = ConstraintCaseName(ct.constraint_case()); @@ -181,6 +182,11 @@ std::string CpModelStats(const CpModelProto& model_proto) { ConstraintProto::ConstraintCase::kAtMostOne) { name_to_num_literals[name] += ct.at_most_one().literals().size(); } + + if (ct.constraint_case() == ConstraintProto::ConstraintCase::kLinear && + ct.linear().vars_size() > 3) { + name_to_num_terms[name] += ct.linear().vars_size(); + } } int num_constants = 0; @@ -264,6 +270,10 @@ std::string CpModelStats(const CpModelProto& model_proto) { absl::StrAppend(&constraints.back(), " (#literals: ", name_to_num_literals[name], ")"); } + if (gtl::ContainsKey(name_to_num_terms, name)) { + absl::StrAppend(&constraints.back(), + " (#terms: ", name_to_num_terms[name], ")"); + } } std::sort(constraints.begin(), constraints.end()); absl::StrAppend(&result, absl::StrJoin(constraints, "\n")); @@ -279,9 +289,9 @@ std::string CpSolverResponseStats(const CpSolverResponse& response, ProtoEnumToString(response.status())); if (has_objective && response.status() != CpSolverStatus::INFEASIBLE) { - absl::StrAppendFormat(&result, "\nobjective: %.9g", + absl::StrAppendFormat(&result, "\nobjective: %.16g", response.objective_value()); - absl::StrAppendFormat(&result, "\nbest_bound: %.9g", + absl::StrAppendFormat(&result, "\nbest_bound: %.16g", response.best_objective_bound()); } else { absl::StrAppend(&result, "\nobjective: NA"); @@ -1230,7 +1240,7 @@ void LoadBaseModel(const CpModelProto& model_proto, VLOG(3) << num_ignored_constraints << " constraints were skipped."; } if (!unsupported_types.empty()) { - VLOG(1) << "There is unsuported constraints types in this model: "; + VLOG(1) << "There is unsupported constraints types in this model: "; for (const std::string& type : unsupported_types) { VLOG(1) << " - " << type; } @@ -1371,6 +1381,11 @@ void LoadCpModel(const CpModelProto& model_proto, objective_proto.coeffs(i) > 0 ? var : NegationOf(var)); } } + + // Register an objective special propagator. + model->TakeOwnership( + new LevelZeroEquality(objective_var, objective_definition->vars, + objective_definition->coeffs, model)); } // Intersect the objective domain with the given one if any. diff --git a/ortools/sat/csharp/CpModel.cs b/ortools/sat/csharp/CpModel.cs index 8232cb94ad..9c1db858f4 100644 --- a/ortools/sat/csharp/CpModel.cs +++ b/ortools/sat/csharp/CpModel.cs @@ -350,7 +350,6 @@ namespace Google.OrTools.Sat { res.Actives.Add(var.Index); } - res.MinLevel = min_level; res.MaxLevel = max_level; ct.Proto.Reservoir = res; diff --git a/ortools/sat/cuts.cc b/ortools/sat/cuts.cc index ac20ee40ef..0eeebb36c4 100644 --- a/ortools/sat/cuts.cc +++ b/ortools/sat/cuts.cc @@ -2133,7 +2133,6 @@ GenerateCumulativeCut(const std::string& cut_name, bool has_quadratic_cuts = false; LinearConstraintBuilder cut(model, kMinIntegerValue, IntegerValue(0)); - IntegerValue fixed_contribution(0); // Build the cut. cut.AddTerm(capacity, -span_of_max_violation); @@ -2142,7 +2141,7 @@ GenerateCumulativeCut(const std::string& cut_name, if (helper->IsPresent(t)) { if (demand_is_fixed(t)) { if (helper->SizeIsFixed(t)) { - fixed_contribution += helper->SizeMin(t) * demand_min(t); + cut.AddConstant(helper->SizeMin(t) * demand_min(t)); } else { cut.AddTerm(helper->SizeVars()[t], demand_min(t)); } @@ -2162,7 +2161,7 @@ GenerateCumulativeCut(const std::string& cut_name, cut.AddTerm(helper->SizeVars()[t], demand_min(t)); cut.AddTerm(demands[t], helper->SizeMin(t)); // Substract the energy counted twice. - fixed_contribution -= helper->SizeMin(t) * demand_min(t); + cut.AddConstant(-helper->SizeMin(t) * demand_min(t)); has_quadratic_cuts = true; } } else { @@ -2177,7 +2176,6 @@ GenerateCumulativeCut(const std::string& cut_name, } } } - cut.AddConstant(fixed_contribution); if (cut_generated) { std::string full_name = cut_name; diff --git a/ortools/sat/encoding.h b/ortools/sat/encoding.h index 23ee1c678f..ca8d1769d7 100644 --- a/ortools/sat/encoding.h +++ b/ortools/sat/encoding.h @@ -126,6 +126,8 @@ class EncodingNode { std::vector literals_; }; +// Note that we use <= because on 32 bits architecture, the size will actually +// be smaller than 64 bytes. #if defined(_M_X64) && defined(_DEBUG) // In debug std::Vector is 32 static_assert(sizeof(EncodingNode) == 72, diff --git a/ortools/sat/integer.h b/ortools/sat/integer.h index 012e94614e..e8df2485ce 100644 --- a/ortools/sat/integer.h +++ b/ortools/sat/integer.h @@ -167,6 +167,8 @@ struct IntegerLiteral { DCHECK_LE(bound, kMaxIntegerValue + 1); } + bool IsValid() const { return var != kNoIntegerVariable; } + // The negation of x >= bound is x <= bound - 1. IntegerLiteral Negated() const; @@ -239,13 +241,6 @@ struct DebugSolution : public gtl::ITIVector { explicit DebugSolution(Model* model) {} }; -// Some heuristics may be generated automatically, for instance by constraints. -// Those will be stored in a SearchHeuristicsVector object owned by the model. -// -// TODO(user): Move this and other similar classes in a "model_singleton" file? -class SearchHeuristicsVector - : public std::vector> {}; - // Each integer variable x will be associated with a set of literals encoding // (x >= v) for some values of v. This class maintains the relationship between // the integer variables and such literals which can be created by a call to diff --git a/ortools/sat/integer_expr.cc b/ortools/sat/integer_expr.cc index 57cdeb2227..917a055d63 100644 --- a/ortools/sat/integer_expr.cc +++ b/ortools/sat/integer_expr.cc @@ -204,6 +204,75 @@ void IntegerSumLE::RegisterWith(GenericLiteralWatcher* watcher) { watcher->RegisterReversibleInt(id, &rev_num_fixed_vars_); } +LevelZeroEquality::LevelZeroEquality(IntegerVariable target, + const std::vector& vars, + const std::vector& coeffs, + Model* model) + : target_(target), + vars_(vars), + coeffs_(coeffs), + trail_(model->GetOrCreate()), + integer_trail_(model->GetOrCreate()) { + auto* watcher = model->GetOrCreate(); + const int id = watcher->Register(this); + watcher->SetPropagatorPriority(id, 2); + watcher->WatchIntegerVariable(target, id); + for (const IntegerVariable& var : vars_) { + watcher->WatchIntegerVariable(var, id); + } +} + +// TODO(user): We could go even further than just the GCD, and do more +// arithmetic to tighten the target bounds. See for instance a problem like +// ej.mps.gz that we don't solve easily, but has just 3 variables! the goal is +// to minimize X, given 31013 X - 41014 Y - 51015 Z = -31013 (all >=0, Y and Z +// bounded with high values). I know some MIP solvers have a basic linear +// diophantine equation support. +bool LevelZeroEquality::Propagate() { + // TODO(user): Once the GCD is not 1, we could at any level make sure the + // objective is of the correct form. For now, this only happen in a few + // miplib problem that we close quickly, so I didn't add the extra code yet. + if (trail_->CurrentDecisionLevel() != 0) return true; + + int64 gcd = 0; + IntegerValue sum(0); + for (int i = 0; i < vars_.size(); ++i) { + if (integer_trail_->IsFixed(vars_[i])) { + sum += coeffs_[i] * integer_trail_->LowerBound(vars_[i]); + continue; + } + gcd = MathUtil::GCD64(gcd, std::abs(coeffs_[i].value())); + if (gcd == 1) break; + } + if (gcd == 0) return true; // All fixed. + + if (gcd > gcd_) { + VLOG(1) << "Objective gcd: " << gcd; + } + CHECK_GE(gcd, gcd_); + gcd_ = IntegerValue(gcd); + + const IntegerValue lb = integer_trail_->LowerBound(target_); + const IntegerValue lb_remainder = PositiveRemainder(lb - sum, gcd_); + if (lb_remainder != 0) { + if (!integer_trail_->Enqueue( + IntegerLiteral::GreaterOrEqual(target_, lb + gcd_ - lb_remainder), + {}, {})) + return false; + } + + const IntegerValue ub = integer_trail_->UpperBound(target_); + const IntegerValue ub_remainder = + PositiveRemainder(ub - sum, IntegerValue(gcd)); + if (ub_remainder != 0) { + if (!integer_trail_->Enqueue( + IntegerLiteral::LowerOrEqual(target_, ub - ub_remainder), {}, {})) + return false; + } + + return true; +} + MinPropagator::MinPropagator(const std::vector& vars, IntegerVariable min_var, IntegerTrail* integer_trail) diff --git a/ortools/sat/integer_expr.h b/ortools/sat/integer_expr.h index 0990f497a1..ea98db0503 100644 --- a/ortools/sat/integer_expr.h +++ b/ortools/sat/integer_expr.h @@ -21,6 +21,7 @@ #include "ortools/base/integral_types.h" #include "ortools/base/logging.h" #include "ortools/base/macros.h" +#include "ortools/base/mathutil.h" #include "ortools/sat/integer.h" #include "ortools/sat/linear_constraint.h" #include "ortools/sat/model.h" @@ -102,6 +103,33 @@ class IntegerSumLE : public PropagatorInterface { DISALLOW_COPY_AND_ASSIGN(IntegerSumLE); }; +// This assumes target = SUM_i coeffs[i] * vars[i], and detects that the target +// must be of the form (a*X + b). +// +// This propagator is quite specific and runs only at level zero. For now, this +// is mainly used for the objective variable. As we fix terms with high +// objective coefficient, it is possible the only terms left have a common +// divisor. This close app2-2.mps in less than a second instead of running +// forever to prove the optimal (in single thread). +class LevelZeroEquality : PropagatorInterface { + public: + LevelZeroEquality(IntegerVariable target, + const std::vector& vars, + const std::vector& coeffs, Model* model); + + bool Propagate() final; + + private: + const IntegerVariable target_; + const std::vector vars_; + const std::vector coeffs_; + + IntegerValue gcd_ = IntegerValue(1); + + Trail* trail_; + IntegerTrail* integer_trail_; +}; + // A min (resp max) contraint of the form min == MIN(vars) can be decomposed // into two inequalities: // 1/ min <= MIN(vars), which is the same as for all v in vars, "min <= v". diff --git a/ortools/sat/integer_search.cc b/ortools/sat/integer_search.cc index a33907cf8b..dc9997be90 100644 --- a/ortools/sat/integer_search.cc +++ b/ortools/sat/integer_search.cc @@ -34,54 +34,42 @@ namespace operations_research { namespace sat { -LiteralIndex BranchDown(IntegerVariable var, IntegerValue value, Model* model) { - auto* encoder = model->GetOrCreate(); - auto* trail = model->GetOrCreate(); - const Literal le = encoder->GetOrCreateAssociatedLiteral( - IntegerLiteral::LowerOrEqual(var, value)); - DCHECK(!trail->Assignment().VariableIsAssigned(le.Variable())); - return le.Index(); -} - -LiteralIndex BranchUp(IntegerVariable var, IntegerValue value, Model* model) { - auto* encoder = model->GetOrCreate(); - auto* trail = model->GetOrCreate(); - const Literal ge = encoder->GetOrCreateAssociatedLiteral( - IntegerLiteral::GreaterOrEqual(var, value)); - DCHECK(!trail->Assignment().VariableIsAssigned(ge.Variable())); - return ge.Index(); -} - -LiteralIndex AtMinValue(IntegerVariable var, IntegerTrail* integer_trail, - IntegerEncoder* integer_encoder) { +IntegerLiteral AtMinValue(IntegerVariable var, IntegerTrail* integer_trail) { DCHECK(!integer_trail->IsCurrentlyIgnored(var)); const IntegerValue lb = integer_trail->LowerBound(var); DCHECK_LE(lb, integer_trail->UpperBound(var)); - if (lb == integer_trail->UpperBound(var)) return kNoLiteralIndex; - - const Literal result = integer_encoder->GetOrCreateAssociatedLiteral( - IntegerLiteral::LowerOrEqual(var, lb)); - return result.Index(); + if (lb == integer_trail->UpperBound(var)) return IntegerLiteral(); + return IntegerLiteral::LowerOrEqual(var, lb); } -LiteralIndex GreaterOrEqualToMiddleValue(IntegerVariable var, Model* model) { +IntegerLiteral ChooseBestObjectiveValue(IntegerVariable var, Model* model) { + const auto& variables = + model->GetOrCreate()->objective_impacting_variables; auto* integer_trail = model->GetOrCreate(); + if (variables.contains(var)) { + return AtMinValue(var, integer_trail); + } else if (variables.contains(NegationOf(var))) { + return AtMinValue(NegationOf(var), integer_trail); + } + return IntegerLiteral(); +} + +IntegerLiteral GreaterOrEqualToMiddleValue(IntegerVariable var, + IntegerTrail* integer_trail) { const IntegerValue var_lb = integer_trail->LowerBound(var); const IntegerValue var_ub = integer_trail->UpperBound(var); CHECK_LT(var_lb, var_ub); const IntegerValue chosen_value = var_lb + std::max(IntegerValue(1), (var_ub - var_lb) / IntegerValue(2)); - return BranchUp(var, chosen_value, model); + return IntegerLiteral::GreaterOrEqual(var, chosen_value); } -LiteralIndex SplitAroundGivenValue(IntegerVariable positive_var, - IntegerValue value, Model* model) { - DCHECK(VariableIsPositive(positive_var)); +IntegerLiteral SplitAroundGivenValue(IntegerVariable var, IntegerValue value, + Model* model) { auto* integer_trail = model->GetOrCreate(); - - const IntegerValue lb = integer_trail->LowerBound(positive_var); - const IntegerValue ub = integer_trail->UpperBound(positive_var); + const IntegerValue lb = integer_trail->LowerBound(var); + const IntegerValue ub = integer_trail->UpperBound(var); const absl::flat_hash_set& variables = model->GetOrCreate()->objective_impacting_variables; @@ -93,20 +81,19 @@ LiteralIndex SplitAroundGivenValue(IntegerVariable positive_var, // kNoLiteralIndex. const bool branch_down_feasible = value >= lb && value < ub; const bool branch_up_feasible = value > lb && value <= ub; - if (variables.contains(positive_var) && branch_down_feasible) { - return BranchDown(positive_var, value, model); - } else if (variables.contains(NegationOf(positive_var)) && - branch_up_feasible) { - return BranchUp(positive_var, value, model); + if (variables.contains(var) && branch_down_feasible) { + return IntegerLiteral::LowerOrEqual(var, value); + } else if (variables.contains(NegationOf(var)) && branch_up_feasible) { + return IntegerLiteral::GreaterOrEqual(var, value); } else if (branch_down_feasible) { - return BranchDown(positive_var, value, model); + return IntegerLiteral::LowerOrEqual(var, value); } else if (branch_up_feasible) { - return BranchUp(positive_var, value, model); + return IntegerLiteral::GreaterOrEqual(var, value); } - return kNoLiteralIndex; + return IntegerLiteral(); } -LiteralIndex SplitAroundLpValue(IntegerVariable var, Model* model) { +IntegerLiteral SplitAroundLpValue(IntegerVariable var, Model* model) { auto* parameters = model->GetOrCreate(); auto* integer_trail = model->GetOrCreate(); auto* lp_dispatcher = model->GetOrCreate(); @@ -118,9 +105,9 @@ LiteralIndex SplitAroundLpValue(IntegerVariable var, Model* model) { // We only use this if the sub-lp has a solution, and depending on the value // of exploit_all_lp_solution() if it is a pure-integer solution. - if (lp == nullptr || !lp->HasSolution()) return kNoLiteralIndex; + if (lp == nullptr || !lp->HasSolution()) return IntegerLiteral(); if (!parameters->exploit_all_lp_solution() && !lp->SolutionIsInteger()) { - return kNoLiteralIndex; + return IntegerLiteral(); } const IntegerValue value = IntegerValue( @@ -128,15 +115,15 @@ LiteralIndex SplitAroundLpValue(IntegerVariable var, Model* model) { // Because our lp solution might be from higher up in the tree, it // is possible that value is now outside the domain of positive_var. - // In this case, this function will return kNoLiteralIndex. + // In this case, this function will return an invalid literal. return SplitAroundGivenValue(positive_var, value, model); } -LiteralIndex SplitUsingBestSolutionValueInRepository( +IntegerLiteral SplitUsingBestSolutionValueInRepository( IntegerVariable var, const SharedSolutionRepository& solution_repo, Model* model) { if (solution_repo.NumSolutions() == 0) { - return kNoLiteralIndex; + return IntegerLiteral(); } const IntegerVariable positive_var = PositiveVariable(var); @@ -145,7 +132,7 @@ LiteralIndex SplitUsingBestSolutionValueInRepository( positive_var); if (proto_var < 0) { - return kNoLiteralIndex; + return IntegerLiteral(); } VLOG(2) << "Using solution value for branching."; @@ -158,27 +145,25 @@ LiteralIndex SplitUsingBestSolutionValueInRepository( // the one below is ok when search_branching is set to SAT_SEARCH because it is // not executed often, but otherwise it is done for each search decision, // which seems expensive. Improve. -std::function FirstUnassignedVarAtItsMinHeuristic( +std::function FirstUnassignedVarAtItsMinHeuristic( const std::vector& vars, Model* model) { auto* integer_trail = model->GetOrCreate(); - auto* integer_encoder = model->GetOrCreate(); - return [/*copy*/ vars, integer_trail, integer_encoder]() { + return [/*copy*/ vars, integer_trail]() { for (const IntegerVariable var : vars) { // Note that there is no point trying to fix a currently ignored variable. if (integer_trail->IsCurrentlyIgnored(var)) continue; - const LiteralIndex decision = - AtMinValue(var, integer_trail, integer_encoder); - if (decision != kNoLiteralIndex) return decision; + const IntegerLiteral decision = AtMinValue(var, integer_trail); + if (decision.IsValid()) return BooleanOrIntegerLiteral(decision); } - return kNoLiteralIndex; + return BooleanOrIntegerLiteral(); }; } -std::function UnassignedVarWithLowestMinAtItsMinHeuristic( +std::function +UnassignedVarWithLowestMinAtItsMinHeuristic( const std::vector& vars, Model* model) { auto* integer_trail = model->GetOrCreate(); - auto* integer_encoder = model->GetOrCreate(); - return [/*copy */ vars, integer_trail, integer_encoder]() { + return [/*copy */ vars, integer_trail]() { IntegerVariable candidate = kNoIntegerVariable; IntegerValue candidate_lb; for (const IntegerVariable var : vars) { @@ -190,44 +175,54 @@ std::function UnassignedVarWithLowestMinAtItsMinHeuristic( candidate_lb = lb; } } - if (candidate == kNoIntegerVariable) return kNoLiteralIndex; - return AtMinValue(candidate, integer_trail, integer_encoder); + if (candidate == kNoIntegerVariable) return BooleanOrIntegerLiteral(); + return BooleanOrIntegerLiteral(AtMinValue(candidate, integer_trail)); }; } -std::function SequentialSearch( - std::vector> heuristics) { +std::function SequentialSearch( + std::vector> heuristics) { return [heuristics]() { for (const auto& h : heuristics) { - const LiteralIndex li = h(); - if (li != kNoLiteralIndex) return li; + const BooleanOrIntegerLiteral decision = h(); + if (decision.HasValue()) return decision; } - return kNoLiteralIndex; + return BooleanOrIntegerLiteral(); }; } -std::function SequentialValueSelection( - std::vector> +std::function SequentialValueSelection( + std::vector> value_selection_heuristics, - std::function var_selection_heuristic, Model* model) { + std::function var_selection_heuristic, + Model* model) { auto* encoder = model->GetOrCreate(); auto* integer_trail = model->GetOrCreate(); return [=]() { // Get the current decision. - const LiteralIndex current_decision = var_selection_heuristic(); - if (current_decision == kNoLiteralIndex) return kNoLiteralIndex; + const BooleanOrIntegerLiteral current_decision = var_selection_heuristic(); + if (!current_decision.HasValue()) return current_decision; - // Decode the decision and get the variable. - for (const IntegerLiteral l : - encoder->GetAllIntegerLiterals(Literal(current_decision))) { + // IntegerLiteral case. + if (current_decision.boolean_literal_index == kNoLiteralIndex) { + for (const auto& value_heuristic : value_selection_heuristics) { + const IntegerLiteral decision = + value_heuristic(current_decision.integer_literal.var); + if (decision.IsValid()) return BooleanOrIntegerLiteral(decision); + } + return current_decision; + } + + // Boolean case. We try to decode the Boolean decision to see if it is + // associated with an integer variable. + for (const IntegerLiteral l : encoder->GetAllIntegerLiterals( + Literal(current_decision.boolean_literal_index))) { if (integer_trail->IsCurrentlyIgnored(l.var)) continue; // Sequentially try the value selection heuristics. for (const auto& value_heuristic : value_selection_heuristics) { - const LiteralIndex decision = value_heuristic(l.var); - if (decision != kNoLiteralIndex) { - return decision; - } + const IntegerLiteral decision = value_heuristic(l.var); + if (decision.IsValid()) return BooleanOrIntegerLiteral(decision); } } @@ -236,25 +231,24 @@ std::function SequentialValueSelection( }; } -// If a variable appear in the objective, branch on its best objective value. -LiteralIndex ChooseBestObjectiveValue(IntegerVariable var, Model* model) { - const auto& variables = - model->GetOrCreate()->objective_impacting_variables; - auto* encoder = model->GetOrCreate(); - auto* integer_trail = model->GetOrCreate(); - if (variables.contains(var)) { - return AtMinValue(var, integer_trail, encoder); - } else if (variables.contains(NegationOf(var))) { - return AtMinValue(NegationOf(var), integer_trail, encoder); +bool LinearizedPartIsLarge(Model* model) { + auto* lp_constraints = + model->GetOrCreate(); + int num_lp_variables = 0; + for (LinearProgrammingConstraint* lp : *lp_constraints) { + num_lp_variables += lp->NumVariables(); } - return kNoLiteralIndex; + const int num_integer_variables = + model->GetOrCreate()->NumIntegerVariables().value() / 2; + return (num_integer_variables <= 2 * num_lp_variables); } // TODO(user): Experiment more with value selection heuristics. -std::function IntegerValueSelectionHeuristic( - std::function var_selection_heuristic, Model* model) { +std::function IntegerValueSelectionHeuristic( + std::function var_selection_heuristic, + Model* model) { const SatParameters& parameters = *(model->GetOrCreate()); - std::vector> + std::vector> value_selection_heuristics; // LP based value. @@ -309,44 +303,46 @@ std::function IntegerValueSelectionHeuristic( var_selection_heuristic, model); } -std::function SatSolverHeuristic(Model* model) { +std::function SatSolverHeuristic(Model* model) { SatSolver* sat_solver = model->GetOrCreate(); Trail* trail = model->GetOrCreate(); SatDecisionPolicy* decision_policy = model->GetOrCreate(); return [sat_solver, trail, decision_policy] { const bool all_assigned = trail->Index() == sat_solver->NumVariables(); - if (all_assigned) return kNoLiteralIndex; + if (all_assigned) return BooleanOrIntegerLiteral(); const Literal result = decision_policy->NextBranch(); CHECK(!sat_solver->Assignment().LiteralIsAssigned(result)); - return result.Index(); + return BooleanOrIntegerLiteral(result.Index()); }; } -std::function PseudoCost(Model* model) { +std::function PseudoCost(Model* model) { auto* objective = model->Get(); const bool has_objective = objective != nullptr && objective->objective_var != kNoIntegerVariable; if (!has_objective) { - return []() { return kNoLiteralIndex; }; + return []() { return BooleanOrIntegerLiteral(); }; } - PseudoCosts* pseudo_costs = model->GetOrCreate(); - return [pseudo_costs, model]() { + auto* pseudo_costs = model->GetOrCreate(); + auto* integer_trail = model->GetOrCreate(); + return [pseudo_costs, integer_trail]() { const IntegerVariable chosen_var = pseudo_costs->GetBestDecisionVar(); - - if (chosen_var == kNoIntegerVariable) return kNoLiteralIndex; - - return GreaterOrEqualToMiddleValue(chosen_var, model); + if (chosen_var == kNoIntegerVariable) return BooleanOrIntegerLiteral(); + return BooleanOrIntegerLiteral( + GreaterOrEqualToMiddleValue(chosen_var, integer_trail)); }; } -std::function RandomizeOnRestartHeuristic(Model* model) { +std::function RandomizeOnRestartHeuristic( + Model* model) { SatSolver* sat_solver = model->GetOrCreate(); SatDecisionPolicy* decision_policy = model->GetOrCreate(); // TODO(user): Add other policy and perform more experiments. - std::function sat_policy = SatSolverHeuristic(model); - std::vector> policies{ + std::function sat_policy = + SatSolverHeuristic(model); + std::vector> policies{ sat_policy, SequentialSearch({PseudoCost(model), sat_policy})}; // The higher weight for the sat policy is because this policy actually // contains a lot of variation as we randomize the sat parameters. @@ -354,7 +350,7 @@ std::function RandomizeOnRestartHeuristic(Model* model) { std::discrete_distribution var_dist{3 /*sat_policy*/, 1 /*Pseudo cost*/}; // Value selection. - std::vector> + std::vector> value_selection_heuristics; std::vector value_selection_weight; @@ -387,18 +383,16 @@ std::function RandomizeOnRestartHeuristic(Model* model) { } // Middle value. - value_selection_heuristics.push_back([model](IntegerVariable var) { - return GreaterOrEqualToMiddleValue(var, model); + auto* integer_trail = model->GetOrCreate(); + value_selection_heuristics.push_back([integer_trail](IntegerVariable var) { + return GreaterOrEqualToMiddleValue(var, integer_trail); }); value_selection_weight.push_back(1); // Min value. - auto* integer_trail = model->GetOrCreate(); - auto* integer_encoder = model->GetOrCreate(); - value_selection_heuristics.push_back( - [integer_trail, integer_encoder](IntegerVariable var) { - return AtMinValue(var, integer_trail, integer_encoder); - }); + value_selection_heuristics.push_back([integer_trail](IntegerVariable var) { + return AtMinValue(var, integer_trail); + }); value_selection_weight.push_back(1); // Special case: Don't change the decision value. @@ -411,6 +405,7 @@ std::function RandomizeOnRestartHeuristic(Model* model) { int policy_index = 0; int val_policy_index = 0; + auto* encoder = model->GetOrCreate(); return [=]() mutable { if (sat_solver->CurrentDecisionLevel() == 0) { auto* random = model->GetOrCreate(); @@ -425,25 +420,31 @@ std::function RandomizeOnRestartHeuristic(Model* model) { } // Get the current decision. - const LiteralIndex current_decision = policies[policy_index](); - if (current_decision == kNoLiteralIndex) return kNoLiteralIndex; + const BooleanOrIntegerLiteral current_decision = policies[policy_index](); + if (!current_decision.HasValue()) return current_decision; // Special case: Don't override the decision value. if (val_policy_index >= value_selection_heuristics.size()) { return current_decision; } + if (current_decision.boolean_literal_index == kNoLiteralIndex) { + const IntegerLiteral new_decision = + value_selection_heuristics[val_policy_index]( + current_decision.integer_literal.var); + if (new_decision.IsValid()) return BooleanOrIntegerLiteral(new_decision); + return current_decision; + } + // Decode the decision and get the variable. - for (const IntegerLiteral l : - integer_encoder->GetAllIntegerLiterals(Literal(current_decision))) { + for (const IntegerLiteral l : encoder->GetAllIntegerLiterals( + Literal(current_decision.boolean_literal_index))) { if (integer_trail->IsCurrentlyIgnored(l.var)) continue; // Try the selected policy. - const LiteralIndex new_decision = + const IntegerLiteral new_decision = value_selection_heuristics[val_policy_index](l.var); - if (new_decision != kNoLiteralIndex) { - return new_decision; - } + if (new_decision.IsValid()) return BooleanOrIntegerLiteral(new_decision); } // Selected policy failed. Revert back to original decision. @@ -452,7 +453,7 @@ std::function RandomizeOnRestartHeuristic(Model* model) { } // TODO(user): Avoid the quadratic algorithm!! -std::function FollowHint( +std::function FollowHint( const std::vector& vars, const std::vector& values, Model* model) { const Trail* trail = model->GetOrCreate(); @@ -462,55 +463,26 @@ std::function FollowHint( const IntegerValue value = values[i]; if (vars[i].bool_var != kNoBooleanVariable) { if (trail->Assignment().VariableIsAssigned(vars[i].bool_var)) continue; - return Literal(vars[i].bool_var, value == 1).Index(); + return BooleanOrIntegerLiteral( + Literal(vars[i].bool_var, value == 1).Index()); } else { const IntegerVariable integer_var = vars[i].int_var; if (integer_trail->IsCurrentlyIgnored(integer_var)) continue; if (integer_trail->IsFixed(integer_var)) continue; const IntegerVariable positive_var = PositiveVariable(integer_var); - const LiteralIndex decision = SplitAroundGivenValue( + const IntegerLiteral decision = SplitAroundGivenValue( positive_var, positive_var != integer_var ? -value : value, model); - if (decision != kNoLiteralIndex) return decision; + if (decision.IsValid()) return BooleanOrIntegerLiteral(decision); // If the value is outside the current possible domain, we skip it. continue; } } - return kNoLiteralIndex; + return BooleanOrIntegerLiteral(); }; } -bool LpSolutionIsExploitable(Model* model) { - auto* lp_constraints = - model->GetOrCreate(); - const SatParameters& parameters = *(model->GetOrCreate()); - - // TODO(user,user): When we have more than one LP, their set of variable - // is always disjoint. So we could still change the polarity if the next - // variable we branch on is part of a LP that has a solution. - for (LinearProgrammingConstraint* lp : *lp_constraints) { - if (!lp->HasSolution() || - !(parameters.exploit_all_lp_solution() || lp->SolutionIsInteger())) { - return false; - } - } - return true; -} - -bool LinearizedPartIsLarge(Model* model) { - auto* lp_constraints = - model->GetOrCreate(); - - int num_lp_variables = 0; - for (LinearProgrammingConstraint* lp : *lp_constraints) { - num_lp_variables += lp->NumVariables(); - } - const int num_integer_variables = - model->GetOrCreate()->NumIntegerVariables().value() / 2; - return (num_integer_variables <= 2 * num_lp_variables); -} - std::function RestartEveryKFailures(int k, SatSolver* solver) { bool reset_at_next_call = true; int next_num_failures = 0; @@ -530,6 +502,15 @@ std::function SatSolverRestartPolicy(Model* model) { return [policy]() { return policy->ShouldRestart(); }; } +namespace { + +std::function WrapIntegerLiteralHeuristic( + std::function f) { + return [f]() { return BooleanOrIntegerLiteral(f()); }; +} + +} // namespace + void ConfigureSearchHeuristics(Model* model) { SearchHeuristics& heuristics = *model->GetOrCreate(); CHECK(heuristics.fixed_search != nullptr); @@ -540,7 +521,7 @@ void ConfigureSearchHeuristics(Model* model) { const SatParameters& parameters = *(model->GetOrCreate()); switch (parameters.search_branching()) { case SatParameters::AUTOMATIC_SEARCH: { - std::function decision_policy; + std::function decision_policy; if (parameters.randomize_search()) { decision_policy = RandomizeOnRestartHeuristic(model); } else { @@ -580,10 +561,21 @@ void ConfigureSearchHeuristics(Model* model) { return; } case SatParameters::PORTFOLIO_SEARCH: { + // TODO(user): This is not used in any of our default config. remove? + // It make also no sense to choose a value in the LP heuristic and then + // override it with IntegerValueSelectionHeuristic(), clean that up. + std::vector> base_heuristics; + base_heuristics.push_back(heuristics.fixed_search); + for (const auto& ct : + *(model->GetOrCreate())) { + base_heuristics.push_back(WrapIntegerLiteralHeuristic( + ct->HeuristicLpReducedCostBinary(model))); + base_heuristics.push_back(WrapIntegerLiteralHeuristic( + ct->HeuristicLpMostInfeasibleBinary(model))); + } heuristics.decision_policies = CompleteHeuristics( - AddModelHeuristics({heuristics.fixed_search}, model), - SequentialSearch( - {SatSolverHeuristic(model), heuristics.fixed_search})); + base_heuristics, SequentialSearch({SatSolverHeuristic(model), + heuristics.fixed_search})); for (auto& ref : heuristics.decision_policies) { ref = IntegerValueSelectionHeuristic(ref, model); } @@ -592,10 +584,11 @@ void ConfigureSearchHeuristics(Model* model) { return; } case SatParameters::LP_SEARCH: { - std::vector> lp_heuristics; + std::vector> lp_heuristics; for (const auto& ct : *(model->GetOrCreate())) { - lp_heuristics.push_back(ct->LPReducedCostAverageBranching()); + lp_heuristics.push_back(WrapIntegerLiteralHeuristic( + ct->HeuristicLpReducedCostAverageBranching())); } if (lp_heuristics.empty()) { // Revert to fixed search. heuristics.decision_policies = {SequentialSearch( @@ -611,7 +604,7 @@ void ConfigureSearchHeuristics(Model* model) { return; } case SatParameters::PSEUDO_COST_SEARCH: { - std::function search = + std::function search = SequentialSearch({PseudoCost(model), SatSolverHeuristic(model), heuristics.fixed_search}); heuristics.decision_policies = { @@ -620,7 +613,7 @@ void ConfigureSearchHeuristics(Model* model) { return; } case SatParameters::PORTFOLIO_WITH_QUICK_RESTART_SEARCH: { - std::function search = SequentialSearch( + std::function search = SequentialSearch( {RandomizeOnRestartHeuristic(model), heuristics.fixed_search}); heuristics.decision_policies = {search}; heuristics.restart_policies = { @@ -630,20 +623,11 @@ void ConfigureSearchHeuristics(Model* model) { } } -std::vector> AddModelHeuristics( - const std::vector>& input_heuristics, - Model* model) { - std::vector> heuristics = input_heuristics; - auto* extra_heuristics = model->GetOrCreate(); - heuristics.insert(heuristics.end(), extra_heuristics->begin(), - extra_heuristics->end()); - return heuristics; -} - -std::vector> CompleteHeuristics( - const std::vector>& incomplete_heuristics, - const std::function& completion_heuristic) { - std::vector> complete_heuristics; +std::vector> CompleteHeuristics( + const std::vector>& + incomplete_heuristics, + const std::function& completion_heuristic) { + std::vector> complete_heuristics; complete_heuristics.reserve(incomplete_heuristics.size()); for (const auto& incomplete : incomplete_heuristics) { complete_heuristics.push_back( @@ -687,6 +671,7 @@ SatSolver::Status SolveIntegerProblem(Model* model) { PseudoCosts* pseudo_costs = model->GetOrCreate(); auto* integer_trail = model->GetOrCreate(); + auto* encoder = model->GetOrCreate(); auto* implied_bounds = model->GetOrCreate(); const SatParameters& sat_parameters = *(model->GetOrCreate()); @@ -727,25 +712,39 @@ SatSolver::Status SolveIntegerProblem(Model* model) { LiteralIndex decision = kNoLiteralIndex; while (true) { + BooleanOrIntegerLiteral new_decision; if (integer_trail->InPropagationLoop()) { const IntegerVariable var = integer_trail->NextVariableToBranchOnInPropagationLoop(); if (var != kNoIntegerVariable) { - decision = GreaterOrEqualToMiddleValue(var, model); + new_decision.integer_literal = + GreaterOrEqualToMiddleValue(var, integer_trail); } } - if (decision == kNoLiteralIndex) { - decision = heuristics.decision_policies[heuristics.policy_index](); + if (!new_decision.HasValue()) { + new_decision = heuristics.decision_policies[heuristics.policy_index](); } - if (decision == kNoLiteralIndex && + if (!new_decision.HasValue() && integer_trail->CurrentBranchHadAnIncompletePropagation()) { const IntegerVariable var = integer_trail->FirstUnassignedVariable(); if (var != kNoIntegerVariable) { - decision = AtMinValue(var, integer_trail, - model->GetOrCreate()); + new_decision.integer_literal = AtMinValue(var, integer_trail); } } - if (decision == kNoLiteralIndex) break; + if (!new_decision.HasValue()) break; + + // Convert integer decision to literal one if needed. + // + // TODO(user): Ideally it would be cool to delay the creation even more + // until we have a conflict with these decisions, but it is currrently + // hard to do so. + if (new_decision.boolean_literal_index != kNoLiteralIndex) { + decision = new_decision.boolean_literal_index; + } else { + decision = + encoder->GetOrCreateAssociatedLiteral(new_decision.integer_literal) + .Index(); + } if (sat_solver->Assignment().LiteralIsAssigned(Literal(decision))) { // TODO(user): It would be nicer if this can never happen. For now, it @@ -781,17 +780,6 @@ SatSolver::Status SolveIntegerProblem(Model* model) { break; } - // Record the changelist and objective bounds for updating pseudo costs. - const std::vector bound_changes = - GetBoundChanges(decision, model); - IntegerValue current_obj_lb = kMinIntegerValue; - IntegerValue current_obj_ub = kMaxIntegerValue; - if (objective_var != kNoIntegerVariable) { - current_obj_lb = integer_trail->LowerBound(objective_var); - current_obj_ub = integer_trail->UpperBound(objective_var); - } - const int old_level = sat_solver->CurrentDecisionLevel(); - // No decision means that we reached a leave of the search tree and that // we have a feasible solution. if (decision == kNoLiteralIndex) { @@ -813,6 +801,17 @@ SatSolver::Status SolveIntegerProblem(Model* model) { return SatSolver::FEASIBLE; } + // Record the changelist and objective bounds for updating pseudo costs. + const std::vector bound_changes = + GetBoundChanges(decision, model); + IntegerValue old_obj_lb = kMinIntegerValue; + IntegerValue old_obj_ub = kMaxIntegerValue; + if (objective_var != kNoIntegerVariable) { + old_obj_lb = integer_trail->LowerBound(objective_var); + old_obj_ub = integer_trail->UpperBound(objective_var); + } + const int old_level = sat_solver->CurrentDecisionLevel(); + // TODO(user): on some problems, this function can be quite long. Expand // so that we can check the time limit at each step? sat_solver->EnqueueDecisionAndBackjumpOnConflict(Literal(decision)); @@ -829,7 +828,7 @@ SatSolver::Status SolveIntegerProblem(Model* model) { const IntegerValue new_obj_lb = integer_trail->LowerBound(objective_var); const IntegerValue new_obj_ub = integer_trail->UpperBound(objective_var); const IntegerValue objective_bound_change = - (new_obj_lb - current_obj_lb) + (current_obj_ub - new_obj_ub); + (new_obj_lb - old_obj_lb) + (old_obj_ub - new_obj_ub); pseudo_costs->UpdateCost(bound_changes, objective_bound_change); } @@ -837,11 +836,17 @@ SatSolver::Status SolveIntegerProblem(Model* model) { if (!sat_solver->ReapplyAssumptionsIfNeeded()) { return sat_solver->UnsatStatus(); } + + // TODO(user): Experiment more around dynamically changing the + // threshold for storing LP solutions in the pool. Alternatively expose + // this as parameter so this can be tuned later. + // + // TODO(user): Avoid adding the same solution many time if the LP didn't + // change. Avoid adding solution that are too deep in the tree (most + // variable fixed). Also use a callback rather than having this here, we + // don't want this file to depend on cp_model.proto. if (model->Get() != nullptr) { num_decisions_since_last_lp_record_++; - // TODO(user): Experiment more around dynamically changing the - // threshold for storing LP solutions in the pool. Alternatively expose - // this as parameter so this can be tuned later. if (num_decisions_since_last_lp_record_ >= 100) { // NOTE: We can actually record LP solutions more frequently. However // this process is time consuming and workers waste a lot of time doing diff --git a/ortools/sat/integer_search.h b/ortools/sat/integer_search.h index 7bf3165b89..f61d84b5ad 100644 --- a/ortools/sat/integer_search.h +++ b/ortools/sat/integer_search.h @@ -32,6 +32,26 @@ namespace operations_research { namespace sat { +// This is used to hold the next decision the solver will take. It is either +// a pure Boolean literal decision or correspond to an IntegerLiteral one. +// +// At most one of the two options should be set. +struct BooleanOrIntegerLiteral { + BooleanOrIntegerLiteral() {} + explicit BooleanOrIntegerLiteral(LiteralIndex index) + : boolean_literal_index(index) {} + explicit BooleanOrIntegerLiteral(IntegerLiteral i_lit) + : integer_literal(i_lit) {} + + bool HasValue() const { + return boolean_literal_index != kNoLiteralIndex || + integer_literal.var != kNoIntegerVariable; + } + + LiteralIndex boolean_literal_index = kNoLiteralIndex; + IntegerLiteral integer_literal = IntegerLiteral(); +}; + // Model struct that contains the search heuristics used to find a feasible // solution to an integer problem. // @@ -41,16 +61,16 @@ struct SearchHeuristics { // Decision and restart heuristics. The two vectors must be of the same size // and restart_policies[i] will always be used in conjunction with // decision_policies[i]. - std::vector> decision_policies; + std::vector> decision_policies; std::vector> restart_policies; - // Index in the vector above that indicate the current configuration. + // Index in the vectors above that indicate the current configuration. int policy_index; // Two special decision functions that are constructed at loading time. // These are used by ConfigureSearchHeuristics() to fill the policies above. - std::function fixed_search = nullptr; - std::function hint_search = nullptr; + std::function fixed_search = nullptr; + std::function hint_search = nullptr; }; // Given a base "fixed_search" function that should mainly control in which @@ -87,35 +107,34 @@ SatSolver::Status ResetAndSolveIntegerProblem( // and then call ResetAndSolveIntegerProblem() without any assumptions. SatSolver::Status SolveIntegerProblemWithLazyEncoding(Model* model); -// Helper methods to return up (>=) and down (<=) branching decisions. -LiteralIndex BranchDown(IntegerVariable var, IntegerValue value, Model* model); -LiteralIndex BranchUp(IntegerVariable var, IntegerValue value, Model* model); +// Returns decision corresponding to var at its lower bound. +// Returns an invalid literal if the variable is fixed. +IntegerLiteral AtMinValue(IntegerVariable var, IntegerTrail* integer_trail); -// Returns decision corresponding to var at its lower bound. Returns -// kNoLiteralIndex if the variable is fixed. -LiteralIndex AtMinValue(IntegerVariable var, IntegerTrail* integer_trail, - IntegerEncoder* integer_encoder); +// If a variable appear in the objective, branch on its best objective value. +IntegerLiteral ChooseBestObjectiveValue(IntegerVariable var, Model* model); // Returns decision corresponding to var >= lb + max(1, (ub - lb) / 2). It also // CHECKs that the variable is not fixed. -LiteralIndex GreaterOrEqualToMiddleValue(IntegerVariable var, Model* model); +IntegerLiteral GreaterOrEqualToMiddleValue(IntegerVariable var, + IntegerTrail* integer_trail); // This method first tries var <= value. If this does not reduce the domain it // tries var >= value. If that also does not reduce the domain then returns -// kNoLiteralIndex. -LiteralIndex SplitAroundGivenValue(IntegerVariable positive_var, - IntegerValue value, Model* model); +// an invalid literal. +IntegerLiteral SplitAroundGivenValue(IntegerVariable var, IntegerValue value, + Model* model); // Returns decision corresponding to var <= round(lp_value). If the variable -// does not appear in the LP, this method returns kNoLiteralIndex. -LiteralIndex SplitAroundLpValue(IntegerVariable var, Model* model); +// does not appear in the LP, this method returns an invalid literal. +IntegerLiteral SplitAroundLpValue(IntegerVariable var, Model* model); // Returns decision corresponding to var <= best_solution[var]. If no solution -// has been found, this method returns kNoLiteralIndex. This was suggested in -// paper: "Solution-Based Phase Saving for CP" (2018) by Emir Demirovic, -// Geoffrey Chu, and Peter J. Stuckey -LiteralIndex SplitDomainUsingBestSolutionValue(IntegerVariable var, - Model* model); +// has been found, this method returns a literal with kNoIntegerVariable. This +// was suggested in paper: "Solution-Based Phase Saving for CP" (2018) by Emir +// Demirovic, Geoffrey Chu, and Peter J. Stuckey. +IntegerLiteral SplitDomainUsingBestSolutionValue(IntegerVariable var, + Model* model); // Decision heuristic for SolveIntegerProblemWithLazyEncoding(). Returns a // function that will return the literal corresponding to the fact that the @@ -123,14 +142,15 @@ LiteralIndex SplitDomainUsingBestSolutionValue(IntegerVariable var, // return kNoLiteralIndex if all the given variables are fixed. // // Note that this function will create the associated literal if needed. -std::function FirstUnassignedVarAtItsMinHeuristic( +std::function FirstUnassignedVarAtItsMinHeuristic( const std::vector& vars, Model* model); // Decision heuristic for SolveIntegerProblemWithLazyEncoding(). Like // FirstUnassignedVarAtItsMinHeuristic() but the function will return the // literal corresponding to the fact that the currently non-assigned variable // with the lowest min has a value <= this min. -std::function UnassignedVarWithLowestMinAtItsMinHeuristic( +std::function +UnassignedVarWithLowestMinAtItsMinHeuristic( const std::vector& vars, Model* model); // Set the first unassigned Literal/Variable to its value. @@ -142,39 +162,40 @@ struct BooleanOrIntegerVariable { BooleanVariable bool_var = kNoBooleanVariable; IntegerVariable int_var = kNoIntegerVariable; }; -std::function FollowHint( +std::function FollowHint( const std::vector& vars, const std::vector& values, Model* model); // Combines search heuristics in order: if the i-th one returns kNoLiteralIndex, // ask the (i+1)-th. If every heuristic returned kNoLiteralIndex, // returns kNoLiteralIndex. -std::function SequentialSearch( - std::vector> heuristics); +std::function SequentialSearch( + std::vector> heuristics); // Changes the value of the given decision by 'var_selection_heuristic'. We try // to see if the decision is "associated" with an IntegerVariable, and if it is // the case, we choose the new value by the first 'value_selection_heuristics' -// that is applicable (return value != kNoLiteralIndex). If none of the -// heuristics are applicable then the given decision by -// 'var_selection_heuristic' is returned. -std::function SequentialValueSelection( - std::vector> +// that is applicable. If none of the heuristics are applicable then the given +// decision by 'var_selection_heuristic' is returned. +std::function SequentialValueSelection( + std::vector> value_selection_heuristics, - std::function var_selection_heuristic, Model* model); + std::function var_selection_heuristic, + Model* model); // Changes the value of the given decision by 'var_selection_heuristic' // according to various value selection heuristics. Looks at the code to know // exactly what heuristic we use. -std::function IntegerValueSelectionHeuristic( - std::function var_selection_heuristic, Model* model); +std::function IntegerValueSelectionHeuristic( + std::function var_selection_heuristic, + Model* model); -// Returns the LiteralIndex advised by the underliying SAT solver. -std::function SatSolverHeuristic(Model* model); +// Returns the BooleanOrIntegerLiteral advised by the underliying SAT solver. +std::function SatSolverHeuristic(Model* model); // Gets the branching variable using pseudo costs and combines it with a value // for branching. -std::function PseudoCost(Model* model); +std::function PseudoCost(Model* model); // Returns true if the number of variables in the linearized part represent // a large enough proportion of all the problem variables. @@ -186,16 +207,12 @@ std::function RestartEveryKFailures(int k, SatSolver* solver); // A restart policy that uses the underlying sat solver's policy. std::function SatSolverRestartPolicy(Model* model); -// Appends model-owned automatic heuristics to input_heuristics in a new vector. -std::vector> AddModelHeuristics( - const std::vector>& input_heuristics, - Model* model); - // Concatenates each input_heuristic with a default heuristic that instantiate // all the problem's Boolean variables, into a new vector. -std::vector> CompleteHeuristics( - const std::vector>& incomplete_heuristics, - const std::function& completion_heuristic); +std::vector> CompleteHeuristics( + const std::vector>& + incomplete_heuristics, + const std::function& completion_heuristic); } // namespace sat } // namespace operations_research diff --git a/ortools/sat/linear_programming_constraint.cc b/ortools/sat/linear_programming_constraint.cc index 972739dd7a..d0aa40f5c4 100644 --- a/ortools/sat/linear_programming_constraint.cc +++ b/ortools/sat/linear_programming_constraint.cc @@ -160,7 +160,6 @@ LinearProgrammingConstraint::LinearProgrammingConstraint(Model* model) time_limit_(model->GetOrCreate()), integer_trail_(model->GetOrCreate()), trail_(model->GetOrCreate()), - model_heuristics_(model->GetOrCreate()), integer_encoder_(model->GetOrCreate()), random_(model->GetOrCreate()), implied_bounds_processor_({}, integer_trail_, @@ -323,7 +322,12 @@ bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { lp_data_.SetVariableBounds(glop::ColIndex(i), lb, ub); } - scaler_.Scale(&lp_data_); + // TODO(user): Better result if we remove fixed variables from the objective? + // Another option, because we have an idea of the actual optimal value as we + // do more and more solve, is that we can scale according to this value. + glop::GlopParameters params; + params.set_cost_scaling(glop::GlopParameters::MEAN_COST_SCALING); + scaler_.Scale(params, &lp_data_); UpdateBoundsOfLpVariables(); lp_data_.NotifyThatColumnsAreClean(); @@ -507,12 +511,6 @@ void LinearProgrammingConstraint::RegisterWith(Model* model) { watcher->SetPropagatorPriority(watcher_id, 2); watcher->AlwaysCallAtLevelZero(watcher_id); - if (integer_variables_.size() >= 20) { // Do not use on small subparts. - auto* container = model->GetOrCreate(); - container->push_back(HeuristicLPPseudoCostBinary(model)); - container->push_back(HeuristicLPMostInfeasibleBinary(model)); - } - // Registering it with the trail make sure this class is always in sync when // it is used in the decision heuristics. integer_trail_->RegisterReversibleClass(this); @@ -924,6 +922,8 @@ void LinearProgrammingConstraint::AddCGCuts() { // corresponding row is not tight under the given lp values. if (basis_col >= integer_variables_.size()) continue; + if (time_limit_->LimitReached()) break; + const glop::ScatteredRow& lambda = simplex_.GetUnitRowLeftInverse(row); glop::DenseColumn lp_multipliers(num_rows, 0.0); double magnitude = 0.0; @@ -1057,6 +1057,8 @@ void LinearProgrammingConstraint::AddMirCuts() { gtl::ITIVector used_rows; std::vector> integer_multipliers; for (const std::pair& entry : base_rows) { + if (time_limit_->LimitReached()) break; + // First try to generate a cut directly from this base row (MIR1). // // Note(user): We abort on success like it seems to be done in the @@ -1235,6 +1237,7 @@ void LinearProgrammingConstraint::AddMirCuts() { void LinearProgrammingConstraint::AddZeroHalfCuts() { CHECK_EQ(trail_->CurrentDecisionLevel(), 0); + if (time_limit_->LimitReached()) return; tmp_lp_values_.clear(); tmp_var_lbs_.clear(); @@ -1260,6 +1263,8 @@ void LinearProgrammingConstraint::AddZeroHalfCuts() { } for (const std::vector>& multipliers : zero_half_cut_helper_.InterestingCandidates(random_)) { + if (time_limit_->LimitReached()) break; + // TODO(user): Make sure that if the resulting linear coefficients are not // too high, we do try a "divisor" of two and thus try a true zero-half cut // instead of just using our best MIR heuristic (which might still be better @@ -2494,11 +2499,9 @@ CutGenerator CreateCVRPCutGenerator(int num_nodes, return result; } -std::function -LinearProgrammingConstraint::HeuristicLPMostInfeasibleBinary(Model* model) { - IntegerTrail* integer_trail = integer_trail_; - IntegerEncoder* integer_encoder = model->GetOrCreate(); - // Gather all 0-1 variables that appear in some LP. +std::function +LinearProgrammingConstraint::HeuristicLpMostInfeasibleBinary(Model* model) { + // Gather all 0-1 variables that appear in this LP. std::vector variables; for (IntegerVariable var : integer_variables_) { if (integer_trail_->LowerBound(var) == 0 && @@ -2509,7 +2512,7 @@ LinearProgrammingConstraint::HeuristicLPMostInfeasibleBinary(Model* model) { VLOG(1) << "HeuristicLPMostInfeasibleBinary has " << variables.size() << " variables."; - return [this, variables, integer_trail, integer_encoder]() { + return [this, variables]() { const double kEpsilon = 1e-6; // Find most fractional value. IntegerVariable fractional_var = kNoIntegerVariable; @@ -2536,17 +2539,14 @@ LinearProgrammingConstraint::HeuristicLPMostInfeasibleBinary(Model* model) { } if (fractional_var != kNoIntegerVariable) { - return integer_encoder - ->GetOrCreateAssociatedLiteral( - IntegerLiteral::GreaterOrEqual(fractional_var, IntegerValue(1))) - .Index(); + IntegerLiteral::GreaterOrEqual(fractional_var, IntegerValue(1)); } - return kNoLiteralIndex; + return IntegerLiteral(); }; } -std::function -LinearProgrammingConstraint::HeuristicLPPseudoCostBinary(Model* model) { +std::function +LinearProgrammingConstraint::HeuristicLpReducedCostBinary(Model* model) { // Gather all 0-1 variables that appear in this LP. std::vector variables; for (IntegerVariable var : integer_variables_) { @@ -2555,7 +2555,7 @@ LinearProgrammingConstraint::HeuristicLPPseudoCostBinary(Model* model) { variables.push_back(var); } } - VLOG(1) << "HeuristicLPPseudoCostBinary has " << variables.size() + VLOG(1) << "HeuristicLpReducedCostBinary has " << variables.size() << " variables."; // Store average of reduced cost from 1 to 0. The best heuristic only sets @@ -2566,7 +2566,6 @@ LinearProgrammingConstraint::HeuristicLPPseudoCostBinary(Model* model) { std::vector num_cost_to_zero(num_vars); int num_calls = 0; - IntegerEncoder* integer_encoder = model->GetOrCreate(); return [=]() mutable { const double kEpsilon = 1e-6; @@ -2617,13 +2616,10 @@ LinearProgrammingConstraint::HeuristicLPPseudoCostBinary(Model* model) { } if (selected_index >= 0) { - const Literal decision = integer_encoder->GetOrCreateAssociatedLiteral( - IntegerLiteral::GreaterOrEqual(variables[selected_index], - IntegerValue(1))); - return decision.Index(); + return IntegerLiteral::GreaterOrEqual(variables[selected_index], + IntegerValue(1)); } - - return kNoLiteralIndex; + return IntegerLiteral(); }; } @@ -2703,12 +2699,13 @@ void LinearProgrammingConstraint::UpdateAverageReducedCosts() { positions_by_decreasing_rc_score_.end()); } -std::function -LinearProgrammingConstraint::LPReducedCostAverageBranching() { +// TODO(user): Remove duplication with HeuristicLpReducedCostBinary(). +std::function +LinearProgrammingConstraint::HeuristicLpReducedCostAverageBranching() { return [this]() { return this->LPReducedCostAverageDecision(); }; } -LiteralIndex LinearProgrammingConstraint::LPReducedCostAverageDecision() { +IntegerLiteral LinearProgrammingConstraint::LPReducedCostAverageDecision() { // Select noninstantiated variable with highest positive average reduced cost. int selected_index = -1; const int size = positions_by_decreasing_rc_score_.size(); @@ -2723,7 +2720,7 @@ LiteralIndex LinearProgrammingConstraint::LPReducedCostAverageDecision() { break; } - if (selected_index == -1) return kNoLiteralIndex; + if (selected_index == -1) return IntegerLiteral(); const IntegerVariable var = integer_variables_[selected_index]; // If ceil(value) is current upper bound, try var == upper bound first. @@ -2734,10 +2731,7 @@ LiteralIndex LinearProgrammingConstraint::LPReducedCostAverageDecision() { const IntegerValue value_ceil( std::ceil(this->GetSolutionValue(var) - kCpEpsilon)); if (value_ceil >= ub) { - const Literal result = integer_encoder_->GetOrCreateAssociatedLiteral( - IntegerLiteral::GreaterOrEqual(var, ub)); - CHECK(!trail_->Assignment().LiteralIsAssigned(result)); - return result.Index(); + return IntegerLiteral::GreaterOrEqual(var, ub); } // If floor(value) is current lower bound, try var == lower bound first. @@ -2746,11 +2740,7 @@ LiteralIndex LinearProgrammingConstraint::LPReducedCostAverageDecision() { const IntegerValue value_floor( std::floor(this->GetSolutionValue(var) + kCpEpsilon)); if (value_floor <= lb) { - const Literal result = integer_encoder_->GetOrCreateAssociatedLiteral( - IntegerLiteral::LowerOrEqual(var, lb)); - CHECK(!trail_->Assignment().LiteralIsAssigned(result)) - << " " << lb << " " << ub; - return result.Index(); + return IntegerLiteral::LowerOrEqual(var, lb); } // Here lb < value_floor <= value_ceil < ub. @@ -2764,15 +2754,9 @@ LiteralIndex LinearProgrammingConstraint::LPReducedCostAverageDecision() { ? sum_cost_down_[selected_index] / num_cost_down_[selected_index] : 0.0; if (a_down < a_up) { - const Literal result = integer_encoder_->GetOrCreateAssociatedLiteral( - IntegerLiteral::LowerOrEqual(var, value_floor)); - CHECK(!trail_->Assignment().LiteralIsAssigned(result)); - return result.Index(); + return IntegerLiteral::LowerOrEqual(var, value_floor); } else { - const Literal result = integer_encoder_->GetOrCreateAssociatedLiteral( - IntegerLiteral::GreaterOrEqual(var, value_ceil)); - CHECK(!trail_->Assignment().LiteralIsAssigned(result)); - return result.Index(); + return IntegerLiteral::GreaterOrEqual(var, value_ceil); } } diff --git a/ortools/sat/linear_programming_constraint.h b/ortools/sat/linear_programming_constraint.h index 5c09075a41..98dd095e9a 100644 --- a/ortools/sat/linear_programming_constraint.h +++ b/ortools/sat/linear_programming_constraint.h @@ -171,7 +171,8 @@ class LinearProgrammingConstraint : public PropagatorInterface, } std::string DimensionString() const { return lp_data_.GetDimensionString(); } - // Returns a LiteralIndex guided by the underlying LP constraints. + // Returns a IntegerLiteral guided by the underlying LP constraints. + // // This looks at all unassigned 0-1 variables, takes the one with // a support value closest to 0.5, and tries to assign it to 1. // If all 0-1 variables have an integer support, returns kNoLiteralIndex. @@ -180,9 +181,10 @@ class LinearProgrammingConstraint : public PropagatorInterface, // TODO(user): This fixes to 1, but for some problems fixing to 0 // or to the std::round(support value) might work better. When this is the // case, change behaviour automatically? - std::function HeuristicLPMostInfeasibleBinary(Model* model); + std::function HeuristicLpMostInfeasibleBinary(Model* model); - // Returns a LiteralIndex guided by the underlying LP constraints. + // Returns a IntegerLiteral guided by the underlying LP constraints. + // // This computes the mean of reduced costs over successive calls, // and tries to fix the variable which has the highest reduced cost. // Tie-breaking is done using the variable natural order. @@ -198,13 +200,14 @@ class LinearProgrammingConstraint : public PropagatorInterface, // does BFS. This might depend on the model, more trials are necessary. We // could also do exponential smoothing instead of decaying every N calls, i.e. // pseudo = a * pseudo + (1-a) reduced. - std::function HeuristicLPPseudoCostBinary(Model* model); + std::function HeuristicLpReducedCostBinary(Model* model); - // Returns a LiteralIndex guided by the underlying LP constraints. + // Returns a IntegerLiteral guided by the underlying LP constraints. + // // This computes the mean of reduced costs over successive calls, // and tries to fix the variable which has the highest reduced cost. // Tie-breaking is done using the variable natural order. - std::function LPReducedCostAverageBranching(); + std::function HeuristicLpReducedCostAverageBranching(); // Average number of nonbasic variables with zero reduced costs. double average_degeneracy() const { @@ -351,7 +354,7 @@ class LinearProgrammingConstraint : public PropagatorInterface, void UpdateAverageReducedCosts(); // Callback underlying LPReducedCostAverageBranching(). - LiteralIndex LPReducedCostAverageDecision(); + IntegerLiteral LPReducedCostAverageDecision(); // Updates the simplex iteration limit for the next visit. // As per current algorithm, we use a limit which is dependent on size of the @@ -427,7 +430,6 @@ class LinearProgrammingConstraint : public PropagatorInterface, TimeLimit* time_limit_; IntegerTrail* integer_trail_; Trail* trail_; - SearchHeuristicsVector* model_heuristics_; IntegerEncoder* integer_encoder_; ModelRandomGenerator* random_; diff --git a/ortools/sat/lp_utils.cc b/ortools/sat/lp_utils.cc index 74472fe00f..75bb10dab8 100644 --- a/ortools/sat/lp_utils.cc +++ b/ortools/sat/lp_utils.cc @@ -219,7 +219,9 @@ std::vector DetectImpliedIntegers(bool log_info, CHECK_NE(var, -1); CHECK(!mp_model->variable(var).is_integer()); CHECK_EQ(var_scaling[var], 1.0); - VLOG_IF(2, scaling != 1.0) << "Scaled " << var << " by " << scaling; + if (scaling != 1.0) { + VLOG(2) << "Scaled " << var << " by " << scaling; + } ++num_detected; max_scaling = std::max(max_scaling, scaling); @@ -433,7 +435,7 @@ struct ConstraintScaler { CpModelProto* cp_model); double max_relative_coeff_error = 0.0; - double max_sum_error = 0.0; + double max_relative_rhs_error = 0.0; double max_scaling_factor = 0.0; double wanted_precision = 1e-6; @@ -471,14 +473,6 @@ ConstraintProto* ConstraintScaler::AddConstraint( constraint->set_name(mp_constraint.name()); auto* arg = constraint->mutable_linear(); - // We use a relative precision for the worst case error on the activity that - // depends on the constraint l2 norm (with a minimum of 1.0). - // - // Note that when we scale variable to integer, we usually multiply them - // by a factor bigger than one, and thus the constraint norm is smaller than - // the one of the original constraint. - double ct_norm = 0.0; - // First scale the coefficients of the constraints so that the constraint // sum can always be computed without integer overflow. var_indices.clear(); @@ -495,19 +489,16 @@ ConstraintProto* ConstraintScaler::AddConstraint( const double coeff = mp_constraint.coefficient(i); if (coeff == 0.0) continue; - ct_norm += coeff * coeff; var_indices.push_back(mp_constraint.var_index(i)); coefficients.push_back(coeff); lower_bounds.push_back(lb); upper_bounds.push_back(ub); } - ct_norm = std::max(1.0, std::sqrt(ct_norm)); - // We also take into account the constraint bounds. + // We compute the worst case error relative to the magnitude of the bounds. Fractional lb = mp_constraint.lower_bound(); Fractional ub = mp_constraint.upper_bound(); - if (lb > 1.0) ct_norm = std::max(ct_norm, lb); - if (ub < -1.0) ct_norm = std::max(ct_norm, -ub); + const double ct_norm = std::max(1.0, std::min(std::abs(lb), std::abs(ub))); double scaling_factor = GetBestScalingOfDoublesToInt64( coefficients, lower_bounds, upper_bounds, scaling_target); @@ -561,14 +552,14 @@ ConstraintProto* ConstraintScaler::AddConstraint( arg->add_coeffs(value); } } - max_sum_error = - std::max(max_sum_error, scaled_sum_error / (scaling_factor * ct_norm)); + max_relative_rhs_error = std::max( + max_relative_rhs_error, scaled_sum_error / (scaling_factor * ct_norm)); // Add the constraint bounds. Because we are sure the scaled constraint fit // on an int64, if the scaled bounds are too large, the constraint is either // always true or always false. if (relax_bound) { - lb -= std::max(std::abs(lb), ct_norm) * wanted_precision; + lb -= std::max(std::abs(lb), 1.0) * wanted_precision; } const Fractional scaled_lb = std::ceil(lb * scaling_factor); if (lb == -kInfinity || scaled_lb <= kint64min) { @@ -580,7 +571,7 @@ ConstraintProto* ConstraintScaler::AddConstraint( } if (relax_bound) { - ub += std::max(std::abs(ub), ct_norm) * wanted_precision; + ub += std::max(std::abs(ub), 1.0) * wanted_precision; } const Fractional scaled_ub = std::floor(ub * scaling_factor); if (ub == kInfinity || scaled_ub >= kint64max) { @@ -759,21 +750,27 @@ bool ConvertMPModelProtoToCpModelProto(const SatParameters& params, } } - double max_relative_coeff_error = scaler.max_relative_coeff_error; - double max_sum_error = scaler.max_sum_error; - double max_scaling_factor = scaler.max_scaling_factor; - - // Display the error/scaling without taking into account the objective first. - VLOG(1) << "Maximum constraint coefficient relative error: " - << max_relative_coeff_error; - VLOG(1) << "Maximum constraint worst-case sum error: " << max_sum_error; - VLOG(1) << "Maximum constraint scaling factor: " << max_scaling_factor; + // Display the error/scaling on the constraints. + const bool log_info = VLOG_IS_ON(1) || params.log_search_progress(); + LOG_IF(INFO, log_info) << "Maximum constraint coefficient relative error: " + << scaler.max_relative_coeff_error; + LOG_IF(INFO, log_info) + << "Maximum constraint worst-case activity relative error: " + << scaler.max_relative_rhs_error + << (scaler.max_relative_rhs_error > params.mip_check_precision() + ? " [Potentially IMPRECISE]" + : ""); + LOG_IF(INFO, log_info) << "Maximum constraint scaling factor: " + << scaler.max_scaling_factor; // Add the objective. std::vector var_indices; std::vector coefficients; std::vector lower_bounds; std::vector upper_bounds; + double min_magnitude = std::numeric_limits::infinity(); + double max_magnitude = 0.0; + double l1_norm = 0.0; for (int i = 0; i < num_variables; ++i) { const MPVariableProto& mp_var = mp_model.variable(i); if (mp_var.objective_coefficient() == 0.0) continue; @@ -787,6 +784,16 @@ bool ConvertMPModelProtoToCpModelProto(const SatParameters& params, coefficients.push_back(mp_var.objective_coefficient()); lower_bounds.push_back(lb); upper_bounds.push_back(ub); + min_magnitude = std::min(min_magnitude, std::abs(coefficients.back())); + max_magnitude = std::max(max_magnitude, std::abs(coefficients.back())); + l1_norm += std::abs(coefficients.back()); + } + if (!coefficients.empty()) { + const double average_magnitude = + l1_norm / static_cast(coefficients.size()); + LOG_IF(INFO, log_info) << "Objective magnitude in [" << min_magnitude + << ", " << max_magnitude + << "] average = " << average_magnitude; } if (!coefficients.empty() || mp_model.objective_offset() != 0.0) { double scaling_factor = GetBestScalingOfDoublesToInt64( @@ -819,14 +826,16 @@ bool ConvertMPModelProtoToCpModelProto(const SatParameters& params, } const int64 gcd = ComputeGcdOfRoundedDoubles(coefficients, scaling_factor); - max_relative_coeff_error = - std::max(relative_coeff_error, max_relative_coeff_error); // Display the objective error/scaling. - VLOG(1) << "objective coefficient relative error: " << relative_coeff_error; - VLOG(1) << "objective worst-case absolute error: " - << scaled_sum_error / scaling_factor; - VLOG(1) << "objective scaling factor: " << scaling_factor / gcd; + LOG_IF(INFO, log_info) + << "objective coefficient relative error: " << relative_coeff_error + << (relative_coeff_error > params.mip_check_precision() ? " [IMPRECISE]" + : ""); + LOG_IF(INFO, log_info) << "objective worst-case absolute error: " + << scaled_sum_error / scaling_factor; + LOG_IF(INFO, log_info) << "objective scaling factor: " + << scaling_factor / gcd; // Note that here we set the scaling factor for the inverse operation of // getting the "true" objective value from the scaled one. Hence the @@ -847,15 +856,6 @@ bool ConvertMPModelProtoToCpModelProto(const SatParameters& params, } } - // Test the precision of the conversion. - const double allowed_error = - std::max(params.mip_check_precision(), params.mip_wanted_precision()); - if (max_sum_error > allowed_error) { - LOG(WARNING) << "The relative error during double -> int64 conversion " - << "is too high! error:" << max_sum_error - << " check_tolerance:" << allowed_error; - return false; - } return true; } diff --git a/ortools/sat/python/cp_model.py b/ortools/sat/python/cp_model.py index fcc28000ce..5632e4debc 100644 --- a/ortools/sat/python/cp_model.py +++ b/ortools/sat/python/cp_model.py @@ -978,7 +978,7 @@ class CpModel(object): raise ValueError('AddAutomaton expects some final states') if not transition_triples: - raise ValueError('AddAutomaton expects some transtion triples') + raise ValueError('AddAutomaton expects some transition triples') ct = Constraint(self.__model.constraints) model_ct = self.__model.constraints[ct.Index()] diff --git a/ortools/sat/python/sat.i b/ortools/sat/python/sat.i index 5cdf0934d3..88628f82fc 100644 --- a/ortools/sat/python/sat.i +++ b/ortools/sat/python/sat.i @@ -99,3 +99,4 @@ PY_PROTO_TYPEMAP(ortools.sat.sat_parameters_pb2, %include "ortools/sat/swig_helper.h" %unignoreall + diff --git a/ortools/sat/rins.cc b/ortools/sat/rins.cc index 191235c1e0..a1f854f420 100644 --- a/ortools/sat/rins.cc +++ b/ortools/sat/rins.cc @@ -30,20 +30,17 @@ void RecordLPRelaxationValues(Model* model) { std::vector relaxation_values( lp_vars.model_vars_size, std::numeric_limits::infinity()); - IntegerTrail* const integer_trail = model->GetOrCreate(); - + auto* integer_trail = model->GetOrCreate(); for (const LPVariable& lp_var : lp_vars.vars) { const IntegerVariable positive_var = lp_var.positive_var; - const int model_var = lp_var.model_var; if (integer_trail->IsCurrentlyIgnored(positive_var)) continue; LinearProgrammingConstraint* lp = lp_var.lp; if (lp == nullptr || !lp->HasSolution()) continue; - const double lp_value = lp->GetSolutionValue(positive_var); - relaxation_values[model_var] = lp_value; + relaxation_values[lp_var.model_var] = lp->GetSolutionValue(positive_var); } - lp_solutions->NewLPSolution(relaxation_values); + lp_solutions->NewLPSolution(std::move(relaxation_values)); } namespace { diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index c8349638be..938a6ce279 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -944,8 +944,9 @@ message SatParameters { // When scaling constraint with double coefficients to integer coefficients, // we will multiply by a power of 2 and round the coefficients. We will choose - // the lowest power such that we have this relative precision on each of the - // constraint (resp. objective) coefficient. + // the lowest power such that we have no potential overflow and the worst case + // constraint activity error do not exceed this threshold relative to the + // constraint bounds. // // We also use this to decide by how much we relax the constraint bounds so // that we can have a feasible integer solution of constraints involving @@ -965,9 +966,7 @@ message SatParameters { optional int32 mip_max_activity_exponent = 127 [default = 53]; // As explained in mip_precision and mip_max_activity_exponent, we cannot - // always reach the wanted coefficient precision during scaling. When we - // cannot, we will report MODEL_INVALID if the relative precision of the worst - // case activity error of a constraint divided by its l2 norm is larger than - // this parameter. - optional double mip_check_precision = 128 [default = 1e-3]; + // always reach the wanted precision during scaling. We use this threshold to + // enphasize in the logs when the precision seems bad. + optional double mip_check_precision = 128 [default = 1e-4]; } diff --git a/ortools/sat/synchronization.cc b/ortools/sat/synchronization.cc index d4d3563c01..57d4d0a368 100644 --- a/ortools/sat/synchronization.cc +++ b/ortools/sat/synchronization.cc @@ -46,7 +46,6 @@ void SharedRelaxationSolutionRepository::NewRelaxationSolution( const CpSolverResponse& response) { // Note that the Add() method already applies mutex lock. So we don't need it // here. - if (response.solution().empty()) return; // Add this solution to the pool. @@ -64,20 +63,16 @@ void SharedRelaxationSolutionRepository::NewRelaxationSolution( } void SharedLPSolutionRepository::NewLPSolution( - const std::vector& lp_solution) { - // Note that the Add() method already applies mutex lock. So we don't need it - // here. - + std::vector lp_solution) { if (lp_solution.empty()) return; // Add this solution to the pool. SharedSolutionRepository::Solution solution; - solution.variable_values.assign(lp_solution.begin(), lp_solution.end()); + solution.variable_values = std::move(lp_solution); // We always prefer to keep the solution from the last synchronize batch. absl::MutexLock mutex_lock(&mutex_); solution.rank = -num_synchronization_; - AddInternal(solution); } @@ -584,16 +579,6 @@ void SharedBoundsManager::ReportPotentialNewBounds( upper_bounds_[var] = new_ub; } changed_variables_since_last_synchronize_.Set(var); - - if (VLOG_IS_ON(3)) { - const IntegerVariableProto& var_proto = model_proto.variables(var); - const std::string& var_name = - var_proto.name().empty() ? absl::StrCat("anonymous_var(", var, ")") - : var_proto.name(); - LOG(INFO) << " '" << worker_name << "' exports new bounds for " - << var_name << ": from [" << old_lb << ", " << old_ub - << "] to [" << new_lb << ", " << new_ub << "]"; - } } } diff --git a/ortools/sat/synchronization.h b/ortools/sat/synchronization.h index 4ae63d7c85..4ab5f39bd1 100644 --- a/ortools/sat/synchronization.h +++ b/ortools/sat/synchronization.h @@ -137,7 +137,7 @@ class SharedLPSolutionRepository : public SharedSolutionRepository { explicit SharedLPSolutionRepository(int num_solutions_to_keep) : SharedSolutionRepository(num_solutions_to_keep) {} - void NewLPSolution(const std::vector& lp_solution); + void NewLPSolution(std::vector lp_solution); }; // Set of partly filled solutions. They are meant to be finished by some lns diff --git a/ortools/util/BUILD b/ortools/util/BUILD index 338a36ac5b..961d3f779e 100644 --- a/ortools/util/BUILD +++ b/ortools/util/BUILD @@ -226,12 +226,12 @@ cc_library( srcs = ["file_util.cc"], hdrs = ["file_util.h"], deps = [ + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "//ortools/base", "//ortools/base:file", "//ortools/base:hash", "//ortools/base:recordio", - "//ortools/base:statusor", "//ortools/base:status_macros", # "//net/proto2/io/public", # "//net/proto2/io/public:io", diff --git a/ortools/util/file_util.cc b/ortools/util/file_util.cc index 44a745f3a2..f76c96712a 100644 --- a/ortools/util/file_util.cc +++ b/ortools/util/file_util.cc @@ -13,6 +13,7 @@ #include "ortools/util/file_util.h" +#include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "google/protobuf/descriptor.h" #include "google/protobuf/io/zero_copy_stream_impl_lite.h" diff --git a/ortools/util/file_util.h b/ortools/util/file_util.h index d76d26b7cb..7975be2c37 100644 --- a/ortools/util/file_util.h +++ b/ortools/util/file_util.h @@ -17,11 +17,11 @@ #include #include +#include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "google/protobuf/message.h" #include "ortools/base/file.h" #include "ortools/base/recordio.h" -#include "ortools/base/statusor.h" namespace operations_research { diff --git a/ortools/util/proto_tools.h b/ortools/util/proto_tools.h index bb0518946a..13a23f2f6a 100644 --- a/ortools/util/proto_tools.h +++ b/ortools/util/proto_tools.h @@ -16,14 +16,60 @@ #include +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_format.h" #include "google/protobuf/message.h" namespace operations_research { + +// Casts a generic google::protobuf::Message* to a specific proto type, or +// returns an InvalidArgumentError if it doesn't seem to be of the right type. +// Comes in non-const and const versions. +// NOTE(user): You should rather use DynamicCastToGenerated() from message.h +// if you don't need the fancy error message or the absl::Status. +template +absl::StatusOr SafeProtoDownCast(google::protobuf::Message* proto); +template +absl::StatusOr SafeProtoConstDownCast( + const google::protobuf::Message* proto); + // Prints a proto2 message as a string, it behaves like TextFormat::Print() // but also prints the default values of unset fields which is useful for // printing parameters. std::string FullProtocolMessageAsString( const google::protobuf::Message& message, int indent_level); +// ============================================================================= +// Implementation of function templates. + +template +absl::StatusOr SafeProtoDownCast(google::protobuf::Message* proto) { + const google::protobuf::Descriptor* expected_descriptor = + Proto::default_instance().GetDescriptor(); + const google::protobuf::Descriptor* actual_descriptor = + proto->GetDescriptor(); + if (actual_descriptor == expected_descriptor) + return reinterpret_cast(proto); + return absl::InvalidArgumentError(absl::StrFormat( + "Expected message type '%s', but got type '%s'", + expected_descriptor->full_name(), actual_descriptor->full_name())); +} + +template +absl::StatusOr SafeProtoConstDownCast( + const google::protobuf::Message* proto) { + const google::protobuf::Descriptor* expected_descriptor = + Proto::default_instance().GetDescriptor(); + const google::protobuf::Descriptor* actual_descriptor = + proto->GetDescriptor(); + if (actual_descriptor == expected_descriptor) { + return reinterpret_cast(proto); + } + return absl::InvalidArgumentError(absl::StrFormat( + "Expected message type '%s', but got type '%s'", + expected_descriptor->full_name(), actual_descriptor->full_name())); +} + } // namespace operations_research #endif // OR_TOOLS_UTIL_PROTO_TOOLS_H_ diff --git a/ortools/util/testing_utils.h b/ortools/util/testing_utils.h new file mode 100644 index 0000000000..7e9c5a7206 --- /dev/null +++ b/ortools/util/testing_utils.h @@ -0,0 +1,23 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_UTIL_TESTING_UTILS_H_ +#define OR_TOOLS_UTIL_TESTING_UTILS_H_ + +namespace operations_research { + +inline bool ProbablyRunningInsideUnitTest() { running false; } + +} // namespace operations_research + +#endif // OR_TOOLS_UTIL_TESTING_UTILS_H_