delpi  0.0.1
DElta-complete LP solver
Loading...
Searching...
No Matches
QsoptexLpSolver.cpp
1
6#include "delpi/solver/QsoptexLpSolver.h"
7
8#include <map>
9#include <ostream>
10#include <set>
11#include <span> // NOLINT(build/include_order): c++20 header
12#include <string>
13#include <unordered_map>
14#include <unordered_set>
15#include <utility>
16#include <vector>
17
18#include "delpi/util/error.h"
19#include "delpi/util/logging.h"
20
21namespace delpi {
22
23namespace {} // namespace
24
25extern "C" void QsoptexPartialSolutionCb(mpq_QSdata const* /*prob*/, const mpq_t* x, const mpq_t* const y,
26 const mpq_t obj_lb, const mpq_t obj_up, const mpq_t /* diff */,
27 const mpq_t /* delta */, const unsigned int precision, void* data) {
28 DELPI_DEBUG_FMT("QsoptexLpSolver::QsoptexPartialSolutionCb called with objective value in [{}, {}]",
29 mpq_class{obj_lb}, mpq_class{obj_up});
30 QsoptexLpSolver& lp_solver = *static_cast<QsoptexLpSolver*>(data);
31 lp_solver.UpdateStats(precision);
32 if (lp_solver.partial_solve_cb())
33 lp_solver.partial_solve_cb()(lp_solver, LpResult::DELTA_OPTIMAL, gmp::ToMpqVector(x, lp_solver.num_columns()),
34 gmp::ToMpqVector(y, lp_solver.num_rows()), mpq_class{obj_lb}, mpq_class{obj_up});
35}
36
37QsoptexLpSolver::QsoptexLpSolver(Config config, const std::string& class_name)
38 : LpSolver{0, 0, std::move(config), class_name},
39 qsx_{nullptr},
40 basis_{.nstruct = 0, .nrows = 0, .cstat = nullptr, .rstat = nullptr},
41 ray_{0},
42 x_{0} {
43 qsopt_ex::QSXStart();
44 ninfinity_ = mpq_class{mpq_NINFTY};
45 infinity_ = mpq_class{mpq_INFTY};
46
47 qsx_ = mpq_QScreate_prob(nullptr, QS_MIN);
48 DELPI_ASSERT(qsx_ != nullptr, "Failed to create QSopt_ex problem");
49 if (config_.verbose_simplex() > 3) {
50 DELPI_RUNTIME_ERROR("With --lp-solver qsoptex, maximum value for --verbose-simplex is 3");
51 }
52 [[maybe_unused]] const int status = mpq_QSset_param(qsx_, QS_PARAM_SIMPLEX_DISPLAY, config_.verbose_simplex());
53 DELPI_ASSERT(!status, "Invalid status");
54 DELPI_DEBUG_FMT("QsoptexTheorySolver::QsoptexTheorySolver: delta = {}", config_.delta());
55}
56
57QsoptexLpSolver::~QsoptexLpSolver() {
58 mpq_QSfree_prob(qsx_);
59 qsopt_ex::QSXFinish();
60}
61
62int QsoptexLpSolver::num_columns() const { return mpq_QSget_colcount(qsx_); }
63int QsoptexLpSolver::num_rows() const { return mpq_QSget_rowcount(qsx_); }
64
65Column QsoptexLpSolver::column(ColumnIndex column_idx) const {
66 DELPI_ASSERT(column_idx < num_columns(), "Column index out of bounds");
67 qsopt_ex::MpqArray obj, lb, ub;
68
69 [[maybe_unused]] const int status =
70 mpq_QSget_columns_list(qsx_, 1, &column_idx, nullptr, nullptr, nullptr, nullptr, obj, lb, ub, nullptr);
71 DELPI_ASSERT(!status, "Invalid status");
72
73 Column column{};
74 column.var = col_to_var_.at(column_idx);
75 // If there is a value, move it to the optional field, otherwise free the memory
76 if (!mpq_equal(lb[0], mpq_NINFTY)) column.lb = std::move(gmp::ToMpqClass(lb[0]));
77 if (!mpq_equal(ub[0], mpq_INFTY)) column.ub = std::move(gmp::ToMpqClass(ub[0]));
78 if (gmp::ToMpqClass(obj[0]) != 0) column.obj = std::move(gmp::ToMpqClass(obj[0]));
79
80 return column;
81}
82Row QsoptexLpSolver::row(RowIndex row_idx) const {
83 DELPI_ASSERT(row_idx < num_rows(), "Row index out of bounds");
84 qsopt_ex::MpqArray row_val, rhs;
85 int *row_cnt = nullptr, *row_ind = nullptr;
86 char* sense = nullptr;
87
88 [[maybe_unused]] const int status =
89 mpq_QSget_rows_list(qsx_, 1, &row_idx, &row_cnt, nullptr, &row_ind, row_val, rhs, &sense, nullptr);
90 DELPI_ASSERT(!status, "Invalid status");
91
92 Row row{};
93 const int non_zero_coefficient_count = row_cnt[0];
94 for (int i = 0; i < non_zero_coefficient_count; i++) {
95 row.addends.emplace_back(col_to_var_.at(row_ind[i]), std::move(gmp::ToMpqClass(row_val[i])));
96 DELPI_DEBUG_FMT("QsoptexTheorySolver::row: row[{}]({}) = {} * {}", row_idx, i, row.addends.back().second,
97 row.addends.back().first);
98 }
99
100 switch (sense[0]) {
101 case 'G':
102 row.lb = std::move(gmp::ToMpqClass(rhs[0]));
103 break;
104 case 'L':
105 row.ub = std::move(gmp::ToMpqClass(rhs[0]));
106 break;
107 case 'E':
108 row.lb = gmp::ToMpqClass(rhs[0]);
109 row.ub = std::move(gmp::ToMpqClass(rhs[0]));
110 break;
111 default:
112 DELPI_UNREACHABLE();
113 }
114
115 mpq_QSfree(row_cnt);
116 mpq_QSfree(row_ind);
117 mpq_QSfree(sense);
118
119 return row;
120}
121
122LpSolver::ColumnIndex QsoptexLpSolver::AddColumn(const Variable& var, const mpq_class& obj, const mpq_class& lb,
123 const mpq_class& ub) {
124 DELPI_ASSERT(!var_to_col_.contains(var), "Variable already exists in the LP.");
125 const int column_idx = num_columns();
126 var_to_col_.emplace(var, column_idx);
127 col_to_var_.emplace_back(var);
128 [[maybe_unused]] const int status = mpq_QSnew_col(qsx_, obj.get_mpq_t(), lb.get_mpq_t(), ub.get_mpq_t(), nullptr);
129 DELPI_ASSERT(!status, "Invalid status");
130 return column_idx;
131}
132LpSolver::RowIndex QsoptexLpSolver::AddRow(const std::vector<Expression::Addend>& addends, const mpq_class& lb,
133 const mpq_class& ub) {
134 // Add the row to the LP. If the row is bounded both ways with an equality, we can add it in one go.
135 if (lb == ub) return AddRow(addends, 'E', lb);
136
137 // Else, add the two bounds separately
138 if (!mpq_equal(lb.get_mpq_t(), mpq_NINFTY)) AddRow(addends, 'G', lb);
139 if (!mpq_equal(ub.get_mpq_t(), mpq_INFTY)) AddRow(addends, 'L', ub);
140 return num_rows() - 1;
141}
142
143LpSolver::RowIndex QsoptexLpSolver::AddRow(const Expression::Addends& lhs, const FormulaKind sense,
144 const mpq_class& rhs) {
145 char qsoptex_sense;
146 switch (sense) {
147 case FormulaKind::Leq:
148 qsoptex_sense = 'L';
149 break;
150 case FormulaKind::Eq:
151 qsoptex_sense = 'E';
152 break;
153 case FormulaKind::Geq:
154 qsoptex_sense = 'G';
155 break;
156 default:
157 DELPI_UNREACHABLE();
158 }
159
160 return AddRow(lhs, qsoptex_sense, rhs);
161}
162void QsoptexLpSolver::SetBound(const Variable var, const mpq_class& lb, const mpq_class& ub) {
163 if (lb == ub) {
164 [[maybe_unused]] const int status = mpq_QSchange_bound(qsx_, var_to_col_.at(var), 'B', lb.get_mpq_t());
165 DELPI_ASSERT(!status, "Invalid status");
166 return;
167 }
168 [[maybe_unused]] const int status1 = mpq_QSchange_bound(qsx_, var_to_col_.at(var), 'L', lb.get_mpq_t());
169 DELPI_ASSERT(!status1, "Invalid status");
170 [[maybe_unused]] const int status2 = mpq_QSchange_bound(qsx_, var_to_col_.at(var), 'U', ub.get_mpq_t());
171 DELPI_ASSERT(!status2, "Invalid status");
172}
173
174void QsoptexLpSolver::SetObjective(const int column, const mpq_class& value) {
175 DELPI_ASSERT_FMT(column < num_columns(), "Column index out of bounds: {} >= {}", column, num_columns());
176 [[maybe_unused]] const int status = mpq_QSchange_objcoef(qsx_, column, mpq_class{value}.get_mpq_t());
177 DELPI_ASSERT(!status, "Invalid status");
178}
179
181 // x: must be allocated/deallocated using QSopt_ex.
182 // Should have room for the (rowcount) "logical" variables, which come after the (colcount) "structural" variables.
183 x_.Resize(num_columns());
184 ray_.Resize(num_rows());
185
186 unsigned int precision;
187 int lp_status = -1;
188 const int status = QSdelta_full_solver(qsx_, mpq_class{config_.delta()}.get_mpq_t(), x_, ray_, obj_lb_.get_mpq_t(),
189 obj_ub_.get_mpq_t(), &basis_, PRIMAL_SIMPLEX, &lp_status, &precision,
190 config_.continuous_output() ? QsoptexPartialSolutionCb : nullptr, this);
191
192 if (status) {
193 DELPI_RUNTIME_ERROR_FMT("QSopt_ex returned {}", status);
194 return LpResult::ERROR;
195 }
196
197 DELPI_DEBUG_FMT("DeltaQsoptexTheorySolver::CheckSat: QSopt_ex has returned with precision = {}", precision);
198
199 UpdateStats(precision);
200 switch (lp_status) {
201 case QS_LP_OPTIMAL:
202 case QS_LP_DELTA_OPTIMAL:
204 return lp_status == QS_LP_OPTIMAL ? LpResult::OPTIMAL : LpResult::DELTA_OPTIMAL;
205 case QS_LP_UNBOUNDED:
207 return LpResult::UNBOUNDED;
208 case QS_LP_INFEASIBLE:
209#if 0
210 if (store_solution) UpdateInfeasible();
211#endif
213 case QS_LP_UNSOLVED:
214 DELPI_ERROR("DeltaQsoptexTheorySolver::CheckSat: QSopt_ex failed to return a result");
215 return LpResult::ERROR;
216 case QS_LP_ITER_LIMIT:
217 DELPI_ERROR("DeltaQsoptexTheorySolver::CheckSat: Iteration limit reached");
218 return LpResult::ERROR;
219 default:
220 DELPI_UNREACHABLE();
221 }
222}
223
225 DELPI_ASSERT(solution_.empty(), "Solution must be empty");
226 DELPI_ASSERT(dual_solution_.empty(), "Dual solution must be empty");
227 // Set the feasible information
228 const int colcount = num_columns();
229 const int rowcount = num_rows();
230 solution_.reserve(colcount);
231 dual_solution_.reserve(rowcount);
232
233 for (int i = 0; i < colcount; i++) solution_.emplace_back(x_[i]);
234 for (int i = 0; i < rowcount; i++) dual_solution_.emplace_back(ray_[i]);
235}
236void QsoptexLpSolver::EnsureSenseCore() { mpq_QSchange_objsense(qsx_, is_min_ ? QS_MIN : QS_MAX); }
237
238void QsoptexLpSolver::UpdateStats(unsigned int precision) {
239 stats_.precision = precision;
240 stats_.refinements = 0;
241}
242#if 0
243void QsoptexLpSolver::UpdateInfeasible() {
244 DELPI_ASSERT(infeasible_rows_.empty(), "Infeasible rows must be empty");
245 DELPI_ASSERT(infeasible_bounds_.empty(), "Infeasible bounds must be empty");
246 // Set the infeasible information
247 const int rowcount = num_rows();
248 const int colcount = num_columns();
249
250 // Add the non-zero rows to the infeasible core
251 for (int i = 0; i < rowcount; i++) {
252 if (mpq_sgn(ray_[i]) == 0) continue;
253 DELPI_TRACE_FMT("QsoptexLpSolver::NotifyInfeasible: ray[{}] = {}", i, gmp::ToMpqClass(ray_[i]));
254 infeasible_rows_.emplace_back(i);
255 }
256 // Multiply the Farkas ray by the row coefficients to get the column violations: ray * A
257 // If the result is non-zero, the sign indicates the bound that caused the violation.
258 mpq_class col_violation{0};
259 mpq_t row_coeff;
260 mpq_init(row_coeff);
261 for (int i = 0; i < colcount; i++) {
262 col_violation = 0;
263 for (int j = 0; j < rowcount; j++) {
264 mpq_QSget_coef(qsx_, j, i, &row_coeff);
265 col_violation += gmp::ToMpqClass(ray_[j]) * gmp::ToMpqClass(row_coeff);
266 }
267 if (col_violation == 0) continue;
268 DELPI_TRACE_FMT("QsoptexLpSolver::NotifyInfeasible: {}[{}] = {}", col_to_var_.at(i), i, col_violation);
269 infeasible_bounds_.emplace_back(i, col_violation > 0);
270 }
271 mpq_clear(row_coeff);
272}
273#endif
274
275template <TypedIterable<std::pair<const Variable, mpq_class>> T>
276void QsoptexLpSolver::SetRowCoeff(int row, const T& literal_monomials) {
277 for (const auto& [var, coeff] : literal_monomials) SetVarCoeff(row, var, coeff);
278}
279
280template <TypedIterable<std::pair<const Variable, mpq_class>> T>
281int QsoptexLpSolver::AddRow(const T& lhs, const char sense, const mpq_class& rhs) {
282 DELPI_TRACE_FMT("QsoptexLpSolver::AddRow(#{}, {}, {})", lhs.size(), sense, rhs);
283 DELPI_ASSERT(sense == 'L' || sense == 'G' || sense == 'E', "Invalid sense");
284 std::vector<int> row_indices;
285 row_indices.reserve(lhs.size());
286 qsopt_ex::MpqArray values{lhs.size()};
287 for (auto& [var, coeff] : lhs) {
288 const int column_idx = var_to_col_.at(var);
289 mpq_set(values[row_indices.size()], coeff.get_mpq_t());
290 row_indices.emplace_back(column_idx);
291 }
292 mpq_t c_rhs;
293 mpq_init(c_rhs);
294 mpq_set(c_rhs, rhs.get_mpq_t());
295
296 [[maybe_unused]] const int status =
297 mpq_QSadd_row(qsx_, static_cast<int>(lhs.size()), row_indices.data(), values, &c_rhs, sense, nullptr);
298 DELPI_ASSERT(!status, "Invalid status");
299
300 mpq_clear(c_rhs);
301 return num_rows() - 1;
302}
303
304void QsoptexLpSolver::SetVarCoeff(const int row, const Variable& var, const mpq_class& value) const {
305 DELPI_ASSERT_FMT(var_to_col_.contains(var), "Variable {} not found in the LP. Did you add it before?", var);
306 const int column = var_to_col_.at(var);
307 // Variable has the coefficients too large
308 if (value <= ninfinity_ || value >= infinity_) DELPI_RUNTIME_ERROR_FMT("LP coefficient too large: {}", value);
309
310 [[maybe_unused]] const int status = mpq_QSchange_coef(qsx_, row, column, mpq_class{value}.get_mpq_t());
311 DELPI_ASSERT(!status, "Invalid status");
312}
313
314#ifndef NDEBUG
315void QsoptexLpSolver::Dump() {
316 mpq_QSdump_prob(qsx_);
317 mpq_QSdump_basis(qsx_);
318 mpq_QSdump_bfeas(qsx_);
319}
320#endif
321
322void QsoptexLpSolver::SetCoefficient(const int row, const int column, const mpq_class& value) {
323 DELPI_ASSERT_FMT(row < num_rows(), "Row index out of bounds: {} >= {}", row, num_rows());
324 DELPI_ASSERT_FMT(column < num_columns(), "Column index out of bounds: {} >= {}", column, num_columns());
325 DELPI_ASSERT_FMT(value <= infinity_ && value >= ninfinity_, "LP coefficient too large: {}", value);
326
327 [[maybe_unused]] const int status = mpq_QSchange_coef(qsx_, row, column, mpq_class{value}.get_mpq_t());
328 DELPI_ASSERT(!status, "Invalid status");
329}
330
331template void QsoptexLpSolver::SetRowCoeff(int, const std::vector<std::pair<const Variable, mpq_class>>&);
332template void QsoptexLpSolver::SetRowCoeff(int, const std::set<std::pair<const Variable, mpq_class>>&);
333template void QsoptexLpSolver::SetRowCoeff(int, const std::unordered_set<std::pair<const Variable, mpq_class>>&);
334template void QsoptexLpSolver::SetRowCoeff(int, const std::span<std::pair<const Variable, mpq_class>>&);
335template void QsoptexLpSolver::SetRowCoeff(int, const std::map<Variable, mpq_class>&);
336template void QsoptexLpSolver::SetRowCoeff(int, const std::unordered_map<Variable, mpq_class>&);
337
338template int QsoptexLpSolver::AddRow(const std::vector<std::pair<const Variable, mpq_class>>&, char, const mpq_class&);
339template int QsoptexLpSolver::AddRow(const std::set<std::pair<const Variable, mpq_class>>&, char, const mpq_class&);
340template int QsoptexLpSolver::AddRow(const std::unordered_set<std::pair<const Variable, mpq_class>>&, char,
341 const mpq_class&);
342template int QsoptexLpSolver::AddRow(const std::span<std::pair<const Variable, mpq_class>>&, char, const mpq_class&);
343template int QsoptexLpSolver::AddRow(const std::map<Variable, mpq_class>&, char, const mpq_class&);
344template int QsoptexLpSolver::AddRow(const std::unordered_map<Variable, mpq_class>&, char, const mpq_class&);
345
346} // namespace delpi
Simple dataclass used to store the configuration of the program.
Definition Config.h:36
Facade class that hides the underlying LP solver used by delpi.
Definition LpSolver.h:60
const Variable & var(const int column) const
Shorthand notation to get the real variable linked with column column.
Definition LpSolver.h:205
mpq_class infinity_
Infinity threshold value.
Definition LpSolver.h:553
bool is_min_
Whether this is a minimization or maximization LP problem.
Definition LpSolver.h:551
mpq_class ninfinity_
Negative infinity threshold value.
Definition LpSolver.h:552
Config config_
Configuration to use.
Definition LpSolver.h:533
std::vector< mpq_class > solution_
Solution vector.
Definition LpSolver.h:543
std::unordered_map< Variable, int > var_to_col_
Theory column ⇔ Variable.
Definition LpSolver.h:537
mpq_class obj_ub_
Upper bound on the objective value, if any.
Definition LpSolver.h:546
std::vector< mpq_class > dual_solution_
Dual solution vector.
Definition LpSolver.h:544
mpq_class obj_lb_
Lower bound on the objective value, if any.
Definition LpSolver.h:545
std::vector< Variable > col_to_var_
Literal ⇔ lp row.
Definition LpSolver.h:540
LpStats stats_
Statistics of the solver.
Definition LpSolver.h:534
Linear programming solver using QSopt_ex.
void SetObjective(int column, const mpq_class &value) override
The the objective coefficient of the given column to the given value.
void SetRowCoeff(int row, const T &literal_monomials)
Parse a sequence of literal_monomials and set the coefficient for each decisional variable appearing ...
void UpdateFeasible()
Use the result from the lp solver to update the solution vector and objective value.
qsopt_ex::MpqArray ray_
Ray of the last infeasible solution.
void UpdateStats(unsigned int precision)
Update the lp stats from the QSopt_ex solver.
void SetVarCoeff(int row, const Variable &var, const mpq_class &value) const
Set the coefficients to apply to var on a specific row.
void EnsureSenseCore() override
Make sure the LP solvers are aware of the sense of the LP problem (minimisation or maximisation).
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 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 SetCoefficient(RowIndex row, ColumnIndex column, const mpq_class &value) override
Set the coefficient of the row constraint to apply at the column decisional variable.
QSbasis basis_
Last basis.
qsopt_ex::MpqArray x_
Solution vector.
mpq_QSprob qsx_
QSopt_ex LP solver.
Column column(int column_idx) const override
Get the column at the given column_idx index.
LpResult SolveCore() override
Internal method that optimises the LP problem with the given delta.
Row row(int row_idx) const override
Get the row at the given row_idx index.
Real symbolic variable.
Definition Variable.h:20
A wrapper around an array of mpq_t elements.
Definition qsopt_ex.h:66
Global namespace for the delpi library.
LpResult
Possible outcomes of the LP solver.
Definition LpResult.h:14
@ INFEASIBLE
The problem is infeasible.
Definition LpResult.h:19
@ DELTA_OPTIMAL
The delta-relaxation of the problem is optimal.
Definition LpResult.h:17
@ UNBOUNDED
The problem is unbounded.
Definition LpResult.h:18
@ ERROR
An error occurred.
Definition LpResult.h:20
@ OPTIMAL
The problem is optimal.
Definition LpResult.h:16
FormulaKind
Kinds of symbolic formulas.
Definition FormulaKind.h:14
Convenient structure representing a column in the LP solver.
Definition Column.h:23
Structure representing a row in the LP solver in the form of a linear combination of variables.
Definition Row.h:24