From 751f50b6c34239f62d92bd46e6b106981d179eb9 Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 4 Jan 2021 17:04:04 +0100 Subject: [PATCH] LP Solver. --- libsolutil/CMakeLists.txt | 3 + libsolutil/LP.cpp | 778 ++++++++++++++++++++++++++++++++++ libsolutil/LP.h | 89 ++++ libsolutil/LinearExpression.h | 192 +++++++++ test/CMakeLists.txt | 1 + test/libsolutil/LP.cpp | 314 ++++++++++++++ 6 files changed, 1377 insertions(+) create mode 100644 libsolutil/LP.cpp create mode 100644 libsolutil/LP.h create mode 100644 libsolutil/LinearExpression.h create mode 100644 test/libsolutil/LP.cpp diff --git a/libsolutil/CMakeLists.txt b/libsolutil/CMakeLists.txt index e055317be..a1d915bb3 100644 --- a/libsolutil/CMakeLists.txt +++ b/libsolutil/CMakeLists.txt @@ -23,9 +23,12 @@ set(sources Keccak256.h LazyInit.h LEB128.h + LP.cpp + LP.h Numeric.cpp Numeric.h picosha2.h + LinearExpression.h Result.h SetOnce.h StringUtils.cpp diff --git a/libsolutil/LP.cpp b/libsolutil/LP.cpp new file mode 100644 index 000000000..e7e47fb5e --- /dev/null +++ b/libsolutil/LP.cpp @@ -0,0 +1,778 @@ +/* + 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 +{ + +/** + * Simplex tableau. + */ +struct Tableau +{ + /// The factors of the objective function (first row of the tableau) + LinearExpression objective; + /// The tableau matrix (equational forrm). + std::vector data; +}; + + +/// Adds slack variables to remove non-equality costraints from a set of constraints. +/// The second return variable is true if new variables have been added. +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 currentVariable = 0; + for (Constraint& constraint: _constraints) + { + solAssert(constraint.data.size() == columns, ""); + constraint.data.factors += vector(varsNeeded, rational{}); + if (!constraint.equality) + { + constraint.equality = true; + constraint.data[columns + currentVariable] = bigint(1); + currentVariable++; + } + } + } + + return make_pair(move(_constraints), varsNeeded > 0); +} + +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; +} + +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 > 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 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 subtractPivotRow = [&](LinearExpression& _row) { + if (_row[_pivotColumn] == rational{1}) + _row -= _pivotRowData; + else if (_row[_pivotColumn] != rational{}) + _row -= _row[_pivotColumn] * _pivotRowData; + }; + + subtractPivotRow(_tableau.objective); + for (size_t i = 0; i < _tableau.data.size(); ++i) + if (i != _pivotRow) + subtractPivotRow(_tableau.data[i]); +} + +void selectLastVectorsAsBasis(Tableau& _tableau) +{ + // We might skip the operation for a column if it is already the correct + // unit vector and its cost 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 basisVariable(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 = basisVariable(_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. +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].factors += vector(rows, bigint{}); + _tableau.data[i][columns + i] = 1; + } + _tableau.objective.factors = + vector(columns, rational{}) + + vector(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::Feasible || result == LPResult::Unbounded, ""); + + vector optimum = solutionVector(_tableau); + + for (size_t i = columns - 1; i < optimum.size(); ++i) + if (optimum[i] != 0) + return make_pair(LPResult::Infeasible, Tableau{}); + + _tableau.objective = 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; + // TODO change toEquationalForm to directly return the tableau + tie(_constraints, hasEquations) = toEquationalForm(_constraints); + for (Constraint& c: _constraints) + tableau.data.emplace_back(move(c.data)); + 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 is not optimal. + LPResult result; + tie(result, tableau) = simplexEq(move(tableau)); + solAssert(result == LPResult::Feasible || result == LPResult::Unbounded, ""); + return make_pair(result, solutionVector(tableau)); +} + +/// Turns all bounds into constraints. +/// @returns false if the bounds make the state infeasible. +bool boundsToConstraints(SolvingState& _state) +{ + size_t columns = _state.variableNames.size(); + + // Turn bounds into constraints. + for (auto const& [index, bounds]: _state.bounds | ranges::views::enumerate | ranges::views::tail) + { + if (bounds[0] && bounds[1]) + { + if (*bounds[0] > *bounds[1]) + return false; + if (*bounds[0] == *bounds[1]) + { + vector c(columns); + c[0] = *bounds[0]; + c[index] = bigint(1); + _state.constraints.emplace_back(Constraint{move(c), true}); + continue; + } + } + if (bounds[0] && *bounds[0] > 0) + { + vector c(columns); + c[0] = -*bounds[0]; + c[index] = bigint(-1); + _state.constraints.emplace_back(Constraint{move(c), false}); + } + if (bounds[1]) + { + vector c(columns); + c[0] = *bounds[1]; + c[index] = bigint(1); + _state.constraints.emplace_back(Constraint{move(c), false}); + } + } + _state.bounds.clear(); + return true; +} + +template +void eraseIndices(T& _data, vector const& _indices) +{ + T result; + for (size_t i = 0; i < _data.size(); i++) + if (!_indices[i]) + result.emplace_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); +} + +bool extractDirectConstraints(SolvingState& _state, bool& _changed) +{ + // Turn constraints of the form ax <= b into an upper bound on x. + vector constraintsToRemove(_state.constraints.size(), false); + bool needsRemoval = false; + for (auto const& [index, constraint]: _state.constraints | ranges::views::enumerate) + { + auto nonzero = 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(nonzero); + if (numNonzero > 1) + continue; + constraintsToRemove[index] = true; + needsRemoval = true; + if (numNonzero == 0) + { + // 0 <= b or 0 = b + if ( + constraint.data.factors.front() < 0 || + (constraint.equality && constraint.data.factors.front() != 0) + ) + return false; // Infeasible. + } + else + { + auto&& [varIndex, factor] = nonzero.front(); + // a * x <= b + rational bound = constraint.data[0] / factor; + if ( + (factor >= 0 || constraint.equality) && + (!_state.bounds[varIndex][1] || bound < _state.bounds[varIndex][1]) + ) + _state.bounds[varIndex][1] = bound; + if ( + (factor <= 0 || constraint.equality) && + (!_state.bounds[varIndex][0] || bound > _state.bounds[varIndex][0]) + ) + // Lower bound must be at least zero. + _state.bounds[varIndex][0] = max(rational{}, bound); + } + } + if (needsRemoval) + { + _changed = true; + eraseIndices(_state.constraints, constraintsToRemove); + } + return true; +} + +bool removeFixedVariables(SolvingState& _state, map& _model, bool& _changed) +{ + // Remove variables that have equal lower and upper bound. + for (auto const& [index, bounds]: _state.bounds | ranges::views::enumerate) + { + if (!bounds[1] || (!bounds[0] && bounds[1]->numerator() > 0)) + continue; + // Lower bound must be at least zero. + rational lower = max(rational{}, bounds[0] ? *bounds[0] : rational{}); + rational upper = *bounds[1]; + if (upper < lower) + return false; // Infeasible. + if (upper != lower) + continue; + _model[_state.variableNames.at(index)] = lower; + _state.bounds[index] = {}; + _changed = true; + + // substitute variable + for (Constraint& constraint: _state.constraints) + if (constraint.data.factors.at(index) != 0) + { + constraint.data[0] -= constraint.data[index] * lower; + constraint.data[index] = 0; + } + } + + return true; +} + +bool removeEmptyColumns(SolvingState& _state, map& _model, bool& _changed) +{ + vector variablesSeen(_state.bounds.size(), false); + for (auto const& constraint: _state.constraints) + { + for (auto&& [index, factor]: constraint.data | ranges::views::enumerate | ranges::views::tail) + if (factor) + variablesSeen[index] = true; + } + + // TODO we could assert that any variable we remove does not have conflicting bounds. + // (We also remove the bounds). + + 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; + // TODO actually it is unbounded if _state.bounds.at(i)[1] is nullopt. + if (_state.bounds.at(i)[0] || _state.bounds.at(i)[1]) + _model[_state.variableNames.at(i)] = + _state.bounds.at(i)[1] ? + *_state.bounds.at(i)[1] : + *_state.bounds.at(i)[0]; + } + if (needsRemoval) + { + _changed = true; + removeColumns(_state, variablesToRemove); + } + return true; +} + +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] != 0; }) | + ranges::views::transform([](auto const& _entry) { return _entry.first; }); +} + +pair, vector> connectedComponent(SolvingState const& _state, size_t _column) +{ + solAssert(_state.variableNames.size() >= 2, ""); + + vector includedColumns(_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 && !includedColumns[index]) + columnsToProcess.push(index); + } + } + return make_pair(move(includedColumns), move(includedRows)); +} + +struct ProblemSplitter +{ + ProblemSplitter(SolvingState const& _state): + state(_state), + column(1), + seenColumns(vector(state.variableNames.size(), false)) + {} + + operator bool() const + { + return column < state.variableNames.size(); + } + + SolvingState next() + { + vector includedColumns; + vector includedRows; + tie(includedColumns, includedRows) = connectedComponent(state, column); + + // Update state. + seenColumns |= includedColumns; + ++column; + while (column < state.variableNames.size() && seenColumns[column]) + ++column; + + // Happens in case of not removed empty column. + // Currently not happening because those are removed during the simplification stage. + // TODO If this is the case, we should actually also check the bounds. + if (includedRows.empty()) + 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) + continue; + splitOff.variableNames.emplace_back(move(state.variableNames[i])); + splitOff.bounds.emplace_back(move(state.bounds[i])); + } + for (auto&& [i, included]: includedRows | ranges::views::enumerate) + { + if (!included) + continue; + Constraint splitRow{{}, state.constraints[i].equality}; + for (size_t j = 0; j < state.constraints[i].data.size(); j++) + if (j == 0 || includedColumns[j]) + splitRow.data.factors.push_back(state.constraints[i].data[j]); + splitOff.constraints.push_back(move(splitRow)); + } + + return splitOff; + } + + SolvingState const& state; + size_t column = 1; + vector seenColumns; +}; + + +/// Simplifies the solving state according to some rules (remove rows without variables, etc). +/// @returns false if the state is determined to be infeasible during this process. +bool simplifySolvingState(SolvingState& _state, map& _model) +{ + // - Constraints with exactly one nonzero coefficient represent "a x <= b" + // and thus are turned into bounds. + // - Constraints with zero nonzero coefficients are constant relations. + // If such a relation is false, answer "infeasible", otherwise remove the constraint. + // - Empty columns can be removed. + // - Variables with matching bounds can be removed from the problem by substitution. + + bool changed = true; + while (changed) + { + changed = false; + + if (!removeFixedVariables(_state, _model, changed)) + return false; + + if (!extractDirectConstraints(_state, changed)) + return false; + + if (!removeFixedVariables(_state, _model, changed)) + return false; + + if (!removeEmptyColumns(_state, _model, changed)) + return false; + } + + // TODO return the values selected for named variables in order to + // be used when returning the model. + return true; +} + +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) + { + rational const& a = data.get(i); + rational const& b = _other.data.get(i); + if (a != b) + return a < b; + } + 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::operator<(SolvingState const& _other) const +{ + if (variableNames == _other.variableNames) + { + if (bounds == _other.bounds) + return constraints < _other.constraints; + else + return bounds < _other.bounds; + } + else + return variableNames < _other.variableNames; +} + +bool SolvingState::operator==(SolvingState const& _other) const +{ + return + variableNames == _other.variableNames && + bounds == _other.bounds && + constraints == _other.constraints; +} + +string SolvingState::toString() const +{ + 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 += + joinHumanReadable(line, " + ") + + (constraint.equality ? " = " : " <= ") + + ::toString(constraint.data.factors.front()) + + "\n"; + } + result += "Bounds:\n"; + for (auto&& [index, bounds]: bounds | ranges::views::enumerate) + { + if (!bounds[0] && !bounds[1]) + continue; + if (bounds[0]) + result += ::toString(*bounds[0]) + " <= "; + result += variableNames.at(index); + if (bounds[1]) + result += " <= " + ::toString(*bounds[1]); + result += "\n"; + } + return result; +} + + +pair> LPSolver::check(SolvingState _state) +{ + normalizeRowLengths(_state); + + map model; + + if (!simplifySolvingState(_state, model)) + return {LPResult::Infeasible, {}}; + + bool canOnlyBeUnknown = false; + ProblemSplitter splitter(_state); + while (splitter) + { + SolvingState split = splitter.next(); + solAssert(!split.constraints.empty(), ""); + solAssert(split.variableNames.size() >= 2, ""); + + LPResult lpResult; + vector solution; + auto it = m_cache.find(split); + if (it != m_cache.end()) + tie(lpResult, solution) = it->second; + else + { + SolvingState orig = split; + if (!boundsToConstraints(split)) + lpResult = LPResult::Infeasible; + else + { + LinearExpression objectives; + objectives.factors = + vector(1, rational(bigint(0))) + + vector(split.constraints.front().data.size() - 1, rational(bigint(1))); + tie(lpResult, solution) = simplex(split.constraints, move(objectives)); + } + m_cache.emplace(move(orig), make_pair(lpResult, solution)); + } + + switch (lpResult) + { + case LPResult::Feasible: + case LPResult::Unbounded: + break; + case LPResult::Infeasible: + return {LPResult::Infeasible, {}}; + 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, {}}; + + return {LPResult::Feasible, move(model)}; +} + diff --git a/libsolutil/LP.h b/libsolutil/LP.h new file mode 100644 index 000000000..e7f27ce92 --- /dev/null +++ b/libsolutil/LP.h @@ -0,0 +1,89 @@ +/* + 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 +#pragma once + +#include +#include + +#include + +#include +#include + +namespace solidity::util +{ + +/** + * Constraint of the form + * - data[1] * x_1 + data[2] * x_2 + ... <= data[0] (equality == false) + * - data[1] * x_1 + data[2] * x_2 + ... = data[0] (equality == true) + * The set and order of variables is implied. + */ +struct Constraint +{ + LinearExpression data; + bool equality = false; + + bool operator<(Constraint const& _other) const; + bool operator==(Constraint const& _other) const; +}; + +/** + * State used when solving an LP problem. + */ +struct SolvingState +{ + /// Names of variables, the index zero should be left empty. + /// TODO can we change that? + std::vector variableNames; + /// Lower and upper bounds for variables (in the sense of >= / <=). + std::vector>, 2>> bounds; + std::vector constraints; + + bool operator<(SolvingState const& _other) const; + bool operator==(SolvingState const& _other) const; + std::string toString() const; +}; + +enum class LPResult +{ + Unknown, + Unbounded, + Feasible, + Infeasible +}; + +/** + * LP solver for rational problems. + * + * Does not solve integer problems! + * + * Tries to split a given problem into sub-problems and utilizes a cache to quickly solve + * similar problems. + */ +class LPSolver +{ +public: + std::pair>> check(SolvingState _state); + +private: + // TODO check if the model is requested in production. If not, we do not need to cache it. + std::map>>> m_cache; +}; + +} diff --git a/libsolutil/LinearExpression.h b/libsolutil/LinearExpression.h new file mode 100644 index 000000000..0d038d260 --- /dev/null +++ b/libsolutil/LinearExpression.h @@ -0,0 +1,192 @@ +/* + 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 +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + + +namespace solidity::util +{ + +using rational = boost::rational; + +/** + * A linear expression of the form + * factors[0] + factors[1] * X1 + factors[2] * X2 + ... + */ +struct LinearExpression +{ + /// Creates the expression "_factor * X_index" + static LinearExpression factorForVariable(size_t _index, rational _factor) + { + LinearExpression result; + result.resizeAndSet(_index, move(_factor)); + return result; + } + + rational const& get(size_t _index) const + { + static rational const zero; + return _index < factors.size() ? factors[_index] : zero; + } + + rational const& operator[](size_t _index) const + { + return factors[_index]; + } + + rational& operator[](size_t _index) + { + return factors[_index]; + } + + auto begin() { return factors.begin(); } + auto end() { return factors.end(); } + + auto begin() const { return factors.begin(); } + auto end() const { return factors.end(); } + + void emplace_back(rational _value) { factors.emplace_back(move(_value)); } + + void resize(size_t _size) + { + factors.resize(_size); + } + + void resizeAndSet(size_t _index, rational _factor) + { + if (factors.size() <= _index) + factors.resize(_index + 1); + factors[_index] = move(_factor); + } + + bool isConstant() const + { + return ranges::all_of(factors | ranges::views::tail, [](rational const& _v) { return _v.numerator() == 0; }); + } + + size_t size() const { return factors.size(); } + + LinearExpression& operator/=(rational const& _divisor) + { + for (rational& x: factors) + if (x.numerator()) + x /= _divisor; + return *this; + } + + LinearExpression& operator*=(rational const& _factor) + { + for (rational& x: factors) + if (x.numerator()) + x *= _factor; + return *this; + } + + friend LinearExpression operator*(rational const& _factor, LinearExpression _expr) + { + for (rational& x: _expr.factors) + if (x.numerator()) + x *= _factor; + return _expr; + } + + LinearExpression& operator-=(LinearExpression const& _y) + { + if (size() < _y.size()) + factors.resize(_y.size()); + for (size_t i = 0; i < size(); ++i) + if (_y.factors[i].numerator()) + factors[i] -= _y.factors[i]; + return *this; + } + + LinearExpression operator-(LinearExpression const& _y) const + { + LinearExpression result = *this; + result -= _y; + return result; + } + + LinearExpression& operator+=(LinearExpression const& _y) + { + if (size() < _y.size()) + factors.resize(_y.size()); + for (size_t i = 0; i < size(); ++i) + if (_y.factors[i].numerator()) + factors[i] += _y.factors[i]; + return *this; + } + + LinearExpression operator+(LinearExpression const& _y) const + { + LinearExpression result = *this; + result += _y; + return result; + } + + + /// Multiply two vectors where the first element of each vector is a constant factor. + /// Only works if at most one of the vector has a nonzero element after the first. + /// If this condition is violated, returns nullopt. + static std::optional vectorProduct( + std::optional _x, + std::optional _y + ) + { + if (!_x || !_y) + return std::nullopt; + if (!_y->isConstant()) + swap(_x, _y); + if (!_y->isConstant()) + return std::nullopt; + + rational const& factor = _y->get(0); + + for (rational& element: _x->factors) + element *= factor; + return _x; + } + + std::vector factors; +}; + +// TODO + + +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; +} + +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 255218c15..c2332621a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,6 +42,7 @@ set(libsolutil_sources libsolutil/Keccak256.cpp libsolutil/LazyInit.cpp libsolutil/LEB128.cpp + libsolutil/LP.cpp libsolutil/StringUtils.cpp libsolutil/SwarmHash.cpp libsolutil/UTF8.cpp diff --git a/test/libsolutil/LP.cpp b/test/libsolutil/LP.cpp new file mode 100644 index 000000000..874111c46 --- /dev/null +++ b/test/libsolutil/LP.cpp @@ -0,0 +1,314 @@ +/* + 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 + +using namespace std; +using namespace solidity::smtutil; +using namespace solidity::util; + + +namespace solidity::util::test +{ + +class LPTestFramework +{ +public: + LPTestFramework() + { + m_solvingState.variableNames.emplace_back(""); + } + + LinearExpression constant(rational _value) + { + return LinearExpression::factorForVariable(0, _value); + } + + LinearExpression variable(string const& _name) + { + return LinearExpression::factorForVariable(variableIndex(_name), 1); + } + + /// Adds the constraint "_lhs <= _rhs". + void addLEConstraint(LinearExpression _lhs, LinearExpression _rhs) + { + _lhs -= _rhs; + _lhs[0] = -_lhs[0]; + m_solvingState.constraints.push_back({move(_lhs), false}); + } + + void addLEConstraint(LinearExpression _lhs, rational _rhs) + { + addLEConstraint(move(_lhs), constant(_rhs)); + } + + /// Adds the constraint "_lhs = _rhs". + void addEQConstraint(LinearExpression _lhs, LinearExpression _rhs) + { + _lhs -= _rhs; + _lhs[0] = -_lhs[0]; + m_solvingState.constraints.push_back({move(_lhs), true}); + } + + void addLowerBound(string _variable, rational _value) + { + size_t index = variableIndex(_variable); + if (index >= m_solvingState.bounds.size()) + m_solvingState.bounds.resize(index + 1); + m_solvingState.bounds.at(index)[0] = _value; + } + + void addUpperBound(string _variable, rational _value) + { + size_t index = variableIndex(_variable); + if (index >= m_solvingState.bounds.size()) + m_solvingState.bounds.resize(index + 1); + m_solvingState.bounds.at(index)[1] = _value; + } + + void feasible(vector> const& _solution) + { + auto [result, model] = m_solver.check(m_solvingState); + BOOST_REQUIRE(result == LPResult::Feasible); + for (auto const& [var, value]: _solution) + BOOST_CHECK_MESSAGE( + value == model.at(var), + var + " = "s + ::toString(model.at(var)) + " (expected " + ::toString(value) + ")" + ); + } + + void infeasible() + { + auto [result, model] = m_solver.check(m_solvingState); + BOOST_CHECK(result == LPResult::Infeasible); + } + +protected: + size_t variableIndex(string const& _name) + { + if (m_solvingState.variableNames.empty()) + m_solvingState.variableNames.emplace_back(""); + auto index = findOffset(m_solvingState.variableNames, _name); + if (!index) + { + index = m_solvingState.variableNames.size(); + m_solvingState.variableNames.emplace_back(_name); + } + return *index; + } + + LPSolver m_solver; + SolvingState m_solvingState; +}; + + +BOOST_FIXTURE_TEST_SUITE(LP, LPTestFramework, *boost::unit_test::label("nooptions")) + +BOOST_AUTO_TEST_CASE(basic) +{ + auto x = variable("x"); + addLEConstraint(2 * x, 10); + feasible({{"x", 5}}); +} + +BOOST_AUTO_TEST_CASE(not_linear_independent) +{ + addLEConstraint(2 * variable("x"), 10); + addLEConstraint(4 * variable("x"), 20); + feasible({{"x", 5}}); +} + +BOOST_AUTO_TEST_CASE(two_vars) +{ + addLEConstraint(variable("y"), 3); + addLEConstraint(variable("x"), 10); + addLEConstraint(variable("x") + variable("y"), 4); + feasible({{"x", 1}, {"y", 3}}); +} + +BOOST_AUTO_TEST_CASE(one_le_the_other) +{ + addLEConstraint(variable("x") + constant(2), variable("y") - constant(1)); + feasible({{"x", 0}, {"y", 3}}); +} + +BOOST_AUTO_TEST_CASE(factors) +{ + auto x = variable("x"); + auto y = variable("y"); + addLEConstraint(2 * y, 3); + addLEConstraint(16 * x, 10); + addLEConstraint(x + y, 4); + feasible({{"x", rational(5) / 8}, {"y", rational(3) / 2}}); +} + +BOOST_AUTO_TEST_CASE(cache) +{ + // This should use the cache already for the second part of the problem. + // We cannot really test that the cache has been used, but we can test + // that it results in the same value. + auto x = variable("x"); + auto y = variable("y"); + addLEConstraint(2 * y, 3); + addLEConstraint(2 * x, 3); + feasible({{"x", rational(3) / 2}, {"y", rational(3) / 2}}); + feasible({{"x", rational(3) / 2}, {"y", rational(3) / 2}}); +} + +BOOST_AUTO_TEST_CASE(bounds) +{ + addUpperBound("x", 200); + feasible({{"x", 200}}); + + addLEConstraint(variable("x"), 100); + feasible({{"x", 100}}); + + addLEConstraint(constant(5), variable("x")); + feasible({{"x", 100}}); + + addLowerBound("x", 20); + feasible({{"x", 100}}); + addLowerBound("x", 25); + feasible({{"x", 100}}); + + addUpperBound("x", 20); + infeasible(); +} + +BOOST_AUTO_TEST_CASE(bounds2) +{ + addLowerBound("x", 200); + addUpperBound("x", 250); + addLowerBound("y", 2); + addUpperBound("y", 3); + feasible({{"x", 250}, {"y", 3}}); + + addLEConstraint(variable("y"), variable("x")); + feasible({{"x", 250}, {"y", 3}}); + + addEQConstraint(variable("y") + constant(231), variable("x")); + feasible({{"x", 234}, {"y", 3}}); + + addEQConstraint(variable("y") + constant(10), variable("x") - variable("z")); + feasible({{"x", 234}, {"y", 3}}); + + addEQConstraint(variable("z") + variable("x"), constant(2)); + infeasible(); +} + +BOOST_AUTO_TEST_CASE(lower_bound) +{ + addLEConstraint(constant(1), variable("y")); + addLEConstraint(variable("x"), constant(10)); + addLEConstraint(2 * variable("x") + variable("y"), 2); + feasible({{"x", 0}, {"y", 2}}); +} + +BOOST_AUTO_TEST_CASE(check_infeasible) +{ + addLEConstraint(variable("x"), 3); + addLEConstraint(constant(5), variable("x")); + infeasible(); +} + +BOOST_AUTO_TEST_CASE(unbounded1) +{ + addLEConstraint(constant(2), variable("x")); + feasible({{"x", 2}}); +} + +BOOST_AUTO_TEST_CASE(unbounded2) +{ + auto x = variable("x"); + auto y = variable("y"); + addLEConstraint(constant(2), x + y); + addLEConstraint(x, 10); + feasible({{"x", 10}, {"y", 0}}); +} + +BOOST_AUTO_TEST_CASE(unbounded3) +{ + addLEConstraint(constant(0) - variable("x") - variable("y"), constant(10)); + feasible({{"x", 0}, {"y", 0}}); + + addLEConstraint(constant(0) - variable("x"), constant(10)); + feasible({{"x", 0}, {"y", 0}}); + + addEQConstraint(variable("y") + constant(3), variable("x")); + feasible({{"x", 3}, {"y", 0}}); + + addLEConstraint(variable("y") + variable("x"), constant(2)); + infeasible(); +} + + +BOOST_AUTO_TEST_CASE(equal) +{ + auto x = variable("x"); + auto y = variable("y"); + addEQConstraint(x, y + constant(10)); + addLEConstraint(x, 20); + feasible({{"x", 20}, {"y", 10}}); +} + + +BOOST_AUTO_TEST_CASE(equal_constant) +{ + auto x = variable("x"); + auto y = variable("y"); + addLEConstraint(x, y); + addEQConstraint(y, constant(5)); + feasible({{"x", 5}, {"y", 5}}); +} + +BOOST_AUTO_TEST_CASE(linear_dependent) +{ + auto x = variable("x"); + auto y = variable("y"); + auto z = variable("z"); + addLEConstraint(x, 5); + addLEConstraint(2 * y, 10); + addLEConstraint(3 * z, 15); + // Here, they should be split into three independent problems. + feasible({{"x", 5}, {"y", 5}, {"z", 5}}); + + addLEConstraint((x + y) + z, 100); + feasible({{"x", 5}, {"y", 5}, {"z", 5}}); + + addLEConstraint((x + y) + z, 2); + feasible({{"x", 2}, {"y", 0}, {"z", 0}}); + + addLEConstraint(constant(2), (x + y) + z); + feasible({{"x", 2}, {"y", 0}, {"z", 0}}); + + addEQConstraint(constant(2), (x + y) + z); + feasible({{"x", 2}, {"y", 0}, {"z", 0}}); +} + + + +BOOST_AUTO_TEST_SUITE_END() + +}