1823 lines
73 KiB
C++
1823 lines
73 KiB
C++
// Copyright 2010-2025 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/math_opt/solvers/glpk_solver.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <thread> // IWYU pragma: keep
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/base/nullability.h"
|
|
#include "absl/cleanup/cleanup.h"
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/log/log.h"
|
|
#include "absl/memory/memory.h"
|
|
#include "absl/status/status.h"
|
|
#include "absl/status/statusor.h"
|
|
#include "absl/strings/str_cat.h"
|
|
#include "absl/strings/str_join.h"
|
|
#include "absl/strings/string_view.h"
|
|
#include "absl/time/clock.h"
|
|
#include "absl/time/time.h"
|
|
#include "absl/types/span.h"
|
|
#include "ortools/base/protoutil.h"
|
|
#include "ortools/base/status_macros.h"
|
|
#include "ortools/math_opt/callback.pb.h"
|
|
#include "ortools/math_opt/core/empty_bounds.h"
|
|
#include "ortools/math_opt/core/inverted_bounds.h"
|
|
#include "ortools/math_opt/core/math_opt_proto_utils.h"
|
|
#include "ortools/math_opt/core/solver_interface.h"
|
|
#include "ortools/math_opt/core/sparse_submatrix.h"
|
|
#include "ortools/math_opt/core/sparse_vector_view.h"
|
|
#include "ortools/math_opt/infeasible_subsystem.pb.h"
|
|
#include "ortools/math_opt/model.pb.h"
|
|
#include "ortools/math_opt/model_parameters.pb.h"
|
|
#include "ortools/math_opt/model_update.pb.h"
|
|
#include "ortools/math_opt/parameters.pb.h"
|
|
#include "ortools/math_opt/result.pb.h"
|
|
#include "ortools/math_opt/solution.pb.h"
|
|
#include "ortools/math_opt/solvers/glpk.pb.h"
|
|
#include "ortools/math_opt/solvers/glpk/gap.h"
|
|
#include "ortools/math_opt/solvers/glpk/glpk_sparse_vector.h"
|
|
#include "ortools/math_opt/solvers/glpk/rays.h"
|
|
#include "ortools/math_opt/solvers/message_callback_data.h"
|
|
#include "ortools/math_opt/sparse_containers.pb.h"
|
|
#include "ortools/math_opt/validators/callback_validator.h"
|
|
#include "ortools/port/proto_utils.h"
|
|
#include "ortools/third_party_solvers/glpk/glpk_env_deleter.h"
|
|
#include "ortools/third_party_solvers/glpk/glpk_formatters.h"
|
|
#include "ortools/util/solve_interrupter.h"
|
|
|
|
namespace operations_research {
|
|
namespace math_opt {
|
|
|
|
namespace {
|
|
|
|
constexpr double kInf = std::numeric_limits<double>::infinity();
|
|
constexpr double kNaN = std::numeric_limits<double>::quiet_NaN();
|
|
|
|
constexpr SupportedProblemStructures kGlpkSupportedStructures = {
|
|
.integer_variables = SupportType::kSupported};
|
|
|
|
// Bounds of rows or columns.
|
|
struct Bounds {
|
|
double lower = -kInf;
|
|
double upper = kInf;
|
|
};
|
|
|
|
// Sets either a row or a column bounds. The index k is the one-based index of
|
|
// the row or the column.
|
|
//
|
|
// The Dimension type should be either GlpkSolver::Variable or
|
|
// GlpkSolver::LinearConstraints.
|
|
//
|
|
// When Dimension::IsInteger() returns true, the bounds are rounded before being
|
|
// applied which is mandatory for integer variables (solvers fail if a model
|
|
// contains non-integer bounds for integer variables). Thus the integrality of
|
|
// variables must be set/updated before calling this function.
|
|
template <typename Dimension>
|
|
void SetBounds(glp_prob* const problem, const int k, const Bounds& bounds) {
|
|
// GLPK wants integer bounds for integer variables.
|
|
const bool is_integer = Dimension::IsInteger(problem, k);
|
|
const double lb = is_integer ? std::ceil(bounds.lower) : bounds.lower;
|
|
const double ub = is_integer ? std::floor(bounds.upper) : bounds.upper;
|
|
int type = GLP_FR;
|
|
if (std::isinf(lb) && std::isinf(ub)) {
|
|
type = GLP_FR;
|
|
} else if (std::isinf(lb)) {
|
|
type = GLP_UP;
|
|
} else if (std::isinf(ub)) {
|
|
type = GLP_LO;
|
|
} else if (lb == ub) {
|
|
type = GLP_FX;
|
|
} else { // Bounds not inf and not equal.
|
|
type = GLP_DB;
|
|
}
|
|
Dimension::kSetBounds(problem, k, type, lb, ub);
|
|
}
|
|
|
|
// Gets either a row or a column bounds. The index k is the one-based index of
|
|
// the row or the column.
|
|
//
|
|
// The Dimension type should be either GlpkSolver::Variable or
|
|
// GlpkSolver::LinearConstraints.
|
|
template <typename Dimension>
|
|
Bounds GetBounds(glp_prob* const problem, const int k) {
|
|
const int type = Dimension::kGetType(problem, k);
|
|
switch (type) {
|
|
case GLP_FR:
|
|
return {};
|
|
case GLP_LO:
|
|
return {.lower = Dimension::kGetLb(problem, k)};
|
|
case GLP_UP:
|
|
return {.upper = Dimension::kGetUb(problem, k)};
|
|
case GLP_DB:
|
|
case GLP_FX:
|
|
return {.lower = Dimension::kGetLb(problem, k),
|
|
.upper = Dimension::kGetUb(problem, k)};
|
|
default:
|
|
LOG(FATAL) << type;
|
|
}
|
|
}
|
|
|
|
// Updates the bounds of either rows or columns.
|
|
//
|
|
// The Dimension type should be either GlpkSolver::Variable or
|
|
// GlpkSolver::LinearConstraints.
|
|
//
|
|
// When Dimension::IsInteger() returns true, the bounds are rounded before being
|
|
// applied which is mandatory for integer variables (solvers fail if a model
|
|
// contains non-integer bounds for integer variables). Thus the integrality of
|
|
// variables must be updated before calling this function.
|
|
template <typename Dimension>
|
|
void UpdateBounds(glp_prob* const problem, const Dimension& dimension,
|
|
const SparseDoubleVectorProto& lower_bounds_proto,
|
|
const SparseDoubleVectorProto& upper_bounds_proto) {
|
|
const auto lower_bounds = MakeView(lower_bounds_proto);
|
|
const auto upper_bounds = MakeView(upper_bounds_proto);
|
|
|
|
auto current_lower_bound = lower_bounds.begin();
|
|
auto current_upper_bound = upper_bounds.begin();
|
|
for (;;) {
|
|
// Get the smallest unvisited id from either sparse container.
|
|
std::optional<int64_t> next_id;
|
|
if (current_lower_bound != lower_bounds.end()) {
|
|
if (!next_id.has_value() || current_lower_bound->first < *next_id) {
|
|
next_id = current_lower_bound->first;
|
|
}
|
|
}
|
|
if (current_upper_bound != upper_bounds.end()) {
|
|
if (!next_id.has_value() || current_upper_bound->first < *next_id) {
|
|
next_id = current_upper_bound->first;
|
|
}
|
|
}
|
|
|
|
if (!next_id.has_value()) {
|
|
// We exhausted all collections.
|
|
break;
|
|
}
|
|
|
|
// Find the corresponding row or column.
|
|
const int row_or_col_index = dimension.id_to_index.at(*next_id);
|
|
CHECK_EQ(dimension.ids[row_or_col_index - 1], *next_id);
|
|
|
|
// Get the updated values for bounds and move the iterator for consumed
|
|
// updates.
|
|
Bounds bounds = GetBounds<Dimension>(problem,
|
|
/*k=*/row_or_col_index);
|
|
if (current_lower_bound != lower_bounds.end() &&
|
|
current_lower_bound->first == *next_id) {
|
|
bounds.lower = current_lower_bound->second;
|
|
++current_lower_bound;
|
|
}
|
|
if (current_upper_bound != upper_bounds.end() &&
|
|
current_upper_bound->first == *next_id) {
|
|
bounds.upper = current_upper_bound->second;
|
|
++current_upper_bound;
|
|
}
|
|
SetBounds<Dimension>(problem, /*k=*/row_or_col_index,
|
|
/*bounds=*/bounds);
|
|
}
|
|
|
|
CHECK(current_lower_bound == lower_bounds.end());
|
|
CHECK(current_upper_bound == upper_bounds.end());
|
|
}
|
|
|
|
// Deletes in-place the data corresponding to the indices of rows/cols.
|
|
//
|
|
// The vector of one-based indices sorted_deleted_rows_or_cols is expected to be
|
|
// sorted and its first element of index 0 is ignored (this is the GLPK
|
|
// convention).
|
|
template <typename V>
|
|
void DeleteRowOrColData(std::vector<V>& data,
|
|
absl::Span<const int> sorted_deleted_rows_or_cols) {
|
|
if (sorted_deleted_rows_or_cols.empty()) {
|
|
// Avoid looping when not necessary.
|
|
return;
|
|
}
|
|
|
|
std::size_t next_insertion_point = 0;
|
|
std::size_t current_row_or_col = 0;
|
|
for (std::size_t i = 1; i < sorted_deleted_rows_or_cols.size(); ++i) {
|
|
const int deleted_row_or_col = sorted_deleted_rows_or_cols[i];
|
|
for (; current_row_or_col + 1 < deleted_row_or_col;
|
|
++current_row_or_col, ++next_insertion_point) {
|
|
DCHECK_LT(current_row_or_col, data.size());
|
|
data[next_insertion_point] = data[current_row_or_col];
|
|
}
|
|
// Skip the deleted row/col.
|
|
++current_row_or_col;
|
|
}
|
|
for (; current_row_or_col < data.size();
|
|
++current_row_or_col, ++next_insertion_point) {
|
|
data[next_insertion_point] = data[current_row_or_col];
|
|
}
|
|
data.resize(next_insertion_point);
|
|
}
|
|
|
|
// Deletes the row or cols of the GLPK problem and returns their indices. As a
|
|
// side effect it updates dimension.ids and dimension.id_to_index.
|
|
//
|
|
// The Dimension type should be either GlpkSolver::Variable or
|
|
// GlpkSolver::LinearConstraints.
|
|
//
|
|
// The returned vector is sorted and the first element (index 0) must be ignored
|
|
// (this is the GLPK convention). It can be used with DeleteRowOrColData().
|
|
template <typename Dimension>
|
|
std::vector<int> DeleteRowsOrCols(
|
|
glp_prob* const problem, Dimension& dimension,
|
|
const google::protobuf::RepeatedField<int64_t>& deleted_ids) {
|
|
if (deleted_ids.empty()) {
|
|
// This is not only an optimization. Functions glp_del_rows() and
|
|
// glp_del_cols() fails if the number of deletion is 0.
|
|
return {};
|
|
}
|
|
|
|
// Delete GLPK rows or columns.
|
|
std::vector<int> deleted_rows_or_cols;
|
|
// Functions glp_del_rows() and glp_del_cols() only use values in ranges
|
|
// [1,n]. The first element is not used.
|
|
deleted_rows_or_cols.reserve(deleted_ids.size() + 1);
|
|
deleted_rows_or_cols.push_back(-1);
|
|
for (const int64_t deleted_id : deleted_ids) {
|
|
deleted_rows_or_cols.push_back(dimension.id_to_index.at(deleted_id));
|
|
}
|
|
Dimension::kDelElts(problem, deleted_rows_or_cols.size() - 1,
|
|
deleted_rows_or_cols.data());
|
|
|
|
// Since deleted_ids are in strictly increasing order and we allocate
|
|
// rows/cols in orders of MathOpt ids; deleted_rows_or_cols should also be
|
|
// sorted.
|
|
CHECK(
|
|
std::is_sorted(deleted_rows_or_cols.begin(), deleted_rows_or_cols.end()));
|
|
|
|
// Update the ids vector.
|
|
DeleteRowOrColData(dimension.ids, deleted_rows_or_cols);
|
|
|
|
// Update the id_to_index map.
|
|
for (const int64_t deleted_id : deleted_ids) {
|
|
CHECK(dimension.id_to_index.erase(deleted_id));
|
|
}
|
|
for (int i = 0; i < dimension.ids.size(); ++i) {
|
|
dimension.id_to_index.at(dimension.ids[i]) = i + 1;
|
|
}
|
|
|
|
return deleted_rows_or_cols;
|
|
}
|
|
|
|
// Translates the input MathOpt indices in row/column GLPK indices to use with
|
|
// glp_load_matrix(). The returned vector first element is always 0 and unused
|
|
// as it is required by GLPK (which uses one-based indices for arrays as well).
|
|
//
|
|
// The id_to_index is supposed to contain GLPK's one-based indices for rows and
|
|
// columns.
|
|
std::vector<int> MatrixIds(
|
|
const google::protobuf::RepeatedField<int64_t>& proto_ids,
|
|
const absl::flat_hash_map<int64_t, int>& id_to_index) {
|
|
std::vector<int> ids;
|
|
ids.reserve(proto_ids.size() + 1);
|
|
// First item (index 0) is not used by GLPK.
|
|
ids.push_back(0);
|
|
for (const int64_t proto_id : proto_ids) {
|
|
ids.push_back(id_to_index.at(proto_id));
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
// Returns a vector of coefficients starting at index 1 (as used by GLPK) to use
|
|
// with glp_load_matrix(). The returned vector first element is always 0 and it
|
|
// is ignored by GLPK.
|
|
std::vector<double> MatrixCoefficients(
|
|
const google::protobuf::RepeatedField<double>& proto_coeffs) {
|
|
std::vector<double> coeffs(proto_coeffs.size() + 1);
|
|
// First item (index 0) is not used by GLPK.
|
|
coeffs[0] = 0.0;
|
|
std::copy(proto_coeffs.begin(), proto_coeffs.end(), coeffs.begin() + 1);
|
|
return coeffs;
|
|
}
|
|
|
|
// Returns true if the input GLPK problem contains integer variables.
|
|
bool IsMip(glp_prob* const problem) {
|
|
const int num_vars = glp_get_num_cols(problem);
|
|
for (int v = 1; v <= num_vars; ++v) {
|
|
if (glp_get_col_kind(problem, v) != GLP_CV) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Returns true if the input GLPK problem has no rows and no cols.
|
|
bool IsEmpty(glp_prob* const problem) {
|
|
return glp_get_num_cols(problem) == 0 && glp_get_num_rows(problem) == 0;
|
|
}
|
|
|
|
// Returns a sparse vector with the values returned by the getter for the input
|
|
// ids and taking into account the provided filter.
|
|
SparseDoubleVectorProto FilteredVector(glp_prob* const problem,
|
|
const SparseVectorFilterProto& filter,
|
|
absl::Span<const int64_t> ids,
|
|
double (*const getter)(glp_prob*, int)) {
|
|
SparseDoubleVectorProto vec;
|
|
vec.mutable_ids()->Reserve(ids.size());
|
|
vec.mutable_values()->Reserve(ids.size());
|
|
|
|
SparseVectorFilterPredicate predicate(filter);
|
|
for (int i = 0; i < ids.size(); ++i) {
|
|
const double value = getter(problem, i + 1);
|
|
if (predicate.AcceptsAndUpdate(ids[i], value)) {
|
|
vec.add_ids(ids[i]);
|
|
vec.add_values(value);
|
|
}
|
|
}
|
|
return vec;
|
|
}
|
|
|
|
// Returns the ray data the corresponds to element id having the given value and
|
|
// all other elements of ids having 0.
|
|
SparseDoubleVectorProto FilteredRay(const SparseVectorFilterProto& filter,
|
|
absl::Span<const int64_t> ids,
|
|
absl::Span<const double> values) {
|
|
CHECK_EQ(ids.size(), values.size());
|
|
SparseDoubleVectorProto vec;
|
|
SparseVectorFilterPredicate predicate(filter);
|
|
for (int i = 0; i < ids.size(); ++i) {
|
|
if (predicate.AcceptsAndUpdate(ids[i], values[i])) {
|
|
vec.add_ids(ids[i]);
|
|
vec.add_values(values[i]);
|
|
}
|
|
}
|
|
return vec;
|
|
}
|
|
|
|
// Sets the parameters shared between MIP and LP and returns warnings for bad
|
|
// parameters.
|
|
//
|
|
// The input Parameters type should be glp_smcp (for LP), glp_iptcp (for LP with
|
|
// interior point) or glp_iocp (for MIP).
|
|
template <typename Parameters>
|
|
absl::Status SetSharedParameters(const SolveParametersProto& parameters,
|
|
const bool has_message_callback,
|
|
Parameters& glpk_parameters) {
|
|
std::vector<std::string> warnings;
|
|
if (parameters.has_threads() && parameters.threads() > 1) {
|
|
warnings.push_back(
|
|
absl::StrCat("GLPK only supports parameters.threads = 1; value ",
|
|
parameters.threads(), " is not supported"));
|
|
}
|
|
if (parameters.enable_output() || has_message_callback) {
|
|
glpk_parameters.msg_lev = GLP_MSG_ALL;
|
|
} else {
|
|
glpk_parameters.msg_lev = GLP_MSG_OFF;
|
|
}
|
|
if (parameters.has_node_limit()) {
|
|
warnings.push_back("parameter node_limit not supported by GLPK");
|
|
}
|
|
if (parameters.has_objective_limit()) {
|
|
warnings.push_back("parameter objective_limit not supported by GLPK");
|
|
}
|
|
if (parameters.has_best_bound_limit()) {
|
|
warnings.push_back("parameter best_bound_limit not supported by GLPK");
|
|
}
|
|
if (parameters.has_cutoff_limit()) {
|
|
warnings.push_back("parameter cutoff_limit not supported by GLPK");
|
|
}
|
|
if (parameters.has_solution_limit()) {
|
|
warnings.push_back("parameter solution_limit not supported by GLPK");
|
|
}
|
|
if (parameters.has_random_seed()) {
|
|
warnings.push_back("parameter random_seed not supported by GLPK");
|
|
}
|
|
if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
|
|
warnings.push_back("parameter cuts not supported by GLPK");
|
|
}
|
|
if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
|
|
warnings.push_back("parameter heuristics not supported by GLPK");
|
|
}
|
|
if (parameters.scaling() != EMPHASIS_UNSPECIFIED) {
|
|
warnings.push_back("parameter scaling not supported by GLPK");
|
|
}
|
|
if (!warnings.empty()) {
|
|
return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
// Sets the time limit parameter which is only supported by some LP algorithm
|
|
// and MIP, but not by interior point.
|
|
//
|
|
// The input Parameters type should be glp_smcp (for LP), or glp_iocp (for MIP).
|
|
template <typename Parameters>
|
|
void SetTimeLimitParameter(const SolveParametersProto& parameters,
|
|
Parameters& glpk_parameters) {
|
|
if (parameters.has_time_limit()) {
|
|
const int64_t time_limit_ms = absl::ToInt64Milliseconds(
|
|
util_time::DecodeGoogleApiProto(parameters.time_limit()).value());
|
|
glpk_parameters.tm_lim = static_cast<int>(std::min(
|
|
static_cast<int64_t>(std::numeric_limits<int>::max()), time_limit_ms));
|
|
}
|
|
}
|
|
|
|
// Sets the LP specific parameters and returns an InvalidArgumentError for
|
|
// invalid parameters or parameter values.
|
|
absl::Status SetLPParameters(const SolveParametersProto& parameters,
|
|
glp_smcp& glpk_parameters) {
|
|
std::vector<std::string> warnings;
|
|
if (parameters.has_iteration_limit()) {
|
|
int limit = static_cast<int>(std::min<int64_t>(
|
|
std::numeric_limits<int>::max(), parameters.iteration_limit()));
|
|
glpk_parameters.it_lim = limit;
|
|
}
|
|
switch (parameters.presolve()) {
|
|
case EMPHASIS_UNSPECIFIED:
|
|
// Keep the default.
|
|
//
|
|
// TODO(b/187027049): default is off, which may be surprising for users.
|
|
break;
|
|
case EMPHASIS_OFF:
|
|
glpk_parameters.presolve = GLP_OFF;
|
|
break;
|
|
default:
|
|
glpk_parameters.presolve = GLP_ON;
|
|
break;
|
|
}
|
|
switch (parameters.lp_algorithm()) {
|
|
case LP_ALGORITHM_UNSPECIFIED:
|
|
break;
|
|
case LP_ALGORITHM_PRIMAL_SIMPLEX:
|
|
glpk_parameters.meth = GLP_PRIMAL;
|
|
break;
|
|
case LP_ALGORITHM_DUAL_SIMPLEX:
|
|
// Use GLP_DUALP to switch back to primal simplex if the dual simplex
|
|
// fails.
|
|
//
|
|
// TODO(b/187027049): GLPK also supports GLP_DUAL to only try dual
|
|
// simplex. We should have an option to support it.
|
|
glpk_parameters.meth = GLP_DUALP;
|
|
break;
|
|
default:
|
|
warnings.push_back(absl::StrCat(
|
|
"GLPK does not support ",
|
|
operations_research::ProtoEnumToString(parameters.lp_algorithm()),
|
|
" for parameters.lp_algorithm"));
|
|
break;
|
|
}
|
|
if (!warnings.empty()) {
|
|
return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
class MipCallbackData {
|
|
public:
|
|
explicit MipCallbackData(
|
|
const SolveInterrupter* absl_nullable const interrupter)
|
|
: interrupter_(interrupter) {}
|
|
|
|
void Callback(glp_tree* const tree) {
|
|
// We only update the best bound on some specific events since it makes a
|
|
// traversal of all active nodes.
|
|
switch (glp_ios_reason(tree)) {
|
|
case GLP_ISELECT:
|
|
// The ISELECT call is the first one that happens after a node has been
|
|
// split on two sub-nodes (IBRANCH) with updated `bound`s based on the
|
|
// integer value of the branched variable.
|
|
case GLP_IBINGO:
|
|
// We found a new integer solution, the `bound` has been updated.
|
|
case GLP_IROWGEN:
|
|
// The IROWGEN call is the first one that happens on a current node
|
|
// after the relaxed problem has been solved and the `bound` field
|
|
// updated.
|
|
//
|
|
// Note that the model/cut pool changes done in IROWGEN and ICUTGEN have
|
|
// no influence on the `bound` and IROWGEN is the first call to happen.
|
|
if (const int best_node = glp_ios_best_node(tree); best_node != 0) {
|
|
best_bound_ = glp_ios_node_bound(tree, best_node);
|
|
}
|
|
break;
|
|
default:
|
|
// We can ignore:
|
|
// - IPREPRO: since the `bound` of the current node has not been
|
|
// computed yet.
|
|
// - IHEUR: since we have IBINGO if the integer solution is better.
|
|
// - ICUTGEN: since the `bound` is not updated with the rows added at
|
|
// IROWGEN so we would get the same best bound.
|
|
// - IBRANCH: since the sub-nodes will be created after that and their
|
|
// `bound`s taken into account at ISELECT.
|
|
break;
|
|
}
|
|
if (interrupter_ != nullptr && interrupter_->IsInterrupted()) {
|
|
glp_ios_terminate(tree);
|
|
interrupted_by_interrupter_ = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool HasBeenInterruptedByInterrupter() const {
|
|
return interrupted_by_interrupter_.load();
|
|
}
|
|
|
|
std::optional<double> best_bound() const { return best_bound_; }
|
|
|
|
private:
|
|
// Optional interrupter.
|
|
const SolveInterrupter* absl_nullable const interrupter_;
|
|
|
|
// Set to true if glp_ios_terminate() has been called due to the interrupter.
|
|
std::atomic<bool> interrupted_by_interrupter_ = false;
|
|
|
|
// Set on each callback that may update the best bound.
|
|
std::optional<double> best_bound_;
|
|
};
|
|
|
|
void MipCallback(glp_tree* const tree, void* const info) {
|
|
static_cast<MipCallbackData*>(info)->Callback(tree);
|
|
}
|
|
|
|
// Returns the MathOpt ids of the rows/columns with lower_bound > upper_bound.
|
|
//
|
|
// For variables we use the unrounded bounds as we don't want to return a
|
|
// failing status when rounded bounds of integer variables cross due to the
|
|
// rounding. See EmptyIntegerBoundsResult() for dealing with this case.
|
|
InvertedBounds ListInvertedBounds(
|
|
glp_prob* const problem, absl::Span<const int64_t> variable_ids,
|
|
absl::Span<const double> unrounded_variable_lower_bounds,
|
|
absl::Span<const double> unrounded_variable_upper_bounds,
|
|
absl::Span<const int64_t> linear_constraint_ids) {
|
|
InvertedBounds inverted_bounds;
|
|
|
|
const int num_cols = glp_get_num_cols(problem);
|
|
for (int c = 1; c <= num_cols; ++c) {
|
|
if (unrounded_variable_lower_bounds[c - 1] >
|
|
unrounded_variable_upper_bounds[c - 1]) {
|
|
inverted_bounds.variables.push_back(variable_ids[c - 1]);
|
|
}
|
|
}
|
|
|
|
const int num_rows = glp_get_num_rows(problem);
|
|
for (int r = 1; r <= num_rows; ++r) {
|
|
if (glp_get_row_lb(problem, r) > glp_get_row_ub(problem, r)) {
|
|
inverted_bounds.linear_constraints.push_back(
|
|
linear_constraint_ids[r - 1]);
|
|
}
|
|
}
|
|
|
|
return inverted_bounds;
|
|
}
|
|
|
|
// Returns the termination reason based on the current MIP data of the problem
|
|
// assuming that the last call to glp_intopt() returned 0 and that the model has
|
|
// not been modified since.
|
|
absl::StatusOr<TerminationProto> MipTerminationOnSuccess(
|
|
glp_prob* const problem, MipCallbackData* const mip_cb_data) {
|
|
if (mip_cb_data == nullptr) {
|
|
return absl::InternalError(
|
|
"MipTerminationOnSuccess() called with nullptr mip_cb_data");
|
|
}
|
|
const int status = glp_mip_status(problem);
|
|
const bool is_maximize = glp_get_obj_dir(problem) == GLP_MAX;
|
|
switch (status) {
|
|
case GLP_OPT:
|
|
case GLP_FEAS: {
|
|
const double objective_value = glp_mip_obj_val(problem);
|
|
if (status == GLP_OPT) {
|
|
// Note that here we don't use MipCallbackData->best_bound(), even if
|
|
// set, as if the Gap was used to interrupt the solve GLPK is supposed
|
|
// to return GLP_EMIPGAP and not 0. And thus we should not go through
|
|
// this code path if the Gap limit is used.
|
|
return OptimalTerminationProto(objective_value, objective_value);
|
|
}
|
|
return FeasibleTerminationProto(
|
|
is_maximize, LIMIT_UNDETERMINED, objective_value,
|
|
mip_cb_data->best_bound(), "glp_mip_status() returned GLP_FEAS");
|
|
}
|
|
case GLP_NOFEAS:
|
|
// According to InfeasibleTerminationProto()'s documentation: "the
|
|
// convention for infeasible MIPs is that dual_feasibility_status is
|
|
// feasible".
|
|
return InfeasibleTerminationProto(
|
|
is_maximize, /*dual_feasibility_status=*/FEASIBILITY_STATUS_FEASIBLE);
|
|
default:
|
|
return absl::InternalError(
|
|
absl::StrCat("glp_intopt() returned 0 but glp_mip_status()"
|
|
"returned the unexpected value ",
|
|
SolutionStatusString(status)));
|
|
}
|
|
}
|
|
|
|
// Returns the termination reason based on the current interior point data of
|
|
// the problem assuming that the last call to glp_interior() returned 0 and that
|
|
// the model has not been modified since.
|
|
absl::StatusOr<TerminationProto> InteriorTerminationOnSuccess(
|
|
glp_prob* const problem, MipCallbackData*) {
|
|
const int status = glp_ipt_status(problem);
|
|
const bool is_maximize = glp_get_obj_dir(problem) == GLP_MAX;
|
|
switch (status) {
|
|
case GLP_OPT: {
|
|
const double objective_value = glp_ipt_obj_val(problem);
|
|
// TODO(b/290359402): here we assume that the objective value of the dual
|
|
// is exactly the same as the one of the primal. This may not be true as
|
|
// some tolerance may apply.
|
|
return OptimalTerminationProto(objective_value, objective_value);
|
|
}
|
|
case GLP_INFEAS:
|
|
return NoSolutionFoundTerminationProto(
|
|
is_maximize, LIMIT_UNDETERMINED,
|
|
/*optional_dual_objective=*/std::nullopt,
|
|
"glp_ipt_status() returned GLP_INFEAS");
|
|
case GLP_NOFEAS:
|
|
// Documentation in glpapi08.c for glp_ipt_status says this status means
|
|
// "no feasible solution exists", but the Reference Manual for GLPK
|
|
// Version 5.0 clarifies that it means "no feasible primal-dual solution
|
|
// exists." (See also the comment in glpipm.c when ipm_solve returns 1).
|
|
// Hence, GLP_NOFEAS corresponds to the solver claiming that either the
|
|
// primal problem, the dual problem (or both) are infeasible. Under this
|
|
// condition if the primal is feasible, then the dual must be infeasible
|
|
// and hence the primal is unbounded.
|
|
return InfeasibleOrUnboundedTerminationProto(
|
|
is_maximize,
|
|
/*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED);
|
|
default:
|
|
return absl::InternalError(
|
|
absl::StrCat("glp_interior() returned 0 but glp_ipt_status()"
|
|
"returned the unexpected value ",
|
|
SolutionStatusString(status)));
|
|
}
|
|
}
|
|
|
|
// Returns the termination reason based on the current interior point data of
|
|
// the problem assuming that the last call to glp_simplex() returned 0 and that
|
|
// the model has not been modified since.
|
|
absl::StatusOr<TerminationProto> SimplexTerminationOnSuccess(
|
|
glp_prob* const problem, MipCallbackData*) {
|
|
// Here we don't use glp_get_status() since it is biased towards the primal
|
|
// simplex algorithm. For example if the dual simplex returns GLP_NOFEAS for
|
|
// the dual and GLP_INFEAS for the primal then glp_get_status() returns
|
|
// GLP_INFEAS. This is misleading since the dual successfully determined that
|
|
// the problem was dual infeasible. So here we use the two statuses of the
|
|
// primal and the dual to get a better result (the glp_get_status() only
|
|
// combines them anyway, it does not have any other benefit).
|
|
const int prim_status = glp_get_prim_stat(problem);
|
|
const int dual_status = glp_get_dual_stat(problem);
|
|
const bool is_maximize = glp_get_obj_dir(problem) == GLP_MAX;
|
|
|
|
// Returns a status error indicating that glp_get_dual_stat() returned an
|
|
// unexpected value.
|
|
const auto unexpected_dual_stat = [&]() -> absl::Status {
|
|
return util::InternalErrorBuilder()
|
|
<< "glp_simplex() returned 0 but glp_get_dual_stat() returned the "
|
|
"unexpected value "
|
|
<< SolutionStatusString(dual_status)
|
|
<< " while glp_get_prim_stat() returned "
|
|
<< SolutionStatusString(prim_status);
|
|
};
|
|
|
|
switch (prim_status) {
|
|
case GLP_FEAS:
|
|
switch (dual_status) {
|
|
case GLP_FEAS: {
|
|
// Dual feasibility here means that the solution is dual feasible
|
|
// (correct signs of the residual costs) and that the complementary
|
|
// slackness condition are respected. Hence the solution is optimal.
|
|
const double objective_value = glp_get_obj_val(problem);
|
|
return OptimalTerminationProto(objective_value, objective_value);
|
|
}
|
|
case GLP_NOFEAS:
|
|
return UnboundedTerminationProto(is_maximize);
|
|
case GLP_INFEAS:
|
|
default:
|
|
return unexpected_dual_stat();
|
|
}
|
|
case GLP_INFEAS:
|
|
switch (dual_status) {
|
|
case GLP_NOFEAS:
|
|
return InfeasibleOrUnboundedTerminationProto(
|
|
is_maximize,
|
|
/*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE);
|
|
case GLP_FEAS:
|
|
case GLP_INFEAS:
|
|
default:
|
|
return unexpected_dual_stat();
|
|
}
|
|
case GLP_NOFEAS:
|
|
switch (dual_status) {
|
|
case GLP_FEAS:
|
|
// Dual being feasible (GLP_FEAS) here would lead to dual unbounded;
|
|
// but this does not exist as a reason.
|
|
return InfeasibleTerminationProto(
|
|
is_maximize,
|
|
/*dual_feasibility_status=*/FEASIBILITY_STATUS_FEASIBLE);
|
|
case GLP_INFEAS:
|
|
return InfeasibleTerminationProto(
|
|
is_maximize,
|
|
/*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED);
|
|
return unexpected_dual_stat();
|
|
case GLP_NOFEAS:
|
|
// If both the primal and dual are proven infeasible (GLP_NOFEAS), the
|
|
// primal wins. Maybe GLPK does never return that though since it
|
|
// implements either primal or dual simplex algorithm but does not
|
|
// combine both of them.
|
|
return InfeasibleTerminationProto(
|
|
is_maximize,
|
|
/*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE);
|
|
default:
|
|
return unexpected_dual_stat();
|
|
}
|
|
default:
|
|
return absl::InternalError(
|
|
absl::StrCat("glp_simplex() returned 0 but glp_get_prim_stat() "
|
|
"returned the unexpected value ",
|
|
SolutionStatusString(prim_status)));
|
|
}
|
|
}
|
|
|
|
// Function called by BuildTermination() when the return code of the solve
|
|
// function is 0.
|
|
//
|
|
// Parameter mip_cb_data is not nullptr iff glp_intopt() was used.
|
|
using TerminationOnSuccessFn = std::function<absl::StatusOr<TerminationProto>(
|
|
glp_prob* problem, MipCallbackData* const mip_cb_data)>;
|
|
|
|
// Returns the termination reason based on the return code rc of calling fn_name
|
|
// function which is glp_simplex(), glp_interior() or glp_intopt().
|
|
//
|
|
// For return code 0 which means successful solve, the function
|
|
// termination_on_success is called to build the termination. Other return
|
|
// values (errors) are dealt with specifically.
|
|
//
|
|
// For glp_intopt(), the optional mip_cb_data is used to test for interruption
|
|
// and the LIMIT_INTERRUPTED is set if the interrupter has been triggered (even
|
|
// if the return code is 0). It is also used to get the dual bound. The
|
|
// gap_limit must also be set to the gap limit parameter.
|
|
//
|
|
// When a primal feasible solution exists, feasible_solution_objective_value
|
|
// must be set with its objective. When this variable is unset this function
|
|
// assumes no such solution exists.
|
|
absl::StatusOr<TerminationProto> BuildTermination(
|
|
glp_prob* const problem, const absl::string_view fn_name, const int rc,
|
|
const TerminationOnSuccessFn termination_on_success,
|
|
MipCallbackData* const mip_cb_data,
|
|
const std::optional<double> feasible_solution_objective_value,
|
|
const double gap_limit) {
|
|
const bool is_maximize = glp_get_obj_dir(problem) == GLP_MAX;
|
|
if (mip_cb_data != nullptr &&
|
|
mip_cb_data->HasBeenInterruptedByInterrupter()) {
|
|
return LimitTerminationProto(is_maximize, LIMIT_INTERRUPTED,
|
|
feasible_solution_objective_value);
|
|
}
|
|
|
|
// TODO(b/187027049): see if GLP_EOBJLL and GLP_EOBJUL should be handled with
|
|
// dual simplex.
|
|
switch (rc) {
|
|
case 0:
|
|
return termination_on_success(problem, mip_cb_data);
|
|
case GLP_EBOUND: {
|
|
// GLP_EBOUND is returned when a variable or a constraint has the GLP_DB
|
|
// bounds type and lower_bound >= upper_bound. The code in this file makes
|
|
// sure we don't use GLP_DB but GLP_FX when lower_bound == upper_bound
|
|
// thus we expect GLP_EBOUND only when lower_bound > upper_bound. This
|
|
// should never happen as we call ListInvertedBounds() and
|
|
// EmptyIntegerBoundsResult() before we call GLPK. Thus we don't expect
|
|
// GLP_EBOUND to happen.
|
|
return util::InternalErrorBuilder()
|
|
<< fn_name << "() returned `" << ReturnCodeString(rc)
|
|
<< "` but the model does not contain variables with inverted "
|
|
"bounds";
|
|
}
|
|
case GLP_EITLIM:
|
|
return LimitTerminationProto(is_maximize, LIMIT_ITERATION,
|
|
feasible_solution_objective_value);
|
|
case GLP_ETMLIM:
|
|
return LimitTerminationProto(is_maximize, LIMIT_TIME,
|
|
feasible_solution_objective_value);
|
|
case GLP_EMIPGAP: {
|
|
if (!feasible_solution_objective_value.has_value()) {
|
|
return util::InternalErrorBuilder()
|
|
<< fn_name << "() returned `" << ReturnCodeString(rc)
|
|
<< "` but glp_mip_status() returned "
|
|
<< SolutionStatusString(glp_mip_status(problem));
|
|
}
|
|
if (!mip_cb_data) {
|
|
return util::InternalErrorBuilder()
|
|
<< fn_name << "() returned `" << ReturnCodeString(rc)
|
|
<< "` but there is no MipCallbackData";
|
|
}
|
|
const double objective_value = feasible_solution_objective_value.value();
|
|
// Here we expect mip_cb_data->best_bound() to always be set. If this is
|
|
// not the case we use a worst estimation of the dual bound.
|
|
return OptimalTerminationProto(
|
|
objective_value,
|
|
mip_cb_data->best_bound().value_or(
|
|
WorstGLPKDualBound(is_maximize, objective_value, gap_limit)));
|
|
}
|
|
case GLP_ESTOP:
|
|
return LimitTerminationProto(is_maximize, LIMIT_INTERRUPTED,
|
|
feasible_solution_objective_value);
|
|
case GLP_ENOPFS:
|
|
// With presolve on, this error is returned if the LP has no feasible
|
|
// solution.
|
|
return InfeasibleTerminationProto(
|
|
is_maximize,
|
|
/*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED);
|
|
case GLP_ENODFS:
|
|
// With presolve on, this error is returned if the LP has no dual
|
|
// feasible solution.
|
|
return InfeasibleOrUnboundedTerminationProto(
|
|
is_maximize,
|
|
/*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE);
|
|
case GLP_ENOCVG:
|
|
// Very slow convergence/divergence (for glp_interior).
|
|
return LimitTerminationProto(is_maximize, LIMIT_SLOW_PROGRESS,
|
|
feasible_solution_objective_value);
|
|
case GLP_EINSTAB:
|
|
// Numeric stability solving Newtonian system (for glp_interior).
|
|
return TerminateForReason(
|
|
is_maximize, TERMINATION_REASON_NUMERICAL_ERROR,
|
|
absl::StrCat(fn_name, "() returned ", ReturnCodeString(rc),
|
|
" which means that there is a numeric stability issue "
|
|
"solving Newtonian system"));
|
|
default:
|
|
return util::InternalErrorBuilder()
|
|
<< fn_name
|
|
<< "() returned unexpected value: " << ReturnCodeString(rc);
|
|
}
|
|
}
|
|
|
|
// Callback for glp_term_hook().
|
|
//
|
|
// It expects `info` to be a pointer on a TermHookData.
|
|
int TermHook(void* const info, const char* const message) {
|
|
static_cast<BufferedMessageCallback*>(info)->OnMessage(message);
|
|
|
|
// Returns non-zero to remove any terminal output.
|
|
return 1;
|
|
}
|
|
|
|
// Returns the objective offset. This is used as a placeholder for function
|
|
// returning the objective value for solve method not supporting solving empty
|
|
// models (glp_exact() and glp_interior()).
|
|
double OffsetOnlyObjVal(glp_prob* const problem) {
|
|
return glp_get_obj_coef(problem, 0);
|
|
}
|
|
|
|
// Returns GLP_OPT. This is used as a placeholder for function returning the
|
|
// status for solve method not supporting solving empty models (glp_exact() and
|
|
// glp_interior()).
|
|
int OptStatus(glp_prob*) { return GLP_OPT; }
|
|
|
|
} // namespace
|
|
|
|
absl::StatusOr<std::unique_ptr<SolverInterface>> GlpkSolver::New(
|
|
const ModelProto& model, const InitArgs& /*init_args*/) {
|
|
RETURN_IF_ERROR(ModelIsSupported(model, kGlpkSupportedStructures, "GLPK"));
|
|
return absl::WrapUnique(new GlpkSolver(model));
|
|
}
|
|
|
|
GlpkSolver::GlpkSolver(const ModelProto& model)
|
|
: thread_id_(std::this_thread::get_id()), problem_(glp_create_prob()) {
|
|
// Make sure glp_free_env() is called at the exit of the current thread.
|
|
SetupGlpkEnvAutomaticDeletion();
|
|
|
|
glp_set_prob_name(problem_, TruncateAndQuoteGLPKName(model.name()).c_str());
|
|
|
|
AddVariables(model.variables());
|
|
|
|
AddLinearConstraints(model.linear_constraints());
|
|
|
|
glp_set_obj_dir(problem_, model.objective().maximize() ? GLP_MAX : GLP_MIN);
|
|
// Glpk uses index 0 for the "shift" of the objective.
|
|
glp_set_obj_coef(problem_, 0, model.objective().offset());
|
|
for (const auto [v, coeff] :
|
|
MakeView(model.objective().linear_coefficients())) {
|
|
const int col_index = variables_.id_to_index.at(v);
|
|
CHECK_EQ(variables_.ids[col_index - 1], v);
|
|
glp_set_obj_coef(problem_, col_index, coeff);
|
|
}
|
|
|
|
const SparseDoubleMatrixProto& proto_matrix =
|
|
model.linear_constraint_matrix();
|
|
glp_load_matrix(
|
|
problem_, proto_matrix.row_ids_size(),
|
|
MatrixIds(proto_matrix.row_ids(), linear_constraints_.id_to_index).data(),
|
|
MatrixIds(proto_matrix.column_ids(), variables_.id_to_index).data(),
|
|
MatrixCoefficients(proto_matrix.coefficients()).data());
|
|
}
|
|
|
|
GlpkSolver::~GlpkSolver() {
|
|
// Here we simply log an error but glp_delete_prob() should crash with an
|
|
// error like: `glp_free: memory allocation error`.
|
|
if (const absl::Status status = CheckCurrentThread(); !status.ok()) {
|
|
LOG(ERROR) << status;
|
|
}
|
|
glp_delete_prob(problem_);
|
|
}
|
|
|
|
namespace {
|
|
|
|
ProblemStatusProto GetMipProblemStatusProto(const int rc, const int mip_status,
|
|
const bool has_finite_dual_bound) {
|
|
ProblemStatusProto problem_status;
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
|
|
|
|
switch (rc) {
|
|
case GLP_ENOPFS:
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
|
|
return problem_status;
|
|
case GLP_ENODFS:
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE);
|
|
return problem_status;
|
|
}
|
|
|
|
switch (mip_status) {
|
|
case GLP_OPT:
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
|
|
return problem_status;
|
|
case GLP_FEAS:
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
|
|
break;
|
|
case GLP_NOFEAS:
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
|
|
break;
|
|
}
|
|
|
|
if (has_finite_dual_bound) {
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
|
|
}
|
|
return problem_status;
|
|
}
|
|
|
|
absl::StatusOr<FeasibilityStatusProto> TranslateProblemStatus(
|
|
const int glpk_status, const absl::string_view fn_name) {
|
|
switch (glpk_status) {
|
|
case GLP_FEAS:
|
|
return FEASIBILITY_STATUS_FEASIBLE;
|
|
case GLP_NOFEAS:
|
|
return FEASIBILITY_STATUS_INFEASIBLE;
|
|
case GLP_INFEAS:
|
|
case GLP_UNDEF:
|
|
return FEASIBILITY_STATUS_UNDETERMINED;
|
|
default:
|
|
return absl::InternalError(
|
|
absl::StrCat(fn_name, " returned the unexpected value ",
|
|
SolutionStatusString(glpk_status)));
|
|
}
|
|
}
|
|
|
|
// Builds problem status from:
|
|
// * glp_simplex_rc: code returned by glp_simplex.
|
|
// * glpk_primal_status: primal status returned by glp_get_prim_stat.
|
|
// * glpk_dual_status: dual status returned by glp_get_dual_stat.
|
|
absl::StatusOr<ProblemStatusProto> GetSimplexProblemStatusProto(
|
|
const int glp_simplex_rc, const int glpk_primal_status,
|
|
const int glpk_dual_status) {
|
|
ProblemStatusProto problem_status;
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
|
|
|
|
switch (glp_simplex_rc) {
|
|
case GLP_ENOPFS:
|
|
// LP presolver concluded primal infeasibility.
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
|
|
return problem_status;
|
|
case GLP_ENODFS:
|
|
// LP presolver concluded dual infeasibility.
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE);
|
|
return problem_status;
|
|
default: {
|
|
// Get primal status from basic solution.
|
|
ASSIGN_OR_RETURN(
|
|
const FeasibilityStatusProto primal_status,
|
|
TranslateProblemStatus(glpk_primal_status, "glp_get_prim_stat"));
|
|
problem_status.set_primal_status(primal_status);
|
|
|
|
// Get dual status from basic solution.
|
|
ASSIGN_OR_RETURN(
|
|
const FeasibilityStatusProto dual_status,
|
|
TranslateProblemStatus(glpk_dual_status, "glp_get_dual_stat"));
|
|
problem_status.set_dual_status(dual_status);
|
|
return problem_status;
|
|
}
|
|
}
|
|
}
|
|
|
|
absl::StatusOr<ProblemStatusProto> GetBarrierProblemStatusProto(
|
|
const int glp_interior_rc, const int ipt_status) {
|
|
ProblemStatusProto problem_status;
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
|
|
|
|
switch (glp_interior_rc) {
|
|
case 0:
|
|
// We only use the glp_ipt_status() result when glp_interior() returned 0.
|
|
switch (ipt_status) {
|
|
case GLP_OPT:
|
|
problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
|
|
problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
|
|
return problem_status;
|
|
case GLP_INFEAS:
|
|
return problem_status;
|
|
case GLP_NOFEAS:
|
|
problem_status.set_primal_or_dual_infeasible(true);
|
|
return problem_status;
|
|
case GLP_UNDEF:
|
|
return problem_status;
|
|
default:
|
|
return absl::InternalError(
|
|
absl::StrCat("glp_ipt_status returned the unexpected value ",
|
|
SolutionStatusString(ipt_status)));
|
|
}
|
|
default:
|
|
return problem_status;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
absl::StatusOr<SolveResultProto> GlpkSolver::Solve(
|
|
const SolveParametersProto& parameters,
|
|
const ModelSolveParametersProto& model_parameters,
|
|
MessageCallback message_cb,
|
|
const CallbackRegistrationProto& callback_registration,
|
|
const Callback /*cb*/,
|
|
const SolveInterrupter* absl_nullable const interrupter) {
|
|
RETURN_IF_ERROR(ModelSolveParametersAreSupported(
|
|
model_parameters, kGlpkSupportedStructures, "GLPK"));
|
|
RETURN_IF_ERROR(CheckCurrentThread());
|
|
|
|
const absl::Time start = absl::Now();
|
|
|
|
const auto set_solve_time =
|
|
[&start](SolveResultProto& result) -> absl::Status {
|
|
RETURN_IF_ERROR(util_time::EncodeGoogleApiProto(
|
|
absl::Now() - start,
|
|
result.mutable_solve_stats()->mutable_solve_time()))
|
|
<< "failed to set SolveResultProto.solve_stats.solve_time";
|
|
return absl::OkStatus();
|
|
};
|
|
|
|
RETURN_IF_ERROR(
|
|
ListInvertedBounds(
|
|
problem_,
|
|
/*variable_ids=*/variables_.ids,
|
|
/*unrounded_variable_lower_bounds=*/variables_.unrounded_lower_bounds,
|
|
/*unrounded_variable_upper_bounds=*/variables_.unrounded_upper_bounds,
|
|
/*linear_constraint_ids=*/linear_constraints_.ids)
|
|
.ToStatus());
|
|
|
|
// Deal with empty integer bounds that result in inverted bounds due to bounds
|
|
// rounding.
|
|
{ // Limit scope of `result`.
|
|
std::optional<SolveResultProto> result = EmptyIntegerBoundsResult();
|
|
if (result.has_value()) {
|
|
RETURN_IF_ERROR(set_solve_time(result.value()));
|
|
return std::move(result).value();
|
|
}
|
|
}
|
|
|
|
RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration,
|
|
/*supported_events=*/{}));
|
|
|
|
BufferedMessageCallback term_hook_data(std::move(message_cb));
|
|
if (term_hook_data.has_user_message_callback()) {
|
|
// Note that glp_term_hook() uses get_env_ptr() that relies on thread local
|
|
// storage to have a different environment per thread. Thus using
|
|
// glp_term_hook() is thread-safe.
|
|
//
|
|
glp_term_hook(TermHook, &term_hook_data);
|
|
}
|
|
|
|
// We must reset the term hook when before exiting or before flushing the last
|
|
// unfinished line.
|
|
auto message_cb_cleanup = absl::MakeCleanup([&]() {
|
|
if (term_hook_data.has_user_message_callback()) {
|
|
glp_term_hook(/*func=*/nullptr, /*info=*/nullptr);
|
|
term_hook_data.Flush();
|
|
}
|
|
});
|
|
|
|
SolveResultProto result;
|
|
|
|
const bool is_mip = IsMip(problem_);
|
|
|
|
// We need to use different functions depending on the solve function we used
|
|
// (or placeholders if no solve function was called in case of empty models).
|
|
int (*get_prim_stat)(glp_prob*) = nullptr;
|
|
double (*obj_val)(glp_prob*) = nullptr;
|
|
double (*col_val)(glp_prob*, int) = nullptr;
|
|
|
|
int (*get_dual_stat)(glp_prob*) = nullptr;
|
|
double (*row_dual)(glp_prob*, int) = nullptr;
|
|
double (*col_dual)(glp_prob*, int) = nullptr;
|
|
|
|
const bool maximize = glp_get_obj_dir(problem_) == GLP_MAX;
|
|
double best_dual_bound = maximize ? kInf : -kInf;
|
|
|
|
// Here we use different solve algorithms depending on the type of problem:
|
|
// * For MIPs: glp_intopt()
|
|
// * For LPs:
|
|
// * glp_interior() when using BARRIER LP algorithm
|
|
// * glp_simplex() for other LP algorithms.
|
|
//
|
|
// These solve algorithms have dedicated data segments in glp_prob which use
|
|
// different access functions to get the solution; hence each branch will set
|
|
// the corresponding function pointers accordingly. They also use a custom
|
|
// struct for parameters that will be initialized and passed to the algorithm.
|
|
if (is_mip) {
|
|
get_prim_stat = glp_mip_status;
|
|
obj_val = glp_mip_obj_val;
|
|
col_val = glp_mip_col_val;
|
|
|
|
glp_iocp glpk_parameters;
|
|
glp_init_iocp(&glpk_parameters);
|
|
RETURN_IF_ERROR(SetSharedParameters(
|
|
parameters, term_hook_data.has_user_message_callback(),
|
|
glpk_parameters));
|
|
SetTimeLimitParameter(parameters, glpk_parameters);
|
|
// TODO(b/187027049): glp_intopt with presolve off requires an optional
|
|
// solution of the relaxed problem. Here we simply always enable pre-solve
|
|
// but we should support disabling the presolve and call glp_simplex() in
|
|
// that case.
|
|
glpk_parameters.presolve = GLP_ON;
|
|
if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
|
|
return util::InvalidArgumentErrorBuilder()
|
|
<< "parameter presolve not supported by GLPK for MIP";
|
|
}
|
|
if (parameters.has_relative_gap_tolerance()) {
|
|
glpk_parameters.mip_gap = parameters.relative_gap_tolerance();
|
|
}
|
|
if (parameters.has_absolute_gap_tolerance()) {
|
|
return util::InvalidArgumentErrorBuilder()
|
|
<< "parameter absolute_gap_tolerance not supported by GLPK "
|
|
"(relative_gap_tolerance is supported)";
|
|
}
|
|
if (parameters.has_iteration_limit()) {
|
|
return util::InvalidArgumentErrorBuilder()
|
|
<< "parameter iteration_limit not supported by GLPK for MIP";
|
|
}
|
|
if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
|
|
return util::InvalidArgumentErrorBuilder()
|
|
<< "parameter lp_algorithm not supported by GLPK for MIP";
|
|
}
|
|
MipCallbackData mip_cb_data(interrupter);
|
|
glpk_parameters.cb_func = MipCallback;
|
|
glpk_parameters.cb_info = &mip_cb_data;
|
|
const int rc = glp_intopt(problem_, &glpk_parameters);
|
|
const int mip_status = glp_mip_status(problem_);
|
|
const bool has_feasible_solution =
|
|
mip_status == GLP_OPT || mip_status == GLP_FEAS;
|
|
const std::optional<double> feasible_solution_objective_value =
|
|
has_feasible_solution ? std::make_optional(glp_mip_obj_val(problem_))
|
|
: std::nullopt;
|
|
ASSIGN_OR_RETURN(
|
|
*result.mutable_termination(),
|
|
BuildTermination(problem_, "glp_intopt", rc, MipTerminationOnSuccess,
|
|
&mip_cb_data, feasible_solution_objective_value,
|
|
glpk_parameters.mip_gap));
|
|
if (mip_cb_data.best_bound().has_value()) {
|
|
best_dual_bound = *mip_cb_data.best_bound();
|
|
}
|
|
*result.mutable_solve_stats()->mutable_problem_status() =
|
|
GetMipProblemStatusProto(rc, mip_status,
|
|
std::isfinite(best_dual_bound));
|
|
} else {
|
|
if (parameters.lp_algorithm() == LP_ALGORITHM_BARRIER) {
|
|
get_prim_stat = glp_ipt_status;
|
|
obj_val = glp_ipt_obj_val;
|
|
col_val = glp_ipt_col_prim;
|
|
|
|
get_dual_stat = glp_ipt_status;
|
|
row_dual = glp_ipt_row_dual;
|
|
col_dual = glp_ipt_col_dual;
|
|
|
|
glp_iptcp glpk_parameters;
|
|
glp_init_iptcp(&glpk_parameters);
|
|
if (parameters.has_time_limit()) {
|
|
return absl::InvalidArgumentError(
|
|
"parameter time_limit not supported by GLPK for interior point "
|
|
"algorithm");
|
|
}
|
|
RETURN_IF_ERROR(SetSharedParameters(
|
|
parameters, term_hook_data.has_user_message_callback(),
|
|
glpk_parameters));
|
|
|
|
// glp_interior() does not support being called with an empty model and
|
|
// returns GLP_EFAIL. Thus we use placeholders in that case.
|
|
//
|
|
// TODO(b/259557110): the emptiness is tested by glp_interior() *after*
|
|
// some pre-processing (including removing fixed variables). The current
|
|
// IsEmpty() is thus not good enough to deal with all cases.
|
|
if (IsEmpty(problem_)) {
|
|
get_prim_stat = OptStatus;
|
|
get_dual_stat = OptStatus;
|
|
obj_val = OffsetOnlyObjVal;
|
|
const double objective_value = OffsetOnlyObjVal(problem_);
|
|
*result.mutable_termination() = OptimalTerminationProto(
|
|
/*finite_primal_objective=*/objective_value,
|
|
/*dual_objective=*/objective_value,
|
|
"glp_interior() not called since the model is empty");
|
|
result.mutable_solve_stats()
|
|
->mutable_problem_status()
|
|
->set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
|
|
result.mutable_solve_stats()->mutable_problem_status()->set_dual_status(
|
|
FEASIBILITY_STATUS_FEASIBLE);
|
|
} else {
|
|
// TODO(b/187027049): add solver specific parameters for
|
|
// glp_iptcp.ord_alg.
|
|
const int glp_interior_rc = glp_interior(problem_, &glpk_parameters);
|
|
const int ipt_status = glp_ipt_status(problem_);
|
|
const bool has_feasible_solution = ipt_status == GLP_OPT;
|
|
const std::optional<double> feasible_solution_objective_value =
|
|
has_feasible_solution
|
|
? std::make_optional(glp_ipt_obj_val(problem_))
|
|
: std::nullopt;
|
|
ASSIGN_OR_RETURN(
|
|
*result.mutable_termination(),
|
|
BuildTermination(problem_, "glp_interior", glp_interior_rc,
|
|
InteriorTerminationOnSuccess,
|
|
/*mip_cb_data=*/nullptr,
|
|
feasible_solution_objective_value,
|
|
/*gap_limit=*/kNaN));
|
|
ASSIGN_OR_RETURN(
|
|
*result.mutable_solve_stats()->mutable_problem_status(),
|
|
GetBarrierProblemStatusProto(/*glp_interior_rc=*/glp_interior_rc,
|
|
/*ipt_status=*/ipt_status));
|
|
}
|
|
} else {
|
|
get_prim_stat = glp_get_prim_stat;
|
|
obj_val = glp_get_obj_val;
|
|
col_val = glp_get_col_prim;
|
|
|
|
get_dual_stat = glp_get_dual_stat;
|
|
row_dual = glp_get_row_dual;
|
|
col_dual = glp_get_col_dual;
|
|
|
|
glp_smcp glpk_parameters;
|
|
glp_init_smcp(&glpk_parameters);
|
|
RETURN_IF_ERROR(SetSharedParameters(
|
|
parameters, term_hook_data.has_user_message_callback(),
|
|
glpk_parameters));
|
|
SetTimeLimitParameter(parameters, glpk_parameters);
|
|
RETURN_IF_ERROR(SetLPParameters(parameters, glpk_parameters));
|
|
|
|
// TODO(b/187027049): add option to use glp_exact().
|
|
const int glp_simplex_rc = glp_simplex(problem_, &glpk_parameters);
|
|
const int prim_stat = glp_get_prim_stat(problem_);
|
|
const bool has_feasible_solution = prim_stat == GLP_FEAS;
|
|
const std::optional<double> feasible_solution_objective_value =
|
|
has_feasible_solution ? std::make_optional(glp_get_obj_val(problem_))
|
|
: std::nullopt;
|
|
ASSIGN_OR_RETURN(*result.mutable_termination(),
|
|
BuildTermination(problem_, "glp_simplex", glp_simplex_rc,
|
|
SimplexTerminationOnSuccess,
|
|
/*mip_cb_data=*/nullptr,
|
|
feasible_solution_objective_value,
|
|
/*gap_limit=*/kNaN));
|
|
|
|
// If the primal is proven infeasible and the dual is feasible, the dual
|
|
// is unbounded. Thus we can compute a better dual bound rather than the
|
|
// default value.
|
|
if (glp_get_prim_stat(problem_) == GLP_NOFEAS &&
|
|
glp_get_dual_stat(problem_) == GLP_FEAS) {
|
|
best_dual_bound = maximize ? -kInf : +kInf;
|
|
}
|
|
|
|
ASSIGN_OR_RETURN(*result.mutable_solve_stats()->mutable_problem_status(),
|
|
GetSimplexProblemStatusProto(
|
|
/*glp_simplex_rc=*/glp_simplex_rc,
|
|
/*glpk_primal_status=*/prim_stat,
|
|
/*glpk_dual_status=*/glp_get_dual_stat(problem_)));
|
|
VLOG(1) << "glp_get_status: "
|
|
<< SolutionStatusString(glp_get_status(problem_))
|
|
<< " glp_get_prim_stat: " << SolutionStatusString(prim_stat)
|
|
<< " glp_get_dual_stat: "
|
|
<< SolutionStatusString(glp_get_dual_stat(problem_));
|
|
}
|
|
}
|
|
|
|
// Unregister the callback and flush the potential last unfinished line.
|
|
std::move(message_cb_cleanup).Invoke();
|
|
|
|
switch (result.termination().reason()) {
|
|
case TERMINATION_REASON_OPTIMAL:
|
|
case TERMINATION_REASON_FEASIBLE:
|
|
result.mutable_solve_stats()->set_best_primal_bound(obj_val(problem_));
|
|
break;
|
|
case TERMINATION_REASON_UNBOUNDED:
|
|
// Here we can't use obj_val(problem_) as it would be a finite value of
|
|
// the feasible solution found.
|
|
result.mutable_solve_stats()->set_best_primal_bound(maximize ? +kInf
|
|
: -kInf);
|
|
break;
|
|
default:
|
|
result.mutable_solve_stats()->set_best_primal_bound(maximize ? -kInf
|
|
: kInf);
|
|
break;
|
|
}
|
|
// TODO(b/187027049): compute the dual value when the dual is feasible (or
|
|
// problem optimal for interior point) based on the bounds and the dual values
|
|
// for LPs.
|
|
result.mutable_solve_stats()->set_best_dual_bound(best_dual_bound);
|
|
SolutionProto solution;
|
|
AddPrimalSolution(get_prim_stat, obj_val, col_val, model_parameters,
|
|
solution);
|
|
if (!is_mip) {
|
|
AddDualSolution(get_dual_stat, obj_val, row_dual, col_dual,
|
|
model_parameters, solution);
|
|
}
|
|
if (solution.has_primal_solution() || solution.has_dual_solution() ||
|
|
solution.has_basis()) {
|
|
*result.add_solutions() = std::move(solution);
|
|
}
|
|
if (parameters.glpk().compute_unbound_rays_if_possible()) {
|
|
RETURN_IF_ERROR(AddPrimalOrDualRay(model_parameters, result));
|
|
}
|
|
|
|
RETURN_IF_ERROR(set_solve_time(result));
|
|
return result;
|
|
}
|
|
|
|
void GlpkSolver::AddVariables(const VariablesProto& new_variables) {
|
|
if (new_variables.ids().empty()) {
|
|
return;
|
|
}
|
|
|
|
// Indices in GLPK are one-based.
|
|
const int first_new_var_index = variables_.ids.size() + 1;
|
|
|
|
variables_.ids.insert(variables_.ids.end(), new_variables.ids().begin(),
|
|
new_variables.ids().end());
|
|
for (int v = 0; v < new_variables.ids_size(); ++v) {
|
|
CHECK(variables_.id_to_index
|
|
.try_emplace(new_variables.ids(v), first_new_var_index + v)
|
|
.second);
|
|
}
|
|
glp_add_cols(problem_, new_variables.ids_size());
|
|
if (!new_variables.names().empty()) {
|
|
for (int v = 0; v < new_variables.names_size(); ++v) {
|
|
glp_set_col_name(
|
|
problem_, v + first_new_var_index,
|
|
TruncateAndQuoteGLPKName(new_variables.names(v)).c_str());
|
|
}
|
|
}
|
|
CHECK_EQ(new_variables.lower_bounds_size(),
|
|
new_variables.upper_bounds_size());
|
|
CHECK_EQ(new_variables.lower_bounds_size(), new_variables.ids_size());
|
|
variables_.unrounded_lower_bounds.insert(
|
|
variables_.unrounded_lower_bounds.end(),
|
|
new_variables.lower_bounds().begin(), new_variables.lower_bounds().end());
|
|
variables_.unrounded_upper_bounds.insert(
|
|
variables_.unrounded_upper_bounds.end(),
|
|
new_variables.upper_bounds().begin(), new_variables.upper_bounds().end());
|
|
for (int i = 0; i < new_variables.lower_bounds_size(); ++i) {
|
|
// Here we don't use the boolean "kind" GLP_BV since it does not exist. It
|
|
// is an artifact of glp_(get|set)_col_kind() functions. When
|
|
// glp_set_col_kind() is called with GLP_BV, in addition to setting the kind
|
|
// to GLP_IV (integer) it also sets the bounds to [0,1]. Symmetrically
|
|
// glp_get_col_kind() returns GLP_BV when the kind is GLP_IV and the bounds
|
|
// are [0,1].
|
|
glp_set_col_kind(problem_, i + first_new_var_index,
|
|
new_variables.integers(i) ? GLP_IV : GLP_CV);
|
|
SetBounds<Variables>(problem_, /*k=*/i + first_new_var_index,
|
|
{.lower = new_variables.lower_bounds(i),
|
|
.upper = new_variables.upper_bounds(i)});
|
|
}
|
|
}
|
|
|
|
void GlpkSolver::AddLinearConstraints(
|
|
const LinearConstraintsProto& new_linear_constraints) {
|
|
if (new_linear_constraints.ids().empty()) {
|
|
return;
|
|
}
|
|
|
|
// Indices in GLPK are one-based.
|
|
const int first_new_cstr_index = linear_constraints_.ids.size() + 1;
|
|
|
|
linear_constraints_.ids.insert(linear_constraints_.ids.end(),
|
|
new_linear_constraints.ids().begin(),
|
|
new_linear_constraints.ids().end());
|
|
for (int c = 0; c < new_linear_constraints.ids_size(); ++c) {
|
|
CHECK(linear_constraints_.id_to_index
|
|
.try_emplace(new_linear_constraints.ids(c),
|
|
first_new_cstr_index + c)
|
|
.second);
|
|
}
|
|
glp_add_rows(problem_, new_linear_constraints.ids_size());
|
|
if (!new_linear_constraints.names().empty()) {
|
|
for (int c = 0; c < new_linear_constraints.names_size(); ++c) {
|
|
glp_set_row_name(
|
|
problem_, c + first_new_cstr_index,
|
|
TruncateAndQuoteGLPKName(new_linear_constraints.names(c)).c_str());
|
|
}
|
|
}
|
|
CHECK_EQ(new_linear_constraints.lower_bounds_size(),
|
|
new_linear_constraints.upper_bounds_size());
|
|
for (int i = 0; i < new_linear_constraints.lower_bounds_size(); ++i) {
|
|
SetBounds<LinearConstraints>(
|
|
problem_, /*k=*/i + first_new_cstr_index,
|
|
{.lower = new_linear_constraints.lower_bounds(i),
|
|
.upper = new_linear_constraints.upper_bounds(i)});
|
|
}
|
|
}
|
|
|
|
void GlpkSolver::UpdateObjectiveCoefficients(
|
|
const SparseDoubleVectorProto& coefficients_proto) {
|
|
for (const auto [id, coeff] : MakeView(coefficients_proto)) {
|
|
const int col_index = variables_.id_to_index.at(id);
|
|
CHECK_EQ(variables_.ids[col_index - 1], id);
|
|
glp_set_obj_coef(problem_, col_index, coeff);
|
|
}
|
|
}
|
|
|
|
void GlpkSolver::UpdateLinearConstraintMatrix(
|
|
const SparseDoubleMatrixProto& matrix_updates,
|
|
const std::optional<int64_t> first_new_var_id,
|
|
const std::optional<int64_t> first_new_cstr_id) {
|
|
// GLPK's does not have an API to set matrix elements one by one. Instead it
|
|
// can either update an entire row or update an entire column or load the
|
|
// entire matrix. On top of that there is no API to get the entire matrix at
|
|
// once.
|
|
//
|
|
// Hence to update existing coefficients we have to read rows (or columns)
|
|
// coefficients, update existing non-zero that have been changed and add new
|
|
// values and write back the result. For new rows and columns we can be more
|
|
// efficient since we don't have to read the existing values back.
|
|
//
|
|
// The strategy used below is to split the matrix in three regions:
|
|
//
|
|
// existing new
|
|
// columns columns
|
|
// / | \
|
|
// existing | 1 | 2 |
|
|
// rows | | |
|
|
// |---------+---------|
|
|
// new | |
|
|
// rows | 3 |
|
|
// \ /
|
|
//
|
|
// We start by updating the region 1 of existing rows and columns to limit the
|
|
// number of reads of existing coefficients. Then we update region 2 with all
|
|
// new columns but we only existing rows. Finally we update region 3 with all
|
|
// new rows and include new columns. Doing updates this way remove the need to
|
|
// read existing coefficients for the updates 2 & 3 since by construction
|
|
// those values are 0.
|
|
|
|
// Updating existing rows (constraints), ignoring the new columns.
|
|
{
|
|
// We reuse the same vectors for all calls to GLPK's API to limit
|
|
// reallocations of these temporary buffers.
|
|
GlpkSparseVector data(static_cast<int>(variables_.ids.size()));
|
|
for (const auto& [row_id, row_coefficients] :
|
|
SparseSubmatrixByRows(matrix_updates,
|
|
/*start_row_id=*/0,
|
|
/*end_row_id=*/first_new_cstr_id,
|
|
/*start_col_id=*/0,
|
|
/*end_col_id=*/first_new_var_id)) {
|
|
// Find the index of the row in GLPK corresponding to the MathOpt's row
|
|
// id.
|
|
const int row_index = linear_constraints_.id_to_index.at(row_id);
|
|
CHECK_EQ(linear_constraints_.ids[row_index - 1], row_id);
|
|
|
|
// Read the current row coefficients.
|
|
data.Load([&](int* const indices, double* const values) {
|
|
return glp_get_mat_row(problem_, row_index, indices, values);
|
|
});
|
|
|
|
// Update the row data.
|
|
for (const auto [col_id, coefficient] : row_coefficients) {
|
|
const int col_index = variables_.id_to_index.at(col_id);
|
|
CHECK_EQ(variables_.ids[col_index - 1], col_id);
|
|
data.Set(col_index, coefficient);
|
|
}
|
|
|
|
// Change the row values.
|
|
glp_set_mat_row(problem_, row_index, data.size(), data.indices(),
|
|
data.values());
|
|
}
|
|
}
|
|
|
|
// Add new columns's coefficients of existing rows. The coefficients of new
|
|
// columns in new rows will be added when adding new rows below.
|
|
if (first_new_var_id.has_value()) {
|
|
GlpkSparseVector data(static_cast<int>(linear_constraints_.ids.size()));
|
|
for (const auto& [col_id, col_coefficients] : TransposeSparseSubmatrix(
|
|
SparseSubmatrixByRows(matrix_updates,
|
|
/*start_row_id=*/0,
|
|
/*end_row_id=*/first_new_cstr_id,
|
|
/*start_col_id=*/*first_new_var_id,
|
|
/*end_col_id=*/std::nullopt))) {
|
|
// Find the index of the column in GLPK corresponding to the MathOpt's
|
|
// column id.
|
|
const int col_index = variables_.id_to_index.at(col_id);
|
|
CHECK_EQ(variables_.ids[col_index - 1], col_id);
|
|
|
|
// Prepare the column data replacing MathOpt ids by GLPK one-based row
|
|
// indices.
|
|
data.Clear();
|
|
for (const auto [row_id, coefficient] : MakeView(col_coefficients)) {
|
|
const int row_index = linear_constraints_.id_to_index.at(row_id);
|
|
CHECK_EQ(linear_constraints_.ids[row_index - 1], row_id);
|
|
data.Set(row_index, coefficient);
|
|
}
|
|
|
|
// Change the column values.
|
|
glp_set_mat_col(problem_, col_index, data.size(), data.indices(),
|
|
data.values());
|
|
}
|
|
}
|
|
|
|
// Add new rows, including the new columns' coefficients.
|
|
if (first_new_cstr_id.has_value()) {
|
|
GlpkSparseVector data(static_cast<int>(variables_.ids.size()));
|
|
for (const auto& [row_id, row_coefficients] :
|
|
SparseSubmatrixByRows(matrix_updates,
|
|
/*start_row_id=*/*first_new_cstr_id,
|
|
/*end_row_id=*/std::nullopt,
|
|
/*start_col_id=*/0,
|
|
/*end_col_id=*/std::nullopt)) {
|
|
// Find the index of the row in GLPK corresponding to the MathOpt's row
|
|
// id.
|
|
const int row_index = linear_constraints_.id_to_index.at(row_id);
|
|
CHECK_EQ(linear_constraints_.ids[row_index - 1], row_id);
|
|
|
|
// Prepare the row data replacing MathOpt ids by GLPK one-based column
|
|
// indices.
|
|
data.Clear();
|
|
for (const auto [col_id, coefficient] : row_coefficients) {
|
|
const int col_index = variables_.id_to_index.at(col_id);
|
|
CHECK_EQ(variables_.ids[col_index - 1], col_id);
|
|
data.Set(col_index, coefficient);
|
|
}
|
|
|
|
// Change the row values.
|
|
glp_set_mat_row(problem_, row_index, data.size(), data.indices(),
|
|
data.values());
|
|
}
|
|
}
|
|
}
|
|
|
|
void GlpkSolver::AddPrimalSolution(
|
|
int (*get_prim_stat)(glp_prob*), double (*obj_val)(glp_prob*),
|
|
double (*col_val)(glp_prob*, int),
|
|
const ModelSolveParametersProto& model_parameters,
|
|
SolutionProto& solution_proto) {
|
|
const int status = get_prim_stat(problem_);
|
|
if (status == GLP_OPT || status == GLP_FEAS) {
|
|
PrimalSolutionProto& primal_solution =
|
|
*solution_proto.mutable_primal_solution();
|
|
primal_solution.set_objective_value(obj_val(problem_));
|
|
primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
|
*primal_solution.mutable_variable_values() =
|
|
FilteredVector(problem_, model_parameters.variable_values_filter(),
|
|
variables_.ids, col_val);
|
|
}
|
|
}
|
|
|
|
void GlpkSolver::AddDualSolution(
|
|
int (*get_dual_stat)(glp_prob*), double (*obj_val)(glp_prob*),
|
|
double (*row_dual)(glp_prob*, int), double (*col_dual)(glp_prob*, int),
|
|
const ModelSolveParametersProto& model_parameters,
|
|
SolutionProto& solution_proto) {
|
|
const int status = get_dual_stat(problem_);
|
|
if (status == GLP_OPT || status == GLP_FEAS) {
|
|
DualSolutionProto& dual_solution = *solution_proto.mutable_dual_solution();
|
|
dual_solution.set_objective_value(obj_val(problem_));
|
|
*dual_solution.mutable_dual_values() =
|
|
FilteredVector(problem_, model_parameters.dual_values_filter(),
|
|
linear_constraints_.ids, row_dual);
|
|
*dual_solution.mutable_reduced_costs() =
|
|
FilteredVector(problem_, model_parameters.reduced_costs_filter(),
|
|
variables_.ids, col_dual);
|
|
// TODO(b/197867442): Check that `status == GLP_FEAS` implies dual feasible
|
|
// solution on early termination with barrier (where both `get_dual_stat`
|
|
// and `get_prim_stat` are equal to `glp_ipt_status`).
|
|
dual_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
|
}
|
|
}
|
|
|
|
absl::Status GlpkSolver::AddPrimalOrDualRay(
|
|
const ModelSolveParametersProto& model_parameters,
|
|
SolveResultProto& result) {
|
|
ASSIGN_OR_RETURN(const std::optional<GlpkRay> opt_unbound_ray,
|
|
GlpkComputeUnboundRay(problem_));
|
|
if (!opt_unbound_ray.has_value()) {
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
const int num_cstrs = linear_constraints_.ids.size();
|
|
switch (opt_unbound_ray->type) {
|
|
case GlpkRayType::kPrimal: {
|
|
const int num_cstrs = static_cast<int>(linear_constraints_.ids.size());
|
|
// Note that GlpkComputeUnboundRay() returned ray considers the variables
|
|
// of the computational form. Thus it contains both structural and
|
|
// auxiliary variables. In the MathOpt's primal ray we only consider
|
|
// structural variables though.
|
|
std::vector<double> ray_values(variables_.ids.size());
|
|
|
|
for (const auto [k, value] : opt_unbound_ray->non_zero_components) {
|
|
if (k <= num_cstrs) {
|
|
// Ignore auxiliary variables.
|
|
continue;
|
|
}
|
|
const int var_index = k - num_cstrs;
|
|
CHECK_GE(var_index, 1);
|
|
ray_values[var_index - 1] = value;
|
|
}
|
|
|
|
*result.add_primal_rays()->mutable_variable_values() =
|
|
FilteredRay(model_parameters.variable_values_filter(), variables_.ids,
|
|
ray_values);
|
|
|
|
return absl::OkStatus();
|
|
}
|
|
case GlpkRayType::kDual: {
|
|
// Note that GlpkComputeUnboundRay() returned ray considers the variables
|
|
// of the computational form. Thus it contains reduced costs of both
|
|
// structural and auxiliary variables. In the MathOpt's dual ray we split
|
|
// the reduced costs. The ones of auxiliary variables (variables of
|
|
// constraints) are called "dual values" and the ones of structural
|
|
// variables are called "reduced costs".
|
|
std::vector<double> ray_reduced_costs(variables_.ids.size());
|
|
std::vector<double> ray_dual_values(num_cstrs);
|
|
|
|
for (const auto [k, value] : opt_unbound_ray->non_zero_components) {
|
|
if (k <= num_cstrs) {
|
|
ray_dual_values[k - 1] = value;
|
|
} else {
|
|
const int var_index = k - num_cstrs;
|
|
CHECK_GE(var_index, 1);
|
|
ray_reduced_costs[var_index - 1] = value;
|
|
}
|
|
}
|
|
|
|
DualRayProto& dual_ray = *result.add_dual_rays();
|
|
*dual_ray.mutable_dual_values() =
|
|
FilteredRay(model_parameters.dual_values_filter(),
|
|
linear_constraints_.ids, ray_dual_values);
|
|
*dual_ray.mutable_reduced_costs() =
|
|
FilteredRay(model_parameters.reduced_costs_filter(), variables_.ids,
|
|
ray_reduced_costs);
|
|
|
|
return absl::OkStatus();
|
|
}
|
|
}
|
|
}
|
|
|
|
absl::StatusOr<bool> GlpkSolver::Update(const ModelUpdateProto& model_update) {
|
|
RETURN_IF_ERROR(CheckCurrentThread());
|
|
|
|
// We must do that *after* testing current thread since the Solver class won't
|
|
// destroy this instance from another thread when the update is not supported
|
|
// (the Solver class destroy the SolverInterface only when an Update() returns
|
|
// false).
|
|
if (!UpdateIsSupported(model_update, kGlpkSupportedStructures)) {
|
|
return false;
|
|
}
|
|
|
|
{
|
|
const std::vector<int> sorted_deleted_cols = DeleteRowsOrCols(
|
|
problem_, variables_, model_update.deleted_variable_ids());
|
|
DeleteRowOrColData(variables_.unrounded_lower_bounds, sorted_deleted_cols);
|
|
DeleteRowOrColData(variables_.unrounded_upper_bounds, sorted_deleted_cols);
|
|
CHECK_EQ(variables_.unrounded_lower_bounds.size(),
|
|
variables_.unrounded_upper_bounds.size());
|
|
CHECK_EQ(variables_.unrounded_lower_bounds.size(), variables_.ids.size());
|
|
}
|
|
DeleteRowsOrCols(problem_, linear_constraints_,
|
|
model_update.deleted_linear_constraint_ids());
|
|
|
|
for (const auto [var_id, is_integer] :
|
|
MakeView(model_update.variable_updates().integers())) {
|
|
// See comment in AddVariables() to see why we don't use GLP_BV here.
|
|
const int var_index = variables_.id_to_index.at(var_id);
|
|
glp_set_col_kind(problem_, var_index, is_integer ? GLP_IV : GLP_CV);
|
|
|
|
// Either restore the fractional bounds if the variable was integer and is
|
|
// now integer, or rounds the existing bounds if the variable was fractional
|
|
// and is now integer. Here we use the old bounds; they will get updated
|
|
// below by the call to UpdateBounds() if they are also changed by this
|
|
// update.
|
|
SetBounds<Variables>(
|
|
problem_, var_index,
|
|
{.lower = variables_.unrounded_lower_bounds[var_index - 1],
|
|
.upper = variables_.unrounded_upper_bounds[var_index - 1]});
|
|
}
|
|
for (const auto [var_id, lower_bound] :
|
|
MakeView(model_update.variable_updates().lower_bounds())) {
|
|
variables_.unrounded_lower_bounds[variables_.id_to_index.at(var_id) - 1] =
|
|
lower_bound;
|
|
}
|
|
for (const auto [var_id, upper_bound] :
|
|
MakeView(model_update.variable_updates().upper_bounds())) {
|
|
variables_.unrounded_upper_bounds[variables_.id_to_index.at(var_id) - 1] =
|
|
upper_bound;
|
|
}
|
|
UpdateBounds(
|
|
problem_, variables_,
|
|
/*lower_bounds_proto=*/model_update.variable_updates().lower_bounds(),
|
|
/*upper_bounds_proto=*/model_update.variable_updates().upper_bounds());
|
|
UpdateBounds(problem_, linear_constraints_,
|
|
/*lower_bounds_proto=*/
|
|
model_update.linear_constraint_updates().lower_bounds(),
|
|
/*upper_bounds_proto=*/
|
|
model_update.linear_constraint_updates().upper_bounds());
|
|
|
|
AddVariables(model_update.new_variables());
|
|
AddLinearConstraints(model_update.new_linear_constraints());
|
|
|
|
if (model_update.objective_updates().has_direction_update()) {
|
|
glp_set_obj_dir(problem_,
|
|
model_update.objective_updates().direction_update()
|
|
? GLP_MAX
|
|
: GLP_MIN);
|
|
}
|
|
if (model_update.objective_updates().has_offset_update()) {
|
|
// Glpk uses index 0 for the "shift" of the objective.
|
|
glp_set_obj_coef(problem_, 0,
|
|
model_update.objective_updates().offset_update());
|
|
}
|
|
UpdateObjectiveCoefficients(
|
|
model_update.objective_updates().linear_coefficients());
|
|
|
|
UpdateLinearConstraintMatrix(
|
|
model_update.linear_constraint_matrix_updates(),
|
|
/*first_new_var_id=*/FirstVariableId(model_update.new_variables()),
|
|
/*first_new_cstr_id=*/
|
|
FirstLinearConstraintId(model_update.new_linear_constraints()));
|
|
|
|
return true;
|
|
}
|
|
|
|
absl::Status GlpkSolver::CheckCurrentThread() {
|
|
if (std::this_thread::get_id() != thread_id_) {
|
|
return absl::InvalidArgumentError(
|
|
"GLPK is not thread-safe and thus the solver should only be used on "
|
|
"the same thread as it was created");
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
std::optional<SolveResultProto> GlpkSolver::EmptyIntegerBoundsResult() {
|
|
const int num_cols = glp_get_num_cols(problem_);
|
|
for (int c = 1; c <= num_cols; ++c) {
|
|
if (!variables_.IsInteger(problem_, c)) {
|
|
continue;
|
|
}
|
|
const double lb = variables_.unrounded_lower_bounds[c - 1];
|
|
const double ub = variables_.unrounded_upper_bounds[c - 1];
|
|
if (lb > ub) {
|
|
// Unrounded bounds are inverted; this case is covered by
|
|
// ListInvertedBounds(). We don't want to depend on the order of calls of
|
|
// the two functions here so we exclude this case.
|
|
continue;
|
|
}
|
|
if (std::ceil(lb) <= std::floor(ub)) {
|
|
continue;
|
|
}
|
|
|
|
// We found a variable with empty integer bounds (that is lb <= ub but
|
|
// ceil(lb) > floor(ub)).
|
|
return ResultForIntegerInfeasible(
|
|
/*is_maximize=*/glp_get_obj_dir(problem_) == GLP_MAX,
|
|
/*bad_variable_id=*/variables_.ids[c - 1],
|
|
/*lb=*/lb, /*ub=*/ub);
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
|
|
GlpkSolver::ComputeInfeasibleSubsystem(
|
|
const SolveParametersProto& parameters, MessageCallback message_cb,
|
|
const SolveInterrupter* absl_nullable const interrupter) {
|
|
return absl::UnimplementedError(
|
|
"GLPK does not provide a method to compute an infeasible subsystem");
|
|
}
|
|
|
|
MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GLPK, GlpkSolver::New)
|
|
|
|
} // namespace math_opt
|
|
} // namespace operations_research
|