/* This file is part of solidity. solidity is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. solidity is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with solidity. If not, see . */ // SPDX-License-Identifier: GPL-3.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace solidity; using namespace solidity::util; using rational = boost::rational; namespace { /// Disjunctively combined two vectors of bools. inline std::vector& operator|=(std::vector& _x, std::vector const& _y) { solAssert(_x.size() == _y.size(), ""); for (size_t i = 0; i < _x.size(); ++i) if (_y[i]) _x[i] = true; return _x; } /** * Simplex tableau. */ struct Tableau { /// The factors of the objective function (first row of the tableau) LinearExpression objective; /// The tableau matrix (equational form). std::vector data; }; string toString(rational const& _x) { if (_x.denominator() == 1) return ::toString(_x.numerator()); else return ::toString(_x.numerator()) + "/" + ::toString(_x.denominator()); } string reasonToString(ReasonSet const& _reasons, size_t _minSize) { auto reasonsAsStrings = _reasons | ranges::views::transform([](size_t _r) { return to_string(_r); }); string result = "[" + joinHumanReadable(reasonsAsStrings) + "]"; if (result.size() < _minSize) result.resize(_minSize, ' '); return result; } /* string toString(LinearExpression const& _expr) { vector items; for (auto&& multiplier: _expr) if (multiplier != 0) items.emplace_back(::toString(multiplier)); else items.emplace_back("_"); for (string& item: items) while (item.size() < 3) item = " " + item; return joinHumanReadable(items, " "); } string toString(Tableau const& _tableau) { string s = toString(_tableau.objective) + "\n"; for (auto&& d: _tableau.data) s += toString(d) + "\n"; return s; } */ /// Adds slack variables to remove non-equality costraints from a set of constraints /// and returns the data part of the tableau / constraints. /// The second return variable is true if the original input had any equality constraints. pair, bool> toEquationalForm(vector _constraints) { size_t varsNeeded = static_cast(ranges::count_if(_constraints, [](Constraint const& _c) { return !_c.equality; })); if (varsNeeded > 0) { size_t columns = _constraints.at(0).data.size(); size_t varsAdded = 0; for (Constraint& constraint: _constraints) { solAssert(constraint.data.size() == columns, ""); constraint.data.resize(columns + varsNeeded); if (!constraint.equality) { constraint.equality = true; constraint.data[columns + varsAdded] = bigint(1); varsAdded++; } } solAssert(varsAdded == varsNeeded); } vector data; for (Constraint& c: _constraints) data.emplace_back(move(c.data)); return make_pair(move(data), varsNeeded < _constraints.size()); } /// Finds the simplex pivot column: The column with the largest positive objective factor. /// If all objective factors are zero or negative, the optimum has been found and nullopt is returned. optional findPivotColumn(Tableau const& _tableau) { auto&& [maxColumn, maxValue] = ranges::max( _tableau.objective | ranges::views::enumerate | ranges::views::tail, {}, [](std::pair const& _x) { return _x.second; } ); if (maxValue <= rational{0}) return nullopt; // found optimum else return maxColumn; } /// Finds the simplex pivot row, given the column: /// If there is no positive factor in the column, the problem is unbounded, nullopt is returned. /// Otherwise, returns the row i such that c[i] / x[i] is minimal and x[i] is positive, where /// c[i] is the constant factor (not the objective factor!) in row i. optional findPivotRow(Tableau const& _tableau, size_t _pivotColumn) { auto positiveColumnEntries = ranges::views::iota(size_t(0), _tableau.data.size()) | ranges::views::transform([&](size_t i) { return make_pair(i, _tableau.data[i][_pivotColumn]); }) | ranges::views::filter([](pair const& _entry) { return _entry.second.numerator() > 0; }); if (positiveColumnEntries.empty()) return nullopt; // unbounded return ranges::min( positiveColumnEntries, {}, [&](std::pair const& _entry) { return _tableau.data[_entry.first][0] / _entry.second; } ).first; } /// Performs equivalence transform on @a _tableau, so that /// the column @a _pivotColumn is all zeros (including the objective row) except for @a _pivotRow, /// where it is 1. void performPivot(Tableau& _tableau, size_t _pivotRow, size_t _pivotColumn) { rational pivot = _tableau.data[_pivotRow][_pivotColumn]; solAssert(pivot != 0, ""); if (pivot != 1) _tableau.data[_pivotRow] /= pivot; solAssert(_tableau.data[_pivotRow][_pivotColumn] == rational(1), ""); LinearExpression const& _pivotRowData = _tableau.data[_pivotRow]; auto subtractMultipleOfPivotRow = [&](LinearExpression& _row) { if (_row[_pivotColumn] == rational{1}) _row -= _pivotRowData; else if (_row[_pivotColumn]) _row -= _row[_pivotColumn] * _pivotRowData; }; subtractMultipleOfPivotRow(_tableau.objective); for (size_t i = 0; i < _tableau.data.size(); ++i) if (i != _pivotRow) subtractMultipleOfPivotRow(_tableau.data[i]); } /// Transforms the tableau such that the last vectors are basis vectors /// and their objective coefficients are zero. /// Makes various assumptions and should only be used after adding /// a certain number of slack variables. void selectLastVectorsAsBasis(Tableau& _tableau) { // We might skip the operation for a column if it is already the correct // unit vector and its objective coefficient is zero. size_t columns = _tableau.objective.size(); size_t rows = _tableau.data.size(); for (size_t i = 0; i < rows; ++i) performPivot(_tableau, i, columns - rows + i); } /// If column @a _column inside tableau is a basis vector /// (i.e. one entry is 1, the others are 0), returns the index /// of the 1, otherwise nullopt. optional basisIndex(Tableau const& _tableau, size_t _column) { optional row; for (size_t i = 0; i < _tableau.data.size(); ++i) if (_tableau.data[i][_column] == bigint(1)) { if (row) return std::nullopt; else row = i; } else if (_tableau.data[i][_column] != 0) return std::nullopt; return row; } /// @returns a solution vector, assuming one exists. /// The solution vector minimizes the objective function if the tableau /// is the result of the simplex algorithm. vector solutionVector(Tableau const& _tableau) { vector result; vector rowsSeen(_tableau.data.size(), false); for (size_t j = 1; j < _tableau.objective.size(); j++) { optional row = basisIndex(_tableau, j); if (row && rowsSeen[*row]) row = nullopt; result.emplace_back(row ? _tableau.data[*row][0] : rational{}); if (row) rowsSeen[*row] = true; } return result; } /// Solve the LP A x = b s.t. min c^T x /// Here, c is _tableau.objective and the first column of _tableau.data /// encodes b and the other columns encode A /// Assumes the tableau has a trivial basic feasible solution. /// Tries for a number of iterations and then gives up. pair simplexEq(Tableau _tableau) { size_t const iterations = min(60, 50 + _tableau.objective.size() * 2); for (size_t step = 0; step <= iterations; ++step) { optional pivotColumn = findPivotColumn(_tableau); if (!pivotColumn) return make_pair(LPResult::Feasible, move(_tableau)); optional pivotRow = findPivotRow(_tableau, *pivotColumn); if (!pivotRow) return make_pair(LPResult::Unbounded, move(_tableau)); performPivot(_tableau, *pivotRow, *pivotColumn); } return make_pair(LPResult::Unknown, Tableau{}); } /// We add slack variables to find a basic feasible solution. /// In particular, there is a slack variable for each row /// which is weighted negatively. Setting the new slack /// variables to one and all other variables to zero yields /// a basic feasible solution. /// If the optimal solution has all slack variables set to zero, /// this is a basic feasible solution. Otherwise, the original /// problem is infeasible. /// This function returns the modified tableau with the original /// objective function and the slack variables removed. pair simplexPhaseI(Tableau _tableau) { LinearExpression originalObjective = _tableau.objective; size_t rows = _tableau.data.size(); size_t columns = _tableau.objective.size(); for (size_t i = 0; i < rows; ++i) { if (_tableau.data[i][0] < 0) _tableau.data[i] *= -1; _tableau.data[i].resize(columns + rows); _tableau.data[i][columns + i] = 1; } _tableau.objective = {}; _tableau.objective.resize(columns); _tableau.objective.resize(columns + rows, rational{-1}); // This sets the objective factors of the slack variables // to zero (and thus selects a basic feasible solution). selectLastVectorsAsBasis(_tableau); LPResult result; tie(result, _tableau) = simplexEq(move(_tableau)); solAssert(result != LPResult::Infeasible, ""); vector optimum = solutionVector(_tableau); // If the solution needs a nonzero factor for a slack variable, // the original system is infeasible. for (size_t i = columns - 1; i < optimum.size(); ++i) if (optimum[i] != 0) return make_pair(LPResult::Infeasible, Tableau{}); // Restore original objective and remove slack variables. _tableau.objective = move(originalObjective); for (auto& row: _tableau.data) row.resize(columns); return make_pair(LPResult::Feasible, move(_tableau)); } /// Returns true if the all-zero solution is not a solution for the tableau. bool needsPhaseI(Tableau const& _tableau) { for (auto const& row: _tableau.data) if (row[0] < 0) return true; return false; } /// Solve the LP Ax <= b s.t. min c^Tx pair> simplex(vector _constraints, LinearExpression _objectives) { Tableau tableau; tableau.objective = move(_objectives); bool hasEquations = false; tie(tableau.data, hasEquations) = toEquationalForm(_constraints); tableau.objective.resize(tableau.data.at(0).size()); if (hasEquations || needsPhaseI(tableau)) { LPResult result; tie(result, tableau) = simplexPhaseI(move(tableau)); if (result == LPResult::Infeasible || result == LPResult::Unknown) return make_pair(result, vector{}); solAssert(result == LPResult::Feasible, ""); } // We know that the system is satisfiable and we know a solution, // but it might not be optimal. LPResult result; tie(result, tableau) = simplexEq(move(tableau)); solAssert(result != LPResult::Infeasible, ""); return make_pair(result, solutionVector(tableau)); } /// Turns all bounds into constraints. /// @returns false if the bounds make the state infeasible. optional boundsToConstraints(SolvingState& _state) { size_t columns = _state.variableNames.size(); // Bound zero should not exist because the variable zero does not exist. for (auto const& [varIndex, bounds]: _state.bounds | ranges::views::enumerate | ranges::views::tail) { if (bounds.lower && bounds.upper) { if (*bounds.lower > *bounds.upper) return bounds.lowerReasons + bounds.upperReasons; if (*bounds.lower == *bounds.upper) { LinearExpression c; c.resize(columns); c[0] = *bounds.lower; c[varIndex] = bigint(1); _state.constraints.emplace_back(Constraint{move(c), true, bounds.lowerReasons + bounds.upperReasons}); continue; } } if (bounds.lower && *bounds.lower > 0) { LinearExpression c; c.resize(columns); c[0] = -*bounds.lower; c[varIndex] = bigint(-1); _state.constraints.emplace_back(Constraint{move(c), false, move(bounds.lowerReasons)}); } if (bounds.upper) { LinearExpression c; c.resize(columns); c[0] = *bounds.upper; c[varIndex] = bigint(1); _state.constraints.emplace_back(Constraint{move(c), false, move(bounds.upperReasons)}); } } _state.bounds.clear(); return nullopt; } /// Removes incides set to true from a vector-like data structure. template void eraseIndices(T& _data, vector const& _indicesToRemove) { T result; for (size_t i = 0; i < _data.size(); i++) if (!_indicesToRemove[i]) result.push_back(move(_data[i])); _data = move(result); } void removeColumns(SolvingState& _state, vector const& _columnsToRemove) { eraseIndices(_state.bounds, _columnsToRemove); for (Constraint& constraint: _state.constraints) eraseIndices(constraint.data, _columnsToRemove); eraseIndices(_state.variableNames, _columnsToRemove); } auto nonZeroEntriesInColumn(SolvingState const& _state, size_t _column) { return _state.constraints | ranges::views::enumerate | ranges::views::filter([=](auto const& _entry) { return _entry.second.data[_column]; }) | ranges::views::transform([](auto const& _entry) { return _entry.first; }); } /// @returns vectors of column- and row-indices that are connected to the given column, /// in the sense of variables occurring in a constraint and constraints for variables. pair, vector> connectedComponent(SolvingState const& _state, size_t _column) { solAssert(_state.variableNames.size() >= 2, ""); vector includedColumns(_state.variableNames.size(), false); vector seenColumns(_state.variableNames.size(), false); vector includedRows(_state.constraints.size(), false); stack columnsToProcess; columnsToProcess.push(_column); while (!columnsToProcess.empty()) { size_t column = columnsToProcess.top(); columnsToProcess.pop(); if (includedColumns[column]) continue; includedColumns[column] = true; for (size_t row: nonZeroEntriesInColumn(_state, column)) { if (includedRows[row]) continue; includedRows[row] = true; for (auto const& [index, entry]: _state.constraints[row].data | ranges::views::enumerate | ranges::views::tail) if (entry && !seenColumns[index]) { seenColumns[index] = true; columnsToProcess.push(index); } } } return make_pair(move(includedColumns), move(includedRows)); } void normalizeRowLengths(SolvingState& _state) { size_t vars = max(_state.variableNames.size(), _state.bounds.size()); for (Constraint const& c: _state.constraints) vars = max(vars, c.data.size()); _state.variableNames.resize(vars); _state.bounds.resize(vars); for (Constraint& c: _state.constraints) c.data.resize(vars); } } bool Constraint::operator<(Constraint const& _other) const { if (equality != _other.equality) return equality < _other.equality; for (size_t i = 0; i < max(data.size(), _other.data.size()); ++i) if (rational diff = data.get(i) - _other.data.get(i)) return diff < 0; return false; } bool Constraint::operator==(Constraint const& _other) const { if (equality != _other.equality) return false; for (size_t i = 0; i < max(data.size(), _other.data.size()); ++i) if (data.get(i) != _other.data.get(i)) return false; return true; } bool SolvingState::Compare::operator()(SolvingState const& _a, SolvingState const& _b) const { if (!considerVariableNames || _a.variableNames == _b.variableNames) { if (_a.bounds == _b.bounds) return _a.constraints < _b.constraints; else return _a.bounds < _b.bounds; } else return _a.variableNames < _b.variableNames; } string SolvingState::toString() const { size_t const reasonLength = 10; string result; for (Constraint const& constraint: constraints) { vector line; for (auto&& [index, multiplier]: constraint.data | ranges::views::enumerate) if (index > 0 && multiplier != 0) { string mult = multiplier == -1 ? "-" : multiplier == 1 ? "" : ::toString(multiplier) + " "; line.emplace_back(mult + variableNames.at(index)); } result += reasonToString(constraint.reasons, reasonLength) + joinHumanReadable(line, " + ") + (constraint.equality ? " = " : " <= ") + ::toString(constraint.data.front()) + "\n"; } result += "Bounds:\n"; for (auto&& [index, bounds]: bounds | ranges::views::enumerate) { if (!bounds.lower && !bounds.upper) continue; if (bounds.lower) result += reasonToString(bounds.lowerReasons, reasonLength) + ::toString(*bounds.lower) + " <= "; result += variableNames.at(index); if (bounds.upper) result += " <= "s + ::toString(*bounds.upper) + " " + reasonToString(bounds.upperReasons, 0); result += "\n"; } return result; } pair> SolvingStateSimplifier::simplify() { do { m_changed = false; if (auto conflict = removeFixedVariables()) return {LPResult::Infeasible, move(*conflict)}; if (auto conflict = extractDirectConstraints()) return {LPResult::Infeasible, move(*conflict)}; // Used twice on purpose if (auto conflict = removeFixedVariables()) return {LPResult::Infeasible, move(*conflict)}; removeEmptyColumns(); } while (m_changed); return {LPResult::Unknown, move(m_model)}; } optional SolvingStateSimplifier::removeFixedVariables() { for (auto const& [index, bounds]: m_state.bounds | ranges::views::enumerate) { if (!bounds.upper || (!bounds.lower && bounds.upper->numerator() > 0)) continue; // Lower bound must be at least zero. rational lower = max(rational{}, bounds.lower ? *bounds.lower : rational{}); rational upper = *bounds.upper; if (upper < lower) // Infeasible. return bounds.lowerReasons + bounds.upperReasons; if (upper != lower) continue; set reasons = bounds.lowerReasons + bounds.upperReasons; m_model[m_state.variableNames.at(index)] = lower; m_state.bounds[index] = {}; m_changed = true; // substitute variable // -> add the bounds to the literals for the conflict // (maybe only if one of these constraints is used) for (Constraint& constraint: m_state.constraints) if (constraint.data[index]) { constraint.data[0] -= constraint.data[index] * lower; constraint.data[index] = 0; constraint.reasons += reasons; } } return nullopt; } optional SolvingStateSimplifier::extractDirectConstraints() { vector constraintsToRemove(m_state.constraints.size(), false); bool needsRemoval = false; for (auto const& [index, constraint]: m_state.constraints | ranges::views::enumerate) { auto nonzeroCoefficients = constraint.data | ranges::views::enumerate | ranges::views::tail | ranges::views::filter( [](std::pair const& _x) { return !!_x.second; } ); // TODO we can exit early on in the loop above since we only care about zero, one or more than one nonzero entries. // TODO could also use iterators and exit if we can advance it twice. auto numNonzero = ranges::distance(nonzeroCoefficients); if (numNonzero > 1) continue; constraintsToRemove[index] = true; needsRemoval = true; if (numNonzero == 0) { // 0 <= b or 0 = b if ( constraint.data.front().numerator() < 0 || (constraint.equality && constraint.data.front()) ) return constraint.reasons; } else { auto&& [varIndex, factor] = nonzeroCoefficients.front(); // a * x <= b rational bound = constraint.data[0] / factor; if ( (factor >= 0 || constraint.equality) && (!m_state.bounds[varIndex].upper || bound < m_state.bounds[varIndex].upper) ) { m_state.bounds[varIndex].upper = bound; m_state.bounds[varIndex].upperReasons = constraint.reasons; } if ( (factor <= 0 || constraint.equality) && bound >= 0 && (!m_state.bounds[varIndex].lower || bound > m_state.bounds[varIndex].lower) ) { m_state.bounds[varIndex].lower = bound; m_state.bounds[varIndex].lowerReasons = constraint.reasons; } } } if (needsRemoval) { m_changed = true; eraseIndices(m_state.constraints, constraintsToRemove); } return nullopt; } void SolvingStateSimplifier::removeEmptyColumns() { vector variablesSeen(m_state.bounds.size(), false); for (auto const& constraint: m_state.constraints) { for (auto&& [index, factor]: constraint.data | ranges::views::enumerate | ranges::views::tail) if (factor) variablesSeen[index] = true; } vector variablesToRemove(variablesSeen.size(), false); bool needsRemoval = false; for (auto&& [i, seen]: variablesSeen | ranges::views::enumerate | ranges::views::tail) if (!seen) { variablesToRemove[i] = true; needsRemoval = true; SolvingState::Bounds const& bounds = m_state.bounds.at(i); // TODO actually it is unbounded if m_state.bounds.at(i).upper is nullopt. if (bounds.lower || bounds.upper) { solAssert(!bounds.upper || bounds.upper >= 0); if (bounds.lower && bounds.upper) solAssert(*bounds.lower <= *bounds.upper); m_model[m_state.variableNames.at(i)] = bounds.upper ? *bounds.upper : *bounds.lower; } } if (needsRemoval) { m_changed = true; removeColumns(m_state, variablesToRemove); } } SolvingState ProblemSplitter::next() { vector includedColumns; vector includedRows; tie(includedColumns, includedRows) = connectedComponent(m_state, m_column); // Update state. m_seenColumns |= includedColumns; ++m_column; while (m_column < m_state.variableNames.size() && m_seenColumns[m_column]) ++m_column; if (includedRows.empty()) { // This should not happen if the SolvingStateSimplifier has been used beforehand. // We just check that we did not miss any bounds. for (auto&& [i, included]: includedColumns | ranges::views::enumerate | ranges::views::tail) if (included) solAssert(!m_state.bounds[i].lower && !!m_state.bounds[i].upper); return next(); } SolvingState splitOff; splitOff.variableNames.emplace_back(); splitOff.bounds.emplace_back(); for (auto&& [i, included]: includedColumns | ranges::views::enumerate | ranges::views::tail) if (included) { splitOff.variableNames.emplace_back(move(m_state.variableNames[i])); splitOff.bounds.emplace_back(move(m_state.bounds[i])); } for (auto&& [i, included]: includedRows | ranges::views::enumerate) if (included) { // Use const& on purpose because moving from the state can lead // to undefined behaviour for connectedComponent Constraint const& constraint = m_state.constraints[i]; Constraint splitRow{{}, constraint.equality, constraint.reasons}; for (size_t j = 0; j < constraint.data.size(); j++) if (j == 0 || includedColumns[j]) splitRow.data.push_back(constraint.data[j]); splitOff.constraints.push_back(move(splitRow)); } return splitOff; } LPSolver::LPSolver(bool _supportModels): m_supportModels(_supportModels), m_cache(SolvingState::Compare{_supportModels}) { } pair> LPSolver::check(SolvingState _state) { normalizeRowLengths(_state); auto&& [simplificationResult, modelOrReasonSet] = SolvingStateSimplifier{_state}.simplify(); switch (simplificationResult) { case LPResult::Infeasible: return {LPResult::Infeasible, modelOrReasonSet}; case LPResult::Feasible: case LPResult::Unbounded: solAssert(false); case LPResult::Unknown: break; } Model model = get(modelOrReasonSet); bool canOnlyBeUnknown = false; ProblemSplitter splitter(move(_state)); while (splitter) { SolvingState split = splitter.next(); solAssert(!split.constraints.empty(), ""); solAssert(split.variableNames.size() >= 2, ""); LPResult lpResult; vector solution; if (auto conflict = boundsToConstraints(split)) return {LPResult::Infeasible, move(*conflict)}; auto it = m_cache.find(split); if (it != m_cache.end()) tie(lpResult, solution) = it->second; else { LinearExpression objectives; objectives.resize(1); objectives.resize(split.constraints.front().data.size(), rational(bigint(1))); tie(lpResult, solution) = simplex(split.constraints, move(objectives)); // If we do not support models, do not store it in the cache because // the variable associations will be wrong. // Otherwise, it is fine to use the model. m_cache.emplace(split, make_pair(lpResult, m_supportModels ? solution : vector{})); } switch (lpResult) { case LPResult::Feasible: case LPResult::Unbounded: break; case LPResult::Infeasible: { solAssert(split.bounds.empty()); set reasons; for (auto const& constraint: split.constraints) reasons += constraint.reasons; return {LPResult::Infeasible, move(reasons)}; } case LPResult::Unknown: // We do not stop here, because another independent query can still be infeasible. canOnlyBeUnknown = true; break; } for (auto&& [index, value]: solution | ranges::views::enumerate) if (index + 1 < split.variableNames.size()) model[split.variableNames.at(index + 1)] = value; } if (canOnlyBeUnknown) return {LPResult::Unknown, Model{}}; return {LPResult::Feasible, move(model)}; }