6#include "delpi/solver/DelpiLpSolver.h"
11#include <unordered_map>
12#include <unordered_set>
16#include "delpi/util/error.h"
17#include "internal/Basis.h"
18#include "internal/BgLinearSystemSolver.h"
31DelpiLpSolver::DelpiLpSolver(
Config config,
const std::string& class_name)
32 :
LpSolver{mpq_class{mpz_class{0}, 0}, mpq_class{mpz_class{0}, 0}, std::move(config), class_name} {}
34int DelpiLpSolver::num_columns()
const {
return static_cast<int>(
problem_.num_columns()); }
35int DelpiLpSolver::num_rows()
const {
return static_cast<int>(
problem_.num_rows()); }
38 DELPI_ASSERT(column_idx < num_columns(),
"Column index out of bounds");
39 const auto [lb, ub, obj] =
problem_.column(column_idx);
43 DELPI_ASSERT(row_idx < num_rows(),
"Row index out of bounds");
44 const Index columns = num_columns();
45 const auto [lp_addends, lb, ub]{
problem_.row(row_idx)};
46 std::vector<std::pair<Variable, mpq_class>> addends;
47 addends.reserve(columns);
48 for (
const auto& [col, coeff] : lp_addends) addends.emplace_back(
col_to_var_.at(col), coeff);
49 return {addends, lb.has_value() ? lb.value() :
ninfinity_, ub.has_value() ? ub.value() :
infinity_};
61 const mpq_class& ub) {
62 DELPI_ASSERT_FMT(!
var_to_col_.contains(
var),
"Variable '{}' already exists in the LP.",
var);
63 const ColumnIndex column_idx = num_columns();
69LpSolver::RowIndex DelpiLpSolver::AddRow(
const std::vector<Expression::Addend>& addends,
const mpq_class& lb,
70 const mpq_class& ub) {
71 if (gmp::IsInfinity(lb) && gmp::IsInfinity(ub)) {
72 DELPI_WARN_FMT(
"Ignoring unbounded row with addends: {}", addends);
75 std::unordered_map<Index, mpq_class> row_lhs;
76 row_lhs.reserve(addends.size());
77 for (
const auto& [
var, coeff] : addends) row_lhs.emplace(
var_to_col_.at(
var), coeff);
79 return num_rows() - 1;
81LpSolver::RowIndex DelpiLpSolver::AddRow(
const Expression::Addends& lhs,
const FormulaKind sense,
82 const mpq_class& rhs) {
84 "Only equality, less than or equal, and greater than or equal constraints are supported");
85 std::unordered_map<Index, mpq_class> row_lhs;
86 row_lhs.reserve(lhs.size());
89 return num_rows() - 1;
92 [[maybe_unused]]
const mpq_class& ub) {
93 DELPI_TRACE_FMT(
"DelpiLpSolver::SetBound({}, {}, {})",
var, lb, ub);
94 DELPI_ASSERT(
var_to_col_.contains(
var),
"Variable not found in the LP");
99 [[maybe_unused]]
const mpq_class& value) {
100 DELPI_TRACE_FMT(
"DelpiLpSolver::SetCoefficient({}, {}, {})",
row,
column, value);
101 DELPI_ASSERT(
row < num_rows(),
"Row index out of bounds");
102 DELPI_ASSERT(
column < num_columns(),
"Column index out of bounds");
106 DELPI_TRACE_FMT(
"DelpiLpSolver::SetObjective({}, {})",
column, value);
107 DELPI_ASSERT(
column < num_columns(),
"Column index out of bounds");
112void DelpiLpSolver::Dump() {
113 std::cout <<
"DelpiLpSolver{ num_columns: " << num_columns() <<
", num_rows: " << num_rows() <<
"\n"
118 DELPI_DEBUG(
"DelpiLpSolver::SolveCore()");
119 Matrix<mpq_class> slack_A;
120 Vector<mpq_class> slack_c;
121 Vector<mpq_class> slack_b;
122 problem_.SlackForm(slack_A, slack_b, slack_c);
123 internal::Basis<mpq_class> slack_basis(slack_A);
124 DELPI_DEV(
"About to check feasibility");
126 const LpResult feasibility_check = FeasibilitySolve(slack_A, slack_b, slack_basis);
127 DELPI_DEV_FMT(
"Feasibility check: {}", feasibility_check);
129 DELPI_ASSERT(feasibility_check ==
LpResult::OPTIMAL,
"Feasibility check must be optimal");
130 DELPI_ASSERT(slack_basis.basis_vectors().determinant() != 0,
"Basis matrix must be non-singular");
132 const LpResult optimality_check = OptimalitySolve(slack_A, slack_b, slack_c, slack_basis);
134 DELPI_DEV_FMT(
"Result: {}. x: {}, c: {}", optimality_check,
x_,
problem_.c());
136 return optimality_check;
139template <IsAnyOf<
double, mpq_
class> T>
141 internal::Basis<T>& basis, Vector<T>*
const x, T*
const obj) {
142 if constexpr (std::is_same_v<T, mpq_class>) {
143 DELPI_ASSERT(tolerance == 0,
"Tolerance must be 0 for exact arithmetic");
146 DELPI_ASSERT(A.rows() == b.size(),
"Inconsistent number of rows in A and b");
147 DELPI_ASSERT(A.cols() == c.size(),
"Inconsistent number of columns in A and c");
148 DELPI_ASSERT(basis.size() == A.rows(),
"Inconsistent number of rows in A and basis");
149 DELPI_ASSERT(&A == &basis.A(),
"Basis must be built from matrix A");
150 DELPI_ASSERT(basis.basis_vectors().determinant() != 0,
"Basis matrix must be non-singular");
153 constexpr int max_iterations = 1000;
154 for (
int i = 0; i < max_iterations; ++i) {
155 internal::BgLinearSystemSolver<T> solver{
config_};
156 solver.Factorise(basis);
157 Vector<T> zb{solver.Solve(b)};
158 DELPI_ASSERT((zb.array() >= -tolerance).all(),
"All values must be non-negative (feasible)");
159 Vector<T> y = solver.TransposeSolve(c(basis.basis_idxs()));
162 Vector<T> r{c - A.transpose() * y};
165 for (; r_idx < r.size(); ++r_idx) {
166 if (r(r_idx) < -tolerance)
break;
168 if (r_idx == r.size()) {
169 if (
nullptr != x ||
nullptr != obj) {
170 Vector<T> _x = Eigen::VectorX<T>::Zero(A.cols());
171 _x(basis.basis_idxs()) = solver.Solve(b);
172 if (x !=
nullptr) *x = _x;
173 if (obj !=
nullptr) *obj = c.transpose() * _x;
181 Vector<T> d{solver.Solve(A.col(r_idx))};
182 mpq_class min_ratio = -1;
184 for (
int d_idx = 0; d_idx < d.size(); ++d_idx) {
185 if (d(d_idx) <= tolerance)
continue;
186 const mpq_class ratio = zb(d_idx) / d(d_idx);
187 if (min_ratio == -1 || ratio < min_ratio) {
198 fmt::println(
"Leaving = {}, entering = {}", min_idx, r_idx);
201 basis.Update(min_idx, r_idx);
204 DELPI_RUNTIME_ERROR(
"Maximum number of iterations reached");
207LpResult DelpiLpSolver::FeasibilitySolve(Matrix<mpq_class>& slack_A, Vector<mpq_class>& slack_b,
208 internal::Basis<mpq_class>& slack_basis) {
209 DELPI_TRACE(
"DelpiLpSolver::FeasibilitySolve()");
210 DELPI_ASSERT(&slack_A == &slack_basis.A(),
"Basis must be built from matrix A");
211 DELPI_ASSERT(slack_A.rows() == slack_b.size(),
"Inconsistent number of rows in A and b");
214 Matrix<mpq_class> aux_A{};
215 Vector<mpq_class> aux_c{};
216 std::vector<Index> aux_columns{};
217 internal::Basis<mpq_class> aux_basis{
AuxForm(slack_A, slack_b, aux_A, aux_c, aux_columns)};
219 DELPI_ASSERT(!aux_basis.basis_idxs().empty(),
"Auxiliary basis must have at least one index");
220 if (*std::ranges::max_element(aux_basis.basis_idxs()) < slack_A.cols()) {
222 slack_basis = internal::Basis<mpq_class>{slack_A};
227 for (
const auto& [precision, tolerance] : precisions) {
230 internal::Basis<mpq_class> feas_basis{aux_A, aux_basis};
232 if (precision == 0) {
234 DELPI_DEBUG(
"Feasibility check with precision=0 (rational)");
236 feas_result =
InternalSolve(aux_A, slack_b, aux_c, mpq_class{0}, feas_basis,
237 static_cast<Vector<mpq_class>*
>(
nullptr), &obj);
238 feas_obj = obj.get_d();
239 }
else if (precision == 64) {
240 DELPI_DEBUG(
"Feasibility check with precision=64 (double)");
241 Matrix<double> aux_A_d = aux_A.cast<
double>();
242 Vector<double> slack_b_d = slack_b.cast<
double>();
243 Vector<double> aux_c_d = aux_c.cast<
double>();
245 internal::Basis<double> aux_basis_d{aux_A_d, aux_basis};
246 feas_result =
InternalSolve(aux_A_d, slack_b_d, aux_c_d, 1e-6, aux_basis_d,
static_cast<Vector<double>*
>(
nullptr),
248 feas_basis = aux_basis_d;
253 DELPI_DEV_FMT(
"Feasibility check: result={}, obj={}, tolerance={}", feas_result, feas_obj, tolerance);
257 if (feas_obj <= tolerance) {
264 const LpResult feasibility = FeasibilityCheck(aux_A, slack_b, aux_c, feas_basis);
272 throw DelpiLpSolverException(
"Could not prove feasibility with the provided precisions");
274LpResult DelpiLpSolver::OptimalitySolve(
const Matrix<mpq_class>& slack_A,
const Vector<mpq_class>& slack_b,
275 const Vector<mpq_class>& slack_c, internal::Basis<mpq_class>& slack_basis) {
276 DELPI_TRACE(
"DelpiLpSolver::OptimalitySolve()");
277 DELPI_ASSERT(&slack_A == &slack_basis.A(),
"Basis must be built from matrix A");
278 DELPI_ASSERT(slack_A.rows() == slack_b.size(),
"Inconsistent number of rows in A and b");
281 for (
const auto& [precision, tolerance] : precisions) {
283 internal::Basis<mpq_class> opt_basis{slack_A, slack_basis};
285 if (precision == 0) {
287 DELPI_DEBUG(
"Feasibility check with precision=0 (rational)");
289 opt_result =
InternalSolve(slack_A, slack_b, slack_c, mpq_class{0}, opt_basis);
290 }
else if (precision == 64) {
291 DELPI_DEBUG(
"Feasibility check with precision=64 (double)");
292 Matrix<double> slack_A_d = slack_A.cast<
double>();
293 Vector<double> slack_b_d = slack_b.cast<
double>();
294 Vector<double> slack_c_d = slack_c.cast<
double>();
296 internal::Basis<double> aux_basis_d{slack_A_d, slack_basis};
297 opt_result =
InternalSolve(slack_A_d, slack_b_d, slack_c_d, 1e-6, aux_basis_d);
298 opt_basis = aux_basis_d;
303 DELPI_DEV_FMT(
"Optimality check: result={} tolerance={}", opt_result, tolerance);
310 slack_basis = opt_basis;
317 if (OptimalityCheck(slack_A, slack_b, slack_c, opt_basis) ==
LpResult::OPTIMAL) {
318 slack_basis = opt_basis;
323 throw DelpiLpSolverException(
"Could not find an optimal solution with the provided precisions");
326LpResult DelpiLpSolver::FeasibilityCheck(
const Matrix<mpq_class>& aux_A,
const Vector<mpq_class>& slack_b,
327 const Vector<mpq_class>& aux_c,
328 const internal::Basis<mpq_class>& feas_basis)
const {
329 DELPI_ASSERT(aux_A.rows() == slack_b.size(),
"Inconsistent number of rows in A and b");
330 DELPI_ASSERT(&aux_A == &feas_basis.A(),
"Basis must be built from matrix A");
331 DELPI_ASSERT(feas_basis.basis_vectors().determinant() != 0,
"Basis matrix must be non-singular");
333 DELPI_DEV(
"Feasibility check: auxiliary problem is feasible and bounded");
334 if (feas_basis.basis_vectors().determinant() == 0)
return LpResult::ERROR;
335 internal::BgLinearSystemSolver<mpq_class> solver{
config_};
336 solver.Factorise(feas_basis);
337 const Vector<mpq_class> zb{solver.Solve(slack_b)};
338 const mpq_class obj{aux_c(feas_basis.basis_idxs()).transpose() * zb};
339 const Vector<mpq_class> y{solver.TransposeSolve(aux_c(feas_basis.basis_idxs()))};
340 const Vector<mpq_class> r{aux_c - aux_A.transpose() * y};
341 DELPI_DEV_FMT(
"Feasibility check: zb>=0 ? {} | r>=0 ? {} | obj={}", (zb.array() >= 0).all(), (r.array() >= 0).all(),
346LpResult DelpiLpSolver::OptimalityCheck(
const Matrix<mpq_class>& slack_A,
const Vector<mpq_class>& slack_b,
347 const Vector<mpq_class>& slack_c,
const internal::Basis<mpq_class>& basis) {
348 DELPI_TRACE(
"DelpiLpSolver::OptimalityCheck()");
350 internal::BgLinearSystemSolver<mpq_class> solver{
config_};
351 solver.Factorise(basis);
352 const Vector<mpq_class> zb{solver.Solve(slack_b)};
355 const Vector<mpq_class> y{solver.TransposeSolve(slack_c(basis.basis_idxs()))};
356 const Vector<mpq_class> r{slack_c - slack_A.transpose() * y};
360 obj_lb_ = slack_b.transpose() * y;
361 obj_ub_ = slack_c(basis.basis_idxs()).transpose() * zb;
366 x_ = Vector<mpq_class>::Zero(slack_A.cols());
367 x_(basis.basis_idxs()) = zb;
374LpResult DelpiLpSolver::UnboundednessCheck(
const Matrix<mpq_class>& slack_A,
const Vector<mpq_class>& slack_b,
375 const Vector<mpq_class>& slack_c,
376 const internal::Basis<mpq_class>& basis)
const {
377 internal::BgLinearSystemSolver<mpq_class> solver{
config_};
378 solver.Factorise(basis);
379 const Vector<mpq_class> zb{solver.Solve(slack_b)};
381 const auto y{solver.TransposeSolve(slack_c(basis.basis_idxs()))};
382 const Vector<mpq_class> r{slack_c - slack_A.transpose() * y};
383 for (Index i = 0; i < r.size(); ++i) {
385 const Vector<mpq_class> d{solver.Solve(slack_A.col(i))};
392 const std::vector<Index>& aux_columns, Matrix<mpq_class>& slack_A,
393 Vector<mpq_class>& slack_b, internal::Basis<mpq_class>& slack_basis)
const {
394 DELPI_TRACE(
"DelpiLpSolver::RemoveAuxiliaryColumns()");
395 DELPI_ASSERT(&slack_A == &slack_basis.A(),
"The basis must be built from the same matrix A");
397 std::vector<Index> rows_to_remove{};
398 std::vector<std::size_t> columns_to_remove{};
399 for (std::size_t i = 0; i < feas_basis.basis_idxs().size(); ++i) {
400 if (feas_basis.basis_idxs()[i] < slack_A.cols())
continue;
403 Index
row = aux_columns.at(feas_basis.basis_idxs()[i] - slack_A.cols());
404 rows_to_remove.emplace_back(
row);
405 columns_to_remove.emplace_back(i);
409 for (
const Index i : rows_to_remove) {
410 DELPI_DEV_FMT(
"Removing row {}", i);
411 slack_A.row(i) = slack_A.row(slack_A.rows() - 1).eval();
412 slack_A.conservativeResize(slack_A.rows() - 1, Eigen::NoChange);
413 slack_b.row(i) = slack_b.row(slack_b.size() - 1).eval();
414 slack_b.conservativeResize(slack_b.size() - 1);
417 slack_basis.FromBasis(feas_basis, columns_to_remove);
418 DELPI_ASSERT(slack_basis.basis_vectors().determinant() != 0,
"Basis matrix must be non-singular");
421 Matrix<mpq_class>& aux_A, Vector<mpq_class>& aux_c,
422 std::vector<Index>& aux_columns)
const {
423 DELPI_TRACE(
"DelpiLpSolver::StdForm()");
424 DELPI_ASSERT(aux_columns.empty(),
"Auxiliary columns must be empty");
425 DELPI_ASSERT(slack_A.rows() == slack_b.size(),
"Inconsistent number of rows in A and b");
427 for (Index i = 0; i < slack_A.rows(); ++i) {
428 aux_columns.emplace_back(i);
431 aux_A = Matrix<mpq_class>{slack_A.rows(), slack_A.cols() + slack_b.size()};
432 aux_A.leftCols(slack_A.cols()) = slack_A;
433 aux_A.rightCols(slack_b.size()).setZero();
435 std::vector<Index> aux_basis_idx{};
436 aux_basis_idx.reserve(slack_A.rows());
438 for (Index i = 0; i < slack_b.size(); ++i) {
439 if (slack_A(i, slack_A.cols() - slack_A.rows() + i) != 0 &&
440 (slack_b(i) < 0) == (slack_A(i, slack_A.cols() - slack_A.rows() + i) < 0)) {
441 aux_basis_idx.emplace_back(slack_A.cols() - slack_A.rows() + i);
443 aux_A(i, slack_A.cols() + i) = slack_b(i) < 0 ? -1 : 1;
444 aux_basis_idx.emplace_back(slack_A.cols() + i);
448 aux_c = Vector<mpq_class>::Zero(slack_A.cols() + slack_b.size());
449 aux_c.tail(slack_b.size()).setConstant(1);
454 std::vector<Index> aux_basis_idx{};
455 aux_basis_idx.reserve(slack_A.rows());
456 aux_columns.reserve(slack_b.size());
457 for (Index i = 0; i < slack_b.size(); ++i) {
458 if ((slack_b(i) < 0) != slack_A(i, slack_A.cols() - slack_A.rows()) < 0) {
459 aux_columns.emplace_back(i);
461 aux_basis_idx.emplace_back(i);
466 aux_A = Matrix<mpq_class>{slack_A.rows(), slack_A.cols() +
static_cast<Index
>(aux_columns.size())};
467 aux_A.leftCols(slack_A.cols()) = slack_A;
468 aux_A.rightCols(aux_columns.size()).setZero();
469 for (Index i = 0; i < static_cast<Index>(aux_columns.size()); ++i) {
470 aux_A.col(slack_A.cols() + i).setZero();
471 aux_A(aux_columns[i], slack_A.cols() + i) = -1;
472 aux_basis_idx.emplace_back(slack_A.cols() + i);
475 aux_c = Vector<mpq_class>::Zero(slack_A.cols() +
static_cast<Index
>(aux_columns.size()));
476 aux_c.tail(
static_cast<Index
>(aux_columns.size())).setConstant(1);
478 DELPI_ASSERT(
static_cast<Index
>(aux_basis_idx.size()) == slack_A.rows(),
479 "Inconsistent number of rows in A and basis");
480 DELPI_ASSERT(
static_cast<Index
>(aux_basis_idx.size()) <= slack_A.rows(),
481 "Inconsistent number of rows in A and aux columns");
484 return {aux_A, std::move(aux_basis_idx)};
489std::ostream& operator<<(std::ostream& os,
const DelpiLpSolver& solver) {
490 return os <<
"DelpiLpSolver{ num_columns: " << solver.num_columns() <<
", num_rows: " << solver.num_rows() <<
"\n"
491 << solver.problem() <<
"}\n";
495 const double&, internal::Basis<double>&, Vector<double>*,
double*);
497 const Vector<mpq_class>&,
const mpq_class&, internal::Basis<mpq_class>&,
498 Vector<mpq_class>*, mpq_class*);
Simple dataclass used to store the configuration of the program.
Linear programming solver using a custom implementation of the Simplex algorithm.
internal::LpProblem problem_
Linear programming problem.
void SetBound(Variable var, const mpq_class &lb, const mpq_class &ub) override
Set the bounds of a var in the LP problem to the given lb and ub.
void RemoveAuxiliaryColumns(const internal::Basis< mpq_class > &feas_basis, const std::vector< Index > &aux_columns, Matrix< mpq_class > &slack_A, Vector< mpq_class > &slack_b, internal::Basis< mpq_class > &slack_basis) const
Use aux_basis to update basis with a set of columns we know feasible.
LpResult InternalSolve(const Matrix< T > &A, const Vector< T > &b, const Vector< T > &c, const T &tolerance, internal::Basis< T > &basis, Vector< T > *x=nullptr, T *obj=nullptr)
Solve the LP problem using the Simplex algorithm.
Vector< mpq_class > x_
Solution vector.
void ReserveRows(int num_rows) override
Reserve space for the given number of rows.
void EnsureSenseCore() override
Make sure the LP solvers are aware of the sense of the LP problem (minimisation or maximisation).
LpResult SolveCore() override
Internal method that optimises the LP problem with the given delta.
void ReserveColumns(int num_columns) override
Reserve space for the given number of columns and rows.
Row row(RowIndex row_idx) const override
Get the row at the given row_idx index.
Column column(ColumnIndex column_idx) const override
Get the column at the given column_idx index.
mpq_class delta_
Precision for the optimality check.
internal::Basis< mpq_class > AuxForm(const Matrix< mpq_class > &slack_A, const Vector< mpq_class > &slack_b, Matrix< mpq_class > &aux_A, Vector< mpq_class > &aux_c, std::vector< Index > &aux_columns) const
Convert a standard form LP problem into a feasible and bounded auxiliary problem.
ColumnIndex AddColumn(const Variable &var, const mpq_class &obj, const mpq_class &lb, const mpq_class &ub) override
Add a new bounded column to the LP problem, ensuring that the variable var is in the range and has t...
void SetObjective(int column, const mpq_class &value) override
The the objective coefficient of the given column to the given value.
void SetCoefficient(RowIndex row, ColumnIndex column, const mpq_class &value) override
Set the coefficient of the row constraint to apply at the column decisional variable.
Facade class that hides the underlying LP solver used by delpi.
virtual void ReserveColumns(int size)
Reserve space for the given number of columns and rows.
const Variable & var(const int column) const
Shorthand notation to get the real variable linked with column column.
mpq_class infinity_
Infinity threshold value.
mpq_class ninfinity_
Negative infinity threshold value.
Config config_
Configuration to use.
std::vector< mpq_class > solution_
Solution vector.
std::unordered_map< Variable, int > var_to_col_
Theory column ⇔ Variable.
mpq_class obj_ub_
Upper bound on the objective value, if any.
mpq_class obj_lb_
Lower bound on the objective value, if any.
std::vector< Variable > col_to_var_
Literal ⇔ lp row.
virtual void ReserveRows(int size)
Reserve space for the given number of rows.
Global namespace for the delpi library.
LpResult
Possible outcomes of the LP solver.
@ INFEASIBLE
The problem is infeasible.
@ UNBOUNDED
The problem is unbounded.
@ ERROR
An error occurred.
@ OPTIMAL
The problem is optimal.
FormulaKind
Kinds of symbolic formulas.
Convenient structure representing a column in the LP solver.
Structure representing a row in the LP solver in the form of a linear combination of variables.