#include "storm/solver/NativeLinearEquationSolver.h" #include #include "storm/environment/solver/NativeSolverEnvironment.h" #include "storm/utility/ConstantsComparator.h" #include "storm/utility/KwekMehlhorn.h" #include "storm/utility/NumberTraits.h" #include "storm/utility/SignalHandler.h" #include "storm/utility/constants.h" #include "storm/utility/vector.h" #include "storm/solver/helper/SoundValueIterationHelper.h" #include "storm/solver/helper/OptimisticValueIterationHelper.h" #include "storm/solver/Multiplier.h" #include "storm/exceptions/InvalidStateException.h" #include "storm/exceptions/InvalidEnvironmentException.h" #include "storm/exceptions/UnmetRequirementException.h" #include "storm/exceptions/PrecisionExceededException.h" #include "storm/exceptions/NotSupportedException.h" namespace storm { namespace solver { template NativeLinearEquationSolver::NativeLinearEquationSolver() : localA(nullptr), A(nullptr) { // Intentionally left empty. } template NativeLinearEquationSolver::NativeLinearEquationSolver(storm::storage::SparseMatrix const& A) : localA(nullptr), A(nullptr) { this->setMatrix(A); } template NativeLinearEquationSolver::NativeLinearEquationSolver(storm::storage::SparseMatrix&& A) : localA(nullptr), A(nullptr) { this->setMatrix(std::move(A)); } template void NativeLinearEquationSolver::setMatrix(storm::storage::SparseMatrix const& A) { localA.reset(); this->A = &A; clearCache(); } template void NativeLinearEquationSolver::setMatrix(storm::storage::SparseMatrix&& A) { localA = std::make_unique>(std::move(A)); this->A = localA.get(); clearCache(); } template bool NativeLinearEquationSolver::solveEquationsSOR(Environment const& env, std::vector& x, std::vector const& b, ValueType const& omega) const { STORM_LOG_INFO("Solving linear equation system (" << x.size() << " rows) with NativeLinearEquationSolver (Gauss-Seidel, SOR omega = " << omega << ")"); if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(getMatrixRowCount()); } ValueType precision = storm::utility::convertNumber(env.solver().native().getPrecision()); uint64_t maxIter = env.solver().native().getMaximalNumberOfIterations(); bool relative = env.solver().native().getRelativeTerminationCriterion(); // Set up additional environment variables. uint_fast64_t iterations = 0; SolverStatus status = SolverStatus::InProgress; this->startMeasureProgress(); while (status == SolverStatus::InProgress && iterations < maxIter) { A->performSuccessiveOverRelaxationStep(omega, x, b); // Now check if the process already converged within our precision. if (storm::utility::vector::equalModuloPrecision(*this->cachedRowVector, x, precision, relative)) { status = SolverStatus::Converged; } // If we did not yet converge, we need to backup the contents of x. if (status != SolverStatus::Converged) { *this->cachedRowVector = x; } // Potentially show progress. this->showProgressIterative(iterations); // Increase iteration count so we can abort if convergence is too slow. ++iterations; status = this->updateStatus(status, x, SolverGuarantee::None, iterations, maxIter); } if (!this->isCachingEnabled()) { clearCache(); } this->reportStatus(status, iterations); return status == SolverStatus::Converged; } template NativeLinearEquationSolver::JacobiDecomposition::JacobiDecomposition(Environment const& env, storm::storage::SparseMatrix const& A) { auto decomposition = A.getJacobiDecomposition(); this->LUMatrix = std::move(decomposition.first); this->DVector = std::move(decomposition.second); this->multiplier = storm::solver::MultiplierFactory().create(env, this->LUMatrix); } template bool NativeLinearEquationSolver::solveEquationsJacobi(Environment const& env, std::vector& x, std::vector const& b) const { STORM_LOG_INFO("Solving linear equation system (" << x.size() << " rows) with NativeLinearEquationSolver (Jacobi)"); if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(getMatrixRowCount()); } // Get a Jacobi decomposition of the matrix A. if (!jacobiDecomposition) { jacobiDecomposition = std::make_unique(env, *A); } ValueType precision = storm::utility::convertNumber(env.solver().native().getPrecision()); uint64_t maxIter = env.solver().native().getMaximalNumberOfIterations(); bool relative = env.solver().native().getRelativeTerminationCriterion(); std::vector* currentX = &x; std::vector* nextX = this->cachedRowVector.get(); // Set up additional environment variables. uint_fast64_t iterations = 0; SolverStatus status = SolverStatus::InProgress; this->startMeasureProgress(); while (status == SolverStatus::InProgress && iterations < maxIter) { // Compute D^-1 * (b - LU * x) and store result in nextX. jacobiDecomposition->multiplier->multiply(env, *currentX, nullptr, *nextX); storm::utility::vector::subtractVectors(b, *nextX, *nextX); storm::utility::vector::multiplyVectorsPointwise(jacobiDecomposition->DVector, *nextX, *nextX); // Now check if the process already converged within our precision. if (storm::utility::vector::equalModuloPrecision(*currentX, *nextX, precision, relative)) { status = SolverStatus::Converged; } // Swap the two pointers as a preparation for the next iteration. std::swap(nextX, currentX); // Potentially show progress. this->showProgressIterative(iterations); // Increase iteration count so we can abort if convergence is too slow. ++iterations; status = this->updateStatus(status, *currentX, SolverGuarantee::None, iterations, maxIter); } // If the last iteration did not write to the original x we have to swap the contents, because the // output has to be written to the input parameter x. if (currentX == this->cachedRowVector.get()) { std::swap(x, *currentX); } if (!this->isCachingEnabled()) { clearCache(); } this->reportStatus(status, iterations); return status == SolverStatus::Converged; } template NativeLinearEquationSolver::WalkerChaeData::WalkerChaeData(Environment const& env, storm::storage::SparseMatrix const& originalMatrix, std::vector const& originalB) : t(storm::utility::convertNumber(1000.0)) { computeWalkerChaeMatrix(originalMatrix); computeNewB(originalB); precomputeAuxiliaryData(); multiplier = storm::solver::MultiplierFactory().create(env, this->matrix); } template void NativeLinearEquationSolver::WalkerChaeData::computeWalkerChaeMatrix(storm::storage::SparseMatrix const& originalMatrix) { storm::storage::BitVector columnsWithNegativeEntries(originalMatrix.getColumnCount()); ValueType zero = storm::utility::zero(); for (auto const& e : originalMatrix) { if (e.getValue() < zero) { columnsWithNegativeEntries.set(e.getColumn()); } } std::vector columnsWithNegativeEntriesBefore = columnsWithNegativeEntries.getNumberOfSetBitsBeforeIndices(); // We now build an extended equation system matrix that only has non-negative coefficients. storm::storage::SparseMatrixBuilder builder; uint64_t row = 0; for (; row < originalMatrix.getRowCount(); ++row) { for (auto const& entry : originalMatrix.getRow(row)) { if (entry.getValue() < zero) { builder.addNextValue(row, originalMatrix.getRowCount() + columnsWithNegativeEntriesBefore[entry.getColumn()], -entry.getValue()); } else { builder.addNextValue(row, entry.getColumn(), entry.getValue()); } } } ValueType one = storm::utility::one(); for (auto column : columnsWithNegativeEntries) { builder.addNextValue(row, column, one); builder.addNextValue(row, originalMatrix.getRowCount() + columnsWithNegativeEntriesBefore[column], one); ++row; } matrix = builder.build(); } template void NativeLinearEquationSolver::WalkerChaeData::computeNewB(std::vector const& originalB) { b = std::vector(originalB); b.resize(matrix.getRowCount()); } template void NativeLinearEquationSolver::WalkerChaeData::precomputeAuxiliaryData() { columnSums = std::vector(matrix.getColumnCount()); for (auto const& e : matrix) { columnSums[e.getColumn()] += e.getValue(); } newX.resize(matrix.getRowCount()); } template bool NativeLinearEquationSolver::solveEquationsWalkerChae(Environment const& env, std::vector& x, std::vector const& b) const { STORM_LOG_INFO("Solving linear equation system (" << x.size() << " rows) with NativeLinearEquationSolver (WalkerChae)"); // (1) Compute an equivalent equation system that has only non-negative coefficients. if (!walkerChaeData) { walkerChaeData = std::make_unique(env, *this->A, b); } // (2) Enlarge the vectors x and b to account for additional variables. x.resize(walkerChaeData->matrix.getRowCount()); // Square the error bound, so we can use it to check for convergence. We take the squared error, because we // do not want to compute the root in the 2-norm computation. ValueType squaredErrorBound = storm::utility::pow(storm::utility::convertNumber(env.solver().native().getPrecision()), 2); uint64_t maxIter = env.solver().native().getMaximalNumberOfIterations(); // Set up references to the x-vectors used in the iteration loop. std::vector* currentX = &x; std::vector* nextX = &walkerChaeData->newX; std::vector tmp = walkerChaeData->matrix.getRowSumVector(); storm::utility::vector::applyPointwise(tmp, walkerChaeData->b, walkerChaeData->b, [this] (ValueType const& first, ValueType const& second) -> ValueType { return walkerChaeData->t * first + second; } ); // Add t to all entries of x. storm::utility::vector::applyPointwise(x, x, [this] (ValueType const& value) -> ValueType { return value + walkerChaeData->t; }); // Create a vector that always holds Ax. std::vector currentAx(x.size()); walkerChaeData->multiplier->multiply(env, *currentX, nullptr, currentAx); // (3) Perform iterations until convergence. SolverStatus status = SolverStatus::InProgress; uint64_t iterations = 0; this->startMeasureProgress(); while (status == SolverStatus::InProgress && iterations < maxIter) { // Perform one Walker-Chae step. walkerChaeData->matrix.performWalkerChaeStep(*currentX, walkerChaeData->columnSums, walkerChaeData->b, currentAx, *nextX); // Compute new Ax. walkerChaeData->multiplier->multiply(env, *nextX, nullptr, currentAx); // Check for convergence. if (storm::utility::vector::computeSquaredNorm2Difference(currentAx, walkerChaeData->b) <= squaredErrorBound) { status = SolverStatus::Converged; } // Swap the x vectors for the next iteration. std::swap(currentX, nextX); // Potentially show progress. this->showProgressIterative(iterations); // Increase iteration count so we can abort if convergence is too slow. ++iterations; if (storm::utility::resources::isTerminate()) { status = SolverStatus::Aborted; } } // If the last iteration did not write to the original x we have to swap the contents, because the // output has to be written to the input parameter x. if (currentX == &walkerChaeData->newX) { std::swap(x, *currentX); } // Resize the solution to the right size. x.resize(this->A->getRowCount()); // Finalize solution vector. storm::utility::vector::applyPointwise(x, x, [this] (ValueType const& value) -> ValueType { return value - walkerChaeData->t; } ); if (!this->isCachingEnabled()) { clearCache(); } this->reportStatus(status, iterations); return status == SolverStatus::Converged; } template typename NativeLinearEquationSolver::PowerIterationResult NativeLinearEquationSolver::performPowerIteration(Environment const& env, std::vector*& currentX, std::vector*& newX, std::vector const& b, ValueType const& precision, bool relative, SolverGuarantee const& guarantee, uint64_t currentIterations, uint64_t maxIterations, storm::solver::MultiplicationStyle const& multiplicationStyle) const { bool useGaussSeidelMultiplication = multiplicationStyle == storm::solver::MultiplicationStyle::GaussSeidel; uint64_t iterations = currentIterations; SolverStatus status = this->terminateNow(*currentX, guarantee) ? SolverStatus::TerminatedEarly : SolverStatus::InProgress; while (status == SolverStatus::InProgress && iterations < maxIterations) { if (useGaussSeidelMultiplication) { *newX = *currentX; this->multiplier->multiplyGaussSeidel(env, *newX, &b); } else { this->multiplier->multiply(env, *currentX, &b, *newX); } // Check for convergence. if (storm::utility::vector::equalModuloPrecision(*currentX, *newX, precision, relative)) { status = SolverStatus::Converged; } // Check for termination. std::swap(currentX, newX); ++iterations; status = this->updateStatus(status, *currentX, guarantee, iterations, maxIterations); // Potentially show progress. this->showProgressIterative(iterations); } return PowerIterationResult(iterations - currentIterations, status); } template bool NativeLinearEquationSolver::solveEquationsPower(Environment const& env, std::vector& x, std::vector const& b) const { STORM_LOG_INFO("Solving linear equation system (" << x.size() << " rows) with NativeLinearEquationSolver (Power)"); // Prepare the solution vectors. if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(getMatrixRowCount()); } if (!this->multiplier) { this->multiplier = storm::solver::MultiplierFactory().create(env, *A); } std::vector* currentX = &x; SolverGuarantee guarantee = SolverGuarantee::None; if (this->hasCustomTerminationCondition()) { if (this->getTerminationCondition().requiresGuarantee(SolverGuarantee::LessOrEqual) && this->hasLowerBound()) { this->createLowerBoundsVector(*currentX); guarantee = SolverGuarantee::LessOrEqual; } else if (this->getTerminationCondition().requiresGuarantee(SolverGuarantee::GreaterOrEqual) && this->hasUpperBound()) { this->createUpperBoundsVector(*currentX); guarantee = SolverGuarantee::GreaterOrEqual; } } std::vector* newX = this->cachedRowVector.get(); // Forward call to power iteration implementation. this->startMeasureProgress(); ValueType precision = storm::utility::convertNumber(env.solver().native().getPrecision()); PowerIterationResult result = this->performPowerIteration(env, currentX, newX, b, precision, env.solver().native().getRelativeTerminationCriterion(), guarantee, 0, env.solver().native().getMaximalNumberOfIterations(), env.solver().native().getPowerMethodMultiplicationStyle()); // Swap the result in place. if (currentX == this->cachedRowVector.get()) { std::swap(x, *newX); } if (!this->isCachingEnabled()) { clearCache(); } this->logIterations(result.status == SolverStatus::Converged, result.status == SolverStatus::TerminatedEarly, result.iterations); return result.status == SolverStatus::Converged || result.status == SolverStatus::TerminatedEarly; } template void preserveOldRelevantValues(std::vector const& allValues, storm::storage::BitVector const& relevantValues, std::vector& oldValues) { storm::utility::vector::selectVectorValues(oldValues, relevantValues, allValues); } template ValueType computeMaxAbsDiff(std::vector const& allValues, storm::storage::BitVector const& relevantValues, std::vector const& oldValues) { ValueType result = storm::utility::zero(); auto oldValueIt = oldValues.begin(); for (auto value : relevantValues) { result = storm::utility::max(result, storm::utility::abs(allValues[value] - *oldValueIt)); } return result; } template ValueType computeMaxAbsDiff(std::vector const& allOldValues, std::vector const& allNewValues, storm::storage::BitVector const& relevantValues) { ValueType result = storm::utility::zero(); for (auto value : relevantValues) { result = storm::utility::max(result, storm::utility::abs(allNewValues[value] - allOldValues[value])); } return result; } template bool NativeLinearEquationSolver::solveEquationsIntervalIteration(Environment const& env, std::vector& x, std::vector const& b) const { STORM_LOG_THROW(this->hasLowerBound(), storm::exceptions::UnmetRequirementException, "Solver requires lower bound, but none was given."); STORM_LOG_THROW(this->hasUpperBound(), storm::exceptions::UnmetRequirementException, "Solver requires upper bound, but none was given."); STORM_LOG_INFO("Solving linear equation system (" << x.size() << " rows) with NativeLinearEquationSolver (IntervalIteration)"); std::vector* lowerX = &x; this->createLowerBoundsVector(*lowerX); this->createUpperBoundsVector(this->cachedRowVector, this->getMatrixRowCount()); std::vector* upperX = this->cachedRowVector.get(); bool useGaussSeidelMultiplication = env.solver().native().getPowerMethodMultiplicationStyle() == storm::solver::MultiplicationStyle::GaussSeidel; std::vector* tmp; if (!useGaussSeidelMultiplication) { cachedRowVector2 = std::make_unique>(x.size()); tmp = cachedRowVector2.get(); } if (!this->multiplier) { this->multiplier = storm::solver::MultiplierFactory().create(env, *A); } SolverStatus status = SolverStatus::InProgress; uint64_t iterations = 0; bool doConvergenceCheck = true; bool useDiffs = this->hasRelevantValues() && !env.solver().native().isSymmetricUpdatesSet(); std::vector oldValues; if (useGaussSeidelMultiplication && useDiffs) { oldValues.resize(this->getRelevantValues().getNumberOfSetBits()); } ValueType maxLowerDiff = storm::utility::zero(); ValueType maxUpperDiff = storm::utility::zero(); ValueType precision = storm::utility::convertNumber(env.solver().native().getPrecision()); bool relative = env.solver().native().getRelativeTerminationCriterion(); if (!relative) { precision *= storm::utility::convertNumber(2.0); } uint64_t maxIter = env.solver().native().getMaximalNumberOfIterations(); this->startMeasureProgress(); while (status == SolverStatus::InProgress && iterations < maxIter) { // Remember in which directions we took steps in this iteration. bool lowerStep = false; bool upperStep = false; // In every thousandth iteration or if the differences are the same, we improve both bounds. if (iterations % 1000 == 0 || maxLowerDiff == maxUpperDiff) { lowerStep = true; upperStep = true; if (useGaussSeidelMultiplication) { if (useDiffs) { preserveOldRelevantValues(*lowerX, this->getRelevantValues(), oldValues); } this->multiplier->multiplyGaussSeidel(env, *lowerX, &b); if (useDiffs) { maxLowerDiff = computeMaxAbsDiff(*lowerX, this->getRelevantValues(), oldValues); preserveOldRelevantValues(*upperX, this->getRelevantValues(), oldValues); } this->multiplier->multiplyGaussSeidel(env, *upperX, &b); if (useDiffs) { maxUpperDiff = computeMaxAbsDiff(*upperX, this->getRelevantValues(), oldValues); } } else { this->multiplier->multiply(env, *lowerX, &b, *tmp); if (useDiffs) { maxLowerDiff = computeMaxAbsDiff(*lowerX, *tmp, this->getRelevantValues()); } std::swap(tmp, lowerX); this->multiplier->multiply(env, *upperX, &b, *tmp); if (useDiffs) { maxUpperDiff = computeMaxAbsDiff(*upperX, *tmp, this->getRelevantValues()); } std::swap(tmp, upperX); } } else { // In the following iterations, we improve the bound with the greatest difference. if (useGaussSeidelMultiplication) { if (maxLowerDiff >= maxUpperDiff) { if (useDiffs) { preserveOldRelevantValues(*lowerX, this->getRelevantValues(), oldValues); } this->multiplier->multiplyGaussSeidel(env, *lowerX, &b); if (useDiffs) { maxLowerDiff = computeMaxAbsDiff(*lowerX, this->getRelevantValues(), oldValues); } lowerStep = true; } else { if (useDiffs) { preserveOldRelevantValues(*upperX, this->getRelevantValues(), oldValues); } this->multiplier->multiplyGaussSeidel(env, *upperX, &b); if (useDiffs) { maxUpperDiff = computeMaxAbsDiff(*upperX, this->getRelevantValues(), oldValues); } upperStep = true; } } else { if (maxLowerDiff >= maxUpperDiff) { this->multiplier->multiply(env, *lowerX, &b, *tmp); if (useDiffs) { maxLowerDiff = computeMaxAbsDiff(*lowerX, *tmp, this->getRelevantValues()); } std::swap(tmp, lowerX); lowerStep = true; } else { this->multiplier->multiply(env, *upperX, &b, *tmp); if (useDiffs) { maxUpperDiff = computeMaxAbsDiff(*upperX, *tmp, this->getRelevantValues()); } std::swap(tmp, upperX); upperStep = true; } } } STORM_LOG_ASSERT(maxLowerDiff >= storm::utility::zero(), "Expected non-negative lower diff."); STORM_LOG_ASSERT(maxUpperDiff >= storm::utility::zero(), "Expected non-negative upper diff."); if (iterations % 1000 == 0) { STORM_LOG_TRACE("Iteration " << iterations << ": lower difference: " << maxLowerDiff << ", upper difference: " << maxUpperDiff << "."); } if (doConvergenceCheck) { // Now check if the process already converged within our precision. Note that we double the target // precision here. Doing so, we need to take the means of the lower and upper values later to guarantee // the original precision. if (this->hasRelevantValues()) { if (storm::utility::vector::equalModuloPrecision(*lowerX, *upperX, this->getRelevantValues(), precision, relative)) { status = SolverStatus::Converged; } } else { if (storm::utility::vector::equalModuloPrecision(*lowerX, *upperX, precision, relative)) { status = SolverStatus::Converged; } } if (lowerStep && this->terminateNow(*lowerX, SolverGuarantee::LessOrEqual)) { status = SolverStatus::TerminatedEarly; } if (upperStep && this->terminateNow(*upperX, SolverGuarantee::GreaterOrEqual)) { status = SolverStatus::TerminatedEarly; } } // Potentially show progress. this->showProgressIterative(iterations); // Set up next iteration. ++iterations; doConvergenceCheck = !doConvergenceCheck; status = this->updateStatus(status, false, iterations, maxIter); } // We take the means of the lower and upper bound so we guarantee the desired precision. storm::utility::vector::applyPointwise(*lowerX, *upperX, *lowerX, [] (ValueType const& a, ValueType const& b) -> ValueType { return (a + b) / storm::utility::convertNumber(2.0); }); // Since we shuffled the pointer around, we need to write the actual results to the input/output vector x. if (&x == tmp) { std::swap(x, *tmp); } else if (&x == this->cachedRowVector.get()) { std::swap(x, *this->cachedRowVector); } if (!this->isCachingEnabled()) { clearCache(); } this->reportStatus(status, iterations); return status == SolverStatus::Converged; } template bool NativeLinearEquationSolver::solveEquationsSoundValueIteration(Environment const& env, std::vector& x, std::vector const& b) const { // Prepare the solution vectors and the helper. assert(x.size() == this->A->getRowCount()); if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(); } if (!this->soundValueIterationHelper) { this->soundValueIterationHelper = std::make_unique>(*this->A, x, *this->cachedRowVector, env.solver().native().getRelativeTerminationCriterion(), storm::utility::convertNumber(env.solver().native().getPrecision())); } else { this->soundValueIterationHelper = std::make_unique>(std::move(*this->soundValueIterationHelper), x, *this->cachedRowVector, env.solver().native().getRelativeTerminationCriterion(), storm::utility::convertNumber(env.solver().native().getPrecision())); } // Prepare initial bounds for the solution (if given) if (this->hasLowerBound()) { this->soundValueIterationHelper->setLowerBound(this->getLowerBound(true)); } if (this->hasUpperBound()) { this->soundValueIterationHelper->setUpperBound(this->getUpperBound(true)); } storm::storage::BitVector const* relevantValuesPtr = nullptr; if (this->hasRelevantValues()) { relevantValuesPtr = &this->getRelevantValues(); } SolverStatus status = SolverStatus::InProgress; this->startMeasureProgress(); uint64_t iterations = 0; while (status == SolverStatus::InProgress && iterations < env.solver().native().getMaximalNumberOfIterations()) { this->soundValueIterationHelper->performIterationStep(b); if (this->soundValueIterationHelper->checkConvergenceUpdateBounds(relevantValuesPtr)) { status = SolverStatus::Converged; } // Update environment variables. ++iterations; // Potentially show progress. this->showProgressIterative(iterations); status = this->updateStatus(status, this->hasCustomTerminationCondition() && this->soundValueIterationHelper->checkCustomTerminationCondition(this->getTerminationCondition()), iterations, env.solver().native().getMaximalNumberOfIterations()); } this->soundValueIterationHelper->setSolutionVector(); this->reportStatus(status, iterations); if (!this->isCachingEnabled()) { clearCache(); } return status == SolverStatus::Converged; } template bool NativeLinearEquationSolver::solveEquationsOptimisticValueIteration(Environment const& env, std::vector& x, std::vector const& b) const { if (!this->multiplier) { this->multiplier = storm::solver::MultiplierFactory().create(env, *this->A); } if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(this->A->getRowCount()); } if (!this->cachedRowVector2) { this->cachedRowVector2 = std::make_unique>(this->A->getRowCount()); } // By default, we can not provide any guarantee SolverGuarantee guarantee = SolverGuarantee::None; // Get handle to multiplier. storm::solver::Multiplier const &multiplier = *this->multiplier; // Allow aliased multiplications. storm::solver::MultiplicationStyle multiplicationStyle = env.solver().native().getPowerMethodMultiplicationStyle(); bool useGaussSeidelMultiplication = multiplicationStyle == storm::solver::MultiplicationStyle::GaussSeidel; boost::optional relevantValues; if (this->hasRelevantValues()) { relevantValues = this->getRelevantValues(); } // x has to start with a lower bound. this->createLowerBoundsVector(x); std::vector* lowerX = &x; std::vector* upperX = this->cachedRowVector.get(); std::vector* auxVector = this->cachedRowVector2.get(); this->startMeasureProgress(); auto statusIters = storm::solver::helper::solveEquationsOptimisticValueIteration(env, lowerX, upperX, auxVector, [&] (std::vector*& y, std::vector*& yPrime, ValueType const& precision, bool const& relative, uint64_t const& i, uint64_t const& maxI) { this->showProgressIterative(i); return performPowerIteration(env, y, yPrime, b, precision, relative, guarantee, i, maxI, multiplicationStyle); }, [&] (std::vector* y, std::vector* yPrime, uint64_t const& i) { this->showProgressIterative(i); if (useGaussSeidelMultiplication) { // Copy over the current vectors so we can modify them in-place. // This is necessary as we want to compare the new values with the current ones. *yPrime = *y; multiplier.multiplyGaussSeidel(env, *y, &b); } else { multiplier.multiply(env, *y, &b, *yPrime); std::swap(y, yPrime); } }, relevantValues); auto two = storm::utility::convertNumber(2.0); storm::utility::vector::applyPointwise(*lowerX, *upperX, x, [&two] (ValueType const& a, ValueType const& b) -> ValueType { return (a + b) / two; }); this->logIterations(statusIters.first == SolverStatus::Converged, statusIters.first == SolverStatus::TerminatedEarly, statusIters.second); if (!this->isCachingEnabled()) { clearCache(); } return statusIters.first == SolverStatus::Converged || statusIters.first == SolverStatus::TerminatedEarly; } template bool NativeLinearEquationSolver::solveEquationsRationalSearch(Environment const& env, std::vector& x, std::vector const& b) const { return solveEquationsRationalSearchHelper(env, x, b); } template struct TemporaryHelper { static std::vector* getTemporary(std::vector& rationalX, std::vector*& currentX, std::vector*& newX) { return &rationalX; } static void swapSolutions(std::vector& rationalX, std::vector*& rationalSolution, std::vector& x, std::vector*& currentX, std::vector*& newX) { // Nothing to do. } }; template struct TemporaryHelper { static std::vector* getTemporary(std::vector& rationalX, std::vector*& currentX, std::vector*& newX) { return newX; } static void swapSolutions(std::vector& rationalX, std::vector*& rationalSolution, std::vector& x, std::vector*& currentX, std::vector*& newX) { if (&rationalX == rationalSolution) { // In this case, the rational solution is in place. // However, since the rational solution is no alias to current x, the imprecise solution is stored // in current x and and rational x is not an alias to x, we can swap the contents of currentX to x. std::swap(x, *currentX); } else { // Still, we may assume that the rational solution is not current x and is therefore new x. std::swap(rationalX, *rationalSolution); std::swap(x, *currentX); } } }; template template bool NativeLinearEquationSolver::solveEquationsRationalSearchHelper(Environment const& env, NativeLinearEquationSolver const& impreciseSolver, storm::storage::SparseMatrix const& rationalA, std::vector& rationalX, std::vector const& rationalB, storm::storage::SparseMatrix const& A, std::vector& x, std::vector const& b, std::vector& tmpX) const { ValueType precision = storm::utility::convertNumber(env.solver().native().getPrecision()); uint64_t maxIter = env.solver().native().getMaximalNumberOfIterations(); bool relative = env.solver().native().getRelativeTerminationCriterion(); auto multiplicationStyle = env.solver().native().getPowerMethodMultiplicationStyle(); std::vector const* originalX = &x; std::vector* currentX = &x; std::vector* newX = &tmpX; SolverStatus status = SolverStatus::InProgress; uint64_t overallIterations = 0; uint64_t valueIterationInvocations = 0; impreciseSolver.startMeasureProgress(); while (status == SolverStatus::InProgress && overallIterations < maxIter) { // Perform value iteration with the current precision. typename NativeLinearEquationSolver::PowerIterationResult result = impreciseSolver.performPowerIteration(env, currentX, newX, b, storm::utility::convertNumber(precision), relative, SolverGuarantee::LessOrEqual, overallIterations, maxIter, multiplicationStyle); // At this point, the result of the imprecise value iteration is stored in the (imprecise) current x. ++valueIterationInvocations; STORM_LOG_TRACE("Completed " << valueIterationInvocations << " power iteration invocations, the last one with precision " << precision << " completed in " << result.iterations << " iterations."); // Count the iterations. overallIterations += result.iterations; // Compute maximal precision until which to sharpen. uint64_t p = storm::utility::convertNumber(storm::utility::ceil(storm::utility::log10(storm::utility::one() / precision))); // Make sure that currentX and rationalX are not aliased. std::vector* temporaryRational = TemporaryHelper::getTemporary(rationalX, currentX, newX); // Sharpen solution and place it in the temporary rational. bool foundSolution = sharpen(p, rationalA, *currentX, rationalB, *temporaryRational); // After sharpen, if a solution was found, it is contained in the free rational. if (foundSolution) { status = SolverStatus::Converged; TemporaryHelper::swapSolutions(rationalX, temporaryRational, x, currentX, newX); } else { // Increase the precision. precision = precision / 10; } if (storm::utility::resources::isTerminate()) { status = SolverStatus::Aborted; } } // Swap the two vectors if the current result is not in the original x. if (currentX != originalX) { std::swap(x, tmpX); } if (status == SolverStatus::InProgress && overallIterations == maxIter) { status = SolverStatus::MaximalIterationsExceeded; } this->logIterations(status == SolverStatus::Converged, status == SolverStatus::TerminatedEarly, overallIterations); return status == SolverStatus::Converged || status == SolverStatus::TerminatedEarly; } template template typename std::enable_if::value && !NumberTraits::IsExact, bool>::type NativeLinearEquationSolver::solveEquationsRationalSearchHelper(Environment const& env, std::vector& x, std::vector const& b) const { // Version for when the overall value type is imprecise. // Create a rational representation of the input so we can check for a proper solution later. storm::storage::SparseMatrix rationalA = this->A->template toValueType(); std::vector rationalX(x.size()); std::vector rationalB = storm::utility::vector::convertNumericVector(b); if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(this->A->getRowCount()); } if (!this->multiplier) { this->multiplier = storm::solver::MultiplierFactory().create(env, *A); } // Forward the call to the core rational search routine. bool converged = solveEquationsRationalSearchHelper(env, *this, rationalA, rationalX, rationalB, *this->A, x, b, *this->cachedRowVector); // Translate back rational result to imprecise result. auto targetIt = x.begin(); for (auto it = rationalX.begin(), ite = rationalX.end(); it != ite; ++it, ++targetIt) { *targetIt = storm::utility::convertNumber(*it); } if (!this->isCachingEnabled()) { this->clearCache(); } return converged; } template template typename std::enable_if::value && NumberTraits::IsExact, bool>::type NativeLinearEquationSolver::solveEquationsRationalSearchHelper(Environment const& env, std::vector& x, std::vector const& b) const { // Version for when the overall value type is exact and the same type is to be used for the imprecise part. if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(this->A->getRowCount()); } if (!this->multiplier) { this->multiplier = storm::solver::MultiplierFactory().create(env, *A); } // Forward the call to the core rational search routine. bool converged = solveEquationsRationalSearchHelper(env, *this, *this->A, x, b, *this->A, *this->cachedRowVector, b, x); if (!this->isCachingEnabled()) { this->clearCache(); } return converged; } template template typename std::enable_if::value, bool>::type NativeLinearEquationSolver::solveEquationsRationalSearchHelper(Environment const& env, std::vector& x, std::vector const& b) const { // Version for when the overall value type is exact and the imprecise one is not. We first try to solve the // problem using the imprecise data type and fall back to the exact type as needed. // Translate A to its imprecise version. storm::storage::SparseMatrix impreciseA = this->A->template toValueType(); // Translate x to its imprecise version. std::vector impreciseX(x.size()); { std::vector tmp(x.size()); this->createLowerBoundsVector(tmp); auto targetIt = impreciseX.begin(); for (auto sourceIt = tmp.begin(); targetIt != impreciseX.end(); ++targetIt, ++sourceIt) { *targetIt = storm::utility::convertNumber(*sourceIt); } } // Create temporary storage for an imprecise x. std::vector impreciseTmpX(x.size()); // Translate b to its imprecise version. std::vector impreciseB(b.size()); auto targetIt = impreciseB.begin(); for (auto sourceIt = b.begin(); targetIt != impreciseB.end(); ++targetIt, ++sourceIt) { *targetIt = storm::utility::convertNumber(*sourceIt); } // Create imprecise solver from the imprecise data. NativeLinearEquationSolver impreciseSolver; impreciseSolver.setMatrix(impreciseA); impreciseSolver.setCachingEnabled(true); impreciseSolver.multiplier = storm::solver::MultiplierFactory().create(env, impreciseA); bool converged = false; try { // Forward the call to the core rational search routine. converged = solveEquationsRationalSearchHelper(env, impreciseSolver, *this->A, x, b, impreciseA, impreciseX, impreciseB, impreciseTmpX); impreciseSolver.clearCache(); } catch (storm::exceptions::PrecisionExceededException const& e) { STORM_LOG_WARN("Precision of value type was exceeded, trying to recover by switching to rational arithmetic."); if (!this->cachedRowVector) { this->cachedRowVector = std::make_unique>(this->A->getRowGroupCount()); } if (!this->multiplier) { this->multiplier = storm::solver::MultiplierFactory().create(env, *A); } // Translate the imprecise value iteration result to the one we are going to use from now on. auto targetIt = this->cachedRowVector->begin(); for (auto it = impreciseX.begin(), ite = impreciseX.end(); it != ite; ++it, ++targetIt) { *targetIt = storm::utility::convertNumber(*it); } // Get rid of the superfluous data structures. impreciseX = std::vector(); impreciseTmpX = std::vector(); impreciseB = std::vector(); impreciseA = storm::storage::SparseMatrix(); // Forward the call to the core rational search routine, but now with our value type as the imprecise value type. converged = solveEquationsRationalSearchHelper(env, *this, *this->A, x, b, *this->A, *this->cachedRowVector, b, x); } if (!this->isCachingEnabled()) { this->clearCache(); } return converged; } template template bool NativeLinearEquationSolver::sharpen(uint64_t precision, storm::storage::SparseMatrix const& A, std::vector const& x, std::vector const& b, std::vector& tmp) { for (uint64_t p = 1; p <= precision; ++p) { storm::utility::kwek_mehlhorn::sharpen(p, x, tmp); if (NativeLinearEquationSolver::isSolution(A, tmp, b)) { return true; } } return false; } template bool NativeLinearEquationSolver::isSolution(storm::storage::SparseMatrix const& matrix, std::vector const& values, std::vector const& b) { storm::utility::ConstantsComparator comparator; auto valueIt = values.begin(); auto bIt = b.begin(); for (uint64_t row = 0; row < matrix.getRowCount(); ++row, ++valueIt, ++bIt) { ValueType rowValue = *bIt + matrix.multiplyRowWithVector(row, values); // If the value does not match the one in the values vector, the given vector is not a solution. if (!comparator.isEqual(rowValue, *valueIt)) { return false; } } // Checked all values at this point. return true; } template void NativeLinearEquationSolver::logIterations(bool converged, bool terminate, uint64_t iterations) const { if (converged) { STORM_LOG_INFO("Iterative solver converged in " << iterations << " iterations."); } else if (terminate) { STORM_LOG_INFO("Iterative solver terminated after " << iterations << " iterations."); } else { STORM_LOG_WARN("Iterative solver did not converge in " << iterations << " iterations."); } } template NativeLinearEquationSolverMethod NativeLinearEquationSolver::getMethod(Environment const& env, bool isExactMode) const { // Adjust the method if none was specified and we want exact or sound computations auto method = env.solver().native().getMethod(); if (isExactMode && method != NativeLinearEquationSolverMethod::RationalSearch) { if (env.solver().native().isMethodSetFromDefault()) { method = NativeLinearEquationSolverMethod::RationalSearch; STORM_LOG_INFO("Selecting '" + toString(method) + "' as the solution technique to guarantee exact results. If you want to override this, please explicitly specify a different method."); } else { STORM_LOG_WARN("The selected solution method does not guarantee exact results."); } } else if (env.solver().isForceSoundness() && method != NativeLinearEquationSolverMethod::SoundValueIteration && method != NativeLinearEquationSolverMethod::OptimisticValueIteration && method != NativeLinearEquationSolverMethod::IntervalIteration && method != NativeLinearEquationSolverMethod::RationalSearch) { if (env.solver().native().isMethodSetFromDefault()) { method = NativeLinearEquationSolverMethod::OptimisticValueIteration; STORM_LOG_INFO("Selecting '" + toString(method) + "' as the solution technique to guarantee sound results. If you want to override this, please explicitly specify a different method."); } else { STORM_LOG_WARN("The selected solution method does not guarantee sound results."); } } return method; } template bool NativeLinearEquationSolver::internalSolveEquations(Environment const& env, std::vector& x, std::vector const& b) const { switch(getMethod(env, storm::NumberTraits::IsExact || env.solver().isForceExact())) { case NativeLinearEquationSolverMethod::SOR: return this->solveEquationsSOR(env, x, b, storm::utility::convertNumber(env.solver().native().getSorOmega())); case NativeLinearEquationSolverMethod::GaussSeidel: return this->solveEquationsSOR(env, x, b, storm::utility::one()); case NativeLinearEquationSolverMethod::Jacobi: return this->solveEquationsJacobi(env, x, b); case NativeLinearEquationSolverMethod::WalkerChae: return this->solveEquationsWalkerChae(env, x, b); case NativeLinearEquationSolverMethod::Power: return this->solveEquationsPower(env, x, b); case NativeLinearEquationSolverMethod::SoundValueIteration: return this->solveEquationsSoundValueIteration(env, x, b); case NativeLinearEquationSolverMethod::OptimisticValueIteration: return this->solveEquationsOptimisticValueIteration(env, x, b); case NativeLinearEquationSolverMethod::IntervalIteration: return this->solveEquationsIntervalIteration(env, x, b); case NativeLinearEquationSolverMethod::RationalSearch: return this->solveEquationsRationalSearch(env, x, b); } STORM_LOG_THROW(false, storm::exceptions::InvalidEnvironmentException, "Unknown solving technique."); return false; } template LinearEquationSolverProblemFormat NativeLinearEquationSolver::getEquationProblemFormat(Environment const& env) const { auto method = getMethod(env, storm::NumberTraits::IsExact || env.solver().isForceExact()); if (method == NativeLinearEquationSolverMethod::Power || method == NativeLinearEquationSolverMethod::SoundValueIteration || method == NativeLinearEquationSolverMethod::OptimisticValueIteration || method == NativeLinearEquationSolverMethod::RationalSearch || method == NativeLinearEquationSolverMethod::IntervalIteration) { return LinearEquationSolverProblemFormat::FixedPointSystem; } else { return LinearEquationSolverProblemFormat::EquationSystem; } } template LinearEquationSolverRequirements NativeLinearEquationSolver::getRequirements(Environment const& env) const { LinearEquationSolverRequirements requirements; auto method = getMethod(env, storm::NumberTraits::IsExact || env.solver().isForceExact()); if (method == NativeLinearEquationSolverMethod::IntervalIteration) { requirements.requireBounds(); } else if (method == NativeLinearEquationSolverMethod::RationalSearch || method == NativeLinearEquationSolverMethod::OptimisticValueIteration) { requirements.requireLowerBounds(); } else if (method == NativeLinearEquationSolverMethod::SoundValueIteration) { requirements.requireBounds(false); } return requirements; } template void NativeLinearEquationSolver::clearCache() const { jacobiDecomposition.reset(); cachedRowVector2.reset(); walkerChaeData.reset(); multiplier.reset(); soundValueIterationHelper.reset(); LinearEquationSolver::clearCache(); } template uint64_t NativeLinearEquationSolver::getMatrixRowCount() const { return this->A->getRowCount(); } template uint64_t NativeLinearEquationSolver::getMatrixColumnCount() const { return this->A->getColumnCount(); } template std::unique_ptr> NativeLinearEquationSolverFactory::create(Environment const& env) const { return std::make_unique>(); } template std::unique_ptr> NativeLinearEquationSolverFactory::clone() const { return std::make_unique>(*this); } // Explicitly instantiate the linear equation solver. template class NativeLinearEquationSolver; template class NativeLinearEquationSolverFactory; #ifdef STORM_HAVE_CARL template class NativeLinearEquationSolver; template class NativeLinearEquationSolverFactory; #endif } }