LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lp_highs.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_LP_HIGHS_H
6#define LINE_UTIL_LP_HIGHS_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * A sparse LP backend for line::lp::LpModel, on HiGHS (MIT).
12 *
13 * WHY IT EXISTS. util/simplex.h carries a DENSE tableau. That is the right
14 * choice for what it was written for -- it is exact under line::Rational, uses
15 * Bland's rule with no tolerance, and needs no dependency -- but it costs
16 * O(rows * cols) per pivot, so it clears a few hundred columns and stalls on a
17 * few thousand. The QRF blocking bounds are MR * B^2 columns
18 * (mapqn_qr_bounds_bas.h) and B^2 (mapqn_qr_bounds_rsrd.h), which is 3894 for
19 * even example_bas_small.m and tens of thousands for the paper instances. This
20 * backend is what makes those reachable.
21 *
22 * IT IS NOT A REPLACEMENT, AND MUST NOT BECOME ONE. HiGHS is double precision.
23 * Four headers in this tree promise that at T = line::Rational the returned
24 * value is the EXACT optimum of the exact polytope, and the mapqn tests assert
25 * that as equalities on fractions (3/4, 2/3, 3/7, 6/7), not as tolerances.
26 * Routing Rational here would silently turn those equalities into rounding.
27 * So the dispatcher below refuses any T other than double, at compile time.
28 *
29 * PRESOLVE IS LEFT ON but the caller should know it exists: the QRF equality
30 * blocks are heavily redundant (matlab/lib/qrf/qrf_independent_rows.m selects a
31 * maximal independent subset by pivoted QR precisely because linprog struggles
32 * otherwise), and presolve is what absorbs that redundancy here. If a model
33 * ever comes back Infeasible where the dense solver says Optimal, re-run it
34 * with presolve off before believing the answer -- that is the analogue of the
35 * `adaptive_rho` trap recorded for the OSQP-backed bounds in _kb.
36 */
37
38#include <cstddef>
39#include <vector>
40
41#include "line/util/simplex.h"
42
43#ifdef LINE_MP_HAVE_HIGHS
44#include "Highs.h"
45#endif
46
47namespace line {
48namespace lp {
49
50/** True when a sparse backend is compiled in. */
51inline bool highs_available() {
52#ifdef LINE_MP_HAVE_HIGHS
53 return true;
54#else
55 return false;
56#endif
57}
58
59#ifdef LINE_MP_HAVE_HIGHS
60
61/**
62 * Solve a double-precision LpModel with HiGHS.
63 *
64 * The translation is direct: LpModel already stores rows in compressed sparse
65 * row form, which is one of the two layouts HiGHS accepts, so the matrix is
66 * handed over without a transpose. Free bounds become +-kHighsInf.
67 */
68inline LpSolution<double> highs_solve(const LpModel<double>& model) {
69 LpSolution<double> out;
70 const std::size_t n = model.num_vars(), m = model.num_rows();
71
72 HighsModel hm;
73 hm.lp_.num_col_ = static_cast<HighsInt>(n);
74 hm.lp_.num_row_ = static_cast<HighsInt>(m);
75 hm.lp_.sense_ = model.maximize() ? ObjSense::kMaximize : ObjSense::kMinimize;
76
77 hm.lp_.col_cost_ = model.costs();
78 hm.lp_.col_lower_.resize(n);
79 hm.lp_.col_upper_.resize(n);
80 for (std::size_t j = 0; j < n; ++j) {
81 hm.lp_.col_lower_[j] = model.lower_is_free(j) ? -kHighsInf : model.lower(j);
82 hm.lp_.col_upper_[j] = model.upper_is_free(j) ? kHighsInf : model.upper(j);
83 }
84
85 hm.lp_.row_lower_.resize(m);
86 hm.lp_.row_upper_.resize(m);
87 // HighsSparseMatrix carries its OWN dimensions and they are NOT inferred
88 // from HighsLp. Leaving them at 0 makes HiGHS read an empty matrix, so
89 // every equality row degenerates to 0 = rhs and the model comes back
90 // Infeasible while the dense tableau solves it happily.
91 hm.lp_.a_matrix_.num_col_ = static_cast<HighsInt>(n);
92 hm.lp_.a_matrix_.num_row_ = static_cast<HighsInt>(m);
93 hm.lp_.a_matrix_.format_ = MatrixFormat::kRowwise;
94 // HighsSparseMatrix DEFAULT-CONSTRUCTS with start_ = {0}. Appending the
95 // leading zero without clearing yields start_ = {0, 0, nnz}, so HiGHS reads
96 // row 0 as spanning [0,0) and reports "0 nonzeros" for the whole matrix --
97 // every equality then degenerates to 0 = rhs and the model is Infeasible.
98 // The symptom is a clean Infeasible on a model the dense tableau solves.
99 hm.lp_.a_matrix_.start_.clear();
100 hm.lp_.a_matrix_.start_.reserve(m + 1);
101 hm.lp_.a_matrix_.index_.reserve(model.num_nonzeros());
102 hm.lp_.a_matrix_.value_.reserve(model.num_nonzeros());
103 hm.lp_.a_matrix_.start_.push_back(0);
104 for (std::size_t i = 0; i < m; ++i) {
105 const double b = model.rhs(i);
106 switch (model.sense(i)) {
107 case LpSense::LE:
108 hm.lp_.row_lower_[i] = -kHighsInf;
109 hm.lp_.row_upper_[i] = b;
110 break;
111 case LpSense::GE:
112 hm.lp_.row_lower_[i] = b;
113 hm.lp_.row_upper_[i] = kHighsInf;
114 break;
115 default:
116 hm.lp_.row_lower_[i] = b;
117 hm.lp_.row_upper_[i] = b;
118 break;
119 }
120 for (std::size_t k = model.row_begin(i); k < model.row_end(i); ++k) {
121 hm.lp_.a_matrix_.index_.push_back(static_cast<HighsInt>(model.col_at(k)));
122 hm.lp_.a_matrix_.value_.push_back(model.val_at(k));
123 }
124 hm.lp_.a_matrix_.start_.push_back(static_cast<HighsInt>(hm.lp_.a_matrix_.index_.size()));
125 }
126
127 Highs highs;
128 highs.setOptionValue("output_flag", false);
129 // kWarning is BENIGN and common -- HiGHS raises it for things like an
130 // unscaled model or a presolve remark, and the solve still returns an
131 // optimal basis. Treating anything other than kOk as failure made every
132 // feasible model here report Infeasible while the dense tableau solved it.
133 // Only kError means the call did not happen.
134 if (highs.passModel(hm) == HighsStatus::kError) {
135 out.status = LpStatus::Infeasible;
136 return out;
137 }
138 if (highs.run() == HighsStatus::kError) {
139 out.status = LpStatus::Infeasible;
140 return out;
141 }
142
143 const HighsModelStatus st = highs.getModelStatus();
144 if (st == HighsModelStatus::kOptimal) {
145 out.status = LpStatus::Optimal;
146 } else if (st == HighsModelStatus::kUnbounded) {
147 out.status = LpStatus::Unbounded;
148 return out;
149 } else if (st == HighsModelStatus::kIterationLimit ||
150 st == HighsModelStatus::kTimeLimit) {
151 out.status = LpStatus::IterationLimit;
152 return out;
153 } else {
154 out.status = LpStatus::Infeasible;
155 return out;
156 }
157
158 out.objective = highs.getInfo().objective_function_value;
159 out.iterations = static_cast<std::size_t>(highs.getInfo().simplex_iteration_count);
160 out.x = highs.getSolution().col_value;
161 return out;
162}
163
164#endif // LINE_MP_HAVE_HIGHS
165
166/**
167 * Solve, choosing the backend by arithmetic and size.
168 *
169 * Rational and every extended-precision T always take the exact dense path, at
170 * compile time, because that is where this tree's exactness guarantees live.
171 * A double model goes to HiGHS only once it is wider than `dense_max_cols`,
172 * so small models keep bit-for-bit the answers their goldens were taken with
173 * and the two backends stay comparable on exactly the instances the tests use.
174 */
175template <class T>
176LpSolution<T> lp_solve(const LpModel<T>& model, std::size_t dense_max_cols = 512) {
177 (void)dense_max_cols;
178 return simplex_solve(model);
179}
180
181#ifdef LINE_MP_HAVE_HIGHS
182template <>
183inline LpSolution<double> lp_solve(const LpModel<double>& model, std::size_t dense_max_cols) {
184 if (model.num_vars() > dense_max_cols) return highs_solve(model);
185 return simplex_solve(model);
186}
187#endif
188
189} // namespace lp
190} // namespace line
191
192#endif // LINE_UTIL_LP_HIGHS_H
Sparse LP in the natural form, with per-variable bounds.
Definition simplex.h:112
LpSolution< T > lp_solve(const LpModel< T > &model, std::size_t dense_max_cols=512)
Solve, choosing the backend by arithmetic and size.
Definition lp_highs.h:176
bool highs_available()
True when a sparse backend is compiled in.
Definition lp_highs.h:51
LpSolution< T > simplex_solve(const LpModel< T > &model, std::size_t max_iterations=0)
Solve the model.
Definition simplex.h:286
@ Infeasible
phase 1 ended with residual artificial mass
Definition simplex.h:78
@ Unbounded
an improving column has no blocking row
Definition simplex.h:79
@ Optimal
an optimal vertex was reached
Definition simplex.h:77
@ IterationLimit
the iteration cap was hit (cannot happen under Bland's rule with exact arithmetic; a guard for inexac...
Definition simplex.h:80
Templated primal simplex with Bland's rule.