diff --git a/ortools/linear_solver/BUILD.bazel b/ortools/linear_solver/BUILD.bazel index b654eb3190..3387fc8bef 100644 --- a/ortools/linear_solver/BUILD.bazel +++ b/ortools/linear_solver/BUILD.bazel @@ -75,6 +75,7 @@ cc_library( "glop_interface.cc", "glop_utils.cc", "gurobi_interface.cc", + "highs_interface.cc", "linear_expr.cc", "linear_solver_callback.cc", "linear_solver.cc", @@ -111,6 +112,7 @@ cc_library( copts = [ "-DUSE_PDLP", "-DUSE_SCIP", + "-DUSE_HIGHS", ], deps = [ ":linear_solver_cc_proto", @@ -160,8 +162,8 @@ cc_library( visibility = ["//visibility:public"], deps = [ ":linear_solver_cc_proto", + "//ortools/base", "//ortools/base:accurate_sum", - "//ortools/base:commandlineflags", "//ortools/base:map_util", "//ortools/port:file", "//ortools/port:proto_utils", diff --git a/ortools/linear_solver/highs_interface.cc b/ortools/linear_solver/highs_interface.cc new file mode 100644 index 0000000000..1d99289867 --- /dev/null +++ b/ortools/linear_solver/highs_interface.cc @@ -0,0 +1,311 @@ +// Copyright 2010-2022 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 +#include +#include +#include +#include +#include + +#include "absl/base/attributes.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/types/optional.h" +#include "google/protobuf/text_format.h" +#include "ortools/base/logging.h" +#include "ortools/linear_solver/linear_solver.h" +#include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/proto_solver/highs_proto_solver.h" +#include "ortools/port/proto_utils.h" + +namespace operations_research { + +class HighsInterface : public MPSolverInterface { + public: + explicit HighsInterface(MPSolver* const solver, bool solve_as_a_mip); + ~HighsInterface() override; + + // ----- Solve ----- + MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; + std::optional DirectlySolveProto( + const MPModelRequest& request, std::atomic* interrupt) override; + + // ----- Model modifications and extraction ----- + void Reset() override; + void SetOptimizationDirection(bool maximize) override; + void SetVariableBounds(int index, double lb, double ub) override; + void SetVariableInteger(int index, bool integer) override; + void SetConstraintBounds(int index, double lb, double ub) override; + void AddRowConstraint(MPConstraint* const ct) override; + void AddVariable(MPVariable* const var) override; + void SetCoefficient(MPConstraint* const constraint, + const MPVariable* const variable, double new_value, + double old_value) override; + void ClearConstraint(MPConstraint* const constraint) override; + void SetObjectiveCoefficient(const MPVariable* const variable, + double coefficient) override; + void SetObjectiveOffset(double value) override; + void ClearObjective() override; + + // ------ Query statistics on the solution and the solve ------ + int64_t iterations() const override; + int64_t nodes() const override; + MPSolver::BasisStatus row_status(int constraint_index) const override; + MPSolver::BasisStatus column_status(int variable_index) const override; + + // ----- Misc ----- + bool IsContinuous() const override; + bool IsLP() const override; + bool IsMIP() const override; + + std::string SolverVersion() const override; + void* underlying_solver() override; + + void ExtractNewVariables() override; + void ExtractNewConstraints() override; + void ExtractObjective() override; + + void SetParameters(const MPSolverParameters& param) override; + void SetRelativeMipGap(double value) override; + void SetPrimalTolerance(double value) override; + void SetDualTolerance(double value) override; + void SetPresolveMode(int value) override; + void SetScalingMode(int value) override; + void SetLpAlgorithm(int value) override; + bool SetSolverSpecificParametersAsString( + const std::string& parameters) override; + absl::Status SetNumThreads(int num_threads) override; + + private: + void NonIncrementalChange(); + + const bool solve_as_a_mip_; +}; + +HighsInterface::HighsInterface(MPSolver* const solver, bool solve_as_a_mip) + : MPSolverInterface(solver), solve_as_a_mip_(solve_as_a_mip) {} + +HighsInterface::~HighsInterface() {} + +MPSolver::ResultStatus HighsInterface::Solve(const MPSolverParameters& param) { + // Reset extraction as this interface is not incremental yet. + Reset(); + ExtractModel(); + + SetParameters(param); + if (quiet_) { + // parameters_.set_verbosity_level(0); + } else { + // parameters_.set_verbosity_level(3); + } + + solver_->SetSolverSpecificParametersAsString( + solver_->solver_specific_parameter_string_); + + // Time limit. + if (solver_->time_limit()) { + VLOG(1) << "Setting time limit = " << solver_->time_limit() << " ms."; + // parameters_.mutable_termination_criteria()->set_time_sec_limit( + // static_cast(solver_->time_limit()) / 1000.0); + } + + // Mark variables and constraints as extracted. + for (int i = 0; i < solver_->variables_.size(); ++i) { + set_variable_as_extracted(i, true); + } + for (int i = 0; i < solver_->constraints_.size(); ++i) { + set_constraint_as_extracted(i, true); + } + + MPModelProto model_proto; + solver_->ExportModelToProto(&model_proto); + MPModelRequest request; + // *request.mutable_model() = std::move(model_proto); + // if (!google::protobuf::TextFormat::PrintToString( + // parameters_, request.mutable_solver_specific_parameters())) { + // LOG(QFATAL) << "Error converting parameters to text format: " + // << parameters_.DebugString(); + // } + absl::StatusOr response = + HighsSolveProto(request, /*relax_integer_variables=*/!solve_as_a_mip_); + + if (!response.ok()) { + LOG(ERROR) << "Unexpected error solving with Highs: " << response.status(); + return MPSolver::ABNORMAL; + } + + // The solution must be marked as synchronized even when no solution exists. + sync_status_ = SOLUTION_SYNCHRONIZED; + result_status_ = static_cast(response->status()); + LOG_IF(DFATAL, !response->has_solver_specific_info()) + << response->DebugString(); + // if (!solve_log_.ParseFromString(response->solver_specific_info())) { + // LOG(DFATAL) << "Unable to parse Highs's SolveLog from + // solver_specific_info"; + // } + + if (response->status() == MPSOLVER_FEASIBLE || + response->status() == MPSOLVER_OPTIMAL) { + const absl::Status result = solver_->LoadSolutionFromProto(*response); + if (!result.ok()) { + LOG(ERROR) << "LoadSolutionFromProto failed: " << result; + } + } + + return result_status_; +} + +std::optional HighsInterface::DirectlySolveProto( + const MPModelRequest& request, std::atomic* interrupt) { + if (interrupt) return std::nullopt; + absl::StatusOr response = + HighsSolveProto(request, /*relax_integer_variables=*/false); + + if (!response.ok()) { + LOG(ERROR) << "Unexpected error solving with Highs: " << response.status(); + MPSolutionResponse error_response; + error_response.set_status(MPSolverResponseStatus::MPSOLVER_ABNORMAL); + error_response.set_status_str(response.status().ToString()); + return error_response; + } + + return *response; +} + +void HighsInterface::Reset() { ResetExtractionInformation(); } + +void HighsInterface::SetOptimizationDirection(bool maximize) { + NonIncrementalChange(); +} + +void HighsInterface::SetVariableBounds(int index, double lb, double ub) { + NonIncrementalChange(); +} + +void HighsInterface::SetVariableInteger(int index, bool integer) { + NonIncrementalChange(); +} + +void HighsInterface::SetConstraintBounds(int index, double lb, double ub) { + NonIncrementalChange(); +} + +void HighsInterface::AddRowConstraint(MPConstraint* const ct) { + NonIncrementalChange(); +} + +void HighsInterface::AddVariable(MPVariable* const var) { + NonIncrementalChange(); +} + +void HighsInterface::SetCoefficient(MPConstraint* const constraint, + const MPVariable* const variable, + double new_value, double old_value) { + NonIncrementalChange(); +} + +void HighsInterface::ClearConstraint(MPConstraint* const constraint) { + NonIncrementalChange(); +} + +void HighsInterface::SetObjectiveCoefficient(const MPVariable* const variable, + double coefficient) { + NonIncrementalChange(); +} + +void HighsInterface::SetObjectiveOffset(double value) { + NonIncrementalChange(); +} + +void HighsInterface::ClearObjective() { NonIncrementalChange(); } + +int64_t HighsInterface::iterations() const { + return 0; // FIXME. +} + +int64_t HighsInterface::nodes() const { + LOG(DFATAL) << "Number of nodes only available for discrete problems"; + return MPSolverInterface::kUnknownNumberOfNodes; +} + +MPSolver::BasisStatus HighsInterface::row_status(int constraint_index) const { + // TODO(user): While basis status isn't well defined for PDLP, we could + // guess statuses that might be useful. + return MPSolver::BasisStatus::FREE; +} + +MPSolver::BasisStatus HighsInterface::column_status(int variable_index) const { + // TODO(user): While basis status isn't well defined for PDLP, we could + // guess statuses that might be useful. + return MPSolver::BasisStatus::FREE; +} + +bool HighsInterface::IsContinuous() const { return true; } + +bool HighsInterface::IsLP() const { return true; } + +bool HighsInterface::IsMIP() const { return solve_as_a_mip_; } + +std::string HighsInterface::SolverVersion() const { return "PDLP Solver"; } + +// TODO(user): Consider returning the SolveLog here, as it could be essential +// for interpreting the PDLP solution. +void* HighsInterface::underlying_solver() { return nullptr; } + +void HighsInterface::ExtractNewVariables() { NonIncrementalChange(); } + +void HighsInterface::ExtractNewConstraints() { NonIncrementalChange(); } + +void HighsInterface::ExtractObjective() { NonIncrementalChange(); } + +void HighsInterface::SetParameters(const MPSolverParameters& param) { + SetCommonParameters(param); +} + +absl::Status HighsInterface::SetNumThreads(int num_threads) { + if (num_threads < 1) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid number of threads: ", num_threads)); + } + // parameters_.set_num_threads(num_threads); + return absl::OkStatus(); +} + +// These have no effect. Use SetSolverSpecificParametersAsString instead. +void HighsInterface::SetPrimalTolerance(double value) {} +void HighsInterface::SetDualTolerance(double value) {} +void HighsInterface::SetScalingMode(int value) {} +void HighsInterface::SetLpAlgorithm(int value) {} +void HighsInterface::SetRelativeMipGap(double value) {} +void HighsInterface::SetPresolveMode(int value) {} + +bool HighsInterface::SetSolverSpecificParametersAsString( + const std::string& parameters) { + // return ProtobufTextFormatMergeFromString(parameters, ¶meters_); + return false; +} + +void HighsInterface::NonIncrementalChange() { + // The current implementation is not incremental. + sync_status_ = MUST_RELOAD; +} + +// Register PDLP in the global linear solver factory. +MPSolverInterface* BuildHighsInterface(MPSolver* const solver, + bool solve_as_a_mip) { + return new HighsInterface(solver, solve_as_a_mip); +} + +} // namespace operations_research diff --git a/ortools/linear_solver/linear_solver.h b/ortools/linear_solver/linear_solver.h index 5fccc2cb3e..8bd9fbae1b 100644 --- a/ortools/linear_solver/linear_solver.h +++ b/ortools/linear_solver/linear_solver.h @@ -859,6 +859,7 @@ class MPSolver { friend class BopInterface; friend class SatInterface; friend class PdlpInterface; + friend class HighsInterface; friend class KnapsackInterface; // Debugging: verify that the given MPVariable* belongs to this solver. @@ -1079,6 +1080,7 @@ class MPObjective { friend class BopInterface; friend class SatInterface; friend class PdlpInterface; + friend class HighsInterface; friend class KnapsackInterface; // Constructor. An objective points to a single MPSolverInterface @@ -1189,6 +1191,7 @@ class MPVariable { friend class BopInterface; friend class SatInterface; friend class PdlpInterface; + friend class HighsInterface; friend class KnapsackInterface; // Constructor. A variable points to a single MPSolverInterface that @@ -1331,6 +1334,7 @@ class MPConstraint { friend class BopInterface; friend class SatInterface; friend class PdlpInterface; + friend class HighsInterface; friend class KnapsackInterface; // Constructor. A constraint points to a single MPSolverInterface diff --git a/ortools/linear_solver/python/pywraplp_test.py b/ortools/linear_solver/python/pywraplp_test.py index baddac5d95..d098d9e3a8 100755 --- a/ortools/linear_solver/python/pywraplp_test.py +++ b/ortools/linear_solver/python/pywraplp_test.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # Copyright 2010-2022 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -47,7 +46,7 @@ class PyWrapLp(unittest.TestCase): text_format.Merge(TEXT_MODEL, input_proto) solver = pywraplp.Solver.CreateSolver('CBC') if not solver: - return + return # For now, create the model from the proto by parsing the proto errors = solver.LoadModelFromProto(input_proto) self.assertFalse(errors) diff --git a/ortools/linear_solver/wrappers/BUILD.bazel b/ortools/linear_solver/wrappers/BUILD.bazel index 8dec5305b2..010b1cefe8 100644 --- a/ortools/linear_solver/wrappers/BUILD.bazel +++ b/ortools/linear_solver/wrappers/BUILD.bazel @@ -32,7 +32,8 @@ cc_library( copts = [ "-DUSE_HIGHS", "-DUSE_PDLP", - "-DUSE_SCIP", ], + "-DUSE_SCIP", + ], visibility = ["//visibility:public"], deps = [ "//ortools/linear_solver",