From 5c36f51664a2b86d9969d297b6dbdfff34c0f83e Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 20 Aug 2026 14:31:55 +0200 Subject: [PATCH 01/29] Add upper and lowerbounds to DTMC and MDP model checking with OVI or II. (cherry picked from commit a428484f10e180d30435df958cc62b08ea3662f6) --- .gitignore | 1 + .../prctl/SparseDtmcPrctlModelChecker.cpp | 18 +- .../prctl/SparseMdpPrctlModelChecker.cpp | 3 + .../helper/MDPModelCheckingHelperReturnType.h | 6 + .../prctl/helper/SparseDtmcPrctlHelper.cpp | 28 +- .../prctl/helper/SparseDtmcPrctlHelper.h | 10 +- .../prctl/helper/SparseMdpPrctlHelper.cpp | 25 +- .../ExplicitQuantitativeCheckResult.cpp | 241 ++++++++++++++---- .../results/ExplicitQuantitativeCheckResult.h | 49 +++- src/storm/solver/AbstractEquationSolver.cpp | 28 ++ src/storm/solver/AbstractEquationSolver.h | 30 +++ .../IterativeMinMaxLinearEquationSolver.cpp | 12 +- src/storm/solver/LinearEquationSolver.cpp | 2 + .../solver/MinMaxLinearEquationSolver.cpp | 2 + .../solver/NativeLinearEquationSolver.cpp | 13 +- src/storm/solver/SolutionBounds.h | 17 ++ .../solver/helper/IntervalterationHelper.cpp | 18 +- .../solver/helper/IntervalterationHelper.h | 3 +- .../helper/OptimisticValueIterationHelper.cpp | 15 +- .../helper/OptimisticValueIterationHelper.h | 7 +- 20 files changed, 452 insertions(+), 76 deletions(-) create mode 100644 src/storm/solver/SolutionBounds.h diff --git a/.gitignore b/.gitignore index a041c170ba..26400a3d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ nbproject/ .idea .vscode *.out +.claude/ diff --git a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp index 02cc325577..19f2bd19f8 100644 --- a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp @@ -151,12 +151,17 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c std::unique_ptr rightResultPointer = this->check(env, pathFormula.getRightSubformula()); ExplicitQualitativeCheckResult const& leftResult = leftResultPointer->template asExplicitQualitativeCheckResult(); ExplicitQualitativeCheckResult const& rightResult = rightResultPointer->template asExplicitQualitativeCheckResult(); + storm::solver::SolutionBounds solutionBounds; std::vector numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), this->getModel().getBackwardTransitions(), leftResult.getTruthValuesVector(), rightResult.getTruthValuesVector(), checkTask.isQualitativeSet(), - checkTask.getHint()); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); + checkTask.getHint(), &solutionBounds); + auto result = std::make_unique>(std::move(numericResult)); + if (solutionBounds) { + result->setBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + } + return result; } template @@ -168,11 +173,16 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c storm::logic::GloballyFormula const& pathFormula = checkTask.getFormula(); std::unique_ptr subResultPointer = this->check(env, pathFormula.getSubformula()); ExplicitQualitativeCheckResult const& subResult = subResultPointer->template asExplicitQualitativeCheckResult(); + storm::solver::SolutionBounds solutionBounds; std::vector numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), - this->getModel().getBackwardTransitions(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet()); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); + this->getModel().getBackwardTransitions(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet(), &solutionBounds); + auto result = std::make_unique>(std::move(numericResult)); + if (solutionBounds) { + result->setBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + } + return result; } } diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index a572bffbb6..b5d615bb91 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -182,6 +182,9 @@ std::unique_ptr SparseMdpPrctlModelChecker::com this->getModel().getBackwardTransitions(), leftResult.getTruthValuesVector(), rightResult.getTruthValuesVector(), checkTask.isQualitativeSet(), checkTask.isProduceSchedulersSet(), checkTask.getHint()); std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(ret.values))); + if (ret.solutionBounds) { + result->asExplicitQuantitativeCheckResult().setBounds(std::move(ret.solutionBounds->first), std::move(ret.solutionBounds->second)); + } if (checkTask.isProduceSchedulersSet() && ret.scheduler) { result->asExplicitQuantitativeCheckResult().setScheduler(std::move(ret.scheduler)); } diff --git a/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h b/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h index a16951c14f..941be866a4 100644 --- a/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h +++ b/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h @@ -2,6 +2,7 @@ #include #include +#include "storm/solver/SolutionBounds.h" #include "storm/storage/Scheduler.h" namespace storm { @@ -30,6 +31,11 @@ struct MDPSparseModelCheckingHelperReturnType { // A scheduler, if it was computed. std::unique_ptr> scheduler; + + // Sound bounds on the values, if the algorithm that computed them provided any. These are bounds on the + // values, so they are expressed in the same type as those: a bound on a reward that may be infinite has to + // be able to say so. + storm::solver::SolutionBounds solutionBounds; }; } // namespace helper diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp index e0023cb043..789f89dc10 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp @@ -169,7 +169,7 @@ template std::vector SparseDtmcPrctlHelper::computeUntilProbabilities( Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, - bool qualitative, ModelCheckerHint const& hint) { + bool qualitative, ModelCheckerHint const& hint, storm::solver::SolutionBounds* solutionBounds) { std::vector result(transitionMatrix.getRowCount(), storm::utility::zero()); // We need to identify the maybe states (states which have a probability for satisfying the until formula @@ -274,6 +274,15 @@ std::vector SparseDtmcPrctlHelpersetBounds(storm::utility::zero(), storm::utility::one()); solver->solveEquations(env, x, b); + if (solutionBounds != nullptr && solver->hasSolutionBounds()) { + // Outside of the maybe states the probability is exactly zero or one, so the entries that + // result already holds bound those states from both sides. + std::vector lower(result), upper(result); + storm::utility::vector::setVectorValues(lower, maybeStates, solver->getSolutionLowerBounds()); + storm::utility::vector::setVectorValues(upper, maybeStates, solver->getSolutionUpperBounds()); + *solutionBounds = std::make_pair(std::move(lower), std::move(upper)); + } + // Set values of resulting vector according to result. storm::utility::vector::setVectorValues(result, maybeStates, x); } @@ -352,16 +361,29 @@ std::vector SparseDtmcPrctlHelper std::vector SparseDtmcPrctlHelper::computeGloballyProbabilities( Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& psiStates, bool qualitative) { + storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& psiStates, bool qualitative, + storm::solver::SolutionBounds* solutionBounds) { if constexpr (storm::IsIntervalType) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "We do not support computing globally probabilities with interval models."); } else { goal.oneMinus(); std::vector result = computeUntilProbabilities(env, std::move(goal), transitionMatrix, backwardTransitions, - storm::storage::BitVector(transitionMatrix.getRowCount(), true), ~psiStates, qualitative); + storm::storage::BitVector(transitionMatrix.getRowCount(), true), ~psiStates, qualitative, + ModelCheckerHint(), solutionBounds); for (auto& entry : result) { entry = storm::utility::one() - entry; } + if (solutionBounds != nullptr && *solutionBounds) { + // One minus is antitone, so the complement of the lower bound bounds the result from above. + auto& bounds = **solutionBounds; + for (auto& entry : bounds.first) { + entry = storm::utility::one() - entry; + } + for (auto& entry : bounds.second) { + entry = storm::utility::one() - entry; + } + std::swap(bounds.first, bounds.second); + } return result; } } diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h index bf36dba0c4..5ec0653516 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h @@ -35,11 +35,16 @@ class SparseDtmcPrctlHelper { static std::vector computeNextProbabilities(Environment const& env, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& nextStates); + /*! + * @param solutionBounds If given, receives sound bounds on the computed probabilities, provided that the + * equation solver produced any. + */ static std::vector computeUntilProbabilities(Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, - bool qualitative, ModelCheckerHint const& hint = ModelCheckerHint()); + bool qualitative, ModelCheckerHint const& hint = ModelCheckerHint(), + storm::solver::SolutionBounds* solutionBounds = nullptr); static std::vector computeAllUntilProbabilities(Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, @@ -49,7 +54,8 @@ class SparseDtmcPrctlHelper { static std::vector computeGloballyProbabilities(Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::SparseMatrix const& backwardTransitions, - storm::storage::BitVector const& psiStates, bool qualitative); + storm::storage::BitVector const& psiStates, bool qualitative, + storm::solver::SolutionBounds* solutionBounds = nullptr); static std::vector computeCumulativeRewards(Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, diff --git a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp index 3892110f4c..d8d61959b9 100644 --- a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp @@ -423,6 +423,9 @@ struct MaybeStateResult { std::vector values; boost::optional> scheduler; + + // Sound bounds on the values, if the solver provided any. + storm::solver::SolutionBounds solutionBounds; }; template @@ -484,6 +487,9 @@ MaybeStateResult computeValuesForMaybeStates(Environment const& en // Create result. MaybeStateResult result(std::move(x)); + if (solver->hasSolutionBounds()) { + result.solutionBounds = std::make_pair(solver->getSolutionLowerBounds(), solver->getSolutionUpperBounds()); + } // If requested, return the requested scheduler. if (produceScheduler) { @@ -690,6 +696,9 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper result(transitionMatrix.getRowGroupCount(), storm::utility::zero()); + // Sound bounds on the result, if the solver provides any. + storm::solver::SolutionBounds resultBounds; + // We need to identify the maybe states (states which have a probability for satisfying the until formula // that is strictly between 0 and 1) and the states that satisfy the formula with probablity 1 and 0, respectively. QualitativeStateSetsUntilProbabilities qualitativeStateSets = @@ -765,7 +774,17 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper) { // For non-interval models, we only operated on the maybe states, and we must recover the qualitative values for the other state. - storm::utility::vector::setVectorValues(result, qualitativeStateSets.maybeStates, resultForMaybeStates.getValues()); + if (resultForMaybeStates.solutionBounds) { + // Outside of the maybe states the probability is exactly zero or one, so the entries + // that result already holds bound those states from both sides. + std::vector lower(result), upper(result); + storm::utility::vector::setVectorValues(lower, qualitativeStateSets.maybeStates, + resultForMaybeStates.solutionBounds->first); + storm::utility::vector::setVectorValues(upper, qualitativeStateSets.maybeStates, + resultForMaybeStates.solutionBounds->second); + resultBounds = std::make_pair(std::move(lower), std::move(upper)); + } + storm::utility::vector::setVectorValues(result, qualitativeStateSets.maybeStates, resultForMaybeStates.getValues()); } else { // For interval models, the result for maybe states indeed also holds values for all qualitative states. STORM_LOG_ASSERT(resultForMaybeStates.getValues().size() == transitionMatrix.getColumnCount(), "Dimensions do not match."); @@ -791,7 +810,9 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelperisMemorylessScheduler(), "Expected a memoryless scheduler."); // Return result. - return MDPSparseModelCheckingHelperReturnType(std::move(result), std::move(scheduler)); + MDPSparseModelCheckingHelperReturnType returnValue(std::move(result), std::move(scheduler)); + returnValue.solutionBounds = std::move(resultBounds); + return returnValue; } template diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 763de9f709..02bbeae4bb 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -15,6 +15,58 @@ namespace storm { namespace modelchecker { + +namespace detail { +/*! + * Restricts the given values to the states selected by the filter. Note that the result is always a map, even + * if the given values are a vector. + */ +template +boost::variant, std::map> filterValues( + boost::variant, std::map> const& values, + storm::storage::BitVector const& filterTruthValues) { + typedef std::vector vector_type; + typedef std::map map_type; + + map_type newMap; + if (values.which() == 0) { + vector_type const& valuesAsVector = boost::get(values); + for (auto element : filterTruthValues) { + STORM_LOG_THROW(element < valuesAsVector.size(), storm::exceptions::InvalidAccessException, "Invalid index in results."); + newMap.emplace(element, valuesAsVector[element]); + } + } else { + for (auto const& element : boost::get(values)) { + if (filterTruthValues.get(element.first)) { + newMap.insert(element); + } + } + STORM_LOG_THROW(newMap.size() == filterTruthValues.getNumberOfSetBits(), storm::exceptions::InvalidOperationException, + "The check result fails to contain some results referred to by the filter."); + } + return newMap; +} + +/*! + * Replaces every value by one minus that value. + */ +template +void complementValues(boost::variant, std::map>& values) { + typedef std::vector vector_type; + typedef std::map map_type; + + if (values.which() == 0) { + for (auto& element : boost::get(values)) { + element = storm::utility::one() - element; + } + } else { + for (auto& element : boost::get(values)) { + element.second = storm::utility::one() - element.second; + } + } +} +} // namespace detail + template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult() : values(map_type()) { // Intentionally left empty. @@ -110,7 +162,10 @@ ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(Expl template std::unique_ptr ExplicitQuantitativeCheckResult::clone() const { - return std::make_unique>(this->values, this->scheduler); + auto result = std::make_unique>(this->values, this->scheduler); + result->lowerBounds = this->lowerBounds; + result->upperBounds = this->upperBounds; + return result; } template @@ -133,6 +188,75 @@ std::vector ExplicitQuantitativeCheckResult::getFiniteValu return storm::utility::narrowFinite(boost::get(values)); } +template +bool ExplicitQuantitativeCheckResult::hasLowerBounds() const { + return lowerBounds.has_value(); +} + +template +bool ExplicitQuantitativeCheckResult::hasUpperBounds() const { + return upperBounds.has_value(); +} + +template +typename ExplicitQuantitativeCheckResult::vector_type const& ExplicitQuantitativeCheckResult::getLowerBoundVector() const { + STORM_LOG_THROW(this->hasLowerBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown lower bounds."); + return boost::get(*lowerBounds); +} + +template +typename ExplicitQuantitativeCheckResult::vector_type const& ExplicitQuantitativeCheckResult::getUpperBoundVector() const { + STORM_LOG_THROW(this->hasUpperBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown upper bounds."); + return boost::get(*upperBounds); +} + +template +typename ExplicitQuantitativeCheckResult::map_type const& ExplicitQuantitativeCheckResult::getLowerBoundMap() const { + STORM_LOG_THROW(this->hasLowerBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown lower bounds."); + return boost::get(*lowerBounds); +} + +template +typename ExplicitQuantitativeCheckResult::map_type const& ExplicitQuantitativeCheckResult::getUpperBoundMap() const { + STORM_LOG_THROW(this->hasUpperBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown upper bounds."); + return boost::get(*upperBounds); +} + +template +void ExplicitQuantitativeCheckResult::assertBoundsShape([[maybe_unused]] boost::variant const& bounds) const { + STORM_LOG_ASSERT(bounds.which() == values.which(), "Bounds must have the same shape as the values."); + if (this->isResultForAllStates()) { + STORM_LOG_ASSERT(boost::get(bounds).size() == boost::get(values).size(), "Bounds must have the same size as the values."); + } else { + STORM_LOG_ASSERT(boost::get(bounds).size() == boost::get(values).size(), "Bounds must have the same size as the values."); + } +} + +template +void ExplicitQuantitativeCheckResult::setLowerBounds(boost::variant lowerBounds) { + this->assertBoundsShape(lowerBounds); + this->lowerBounds = std::move(lowerBounds); +} + +template +void ExplicitQuantitativeCheckResult::setUpperBounds(boost::variant upperBounds) { + this->assertBoundsShape(upperBounds); + this->upperBounds = std::move(upperBounds); +} + +template +void ExplicitQuantitativeCheckResult::setBounds(boost::variant lowerBounds, + boost::variant upperBounds) { + this->setLowerBounds(std::move(lowerBounds)); + this->setUpperBounds(std::move(upperBounds)); +} + +template +void ExplicitQuantitativeCheckResult::clearBounds() { + lowerBounds = std::nullopt; + upperBounds = std::nullopt; +} + template void ExplicitQuantitativeCheckResult::filter(QualitativeCheckResult const& filter) { STORM_LOG_THROW(filter.isExplicitQualitativeCheckResult(), storm::exceptions::InvalidOperationException, @@ -142,29 +266,13 @@ void ExplicitQuantitativeCheckResult::filter(QualitativeCheckResult c ExplicitQualitativeCheckResult const& explicitFilter = filter.template asExplicitQualitativeCheckResult(); typename ExplicitQualitativeCheckResult::vector_type const& filterTruthValues = explicitFilter.getTruthValuesVector(); - if (this->isResultForAllStates()) { - map_type newMap; - - for (auto element : filterTruthValues) { - STORM_LOG_THROW(element < this->getValueVector().size(), storm::exceptions::InvalidAccessException, "Invalid index in results."); - newMap.emplace(element, this->getValueVector()[element]); - } - this->values = newMap; - } else { - map_type const& map = boost::get(values); - - map_type newMap; - for (auto const& element : map) { - if (filterTruthValues.get(element.first)) { - newMap.insert(element); - } - } - - STORM_LOG_THROW(newMap.size() == filterTruthValues.getNumberOfSetBits(), storm::exceptions::InvalidOperationException, - "The check result fails to contain some results referred to by the filter."); - - this->values = newMap; + if (this->hasLowerBounds()) { + this->lowerBounds = detail::filterValues(*this->lowerBounds, filterTruthValues); } + if (this->hasUpperBounds()) { + this->upperBounds = detail::filterValues(*this->upperBounds, filterTruthValues); + } + this->values = detail::filterValues(this->values, filterTruthValues); } template @@ -320,6 +428,29 @@ void printRange(std::ostream& out, ValueType const& min, ValueType const& max) { out << " (range)"; } +template +void ExplicitQuantitativeCheckResult::printValue(std::ostream& out, storm::storage::sparse::state_type state) const { + print(out, (*this)[state]); + if (!this->hasLowerBounds() && !this->hasUpperBounds()) { + return; + } + // The enclosure is printed next to the estimate. Note that this is unrelated to the range over all states + // that is printed for large results below. + out << " ["; + if (this->hasLowerBounds()) { + print(out, this->isResultForAllStates() ? this->getLowerBoundVector()[state] : this->getLowerBoundMap().at(state)); + } else { + out << "-inf"; + } + out << ", "; + if (this->hasUpperBounds()) { + print(out, this->isResultForAllStates() ? this->getUpperBoundVector()[state] : this->getUpperBoundMap().at(state)); + } else { + out << "inf"; + } + out << "]"; +} + template std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ostream& out) const { bool minMaxSupported = std::is_same::value || std::is_same::value; @@ -332,13 +463,13 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost } else { out << "{"; bool first = true; - for (auto const& element : valuesAsVector) { + for (uint64_t state = 0; state < valuesAsVector.size(); ++state) { if (!first) { out << ", "; } else { first = false; } - print(out, element); + this->printValue(out, state); } out << "}"; } @@ -348,7 +479,7 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost printAsRange = true; } else { if (valuesAsMap.size() == 1) { - print(out, valuesAsMap.begin()->second); + this->printValue(out, valuesAsMap.begin()->first); } else { out << "{"; bool first = true; @@ -358,7 +489,7 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost } else { first = false; } - print(out, element.second); + this->printValue(out, element.first); } out << "}"; } @@ -486,35 +617,51 @@ bool ExplicitQuantitativeCheckResult::isExplicitQuantitativeCheckResu template void ExplicitQuantitativeCheckResult::oneMinus() { - if (this->isResultForAllStates()) { - for (auto& element : boost::get(values)) { - element = storm::utility::one() - element; - } + detail::complementValues(values); + if (this->hasLowerBounds()) { + detail::complementValues(*lowerBounds); + } + if (this->hasUpperBounds()) { + detail::complementValues(*upperBounds); + } + // One minus is antitone, so what used to bound the values from below now bounds them from above. + std::swap(lowerBounds, upperBounds); +} + +/*! + * Writes the given value under the given key, spelling out an infinity that the plain value type cannot represent. + */ +template +void insertJsonValue(storm::json& entry, std::string const& key, storm::utility::ExtendedValueType const& value) { + if (storm::utility::isInfinity(value)) { + entry[key] = "inf"; + } else if (storm::utility::isNegativeInfinity(value)) { + entry[key] = "-inf"; + } else if constexpr (std::is_same_v, ValueType>) { + entry[key] = value; } else { - for (auto& element : boost::get(values)) { - element.second = storm::utility::one() - element.second; - } + entry[key] = value.getFinite(); } } template void insertJsonEntry(storm::json& json, uint64_t const& id, storm::utility::ExtendedValueType const& value, std::optional const& stateValuations = std::nullopt, - std::optional const& stateLabels = std::nullopt) { + std::optional const& stateLabels = std::nullopt, + std::optional> const& lowerBound = std::nullopt, + std::optional> const& upperBound = std::nullopt) { typename storm::json entry; if (stateValuations) { entry["s"] = stateValuations->template toJson(id); } else { entry["s"] = id; } - if (storm::utility::isInfinity(value)) { - entry["v"] = "inf"; - } else if (storm::utility::isNegativeInfinity(value)) { - entry["v"] = "-inf"; - } else if constexpr (std::is_same_v, ValueType>) { - entry["v"] = value; - } else { - entry["v"] = value.getFinite(); + insertJsonValue(entry, "v", value); + if (lowerBound) { + insertJsonValue(entry, "lb", *lowerBound); + } + if (upperBound) { + insertJsonValue(entry, "ub", *upperBound); } if (stateLabels) { auto labs = stateLabels->getLabelsOfState(id); @@ -530,12 +677,16 @@ storm::json ExplicitQuantitativeCheckResult::toJson(std::o if (this->isResultForAllStates()) { vector_type const& valuesAsVector = boost::get(values); for (uint64_t state = 0; state < valuesAsVector.size(); ++state) { - insertJsonEntry(result, state, valuesAsVector[state], stateValuations, stateLabels); + insertJsonEntry(result, state, valuesAsVector[state], stateValuations, stateLabels, + this->hasLowerBounds() ? std::make_optional(this->getLowerBoundVector()[state]) : std::nullopt, + this->hasUpperBounds() ? std::make_optional(this->getUpperBoundVector()[state]) : std::nullopt); } } else { map_type const& valuesAsMap = boost::get(values); for (auto const& stateValue : valuesAsMap) { - insertJsonEntry(result, stateValue.first, stateValue.second, stateValuations, stateLabels); + insertJsonEntry(result, stateValue.first, stateValue.second, stateValuations, stateLabels, + this->hasLowerBounds() ? std::make_optional(this->getLowerBoundMap().at(stateValue.first)) : std::nullopt, + this->hasUpperBounds() ? std::make_optional(this->getUpperBoundMap().at(stateValue.first)) : std::nullopt); } } return result; diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h index e7baaf749b..dbe20b2586 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h @@ -85,6 +85,36 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult getSentinelValueVector() const; + /*! + * Retrieves whether sound lower resp. upper bounds on the actual values are known. + * A bound that is not known is not stored at all, so that "no information" cannot be confused with an + * infinite bound: an individual entry of a known bound may well be (minus) infinity. + */ + bool hasLowerBounds() const; + bool hasUpperBounds() const; + + /*! + * Retrieves the sound lower resp. upper bounds on the actual values. + * @pre The respective bounds are known. + */ + vector_type const& getLowerBoundVector() const; + vector_type const& getUpperBoundVector() const; + map_type const& getLowerBoundMap() const; + map_type const& getUpperBoundMap() const; + + /*! + * Sets sound bounds on the actual values. The bounds must have the same shape as the values, i.e. a vector + * of the same size resp. a map with the same keys. + */ + void setLowerBounds(boost::variant lowerBounds); + void setUpperBounds(boost::variant upperBounds); + void setBounds(boost::variant lowerBounds, boost::variant upperBounds); + + /*! + * Drops all bounds, e.g. after an operation that cannot maintain them. + */ + void clearBounds(); + virtual std::ostream& writeToStream(std::ostream& out) const override; virtual void filter(QualitativeCheckResult const& filter) override; @@ -110,9 +140,26 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult const& bounds) const; + + /*! + * Writes the value of the given state, followed by its bounds if any are known. + */ + void printValue(std::ostream& out, storm::storage::sparse::state_type state) const; + + // The values of the quantitative check result. These are estimates of the actual values, which lie within + // the bounds below but carry no further guarantee. boost::variant values; + // Sound lower bounds on the actual values, if an algorithm provided them. + std::optional> lowerBounds; + + // Sound upper bounds on the actual values, if an algorithm provided them. + std::optional> upperBounds; + // An optional scheduler that accompanies the values. std::optional>> scheduler; }; diff --git a/src/storm/solver/AbstractEquationSolver.cpp b/src/storm/solver/AbstractEquationSolver.cpp index 00ab9896aa..85f735415d 100644 --- a/src/storm/solver/AbstractEquationSolver.cpp +++ b/src/storm/solver/AbstractEquationSolver.cpp @@ -240,6 +240,34 @@ void AbstractEquationSolver::setBoundsFromOtherSolver(AbstractEquatio } } +template +bool AbstractEquationSolver::hasSolutionBounds() const { + return solutionBounds.has_value(); +} + +template +std::vector const& AbstractEquationSolver::getSolutionLowerBounds() const { + STORM_LOG_ASSERT(this->hasSolutionBounds(), "No bounds on the solution were computed."); + return solutionBounds->first; +} + +template +std::vector const& AbstractEquationSolver::getSolutionUpperBounds() const { + STORM_LOG_ASSERT(this->hasSolutionBounds(), "No bounds on the solution were computed."); + return solutionBounds->second; +} + +template +void AbstractEquationSolver::setSolutionBounds(std::vector lower, std::vector upper) const { + STORM_LOG_ASSERT(lower.size() == upper.size(), "Bounds on the solution must have the same size."); + solutionBounds = std::make_pair(std::move(lower), std::move(upper)); +} + +template +void AbstractEquationSolver::clearSolutionBounds() const { + solutionBounds = std::nullopt; +} + template void AbstractEquationSolver::clearBounds() { lowerBound = boost::none; diff --git a/src/storm/solver/AbstractEquationSolver.h b/src/storm/solver/AbstractEquationSolver.h index 1564af3a67..7f7e5a0a89 100644 --- a/src/storm/solver/AbstractEquationSolver.h +++ b/src/storm/solver/AbstractEquationSolver.h @@ -5,6 +5,7 @@ #include #include +#include "storm/solver/SolutionBounds.h" #include "storm/solver/SolverStatus.h" #include "storm/solver/TerminationCondition.h" #include "storm/utility/ProgressMeasurement.h" @@ -182,6 +183,20 @@ class AbstractEquationSolver { */ void clearBounds(); + /*! + * Retrieves whether the last call to this solver computed sound bounds on the solution. + * Only some algorithms, most notably interval iteration and optimistic value iteration, provide these, and + * even those only when they converged. + */ + bool hasSolutionBounds() const; + + /*! + * Retrieves sound bounds on the solution that the last call to this solver computed. + * @pre Such bounds were computed, see hasSolutionBounds(). + */ + std::vector const& getSolutionLowerBounds() const; + std::vector const& getSolutionUpperBounds() const; + /*! * Retrieves whether progress is to be shown. */ @@ -211,6 +226,18 @@ class AbstractEquationSolver { TerminationCondition const& getTerminationCondition() const; std::unique_ptr> const& getTerminationConditionPointer() const; + /*! + * Stores sound bounds on the solution that were obtained while solving. Note that solving is const, so + * that this is as well. + */ + void setSolutionBounds(std::vector lower, std::vector upper) const; + + /*! + * Discards any bounds on the solution obtained by a previous call. This must happen whenever solving + * starts, so that a solver that is reused does not report stale bounds. + */ + void clearSolutionBounds() const; + void createUpperBoundsVector(std::vector& upperBoundsVector) const; void createUpperBoundsVector(std::unique_ptr>& upperBoundsVector, uint64_t length) const; void createLowerBoundsVector(std::vector& lowerBoundsVector) const; @@ -263,6 +290,9 @@ class AbstractEquationSolver { boost::optional> upperBounds; private: + // Sound bounds on the solution, if the last call to this solver produced any. + mutable SolutionBounds solutionBounds; + // Indicates the progress of this solver. mutable boost::optional progressMeasurement; }; diff --git a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp index a60cf187a0..8ba2fa8ff2 100644 --- a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp +++ b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp @@ -596,8 +596,12 @@ bool IterativeMinMaxLinearEquationSolver::solveEquation guessingFactor = storm::utility::convertNumber(*env.solver().ovi().getUpperBoundGuessingFactor()); } this->startMeasureProgress(); + storm::solver::SolutionBounds solutionBounds; auto status = oviHelper.OVI(x, b, numIterations, env.solver().minMax().getRelativeTerminationCriterion(), prec, dir, guessingFactor, lowerBound, - upperBound, oviCallback); + upperBound, oviCallback, &solutionBounds); + if (solutionBounds) { + this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + } this->reportStatus(status, numIterations); // If requested, we store the scheduler for retrieval. @@ -806,8 +810,12 @@ bool IterativeMinMaxLinearEquationSolver::solveEquation optionalRelevantValues = this->getRelevantValues(); } this->startMeasureProgress(); + storm::solver::SolutionBounds solutionBounds; auto status = iiHelper.II(x, b, numIterations, env.solver().minMax().getRelativeTerminationCriterion(), prec, lowerBoundsCallback, upperBoundsCallback, - dir, iiCallback, optionalRelevantValues); + dir, iiCallback, optionalRelevantValues, &solutionBounds); + if (solutionBounds) { + this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + } this->reportStatus(status, numIterations); // If requested, we store the scheduler for retrieval. diff --git a/src/storm/solver/LinearEquationSolver.cpp b/src/storm/solver/LinearEquationSolver.cpp index 37e05d236a..af9337b9a5 100644 --- a/src/storm/solver/LinearEquationSolver.cpp +++ b/src/storm/solver/LinearEquationSolver.cpp @@ -24,6 +24,8 @@ LinearEquationSolver::LinearEquationSolver() : cachingEnabled(false) template bool LinearEquationSolver::solveEquations(Environment const& env, std::vector& x, std::vector const& b) const { + // A reused solver must not report bounds that a previous call computed. + this->clearSolutionBounds(); return this->internalSolveEquations(env, x, b); } diff --git a/src/storm/solver/MinMaxLinearEquationSolver.cpp b/src/storm/solver/MinMaxLinearEquationSolver.cpp index bece987ea6..59c43a96f0 100644 --- a/src/storm/solver/MinMaxLinearEquationSolver.cpp +++ b/src/storm/solver/MinMaxLinearEquationSolver.cpp @@ -40,6 +40,8 @@ bool MinMaxLinearEquationSolver::solveEquations(Environ STORM_LOG_WARN_COND_DEBUG(this->isRequirementsCheckedSet(), "The requirements of the solver have not been marked as checked. Please provide the appropriate check or mark the requirements " "as checked (if applicable)."); + // A reused solver must not report bounds that a previous call computed. + this->clearSolutionBounds(); return internalSolveEquations(env, d, x, b); } diff --git a/src/storm/solver/NativeLinearEquationSolver.cpp b/src/storm/solver/NativeLinearEquationSolver.cpp index 0b2ec60068..48bd585278 100644 --- a/src/storm/solver/NativeLinearEquationSolver.cpp +++ b/src/storm/solver/NativeLinearEquationSolver.cpp @@ -441,8 +441,12 @@ bool NativeLinearEquationSolver::solveEquationsIntervalIteration(Envi optionalRelevantValues = this->getRelevantValues(); } this->startMeasureProgress(); + storm::solver::SolutionBounds solutionBounds; auto status = iiHelper.II(x, b, numIterations, env.solver().native().getRelativeTerminationCriterion(), prec, lowerBoundsCallback, upperBoundsCallback, {}, - iiCallback, optionalRelevantValues); + iiCallback, optionalRelevantValues, &solutionBounds); + if (solutionBounds) { + this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + } this->reportStatus(status, numIterations); if (!this->isCachingEnabled()) { @@ -525,7 +529,12 @@ bool NativeLinearEquationSolver::solveEquationsOptimisticValueIterati guessingFactor = storm::utility::convertNumber(*env.solver().ovi().getUpperBoundGuessingFactor()); } this->startMeasureProgress(); - auto status = oviHelper.OVI(x, b, env.solver().native().getRelativeTerminationCriterion(), prec, {}, guessingFactor, lowerBound, upperBound, oviCallback); + storm::solver::SolutionBounds solutionBounds; + auto status = oviHelper.OVI(x, b, env.solver().native().getRelativeTerminationCriterion(), prec, {}, guessingFactor, lowerBound, upperBound, oviCallback, + &solutionBounds); + if (solutionBounds) { + this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + } this->reportStatus(status, numIterations); if (!this->isCachingEnabled()) { diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h new file mode 100644 index 0000000000..a4d53e651f --- /dev/null +++ b/src/storm/solver/SolutionBounds.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace storm::solver { + +/*! + * Sound lower and upper bounds on a solution vector, as computed by e.g. interval iteration. + * The first component bounds the solution from below, the second one from above. Both have the same size as + * the solution. An unset value means that the algorithm in question did not provide any bounds. + */ +template +using SolutionBounds = std::optional, std::vector>>; + +} // namespace storm::solver diff --git a/src/storm/solver/helper/IntervalterationHelper.cpp b/src/storm/solver/helper/IntervalterationHelper.cpp index 7923f1d457..864ad8477d 100644 --- a/src/storm/solver/helper/IntervalterationHelper.cpp +++ b/src/storm/solver/helper/IntervalterationHelper.cpp @@ -116,13 +116,11 @@ SolverStatus IntervalIterationHelper::II(std::pai } template -SolverStatus IntervalIterationHelper::II(std::vector& operand, std::vector const& offsets, - uint64_t& numIterations, bool relative, ValueType const& precision, - std::function&)> const& prepareLowerBounds, - std::function&)> const& prepareUpperBounds, - std::optional const& dir, - std::function const&)> const& iterationCallback, - std::optional const& relevantValues) const { +SolverStatus IntervalIterationHelper::II( + std::vector& operand, std::vector const& offsets, uint64_t& numIterations, bool relative, ValueType const& precision, + std::function&)> const& prepareLowerBounds, std::function&)> const& prepareUpperBounds, + std::optional const& dir, std::function const&)> const& iterationCallback, + std::optional const& relevantValues, SolutionBounds* solutionBounds) const { // Create two vectors x and y using the given operand plus an auxiliary vector. std::pair, std::vector> xy; auto& auxVector = viOperator->allocateAuxiliaryVector(operand.size()); @@ -140,6 +138,12 @@ SolverStatus IntervalIterationHelper::II(std::vec } else { status = II(xy, offsets, numIterations, relative, precision, iterationCallback, relevantValues); } + if (solutionBounds != nullptr) { + // Hand out the enclosure before it is collapsed into the point estimate below. Interval iteration + // maintains xy.first below and xy.second above the solution in every iteration, so these are sound + // even if the iteration was aborted before converging. + *solutionBounds = std::make_pair(xy.first, xy.second); + } auto two = storm::utility::convertNumber(2.0); // get the average of lower- and upper result storm::utility::vector::applyPointwise( diff --git a/src/storm/solver/helper/IntervalterationHelper.h b/src/storm/solver/helper/IntervalterationHelper.h index 2eb4e77cb4..9ff6355d2a 100644 --- a/src/storm/solver/helper/IntervalterationHelper.h +++ b/src/storm/solver/helper/IntervalterationHelper.h @@ -6,6 +6,7 @@ #include #include "storm/solver/OptimizationDirection.h" +#include "storm/solver/SolutionBounds.h" #include "storm/solver/SolverStatus.h" #include "storm/solver/helper/ValueIterationOperatorForward.h" #include "storm/storage/BitVector.h" @@ -37,7 +38,7 @@ class IntervalIterationHelper { std::function&)> const& prepareLowerBounds, std::function&)> const& prepareUpperBounds, std::optional const& dir = {}, std::function const&)> const& iterationCallback = {}, - std::optional const& relevantValues = {}) const; + std::optional const& relevantValues = {}, SolutionBounds* solutionBounds = nullptr) const; SolverStatus II(std::vector& operand, std::vector const& offsets, bool relative, ValueType const& precision, std::function&)> const& prepareLowerBounds, diff --git a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp index 8c7770567e..390ad1ed85 100644 --- a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp +++ b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp @@ -269,8 +269,8 @@ template SolverStatus OptimisticValueIterationHelper::OVI( std::vector& operand, std::vector const& offsets, uint64_t& numIterations, bool relative, ValueType const& precision, std::optional const& dir, std::optional const& guessValue, std::optional const& lowerBound, - std::optional const& upperBound, - std::function const&)> const& iterationCallback) const { + std::optional const& upperBound, std::function const&)> const& iterationCallback, + SolutionBounds* solutionBounds) const { // Create two vectors v and u using the given operand plus an auxiliary vector. std::pair, std::vector> vu; auto& auxVector = viOperator->allocateAuxiliaryVector(operand.size()); @@ -281,6 +281,11 @@ SolverStatus OptimisticValueIterationHelper::OVI( doublePrec -= precision * 1e-6; // be slightly more precise to avoid a good chunk of floating point issues } auto status = OVI(vu, offsets, numIterations, relative, doublePrec, dir, guessValue ? *guessValue : doublePrec, lowerBound, upperBound, iterationCallback); + if (solutionBounds != nullptr && status == SolverStatus::Converged) { + // Only a converged run has verified that vu.second lies above the solution. Until the verification + // phase succeeds it is merely a guess, so handing it out as an upper bound would be unsound. + *solutionBounds = std::make_pair(vu.first, vu.second); + } auto two = storm::utility::convertNumber(2.0); // get the average of lower- and upper result storm::utility::vector::applyPointwise( @@ -296,10 +301,10 @@ template SolverStatus OptimisticValueIterationHelper::OVI( std::vector& operand, std::vector const& offsets, bool relative, ValueType const& precision, std::optional const& dir, std::optional const& guessValue, std::optional const& lowerBound, - std::optional const& upperBound, - std::function const&)> const& iterationCallback) const { + std::optional const& upperBound, std::function const&)> const& iterationCallback, + SolutionBounds* solutionBounds) const { uint64_t numIterations = 0; - return OVI(operand, offsets, numIterations, relative, precision, dir, guessValue, lowerBound, upperBound, iterationCallback); + return OVI(operand, offsets, numIterations, relative, precision, dir, guessValue, lowerBound, upperBound, iterationCallback, solutionBounds); } template class OptimisticValueIterationHelper; diff --git a/src/storm/solver/helper/OptimisticValueIterationHelper.h b/src/storm/solver/helper/OptimisticValueIterationHelper.h index 5491046f1f..60f2d0b458 100644 --- a/src/storm/solver/helper/OptimisticValueIterationHelper.h +++ b/src/storm/solver/helper/OptimisticValueIterationHelper.h @@ -7,6 +7,7 @@ #include #include "storm/solver/OptimizationDirection.h" +#include "storm/solver/SolutionBounds.h" #include "storm/solver/SolverStatus.h" #include "storm/solver/helper/ValueIterationOperatorForward.h" @@ -36,12 +37,14 @@ class OptimisticValueIterationHelper { SolverStatus OVI(std::vector& operand, std::vector const& offsets, uint64_t& numIterations, bool relative, ValueType const& precision, std::optional const& dir = {}, std::optional const& guessValue = {}, std::optional const& lowerBound = {}, std::optional const& upperBound = {}, - std::function const&)> const& iterationCallback = {}) const; + std::function const&)> const& iterationCallback = {}, + SolutionBounds* solutionBounds = nullptr) const; SolverStatus OVI(std::vector& operand, std::vector const& offsets, bool relative, ValueType const& precision, std::optional const& dir = {}, std::optional const& guessValue = {}, std::optional const& lowerBound = {}, std::optional const& upperBound = {}, - std::function const&)> const& iterationCallback = {}) const; + std::function const&)> const& iterationCallback = {}, + SolutionBounds* solutionBounds = nullptr) const; private: template From 99b69995c7fbb371fe7b39476f1a7457eccdcc5f Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Mon, 24 Aug 2026 15:47:25 +0200 Subject: [PATCH 02/29] Better handling of moving around the bounds between functions. (cherry picked from commit 6e304c50ad19fd0d19d6cfa19db726238a2ed70e) --- .../api/counterexamples.cpp | 2 +- .../SMTMinimalLabelSetGenerator.h | 5 +- .../abstraction/GameBasedMdpModelChecker.cpp | 12 ++- .../csl/SparseCtmcCslModelChecker.cpp | 8 +- .../csl/helper/SparseCtmcCslHelper.cpp | 6 +- .../helper/ltl/SparseLTLHelper.cpp | 8 +- .../prctl/SparseDtmcPrctlModelChecker.cpp | 30 +++----- .../prctl/SparseMdpPrctlModelChecker.cpp | 5 +- .../prctl/helper/SparseDtmcPrctlHelper.cpp | 75 +++++++++++-------- .../prctl/helper/SparseDtmcPrctlHelper.h | 25 +++---- .../prctl/helper/SparseMdpPrctlHelper.cpp | 41 +++++++--- .../results/ExplicitQuantitativeCheckResult.h | 15 ++++ src/storm/solver/AbstractEquationSolver.cpp | 26 ++++--- src/storm/solver/AbstractEquationSolver.h | 12 +-- .../IterativeMinMaxLinearEquationSolver.cpp | 8 +- .../solver/NativeLinearEquationSolver.cpp | 12 +-- src/storm/solver/SolutionBounds.h | 32 +++++++- .../solver/helper/IntervalterationHelper.cpp | 7 +- .../helper/OptimisticValueIterationHelper.cpp | 23 ++++-- 19 files changed, 221 insertions(+), 131 deletions(-) diff --git a/src/storm-counterexamples/api/counterexamples.cpp b/src/storm-counterexamples/api/counterexamples.cpp index eed7d3cfcd..e73e57122b 100644 --- a/src/storm-counterexamples/api/counterexamples.cpp +++ b/src/storm-counterexamples/api/counterexamples.cpp @@ -51,7 +51,7 @@ std::shared_ptr computeKShortestPathCoun storm::storage::BitVector phiStates(model->getNumberOfStates(), true); auto results = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( env, false, model->getTransitionMatrix(), model->getBackwardTransitions(), phiStates, subQualitativeResult.getTruthValuesVector(), true); - double reachProb = results.at(initialState); + double reachProb = results.values.at(initialState); STORM_LOG_THROW((reachProb > threshold) || (strictBound && reachProb >= threshold), storm::exceptions::InvalidArgumentException, "Given probability threshold " << threshold << " cannot be " << (strictBound ? "achieved" : "exceeded") << " in model with maximal reachability probability of " << reachProb << "."); diff --git a/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h b/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h index 9445855d2d..2b4b8857ef 100644 --- a/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h +++ b/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h @@ -1668,8 +1668,9 @@ class SMTMinimalLabelSetGenerator { if (model.isOfType(storm::models::ModelType::Dtmc)) { if (rewardName == boost::none) { results.push_back(storm::utility::zero()); - allStatesResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - env, false, model.getTransitionMatrix(), model.getBackwardTransitions(), phiStates, psiStates, false); + allStatesResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + env, false, model.getTransitionMatrix(), model.getBackwardTransitions(), phiStates, psiStates, false) + .values); for (auto state : model.getInitialStates()) { STORM_LOG_TRACE("Found probability " << allStatesResult[state]); results.back() = std::max(results.back(), allStatesResult[state]); diff --git a/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp b/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp index 615fee4261..1f81c3e114 100644 --- a/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp +++ b/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp @@ -1286,8 +1286,10 @@ void postProcessStrategies(uint64_t iteration, storm::OptimizationDirection cons } } auto dtmcMatrix = dtmcMatrixBuilder.build(); - std::vector sanityValues = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false); + std::vector sanityValues = + std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false) + .values); ValueType maxDiff = storm::utility::zero(); uint64_t maxState = 0; @@ -1324,8 +1326,10 @@ void postProcessStrategies(uint64_t iteration, storm::OptimizationDirection cons } } dtmcMatrix = dtmcMatrixBuilder.build(); - sanityValues = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false); + sanityValues = + std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false) + .values); maxDiff = storm::utility::zero(); maxState = 0; diff --git a/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp b/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp index 37d79e0ec0..8dd8097015 100644 --- a/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp +++ b/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp @@ -94,9 +94,11 @@ std::unique_ptr SparseCtmcCslModelChecker::com std::unique_ptr subResultPointer = this->check(env, pathFormula.getSubformula()); ExplicitQualitativeCheckResult const& subResult = subResultPointer->template asExplicitQualitativeCheckResult(); auto probabilisticTransitions = this->getModel().computeProbabilityMatrix(); - std::vector numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( - env, storm::solver::SolveGoal(this->getModel(), checkTask), probabilisticTransitions, probabilisticTransitions.transpose(), - subResult.getTruthValuesVector(), checkTask.isQualitativeSet()); + // CTMC results do not carry bounds yet. + std::vector numericResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( + env, storm::solver::SolveGoal(this->getModel(), checkTask), probabilisticTransitions, + probabilisticTransitions.transpose(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet()) + .values); return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); } diff --git a/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp b/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp index 5bdeffaccf..e01e2ebfdc 100644 --- a/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp +++ b/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp @@ -258,8 +258,10 @@ std::vector SparseCtmcCslHelper::computeUntilProbabilities(Environmen storm::storage::SparseMatrix const& backwardTransitions, std::vector const& exitRateVector, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, bool qualitative) { - return SparseDtmcPrctlHelper::computeUntilProbabilities(env, std::move(goal), computeProbabilityMatrix(rateMatrix, exitRateVector), - backwardTransitions, phiStates, psiStates, qualitative); + // The CTMC helper does not expose bounds to its callers yet. + return std::move(SparseDtmcPrctlHelper::computeUntilProbabilities(env, std::move(goal), computeProbabilityMatrix(rateMatrix, exitRateVector), + backwardTransitions, phiStates, psiStates, qualitative) + .values); } template diff --git a/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp b/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp index c26c8350b1..6e75f5e4da 100644 --- a/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp +++ b/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp @@ -314,9 +314,11 @@ std::vector SparseLTLHelper::computeDAPr } } else { - prodNumericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - env, std::move(solveGoalProduct), product->getProductModel().getTransitionMatrix(), product->getProductModel().getBackwardTransitions(), bvTrue, - acceptingStates, this->isQualitativeSet()); + // Any bounds would have to be projected back to the original model alongside the values, which is not done yet. + prodNumericResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + env, std::move(solveGoalProduct), product->getProductModel().getTransitionMatrix(), + product->getProductModel().getBackwardTransitions(), bvTrue, acceptingStates, this->isQualitativeSet()) + .values); } std::vector numericResult = product->projectToOriginalModel(this->_transitionMatrix.getRowGroupCount(), prodNumericResult); diff --git a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp index 19f2bd19f8..1de00cdbed 100644 --- a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp @@ -151,16 +151,12 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c std::unique_ptr rightResultPointer = this->check(env, pathFormula.getRightSubformula()); ExplicitQualitativeCheckResult const& leftResult = leftResultPointer->template asExplicitQualitativeCheckResult(); ExplicitQualitativeCheckResult const& rightResult = rightResultPointer->template asExplicitQualitativeCheckResult(); - storm::solver::SolutionBounds solutionBounds; - std::vector numericResult = - storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), - this->getModel().getBackwardTransitions(), leftResult.getTruthValuesVector(), rightResult.getTruthValuesVector(), checkTask.isQualitativeSet(), - checkTask.getHint(), &solutionBounds); - auto result = std::make_unique>(std::move(numericResult)); - if (solutionBounds) { - result->setBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); - } + auto ret = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), + this->getModel().getBackwardTransitions(), leftResult.getTruthValuesVector(), rightResult.getTruthValuesVector(), checkTask.isQualitativeSet(), + checkTask.getHint()); + auto result = std::make_unique>(std::move(ret.values)); + setBounds(*result, ret.solutionBounds); return result; } @@ -173,15 +169,11 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c storm::logic::GloballyFormula const& pathFormula = checkTask.getFormula(); std::unique_ptr subResultPointer = this->check(env, pathFormula.getSubformula()); ExplicitQualitativeCheckResult const& subResult = subResultPointer->template asExplicitQualitativeCheckResult(); - storm::solver::SolutionBounds solutionBounds; - std::vector numericResult = - storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( - env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), - this->getModel().getBackwardTransitions(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet(), &solutionBounds); - auto result = std::make_unique>(std::move(numericResult)); - if (solutionBounds) { - result->setBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); - } + auto ret = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( + env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), + this->getModel().getBackwardTransitions(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet()); + auto result = std::make_unique>(std::move(ret.values)); + setBounds(*result, ret.solutionBounds); return result; } } diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index b5d615bb91..8d9d028c46 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -182,9 +182,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com this->getModel().getBackwardTransitions(), leftResult.getTruthValuesVector(), rightResult.getTruthValuesVector(), checkTask.isQualitativeSet(), checkTask.isProduceSchedulersSet(), checkTask.getHint()); std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(ret.values))); - if (ret.solutionBounds) { - result->asExplicitQuantitativeCheckResult().setBounds(std::move(ret.solutionBounds->first), std::move(ret.solutionBounds->second)); - } + setBounds(result->asExplicitQuantitativeCheckResult(), ret.solutionBounds); if (checkTask.isProduceSchedulersSet() && ret.scheduler) { result->asExplicitQuantitativeCheckResult().setScheduler(std::move(ret.scheduler)); } @@ -203,6 +201,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), this->getModel().getBackwardTransitions(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet(), checkTask.isProduceSchedulersSet()); std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(ret.values))); + setBounds(result->asExplicitQuantitativeCheckResult(), ret.solutionBounds); if (checkTask.isProduceSchedulersSet() && ret.scheduler) { result->asExplicitQuantitativeCheckResult().setScheduler(std::move(ret.scheduler)); } diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp index 789f89dc10..a89275002c 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp @@ -166,10 +166,11 @@ std::vector computeRobustValuesForMaybeStates(Environment const& e } template -std::vector SparseDtmcPrctlHelper::computeUntilProbabilities( +DTMCSparseModelCheckingHelperReturnType SparseDtmcPrctlHelper::computeUntilProbabilities( Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, - bool qualitative, ModelCheckerHint const& hint, storm::solver::SolutionBounds* solutionBounds) { + bool qualitative, ModelCheckerHint const& hint) { + storm::solver::SolutionBounds solutionBounds; std::vector result(transitionMatrix.getRowCount(), storm::utility::zero()); // We need to identify the maybe states (states which have a probability for satisfying the until formula @@ -274,13 +275,19 @@ std::vector SparseDtmcPrctlHelpersetBounds(storm::utility::zero(), storm::utility::one()); solver->solveEquations(env, x, b); - if (solutionBounds != nullptr && solver->hasSolutionBounds()) { - // Outside of the maybe states the probability is exactly zero or one, so the entries that - // result already holds bound those states from both sides. - std::vector lower(result), upper(result); - storm::utility::vector::setVectorValues(lower, maybeStates, solver->getSolutionLowerBounds()); - storm::utility::vector::setVectorValues(upper, maybeStates, solver->getSolutionUpperBounds()); - *solutionBounds = std::make_pair(std::move(lower), std::move(upper)); + // Outside of the maybe states the probability is exactly zero or one, so the entries that + // result already holds bound those states from both sides. Note that the solver may well know + // only one of the two bounds, e.g. optimistic value iteration that did not converge. + auto embedBound = [&result, &maybeStates](std::vector const& boundForMaybeStates) { + std::vector bound(result); + storm::utility::vector::setVectorValues(bound, maybeStates, boundForMaybeStates); + return bound; + }; + if (solver->hasSolutionLowerBounds()) { + solutionBounds.lower = embedBound(solver->getSolutionLowerBounds()); + } + if (solver->hasSolutionUpperBounds()) { + solutionBounds.upper = embedBound(solver->getSolutionUpperBounds()); } // Set values of resulting vector according to result. @@ -288,7 +295,9 @@ std::vector SparseDtmcPrctlHelper returnValue(std::move(result)); + returnValue.solutionBounds = std::move(solutionBounds); + return returnValue; } template @@ -359,32 +368,31 @@ std::vector SparseDtmcPrctlHelper -std::vector SparseDtmcPrctlHelper::computeGloballyProbabilities( +DTMCSparseModelCheckingHelperReturnType SparseDtmcPrctlHelper::computeGloballyProbabilities( Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& psiStates, bool qualitative, - storm::solver::SolutionBounds* solutionBounds) { + storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& psiStates, bool qualitative) { if constexpr (storm::IsIntervalType) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "We do not support computing globally probabilities with interval models."); } else { goal.oneMinus(); - std::vector result = computeUntilProbabilities(env, std::move(goal), transitionMatrix, backwardTransitions, - storm::storage::BitVector(transitionMatrix.getRowCount(), true), ~psiStates, qualitative, - ModelCheckerHint(), solutionBounds); - for (auto& entry : result) { + DTMCSparseModelCheckingHelperReturnType returnValue = + computeUntilProbabilities(env, std::move(goal), transitionMatrix, backwardTransitions, + storm::storage::BitVector(transitionMatrix.getRowCount(), true), ~psiStates, qualitative); + for (auto& entry : returnValue.values) { entry = storm::utility::one() - entry; } - if (solutionBounds != nullptr && *solutionBounds) { - // One minus is antitone, so the complement of the lower bound bounds the result from above. - auto& bounds = **solutionBounds; - for (auto& entry : bounds.first) { - entry = storm::utility::one() - entry; - } - for (auto& entry : bounds.second) { - entry = storm::utility::one() - entry; + // One minus is antitone, so the complement of the lower bound bounds the result from above and vice + // versa. In particular, knowing only one side before means knowing only the other side afterwards. + auto& bounds = returnValue.solutionBounds; + for (auto* bound : {&bounds.lower, &bounds.upper}) { + if (*bound) { + for (auto& entry : **bound) { + entry = storm::utility::one() - entry; + } } - std::swap(bounds.first, bounds.second); } - return result; + std::swap(bounds.lower, bounds.upper); + return returnValue; } } @@ -738,8 +746,9 @@ SparseDtmcPrctlHelper::computeBaierTra // Start by computing all 'before' states, i.e. the states for which the conditional probability is defined. std::vector probabilitiesToReachConditionStates = - computeUntilProbabilities(env, storm::solver::SolveGoal(), transitionMatrix, backwardTransitions, - storm::storage::BitVector(transitionMatrix.getRowCount(), true), conditionStates, false); + std::move(computeUntilProbabilities(env, storm::solver::SolveGoal(), transitionMatrix, backwardTransitions, + storm::storage::BitVector(transitionMatrix.getRowCount(), true), conditionStates, false) + .values); result.beforeStates = storm::storage::BitVector(targetStates.size(), true); uint_fast64_t state = 0; @@ -909,9 +918,11 @@ SparseDtmcPrctlHelper::computeConditio newRelevantValues = transformedModel.getNewRelevantStates(); } goal.setRelevantValues(std::move(newRelevantValues)); - std::vector conditionalProbabilities = computeUntilProbabilities( - env, std::move(goal), newTransitionMatrix, newTransitionMatrix.transpose(), - storm::storage::BitVector(newTransitionMatrix.getRowCount(), true), transformedModel.targetStates.get(), qualitative); + std::vector conditionalProbabilities = + std::move(computeUntilProbabilities(env, std::move(goal), newTransitionMatrix, newTransitionMatrix.transpose(), + storm::storage::BitVector(newTransitionMatrix.getRowCount(), true), transformedModel.targetStates.get(), + qualitative) + .values); storm::utility::vector::setVectorValues(result, transformedModel.beforeStates, conditionalProbabilities); } diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h index 5ec0653516..3c5dfabe31 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h @@ -5,6 +5,7 @@ #include #include "storm/modelchecker/hints/ModelCheckerHint.h" +#include "storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h" #include "storm/models/sparse/Dtmc.h" #include "storm/models/sparse/StandardRewardModel.h" @@ -36,26 +37,24 @@ class SparseDtmcPrctlHelper { storm::storage::BitVector const& nextStates); /*! - * @param solutionBounds If given, receives sound bounds on the computed probabilities, provided that the - * equation solver produced any. + * @return The probabilities, together with sound bounds on them if the equation solver provided any. */ - static std::vector computeUntilProbabilities(Environment const& env, storm::solver::SolveGoal&& goal, - storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::SparseMatrix const& backwardTransitions, - storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, - bool qualitative, ModelCheckerHint const& hint = ModelCheckerHint(), - storm::solver::SolutionBounds* solutionBounds = nullptr); + static DTMCSparseModelCheckingHelperReturnType computeUntilProbabilities( + Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& phiStates, + storm::storage::BitVector const& psiStates, bool qualitative, ModelCheckerHint const& hint = ModelCheckerHint()); static std::vector computeAllUntilProbabilities(Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& initialStates, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates); - static std::vector computeGloballyProbabilities(Environment const& env, storm::solver::SolveGoal&& goal, - storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::SparseMatrix const& backwardTransitions, - storm::storage::BitVector const& psiStates, bool qualitative, - storm::solver::SolutionBounds* solutionBounds = nullptr); + /*! + * @return The probabilities, together with sound bounds on them if the equation solver provided any. + */ + static DTMCSparseModelCheckingHelperReturnType computeGloballyProbabilities( + Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& psiStates, bool qualitative); static std::vector computeCumulativeRewards(Environment const& env, storm::solver::SolveGoal&& goal, storm::storage::SparseMatrix const& transitionMatrix, diff --git a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp index d8d61959b9..fec2e6c41f 100644 --- a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp @@ -487,8 +487,13 @@ MaybeStateResult computeValuesForMaybeStates(Environment const& en // Create result. MaybeStateResult result(std::move(x)); - if (solver->hasSolutionBounds()) { - result.solutionBounds = std::make_pair(solver->getSolutionLowerBounds(), solver->getSolutionUpperBounds()); + // Note that the solver may well know only one of the two bounds, e.g. optimistic value iteration that did + // not converge. + if (solver->hasSolutionLowerBounds()) { + result.solutionBounds.lower = solver->getSolutionLowerBounds(); + } + if (solver->hasSolutionUpperBounds()) { + result.solutionBounds.upper = solver->getSolutionUpperBounds(); } // If requested, return the requested scheduler. @@ -774,15 +779,18 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper) { // For non-interval models, we only operated on the maybe states, and we must recover the qualitative values for the other state. - if (resultForMaybeStates.solutionBounds) { - // Outside of the maybe states the probability is exactly zero or one, so the entries - // that result already holds bound those states from both sides. - std::vector lower(result), upper(result); - storm::utility::vector::setVectorValues(lower, qualitativeStateSets.maybeStates, - resultForMaybeStates.solutionBounds->first); - storm::utility::vector::setVectorValues(upper, qualitativeStateSets.maybeStates, - resultForMaybeStates.solutionBounds->second); - resultBounds = std::make_pair(std::move(lower), std::move(upper)); + // Outside of the maybe states the probability is exactly zero or one, so the entries that + // result already holds bound those states from both sides. + auto embedBound = [&result, &qualitativeStateSets](std::vector const& boundForMaybeStates) { + std::vector bound(result); + storm::utility::vector::setVectorValues(bound, qualitativeStateSets.maybeStates, boundForMaybeStates); + return bound; + }; + if (resultForMaybeStates.solutionBounds.hasLower()) { + resultBounds.lower = embedBound(*resultForMaybeStates.solutionBounds.lower); + } + if (resultForMaybeStates.solutionBounds.hasUpper()) { + resultBounds.upper = embedBound(*resultForMaybeStates.solutionBounds.upper); } storm::utility::vector::setVectorValues(result, qualitativeStateSets.maybeStates, resultForMaybeStates.getValues()); } else { @@ -840,6 +848,17 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper() - element; } + // One minus is antitone, so the complement of the lower bound bounds the result from above and vice + // versa. In particular, knowing only one side before means knowing only the other side afterwards. + auto& bounds = result.solutionBounds; + for (auto* bound : {&bounds.lower, &bounds.upper}) { + if (*bound) { + for (auto& entry : **bound) { + entry = storm::utility::one() - entry; + } + } + } + std::swap(bounds.lower, bounds.upper); return result; } } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h index dbe20b2586..8943f148fe 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h @@ -8,6 +8,7 @@ #include "storm/adapters/JsonForward.h" #include "storm/modelchecker/results/QuantitativeCheckResult.h" #include "storm/models/sparse/StateLabeling.h" +#include "storm/solver/SolutionBounds.h" #include "storm/storage/Scheduler.h" #include "storm/storage/sparse/StateType.h" #include "storm/storage/valuations/Valuations.h" @@ -163,5 +164,19 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult>> scheduler; }; + +/*! + * Transfers the sound bounds that an algorithm computed to the given check result. Each of the two sides is + * transferred only if the algorithm established it; the other side is left alone. + */ +template +void setBounds(ExplicitQuantitativeCheckResult& result, storm::solver::SolutionBounds& bounds) { + if (bounds.hasLower()) { + result.setLowerBounds(std::move(*bounds.lower)); + } + if (bounds.hasUpper()) { + result.setUpperBounds(std::move(*bounds.upper)); + } +} } // namespace modelchecker } // namespace storm diff --git a/src/storm/solver/AbstractEquationSolver.cpp b/src/storm/solver/AbstractEquationSolver.cpp index 85f735415d..3363796c0e 100644 --- a/src/storm/solver/AbstractEquationSolver.cpp +++ b/src/storm/solver/AbstractEquationSolver.cpp @@ -241,31 +241,37 @@ void AbstractEquationSolver::setBoundsFromOtherSolver(AbstractEquatio } template -bool AbstractEquationSolver::hasSolutionBounds() const { - return solutionBounds.has_value(); +bool AbstractEquationSolver::hasSolutionLowerBounds() const { + return solutionBounds.hasLower(); +} + +template +bool AbstractEquationSolver::hasSolutionUpperBounds() const { + return solutionBounds.hasUpper(); } template std::vector const& AbstractEquationSolver::getSolutionLowerBounds() const { - STORM_LOG_ASSERT(this->hasSolutionBounds(), "No bounds on the solution were computed."); - return solutionBounds->first; + STORM_LOG_ASSERT(this->hasSolutionLowerBounds(), "No lower bound on the solution was computed."); + return *solutionBounds.lower; } template std::vector const& AbstractEquationSolver::getSolutionUpperBounds() const { - STORM_LOG_ASSERT(this->hasSolutionBounds(), "No bounds on the solution were computed."); - return solutionBounds->second; + STORM_LOG_ASSERT(this->hasSolutionUpperBounds(), "No upper bound on the solution was computed."); + return *solutionBounds.upper; } template -void AbstractEquationSolver::setSolutionBounds(std::vector lower, std::vector upper) const { - STORM_LOG_ASSERT(lower.size() == upper.size(), "Bounds on the solution must have the same size."); - solutionBounds = std::make_pair(std::move(lower), std::move(upper)); +void AbstractEquationSolver::setSolutionBounds(SolutionBounds bounds) const { + STORM_LOG_ASSERT(!bounds.hasLower() || !bounds.hasUpper() || bounds.lower->size() == bounds.upper->size(), + "Bounds on the solution must have the same size."); + solutionBounds = std::move(bounds); } template void AbstractEquationSolver::clearSolutionBounds() const { - solutionBounds = std::nullopt; + solutionBounds.clear(); } template diff --git a/src/storm/solver/AbstractEquationSolver.h b/src/storm/solver/AbstractEquationSolver.h index 7f7e5a0a89..466ca10df4 100644 --- a/src/storm/solver/AbstractEquationSolver.h +++ b/src/storm/solver/AbstractEquationSolver.h @@ -184,15 +184,17 @@ class AbstractEquationSolver { void clearBounds(); /*! - * Retrieves whether the last call to this solver computed sound bounds on the solution. + * Retrieves whether the last call to this solver computed a sound lower resp. upper bound on the solution. * Only some algorithms, most notably interval iteration and optimistic value iteration, provide these, and - * even those only when they converged. + * they need not provide both: optimistic value iteration, for instance, only verifies its upper bound once + * it converges, while its lower bound is available in any case. */ - bool hasSolutionBounds() const; + bool hasSolutionLowerBounds() const; + bool hasSolutionUpperBounds() const; /*! * Retrieves sound bounds on the solution that the last call to this solver computed. - * @pre Such bounds were computed, see hasSolutionBounds(). + * @pre The respective bound was computed, see hasSolutionLowerBounds() resp. hasSolutionUpperBounds(). */ std::vector const& getSolutionLowerBounds() const; std::vector const& getSolutionUpperBounds() const; @@ -230,7 +232,7 @@ class AbstractEquationSolver { * Stores sound bounds on the solution that were obtained while solving. Note that solving is const, so * that this is as well. */ - void setSolutionBounds(std::vector lower, std::vector upper) const; + void setSolutionBounds(SolutionBounds bounds) const; /*! * Discards any bounds on the solution obtained by a previous call. This must happen whenever solving diff --git a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp index 8ba2fa8ff2..6a6402c250 100644 --- a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp +++ b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp @@ -599,8 +599,8 @@ bool IterativeMinMaxLinearEquationSolver::solveEquation storm::solver::SolutionBounds solutionBounds; auto status = oviHelper.OVI(x, b, numIterations, env.solver().minMax().getRelativeTerminationCriterion(), prec, dir, guessingFactor, lowerBound, upperBound, oviCallback, &solutionBounds); - if (solutionBounds) { - this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + if (solutionBounds.hasAny()) { + this->setSolutionBounds(std::move(solutionBounds)); } this->reportStatus(status, numIterations); @@ -813,8 +813,8 @@ bool IterativeMinMaxLinearEquationSolver::solveEquation storm::solver::SolutionBounds solutionBounds; auto status = iiHelper.II(x, b, numIterations, env.solver().minMax().getRelativeTerminationCriterion(), prec, lowerBoundsCallback, upperBoundsCallback, dir, iiCallback, optionalRelevantValues, &solutionBounds); - if (solutionBounds) { - this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + if (solutionBounds.hasAny()) { + this->setSolutionBounds(std::move(solutionBounds)); } this->reportStatus(status, numIterations); diff --git a/src/storm/solver/NativeLinearEquationSolver.cpp b/src/storm/solver/NativeLinearEquationSolver.cpp index 48bd585278..1831b88b32 100644 --- a/src/storm/solver/NativeLinearEquationSolver.cpp +++ b/src/storm/solver/NativeLinearEquationSolver.cpp @@ -444,8 +444,8 @@ bool NativeLinearEquationSolver::solveEquationsIntervalIteration(Envi storm::solver::SolutionBounds solutionBounds; auto status = iiHelper.II(x, b, numIterations, env.solver().native().getRelativeTerminationCriterion(), prec, lowerBoundsCallback, upperBoundsCallback, {}, iiCallback, optionalRelevantValues, &solutionBounds); - if (solutionBounds) { - this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + if (solutionBounds.hasAny()) { + this->setSolutionBounds(std::move(solutionBounds)); } this->reportStatus(status, numIterations); @@ -530,10 +530,10 @@ bool NativeLinearEquationSolver::solveEquationsOptimisticValueIterati } this->startMeasureProgress(); storm::solver::SolutionBounds solutionBounds; - auto status = oviHelper.OVI(x, b, env.solver().native().getRelativeTerminationCriterion(), prec, {}, guessingFactor, lowerBound, upperBound, oviCallback, - &solutionBounds); - if (solutionBounds) { - this->setSolutionBounds(std::move(solutionBounds->first), std::move(solutionBounds->second)); + auto status = oviHelper.OVI(x, b, numIterations, env.solver().native().getRelativeTerminationCriterion(), prec, {}, guessingFactor, lowerBound, upperBound, + oviCallback, &solutionBounds); + if (solutionBounds.hasAny()) { + this->setSolutionBounds(std::move(solutionBounds)); } this->reportStatus(status, numIterations); diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h index a4d53e651f..a964218fda 100644 --- a/src/storm/solver/SolutionBounds.h +++ b/src/storm/solver/SolutionBounds.h @@ -1,17 +1,41 @@ #pragma once #include -#include #include namespace storm::solver { /*! * Sound lower and upper bounds on a solution vector, as computed by e.g. interval iteration. - * The first component bounds the solution from below, the second one from above. Both have the same size as - * the solution. An unset value means that the algorithm in question did not provide any bounds. + * Both bounds have the same size as the solution. They are tracked independently because an algorithm may + * well establish only one of them: optimistic value iteration, for instance, keeps a sound lower bound in + * every iteration but only verifies its upper bound once it converges. An unset bound means that the + * algorithm in question did not provide that side. */ template -using SolutionBounds = std::optional, std::vector>>; +struct SolutionBounds { + std::optional> lower; + std::optional> upper; + + bool hasLower() const { + return lower.has_value(); + } + + bool hasUpper() const { + return upper.has_value(); + } + + /*! + * Retrieves whether at least one of the two bounds is known. + */ + bool hasAny() const { + return hasLower() || hasUpper(); + } + + void clear() { + lower = std::nullopt; + upper = std::nullopt; + } +}; } // namespace storm::solver diff --git a/src/storm/solver/helper/IntervalterationHelper.cpp b/src/storm/solver/helper/IntervalterationHelper.cpp index 864ad8477d..b51bdb0d9c 100644 --- a/src/storm/solver/helper/IntervalterationHelper.cpp +++ b/src/storm/solver/helper/IntervalterationHelper.cpp @@ -140,9 +140,10 @@ SolverStatus IntervalIterationHelper::II( } if (solutionBounds != nullptr) { // Hand out the enclosure before it is collapsed into the point estimate below. Interval iteration - // maintains xy.first below and xy.second above the solution in every iteration, so these are sound - // even if the iteration was aborted before converging. - *solutionBounds = std::make_pair(xy.first, xy.second); + // maintains xy.first below and xy.second above the solution in every iteration, so both sides are + // sound even if the iteration was aborted before converging. + solutionBounds->lower = xy.first; + solutionBounds->upper = xy.second; } auto two = storm::utility::convertNumber(2.0); // get the average of lower- and upper result diff --git a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp index 390ad1ed85..9622678733 100644 --- a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp +++ b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp @@ -281,15 +281,26 @@ SolverStatus OptimisticValueIterationHelper::OVI( doublePrec -= precision * 1e-6; // be slightly more precise to avoid a good chunk of floating point issues } auto status = OVI(vu, offsets, numIterations, relative, doublePrec, dir, guessValue ? *guessValue : doublePrec, lowerBound, upperBound, iterationCallback); - if (solutionBounds != nullptr && status == SolverStatus::Converged) { + bool const converged = status == SolverStatus::Converged; + if (solutionBounds != nullptr) { + // The operand was initialized below the solution and every iteration -- both the ones of the value + // iteration phase and the ones of the verification phase -- applies the (monotone) operator to it, so + // vu.first lies below the solution no matter why we stopped iterating. + solutionBounds->lower = vu.first; // Only a converged run has verified that vu.second lies above the solution. Until the verification // phase succeeds it is merely a guess, so handing it out as an upper bound would be unsound. - *solutionBounds = std::make_pair(vu.first, vu.second); + if (converged) { + solutionBounds->upper = vu.second; + } + } + if (converged) { + auto two = storm::utility::convertNumber(2.0); + // get the average of lower- and upper result + storm::utility::vector::applyPointwise( + vu.first, vu.second, vu.first, [&two](ValueType const& a, ValueType const& b) -> ValueType { return (a + b) / two; }); } - auto two = storm::utility::convertNumber(2.0); - // get the average of lower- and upper result - storm::utility::vector::applyPointwise( - vu.first, vu.second, vu.first, [&two](ValueType const& a, ValueType const& b) -> ValueType { return (a + b) / two; }); + // Otherwise vu.second holds an unverified guess (or, if we never got to guessing, uninitialized data), so + // averaging it into the result would be meaningless and could even push the result below vu.first. // Swap operand and aux vector back to original positions. vu.first.swap(operand); vu.second.swap(auxVector); From c6dfe5f90e0077931a23fea9158c3fde3b5347f3 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Mon, 24 Aug 2026 15:47:38 +0200 Subject: [PATCH 03/29] Adding the DTMC return type (cherry picked from commit 68f0d738e2b01dcbfc6b9d38d694f780cccec488) --- .../DTMCModelCheckingHelperReturnType.h | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h diff --git a/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h b/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h new file mode 100644 index 0000000000..566ee9a79e --- /dev/null +++ b/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "storm/solver/SolutionBounds.h" + +namespace storm { +namespace modelchecker { +namespace helper { + +/*! + * The outcome of a quantitative model checking query on a sparse DTMC. Unlike its MDP counterpart it carries no + * scheduler, as a DTMC has no nondeterminism to resolve. + */ +template +struct DTMCSparseModelCheckingHelperReturnType { + DTMCSparseModelCheckingHelperReturnType(DTMCSparseModelCheckingHelperReturnType const&) = delete; + DTMCSparseModelCheckingHelperReturnType(DTMCSparseModelCheckingHelperReturnType&&) = default; + DTMCSparseModelCheckingHelperReturnType& operator=(DTMCSparseModelCheckingHelperReturnType&&) = default; + + explicit DTMCSparseModelCheckingHelperReturnType(std::vector&& values) : values(std::move(values)) { + // Intentionally left empty. + } + + virtual ~DTMCSparseModelCheckingHelperReturnType() { + // Intentionally left empty. + } + + // The values computed for the states. + std::vector values; + + // Sound bounds on the values, if the algorithm that computed them provided any. + storm::solver::SolutionBounds solutionBounds; +}; + +} // namespace helper +} // namespace modelchecker +} // namespace storm From 26631f16f2b289f34321ce8d441c8c7d354f3ca3 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Fri, 28 Aug 2026 10:30:23 +0200 Subject: [PATCH 04/29] Remove map as vector type from CheckResults, replaced by bitvector of available states in vector. (cherry picked from commit 5a6f464f04102bc2daba3eceb025a326c368bda8) --- .../modelchecker/DFTModelChecker.cpp | 2 +- .../prctl/SparseDtmcPrctlModelChecker.cpp | 8 +- .../prctl/SparseMdpPrctlModelChecker.cpp | 9 +- .../prctl/helper/SparseDtmcPrctlHelper.cpp | 11 +- .../prctl/helper/SparseDtmcPrctlHelper.h | 7 +- .../prctl/helper/SparseMdpPrctlHelper.cpp | 7 +- .../prctl/helper/SparseMdpPrctlHelper.h | 9 +- .../SparseDtmcEliminationModelChecker.cpp | 16 +- .../ExplicitQualitativeCheckResult.cpp | 244 +++----- .../results/ExplicitQualitativeCheckResult.h | 90 ++- .../ExplicitQuantitativeCheckResult.cpp | 522 ++++++------------ .../results/ExplicitQuantitativeCheckResult.h | 147 +++-- 12 files changed, 460 insertions(+), 612 deletions(-) diff --git a/src/storm-dft/modelchecker/DFTModelChecker.cpp b/src/storm-dft/modelchecker/DFTModelChecker.cpp index 10851bdbd0..fb105b0f58 100644 --- a/src/storm-dft/modelchecker/DFTModelChecker.cpp +++ b/src/storm-dft/modelchecker/DFTModelChecker.cpp @@ -471,7 +471,7 @@ std::vector::ExtendedValueType> DFTModelChec if (result) { result->filter(storm::modelchecker::ExplicitQualitativeCheckResult(model->getInitialStates())); - results.push_back(result->asExplicitQuantitativeCheckResult().getValueMap().begin()->second); + results.push_back(result->asExplicitQuantitativeCheckResult().getValueVector().front()); } else { STORM_LOG_WARN("The property '" << *property << "' could not be checked with the current settings."); results.push_back(-storm::utility::one()); diff --git a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp index 1de00cdbed..f012044d6f 100644 --- a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp @@ -98,7 +98,7 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c auto formula = std::make_shared(checkTask.getFormula().asSharedPointer(), opInfo); auto numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeRewardBoundedValues( env, this->getModel(), formula); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); + return std::unique_ptr(new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } else { STORM_LOG_THROW(pathFormula.hasUpperBound(), storm::exceptions::InvalidPropertyException, "Formula needs to have (a single) upper step bound."); STORM_LOG_THROW(pathFormula.hasIntegerLowerBound(), storm::exceptions::InvalidPropertyException, "Formula lower step bound must be discrete/integral."); @@ -156,7 +156,7 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c this->getModel().getBackwardTransitions(), leftResult.getTruthValuesVector(), rightResult.getTruthValuesVector(), checkTask.isQualitativeSet(), checkTask.getHint()); auto result = std::make_unique>(std::move(ret.values)); - setBounds(*result, ret.solutionBounds); + result->setBounds(std::move(ret.solutionBounds)); return result; } @@ -173,7 +173,7 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), this->getModel().getBackwardTransitions(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet()); auto result = std::make_unique>(std::move(ret.values)); - setBounds(*result, ret.solutionBounds); + result->setBounds(std::move(ret.solutionBounds)); return result; } } @@ -238,7 +238,7 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c auto formula = std::make_shared(checkTask.getFormula().asSharedPointer(), checkTask.getRewardModel(), opInfo); auto numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeRewardBoundedValues( env, this->getModel(), formula); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); + return std::unique_ptr(new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } else { STORM_LOG_THROW(rewardPathFormula.hasIntegerBound(), storm::exceptions::InvalidPropertyException, "Formula needs to have a discrete time bound."); auto rewardModel = storm::utility::createFilteredRewardModel(this->getModel(), checkTask); diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 8d9d028c46..7879db0a88 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -134,7 +134,8 @@ std::unique_ptr SparseMdpPrctlModelChecker::com helper::rewardbounded::MultiDimensionalRewardUnfolding rewardUnfolding(this->getModel(), formula); auto numericResult = storm::modelchecker::helper::SparseMdpPrctlHelper::computeRewardBoundedValues( env, checkTask.getOptimizationDirection(), rewardUnfolding, this->getModel().getInitialStates()); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); + return std::unique_ptr( + new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } } else { STORM_LOG_THROW(pathFormula.hasUpperBound(), storm::exceptions::InvalidPropertyException, "Formula needs to have (a single) upper step bound."); @@ -182,7 +183,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com this->getModel().getBackwardTransitions(), leftResult.getTruthValuesVector(), rightResult.getTruthValuesVector(), checkTask.isQualitativeSet(), checkTask.isProduceSchedulersSet(), checkTask.getHint()); std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(ret.values))); - setBounds(result->asExplicitQuantitativeCheckResult(), ret.solutionBounds); + result->asExplicitQuantitativeCheckResult().setBounds(std::move(ret.solutionBounds)); if (checkTask.isProduceSchedulersSet() && ret.scheduler) { result->asExplicitQuantitativeCheckResult().setScheduler(std::move(ret.scheduler)); } @@ -201,7 +202,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com env, storm::solver::SolveGoal(this->getModel(), checkTask), this->getModel().getTransitionMatrix(), this->getModel().getBackwardTransitions(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet(), checkTask.isProduceSchedulersSet()); std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(ret.values))); - setBounds(result->asExplicitQuantitativeCheckResult(), ret.solutionBounds); + result->asExplicitQuantitativeCheckResult().setBounds(std::move(ret.solutionBounds)); if (checkTask.isProduceSchedulersSet() && ret.scheduler) { result->asExplicitQuantitativeCheckResult().setScheduler(std::move(ret.scheduler)); } @@ -316,7 +317,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com helper::rewardbounded::MultiDimensionalRewardUnfolding rewardUnfolding(this->getModel(), formula); auto numericResult = storm::modelchecker::helper::SparseMdpPrctlHelper::computeRewardBoundedValues( env, checkTask.getOptimizationDirection(), rewardUnfolding, this->getModel().getInitialStates()); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); + return std::unique_ptr(new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } else { STORM_LOG_THROW(rewardPathFormula.hasIntegerBound(), storm::exceptions::InvalidPropertyException, "Formula needs to have a discrete time bound."); auto rewardModel = storm::utility::createFilteredRewardModel(this->getModel(), checkTask); diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp index a89275002c..0c9df446e8 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp @@ -31,15 +31,15 @@ namespace modelchecker { namespace helper { template<> -std::map SparseDtmcPrctlHelper::computeRewardBoundedValues( +std::vector SparseDtmcPrctlHelper::computeRewardBoundedValues( Environment const& /*env*/, storm::models::sparse::Dtmc const& /*model*/, std::shared_ptr /*rewardBoundedFormula*/) { STORM_LOG_THROW(false, storm::exceptions::NotSupportedException, "The specified property is not supported by this value type."); - return std::map(); + return std::vector(); } template -std::map SparseDtmcPrctlHelper::computeRewardBoundedValues( +std::vector SparseDtmcPrctlHelper::computeRewardBoundedValues( Environment const& env, storm::models::sparse::Dtmc const& model, std::shared_ptr rewardBoundedFormula) { if constexpr (storm::IsIntervalType) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "We do not support computing reward bounded values with interval models."); @@ -100,9 +100,10 @@ std::map SparseDtmcPrctlHelper } } - std::map result; + std::vector result; + result.reserve(model.getInitialStates().getNumberOfSetBits()); for (auto initState : model.getInitialStates()) { - result[initState] = rewardUnfolding.getInitialStateResult(initEpoch, initState); + result.push_back(rewardUnfolding.getInitialStateResult(initEpoch, initState)); } swAll.stop(); diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h index 3c5dfabe31..7256145c3d 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h @@ -30,8 +30,11 @@ class SparseDtmcPrctlHelper { public: using ExtendedSolutionType = storm::utility::ExtendedValueType; - static std::map computeRewardBoundedValues( - Environment const& env, storm::models::sparse::Dtmc const& model, std::shared_ptr rewardBoundedFormula); + /*! + * @return One value per initial state of the given model, in the order of those states. + */ + static std::vector computeRewardBoundedValues(Environment const& env, storm::models::sparse::Dtmc const& model, + std::shared_ptr rewardBoundedFormula); static std::vector computeNextProbabilities(Environment const& env, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& nextStates); diff --git a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp index fec2e6c41f..96579bbd9a 100644 --- a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp @@ -37,7 +37,7 @@ namespace modelchecker { namespace helper { template -std::map SparseMdpPrctlHelper::computeRewardBoundedValues( +std::vector SparseMdpPrctlHelper::computeRewardBoundedValues( Environment const& env, OptimizationDirection dir, rewardbounded::MultiDimensionalRewardUnfolding& rewardUnfolding, storm::storage::BitVector const& initialStates) { if constexpr (storm::IsIntervalType) { @@ -93,9 +93,10 @@ std::map SparseMdpPrctlHelper< } } - std::map result; + std::vector result; + result.reserve(initialStates.getNumberOfSetBits()); for (uint64_t initState : initialStates) { - result[initState] = rewardUnfolding.getInitialStateResult(initEpoch, initState); + result.push_back(rewardUnfolding.getInitialStateResult(initEpoch, initState)); } swAll.stop(); diff --git a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h index b61ab44343..3eff752ed1 100644 --- a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h +++ b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h @@ -38,9 +38,12 @@ class SparseMdpPrctlHelper { using ExtendedSolutionType = storm::utility::ExtendedValueType; using ExtendedReturnType = MDPSparseModelCheckingHelperReturnType; - static std::map computeRewardBoundedValues( - Environment const& env, OptimizationDirection dir, rewardbounded::MultiDimensionalRewardUnfolding& rewardUnfolding, - storm::storage::BitVector const& initialStates); + /*! + * @return One value per given initial state, in the order of those states. + */ + static std::vector computeRewardBoundedValues(Environment const& env, OptimizationDirection dir, + rewardbounded::MultiDimensionalRewardUnfolding& rewardUnfolding, + storm::storage::BitVector const& initialStates); static std::vector computeNextProbabilities(Environment const& env, OptimizationDirection dir, UncertaintyResolutionMode uncertaintyResolutionMode, diff --git a/src/storm/modelchecker/reachability/SparseDtmcEliminationModelChecker.cpp b/src/storm/modelchecker/reachability/SparseDtmcEliminationModelChecker.cpp index 579634ed53..c3b730ee9c 100644 --- a/src/storm/modelchecker/reachability/SparseDtmcEliminationModelChecker.cpp +++ b/src/storm/modelchecker/reachability/SparseDtmcEliminationModelChecker.cpp @@ -512,11 +512,9 @@ std::unique_ptr SparseDtmcEliminationModelChecker> checkResult = std::make_unique>(); - for (uint64_t state : ~maybeStates | initialStates) { - (*checkResult)[state] = result[state]; - } - return std::move(checkResult); // move() required by, e.g., clang 3.8 + storm::storage::BitVector relevantStates = ~maybeStates | initialStates; + std::vector relevantValues = storm::utility::vector::filterVector(result, relevantStates); + return std::make_unique>(std::move(relevantStates), std::move(relevantValues)); } return std::make_unique>(result); } @@ -621,11 +619,9 @@ std::unique_ptr SparseDtmcEliminationModelChecker> checkResult = std::make_unique>(); - for (uint64_t state : ~maybeStates | initialStates) { - (*checkResult)[state] = result[state]; - } - return std::move(checkResult); // move() required by, e.g., clang 3.8 + storm::storage::BitVector relevantStates = ~maybeStates | initialStates; + auto relevantValues = storm::utility::vector::filterVector(result, relevantStates); + return std::make_unique>(std::move(relevantStates), std::move(relevantValues)); } return std::make_unique>(result); } diff --git a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp index 959c31ee3e..43941d2392 100644 --- a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp @@ -4,6 +4,8 @@ #include "storm/modelchecker/results/ExplicitQualitativeCheckResult.h" +#include + #include "storm/adapters/JsonAdapter.h" #include "storm/exceptions/InvalidOperationException.h" #include "storm/utility/macros.h" @@ -12,52 +14,35 @@ namespace storm { namespace modelchecker { template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult() : truthValues(map_type()) { - // Intentionally left empty. -} - -template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(map_type const& map) : truthValues(map) { - // Intentionally left empty. -} - -template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(map_type&& map) : truthValues(map) { - // Intentionally left empty. -} - -template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(storm::storage::sparse::state_type state, bool value) : truthValues(map_type()) { - boost::get(truthValues)[state] = value; -} - -template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(storm::storage::BitVector const& truthValues) : truthValues(truthValues) { - // Intentionally left empty. +ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(storm::storage::sparse::state_type state, bool value) + : states(storm::storage::BitVector(state + 1)), truthValues(1, value) { + states->set(state); } template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(storm::storage::BitVector&& truthValues) : truthValues(std::move(truthValues)) { +ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(vector_type const& truthValues, + std::optional>> scheduler) + : truthValues(truthValues), scheduler(scheduler) { // Intentionally left empty. } template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(boost::variant const& truthValues, +ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(vector_type&& truthValues, std::optional>> scheduler) - : truthValues(truthValues), scheduler(scheduler) { + : truthValues(std::move(truthValues)), scheduler(scheduler) { // Intentionally left empty. } template -ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(boost::variant&& truthValues, +ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(storm::storage::BitVector states, vector_type&& truthValues, std::optional>> scheduler) - : truthValues(std::move(truthValues)), scheduler(scheduler) { - // Intentionally left empty. + : states(std::move(states)), truthValues(std::move(truthValues)), scheduler(scheduler) { + STORM_LOG_ASSERT(this->states->getNumberOfSetBits() == this->truthValues.size(), "Expected one truth value per selected state."); } template std::unique_ptr ExplicitQualitativeCheckResult::clone() const { - return std::make_unique>(this->truthValues); + return std::make_unique>(*this); } template @@ -65,34 +50,13 @@ void ExplicitQualitativeCheckResult::performLogicalOperation(Explicit bool logicalAnd) { STORM_LOG_THROW(second.isExplicitQualitativeCheckResult(), storm::exceptions::InvalidOperationException, "Cannot perform logical 'and' on check results of incompatible type."); - STORM_LOG_THROW(first.isResultForAllStates() == second.isResultForAllStates(), storm::exceptions::InvalidOperationException, - "Cannot perform logical 'and' on check results of incompatible type."); ExplicitQualitativeCheckResult const& secondCheckResult = static_cast const&>(second); - if (first.isResultForAllStates()) { - if (logicalAnd) { - boost::get(first.truthValues) &= boost::get(secondCheckResult.truthValues); - } else { - boost::get(first.truthValues) |= boost::get(secondCheckResult.truthValues); - } + STORM_LOG_THROW(first.states == secondCheckResult.states && first.truthValues.size() == secondCheckResult.truthValues.size(), + storm::exceptions::InvalidOperationException, "Cannot perform logical 'and' on check results of incompatible type."); + if (logicalAnd) { + first.truthValues &= secondCheckResult.truthValues; } else { - std::function function = logicalAnd ? std::function([](bool a, bool b) { return a && b; }) - : std::function([](bool a, bool b) { return a || b; }); - - map_type& map1 = boost::get(first.truthValues); - map_type const& map2 = boost::get(secondCheckResult.truthValues); - for (auto& element1 : map1) { - auto const& keyValuePair = map2.find(element1.first); - STORM_LOG_THROW(keyValuePair != map2.end(), storm::exceptions::InvalidOperationException, - "Cannot perform logical 'and' on check results of incompatible type."); - element1.second = function(element1.second, keyValuePair->second); - } - - // Double-check that there are no entries in map2 that the current result does not have. - for (auto const& element2 : map2) { - auto const& keyValuePair = map1.find(element2.first); - STORM_LOG_THROW(keyValuePair != map1.end(), storm::exceptions::InvalidOperationException, - "Cannot perform logical 'and' on check results of incompatible type."); - } + first.truthValues |= secondCheckResult.truthValues; } } @@ -110,78 +74,53 @@ QualitativeCheckResult& ExplicitQualitativeCheckResult::operator|=(Qu template bool ExplicitQualitativeCheckResult::existsTrue() const { - if (this->isResultForAllStates()) { - return !boost::get(truthValues).empty(); - } else { - for (auto& element : boost::get(truthValues)) { - if (element.second) { - return true; - } - } - return false; - } + return !truthValues.empty(); } template bool ExplicitQualitativeCheckResult::forallTrue() const { - if (this->isResultForAllStates()) { - return boost::get(truthValues).full(); - } else { - for (auto& element : boost::get(truthValues)) { - if (!element.second) { - return false; - } - } - return true; - } + return truthValues.full(); } template uint64_t ExplicitQualitativeCheckResult::count() const { - if (this->isResultForAllStates()) { - return boost::get(truthValues).getNumberOfSetBits(); - } else { - uint64_t result = 0; - for (auto& element : boost::get(truthValues)) { - if (element.second) { - ++result; - } - } - return result; + return truthValues.getNumberOfSetBits(); +} + +template +bool ExplicitQualitativeCheckResult::hasValueForState(storm::storage::sparse::state_type state) const { + if (states) { + return state < states->size() && states->get(state); } + return state < truthValues.size(); +} + +template +uint64_t ExplicitQualitativeCheckResult::getOffset(storm::storage::sparse::state_type state) const { + STORM_LOG_THROW(this->hasValueForState(state), storm::exceptions::InvalidOperationException, "Unknown key '" << state << "'."); + return states ? states->getNumberOfSetBitsBeforeIndex(state) : state; } template bool ExplicitQualitativeCheckResult::operator[](storm::storage::sparse::state_type state) const { - if (this->isResultForAllStates()) { - return boost::get(truthValues).get(state); - } else { - map_type const& map = boost::get(truthValues); - auto const& keyValuePair = map.find(state); - STORM_LOG_THROW(keyValuePair != map.end(), storm::exceptions::InvalidOperationException, "Unknown key '" << state << "'."); - return keyValuePair->second; - } + return truthValues.get(this->getOffset(state)); } template -typename ExplicitQualitativeCheckResult::vector_type const& ExplicitQualitativeCheckResult::getTruthValuesVector() const { - return boost::get(truthValues); +storm::storage::BitVector const& ExplicitQualitativeCheckResult::getStates() const { + STORM_LOG_THROW(!this->isResultForAllStates(), storm::exceptions::InvalidOperationException, + "Unable to retrieve the states of a result that is for all states."); + return *states; } template -typename ExplicitQualitativeCheckResult::map_type const& ExplicitQualitativeCheckResult::getTruthValuesMap() const { - return boost::get(truthValues); +typename ExplicitQualitativeCheckResult::vector_type const& ExplicitQualitativeCheckResult::getTruthValuesVector() const { + return truthValues; } template void ExplicitQualitativeCheckResult::complement() { - if (this->isResultForAllStates()) { - boost::get(truthValues).complement(); - } else { - for (auto& element : boost::get(truthValues)) { - element.second = !element.second; - } - } + truthValues.complement(); } template @@ -191,7 +130,7 @@ bool ExplicitQualitativeCheckResult::isExplicit() const { template bool ExplicitQualitativeCheckResult::isResultForAllStates() const { - return truthValues.which() == 0; + return !states.has_value(); } template @@ -201,44 +140,16 @@ bool ExplicitQualitativeCheckResult::isExplicitQualitativeCheckResult template std::ostream& ExplicitQualitativeCheckResult::writeToStream(std::ostream& out) const { - if (this->isResultForAllStates()) { - vector_type const& vector = boost::get(truthValues); - bool allTrue = vector.full(); - bool allFalse = !allTrue && vector.empty(); - if (allTrue) { - out << "{true}"; - } else if (allFalse) { - out << "{false}"; - } else { - out << "{true, false}"; - } + if (!this->isResultForAllStates() && truthValues.size() == 1) { + std::ios::fmtflags oldflags(out.flags()); + out << std::boolalpha << truthValues.get(0); + out.flags(oldflags); + } else if (truthValues.full()) { + out << "{true}"; + } else if (truthValues.empty()) { + out << "{false}"; } else { - std::ios::fmtflags oldflags(std::cout.flags()); - out << std::boolalpha; - - map_type const& map = boost::get(truthValues); - if (map.size() == 1) { - out << map.begin()->second; - } else { - bool allTrue = true; - bool allFalse = true; - for (auto const& entry : map) { - if (entry.second) { - allFalse = false; - } else { - allTrue = false; - } - } - if (allTrue) { - out << "{true}"; - } else if (allFalse) { - out << "{false}"; - } else { - out << "{true, false}"; - } - } - - std::cout.flags(oldflags); + out << "{true, false}"; } return out; } @@ -251,27 +162,20 @@ void ExplicitQualitativeCheckResult::filter(QualitativeCheckResult co ExplicitQualitativeCheckResult const& explicitFilter = filter.template asExplicitQualitativeCheckResult(); vector_type const& filterTruthValues = explicitFilter.getTruthValuesVector(); - if (this->isResultForAllStates()) { - map_type newMap; - for (auto element : filterTruthValues) { - newMap.emplace(element, this->getTruthValuesVector().get(element)); - } - this->truthValues = newMap; - } else { - map_type const& map = boost::get(truthValues); - - map_type newMap; - for (auto const& element : map) { - if (filterTruthValues.get(element.first)) { - newMap.insert(element); - } - } - - STORM_LOG_THROW(newMap.size() == filterTruthValues.getNumberOfSetBits(), storm::exceptions::InvalidOperationException, - "The check result fails to contain some results referred to by the filter."); - - this->truthValues = newMap; - } + // Line the filter up with the states this result has truth values for. The two need not span the same range + // of states, e.g. if this result holds a truth value for a single state only. + uint64_t const numStates = std::max(filterTruthValues.size(), states ? states->size() : truthValues.size()); + storm::storage::BitVector available = states ? *states : storm::storage::BitVector(truthValues.size(), true); + available.resize(numStates); + storm::storage::BitVector selected(filterTruthValues); + selected.resize(numStates); + STORM_LOG_THROW(selected.isSubsetOf(available), storm::exceptions::InvalidOperationException, + "The check result fails to contain some results referred to by the filter."); + + // Keep the truth values of the selected states. Note that the result is a result for the selected states + // even if those happen to be all of them. + truthValues = truthValues % (selected % available); + states = filterTruthValues; } template @@ -319,17 +223,9 @@ template storm::json ExplicitQualitativeCheckResult::toJson(std::optional const& stateValuations, std::optional const& stateLabels) const { storm::json result; - if (this->isResultForAllStates()) { - vector_type const& valuesAsVector = boost::get(truthValues); - for (uint64_t state = 0; state < valuesAsVector.size(); ++state) { - insertJsonEntry(result, state, valuesAsVector.get(state), stateValuations, stateLabels); - } - } else { - map_type const& valuesAsMap = boost::get(truthValues); - for (auto const& stateValue : valuesAsMap) { - insertJsonEntry(result, stateValue.first, stateValue.second, stateValuations, stateLabels); - } - } + this->forEachState([&](storm::storage::sparse::state_type state, uint64_t offset) { + insertJsonEntry(result, state, truthValues.get(offset), stateValuations, stateLabels); + }); return result; } diff --git a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h index 503e227c61..76f67720c8 100644 --- a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include #include "storm/adapters/JsonForward.h" @@ -16,24 +14,38 @@ namespace storm { namespace modelchecker { +/*! + * A qualitative check result over the states of a sparse model. + * + * The result either covers every state of the model or just a subset of them, e.g. after filtering it to the + * initial states. In the latter case the states in question are recorded in a bit vector and the truth values are + * stored compressed, i.e. the i-th truth value belongs to the i-th state selected by that bit vector. + */ template class ExplicitQualitativeCheckResult : public QualitativeCheckResult { public: typedef storm::storage::BitVector vector_type; - typedef std::map map_type; - ExplicitQualitativeCheckResult(); - virtual ~ExplicitQualitativeCheckResult() = default; - ExplicitQualitativeCheckResult(map_type const& map); - ExplicitQualitativeCheckResult(map_type&& map); + /*! + * Creates a result that holds the given truth value for the given state only. + */ ExplicitQualitativeCheckResult(storm::storage::sparse::state_type state, bool value); - ExplicitQualitativeCheckResult(vector_type const& truthValues); - ExplicitQualitativeCheckResult(vector_type&& truthValues); - ExplicitQualitativeCheckResult(boost::variant const& truthValues, - std::optional>> scheduler = {}); - ExplicitQualitativeCheckResult(boost::variant&& truthValues, + + /*! + * Creates a result for all states of a model with as many states as the given bit vector has bits. + */ + ExplicitQualitativeCheckResult(vector_type const& truthValues, std::optional>> scheduler = {}); + ExplicitQualitativeCheckResult(vector_type&& truthValues, std::optional>> scheduler = {}); + + /*! + * Creates a result for the given states only. + * @param states The states the result is for. + * @param truthValues One truth value per state selected by @p states, in the order of the selected states. + */ + ExplicitQualitativeCheckResult(storm::storage::BitVector states, vector_type&& truthValues, std::optional>> scheduler = {}); + virtual ~ExplicitQualitativeCheckResult() = default; ExplicitQualitativeCheckResult(ExplicitQualitativeCheckResult const& other) = default; ExplicitQualitativeCheckResult& operator=(ExplicitQualitativeCheckResult const& other) = default; ExplicitQualitativeCheckResult(ExplicitQualitativeCheckResult&& other) = default; @@ -41,7 +53,11 @@ class ExplicitQualitativeCheckResult : public QualitativeCheckResult { virtual std::unique_ptr clone() const override; - bool operator[](storm::storage::sparse::state_type index) const; + /*! + * Retrieves the truth value of the given state. + * @pre The result holds a truth value for that state. + */ + bool operator[](storm::storage::sparse::state_type state) const; virtual bool isExplicit() const override; virtual bool isResultForAllStates() const override; @@ -52,8 +68,22 @@ class ExplicitQualitativeCheckResult : public QualitativeCheckResult { virtual QualitativeCheckResult& operator|=(QualitativeCheckResult const& other) override; virtual void complement() override; + /*! + * Retrieves whether the result holds a truth value for the given state. + */ + bool hasValueForState(storm::storage::sparse::state_type state) const; + + /*! + * Retrieves the states this result holds truth values for. + * @pre This is not a result for all states. + */ + storm::storage::BitVector const& getStates() const; + + /*! + * Retrieves the truth values, one per state this result is for. If this is not a result for all states, the + * i-th truth value belongs to the i-th state selected by getStates(). + */ vector_type const& getTruthValuesVector() const; - map_type const& getTruthValuesMap() const; virtual bool existsTrue() const override; virtual bool forallTrue() const override; @@ -79,8 +109,36 @@ class ExplicitQualitativeCheckResult : public QualitativeCheckResult { static void performLogicalOperation(ExplicitQualitativeCheckResult& first, QualitativeCheckResult const& second, bool logicalAnd); - // The values of the quantitative check result. - boost::variant truthValues; + /*! + * Retrieves the index at which the truth value of the given state is stored. + * @pre The result holds a truth value for that state. + */ + uint64_t getOffset(storm::storage::sparse::state_type state) const; + + /*! + * Invokes the given function with the state and the offset of its truth value, for every state this result is + * for. + */ + template + void forEachState(Function const& f) const { + if (states) { + uint64_t offset = 0; + for (auto const& state : *states) { + f(state, offset); + ++offset; + } + } else { + for (uint64_t state = 0; state < truthValues.size(); ++state) { + f(state, state); + } + } + } + + // The states this result holds truth values for, or nothing at all if it is a result for all states. + std::optional states; + + // The truth values of the qualitative check result, one per state this result is for. + vector_type truthValues; // An optional scheduler that accompanies the values. std::optional>> scheduler; diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 02bbeae4bb..76edf2905b 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -2,13 +2,13 @@ #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" +#include + #include "storm/adapters/JsonAdapter.h" #include "storm/adapters/RationalFunctionAdapter.h" -#include "storm/exceptions/InvalidAccessException.h" #include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/NotSupportedException.h" #include "storm/modelchecker/results/ExplicitQualitativeCheckResult.h" -#include "storm/storage/BitVector.h" #include "storm/utility/constants.h" #include "storm/utility/macros.h" #include "storm/utility/vector.h" @@ -16,93 +16,38 @@ namespace storm { namespace modelchecker { -namespace detail { -/*! - * Restricts the given values to the states selected by the filter. Note that the result is always a map, even - * if the given values are a vector. - */ -template -boost::variant, std::map> filterValues( - boost::variant, std::map> const& values, - storm::storage::BitVector const& filterTruthValues) { - typedef std::vector vector_type; - typedef std::map map_type; - - map_type newMap; - if (values.which() == 0) { - vector_type const& valuesAsVector = boost::get(values); - for (auto element : filterTruthValues) { - STORM_LOG_THROW(element < valuesAsVector.size(), storm::exceptions::InvalidAccessException, "Invalid index in results."); - newMap.emplace(element, valuesAsVector[element]); - } - } else { - for (auto const& element : boost::get(values)) { - if (filterTruthValues.get(element.first)) { - newMap.insert(element); - } - } - STORM_LOG_THROW(newMap.size() == filterTruthValues.getNumberOfSetBits(), storm::exceptions::InvalidOperationException, - "The check result fails to contain some results referred to by the filter."); - } - return newMap; -} - -/*! - * Replaces every value by one minus that value. - */ template -void complementValues(boost::variant, std::map>& values) { - typedef std::vector vector_type; - typedef std::map map_type; - - if (values.which() == 0) { - for (auto& element : boost::get(values)) { - element = storm::utility::one() - element; - } - } else { - for (auto& element : boost::get(values)) { - element.second = storm::utility::one() - element.second; - } - } -} -} // namespace detail - -template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult() : values(map_type()) { - // Intentionally left empty. -} - -template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(map_type const& values) : values(values) { - // Intentionally left empty. +ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::sparse::state_type const& state, ExtendedValueType const& value) + : states(storm::storage::BitVector(state + 1)), values({value}) { + states->set(state); } template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(map_type&& values) : values(std::move(values)) { +ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(vector_type const& values, + std::optional>> scheduler) + : values(values), scheduler(scheduler) { // Intentionally left empty. } template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::sparse::state_type const& state, ExtendedValueType const& value) - : values(map_type()) { - boost::get(values).emplace(state, value); -} - -template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(vector_type const& values) : values(values) { +ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(vector_type&& values, + std::optional>> scheduler) + : values(std::move(values)), scheduler(scheduler) { // Intentionally left empty. } template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(vector_type&& values) : values(std::move(values)) { - // Intentionally left empty. +ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::BitVector states, vector_type&& values, + std::optional>> scheduler) + : states(std::move(states)), values(std::move(values)), scheduler(scheduler) { + STORM_LOG_ASSERT(this->states->getNumberOfSetBits() == this->values.size(), "Expected one value per selected state."); } template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(std::vector const& values) requires(!std::is_same_v, ValueType>) - : values(map_type()) { - this->values = storm::utility::fromSentinel(std::vector(values)); + : values(storm::utility::fromSentinel(std::vector(values))) { + // Intentionally left empty. } template @@ -113,148 +58,153 @@ ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(std: } template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(std::map const& values) +ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::BitVector states, std::vector&& values) requires(!std::is_same_v, ValueType>) - : values(map_type()) { - map_type& widened = boost::get(this->values); - for (auto const& stateValue : values) { - widened.emplace(stateValue.first, storm::utility::fromSentinel(stateValue.second)); - } + : states(std::move(states)), values(storm::utility::fromSentinel(std::move(values))) { + STORM_LOG_ASSERT(this->states->getNumberOfSetBits() == this->values.size(), "Expected one value per selected state."); } template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(boost::variant const& values, - std::optional>> scheduler) - : values(values), scheduler(scheduler) { - // Intentionally left empty. +ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(ExplicitQualitativeCheckResult const& other) { + auto const toValue = [](bool truthValue) { return truthValue ? storm::utility::one() : storm::utility::zero(); }; + + storm::storage::BitVector const& truthValues = other.getTruthValuesVector(); + values.reserve(truthValues.size()); + for (std::size_t i = 0, n = truthValues.size(); i < n; i++) { + values.push_back(toValue(truthValues.get(i))); + } + if (!other.isResultForAllStates()) { + states = other.getStates(); + } } template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(boost::variant&& values, - std::optional>> scheduler) - : values(std::move(values)), scheduler(scheduler) { - // Intentionally left empty. +std::unique_ptr ExplicitQuantitativeCheckResult::clone() const { + return std::make_unique>(*this); } template -ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(ExplicitQualitativeCheckResult const& other) { - if (other.isResultForAllStates()) { - storm::storage::BitVector const& bvValues = other.getTruthValuesVector(); - - vector_type newVector; - newVector.reserve(bvValues.size()); - for (std::size_t i = 0, n = bvValues.size(); i < n; i++) { - newVector.push_back(bvValues.get(i) ? storm::utility::one() : storm::utility::zero()); - } - - values = newVector; - } else { - typename ExplicitQualitativeCheckResult::map_type const& bitMap = other.getTruthValuesMap(); - - map_type newMap; - for (auto const& e : bitMap) { - newMap[e.first] = e.second ? storm::utility::one() : storm::utility::zero(); - } - - values = newMap; +bool ExplicitQuantitativeCheckResult::hasValueForState(storm::storage::sparse::state_type state) const { + if (states) { + return state < states->size() && states->get(state); } + return state < values.size(); } template -std::unique_ptr ExplicitQuantitativeCheckResult::clone() const { - auto result = std::make_unique>(this->values, this->scheduler); - result->lowerBounds = this->lowerBounds; - result->upperBounds = this->upperBounds; - return result; +uint64_t ExplicitQuantitativeCheckResult::getOffset(storm::storage::sparse::state_type state) const { + STORM_LOG_THROW(this->hasValueForState(state), storm::exceptions::InvalidOperationException, "Unknown key '" << state << "'."); + return states ? states->getNumberOfSetBitsBeforeIndex(state) : state; +} + +template +storm::storage::BitVector const& ExplicitQuantitativeCheckResult::getStates() const { + STORM_LOG_THROW(!this->isResultForAllStates(), storm::exceptions::InvalidOperationException, + "Unable to retrieve the states of a result that is for all states."); + return *states; } template typename ExplicitQuantitativeCheckResult::vector_type const& ExplicitQuantitativeCheckResult::getValueVector() const { - return boost::get(values); + return values; } template typename ExplicitQuantitativeCheckResult::vector_type& ExplicitQuantitativeCheckResult::getValueVector() { - return boost::get(values); + return values; } template -typename ExplicitQuantitativeCheckResult::map_type const& ExplicitQuantitativeCheckResult::getValueMap() const { - return boost::get(values); +std::vector ExplicitQuantitativeCheckResult::getFiniteValueVector() const { + return storm::utility::narrowFinite(values); } template -std::vector ExplicitQuantitativeCheckResult::getFiniteValueVector() const { - return storm::utility::narrowFinite(boost::get(values)); +std::vector ExplicitQuantitativeCheckResult::getSentinelValueVector() const { + if constexpr (!std::is_same_v) { + std::vector result; + result.reserve(values.size()); + for (auto const& value : values) { + result.push_back(storm::utility::toSentinel(value)); + } + return result; + } else { + return values; + } } template bool ExplicitQuantitativeCheckResult::hasLowerBounds() const { - return lowerBounds.has_value(); + return bounds.hasLower(); } template bool ExplicitQuantitativeCheckResult::hasUpperBounds() const { - return upperBounds.has_value(); + return bounds.hasUpper(); } template typename ExplicitQuantitativeCheckResult::vector_type const& ExplicitQuantitativeCheckResult::getLowerBoundVector() const { STORM_LOG_THROW(this->hasLowerBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown lower bounds."); - return boost::get(*lowerBounds); + return *bounds.lower; } template typename ExplicitQuantitativeCheckResult::vector_type const& ExplicitQuantitativeCheckResult::getUpperBoundVector() const { STORM_LOG_THROW(this->hasUpperBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown upper bounds."); - return boost::get(*upperBounds); + return *bounds.upper; } template -typename ExplicitQuantitativeCheckResult::map_type const& ExplicitQuantitativeCheckResult::getLowerBoundMap() const { - STORM_LOG_THROW(this->hasLowerBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown lower bounds."); - return boost::get(*lowerBounds); +storm::solver::SolutionBounds::ExtendedValueType> const& +ExplicitQuantitativeCheckResult::getSolutionBounds() const { + return bounds; } template -typename ExplicitQuantitativeCheckResult::map_type const& ExplicitQuantitativeCheckResult::getUpperBoundMap() const { - STORM_LOG_THROW(this->hasUpperBounds(), storm::exceptions::InvalidOperationException, "Unable to retrieve unknown upper bounds."); - return boost::get(*upperBounds); +void ExplicitQuantitativeCheckResult::assertBoundsShape([[maybe_unused]] vector_type const& bounds) const { + STORM_LOG_ASSERT(bounds.size() == values.size(), "Bounds must have the same size as the values."); } template -void ExplicitQuantitativeCheckResult::assertBoundsShape([[maybe_unused]] boost::variant const& bounds) const { - STORM_LOG_ASSERT(bounds.which() == values.which(), "Bounds must have the same shape as the values."); - if (this->isResultForAllStates()) { - STORM_LOG_ASSERT(boost::get(bounds).size() == boost::get(values).size(), "Bounds must have the same size as the values."); - } else { - STORM_LOG_ASSERT(boost::get(bounds).size() == boost::get(values).size(), "Bounds must have the same size as the values."); - } +void ExplicitQuantitativeCheckResult::setLowerBounds(vector_type lowerBounds) { + this->assertBoundsShape(lowerBounds); + this->bounds.lower = std::move(lowerBounds); } template -void ExplicitQuantitativeCheckResult::setLowerBounds(boost::variant lowerBounds) { - this->assertBoundsShape(lowerBounds); - this->lowerBounds = std::move(lowerBounds); +void ExplicitQuantitativeCheckResult::setUpperBounds(vector_type upperBounds) { + this->assertBoundsShape(upperBounds); + this->bounds.upper = std::move(upperBounds); } template -void ExplicitQuantitativeCheckResult::setUpperBounds(boost::variant upperBounds) { - this->assertBoundsShape(upperBounds); - this->upperBounds = std::move(upperBounds); +void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) { + // Each of the two sides is transferred only if the algorithm in question established it; the other side is + // left as it is. + if (bounds.hasLower()) { + this->setLowerBounds(std::move(*bounds.lower)); + } + if (bounds.hasUpper()) { + this->setUpperBounds(std::move(*bounds.upper)); + } } template -void ExplicitQuantitativeCheckResult::setBounds(boost::variant lowerBounds, - boost::variant upperBounds) { - this->setLowerBounds(std::move(lowerBounds)); - this->setUpperBounds(std::move(upperBounds)); +void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) + requires(!std::is_same_v, ValueType>) +{ + if (bounds.hasLower()) { + this->setLowerBounds(storm::utility::fromSentinel(std::move(*bounds.lower))); + } + if (bounds.hasUpper()) { + this->setUpperBounds(storm::utility::fromSentinel(std::move(*bounds.upper))); + } } template void ExplicitQuantitativeCheckResult::clearBounds() { - lowerBounds = std::nullopt; - upperBounds = std::nullopt; + bounds.clear(); } template @@ -266,62 +216,47 @@ void ExplicitQuantitativeCheckResult::filter(QualitativeCheckResult c ExplicitQualitativeCheckResult const& explicitFilter = filter.template asExplicitQualitativeCheckResult(); typename ExplicitQualitativeCheckResult::vector_type const& filterTruthValues = explicitFilter.getTruthValuesVector(); + // Line the filter up with the states this result has values for. The two need not span the same range of + // states, e.g. if this result holds a value for a single state only. + uint64_t const numStates = std::max(filterTruthValues.size(), states ? states->size() : values.size()); + storm::storage::BitVector available = states ? *states : storm::storage::BitVector(values.size(), true); + available.resize(numStates); + storm::storage::BitVector selected(filterTruthValues); + selected.resize(numStates); + STORM_LOG_THROW(selected.isSubsetOf(available), storm::exceptions::InvalidOperationException, + "The check result fails to contain some results referred to by the filter."); + + // The entries that survive, in the index space the values are stored in. Note that the result is a result + // for the selected states even if those happen to be all of them. + storm::storage::BitVector const keep = selected % available; + if (this->hasLowerBounds()) { - this->lowerBounds = detail::filterValues(*this->lowerBounds, filterTruthValues); + bounds.lower = storm::utility::vector::filterVector(*bounds.lower, keep); } if (this->hasUpperBounds()) { - this->upperBounds = detail::filterValues(*this->upperBounds, filterTruthValues); - } - this->values = detail::filterValues(this->values, filterTruthValues); -} - -template -std::vector ExplicitQuantitativeCheckResult::getSentinelValueVector() const { - vector_type const& valuesAsVector = boost::get(values); - if constexpr (!std::is_same_v) { - std::vector result; - result.reserve(valuesAsVector.size()); - for (auto const& value : valuesAsVector) { - result.push_back(storm::utility::toSentinel(value)); - } - return result; - } else { - return valuesAsVector; + bounds.upper = storm::utility::vector::filterVector(*bounds.upper, keep); } + values = storm::utility::vector::filterVector(values, keep); + states = filterTruthValues; } template typename ExplicitQuantitativeCheckResult::ExtendedValueType ExplicitQuantitativeCheckResult::getMin() const { STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Minimum of empty set is not defined."); - - if (this->isResultForAllStates()) { - return storm::utility::minimum(boost::get(values)); - } else { - return storm::utility::minimum(boost::get(values)); - } + return storm::utility::minimum(values); } template typename ExplicitQuantitativeCheckResult::ExtendedValueType ExplicitQuantitativeCheckResult::getMax() const { - STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Minimum of empty set is not defined."); - - if (this->isResultForAllStates()) { - return storm::utility::maximum(boost::get(values)); - } else { - return storm::utility::maximum(boost::get(values)); - } + STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Maximum of empty set is not defined."); + return storm::utility::maximum(values); } template std::pair::ExtendedValueType, typename ExplicitQuantitativeCheckResult::ExtendedValueType> ExplicitQuantitativeCheckResult::getMinMax() const { STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Minimum/maximum of empty set is not defined."); - - if (this->isResultForAllStates()) { - return storm::utility::minmax(boost::get(values)); - } else { - return storm::utility::minmax(boost::get(values)); - } + return storm::utility::minmax(values); } template @@ -329,18 +264,10 @@ typename ExplicitQuantitativeCheckResult::ExtendedValueType ExplicitQ STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Sum of empty set is not defined."); ExtendedValueType sum = storm::utility::zero(); - if (this->isResultForAllStates()) { - for (auto const& element : boost::get(values)) { - STORM_LOG_THROW(!storm::utility::isInfinity(element), storm::exceptions::InvalidOperationException, - "Cannot compute the sum of values containing infinity."); - sum += element; - } - } else { - for (auto const& element : boost::get(values)) { - STORM_LOG_THROW(!storm::utility::isInfinity(element.second), storm::exceptions::InvalidOperationException, - "Cannot compute the sum of values containing infinity."); - sum += element.second; - } + for (auto const& element : values) { + STORM_LOG_THROW(!storm::utility::isInfinity(element), storm::exceptions::InvalidOperationException, + "Cannot compute the sum of values containing infinity."); + sum += element; } return sum; } @@ -350,23 +277,12 @@ typename ExplicitQuantitativeCheckResult::ExtendedValueType ExplicitQ STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Average of empty set is not defined."); ExtendedValueType sum = storm::utility::zero(); - uint64_t count = 0; - if (this->isResultForAllStates()) { - for (auto const& element : boost::get(values)) { - STORM_LOG_THROW(!storm::utility::isInfinity(element), storm::exceptions::InvalidOperationException, - "Cannot compute the average of values containing infinity."); - sum += element; - } - count = boost::get(values).size(); - } else { - for (auto const& element : boost::get(values)) { - STORM_LOG_THROW(!storm::utility::isInfinity(element.second), storm::exceptions::InvalidOperationException, - "Cannot compute the average of values containing infinity."); - sum += element.second; - } - count = boost::get(values).size(); + for (auto const& element : values) { + STORM_LOG_THROW(!storm::utility::isInfinity(element), storm::exceptions::InvalidOperationException, + "Cannot compute the average of values containing infinity."); + sum += element; } - return sum / storm::utility::convertNumber(count); + return sum / storm::utility::convertNumber(values.size()); } template @@ -429,8 +345,8 @@ void printRange(std::ostream& out, ValueType const& min, ValueType const& max) { } template -void ExplicitQuantitativeCheckResult::printValue(std::ostream& out, storm::storage::sparse::state_type state) const { - print(out, (*this)[state]); +void ExplicitQuantitativeCheckResult::printValue(std::ostream& out, uint64_t offset) const { + print(out, values[offset]); if (!this->hasLowerBounds() && !this->hasUpperBounds()) { return; } @@ -438,13 +354,13 @@ void ExplicitQuantitativeCheckResult::printValue(std::ostream& out, s // that is printed for large results below. out << " ["; if (this->hasLowerBounds()) { - print(out, this->isResultForAllStates() ? this->getLowerBoundVector()[state] : this->getLowerBoundMap().at(state)); + print(out, this->getLowerBoundVector()[offset]); } else { out << "-inf"; } out << ", "; if (this->hasUpperBounds()) { - print(out, this->isResultForAllStates() ? this->getUpperBoundVector()[state] : this->getUpperBoundMap().at(state)); + print(out, this->getUpperBoundVector()[offset]); } else { out << "inf"; } @@ -454,51 +370,21 @@ void ExplicitQuantitativeCheckResult::printValue(std::ostream& out, s template std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ostream& out) const { bool minMaxSupported = std::is_same::value || std::is_same::value; - bool printAsRange = false; - if (this->isResultForAllStates()) { - vector_type const& valuesAsVector = boost::get(values); - if (valuesAsVector.size() >= 10 && minMaxSupported) { - printAsRange = true; - } else { - out << "{"; - bool first = true; - for (uint64_t state = 0; state < valuesAsVector.size(); ++state) { - if (!first) { - out << ", "; - } else { - first = false; - } - this->printValue(out, state); - } - out << "}"; - } + if (values.size() >= 10 && minMaxSupported) { + std::pair minmax = this->getMinMax(); + printRange(out, minmax.first, minmax.second); + } else if (!this->isResultForAllStates() && values.size() == 1) { + this->printValue(out, 0); } else { - map_type const& valuesAsMap = boost::get(values); - if (valuesAsMap.size() >= 10 && minMaxSupported) { - printAsRange = true; - } else { - if (valuesAsMap.size() == 1) { - this->printValue(out, valuesAsMap.begin()->first); - } else { - out << "{"; - bool first = true; - for (auto const& element : valuesAsMap) { - if (!first) { - out << ", "; - } else { - first = false; - } - this->printValue(out, element.first); - } - out << "}"; + out << "{"; + for (uint64_t offset = 0; offset < values.size(); ++offset) { + if (offset > 0) { + out << ", "; } + this->printValue(out, offset); } - } - - if (printAsRange) { - std::pair minmax = this->getMinMax(); - printRange(out, minmax.first, minmax.second); + out << "}"; } return out; @@ -507,66 +393,39 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost template std::unique_ptr ExplicitQuantitativeCheckResult::compareAgainstBound(storm::logic::ComparisonType comparisonType, ValueType const& bound) const { - if (this->isResultForAllStates()) { - vector_type const& valuesAsVector = boost::get(values); - storm::storage::BitVector result(valuesAsVector.size()); + auto const compare = [&comparisonType, &bound](ExtendedValueType const& value) { switch (comparisonType) { case logic::ComparisonType::Less: - for (uint_fast64_t index = 0; index < valuesAsVector.size(); ++index) { - if (valuesAsVector[index] < bound) { - result.set(index); - } - } - break; + return value < bound; case logic::ComparisonType::LessEqual: - for (uint_fast64_t index = 0; index < valuesAsVector.size(); ++index) { - if (valuesAsVector[index] <= bound) { - result.set(index); - } - } - break; + return value <= bound; case logic::ComparisonType::Greater: - for (uint_fast64_t index = 0; index < valuesAsVector.size(); ++index) { - if (valuesAsVector[index] > bound) { - result.set(index); - } - } - break; + return value > bound; case logic::ComparisonType::GreaterEqual: - for (uint_fast64_t index = 0; index < valuesAsVector.size(); ++index) { - if (valuesAsVector[index] >= bound) { - result.set(index); - } - } - break; + return value >= bound; } - return std::unique_ptr(new ExplicitQualitativeCheckResult(std::move(result), std::move(scheduler))); - } else { - map_type const& valuesAsMap = boost::get(values); - std::map result; - switch (comparisonType) { - case logic::ComparisonType::Less: - for (auto const& element : valuesAsMap) { - result[element.first] = element.second < bound; - } - break; - case logic::ComparisonType::LessEqual: - for (auto const& element : valuesAsMap) { - result[element.first] = element.second <= bound; - } - break; - case logic::ComparisonType::Greater: - for (auto const& element : valuesAsMap) { - result[element.first] = element.second > bound; - } - break; - case logic::ComparisonType::GreaterEqual: - for (auto const& element : valuesAsMap) { - result[element.first] = element.second >= bound; - } - break; + return false; + }; + + // A comparison is only sound if the bound fall outside the lower and upperbound of te result. + if (this->hasLowerBounds() && this->hasUpperBounds()) { + for (uint64_t offset = 0; offset < values.size(); ++offset) { + STORM_LOG_ASSERT(!((*bounds.lower)[offset] < bound && bound < (*bounds.upper)[offset]), + "The bound " << bound << " lies between the lower bound " << (*bounds.lower)[offset] << " and the upper bound " + << (*bounds.upper)[offset] << ", so the comparison against it is not decided at state " << offset << "."); + } + } + + storm::storage::BitVector result(values.size()); + for (uint64_t offset = 0; offset < values.size(); ++offset) { + if (compare(values[offset])) { + result.set(offset); } - return std::unique_ptr(new ExplicitQualitativeCheckResult(std::move(result), std::move(scheduler))); + } + if (this->isResultForAllStates()) { + return std::unique_ptr(new ExplicitQualitativeCheckResult(std::move(result), scheduler)); + } else { + return std::unique_ptr(new ExplicitQualitativeCheckResult(*states, std::move(result), scheduler)); } } @@ -580,24 +439,13 @@ std::unique_ptr ExplicitQuantitativeCheckResult typename ExplicitQuantitativeCheckResult::ExtendedValueType& ExplicitQuantitativeCheckResult::operator[]( storm::storage::sparse::state_type state) { - if (this->isResultForAllStates()) { - return boost::get(values)[state]; - } else { - return boost::get(values)[state]; - } + return values[this->getOffset(state)]; } template typename ExplicitQuantitativeCheckResult::ExtendedValueType const& ExplicitQuantitativeCheckResult::operator[]( storm::storage::sparse::state_type state) const { - if (this->isResultForAllStates()) { - return boost::get(values)[state]; - } else { - map_type const& valuesAsMap = boost::get(values); - auto const& keyValuePair = valuesAsMap.find(state); - STORM_LOG_THROW(keyValuePair != valuesAsMap.end(), storm::exceptions::InvalidOperationException, "Unknown key '" << state << "'."); - return keyValuePair->second; - } + return values[this->getOffset(state)]; } template @@ -607,7 +455,7 @@ bool ExplicitQuantitativeCheckResult::isExplicit() const { template bool ExplicitQuantitativeCheckResult::isResultForAllStates() const { - return values.which() == 0; + return !states.has_value(); } template @@ -617,15 +465,15 @@ bool ExplicitQuantitativeCheckResult::isExplicitQuantitativeCheckResu template void ExplicitQuantitativeCheckResult::oneMinus() { - detail::complementValues(values); + storm::utility::vector::subtractFromConstantOneVector(values); if (this->hasLowerBounds()) { - detail::complementValues(*lowerBounds); + storm::utility::vector::subtractFromConstantOneVector(*bounds.lower); } if (this->hasUpperBounds()) { - detail::complementValues(*upperBounds); + storm::utility::vector::subtractFromConstantOneVector(*bounds.upper); } // One minus is antitone, so what used to bound the values from below now bounds them from above. - std::swap(lowerBounds, upperBounds); + std::swap(bounds.lower, bounds.upper); } /*! @@ -674,21 +522,11 @@ template storm::json ExplicitQuantitativeCheckResult::toJson(std::optional const& stateValuations, std::optional const& stateLabels) const { storm::json result; - if (this->isResultForAllStates()) { - vector_type const& valuesAsVector = boost::get(values); - for (uint64_t state = 0; state < valuesAsVector.size(); ++state) { - insertJsonEntry(result, state, valuesAsVector[state], stateValuations, stateLabels, - this->hasLowerBounds() ? std::make_optional(this->getLowerBoundVector()[state]) : std::nullopt, - this->hasUpperBounds() ? std::make_optional(this->getUpperBoundVector()[state]) : std::nullopt); - } - } else { - map_type const& valuesAsMap = boost::get(values); - for (auto const& stateValue : valuesAsMap) { - insertJsonEntry(result, stateValue.first, stateValue.second, stateValuations, stateLabels, - this->hasLowerBounds() ? std::make_optional(this->getLowerBoundMap().at(stateValue.first)) : std::nullopt, - this->hasUpperBounds() ? std::make_optional(this->getUpperBoundMap().at(stateValue.first)) : std::nullopt); - } - } + this->forEachState([&](storm::storage::sparse::state_type state, uint64_t offset) { + insertJsonEntry(result, state, values[offset], stateValuations, stateLabels, + this->hasLowerBounds() ? std::make_optional(this->getLowerBoundVector()[offset]) : std::nullopt, + this->hasUpperBounds() ? std::make_optional(this->getUpperBoundVector()[offset]) : std::nullopt); + }); return result; } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h index 8943f148fe..872ec1a7e4 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h @@ -1,7 +1,4 @@ #pragma once -#include -#include -#include #include #include @@ -9,6 +6,7 @@ #include "storm/modelchecker/results/QuantitativeCheckResult.h" #include "storm/models/sparse/StateLabeling.h" #include "storm/solver/SolutionBounds.h" +#include "storm/storage/BitVector.h" #include "storm/storage/Scheduler.h" #include "storm/storage/sparse/StateType.h" #include "storm/storage/valuations/Valuations.h" @@ -21,34 +19,48 @@ namespace modelchecker { template class ExplicitQualitativeCheckResult; +/*! + * A quantitative check result over the states of a sparse model. + * + * The result either covers every state of the model or just a subset of them, e.g. after filtering it to the + * initial states. In the latter case the states in question are recorded in a bit vector and the values are + * stored compressed, i.e. the i-th value belongs to the i-th state selected by that bit vector. + */ template class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult { public: typedef typename QuantitativeCheckResult::ExtendedValueType ExtendedValueType; typedef std::vector vector_type; - typedef std::map map_type; - ExplicitQuantitativeCheckResult(); - ExplicitQuantitativeCheckResult(map_type const& values); - ExplicitQuantitativeCheckResult(map_type&& values); + /*! + * Creates a result that holds the given value for the given state only. + */ ExplicitQuantitativeCheckResult(storm::storage::sparse::state_type const& state, ExtendedValueType const& value); - ExplicitQuantitativeCheckResult(vector_type const& values); - ExplicitQuantitativeCheckResult(vector_type&& values); /*! - * Takes over a result that is still expressed in the plain value type, reading an entry equal to the value that + * Creates a result for all states of a model with the given number of states. + */ + ExplicitQuantitativeCheckResult(vector_type const& values, std::optional>> scheduler = {}); + ExplicitQuantitativeCheckResult(vector_type&& values, std::optional>> scheduler = {}); + + /*! + * Creates a result for the given states only. + * @param states The states the result is for. + * @param values One value per state selected by @p states, in the order of the selected states. + */ + ExplicitQuantitativeCheckResult(storm::storage::BitVector states, vector_type&& values, + std::optional>> scheduler = {}); + + /*! + * Takes over values that are still expressed in the plain value type, reading an entry equal to the value that * storm::utility::infinity yields as an infinity. */ ExplicitQuantitativeCheckResult(std::vector const& values) requires(!std::is_same_v, ValueType>); ExplicitQuantitativeCheckResult(std::vector&& values) requires(!std::is_same_v, ValueType>); - ExplicitQuantitativeCheckResult(std::map const& values) + ExplicitQuantitativeCheckResult(storm::storage::BitVector states, std::vector&& values) requires(!std::is_same_v, ValueType>); - ExplicitQuantitativeCheckResult(boost::variant const& values, - std::optional>> scheduler = {}); - ExplicitQuantitativeCheckResult(boost::variant&& values, - std::optional>> scheduler = {}); ExplicitQuantitativeCheckResult(ExplicitQuantitativeCheckResult const& other) = default; ExplicitQuantitativeCheckResult& operator=(ExplicitQuantitativeCheckResult const& other) = default; @@ -60,6 +72,10 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult clone() const override; + /*! + * Retrieves the value of the given state. + * @pre The result holds a value for that state. + */ ExtendedValueType& operator[](storm::storage::sparse::state_type state); ExtendedValueType const& operator[](storm::storage::sparse::state_type state) const; @@ -70,9 +86,23 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult const& getSolutionBounds() const; + + /*! + * Sets sound bounds on the actual values. Each bound must have the same shape as the values, i.e. hold one + * entry per state this result is for. */ - void setLowerBounds(boost::variant lowerBounds); - void setUpperBounds(boost::variant upperBounds); - void setBounds(boost::variant lowerBounds, boost::variant upperBounds); + void setLowerBounds(vector_type lowerBounds); + void setUpperBounds(vector_type upperBounds); + void setBounds(storm::solver::SolutionBounds bounds); + + /*! + * Sets bounds that are still expressed in the plain value type, reading an entry equal to the value that + * storm::utility::infinity yields as an infinity. + */ + void setBounds(storm::solver::SolutionBounds bounds) + requires(!std::is_same_v, ValueType>); /*! * Drops all bounds, e.g. after an operation that cannot maintain them. @@ -141,42 +182,52 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult + void forEachState(Function const& f) const { + if (states) { + uint64_t offset = 0; + for (auto const& state : *states) { + f(state, offset); + ++offset; + } + } else { + for (uint64_t state = 0; state < values.size(); ++state) { + f(state, state); + } + } + } + /*! * Asserts that the given bounds have the same shape as the values. */ - void assertBoundsShape(boost::variant const& bounds) const; + void assertBoundsShape(vector_type const& bounds) const; /*! - * Writes the value of the given state, followed by its bounds if any are known. + * Writes the value stored at the given offset, followed by its bounds if any are known. */ - void printValue(std::ostream& out, storm::storage::sparse::state_type state) const; + void printValue(std::ostream& out, uint64_t offset) const; - // The values of the quantitative check result. These are estimates of the actual values, which lie within - // the bounds below but carry no further guarantee. - boost::variant values; + // The states this result holds values for, or nothing at all if it is a result for all states. + std::optional states; - // Sound lower bounds on the actual values, if an algorithm provided them. - std::optional> lowerBounds; + // The values of the quantitative check result, one per state this result is for. These are estimates of the + // actual values, which lie within the bounds below but carry no further guarantee. + vector_type values; - // Sound upper bounds on the actual values, if an algorithm provided them. - std::optional> upperBounds; + // Sound bounds on the actual values, if an algorithm provided them. + storm::solver::SolutionBounds bounds; // An optional scheduler that accompanies the values. std::optional>> scheduler; }; - -/*! - * Transfers the sound bounds that an algorithm computed to the given check result. Each of the two sides is - * transferred only if the algorithm established it; the other side is left alone. - */ -template -void setBounds(ExplicitQuantitativeCheckResult& result, storm::solver::SolutionBounds& bounds) { - if (bounds.hasLower()) { - result.setLowerBounds(std::move(*bounds.lower)); - } - if (bounds.hasUpper()) { - result.setUpperBounds(std::move(*bounds.upper)); - } -} } // namespace modelchecker } // namespace storm From e46d07442cd2c3837f45c268c40b62ac9bdca665 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Mon, 7 Sep 2026 16:23:47 +0200 Subject: [PATCH 05/29] Widen bounds that arrive in the plain value type instead of reading a sentinel The check result stores its bounds in the extended value type so that a single interface serves every algorithm, whether or not it can bound a value by infinity. Bounds that arrive in the plain value type are therefore widened, and they are taken to be finite throughout while doing so: nothing hands bounds around by sentinel, and an algorithm that has an infinite bound to report says so by handing over the extended type in the first place. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011oamBujHwK891QqnAYZ6Wr (cherry picked from commit 5632fd11536430e116ed9add7c65924ce2a13c95) --- .../modelchecker/results/ExplicitQuantitativeCheckResult.cpp | 4 ++-- .../modelchecker/results/ExplicitQuantitativeCheckResult.h | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 76edf2905b..8275c1e9a6 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -195,10 +195,10 @@ void ExplicitQuantitativeCheckResult::setBounds(storm::solver::Soluti requires(!std::is_same_v, ValueType>) { if (bounds.hasLower()) { - this->setLowerBounds(storm::utility::fromSentinel(std::move(*bounds.lower))); + this->setLowerBounds(storm::utility::widen(std::move(*bounds.lower))); } if (bounds.hasUpper()) { - this->setUpperBounds(storm::utility::fromSentinel(std::move(*bounds.upper))); + this->setUpperBounds(storm::utility::widen(std::move(*bounds.upper))); } } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h index 872ec1a7e4..afb5aa585f 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h @@ -146,8 +146,9 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult bounds); /*! - * Sets bounds that are still expressed in the plain value type, reading an entry equal to the value that - * storm::utility::infinity yields as an infinity. + * Sets bounds that are still expressed in the plain value type. Bounds that arrive in that type are taken to + * be finite throughout: no sentinel is read out of them, since nothing hands bounds around by sentinel. + * An algorithm that can bound a value by infinity states so by handing over the extended type instead. */ void setBounds(storm::solver::SolutionBounds bounds) requires(!std::is_same_v, ValueType>); From c685a08ca8bf318308367d126205e7fb15f74e65 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 10 Sep 2026 14:59:32 +0200 Subject: [PATCH 06/29] Pass the optional solution bounds by OptionalRef instead of by pointer The out-parameter that the interval and optimistic value iteration helpers use to hand back the bounds they maintain was a raw pointer defaulting to nullptr. Storm has a type for exactly this, storm::OptionalRef, whose own documentation names optional function arguments as its use case, and which is already used in this way elsewhere in the core library. Using it here keeps the callers from spelling out an address-of and gives the parameter a name that says it does not own anything. The pointer form remains the prevailing idiom for other optional out-parameters in this directory (Multiplier, GameSolver, LpMinMaxLinearEquationSolver); those are left alone, as changing them has nothing to do with the bounds. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL --- src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp | 4 ++-- src/storm/solver/NativeLinearEquationSolver.cpp | 4 ++-- src/storm/solver/helper/IntervalterationHelper.cpp | 4 ++-- src/storm/solver/helper/IntervalterationHelper.h | 4 +++- src/storm/solver/helper/OptimisticValueIterationHelper.cpp | 6 +++--- src/storm/solver/helper/OptimisticValueIterationHelper.h | 5 +++-- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp index 6a6402c250..c7e631cb65 100644 --- a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp +++ b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp @@ -598,7 +598,7 @@ bool IterativeMinMaxLinearEquationSolver::solveEquation this->startMeasureProgress(); storm::solver::SolutionBounds solutionBounds; auto status = oviHelper.OVI(x, b, numIterations, env.solver().minMax().getRelativeTerminationCriterion(), prec, dir, guessingFactor, lowerBound, - upperBound, oviCallback, &solutionBounds); + upperBound, oviCallback, solutionBounds); if (solutionBounds.hasAny()) { this->setSolutionBounds(std::move(solutionBounds)); } @@ -812,7 +812,7 @@ bool IterativeMinMaxLinearEquationSolver::solveEquation this->startMeasureProgress(); storm::solver::SolutionBounds solutionBounds; auto status = iiHelper.II(x, b, numIterations, env.solver().minMax().getRelativeTerminationCriterion(), prec, lowerBoundsCallback, upperBoundsCallback, - dir, iiCallback, optionalRelevantValues, &solutionBounds); + dir, iiCallback, optionalRelevantValues, solutionBounds); if (solutionBounds.hasAny()) { this->setSolutionBounds(std::move(solutionBounds)); } diff --git a/src/storm/solver/NativeLinearEquationSolver.cpp b/src/storm/solver/NativeLinearEquationSolver.cpp index 1831b88b32..c54271a675 100644 --- a/src/storm/solver/NativeLinearEquationSolver.cpp +++ b/src/storm/solver/NativeLinearEquationSolver.cpp @@ -443,7 +443,7 @@ bool NativeLinearEquationSolver::solveEquationsIntervalIteration(Envi this->startMeasureProgress(); storm::solver::SolutionBounds solutionBounds; auto status = iiHelper.II(x, b, numIterations, env.solver().native().getRelativeTerminationCriterion(), prec, lowerBoundsCallback, upperBoundsCallback, {}, - iiCallback, optionalRelevantValues, &solutionBounds); + iiCallback, optionalRelevantValues, solutionBounds); if (solutionBounds.hasAny()) { this->setSolutionBounds(std::move(solutionBounds)); } @@ -531,7 +531,7 @@ bool NativeLinearEquationSolver::solveEquationsOptimisticValueIterati this->startMeasureProgress(); storm::solver::SolutionBounds solutionBounds; auto status = oviHelper.OVI(x, b, numIterations, env.solver().native().getRelativeTerminationCriterion(), prec, {}, guessingFactor, lowerBound, upperBound, - oviCallback, &solutionBounds); + oviCallback, solutionBounds); if (solutionBounds.hasAny()) { this->setSolutionBounds(std::move(solutionBounds)); } diff --git a/src/storm/solver/helper/IntervalterationHelper.cpp b/src/storm/solver/helper/IntervalterationHelper.cpp index b51bdb0d9c..a59291e8ce 100644 --- a/src/storm/solver/helper/IntervalterationHelper.cpp +++ b/src/storm/solver/helper/IntervalterationHelper.cpp @@ -120,7 +120,7 @@ SolverStatus IntervalIterationHelper::II( std::vector& operand, std::vector const& offsets, uint64_t& numIterations, bool relative, ValueType const& precision, std::function&)> const& prepareLowerBounds, std::function&)> const& prepareUpperBounds, std::optional const& dir, std::function const&)> const& iterationCallback, - std::optional const& relevantValues, SolutionBounds* solutionBounds) const { + std::optional const& relevantValues, storm::OptionalRef> solutionBounds) const { // Create two vectors x and y using the given operand plus an auxiliary vector. std::pair, std::vector> xy; auto& auxVector = viOperator->allocateAuxiliaryVector(operand.size()); @@ -138,7 +138,7 @@ SolverStatus IntervalIterationHelper::II( } else { status = II(xy, offsets, numIterations, relative, precision, iterationCallback, relevantValues); } - if (solutionBounds != nullptr) { + if (solutionBounds.has_value()) { // Hand out the enclosure before it is collapsed into the point estimate below. Interval iteration // maintains xy.first below and xy.second above the solution in every iteration, so both sides are // sound even if the iteration was aborted before converging. diff --git a/src/storm/solver/helper/IntervalterationHelper.h b/src/storm/solver/helper/IntervalterationHelper.h index 9ff6355d2a..9bad1fcb86 100644 --- a/src/storm/solver/helper/IntervalterationHelper.h +++ b/src/storm/solver/helper/IntervalterationHelper.h @@ -10,6 +10,7 @@ #include "storm/solver/SolverStatus.h" #include "storm/solver/helper/ValueIterationOperatorForward.h" #include "storm/storage/BitVector.h" +#include "storm/utility/OptionalRef.h" namespace storm::solver::helper { @@ -38,7 +39,8 @@ class IntervalIterationHelper { std::function&)> const& prepareLowerBounds, std::function&)> const& prepareUpperBounds, std::optional const& dir = {}, std::function const&)> const& iterationCallback = {}, - std::optional const& relevantValues = {}, SolutionBounds* solutionBounds = nullptr) const; + std::optional const& relevantValues = {}, + storm::OptionalRef> solutionBounds = storm::NullRef) const; SolverStatus II(std::vector& operand, std::vector const& offsets, bool relative, ValueType const& precision, std::function&)> const& prepareLowerBounds, diff --git a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp index 9622678733..6784cd5491 100644 --- a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp +++ b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp @@ -270,7 +270,7 @@ SolverStatus OptimisticValueIterationHelper::OVI( std::vector& operand, std::vector const& offsets, uint64_t& numIterations, bool relative, ValueType const& precision, std::optional const& dir, std::optional const& guessValue, std::optional const& lowerBound, std::optional const& upperBound, std::function const&)> const& iterationCallback, - SolutionBounds* solutionBounds) const { + storm::OptionalRef> solutionBounds) const { // Create two vectors v and u using the given operand plus an auxiliary vector. std::pair, std::vector> vu; auto& auxVector = viOperator->allocateAuxiliaryVector(operand.size()); @@ -282,7 +282,7 @@ SolverStatus OptimisticValueIterationHelper::OVI( } auto status = OVI(vu, offsets, numIterations, relative, doublePrec, dir, guessValue ? *guessValue : doublePrec, lowerBound, upperBound, iterationCallback); bool const converged = status == SolverStatus::Converged; - if (solutionBounds != nullptr) { + if (solutionBounds.has_value()) { // The operand was initialized below the solution and every iteration -- both the ones of the value // iteration phase and the ones of the verification phase -- applies the (monotone) operator to it, so // vu.first lies below the solution no matter why we stopped iterating. @@ -313,7 +313,7 @@ SolverStatus OptimisticValueIterationHelper::OVI( std::vector& operand, std::vector const& offsets, bool relative, ValueType const& precision, std::optional const& dir, std::optional const& guessValue, std::optional const& lowerBound, std::optional const& upperBound, std::function const&)> const& iterationCallback, - SolutionBounds* solutionBounds) const { + storm::OptionalRef> solutionBounds) const { uint64_t numIterations = 0; return OVI(operand, offsets, numIterations, relative, precision, dir, guessValue, lowerBound, upperBound, iterationCallback, solutionBounds); } diff --git a/src/storm/solver/helper/OptimisticValueIterationHelper.h b/src/storm/solver/helper/OptimisticValueIterationHelper.h index 60f2d0b458..7c51c02067 100644 --- a/src/storm/solver/helper/OptimisticValueIterationHelper.h +++ b/src/storm/solver/helper/OptimisticValueIterationHelper.h @@ -11,6 +11,7 @@ #include "storm/solver/SolverStatus.h" #include "storm/solver/helper/ValueIterationOperatorForward.h" +#include "storm/utility/OptionalRef.h" namespace storm::solver::helper { @@ -38,13 +39,13 @@ class OptimisticValueIterationHelper { std::optional const& dir = {}, std::optional const& guessValue = {}, std::optional const& lowerBound = {}, std::optional const& upperBound = {}, std::function const&)> const& iterationCallback = {}, - SolutionBounds* solutionBounds = nullptr) const; + storm::OptionalRef> solutionBounds = storm::NullRef) const; SolverStatus OVI(std::vector& operand, std::vector const& offsets, bool relative, ValueType const& precision, std::optional const& dir = {}, std::optional const& guessValue = {}, std::optional const& lowerBound = {}, std::optional const& upperBound = {}, std::function const&)> const& iterationCallback = {}, - SolutionBounds* solutionBounds = nullptr) const; + storm::OptionalRef> solutionBounds = storm::NullRef) const; private: template From 9e287256a927b5f7e241c03b5d14771e2ea17fc3 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 10 Sep 2026 12:04:04 +0200 Subject: [PATCH 07/29] Warn users if qualitative bound is between lower and upperbound of a value. (cherry picked from commit 16668e4ef7d90c09edbf37f518e85423aad84365) --- .../results/ExplicitQuantitativeCheckResult.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 8275c1e9a6..ad50839a7f 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -410,9 +410,9 @@ std::unique_ptr ExplicitQuantitativeCheckResult::compare // A comparison is only sound if the bound fall outside the lower and upperbound of te result. if (this->hasLowerBounds() && this->hasUpperBounds()) { for (uint64_t offset = 0; offset < values.size(); ++offset) { - STORM_LOG_ASSERT(!((*bounds.lower)[offset] < bound && bound < (*bounds.upper)[offset]), - "The bound " << bound << " lies between the lower bound " << (*bounds.lower)[offset] << " and the upper bound " - << (*bounds.upper)[offset] << ", so the comparison against it is not decided at state " << offset << "."); + STORM_LOG_WARN_COND(!((*bounds.lower)[offset] < bound && bound < (*bounds.upper)[offset]), + "The bound " << bound << " lies between the lower bound " << (*bounds.lower)[offset] << " and the upper bound " + << (*bounds.upper)[offset] << ", so the comparison against it is not decided at state " << offset << "."); } } From 941b871d2bbc3c1cc564690344dc7029634143e5 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 10 Sep 2026 14:43:10 +0200 Subject: [PATCH 08/29] Take values arriving in the plain type as finite Constructing a check result from a plain vector of values scanned every entry for the sentinel that storm::utility::infinity yields, which for rationals is the literal 100000000000. On an exact reachability probability that is a comparison against a fake infinity for every state, and it made "P=? [F ...] --exact" report two deprecation warnings about a sentinel that path never produces. These constructors now widen instead, applying to the values the rule that already governs the bounds: what arrives in the plain type is finite, and a computation that can produce an infinity says so by handing over the extended type. Every sparse path that can produce one does -- the reachability reward, total reward and expected visiting time helpers all return extended vectors -- so nothing is lost. Verified that an unreachable target still reports [inf, inf] in both the exact and the floating point case. The remaining callers of fromSentinel are the symbolic and hybrid results, whose helpers do still fill ADDs with the sentinel. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL (cherry picked from commit 6b71f2f17394f2b0f6e6419debf459cb1f7f3ad0) --- .../results/ExplicitQuantitativeCheckResult.cpp | 6 +++--- .../modelchecker/results/ExplicitQuantitativeCheckResult.h | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index ad50839a7f..32f521bf26 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -46,21 +46,21 @@ ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(stor template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(std::vector const& values) requires(!std::is_same_v, ValueType>) - : values(storm::utility::fromSentinel(std::vector(values))) { + : values(storm::utility::widen(std::vector(values))) { // Intentionally left empty. } template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(std::vector&& values) requires(!std::is_same_v, ValueType>) - : values(storm::utility::fromSentinel(std::move(values))) { + : values(storm::utility::widen(std::move(values))) { // Intentionally left empty. } template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::BitVector states, std::vector&& values) requires(!std::is_same_v, ValueType>) - : states(std::move(states)), values(storm::utility::fromSentinel(std::move(values))) { + : states(std::move(states)), values(storm::utility::widen(std::move(values))) { STORM_LOG_ASSERT(this->states->getNumberOfSetBits() == this->values.size(), "Expected one value per selected state."); } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h index afb5aa585f..c86389192d 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h @@ -52,8 +52,10 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult>> scheduler = {}); /*! - * Takes over values that are still expressed in the plain value type, reading an entry equal to the value that - * storm::utility::infinity yields as an infinity. + * Takes over values that are still expressed in the plain value type. Those are taken to be finite throughout: + * no sentinel is read out of them, mirroring how bounds arriving in the plain type are treated. A computation + * that can produce an infinite value says so by handing over the extended type instead, which every such path + * in the sparse engine does. */ ExplicitQuantitativeCheckResult(std::vector const& values) requires(!std::is_same_v, ValueType>); From d5414ee194c208797330a012f39931d503ed13b7 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 10 Sep 2026 14:59:59 +0200 Subject: [PATCH 09/29] Write an unknown bound as a dash rather than as an infinity A side that was never proven was printed as the infinity that bounds anything, so a probability computed by a procedure that certified only one side read as [0.3828117384, inf] -- which says the value might be arbitrarily large about a number that cannot exceed one. The reader cannot tell that apart from a genuinely infinite bound, which is a statement the tool does make: a state that cannot reach the target has an expected reward of exactly [inf, inf]. Both are now distinct. A missing side prints as "-", and "inf" is reserved for a bound that really is infinite. The JSON export already omitted the key for a missing side and is unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL --- .../results/ExplicitQuantitativeCheckResult.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 32f521bf26..062af6fd53 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -353,16 +353,18 @@ void ExplicitQuantitativeCheckResult::printValue(std::ostream& out, u // The enclosure is printed next to the estimate. Note that this is unrelated to the range over all states // that is printed for large results below. out << " ["; + // A side that is not known is written as a dash rather than as the infinity that would bound anything, so + // that "nothing was proven here" cannot be read as "the value may be arbitrarily large". if (this->hasLowerBounds()) { print(out, this->getLowerBoundVector()[offset]); } else { - out << "-inf"; + out << "-"; } out << ", "; if (this->hasUpperBounds()) { print(out, this->getUpperBoundVector()[offset]); } else { - out << "inf"; + out << "-"; } out << "]"; } From b7a9905b83baa2927f113341fafdaa56b2f40047 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 10 Sep 2026 15:03:01 +0200 Subject: [PATCH 10/29] Test that the bounds interval and optimistic value iteration report enclose the answer Nothing asserted the one thing the bounds claim. This adds a suite that checks, for each of the two configurations that produce them, that lower <= exact <= upper and lower <= reported value <= upper on properties whose answer is known in closed form: the Knuth-Yao die and the two process consensus protocol, as until probabilities on a DTMC and on an MDP. The configurations are listed as ones that must report both bounds, so a path that quietly stops reporting fails rather than passing vacuously. The tolerance of 1e-12, far below the solver precision the configurations ask for, is there because a bound computed in doubles can land an ulp on the far side of an answer that doubles cannot represent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL --- src/test/storm/CMakeLists.txt | 2 +- .../bounds/SolutionBoundsTest.cpp | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp diff --git a/src/test/storm/CMakeLists.txt b/src/test/storm/CMakeLists.txt index fc5ea78048..dec5a7474b 100755 --- a/src/test/storm/CMakeLists.txt +++ b/src/test/storm/CMakeLists.txt @@ -6,7 +6,7 @@ include_directories(${GTEST_INCLUDE_DIR}) # Set split and non-split test directories set(NON_SPLIT_TESTS adapter automata builder logic model parser settings simulator solver storage transformer utility) -set(MODELCHECKER_TEST_SPLITS csl exploration lexicographic multiobjective reachability) +set(MODELCHECKER_TEST_SPLITS bounds csl exploration lexicographic multiobjective reachability) set(MODELCHECKER_PRCTL_TEST_SPLITS dtmc mdp) function(configure_testsuite_target testsuite) diff --git a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp new file mode 100644 index 0000000000..6e397b172d --- /dev/null +++ b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp @@ -0,0 +1,143 @@ +#include "storm-config.h" +#include "test/storm_gtest.h" + +#include "storm-parsers/api/model_descriptions.h" +#include "storm-parsers/api/properties.h" +#include "storm/api/builder.h" +#include "storm/api/properties.h" +#include "storm/environment/solver/MinMaxSolverEnvironment.h" +#include "storm/environment/solver/NativeSolverEnvironment.h" +#include "storm/environment/solver/SolverEnvironment.h" +#include "storm/modelchecker/CheckTask.h" +#include "storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.h" +#include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" +#include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" +#include "storm/models/sparse/Dtmc.h" +#include "storm/models/sparse/Mdp.h" +#include "storm/models/sparse/StandardRewardModel.h" +#include "storm/utility/constants.h" + +/* + * These tests are about the sound bounds that a model checking call reports alongside its values, not about the + * values themselves. The claim under test is the one the bounds make, so every property here has an answer that + * is known in closed form and each case asserts + * + * lower <= exact <= upper and lower <= reported value <= upper. + * + * Every configuration listed below is one that must report both bounds, so a path that silently stops reporting + * them fails these tests rather than passing them vacuously. + */ + +namespace { + +class NativeIiEnvironment { + public: + typedef double ValueType; + static storm::Environment createEnvironment() { + storm::Environment env; + env.solver().setForceSoundness(true); + env.solver().setLinearEquationSolverType(storm::solver::EquationSolverType::Native); + env.solver().native().setMethod(storm::solver::NativeLinearEquationSolverMethod::IntervalIteration); + env.solver().minMax().setMethod(storm::solver::MinMaxMethod::IntervalIteration); + return env; + } +}; + +class NativeOviEnvironment { + public: + typedef double ValueType; + static storm::Environment createEnvironment() { + storm::Environment env; + env.solver().setForceSoundness(true); + env.solver().setLinearEquationSolverType(storm::solver::EquationSolverType::Native); + env.solver().native().setMethod(storm::solver::NativeLinearEquationSolverMethod::OptimisticValueIteration); + env.solver().minMax().setMethod(storm::solver::MinMaxMethod::OptimisticValueIteration); + return env; + } +}; + +template +class SolutionBoundsTest : public ::testing::Test { + public: + typedef typename TestType::ValueType ValueType; + typedef storm::utility::ExtendedValueType ExtendedValueType; + typedef storm::modelchecker::ExplicitQuantitativeCheckResult QuantitativeResult; + + SolutionBoundsTest() : _environment(TestType::createEnvironment()) {} + + storm::Environment const& env() const { + return _environment; + } + + ExtendedValueType parseNumber(std::string const& input) const { + return storm::utility::convertNumber(storm::utility::convertNumber(input)); + } + + /*! + * How far outside the reported enclosure the exact answer may still lie. A bound is only ever as sound as the + * arithmetic it is computed in: a procedure working in doubles can land a bound an ulp or two on the wrong + * side of an answer that is not representable at all. That is the rounding we accept, not a gap in the + * reasoning, so the slack is far below the solver precision these configurations ask for. + */ + ExtendedValueType tolerance() const { + return this->parseNumber("1e-12"); + } + + /*! + * Builds the given PRISM model and checks the given property on it. + */ + template + std::unique_ptr check(std::string const& modelFile, std::string const& propertyString) const { + storm::prism::Program program = storm::api::parseProgram(modelFile); + program = program.preprocess(); + auto formulas = storm::api::extractFormulasFromProperties(storm::api::parsePropertiesForPrismProgram(propertyString, program)); + auto model = storm::api::buildSparseModel(program, formulas)->template as(); + storm::modelchecker::CheckTask task(*formulas.front(), true); + if constexpr (std::is_same_v>) { + return storm::modelchecker::SparseDtmcPrctlModelChecker(*model).check(this->env(), task); + } else { + return storm::modelchecker::SparseMdpPrctlModelChecker(*model).check(this->env(), task); + } + } + + /*! + * Asserts that the bounds the given result reports for the initial state enclose the given exact answer, and + * that the value it reports lies between them as well. + */ + void expectEncloses(std::unique_ptr const& result, ExtendedValueType const& exact) const { + QuantitativeResult const& quantitative = result->template asExplicitQuantitativeCheckResult(); + ASSERT_TRUE(quantitative.hasLowerBounds()) << "No lower bound on the solution was reported at all."; + ASSERT_TRUE(quantitative.hasUpperBounds()) << "No upper bound on the solution was reported at all."; + uint64_t const offset = 0; + ExtendedValueType const& value = quantitative.getValueVector()[offset]; + ExtendedValueType const& lower = quantitative.getLowerBoundVector()[offset]; + ExtendedValueType const& upper = quantitative.getUpperBoundVector()[offset]; + EXPECT_LE(lower, exact + this->tolerance()) << "The lower bound exceeds the exact answer."; + EXPECT_LE(exact, upper + this->tolerance()) << "The upper bound falls below the exact answer."; + EXPECT_LE(lower, value) << "The lower bound exceeds the reported value."; + EXPECT_LE(value, upper) << "The reported value exceeds the upper bound."; + } + + private: + storm::Environment _environment; +}; + +typedef ::testing::Types TestingTypes; + +TYPED_TEST_SUITE(SolutionBoundsTest, TestingTypes, ); + +// The Knuth-Yao die yields a one with probability exactly 1/6. +TYPED_TEST(SolutionBoundsTest, DtmcUntilProbabilities) { + typedef typename TestFixture::ValueType ValueType; + auto result = this->template check>(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "P=? [F s=7 & d=1]"); + this->expectEncloses(result, this->parseNumber("1/6")); +} + +TYPED_TEST(SolutionBoundsTest, MdpUntilProbabilities) { + typedef typename TestFixture::ValueType ValueType; + auto result = this->template check>(STORM_TEST_RESOURCES_DIR "/mdp/coin2-2.nm", + "Pmin=? [F \"finished\" & \"all_coins_equal_1\"]"); + this->expectEncloses(result, this->parseNumber("49/128")); +} + +} // namespace From a718228c0fd8e36c008b1831db78c4e4c60dbbf0 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 10 Sep 2026 16:22:17 +0200 Subject: [PATCH 11/29] Trim the comments added for the solution bounds Comments that restate the code or a member name are removed, as are notes about work that is not done. What is left states a reason the code does not: why copying the result vector supplies the values outside the maybe states, why the two bounds are swapped when complementing, and why an unverified guess is not an upper bound. The convention for the two optional sides is stated once, on SolutionBounds, rather than repeated with examples at each use. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL --- .../csl/SparseCtmcCslModelChecker.cpp | 1 - .../csl/helper/SparseCtmcCslHelper.cpp | 1 - .../modelchecker/helper/ltl/SparseLTLHelper.cpp | 1 - .../helper/DTMCModelCheckingHelperReturnType.h | 1 - .../helper/MDPModelCheckingHelperReturnType.h | 3 --- .../prctl/helper/SparseDtmcPrctlHelper.cpp | 7 ++----- .../prctl/helper/SparseMdpPrctlHelper.cpp | 10 ++-------- .../results/ExplicitQualitativeCheckResult.cpp | 2 -- .../results/ExplicitQuantitativeCheckResult.cpp | 13 +++---------- .../results/ExplicitQuantitativeCheckResult.h | 17 ++++++----------- src/storm/solver/AbstractEquationSolver.h | 4 +--- src/storm/solver/SolutionBounds.h | 10 ++-------- .../solver/helper/IntervalterationHelper.cpp | 5 ++--- .../helper/OptimisticValueIterationHelper.cpp | 11 +++-------- 14 files changed, 21 insertions(+), 65 deletions(-) diff --git a/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp b/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp index 8dd8097015..6324d09b07 100644 --- a/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp +++ b/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp @@ -94,7 +94,6 @@ std::unique_ptr SparseCtmcCslModelChecker::com std::unique_ptr subResultPointer = this->check(env, pathFormula.getSubformula()); ExplicitQualitativeCheckResult const& subResult = subResultPointer->template asExplicitQualitativeCheckResult(); auto probabilisticTransitions = this->getModel().computeProbabilityMatrix(); - // CTMC results do not carry bounds yet. std::vector numericResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( env, storm::solver::SolveGoal(this->getModel(), checkTask), probabilisticTransitions, probabilisticTransitions.transpose(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet()) diff --git a/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp b/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp index e01e2ebfdc..e278b87626 100644 --- a/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp +++ b/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp @@ -258,7 +258,6 @@ std::vector SparseCtmcCslHelper::computeUntilProbabilities(Environmen storm::storage::SparseMatrix const& backwardTransitions, std::vector const& exitRateVector, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, bool qualitative) { - // The CTMC helper does not expose bounds to its callers yet. return std::move(SparseDtmcPrctlHelper::computeUntilProbabilities(env, std::move(goal), computeProbabilityMatrix(rateMatrix, exitRateVector), backwardTransitions, phiStates, psiStates, qualitative) .values); diff --git a/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp b/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp index 6e75f5e4da..6d4e3db0db 100644 --- a/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp +++ b/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp @@ -314,7 +314,6 @@ std::vector SparseLTLHelper::computeDAPr } } else { - // Any bounds would have to be projected back to the original model alongside the values, which is not done yet. prodNumericResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( env, std::move(solveGoalProduct), product->getProductModel().getTransitionMatrix(), product->getProductModel().getBackwardTransitions(), bvTrue, acceptingStates, this->isQualitativeSet()) diff --git a/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h b/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h index 566ee9a79e..db087f68fd 100644 --- a/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h +++ b/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h @@ -30,7 +30,6 @@ struct DTMCSparseModelCheckingHelperReturnType { // The values computed for the states. std::vector values; - // Sound bounds on the values, if the algorithm that computed them provided any. storm::solver::SolutionBounds solutionBounds; }; diff --git a/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h b/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h index 941be866a4..78351deb1e 100644 --- a/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h +++ b/src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h @@ -32,9 +32,6 @@ struct MDPSparseModelCheckingHelperReturnType { // A scheduler, if it was computed. std::unique_ptr> scheduler; - // Sound bounds on the values, if the algorithm that computed them provided any. These are bounds on the - // values, so they are expressed in the same type as those: a bound on a reward that may be infinite has to - // be able to say so. storm::solver::SolutionBounds solutionBounds; }; } // namespace helper diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp index 0c9df446e8..6c7d3bb4b8 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp @@ -276,9 +276,7 @@ DTMCSparseModelCheckingHelperReturnType SparseDtmcPrctlHelpersetBounds(storm::utility::zero(), storm::utility::one()); solver->solveEquations(env, x, b); - // Outside of the maybe states the probability is exactly zero or one, so the entries that - // result already holds bound those states from both sides. Note that the solver may well know - // only one of the two bounds, e.g. optimistic value iteration that did not converge. + // The copy of result supplies the exact values outside the maybe states. auto embedBound = [&result, &maybeStates](std::vector const& boundForMaybeStates) { std::vector bound(result); storm::utility::vector::setVectorValues(bound, maybeStates, boundForMaybeStates); @@ -382,8 +380,7 @@ DTMCSparseModelCheckingHelperReturnType SparseDtmcPrctlHelper() - entry; } - // One minus is antitone, so the complement of the lower bound bounds the result from above and vice - // versa. In particular, knowing only one side before means knowing only the other side afterwards. + // Inverse the bounds, lb = 1 - ub and ub = 1 - lb. auto& bounds = returnValue.solutionBounds; for (auto* bound : {&bounds.lower, &bounds.upper}) { if (*bound) { diff --git a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp index 96579bbd9a..75446101da 100644 --- a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp @@ -425,7 +425,6 @@ struct MaybeStateResult { std::vector values; boost::optional> scheduler; - // Sound bounds on the values, if the solver provided any. storm::solver::SolutionBounds solutionBounds; }; @@ -488,8 +487,6 @@ MaybeStateResult computeValuesForMaybeStates(Environment const& en // Create result. MaybeStateResult result(std::move(x)); - // Note that the solver may well know only one of the two bounds, e.g. optimistic value iteration that did - // not converge. if (solver->hasSolutionLowerBounds()) { result.solutionBounds.lower = solver->getSolutionLowerBounds(); } @@ -702,7 +699,6 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper result(transitionMatrix.getRowGroupCount(), storm::utility::zero()); - // Sound bounds on the result, if the solver provides any. storm::solver::SolutionBounds resultBounds; // We need to identify the maybe states (states which have a probability for satisfying the until formula @@ -780,8 +776,7 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper) { // For non-interval models, we only operated on the maybe states, and we must recover the qualitative values for the other state. - // Outside of the maybe states the probability is exactly zero or one, so the entries that - // result already holds bound those states from both sides. + // The copy of result supplies the exact values outside the maybe states. auto embedBound = [&result, &qualitativeStateSets](std::vector const& boundForMaybeStates) { std::vector bound(result); storm::utility::vector::setVectorValues(bound, qualitativeStateSets.maybeStates, boundForMaybeStates); @@ -849,8 +844,7 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper() - element; } - // One minus is antitone, so the complement of the lower bound bounds the result from above and vice - // versa. In particular, knowing only one side before means knowing only the other side afterwards. + // Inverse the bounds, lb = 1 - ub and ub = 1 - lb. auto& bounds = result.solutionBounds; for (auto* bound : {&bounds.lower, &bounds.upper}) { if (*bound) { diff --git a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp index 43941d2392..b9a3c995d2 100644 --- a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp @@ -172,8 +172,6 @@ void ExplicitQualitativeCheckResult::filter(QualitativeCheckResult co STORM_LOG_THROW(selected.isSubsetOf(available), storm::exceptions::InvalidOperationException, "The check result fails to contain some results referred to by the filter."); - // Keep the truth values of the selected states. Note that the result is a result for the selected states - // even if those happen to be all of them. truthValues = truthValues % (selected % available); states = filterTruthValues; } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 062af6fd53..720a0d107b 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -180,8 +180,6 @@ void ExplicitQuantitativeCheckResult::setUpperBounds(vector_type uppe template void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) { - // Each of the two sides is transferred only if the algorithm in question established it; the other side is - // left as it is. if (bounds.hasLower()) { this->setLowerBounds(std::move(*bounds.lower)); } @@ -226,8 +224,6 @@ void ExplicitQuantitativeCheckResult::filter(QualitativeCheckResult c STORM_LOG_THROW(selected.isSubsetOf(available), storm::exceptions::InvalidOperationException, "The check result fails to contain some results referred to by the filter."); - // The entries that survive, in the index space the values are stored in. Note that the result is a result - // for the selected states even if those happen to be all of them. storm::storage::BitVector const keep = selected % available; if (this->hasLowerBounds()) { @@ -350,11 +346,8 @@ void ExplicitQuantitativeCheckResult::printValue(std::ostream& out, u if (!this->hasLowerBounds() && !this->hasUpperBounds()) { return; } - // The enclosure is printed next to the estimate. Note that this is unrelated to the range over all states - // that is printed for large results below. out << " ["; - // A side that is not known is written as a dash rather than as the infinity that would bound anything, so - // that "nothing was proven here" cannot be read as "the value may be arbitrarily large". + // A side that is not known is written as a dash, so that it cannot be read as an infinite bound. if (this->hasLowerBounds()) { print(out, this->getLowerBoundVector()[offset]); } else { @@ -409,7 +402,7 @@ std::unique_ptr ExplicitQuantitativeCheckResult::compare return false; }; - // A comparison is only sound if the bound fall outside the lower and upperbound of te result. + // A comparison is only sound if the bound falls outside the lower and upper bound of the result. if (this->hasLowerBounds() && this->hasUpperBounds()) { for (uint64_t offset = 0; offset < values.size(); ++offset) { STORM_LOG_WARN_COND(!((*bounds.lower)[offset] < bound && bound < (*bounds.upper)[offset]), @@ -474,7 +467,7 @@ void ExplicitQuantitativeCheckResult::oneMinus() { if (this->hasUpperBounds()) { storm::utility::vector::subtractFromConstantOneVector(*bounds.upper); } - // One minus is antitone, so what used to bound the values from below now bounds them from above. + // Inverse the bounds, lb = 1 - ub and ub = 1 - lb. std::swap(bounds.lower, bounds.upper); } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h index c86389192d..3faf4c83ec 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h @@ -52,10 +52,8 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult>> scheduler = {}); /*! - * Takes over values that are still expressed in the plain value type. Those are taken to be finite throughout: - * no sentinel is read out of them, mirroring how bounds arriving in the plain type are treated. A computation - * that can produce an infinite value says so by handing over the extended type instead, which every such path - * in the sparse engine does. + * Takes over values that are still expressed in the plain value type, which are taken to be finite. A + * computation that can produce an infinite value hands over the extended type instead. */ ExplicitQuantitativeCheckResult(std::vector const& values) requires(!std::is_same_v, ValueType>); @@ -120,8 +118,6 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult bounds); /*! - * Sets bounds that are still expressed in the plain value type. Bounds that arrive in that type are taken to - * be finite throughout: no sentinel is read out of them, since nothing hands bounds around by sentinel. - * An algorithm that can bound a value by infinity states so by handing over the extended type instead. + * Sets bounds that are still expressed in the plain value type, which are taken to be finite. An algorithm + * that can bound a value by infinity hands over the extended type instead. */ void setBounds(storm::solver::SolutionBounds bounds) requires(!std::is_same_v, ValueType>); @@ -222,8 +217,8 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult states; - // The values of the quantitative check result, one per state this result is for. These are estimates of the - // actual values, which lie within the bounds below but carry no further guarantee. + // The values of the quantitative check result, one per state this result is for. These are estimates that + // lie within the bounds below but carry no further guarantee. vector_type values; // Sound bounds on the actual values, if an algorithm provided them. diff --git a/src/storm/solver/AbstractEquationSolver.h b/src/storm/solver/AbstractEquationSolver.h index 466ca10df4..cd1e0c843f 100644 --- a/src/storm/solver/AbstractEquationSolver.h +++ b/src/storm/solver/AbstractEquationSolver.h @@ -185,9 +185,7 @@ class AbstractEquationSolver { /*! * Retrieves whether the last call to this solver computed a sound lower resp. upper bound on the solution. - * Only some algorithms, most notably interval iteration and optimistic value iteration, provide these, and - * they need not provide both: optimistic value iteration, for instance, only verifies its upper bound once - * it converges, while its lower bound is available in any case. + * Only some algorithms provide these, and they need not provide both. */ bool hasSolutionLowerBounds() const; bool hasSolutionUpperBounds() const; diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h index a964218fda..fb03835917 100644 --- a/src/storm/solver/SolutionBounds.h +++ b/src/storm/solver/SolutionBounds.h @@ -6,11 +6,8 @@ namespace storm::solver { /*! - * Sound lower and upper bounds on a solution vector, as computed by e.g. interval iteration. - * Both bounds have the same size as the solution. They are tracked independently because an algorithm may - * well establish only one of them: optimistic value iteration, for instance, keeps a sound lower bound in - * every iteration but only verifies its upper bound once it converges. An unset bound means that the - * algorithm in question did not provide that side. + * Sound lower and upper bounds on a solution vector. Both have the same size as the solution. They are tracked + * independently, as an algorithm may establish only one of them; an unset bound means that side is not known. */ template struct SolutionBounds { @@ -25,9 +22,6 @@ struct SolutionBounds { return upper.has_value(); } - /*! - * Retrieves whether at least one of the two bounds is known. - */ bool hasAny() const { return hasLower() || hasUpper(); } diff --git a/src/storm/solver/helper/IntervalterationHelper.cpp b/src/storm/solver/helper/IntervalterationHelper.cpp index a59291e8ce..3bfebd5a88 100644 --- a/src/storm/solver/helper/IntervalterationHelper.cpp +++ b/src/storm/solver/helper/IntervalterationHelper.cpp @@ -139,9 +139,8 @@ SolverStatus IntervalIterationHelper::II( status = II(xy, offsets, numIterations, relative, precision, iterationCallback, relevantValues); } if (solutionBounds.has_value()) { - // Hand out the enclosure before it is collapsed into the point estimate below. Interval iteration - // maintains xy.first below and xy.second above the solution in every iteration, so both sides are - // sound even if the iteration was aborted before converging. + // Interval iteration keeps xy.first below and xy.second above the solution in every iteration, so both + // sides are sound even if it was aborted. Read them out before they are collapsed into the estimate. solutionBounds->lower = xy.first; solutionBounds->upper = xy.second; } diff --git a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp index 6784cd5491..643ce93a3a 100644 --- a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp +++ b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp @@ -283,24 +283,19 @@ SolverStatus OptimisticValueIterationHelper::OVI( auto status = OVI(vu, offsets, numIterations, relative, doublePrec, dir, guessValue ? *guessValue : doublePrec, lowerBound, upperBound, iterationCallback); bool const converged = status == SolverStatus::Converged; if (solutionBounds.has_value()) { - // The operand was initialized below the solution and every iteration -- both the ones of the value - // iteration phase and the ones of the verification phase -- applies the (monotone) operator to it, so - // vu.first lies below the solution no matter why we stopped iterating. + // The operand started below the solution and every iteration applies the monotone operator to it, so + // vu.first lies below the solution whatever stopped the iteration. solutionBounds->lower = vu.first; - // Only a converged run has verified that vu.second lies above the solution. Until the verification - // phase succeeds it is merely a guess, so handing it out as an upper bound would be unsound. + // Until the verification phase succeeds vu.second is merely a guess, not an upper bound. if (converged) { solutionBounds->upper = vu.second; } } if (converged) { auto two = storm::utility::convertNumber(2.0); - // get the average of lower- and upper result storm::utility::vector::applyPointwise( vu.first, vu.second, vu.first, [&two](ValueType const& a, ValueType const& b) -> ValueType { return (a + b) / two; }); } - // Otherwise vu.second holds an unverified guess (or, if we never got to guessing, uninitialized data), so - // averaging it into the result would be meaningless and could even push the result below vu.first. // Swap operand and aux vector back to original positions. vu.first.swap(operand); vu.second.swap(auxVector); From 73611a8ae142f019f6c0653a0f29055471a701a2 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 10 Sep 2026 16:26:19 +0200 Subject: [PATCH 12/29] One more comment --- src/storm/solver/SolutionBounds.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h index fb03835917..e1d8b781cb 100644 --- a/src/storm/solver/SolutionBounds.h +++ b/src/storm/solver/SolutionBounds.h @@ -14,18 +14,30 @@ struct SolutionBounds { std::optional> lower; std::optional> upper; + /* + * Returns true if the lower bound is set. + */ bool hasLower() const { return lower.has_value(); } + /* + * Returns true if the upper bound is set. + */ bool hasUpper() const { return upper.has_value(); } + /* + * Returns true if at least one of the bounds is set. + */ bool hasAny() const { return hasLower() || hasUpper(); } + /* + * Clears both bounds. + */ void clear() { lower = std::nullopt; upper = std::nullopt; From 624df5768ebd1ec5f15bcc51e011cfb0a2305eb7 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Fri, 11 Sep 2026 10:31:11 +0200 Subject: [PATCH 13/29] Fix z3 missing --- src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp index 6e397b172d..3f8cf36517 100644 --- a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp +++ b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp @@ -65,6 +65,12 @@ class SolutionBoundsTest : public ::testing::Test { SolutionBoundsTest() : _environment(TestType::createEnvironment()) {} + void SetUp() override { +#ifndef STORM_HAVE_Z3 + GTEST_SKIP() << "Z3 not available."; +#endif + } + storm::Environment const& env() const { return _environment; } From 19d205d90e4bc294abeabb0286aaf070f315c843 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 17 Sep 2026 10:25:19 +0200 Subject: [PATCH 14/29] Lift the bounds from the end component quotient in MDP until probabilities Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../prctl/helper/SparseMdpPrctlHelper.cpp | 12 +++++++ .../bounds/SolutionBoundsTest.cpp | 31 +++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp index 75446101da..154e0729fd 100644 --- a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp @@ -766,6 +766,18 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "We do not support this end component with interval models."); } else { + // The copy of result supplies the exact values outside the maybe states. + auto liftBound = [&result, &qualitativeStateSets, &ecInformation](std::vector const& boundInEcQuotient) { + std::vector bound(result); + ecInformation.get().setValues(bound, qualitativeStateSets.maybeStates, boundInEcQuotient); + return bound; + }; + if (resultForMaybeStates.solutionBounds.hasLower()) { + resultBounds.lower = liftBound(*resultForMaybeStates.solutionBounds.lower); + } + if (resultForMaybeStates.solutionBounds.hasUpper()) { + resultBounds.upper = liftBound(*resultForMaybeStates.solutionBounds.upper); + } ecInformation.get().setValues(result, qualitativeStateSets.maybeStates, resultForMaybeStates.getValues()); if (produceScheduler) { ecInformation.get().setScheduler(*scheduler, qualitativeStateSets.maybeStates, transitionMatrix, backwardTransitions, diff --git a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp index 3f8cf36517..0333a7d9d9 100644 --- a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp +++ b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp @@ -3,6 +3,7 @@ #include "storm-parsers/api/model_descriptions.h" #include "storm-parsers/api/properties.h" +#include "storm-parsers/parser/PrismParser.h" #include "storm/api/builder.h" #include "storm/api/properties.h" #include "storm/environment/solver/MinMaxSolverEnvironment.h" @@ -90,11 +91,18 @@ class SolutionBoundsTest : public ::testing::Test { } /*! - * Builds the given PRISM model and checks the given property on it. + * Builds the PRISM model in the given file and checks the given property on it. */ template std::unique_ptr check(std::string const& modelFile, std::string const& propertyString) const { - storm::prism::Program program = storm::api::parseProgram(modelFile); + return this->template check(storm::api::parseProgram(modelFile), propertyString); + } + + /*! + * Builds the given PRISM model and checks the given property on it. + */ + template + std::unique_ptr check(storm::prism::Program program, std::string const& propertyString) const { program = program.preprocess(); auto formulas = storm::api::extractFormulasFromProperties(storm::api::parsePropertiesForPrismProgram(propertyString, program)); auto model = storm::api::buildSparseModel(program, formulas)->template as(); @@ -146,4 +154,23 @@ TYPED_TEST(SolutionBoundsTest, MdpUntilProbabilities) { this->expectEncloses(result, this->parseNumber("49/128")); } +// Maximizing makes the sound methods eliminate the end component {x=0} of the maybe states, so the bounds come from the quotient system. +// The best scheduler leaves x=0 and x=1 through their probabilistic choices, reaching x=2 with probability 1/2 * 1/2. +TYPED_TEST(SolutionBoundsTest, MdpUntilProbabilitiesWithEndComponent) { + typedef typename TestFixture::ValueType ValueType; + std::string const programString = R"(mdp +module main + x : [0..3]; + [] x=0 -> (x'=0); + [] x=0 -> 0.5 : (x'=1) + 0.5 : (x'=3); + [] x=1 -> (x'=0); + [] x=1 -> 0.5 : (x'=2) + 0.5 : (x'=3); + [] x>=2 -> true; +endmodule +)"; + auto result = + this->template check>(storm::parser::PrismParser::parseFromString(programString, ""), "Pmax=? [F x=2]"); + this->expectEncloses(result, this->parseNumber("1/4")); +} + } // namespace From 31218941f40a01fbd8a2e6308959d494ff1ea496 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 17 Sep 2026 10:25:19 +0200 Subject: [PATCH 15/29] Show the bounds when a result is written as a range Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../results/ExplicitQuantitativeCheckResult.cpp | 16 ++++++++++++++++ .../modelchecker/bounds/SolutionBoundsTest.cpp | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 720a0d107b..9e05a2e112 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -369,6 +369,22 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost if (values.size() >= 10 && minMaxSupported) { std::pair minmax = this->getMinMax(); printRange(out, minmax.first, minmax.second); + if (this->hasLowerBounds() || this->hasUpperBounds()) { + // The smallest lower and the largest upper bound enclose all values, with a dash for a side that is not known. + out << " ["; + if (this->hasLowerBounds()) { + print(out, storm::utility::minmax(this->getLowerBoundVector()).first); + } else { + out << "-"; + } + out << ", "; + if (this->hasUpperBounds()) { + print(out, storm::utility::minmax(this->getUpperBoundVector()).second); + } else { + out << "-"; + } + out << "] (bounds)"; + } } else if (!this->isResultForAllStates() && values.size() == 1) { this->printValue(out, 0); } else { diff --git a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp index 0333a7d9d9..57b26b528a 100644 --- a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp +++ b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp @@ -173,4 +173,18 @@ endmodule this->expectEncloses(result, this->parseNumber("1/4")); } +// A result with ten or more values is written as the range of its values, which then also has to show the bounds. +TEST(SolutionBoundsOutputTest, RangeOutputShowsBounds) { + std::vector values(10, 0.5); + values.front() = 0.25; + storm::modelchecker::ExplicitQuantitativeCheckResult result(values); + std::vector lower(10, 0.4); + lower.front() = 0.2; + result.setLowerBounds(storm::utility::widen(std::move(lower))); + + std::stringstream stream; + result.writeToStream(stream); + EXPECT_EQ("[0.25, 0.5] (range) [0.2, -] (bounds)", stream.str()); +} + } // namespace From 2720ecd1979343f40a918e62fd729ebf86572dc2 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Tue, 22 Sep 2026 10:29:07 +0200 Subject: [PATCH 16/29] Add a check that bounds enclose given values Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- src/storm/solver/SolutionBounds.h | 22 ++ .../bounds/SolutionBoundsTest.cpp | 190 ------------------ 2 files changed, 22 insertions(+), 190 deletions(-) delete mode 100644 src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h index e1d8b781cb..c4863e7d8f 100644 --- a/src/storm/solver/SolutionBounds.h +++ b/src/storm/solver/SolutionBounds.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -35,6 +36,27 @@ struct SolutionBounds { return hasLower() || hasUpper(); } + /* + * Returns true if every one of the given values lies within the bounds that are set. Meant for assertions. + */ + bool enclose(std::vector const& values) const { + auto const isBoundedBy = [&values](std::optional> const& bound, bool fromBelow) { + if (!bound.has_value()) { + return true; + } + if (bound->size() != values.size()) { + return false; + } + for (uint64_t i = 0; i < values.size(); ++i) { + if (fromBelow ? (*bound)[i] > values[i] : (*bound)[i] < values[i]) { + return false; + } + } + return true; + }; + return isBoundedBy(lower, true) && isBoundedBy(upper, false); + } + /* * Clears both bounds. */ diff --git a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp b/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp deleted file mode 100644 index 57b26b528a..0000000000 --- a/src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "storm-config.h" -#include "test/storm_gtest.h" - -#include "storm-parsers/api/model_descriptions.h" -#include "storm-parsers/api/properties.h" -#include "storm-parsers/parser/PrismParser.h" -#include "storm/api/builder.h" -#include "storm/api/properties.h" -#include "storm/environment/solver/MinMaxSolverEnvironment.h" -#include "storm/environment/solver/NativeSolverEnvironment.h" -#include "storm/environment/solver/SolverEnvironment.h" -#include "storm/modelchecker/CheckTask.h" -#include "storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.h" -#include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" -#include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" -#include "storm/models/sparse/Dtmc.h" -#include "storm/models/sparse/Mdp.h" -#include "storm/models/sparse/StandardRewardModel.h" -#include "storm/utility/constants.h" - -/* - * These tests are about the sound bounds that a model checking call reports alongside its values, not about the - * values themselves. The claim under test is the one the bounds make, so every property here has an answer that - * is known in closed form and each case asserts - * - * lower <= exact <= upper and lower <= reported value <= upper. - * - * Every configuration listed below is one that must report both bounds, so a path that silently stops reporting - * them fails these tests rather than passing them vacuously. - */ - -namespace { - -class NativeIiEnvironment { - public: - typedef double ValueType; - static storm::Environment createEnvironment() { - storm::Environment env; - env.solver().setForceSoundness(true); - env.solver().setLinearEquationSolverType(storm::solver::EquationSolverType::Native); - env.solver().native().setMethod(storm::solver::NativeLinearEquationSolverMethod::IntervalIteration); - env.solver().minMax().setMethod(storm::solver::MinMaxMethod::IntervalIteration); - return env; - } -}; - -class NativeOviEnvironment { - public: - typedef double ValueType; - static storm::Environment createEnvironment() { - storm::Environment env; - env.solver().setForceSoundness(true); - env.solver().setLinearEquationSolverType(storm::solver::EquationSolverType::Native); - env.solver().native().setMethod(storm::solver::NativeLinearEquationSolverMethod::OptimisticValueIteration); - env.solver().minMax().setMethod(storm::solver::MinMaxMethod::OptimisticValueIteration); - return env; - } -}; - -template -class SolutionBoundsTest : public ::testing::Test { - public: - typedef typename TestType::ValueType ValueType; - typedef storm::utility::ExtendedValueType ExtendedValueType; - typedef storm::modelchecker::ExplicitQuantitativeCheckResult QuantitativeResult; - - SolutionBoundsTest() : _environment(TestType::createEnvironment()) {} - - void SetUp() override { -#ifndef STORM_HAVE_Z3 - GTEST_SKIP() << "Z3 not available."; -#endif - } - - storm::Environment const& env() const { - return _environment; - } - - ExtendedValueType parseNumber(std::string const& input) const { - return storm::utility::convertNumber(storm::utility::convertNumber(input)); - } - - /*! - * How far outside the reported enclosure the exact answer may still lie. A bound is only ever as sound as the - * arithmetic it is computed in: a procedure working in doubles can land a bound an ulp or two on the wrong - * side of an answer that is not representable at all. That is the rounding we accept, not a gap in the - * reasoning, so the slack is far below the solver precision these configurations ask for. - */ - ExtendedValueType tolerance() const { - return this->parseNumber("1e-12"); - } - - /*! - * Builds the PRISM model in the given file and checks the given property on it. - */ - template - std::unique_ptr check(std::string const& modelFile, std::string const& propertyString) const { - return this->template check(storm::api::parseProgram(modelFile), propertyString); - } - - /*! - * Builds the given PRISM model and checks the given property on it. - */ - template - std::unique_ptr check(storm::prism::Program program, std::string const& propertyString) const { - program = program.preprocess(); - auto formulas = storm::api::extractFormulasFromProperties(storm::api::parsePropertiesForPrismProgram(propertyString, program)); - auto model = storm::api::buildSparseModel(program, formulas)->template as(); - storm::modelchecker::CheckTask task(*formulas.front(), true); - if constexpr (std::is_same_v>) { - return storm::modelchecker::SparseDtmcPrctlModelChecker(*model).check(this->env(), task); - } else { - return storm::modelchecker::SparseMdpPrctlModelChecker(*model).check(this->env(), task); - } - } - - /*! - * Asserts that the bounds the given result reports for the initial state enclose the given exact answer, and - * that the value it reports lies between them as well. - */ - void expectEncloses(std::unique_ptr const& result, ExtendedValueType const& exact) const { - QuantitativeResult const& quantitative = result->template asExplicitQuantitativeCheckResult(); - ASSERT_TRUE(quantitative.hasLowerBounds()) << "No lower bound on the solution was reported at all."; - ASSERT_TRUE(quantitative.hasUpperBounds()) << "No upper bound on the solution was reported at all."; - uint64_t const offset = 0; - ExtendedValueType const& value = quantitative.getValueVector()[offset]; - ExtendedValueType const& lower = quantitative.getLowerBoundVector()[offset]; - ExtendedValueType const& upper = quantitative.getUpperBoundVector()[offset]; - EXPECT_LE(lower, exact + this->tolerance()) << "The lower bound exceeds the exact answer."; - EXPECT_LE(exact, upper + this->tolerance()) << "The upper bound falls below the exact answer."; - EXPECT_LE(lower, value) << "The lower bound exceeds the reported value."; - EXPECT_LE(value, upper) << "The reported value exceeds the upper bound."; - } - - private: - storm::Environment _environment; -}; - -typedef ::testing::Types TestingTypes; - -TYPED_TEST_SUITE(SolutionBoundsTest, TestingTypes, ); - -// The Knuth-Yao die yields a one with probability exactly 1/6. -TYPED_TEST(SolutionBoundsTest, DtmcUntilProbabilities) { - typedef typename TestFixture::ValueType ValueType; - auto result = this->template check>(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "P=? [F s=7 & d=1]"); - this->expectEncloses(result, this->parseNumber("1/6")); -} - -TYPED_TEST(SolutionBoundsTest, MdpUntilProbabilities) { - typedef typename TestFixture::ValueType ValueType; - auto result = this->template check>(STORM_TEST_RESOURCES_DIR "/mdp/coin2-2.nm", - "Pmin=? [F \"finished\" & \"all_coins_equal_1\"]"); - this->expectEncloses(result, this->parseNumber("49/128")); -} - -// Maximizing makes the sound methods eliminate the end component {x=0} of the maybe states, so the bounds come from the quotient system. -// The best scheduler leaves x=0 and x=1 through their probabilistic choices, reaching x=2 with probability 1/2 * 1/2. -TYPED_TEST(SolutionBoundsTest, MdpUntilProbabilitiesWithEndComponent) { - typedef typename TestFixture::ValueType ValueType; - std::string const programString = R"(mdp -module main - x : [0..3]; - [] x=0 -> (x'=0); - [] x=0 -> 0.5 : (x'=1) + 0.5 : (x'=3); - [] x=1 -> (x'=0); - [] x=1 -> 0.5 : (x'=2) + 0.5 : (x'=3); - [] x>=2 -> true; -endmodule -)"; - auto result = - this->template check>(storm::parser::PrismParser::parseFromString(programString, ""), "Pmax=? [F x=2]"); - this->expectEncloses(result, this->parseNumber("1/4")); -} - -// A result with ten or more values is written as the range of its values, which then also has to show the bounds. -TEST(SolutionBoundsOutputTest, RangeOutputShowsBounds) { - std::vector values(10, 0.5); - values.front() = 0.25; - storm::modelchecker::ExplicitQuantitativeCheckResult result(values); - std::vector lower(10, 0.4); - lower.front() = 0.2; - result.setLowerBounds(storm::utility::widen(std::move(lower))); - - std::stringstream stream; - result.writeToStream(stream); - EXPECT_EQ("[0.25, 0.5] (range) [0.2, -] (bounds)", stream.str()); -} - -} // namespace From a0ddbdd25873dbbf641da0b268e4f50a916e116b Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Tue, 22 Sep 2026 10:29:07 +0200 Subject: [PATCH 17/29] Always keep the states a check result is for Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../ExplicitQualitativeCheckResult.cpp | 36 ++++----- .../results/ExplicitQualitativeCheckResult.h | 32 +++----- .../ExplicitQuantitativeCheckResult.cpp | 78 ++++++++----------- .../results/ExplicitQuantitativeCheckResult.h | 36 +++------ 4 files changed, 69 insertions(+), 113 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp index b9a3c995d2..77ab969a57 100644 --- a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp @@ -16,20 +16,20 @@ namespace modelchecker { template ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(storm::storage::sparse::state_type state, bool value) : states(storm::storage::BitVector(state + 1)), truthValues(1, value) { - states->set(state); + states.set(state); } template ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(vector_type const& truthValues, std::optional>> scheduler) - : truthValues(truthValues), scheduler(scheduler) { + : states(truthValues.size(), true), truthValues(truthValues), scheduler(scheduler) { // Intentionally left empty. } template ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(vector_type&& truthValues, std::optional>> scheduler) - : truthValues(std::move(truthValues)), scheduler(scheduler) { + : states(truthValues.size(), true), truthValues(std::move(truthValues)), scheduler(scheduler) { // Intentionally left empty. } @@ -37,7 +37,7 @@ template ExplicitQualitativeCheckResult::ExplicitQualitativeCheckResult(storm::storage::BitVector states, vector_type&& truthValues, std::optional>> scheduler) : states(std::move(states)), truthValues(std::move(truthValues)), scheduler(scheduler) { - STORM_LOG_ASSERT(this->states->getNumberOfSetBits() == this->truthValues.size(), "Expected one truth value per selected state."); + STORM_LOG_ASSERT(this->states.getNumberOfSetBits() == this->truthValues.size(), "Expected one truth value per selected state."); } template @@ -89,16 +89,13 @@ uint64_t ExplicitQualitativeCheckResult::count() const { template bool ExplicitQualitativeCheckResult::hasValueForState(storm::storage::sparse::state_type state) const { - if (states) { - return state < states->size() && states->get(state); - } - return state < truthValues.size(); + return state < states.size() && states.get(state); } template uint64_t ExplicitQualitativeCheckResult::getOffset(storm::storage::sparse::state_type state) const { - STORM_LOG_THROW(this->hasValueForState(state), storm::exceptions::InvalidOperationException, "Unknown key '" << state << "'."); - return states ? states->getNumberOfSetBitsBeforeIndex(state) : state; + STORM_LOG_ASSERT(this->hasValueForState(state), "State '" << state << "' has no value."); + return states.getNumberOfSetBitsBeforeIndex(state); } template @@ -108,9 +105,7 @@ bool ExplicitQualitativeCheckResult::operator[](storm::storage::spars template storm::storage::BitVector const& ExplicitQualitativeCheckResult::getStates() const { - STORM_LOG_THROW(!this->isResultForAllStates(), storm::exceptions::InvalidOperationException, - "Unable to retrieve the states of a result that is for all states."); - return *states; + return states; } template @@ -130,7 +125,7 @@ bool ExplicitQualitativeCheckResult::isExplicit() const { template bool ExplicitQualitativeCheckResult::isResultForAllStates() const { - return !states.has_value(); + return states.full(); } template @@ -141,9 +136,7 @@ bool ExplicitQualitativeCheckResult::isExplicitQualitativeCheckResult template std::ostream& ExplicitQualitativeCheckResult::writeToStream(std::ostream& out) const { if (!this->isResultForAllStates() && truthValues.size() == 1) { - std::ios::fmtflags oldflags(out.flags()); - out << std::boolalpha << truthValues.get(0); - out.flags(oldflags); + out << (truthValues.get(0) ? "true" : "false"); } else if (truthValues.full()) { out << "{true}"; } else if (truthValues.empty()) { @@ -164,8 +157,8 @@ void ExplicitQualitativeCheckResult::filter(QualitativeCheckResult co // Line the filter up with the states this result has truth values for. The two need not span the same range // of states, e.g. if this result holds a truth value for a single state only. - uint64_t const numStates = std::max(filterTruthValues.size(), states ? states->size() : truthValues.size()); - storm::storage::BitVector available = states ? *states : storm::storage::BitVector(truthValues.size(), true); + uint64_t const numStates = std::max(filterTruthValues.size(), states.size()); + storm::storage::BitVector available = states; available.resize(numStates); storm::storage::BitVector selected(filterTruthValues); selected.resize(numStates); @@ -221,9 +214,8 @@ template storm::json ExplicitQualitativeCheckResult::toJson(std::optional const& stateValuations, std::optional const& stateLabels) const { storm::json result; - this->forEachState([&](storm::storage::sparse::state_type state, uint64_t offset) { - insertJsonEntry(result, state, truthValues.get(offset), stateValuations, stateLabels); - }); + this->forEachState( + [&](storm::storage::sparse::state_type state, bool truthValue) { insertJsonEntry(result, state, truthValue, stateValuations, stateLabels); }); return result; } diff --git a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h index 76f67720c8..a25ca255c6 100644 --- a/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h @@ -17,9 +17,9 @@ namespace modelchecker { /*! * A qualitative check result over the states of a sparse model. * - * The result either covers every state of the model or just a subset of them, e.g. after filtering it to the - * initial states. In the latter case the states in question are recorded in a bit vector and the truth values are - * stored compressed, i.e. the i-th truth value belongs to the i-th state selected by that bit vector. + * The states the result is for are recorded in a bit vector, which selects every state of the model unless the + * result was narrowed down, e.g. by filtering it to the initial states. The truth values are stored compressed, + * i.e. the i-th truth value belongs to the i-th state selected by that bit vector. */ template class ExplicitQualitativeCheckResult : public QualitativeCheckResult { @@ -75,13 +75,12 @@ class ExplicitQualitativeCheckResult : public QualitativeCheckResult { /*! * Retrieves the states this result holds truth values for. - * @pre This is not a result for all states. */ storm::storage::BitVector const& getStates() const; /*! - * Retrieves the truth values, one per state this result is for. If this is not a result for all states, the - * i-th truth value belongs to the i-th state selected by getStates(). + * Retrieves the truth values, one per state this result is for. + * The i-th truth value belongs to the i-th state selected by getStates(). */ vector_type const& getTruthValuesVector() const; @@ -116,26 +115,19 @@ class ExplicitQualitativeCheckResult : public QualitativeCheckResult { uint64_t getOffset(storm::storage::sparse::state_type state) const; /*! - * Invokes the given function with the state and the offset of its truth value, for every state this result is - * for. + * Invokes the given function with the state and its truth value, for every state this result is for. */ template void forEachState(Function const& f) const { - if (states) { - uint64_t offset = 0; - for (auto const& state : *states) { - f(state, offset); - ++offset; - } - } else { - for (uint64_t state = 0; state < truthValues.size(); ++state) { - f(state, state); - } + uint64_t offset = 0; + for (auto const& state : states) { + f(state, truthValues.get(offset)); + ++offset; } } - // The states this result holds truth values for, or nothing at all if it is a result for all states. - std::optional states; + // The states this result holds truth values for, which are all states of the model unless it was narrowed down. + storm::storage::BitVector states; // The truth values of the qualitative check result, one per state this result is for. vector_type truthValues; diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 9e05a2e112..4c9ce08ffc 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -19,20 +19,20 @@ namespace modelchecker { template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::sparse::state_type const& state, ExtendedValueType const& value) : states(storm::storage::BitVector(state + 1)), values({value}) { - states->set(state); + states.set(state); } template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(vector_type const& values, std::optional>> scheduler) - : values(values), scheduler(scheduler) { + : states(values.size(), true), values(values), scheduler(scheduler) { // Intentionally left empty. } template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(vector_type&& values, std::optional>> scheduler) - : values(std::move(values)), scheduler(scheduler) { + : states(values.size(), true), values(std::move(values)), scheduler(scheduler) { // Intentionally left empty. } @@ -40,20 +40,20 @@ template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::BitVector states, vector_type&& values, std::optional>> scheduler) : states(std::move(states)), values(std::move(values)), scheduler(scheduler) { - STORM_LOG_ASSERT(this->states->getNumberOfSetBits() == this->values.size(), "Expected one value per selected state."); + STORM_LOG_ASSERT(this->states.getNumberOfSetBits() == this->values.size(), "Expected one value per selected state."); } template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(std::vector const& values) requires(!std::is_same_v, ValueType>) - : values(storm::utility::widen(std::vector(values))) { + : states(values.size(), true), values(storm::utility::widen(std::vector(values))) { // Intentionally left empty. } template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(std::vector&& values) requires(!std::is_same_v, ValueType>) - : values(storm::utility::widen(std::move(values))) { + : states(values.size(), true), values(storm::utility::widen(std::move(values))) { // Intentionally left empty. } @@ -61,7 +61,7 @@ template ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(storm::storage::BitVector states, std::vector&& values) requires(!std::is_same_v, ValueType>) : states(std::move(states)), values(storm::utility::widen(std::move(values))) { - STORM_LOG_ASSERT(this->states->getNumberOfSetBits() == this->values.size(), "Expected one value per selected state."); + STORM_LOG_ASSERT(this->states.getNumberOfSetBits() == this->values.size(), "Expected one value per selected state."); } template @@ -70,12 +70,10 @@ ExplicitQuantitativeCheckResult::ExplicitQuantitativeCheckResult(Expl storm::storage::BitVector const& truthValues = other.getTruthValuesVector(); values.reserve(truthValues.size()); - for (std::size_t i = 0, n = truthValues.size(); i < n; i++) { + for (std::size_t i = 0; i < truthValues.size(); i++) { values.push_back(toValue(truthValues.get(i))); } - if (!other.isResultForAllStates()) { - states = other.getStates(); - } + states = other.getStates(); } template @@ -85,23 +83,18 @@ std::unique_ptr ExplicitQuantitativeCheckResult::clone() template bool ExplicitQuantitativeCheckResult::hasValueForState(storm::storage::sparse::state_type state) const { - if (states) { - return state < states->size() && states->get(state); - } - return state < values.size(); + return state < states.size() && states.get(state); } template uint64_t ExplicitQuantitativeCheckResult::getOffset(storm::storage::sparse::state_type state) const { - STORM_LOG_THROW(this->hasValueForState(state), storm::exceptions::InvalidOperationException, "Unknown key '" << state << "'."); - return states ? states->getNumberOfSetBitsBeforeIndex(state) : state; + STORM_LOG_ASSERT(this->hasValueForState(state), "State '" << state << "' has no value."); + return states.getNumberOfSetBitsBeforeIndex(state); } template storm::storage::BitVector const& ExplicitQuantitativeCheckResult::getStates() const { - STORM_LOG_THROW(!this->isResultForAllStates(), storm::exceptions::InvalidOperationException, - "Unable to retrieve the states of a result that is for all states."); - return *states; + return states; } template @@ -161,43 +154,36 @@ ExplicitQuantitativeCheckResult::getSolutionBounds() const { return bounds; } -template -void ExplicitQuantitativeCheckResult::assertBoundsShape([[maybe_unused]] vector_type const& bounds) const { - STORM_LOG_ASSERT(bounds.size() == values.size(), "Bounds must have the same size as the values."); -} - template void ExplicitQuantitativeCheckResult::setLowerBounds(vector_type lowerBounds) { - this->assertBoundsShape(lowerBounds); + STORM_LOG_ASSERT(lowerBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.lower = std::move(lowerBounds); } template void ExplicitQuantitativeCheckResult::setUpperBounds(vector_type upperBounds) { - this->assertBoundsShape(upperBounds); + STORM_LOG_ASSERT(upperBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.upper = std::move(upperBounds); } template void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) { - if (bounds.hasLower()) { - this->setLowerBounds(std::move(*bounds.lower)); - } - if (bounds.hasUpper()) { - this->setUpperBounds(std::move(*bounds.upper)); - } + STORM_LOG_ASSERT(bounds.enclose(values), "Bounds must have the same size as the values and enclose them."); + this->bounds = std::move(bounds); } template void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) requires(!std::is_same_v, ValueType>) { + storm::solver::SolutionBounds widened; if (bounds.hasLower()) { - this->setLowerBounds(storm::utility::widen(std::move(*bounds.lower))); + widened.lower = storm::utility::widen(std::move(*bounds.lower)); } if (bounds.hasUpper()) { - this->setUpperBounds(storm::utility::widen(std::move(*bounds.upper))); + widened.upper = storm::utility::widen(std::move(*bounds.upper)); } + this->setBounds(std::move(widened)); } template @@ -216,8 +202,8 @@ void ExplicitQuantitativeCheckResult::filter(QualitativeCheckResult c // Line the filter up with the states this result has values for. The two need not span the same range of // states, e.g. if this result holds a value for a single state only. - uint64_t const numStates = std::max(filterTruthValues.size(), states ? states->size() : values.size()); - storm::storage::BitVector available = states ? *states : storm::storage::BitVector(values.size(), true); + uint64_t const numStates = std::max(filterTruthValues.size(), states.size()); + storm::storage::BitVector available = states; available.resize(numStates); storm::storage::BitVector selected(filterTruthValues); selected.resize(numStates); @@ -373,13 +359,13 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost // The smallest lower and the largest upper bound enclose all values, with a dash for a side that is not known. out << " ["; if (this->hasLowerBounds()) { - print(out, storm::utility::minmax(this->getLowerBoundVector()).first); + print(out, storm::utility::minimum(this->getLowerBoundVector())); } else { out << "-"; } out << ", "; if (this->hasUpperBounds()) { - print(out, storm::utility::minmax(this->getUpperBoundVector()).second); + print(out, storm::utility::maximum(this->getUpperBoundVector())); } else { out << "-"; } @@ -433,11 +419,7 @@ std::unique_ptr ExplicitQuantitativeCheckResult::compare result.set(offset); } } - if (this->isResultForAllStates()) { - return std::unique_ptr(new ExplicitQualitativeCheckResult(std::move(result), scheduler)); - } else { - return std::unique_ptr(new ExplicitQualitativeCheckResult(*states, std::move(result), scheduler)); - } + return std::unique_ptr(new ExplicitQualitativeCheckResult(states, std::move(result), scheduler)); } template<> @@ -466,7 +448,7 @@ bool ExplicitQuantitativeCheckResult::isExplicit() const { template bool ExplicitQuantitativeCheckResult::isResultForAllStates() const { - return !states.has_value(); + return states.full(); } template @@ -533,10 +515,12 @@ template storm::json ExplicitQuantitativeCheckResult::toJson(std::optional const& stateValuations, std::optional const& stateLabels) const { storm::json result; - this->forEachState([&](storm::storage::sparse::state_type state, uint64_t offset) { - insertJsonEntry(result, state, values[offset], stateValuations, stateLabels, + uint64_t offset = 0; + this->forEachState([&](storm::storage::sparse::state_type state, ExtendedValueType const& value) { + insertJsonEntry(result, state, value, stateValuations, stateLabels, this->hasLowerBounds() ? std::make_optional(this->getLowerBoundVector()[offset]) : std::nullopt, this->hasUpperBounds() ? std::make_optional(this->getUpperBoundVector()[offset]) : std::nullopt); + ++offset; }); return result; } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h index 3faf4c83ec..6eefea652c 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h @@ -22,9 +22,9 @@ class ExplicitQualitativeCheckResult; /*! * A quantitative check result over the states of a sparse model. * - * The result either covers every state of the model or just a subset of them, e.g. after filtering it to the - * initial states. In the latter case the states in question are recorded in a bit vector and the values are - * stored compressed, i.e. the i-th value belongs to the i-th state selected by that bit vector. + * The states the result is for are recorded in a bit vector, which selects every state of the model unless the + * result was narrowed down, e.g. by filtering it to the initial states. The values are stored compressed, i.e. + * the i-th value belongs to the i-th state selected by that bit vector. */ template class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult { @@ -93,13 +93,12 @@ class ExplicitQuantitativeCheckResult : public QuantitativeCheckResult void forEachState(Function const& f) const { - if (states) { - uint64_t offset = 0; - for (auto const& state : *states) { - f(state, offset); - ++offset; - } - } else { - for (uint64_t state = 0; state < values.size(); ++state) { - f(state, state); - } + uint64_t offset = 0; + for (auto const& state : states) { + f(state, values[offset]); + ++offset; } } - /*! - * Asserts that the given bounds have the same shape as the values. - */ - void assertBoundsShape(vector_type const& bounds) const; - /*! * Writes the value stored at the given offset, followed by its bounds if any are known. */ void printValue(std::ostream& out, uint64_t offset) const; - // The states this result holds values for, or nothing at all if it is a result for all states. - std::optional states; + // The states this result holds values for, which are all states of the model unless it was narrowed down. + storm::storage::BitVector states; // The values of the quantitative check result, one per state this result is for. These are estimates that // lie within the bounds below but carry no further guarantee. From 606eb847edbfe6a9f5ef5bdec766e4dd6ae54a39 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Tue, 22 Sep 2026 10:29:07 +0200 Subject: [PATCH 18/29] Check the solution bounds in the model checking tests Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../mdp/maybe_state_end_component.nm | 15 ++ src/test/storm/CMakeLists.txt | 2 +- .../prctl/dtmc/DtmcPrctlModelCheckerTest.cpp | 88 +++++++---- .../prctl/dtmc/ExportCheckResultTest.cpp | 16 ++ .../prctl/mdp/MdpPrctlModelCheckerTest.cpp | 147 +++++++++++------- 5 files changed, 180 insertions(+), 88 deletions(-) create mode 100644 resources/examples/testfiles/mdp/maybe_state_end_component.nm diff --git a/resources/examples/testfiles/mdp/maybe_state_end_component.nm b/resources/examples/testfiles/mdp/maybe_state_end_component.nm new file mode 100644 index 0000000000..94d48918e3 --- /dev/null +++ b/resources/examples/testfiles/mdp/maybe_state_end_component.nm @@ -0,0 +1,15 @@ +// An MDP whose maybe states contain an end component: staying in x=0 or x=1 forever is a choice, so a sound +// method has to eliminate that end component before it can solve for the maximal probability of reaching x=2. +mdp + +module main + x : [0..3]; + + [] x=0 -> (x'=0); + [] x=0 -> 0.5 : (x'=1) + 0.5 : (x'=3); + [] x=1 -> (x'=0); + [] x=1 -> 0.5 : (x'=2) + 0.5 : (x'=3); + [] x>=2 -> true; +endmodule + +label "target" = x=2; diff --git a/src/test/storm/CMakeLists.txt b/src/test/storm/CMakeLists.txt index dec5a7474b..fc5ea78048 100755 --- a/src/test/storm/CMakeLists.txt +++ b/src/test/storm/CMakeLists.txt @@ -6,7 +6,7 @@ include_directories(${GTEST_INCLUDE_DIR}) # Set split and non-split test directories set(NON_SPLIT_TESTS adapter automata builder logic model parser settings simulator solver storage transformer utility) -set(MODELCHECKER_TEST_SPLITS bounds csl exploration lexicographic multiobjective reachability) +set(MODELCHECKER_TEST_SPLITS csl exploration lexicographic multiobjective reachability) set(MODELCHECKER_PRCTL_TEST_SPLITS dtmc mdp) function(configure_testsuite_target testsuite) diff --git a/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp b/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp index 6338cefca2..8e6dc39318 100644 --- a/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp +++ b/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp @@ -19,6 +19,7 @@ #include "storm/modelchecker/prctl/SymbolicDtmcPrctlModelChecker.h" #include "storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h" #include "storm/modelchecker/results/ExplicitQualitativeCheckResult.h" +#include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" #include "storm/modelchecker/results/QualitativeCheckResult.h" #include "storm/modelchecker/results/QuantitativeCheckResult.h" #include "storm/modelchecker/results/SymbolicQualitativeCheckResult.h" @@ -640,6 +641,27 @@ class DtmcPrctlModelCheckerTest : public ::testing::Test { return result->asQuantitativeCheckResult().getMin(); } + /*! + * Expects the value at the initial states to be the given one. Where the configuration reports sound bounds on + * the solution, they must enclose that value as well. + */ + void expectQuantitativeResultAtInitialState(std::shared_ptr> const& model, + std::unique_ptr& result, ValueType const& expected) { + EXPECT_NEAR(expected, this->getQuantitativeResultAtInitialState(model, result), this->precision()); + if (!result->isExplicitQuantitativeCheckResult()) { + return; // Only explicit results carry bounds. + } + // The result is filtered to the initial states at this point, so its bounds are those of the initial states. + auto const& explicitResult = result->template asExplicitQuantitativeCheckResult(); + ValueType const tolerance = TestType::isExact ? this->parseNumber("0") : this->parseNumber("1e-12"); + if (explicitResult.hasLowerBounds()) { + EXPECT_LE(storm::utility::minimum(explicitResult.getLowerBoundVector()), expected + tolerance) << "The lower bound exceeds the expected value."; + } + if (explicitResult.hasUpperBounds()) { + EXPECT_LE(expected, storm::utility::maximum(explicitResult.getUpperBoundVector()) + tolerance) << "The upper bound falls below the expected value."; + } + } + private: storm::Environment _environment; @@ -685,16 +707,16 @@ TYPED_TEST(DtmcPrctlModelCheckerTest, Die) { std::unique_ptr result; result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(this->env(), tasks[3]); - EXPECT_NEAR(this->parseNumber("11/3"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("11/3")); }); } @@ -714,13 +736,13 @@ STORM_EXPENSIVE_TYPED_TEST(DtmcPrctlModelCheckerTest, Crowds) { std::unique_ptr result; result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("78686542099694893/1268858272000000000"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("78686542099694893/1268858272000000000")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("40300855878315123/1268858272000000000"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("40300855878315123/1268858272000000000")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("13433618626105041/1268858272000000000"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("13433618626105041/1268858272000000000")); }); } @@ -740,13 +762,13 @@ TYPED_TEST(DtmcPrctlModelCheckerTest, SynchronousLeader) { std::unique_ptr result; result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("24/25"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("24/25")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("25/24"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("25/24")); }); } @@ -867,25 +889,25 @@ TYPED_TEST(DtmcPrctlModelCheckerTest, LtlProbabilitiesDie) { // LTL not supported in all engines (Hybrid, PrismDd, JaniDd) if (TypeParam::engine == DtmcEngine::PrismSparse || TypeParam::engine == DtmcEngine::JaniSparse) { result = checker->check(tasks[0]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(tasks[1]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(tasks[2]); - EXPECT_NEAR(this->parseNumber("1/24"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/24")); result = checker->check(tasks[3]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(tasks[4]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); result = checker->check(tasks[5]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(tasks[6]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); } @@ -916,19 +938,19 @@ TYPED_TEST(DtmcPrctlModelCheckerTest, LtlProbabilitiesSynchronousLeader) { // LTL not supported in all engines (Hybrid, PrismDd, JaniDd) if (TypeParam::engine == DtmcEngine::PrismSparse || TypeParam::engine == DtmcEngine::JaniSparse) { result = checker->check(tasks[0]); - EXPECT_NEAR(this->parseNumber("16/25"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("16/25")); result = checker->check(tasks[1]); - EXPECT_NEAR(this->parseNumber("9/25"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("9/25")); result = checker->check(tasks[2]); - EXPECT_NEAR(this->parseNumber("1/25"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/25")); result = checker->check(tasks[3]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); result = checker->check(tasks[4]); - EXPECT_NEAR(this->parseNumber("1/5"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/5")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); @@ -968,22 +990,22 @@ TYPED_TEST(DtmcPrctlModelCheckerTest, HOAProbabilitiesDie) { // Not supported in all engines (Hybrid, PrismDd, JaniDd) if (TypeParam::engine == DtmcEngine::PrismSparse || TypeParam::engine == DtmcEngine::JaniSparse) { result = checker->check(tasks[0]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(tasks[1]); - EXPECT_NEAR(this->parseNumber("1/3"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/3")); result = checker->check(tasks[2]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(tasks[3]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(tasks[4]); - EXPECT_NEAR(this->parseNumber("1/8"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/8")); result = checker->check(tasks[5]); - EXPECT_NEAR(this->parseNumber("1/3"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/3")); result = checker->check(tasks[6]); EXPECT_TRUE(this->getQualitativeResultAtInitialState(model, result)); @@ -1012,16 +1034,16 @@ TYPED_TEST(DtmcPrctlModelCheckerTest, SmallDiscount) { if (TypeParam::engine == DtmcEngine::PrismSparse || TypeParam::engine == DtmcEngine::JaniSparse) { std::unique_ptr result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("5"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("5")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("45/14"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("45/14")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("15/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("15/4")); result = checker->check(this->env(), tasks[3]); - EXPECT_NEAR(this->parseNumber("12591/6250"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("12591/6250")); result = checker->check(this->env(), tasks[4]); - EXPECT_NEAR(this->parseNumber("555/256"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("555/256")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); EXPECT_FALSE(checker->canHandle(tasks[1])); diff --git a/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp b/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp index d2942b0b58..95934800af 100644 --- a/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp +++ b/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp @@ -182,6 +182,22 @@ TEST(ExportCheckResultTest, PrintsNoApproximationOfAnInexactValue) { EXPECT_EQ(std::string::npos, out.str().find("(approx.")) << "An approximation was printed for " << out.str() << "."; } +/*! + * A result with ten or more values is written as the range of its values, which then also has to show the bounds. + */ +TEST(ExportCheckResultTest, RangeOutputShowsBounds) { + std::vector values(10, 0.5); + values.front() = 0.25; + storm::modelchecker::ExplicitQuantitativeCheckResult result(values); + std::vector lower(10, 0.4); + lower.front() = 0.2; + result.setLowerBounds(storm::utility::widen(std::move(lower))); + + std::stringstream out; + result.writeToStream(out); + EXPECT_EQ("[0.25, 0.5] (range) [0.2, -] (bounds)", out.str()); +} + TEST(ExportCheckResultTest, InfiniteRewardDouble) { storm::Environment env; runExportTest(env); diff --git a/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp b/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp index ecf921cbb6..fb692b377f 100644 --- a/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp @@ -15,6 +15,7 @@ #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/prctl/SymbolicMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQualitativeCheckResult.h" +#include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" #include "storm/modelchecker/results/QualitativeCheckResult.h" #include "storm/modelchecker/results/QuantitativeCheckResult.h" #include "storm/modelchecker/results/SymbolicQualitativeCheckResult.h" @@ -665,6 +666,27 @@ class MdpPrctlModelCheckerTest : public ::testing::Test { return result->asQuantitativeCheckResult().getMin(); } + /*! + * Expects the value at the initial states to be the given one. Where the configuration reports sound bounds on + * the solution, they must enclose that value as well. + */ + void expectQuantitativeResultAtInitialState(std::shared_ptr> const& model, + std::unique_ptr& result, ValueType const& expected) { + EXPECT_NEAR(expected, this->getQuantitativeResultAtInitialState(model, result), this->precision()); + if (!result->isExplicitQuantitativeCheckResult()) { + return; // Only explicit results carry bounds. + } + // The result is filtered to the initial states at this point, so its bounds are those of the initial states. + auto const& explicitResult = result->template asExplicitQuantitativeCheckResult(); + ValueType const tolerance = TestType::isExact ? this->parseNumber("0") : this->parseNumber("1e-12"); + if (explicitResult.hasLowerBounds()) { + EXPECT_LE(storm::utility::minimum(explicitResult.getLowerBoundVector()), expected + tolerance) << "The lower bound exceeds the expected value."; + } + if (explicitResult.hasUpperBounds()) { + EXPECT_LE(expected, storm::utility::maximum(explicitResult.getUpperBoundVector()) + tolerance) << "The upper bound falls below the expected value."; + } + } + private: storm::Environment _environment; @@ -715,28 +737,28 @@ TYPED_TEST(MdpPrctlModelCheckerTest, Dice) { std::unique_ptr result; result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/36")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("1/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/36")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("2/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("2/36")); result = checker->check(this->env(), tasks[3]); - EXPECT_NEAR(this->parseNumber("2/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("2/36")); result = checker->check(this->env(), tasks[4]); - EXPECT_NEAR(this->parseNumber("3/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("3/36")); result = checker->check(this->env(), tasks[5]); - EXPECT_NEAR(this->parseNumber("3/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("3/36")); result = checker->check(this->env(), tasks[6]); - EXPECT_NEAR(this->parseNumber("22/3"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("22/3")); result = checker->check(this->env(), tasks[7]); - EXPECT_NEAR(this->parseNumber("22/3"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("22/3")); }); } @@ -759,25 +781,42 @@ STORM_EXPENSIVE_TYPED_TEST(MdpPrctlModelCheckerTest, AsynchronousLeader) { std::unique_ptr result; result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("1"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("1/16"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/16")); result = checker->check(this->env(), tasks[3]); - EXPECT_NEAR(this->parseNumber("1/16"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/16")); result = checker->check(this->env(), tasks[4]); - EXPECT_NEAR(this->parseNumber("30/7"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("30/7")); result = checker->check(this->env(), tasks[5]); - EXPECT_NEAR(this->parseNumber("30/7"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("30/7")); }); } +TYPED_TEST(MdpPrctlModelCheckerTest, MaybeStateEndComponent) { + // The best scheduler leaves x=0 and x=1 through their probabilistic choices, reaching the target with + // probability 1/2 * 1/2. + if (!this->isSparseModel()) { + GTEST_SKIP() << "The sound methods of the symbolic engines answer 1/2 on this model."; + } + auto modelFormulas = this->buildModelFormulas(STORM_TEST_RESOURCES_DIR "/mdp/maybe_state_end_component.nm", "Pmax=? [F \"target\"]"); + auto model = std::move(modelFormulas.first); + auto tasks = this->getTasks(modelFormulas.second); + EXPECT_EQ(4ul, model->getNumberOfStates()); + ASSERT_EQ(model->getType(), storm::models::ModelType::Mdp); + + auto checker = this->createModelChecker(model); + auto result = checker->check(this->env(), tasks[0]); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/4")); +} + STORM_EXPENSIVE_TYPED_TEST(MdpPrctlModelCheckerTest, consensus) { std::string formulasString = "Pmax=? [F \"finished\"]"; formulasString += "; Pmax=? [F \"all_coins_equal_1\"]"; @@ -799,10 +838,10 @@ STORM_EXPENSIVE_TYPED_TEST(MdpPrctlModelCheckerTest, consensus) { std::unique_ptr result; result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("57/64"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("57/64")); result = checker->check(this->env(), tasks[2]); EXPECT_FALSE(this->getQualitativeResultAtInitialState(model, result)); @@ -817,10 +856,10 @@ STORM_EXPENSIVE_TYPED_TEST(MdpPrctlModelCheckerTest, consensus) { EXPECT_TRUE(storm::utility::isInfinity(this->getQuantitativeResultAtInitialState(model, result))); result = checker->check(this->env(), tasks[6]); - EXPECT_NEAR(this->parseNumber("75"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("75")); result = checker->check(this->env(), tasks[7]); - EXPECT_NEAR(this->parseNumber("48"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("48")); }); } @@ -843,7 +882,7 @@ TYPED_TEST(MdpPrctlModelCheckerTest, TinyRewards) { STORM_SILENT_EXPECT_THROW(checker->check(this->env(), tasks[0]), storm::exceptions::UncheckedRequirementException); } else { result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1")); } }); } @@ -864,7 +903,7 @@ TYPED_TEST(MdpPrctlModelCheckerTest, Team) { if (TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse) { result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("114/49"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("114/49")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); } @@ -893,10 +932,10 @@ TYPED_TEST(MdpPrctlModelCheckerTest, LtlProbabilitiesCoin) { // LTL not supported in all engines (Hybrid, PrismDd, JaniDd) if (TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse) { result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("4/9"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("4/9")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("5/9"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("5/9")); result = checker->check(this->env(), tasks[2]); EXPECT_TRUE(this->getQualitativeResultAtInitialState(model, result)); @@ -905,7 +944,7 @@ TYPED_TEST(MdpPrctlModelCheckerTest, LtlProbabilitiesCoin) { EXPECT_TRUE(this->getQualitativeResultAtInitialState(model, result)); result = checker->check(this->env(), tasks[4]); - EXPECT_NEAR(this->parseNumber("5/9"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("5/9")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); @@ -938,19 +977,19 @@ TYPED_TEST(MdpPrctlModelCheckerTest, LtlDice) { // LTL not supported in all engines (Hybrid, PrismDd, JaniDd) if (TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse) { result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/6")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("1/24"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/24")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("1/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/36")); result = checker->check(this->env(), tasks[3]); - EXPECT_NEAR(this->parseNumber("5/6"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("5/6")); result = checker->check(this->env(), tasks[4]); - EXPECT_NEAR(this->parseNumber("31/36"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("31/36")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); @@ -984,28 +1023,28 @@ TYPED_TEST(MdpPrctlModelCheckerTest, LtlCoinFlips) { // LTL not supported in all engines (Hybrid, PrismDd, JaniDd) if (TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse) { result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("1021/1024"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1021/1024")); result = checker->check(this->env(), tasks[3]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); result = checker->check(this->env(), tasks[4]); - EXPECT_NEAR(this->parseNumber("1/524288"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/524288")); result = checker->check(this->env(), tasks[5]); - EXPECT_NEAR(this->parseNumber("7/8"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("7/8")); result = checker->check(this->env(), tasks[6]); - EXPECT_NEAR(this->parseNumber("1/2"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/2")); result = checker->check(this->env(), tasks[7]); - EXPECT_NEAR(this->parseNumber("1021/1024"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1021/1024")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); @@ -1035,10 +1074,10 @@ TYPED_TEST(MdpPrctlModelCheckerTest, HOADice) { // Not supported in all engines (Hybrid, PrismDd, JaniDd) if (TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse) { result = checker->check(tasks[0]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); result = checker->check(tasks[1]); - EXPECT_NEAR(this->parseNumber("3/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("3/4")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); } @@ -1062,11 +1101,11 @@ TYPED_TEST(MdpPrctlModelCheckerTest, SmallDiscount) { if (TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse) { std::unique_ptr result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("5"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("5")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("18/5"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("18/5")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("15/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("15/4")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); EXPECT_FALSE(checker->canHandle(tasks[1])); @@ -1091,11 +1130,11 @@ TYPED_TEST(MdpPrctlModelCheckerTest, OneStateDiscounting) { if ((TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse)) { std::unique_ptr result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("1000"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1000")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("40951/100"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("40951/100")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("6513215599/10000000"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("6513215599/10000000")); } else { EXPECT_FALSE(checker->canHandle(tasks[0])); EXPECT_FALSE(checker->canHandle(tasks[1])); @@ -1124,25 +1163,25 @@ TYPED_TEST(MdpPrctlModelCheckerTest, StepBoundedReachability) { if (TypeParam::engine == MdpEngine::PrismSparse || TypeParam::engine == MdpEngine::JaniSparse) { std::unique_ptr result = checker->check(this->env(), tasks[0]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); result = checker->check(this->env(), tasks[1]); - EXPECT_NEAR(this->parseNumber("1/2"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/2")); result = checker->check(this->env(), tasks[2]); - EXPECT_NEAR(this->parseNumber("3/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("3/4")); result = checker->check(this->env(), tasks[3]); - EXPECT_NEAR(this->parseNumber("1/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/4")); result = checker->check(this->env(), tasks[4]); - EXPECT_NEAR(this->parseNumber("3/8"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("3/8")); result = checker->check(this->env(), tasks[5]); - EXPECT_NEAR(this->parseNumber("0"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("0")); result = checker->check(this->env(), tasks[6]); - EXPECT_NEAR(this->parseNumber("1/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/4")); result = checker->check(this->env(), tasks[7]); - EXPECT_NEAR(this->parseNumber("1/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/4")); result = checker->check(this->env(), tasks[8]); - EXPECT_NEAR(this->parseNumber("1/4"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("1/4")); result = checker->check(this->env(), tasks[9]); - EXPECT_NEAR(this->parseNumber("7/8"), this->getQuantitativeResultAtInitialState(model, result), this->precision()); + this->expectQuantitativeResultAtInitialState(model, result, this->parseNumber("7/8")); } } } // namespace \ No newline at end of file From e283ff709f789ff5339a83301541ba4728465219 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Tue, 22 Sep 2026 11:49:06 +0200 Subject: [PATCH 19/29] Fix the smaller findings of the bounds review Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../SMTMinimalLabelSetGenerator.h | 10 +++--- .../abstraction/GameBasedMdpModelChecker.cpp | 13 ++++---- .../csl/SparseCtmcCslModelChecker.cpp | 8 ++--- .../csl/helper/SparseCtmcCslHelper.cpp | 6 ++-- .../helper/ltl/SparseLTLHelper.cpp | 8 ++--- .../DTMCModelCheckingHelperReturnType.h | 4 --- .../prctl/helper/SparseDtmcPrctlHelper.cpp | 31 +++++++++---------- .../ExplicitQuantitativeCheckResult.cpp | 14 +++++---- src/storm/solver/SolutionBounds.h | 12 +++---- 9 files changed, 50 insertions(+), 56 deletions(-) diff --git a/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h b/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h index 2b4b8857ef..9a21bbfa51 100644 --- a/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h +++ b/src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h @@ -1668,9 +1668,9 @@ class SMTMinimalLabelSetGenerator { if (model.isOfType(storm::models::ModelType::Dtmc)) { if (rewardName == boost::none) { results.push_back(storm::utility::zero()); - allStatesResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - env, false, model.getTransitionMatrix(), model.getBackwardTransitions(), phiStates, psiStates, false) - .values); + allStatesResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + env, false, model.getTransitionMatrix(), model.getBackwardTransitions(), phiStates, psiStates, false) + .values; for (auto state : model.getInitialStates()) { STORM_LOG_TRACE("Found probability " << allStatesResult[state]); results.back() = std::max(results.back(), allStatesResult[state]); @@ -1690,10 +1690,10 @@ class SMTMinimalLabelSetGenerator { if (rewardName == boost::none) { results.push_back(storm::utility::zero()); storm::modelchecker::helper::SparseMdpPrctlHelper modelCheckerHelper; - allStatesResult = std::move( + allStatesResult = modelCheckerHelper .computeUntilProbabilities(env, false, model.getTransitionMatrix(), model.getBackwardTransitions(), phiStates, psiStates, false, false) - .values); + .values; for (auto state : model.getInitialStates()) { results.back() = std::max(results.back(), allStatesResult[state]); } diff --git a/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp b/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp index 1f81c3e114..73395e96a0 100644 --- a/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp +++ b/src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp @@ -1287,9 +1287,9 @@ void postProcessStrategies(uint64_t iteration, storm::OptimizationDirection cons } auto dtmcMatrix = dtmcMatrixBuilder.build(); std::vector sanityValues = - std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false) - .values); + storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false) + .values; ValueType maxDiff = storm::utility::zero(); uint64_t maxState = 0; @@ -1326,10 +1326,9 @@ void postProcessStrategies(uint64_t iteration, storm::OptimizationDirection cons } } dtmcMatrix = dtmcMatrixBuilder.build(); - sanityValues = - std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false) - .values); + sanityValues = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + Environment(), storm::solver::SolveGoal(), dtmcMatrix, dtmcMatrix.transpose(), constraintStates, targetStates, false) + .values; maxDiff = storm::utility::zero(); maxState = 0; diff --git a/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp b/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp index 6324d09b07..0794753dc5 100644 --- a/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp +++ b/src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp @@ -94,10 +94,10 @@ std::unique_ptr SparseCtmcCslModelChecker::com std::unique_ptr subResultPointer = this->check(env, pathFormula.getSubformula()); ExplicitQualitativeCheckResult const& subResult = subResultPointer->template asExplicitQualitativeCheckResult(); auto probabilisticTransitions = this->getModel().computeProbabilityMatrix(); - std::vector numericResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( - env, storm::solver::SolveGoal(this->getModel(), checkTask), probabilisticTransitions, - probabilisticTransitions.transpose(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet()) - .values); + std::vector numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeGloballyProbabilities( + env, storm::solver::SolveGoal(this->getModel(), checkTask), probabilisticTransitions, + probabilisticTransitions.transpose(), subResult.getTruthValuesVector(), checkTask.isQualitativeSet()) + .values; return std::unique_ptr(new ExplicitQuantitativeCheckResult(std::move(numericResult))); } diff --git a/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp b/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp index e278b87626..5c12ae78a7 100644 --- a/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp +++ b/src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp @@ -258,9 +258,9 @@ std::vector SparseCtmcCslHelper::computeUntilProbabilities(Environmen storm::storage::SparseMatrix const& backwardTransitions, std::vector const& exitRateVector, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, bool qualitative) { - return std::move(SparseDtmcPrctlHelper::computeUntilProbabilities(env, std::move(goal), computeProbabilityMatrix(rateMatrix, exitRateVector), - backwardTransitions, phiStates, psiStates, qualitative) - .values); + return SparseDtmcPrctlHelper::computeUntilProbabilities(env, std::move(goal), computeProbabilityMatrix(rateMatrix, exitRateVector), + backwardTransitions, phiStates, psiStates, qualitative) + .values; } template diff --git a/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp b/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp index 6d4e3db0db..2c5207425d 100644 --- a/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp +++ b/src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp @@ -314,10 +314,10 @@ std::vector SparseLTLHelper::computeDAPr } } else { - prodNumericResult = std::move(storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( - env, std::move(solveGoalProduct), product->getProductModel().getTransitionMatrix(), - product->getProductModel().getBackwardTransitions(), bvTrue, acceptingStates, this->isQualitativeSet()) - .values); + prodNumericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeUntilProbabilities( + env, std::move(solveGoalProduct), product->getProductModel().getTransitionMatrix(), + product->getProductModel().getBackwardTransitions(), bvTrue, acceptingStates, this->isQualitativeSet()) + .values; } std::vector numericResult = product->projectToOriginalModel(this->_transitionMatrix.getRowGroupCount(), prodNumericResult); diff --git a/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h b/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h index db087f68fd..b021ccc91c 100644 --- a/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h +++ b/src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h @@ -23,10 +23,6 @@ struct DTMCSparseModelCheckingHelperReturnType { // Intentionally left empty. } - virtual ~DTMCSparseModelCheckingHelperReturnType() { - // Intentionally left empty. - } - // The values computed for the states. std::vector values; diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp index 6c7d3bb4b8..1ff53e0f6a 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp @@ -377,18 +377,15 @@ DTMCSparseModelCheckingHelperReturnType SparseDtmcPrctlHelper returnValue = computeUntilProbabilities(env, std::move(goal), transitionMatrix, backwardTransitions, storm::storage::BitVector(transitionMatrix.getRowCount(), true), ~psiStates, qualitative); - for (auto& entry : returnValue.values) { - entry = storm::utility::one() - entry; - } - // Inverse the bounds, lb = 1 - ub and ub = 1 - lb. + storm::utility::vector::subtractFromConstantOneVector(returnValue.values); auto& bounds = returnValue.solutionBounds; - for (auto* bound : {&bounds.lower, &bounds.upper}) { - if (*bound) { - for (auto& entry : **bound) { - entry = storm::utility::one() - entry; - } - } + if (bounds.hasLower()) { + storm::utility::vector::subtractFromConstantOneVector(*bounds.lower); + } + if (bounds.hasUpper()) { + storm::utility::vector::subtractFromConstantOneVector(*bounds.upper); } + // Inverse the bounds, lb = 1 - ub and ub = 1 - lb. std::swap(bounds.lower, bounds.upper); return returnValue; } @@ -744,9 +741,9 @@ SparseDtmcPrctlHelper::computeBaierTra // Start by computing all 'before' states, i.e. the states for which the conditional probability is defined. std::vector probabilitiesToReachConditionStates = - std::move(computeUntilProbabilities(env, storm::solver::SolveGoal(), transitionMatrix, backwardTransitions, - storm::storage::BitVector(transitionMatrix.getRowCount(), true), conditionStates, false) - .values); + computeUntilProbabilities(env, storm::solver::SolveGoal(), transitionMatrix, backwardTransitions, + storm::storage::BitVector(transitionMatrix.getRowCount(), true), conditionStates, false) + .values; result.beforeStates = storm::storage::BitVector(targetStates.size(), true); uint_fast64_t state = 0; @@ -917,10 +914,10 @@ SparseDtmcPrctlHelper::computeConditio } goal.setRelevantValues(std::move(newRelevantValues)); std::vector conditionalProbabilities = - std::move(computeUntilProbabilities(env, std::move(goal), newTransitionMatrix, newTransitionMatrix.transpose(), - storm::storage::BitVector(newTransitionMatrix.getRowCount(), true), transformedModel.targetStates.get(), - qualitative) - .values); + computeUntilProbabilities(env, std::move(goal), newTransitionMatrix, newTransitionMatrix.transpose(), + storm::storage::BitVector(newTransitionMatrix.getRowCount(), true), transformedModel.targetStates.get(), + qualitative) + .values; storm::utility::vector::setVectorValues(result, transformedModel.beforeStates, conditionalProbabilities); } diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 4c9ce08ffc..e34c6cbe59 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -156,19 +156,19 @@ ExplicitQuantitativeCheckResult::getSolutionBounds() const { template void ExplicitQuantitativeCheckResult::setLowerBounds(vector_type lowerBounds) { - STORM_LOG_ASSERT(lowerBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.lower = std::move(lowerBounds); + STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); } template void ExplicitQuantitativeCheckResult::setUpperBounds(vector_type upperBounds) { - STORM_LOG_ASSERT(upperBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.upper = std::move(upperBounds); + STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); } template void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) { - STORM_LOG_ASSERT(bounds.enclose(values), "Bounds must have the same size as the values and enclose them."); + STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); this->bounds = std::move(bounds); } @@ -371,7 +371,7 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost } out << "] (bounds)"; } - } else if (!this->isResultForAllStates() && values.size() == 1) { + } else if (values.size() == 1) { this->printValue(out, 0); } else { out << "{"; @@ -406,10 +406,12 @@ std::unique_ptr ExplicitQuantitativeCheckResult::compare // A comparison is only sound if the bound falls outside the lower and upper bound of the result. if (this->hasLowerBounds() && this->hasUpperBounds()) { - for (uint64_t offset = 0; offset < values.size(); ++offset) { + uint64_t offset = 0; + for (auto const& state : states) { STORM_LOG_WARN_COND(!((*bounds.lower)[offset] < bound && bound < (*bounds.upper)[offset]), "The bound " << bound << " lies between the lower bound " << (*bounds.lower)[offset] << " and the upper bound " - << (*bounds.upper)[offset] << ", so the comparison against it is not decided at state " << offset << "."); + << (*bounds.upper)[offset] << ", so the comparison against it is not decided at state " << state << "."); + ++offset; } } diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h index c4863e7d8f..dda03ff1e6 100644 --- a/src/storm/solver/SolutionBounds.h +++ b/src/storm/solver/SolutionBounds.h @@ -15,31 +15,31 @@ struct SolutionBounds { std::optional> lower; std::optional> upper; - /* + /*! * Returns true if the lower bound is set. */ bool hasLower() const { return lower.has_value(); } - /* + /*! * Returns true if the upper bound is set. */ bool hasUpper() const { return upper.has_value(); } - /* + /*! * Returns true if at least one of the bounds is set. */ bool hasAny() const { return hasLower() || hasUpper(); } - /* + /*! * Returns true if every one of the given values lies within the bounds that are set. Meant for assertions. */ - bool enclose(std::vector const& values) const { + bool encloses(std::vector const& values) const { auto const isBoundedBy = [&values](std::optional> const& bound, bool fromBelow) { if (!bound.has_value()) { return true; @@ -57,7 +57,7 @@ struct SolutionBounds { return isBoundedBy(lower, true) && isBoundedBy(upper, false); } - /* + /*! * Clears both bounds. */ void clear() { From bace44970a291d0d827ba08c2b46d06255b53ef2 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Tue, 22 Sep 2026 11:55:59 +0200 Subject: [PATCH 20/29] Say how the reward bounded values line up with the initial states Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp | 2 ++ src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp index f012044d6f..3ba81adaa0 100644 --- a/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp @@ -98,6 +98,7 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c auto formula = std::make_shared(checkTask.getFormula().asSharedPointer(), opInfo); auto numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeRewardBoundedValues( env, this->getModel(), formula); + // The helper returns one value per initial state, in the order in which the bit vector selects them. return std::unique_ptr(new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } else { STORM_LOG_THROW(pathFormula.hasUpperBound(), storm::exceptions::InvalidPropertyException, "Formula needs to have (a single) upper step bound."); @@ -238,6 +239,7 @@ std::unique_ptr SparseDtmcPrctlModelChecker::c auto formula = std::make_shared(checkTask.getFormula().asSharedPointer(), checkTask.getRewardModel(), opInfo); auto numericResult = storm::modelchecker::helper::SparseDtmcPrctlHelper::computeRewardBoundedValues( env, this->getModel(), formula); + // The helper returns one value per initial state, in the order in which the bit vector selects them. return std::unique_ptr(new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } else { STORM_LOG_THROW(rewardPathFormula.hasIntegerBound(), storm::exceptions::InvalidPropertyException, "Formula needs to have a discrete time bound."); diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 7879db0a88..713292efaf 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -134,6 +134,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com helper::rewardbounded::MultiDimensionalRewardUnfolding rewardUnfolding(this->getModel(), formula); auto numericResult = storm::modelchecker::helper::SparseMdpPrctlHelper::computeRewardBoundedValues( env, checkTask.getOptimizationDirection(), rewardUnfolding, this->getModel().getInitialStates()); + // The helper returns one value per initial state, in the order in which the bit vector selects them. return std::unique_ptr( new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } @@ -317,6 +318,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com helper::rewardbounded::MultiDimensionalRewardUnfolding rewardUnfolding(this->getModel(), formula); auto numericResult = storm::modelchecker::helper::SparseMdpPrctlHelper::computeRewardBoundedValues( env, checkTask.getOptimizationDirection(), rewardUnfolding, this->getModel().getInitialStates()); + // The helper returns one value per initial state, in the order in which the bit vector selects them. return std::unique_ptr(new ExplicitQuantitativeCheckResult(this->getModel().getInitialStates(), std::move(numericResult))); } else { STORM_LOG_THROW(rewardPathFormula.hasIntegerBound(), storm::exceptions::InvalidPropertyException, "Formula needs to have a discrete time bound."); From 1c05cd82e6ddd147896c142110fa8510899b2e0b Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Tue, 22 Sep 2026 12:01:04 +0200 Subject: [PATCH 21/29] Keep averaging the two optimistic value iteration vectors Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../solver/helper/OptimisticValueIterationHelper.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp index 643ce93a3a..efc8802d03 100644 --- a/src/storm/solver/helper/OptimisticValueIterationHelper.cpp +++ b/src/storm/solver/helper/OptimisticValueIterationHelper.cpp @@ -281,21 +281,18 @@ SolverStatus OptimisticValueIterationHelper::OVI( doublePrec -= precision * 1e-6; // be slightly more precise to avoid a good chunk of floating point issues } auto status = OVI(vu, offsets, numIterations, relative, doublePrec, dir, guessValue ? *guessValue : doublePrec, lowerBound, upperBound, iterationCallback); - bool const converged = status == SolverStatus::Converged; if (solutionBounds.has_value()) { // The operand started below the solution and every iteration applies the monotone operator to it, so // vu.first lies below the solution whatever stopped the iteration. solutionBounds->lower = vu.first; // Until the verification phase succeeds vu.second is merely a guess, not an upper bound. - if (converged) { + if (status == SolverStatus::Converged) { solutionBounds->upper = vu.second; } } - if (converged) { - auto two = storm::utility::convertNumber(2.0); - storm::utility::vector::applyPointwise( - vu.first, vu.second, vu.first, [&two](ValueType const& a, ValueType const& b) -> ValueType { return (a + b) / two; }); - } + auto two = storm::utility::convertNumber(2.0); + storm::utility::vector::applyPointwise( + vu.first, vu.second, vu.first, [&two](ValueType const& a, ValueType const& b) -> ValueType { return (a + b) / two; }); // Swap operand and aux vector back to original positions. vu.first.swap(operand); vu.second.swap(auxVector); From 64f8ffc5c70f362005fab5fbf22ec09dcd57656a Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Tue, 22 Sep 2026 12:19:57 +0200 Subject: [PATCH 22/29] Do not require the values to lie within the bounds Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../results/ExplicitQuantitativeCheckResult.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index e34c6cbe59..7208b3389c 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -156,19 +156,20 @@ ExplicitQuantitativeCheckResult::getSolutionBounds() const { template void ExplicitQuantitativeCheckResult::setLowerBounds(vector_type lowerBounds) { + STORM_LOG_ASSERT(lowerBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.lower = std::move(lowerBounds); - STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); } template void ExplicitQuantitativeCheckResult::setUpperBounds(vector_type upperBounds) { + STORM_LOG_ASSERT(upperBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.upper = std::move(upperBounds); - STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); } template void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) { - STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); + STORM_LOG_ASSERT(!bounds.hasLower() || bounds.lower->size() == values.size(), "Bounds must have the same size as the values."); + STORM_LOG_ASSERT(!bounds.hasUpper() || bounds.upper->size() == values.size(), "Bounds must have the same size as the values."); this->bounds = std::move(bounds); } From 7719ccd0c2699a2be9d7262ac5c0933fcc935ea5 Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Wed, 23 Sep 2026 15:54:34 +0200 Subject: [PATCH 23/29] Read the bounds of the initial state directly in the tests Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp | 6 +++--- .../modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp b/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp index 8e6dc39318..865b471c21 100644 --- a/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp +++ b/src/test/storm/modelchecker/prctl/dtmc/DtmcPrctlModelCheckerTest.cpp @@ -651,14 +651,14 @@ class DtmcPrctlModelCheckerTest : public ::testing::Test { if (!result->isExplicitQuantitativeCheckResult()) { return; // Only explicit results carry bounds. } - // The result is filtered to the initial states at this point, so its bounds are those of the initial states. + // The result is filtered to the initial states at this point, so the first entry belongs to the first initial state. auto const& explicitResult = result->template asExplicitQuantitativeCheckResult(); ValueType const tolerance = TestType::isExact ? this->parseNumber("0") : this->parseNumber("1e-12"); if (explicitResult.hasLowerBounds()) { - EXPECT_LE(storm::utility::minimum(explicitResult.getLowerBoundVector()), expected + tolerance) << "The lower bound exceeds the expected value."; + EXPECT_LE(explicitResult.getLowerBoundVector().front(), expected + tolerance) << "The lower bound exceeds the expected value."; } if (explicitResult.hasUpperBounds()) { - EXPECT_LE(expected, storm::utility::maximum(explicitResult.getUpperBoundVector()) + tolerance) << "The upper bound falls below the expected value."; + EXPECT_LE(expected, explicitResult.getUpperBoundVector().front() + tolerance) << "The upper bound falls below the expected value."; } } diff --git a/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp b/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp index fb692b377f..9a047b4ee5 100644 --- a/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/MdpPrctlModelCheckerTest.cpp @@ -676,14 +676,14 @@ class MdpPrctlModelCheckerTest : public ::testing::Test { if (!result->isExplicitQuantitativeCheckResult()) { return; // Only explicit results carry bounds. } - // The result is filtered to the initial states at this point, so its bounds are those of the initial states. + // The result is filtered to the initial states at this point, so the first entry belongs to the first initial state. auto const& explicitResult = result->template asExplicitQuantitativeCheckResult(); ValueType const tolerance = TestType::isExact ? this->parseNumber("0") : this->parseNumber("1e-12"); if (explicitResult.hasLowerBounds()) { - EXPECT_LE(storm::utility::minimum(explicitResult.getLowerBoundVector()), expected + tolerance) << "The lower bound exceeds the expected value."; + EXPECT_LE(explicitResult.getLowerBoundVector().front(), expected + tolerance) << "The lower bound exceeds the expected value."; } if (explicitResult.hasUpperBounds()) { - EXPECT_LE(expected, storm::utility::maximum(explicitResult.getUpperBoundVector()) + tolerance) << "The upper bound falls below the expected value."; + EXPECT_LE(expected, explicitResult.getUpperBoundVector().front() + tolerance) << "The upper bound falls below the expected value."; } } From f945f74e6c3dc75e30e6f7ca3fd12be08ac91f0c Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Wed, 23 Sep 2026 15:54:34 +0200 Subject: [PATCH 24/29] Revert "Do not require the values to lie within the bounds" This reverts commit 64f8ffc5c70f362005fab5fbf22ec09dcd57656a. --- .../results/ExplicitQuantitativeCheckResult.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 7208b3389c..e34c6cbe59 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -156,20 +156,19 @@ ExplicitQuantitativeCheckResult::getSolutionBounds() const { template void ExplicitQuantitativeCheckResult::setLowerBounds(vector_type lowerBounds) { - STORM_LOG_ASSERT(lowerBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.lower = std::move(lowerBounds); + STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); } template void ExplicitQuantitativeCheckResult::setUpperBounds(vector_type upperBounds) { - STORM_LOG_ASSERT(upperBounds.size() == values.size(), "Bounds must have the same size as the values."); this->bounds.upper = std::move(upperBounds); + STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); } template void ExplicitQuantitativeCheckResult::setBounds(storm::solver::SolutionBounds bounds) { - STORM_LOG_ASSERT(!bounds.hasLower() || bounds.lower->size() == values.size(), "Bounds must have the same size as the values."); - STORM_LOG_ASSERT(!bounds.hasUpper() || bounds.upper->size() == values.size(), "Bounds must have the same size as the values."); + STORM_LOG_ASSERT(bounds.encloses(values), "Bounds must have the same size as the values and enclose them."); this->bounds = std::move(bounds); } From 9d208f682b9a455578a180c0167076440b89df7d Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 24 Sep 2026 10:04:20 +0200 Subject: [PATCH 25/29] Invert probability bounds through a method on SolutionBounds Both the DTMC and the MDP globally-probability computation turned bounds on 1-p back into bounds on p by hand. Move that into the class. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017n6Z1D3qCc4kFkU1moVg5L --- .../prctl/helper/SparseDtmcPrctlHelper.cpp | 10 +--------- .../prctl/helper/SparseMdpPrctlHelper.cpp | 11 +---------- src/storm/solver/SolutionBounds.h | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp index 1ff53e0f6a..17287a7b29 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp @@ -378,15 +378,7 @@ DTMCSparseModelCheckingHelperReturnType SparseDtmcPrctlHelper SparseMdpPrctlHelper() - element; } - // Inverse the bounds, lb = 1 - ub and ub = 1 - lb. - auto& bounds = result.solutionBounds; - for (auto* bound : {&bounds.lower, &bounds.upper}) { - if (*bound) { - for (auto& entry : **bound) { - entry = storm::utility::one() - entry; - } - } - } - std::swap(bounds.lower, bounds.upper); + result.solutionBounds.invertProbabilityBounds(); return result; } } diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h index dda03ff1e6..504153447c 100644 --- a/src/storm/solver/SolutionBounds.h +++ b/src/storm/solver/SolutionBounds.h @@ -1,9 +1,12 @@ #pragma once +#include #include #include #include +#include "storm/utility/constants.h" + namespace storm::solver { /*! @@ -57,6 +60,20 @@ struct SolutionBounds { return isBoundedBy(lower, true) && isBoundedBy(upper, false); } + /*! + * Turns bounds on a probability p into bounds on 1-p, i.e. the new lower bound is one minus the old upper bound and vice versa. + */ + void invertProbabilityBounds() { + auto const oneMinus = [](ValueType const& value) { return storm::utility::one() - value; }; + if (hasLower()) { + std::ranges::transform(*lower, lower->begin(), oneMinus); + } + if (hasUpper()) { + std::ranges::transform(*upper, upper->begin(), oneMinus); + } + std::swap(lower, upper); + } + /*! * Clears both bounds. */ From 7025a492cfa07e29c5e9cba3bfc13099d86ec7aa Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 24 Sep 2026 11:16:54 +0200 Subject: [PATCH 26/29] Review fixes --- .gitignore | 1 - .../modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp | 5 +++++ .../modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp | 5 +++++ .../results/ExplicitQuantitativeCheckResult.cpp | 8 ++++---- src/storm/solver/AbstractEquationSolver.cpp | 7 +++++++ src/storm/solver/AbstractEquationSolver.h | 7 +++++++ src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp | 1 + src/storm/solver/SolutionBounds.h | 8 ++++++++ 8 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 26400a3d2e..a041c170ba 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,3 @@ nbproject/ .idea .vscode *.out -.claude/ diff --git a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp index 17287a7b29..5c44f97bd9 100644 --- a/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp @@ -294,6 +294,11 @@ DTMCSparseModelCheckingHelperReturnType SparseDtmcPrctlHelper returnValue(std::move(result)); returnValue.solutionBounds = std::move(solutionBounds); return returnValue; diff --git a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp index 224c09de55..4faca33f46 100644 --- a/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp +++ b/src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp @@ -814,6 +814,11 @@ MDPSparseModelCheckingHelperReturnType SparseMdpPrctlHelper::printValue(std::ostream& out, u if (this->hasLowerBounds()) { print(out, this->getLowerBoundVector()[offset]); } else { - out << "-"; + out << "?"; } out << ", "; if (this->hasUpperBounds()) { print(out, this->getUpperBoundVector()[offset]); } else { - out << "-"; + out << "?"; } out << "]"; } @@ -361,13 +361,13 @@ std::ostream& ExplicitQuantitativeCheckResult::writeToStream(std::ost if (this->hasLowerBounds()) { print(out, storm::utility::minimum(this->getLowerBoundVector())); } else { - out << "-"; + out << "?"; } out << ", "; if (this->hasUpperBounds()) { print(out, storm::utility::maximum(this->getUpperBoundVector())); } else { - out << "-"; + out << "?"; } out << "] (bounds)"; } diff --git a/src/storm/solver/AbstractEquationSolver.cpp b/src/storm/solver/AbstractEquationSolver.cpp index 3363796c0e..5e0ed7e679 100644 --- a/src/storm/solver/AbstractEquationSolver.cpp +++ b/src/storm/solver/AbstractEquationSolver.cpp @@ -269,6 +269,13 @@ void AbstractEquationSolver::setSolutionBounds(SolutionBounds +void AbstractEquationSolver::setSolutionBoundsExact(std::vector const& x) const { + SolutionBounds bounds; + bounds.setExact(x); + this->setSolutionBounds(std::move(bounds)); +} + template void AbstractEquationSolver::clearSolutionBounds() const { solutionBounds.clear(); diff --git a/src/storm/solver/AbstractEquationSolver.h b/src/storm/solver/AbstractEquationSolver.h index cd1e0c843f..e178c9de8a 100644 --- a/src/storm/solver/AbstractEquationSolver.h +++ b/src/storm/solver/AbstractEquationSolver.h @@ -232,6 +232,13 @@ class AbstractEquationSolver { */ void setSolutionBounds(SolutionBounds bounds) const; + /*! + * Reports the given solution as an exact one, i.e. stores it as both the lower and the upper bound. + * + * @param x The computed solution. + */ + void setSolutionBoundsExact(std::vector const& x) const; + /*! * Discards any bounds on the solution obtained by a previous call. This must happen whenever solving * starts, so that a solver that is reused does not report stale bounds. diff --git a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp index c7e631cb65..e1f69f823b 100644 --- a/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp +++ b/src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp @@ -571,6 +571,7 @@ bool IterativeMinMaxLinearEquationSolver::solveEquation if (this->isTrackSchedulerSet()) { this->schedulerChoices = std::vector(x.size(), 0); } + this->setSolutionBoundsExact(x); return true; } diff --git a/src/storm/solver/SolutionBounds.h b/src/storm/solver/SolutionBounds.h index 504153447c..dc0321bfa9 100644 --- a/src/storm/solver/SolutionBounds.h +++ b/src/storm/solver/SolutionBounds.h @@ -60,6 +60,14 @@ struct SolutionBounds { return isBoundedBy(lower, true) && isBoundedBy(upper, false); } + /*! + * Sets both bounds to the given values, i.e. states that these values are known exactly. + */ + void setExact(std::vector const& values) { + lower = values; + upper = values; + } + /*! * Turns bounds on a probability p into bounds on 1-p, i.e. the new lower bound is one minus the old upper bound and vice versa. */ From ffdd3e47a8ff4167666a3d7511cca3d6b8bd725e Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 24 Sep 2026 11:24:52 +0200 Subject: [PATCH 27/29] Fix infinity avg and sum bug. --- .../ExplicitQuantitativeCheckResult.cpp | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp index 4f1d636d31..5ba2dd575b 100644 --- a/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp +++ b/src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp @@ -245,11 +245,27 @@ template typename ExplicitQuantitativeCheckResult::ExtendedValueType ExplicitQuantitativeCheckResult::sum() const { STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Sum of empty set is not defined."); + // Infinities are kept out of the running sum, as adding them to it is either a no-op or, for the types that + // carry infinity as a separate kind, an error. They only decide what the sum is once all values are seen. + bool hasPositiveInfinity = false; + bool hasNegativeInfinity = false; ExtendedValueType sum = storm::utility::zero(); for (auto const& element : values) { - STORM_LOG_THROW(!storm::utility::isInfinity(element), storm::exceptions::InvalidOperationException, - "Cannot compute the sum of values containing infinity."); - sum += element; + if (storm::utility::isInfinity(element)) { + hasPositiveInfinity = true; + } else if (storm::utility::isNegativeInfinity(element)) { + hasNegativeInfinity = true; + } else { + sum += element; + } + } + STORM_LOG_THROW(!hasPositiveInfinity || !hasNegativeInfinity, storm::exceptions::InvalidOperationException, + "Cannot compute the sum of values containing both infinity and -infinity."); + if (hasPositiveInfinity) { + return storm::utility::positiveInfinity(); + } + if (hasNegativeInfinity) { + return storm::utility::negativeInfinity(); } return sum; } @@ -258,13 +274,12 @@ template typename ExplicitQuantitativeCheckResult::ExtendedValueType ExplicitQuantitativeCheckResult::average() const { STORM_LOG_THROW(!values.empty(), storm::exceptions::InvalidOperationException, "Average of empty set is not defined."); - ExtendedValueType sum = storm::utility::zero(); - for (auto const& element : values) { - STORM_LOG_THROW(!storm::utility::isInfinity(element), storm::exceptions::InvalidOperationException, - "Cannot compute the average of values containing infinity."); - sum += element; + ExtendedValueType const total = sum(); + if (storm::utility::isInfinity(total) || storm::utility::isNegativeInfinity(total)) { + // Dividing an infinite sum by the finite number of values leaves it unchanged. + return total; } - return sum / storm::utility::convertNumber(values.size()); + return total / storm::utility::convertNumber(values.size()); } template From ecdf84df29f3300a3c7f1a20513dd9564a6d3b0a Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 24 Sep 2026 12:58:51 +0200 Subject: [PATCH 28/29] fix test for print bounds --- .../storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp b/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp index 95934800af..126e59e797 100644 --- a/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp +++ b/src/test/storm/modelchecker/prctl/dtmc/ExportCheckResultTest.cpp @@ -195,7 +195,7 @@ TEST(ExportCheckResultTest, RangeOutputShowsBounds) { std::stringstream out; result.writeToStream(out); - EXPECT_EQ("[0.25, 0.5] (range) [0.2, -] (bounds)", out.str()); + EXPECT_EQ("[0.25, 0.5] (range) [0.2, ?] (bounds)", out.str()); } TEST(ExportCheckResultTest, InfiniteRewardDouble) { From 7c5ee66acd4816fb11ad4713d926b80eb667111e Mon Sep 17 00:00:00 2001 From: Luko van der Maas Date: Thu, 24 Sep 2026 15:43:31 +0200 Subject: [PATCH 29/29] Change template param name of abstract equation solver to reflect actual meaning --- src/storm/solver/AbstractEquationSolver.cpp | 220 ++++++++++---------- src/storm/solver/AbstractEquationSolver.h | 72 +++---- 2 files changed, 146 insertions(+), 146 deletions(-) diff --git a/src/storm/solver/AbstractEquationSolver.cpp b/src/storm/solver/AbstractEquationSolver.cpp index 5e0ed7e679..2d2c717e71 100644 --- a/src/storm/solver/AbstractEquationSolver.cpp +++ b/src/storm/solver/AbstractEquationSolver.cpp @@ -10,45 +10,45 @@ namespace storm { namespace solver { -template -AbstractEquationSolver::AbstractEquationSolver() { +template +AbstractEquationSolver::AbstractEquationSolver() { // Intentionally left empty. Call setShowProgress() after construction to enable progress. } -template -void AbstractEquationSolver::setShowProgress(bool verbose, uint64_t delay) { +template +void AbstractEquationSolver::setShowProgress(bool verbose, uint64_t delay) { if (verbose) { this->progressMeasurement = storm::utility::ProgressMeasurement("iterations", delay); } } -template -void AbstractEquationSolver::setTerminationCondition(std::unique_ptr> terminationCondition) { +template +void AbstractEquationSolver::setTerminationCondition(std::unique_ptr> terminationCondition) { this->terminationCondition = std::move(terminationCondition); } -template -void AbstractEquationSolver::resetTerminationCondition() { +template +void AbstractEquationSolver::resetTerminationCondition() { this->terminationCondition = nullptr; } -template -bool AbstractEquationSolver::hasCustomTerminationCondition() const { +template +bool AbstractEquationSolver::hasCustomTerminationCondition() const { return static_cast(this->terminationCondition); } -template -TerminationCondition const& AbstractEquationSolver::getTerminationCondition() const { +template +TerminationCondition const& AbstractEquationSolver::getTerminationCondition() const { return *terminationCondition; } -template -std::unique_ptr> const& AbstractEquationSolver::getTerminationConditionPointer() const { +template +std::unique_ptr> const& AbstractEquationSolver::getTerminationConditionPointer() const { return terminationCondition; } -template -bool AbstractEquationSolver::terminateNow(std::vector const& values, SolverGuarantee const& guarantee) const { +template +bool AbstractEquationSolver::terminateNow(std::vector const& values, SolverGuarantee const& guarantee) const { if (!this->hasCustomTerminationCondition()) { return false; } @@ -56,38 +56,38 @@ bool AbstractEquationSolver::terminateNow(std::vector cons return this->getTerminationCondition().terminateNow(values, guarantee); } -template -bool AbstractEquationSolver::hasRelevantValues() const { +template +bool AbstractEquationSolver::hasRelevantValues() const { return static_cast(relevantValues); } -template -storm::storage::BitVector const& AbstractEquationSolver::getRelevantValues() const { +template +storm::storage::BitVector const& AbstractEquationSolver::getRelevantValues() const { return relevantValues.get(); } -template -boost::optional const& AbstractEquationSolver::getOptionalRelevantValues() const { +template +boost::optional const& AbstractEquationSolver::getOptionalRelevantValues() const { return relevantValues; } -template -void AbstractEquationSolver::setRelevantValues(storm::storage::BitVector&& relevantValues) { +template +void AbstractEquationSolver::setRelevantValues(storm::storage::BitVector&& relevantValues) { this->relevantValues = std::move(relevantValues); } -template -void AbstractEquationSolver::setRelevantValues(storm::storage::BitVector const& relevantValues) { +template +void AbstractEquationSolver::setRelevantValues(storm::storage::BitVector const& relevantValues) { this->relevantValues = relevantValues; } -template -void AbstractEquationSolver::clearRelevantValues() { +template +void AbstractEquationSolver::clearRelevantValues() { relevantValues = boost::none; } -template -bool AbstractEquationSolver::hasLowerBound(BoundType const& type) const { +template +bool AbstractEquationSolver::hasLowerBound(BoundType const& type) const { if (type == BoundType::Any) { return static_cast(lowerBound) || static_cast(lowerBounds); } else if (type == BoundType::Global) { @@ -98,8 +98,8 @@ bool AbstractEquationSolver::hasLowerBound(BoundType const& type) con return false; } -template -bool AbstractEquationSolver::hasUpperBound(BoundType const& type) const { +template +bool AbstractEquationSolver::hasUpperBound(BoundType const& type) const { if (type == BoundType::Any) { return static_cast(upperBound) || static_cast(upperBounds); } else if (type == BoundType::Global) { @@ -110,29 +110,29 @@ bool AbstractEquationSolver::hasUpperBound(BoundType const& type) con return false; } -template -void AbstractEquationSolver::setLowerBound(ValueType const& value) { +template +void AbstractEquationSolver::setLowerBound(SolutionType const& value) { lowerBound = value; } -template -void AbstractEquationSolver::setUpperBound(ValueType const& value) { +template +void AbstractEquationSolver::setUpperBound(SolutionType const& value) { upperBound = value; } -template -void AbstractEquationSolver::setBounds(ValueType const& lower, ValueType const& upper) { +template +void AbstractEquationSolver::setBounds(SolutionType const& lower, SolutionType const& upper) { setLowerBound(lower); setUpperBound(upper); } -template -ValueType const& AbstractEquationSolver::getLowerBound() const { +template +SolutionType const& AbstractEquationSolver::getLowerBound() const { return lowerBound.get(); } -template -ValueType const& AbstractEquationSolver::getLowerBound(uint64_t const& index) const { +template +SolutionType const& AbstractEquationSolver::getLowerBound(uint64_t const& index) const { if (lowerBounds) { STORM_LOG_ASSERT(index < lowerBounds->size(), "Invalid row index " << index << " for vector of size " << lowerBounds->size()); if (lowerBound) { @@ -146,24 +146,24 @@ ValueType const& AbstractEquationSolver::getLowerBound(uint64_t const } } -template -ValueType AbstractEquationSolver::getLowerBound(bool convertLocalBounds) const { +template +SolutionType AbstractEquationSolver::getLowerBound(bool convertLocalBounds) const { if (lowerBound) { return lowerBound.get(); } else if (convertLocalBounds) { return *std::min_element(lowerBounds->begin(), lowerBounds->end()); } STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "No lower bound available but some was requested."); - return ValueType(); + return SolutionType(); } -template -ValueType const& AbstractEquationSolver::getUpperBound() const { +template +SolutionType const& AbstractEquationSolver::getUpperBound() const { return upperBound.get(); } -template -ValueType const& AbstractEquationSolver::getUpperBound(uint64_t const& index) const { +template +SolutionType const& AbstractEquationSolver::getUpperBound(uint64_t const& index) const { if (upperBounds) { STORM_LOG_ASSERT(index < upperBounds->size(), "Invalid row index " << index << " for vector of size " << upperBounds->size()); if (upperBound) { @@ -177,55 +177,55 @@ ValueType const& AbstractEquationSolver::getUpperBound(uint64_t const } } -template -ValueType AbstractEquationSolver::getUpperBound(bool convertLocalBounds) const { +template +SolutionType AbstractEquationSolver::getUpperBound(bool convertLocalBounds) const { if (upperBound) { return upperBound.get(); } else if (convertLocalBounds) { return *std::max_element(upperBounds->begin(), upperBounds->end()); } STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "No upper bound available but some was requested."); - return ValueType(); + return SolutionType(); } -template -std::vector const& AbstractEquationSolver::getLowerBounds() const { +template +std::vector const& AbstractEquationSolver::getLowerBounds() const { return lowerBounds.get(); } -template -std::vector const& AbstractEquationSolver::getUpperBounds() const { +template +std::vector const& AbstractEquationSolver::getUpperBounds() const { return upperBounds.get(); } -template -void AbstractEquationSolver::setLowerBounds(std::vector const& values) { +template +void AbstractEquationSolver::setLowerBounds(std::vector const& values) { lowerBounds = values; } -template -void AbstractEquationSolver::setLowerBounds(std::vector&& values) { +template +void AbstractEquationSolver::setLowerBounds(std::vector&& values) { lowerBounds = std::move(values); } -template -void AbstractEquationSolver::setUpperBounds(std::vector const& values) { +template +void AbstractEquationSolver::setUpperBounds(std::vector const& values) { upperBounds = values; } -template -void AbstractEquationSolver::setUpperBounds(std::vector&& values) { +template +void AbstractEquationSolver::setUpperBounds(std::vector&& values) { upperBounds = std::move(values); } -template -void AbstractEquationSolver::setBounds(std::vector const& lower, std::vector const& upper) { +template +void AbstractEquationSolver::setBounds(std::vector const& lower, std::vector const& upper) { setLowerBounds(lower); setUpperBounds(upper); } -template -void AbstractEquationSolver::setBoundsFromOtherSolver(AbstractEquationSolver const& other) { +template +void AbstractEquationSolver::setBoundsFromOtherSolver(AbstractEquationSolver const& other) { if (other.hasLowerBound(BoundType::Global)) { this->setLowerBound(other.getLowerBound()); } @@ -240,69 +240,69 @@ void AbstractEquationSolver::setBoundsFromOtherSolver(AbstractEquatio } } -template -bool AbstractEquationSolver::hasSolutionLowerBounds() const { +template +bool AbstractEquationSolver::hasSolutionLowerBounds() const { return solutionBounds.hasLower(); } -template -bool AbstractEquationSolver::hasSolutionUpperBounds() const { +template +bool AbstractEquationSolver::hasSolutionUpperBounds() const { return solutionBounds.hasUpper(); } -template -std::vector const& AbstractEquationSolver::getSolutionLowerBounds() const { +template +std::vector const& AbstractEquationSolver::getSolutionLowerBounds() const { STORM_LOG_ASSERT(this->hasSolutionLowerBounds(), "No lower bound on the solution was computed."); return *solutionBounds.lower; } -template -std::vector const& AbstractEquationSolver::getSolutionUpperBounds() const { +template +std::vector const& AbstractEquationSolver::getSolutionUpperBounds() const { STORM_LOG_ASSERT(this->hasSolutionUpperBounds(), "No upper bound on the solution was computed."); return *solutionBounds.upper; } -template -void AbstractEquationSolver::setSolutionBounds(SolutionBounds bounds) const { +template +void AbstractEquationSolver::setSolutionBounds(SolutionBounds bounds) const { STORM_LOG_ASSERT(!bounds.hasLower() || !bounds.hasUpper() || bounds.lower->size() == bounds.upper->size(), "Bounds on the solution must have the same size."); solutionBounds = std::move(bounds); } -template -void AbstractEquationSolver::setSolutionBoundsExact(std::vector const& x) const { - SolutionBounds bounds; +template +void AbstractEquationSolver::setSolutionBoundsExact(std::vector const& x) const { + SolutionBounds bounds; bounds.setExact(x); this->setSolutionBounds(std::move(bounds)); } -template -void AbstractEquationSolver::clearSolutionBounds() const { +template +void AbstractEquationSolver::clearSolutionBounds() const { solutionBounds.clear(); } -template -void AbstractEquationSolver::clearBounds() { +template +void AbstractEquationSolver::clearBounds() { lowerBound = boost::none; upperBound = boost::none; lowerBounds = boost::none; upperBounds = boost::none; } -template -void AbstractEquationSolver::createLowerBoundsVector(std::vector& lowerBoundsVector) const { +template +void AbstractEquationSolver::createLowerBoundsVector(std::vector& lowerBoundsVector) const { if (this->hasLowerBound(BoundType::Local)) { lowerBoundsVector = this->getLowerBounds(); } else { - ValueType lowerBound = this->hasLowerBound(BoundType::Global) ? this->getLowerBound() : storm::utility::zero(); + SolutionType lowerBound = this->hasLowerBound(BoundType::Global) ? this->getLowerBound() : storm::utility::zero(); for (auto& e : lowerBoundsVector) { e = lowerBound; } } } -template -void AbstractEquationSolver::createUpperBoundsVector(std::vector& upperBoundsVector) const { +template +void AbstractEquationSolver::createUpperBoundsVector(std::vector& upperBoundsVector) const { STORM_LOG_ASSERT(this->hasUpperBound(), "Expecting upper bound(s)."); if (this->hasUpperBound(BoundType::Global)) { upperBoundsVector.assign(upperBoundsVector.size(), this->getUpperBound()); @@ -311,41 +311,41 @@ void AbstractEquationSolver::createUpperBoundsVector(std::vector -void AbstractEquationSolver::createUpperBoundsVector(std::unique_ptr>& upperBoundsVector, uint64_t length) const { +template +void AbstractEquationSolver::createUpperBoundsVector(std::unique_ptr>& upperBoundsVector, uint64_t length) const { STORM_LOG_ASSERT(this->hasUpperBound(), "Expecting upper bound(s)."); if (!upperBoundsVector) { if (this->hasUpperBound(BoundType::Local)) { STORM_LOG_ASSERT(length == this->getUpperBounds().size(), "Mismatching sizes."); - upperBoundsVector = std::make_unique>(this->getUpperBounds()); + upperBoundsVector = std::make_unique>(this->getUpperBounds()); } else { - upperBoundsVector = std::make_unique>(length, this->getUpperBound()); + upperBoundsVector = std::make_unique>(length, this->getUpperBound()); } } else { createUpperBoundsVector(*upperBoundsVector); } } -template -bool AbstractEquationSolver::isShowProgressSet() const { +template +bool AbstractEquationSolver::isShowProgressSet() const { return this->progressMeasurement.is_initialized(); } -template -uint64_t AbstractEquationSolver::getShowProgressDelay() const { +template +uint64_t AbstractEquationSolver::getShowProgressDelay() const { STORM_LOG_ASSERT(this->isShowProgressSet(), "Tried to get the progress message delay but progress is not shown."); return this->progressMeasurement->getShowProgressDelay(); } -template -void AbstractEquationSolver::startMeasureProgress(uint64_t startingIteration) const { +template +void AbstractEquationSolver::startMeasureProgress(uint64_t startingIteration) const { if (this->isShowProgressSet()) { this->progressMeasurement->startNewMeasurement(startingIteration); } } -template -void AbstractEquationSolver::showProgressIterative(uint64_t iteration, boost::optional const& bound) const { +template +void AbstractEquationSolver::showProgressIterative(uint64_t iteration, boost::optional const& bound) const { if (this->isShowProgressSet()) { if (bound) { this->progressMeasurement->setMaxCount(bound.get()); @@ -354,8 +354,8 @@ void AbstractEquationSolver::showProgressIterative(uint64_t iteration } } -template -void AbstractEquationSolver::reportStatus(SolverStatus status, boost::optional const& iterations) const { +template +void AbstractEquationSolver::reportStatus(SolverStatus status, boost::optional const& iterations) const { if (iterations) { switch (status) { case SolverStatus::Converged: @@ -394,9 +394,9 @@ void AbstractEquationSolver::reportStatus(SolverStatus status, boost: } } -template -SolverStatus AbstractEquationSolver::updateStatus(SolverStatus status, bool earlyTermination, uint64_t iterations, - uint64_t maximalNumberOfIterations) const { +template +SolverStatus AbstractEquationSolver::updateStatus(SolverStatus status, bool earlyTermination, uint64_t iterations, + uint64_t maximalNumberOfIterations) const { if (status != SolverStatus::Converged) { if (earlyTermination) { status = SolverStatus::TerminatedEarly; @@ -409,9 +409,9 @@ SolverStatus AbstractEquationSolver::updateStatus(SolverStatus status return status; } -template -SolverStatus AbstractEquationSolver::updateStatus(SolverStatus status, std::vector const& x, SolverGuarantee const& guarantee, - uint64_t iterations, uint64_t maximalNumberOfIterations) const { +template +SolverStatus AbstractEquationSolver::updateStatus(SolverStatus status, std::vector const& x, SolverGuarantee const& guarantee, + uint64_t iterations, uint64_t maximalNumberOfIterations) const { return this->updateStatus(status, this->hasCustomTerminationCondition() && this->getTerminationCondition().terminateNow(x, guarantee), iterations, maximalNumberOfIterations); } diff --git a/src/storm/solver/AbstractEquationSolver.h b/src/storm/solver/AbstractEquationSolver.h index e178c9de8a..c262fbe477 100644 --- a/src/storm/solver/AbstractEquationSolver.h +++ b/src/storm/solver/AbstractEquationSolver.h @@ -13,7 +13,7 @@ namespace storm { namespace solver { -template +template class AbstractEquationSolver { public: AbstractEquationSolver(); @@ -32,7 +32,7 @@ class AbstractEquationSolver { * * @param terminationCondition An object that can be queried whether to terminate early or not. */ - void setTerminationCondition(std::unique_ptr> terminationCondition); + void setTerminationCondition(std::unique_ptr> terminationCondition); /*! * Removes a previously set custom termination condition. @@ -48,7 +48,7 @@ class AbstractEquationSolver { * Checks whether the solver can terminate wrt. to its termination condition. If no termination condition, * this will yield false. */ - bool terminateNow(std::vector const& values, SolverGuarantee const& guarantee) const; + bool terminateNow(std::vector const& values, SolverGuarantee const& guarantee) const; /*! * Retrieves whether this solver has particularly relevant values. @@ -91,92 +91,92 @@ class AbstractEquationSolver { /*! * Sets a lower bound for the solution that can potentially be used by the solver. */ - void setLowerBound(ValueType const& value); + void setLowerBound(SolutionType const& value); /*! * Sets an upper bound for the solution that can potentially be used by the solver. */ - void setUpperBound(ValueType const& value); + void setUpperBound(SolutionType const& value); /*! * Sets bounds for the solution that can potentially be used by the solver. */ - void setBounds(ValueType const& lower, ValueType const& upper); + void setBounds(SolutionType const& lower, SolutionType const& upper); /*! * Retrieves the lower bound (if there is any). */ - ValueType const& getLowerBound() const; + SolutionType const& getLowerBound() const; /*! * Retrieves the lower bound for the variable with the given index (if there is any lower bound). * @pre some lower bound (local or global) has been specified * @return the largest lower bound known for the given row */ - ValueType const& getLowerBound(uint64_t const& index) const; + SolutionType const& getLowerBound(uint64_t const& index) const; /*! * Retrieves the lower bound (if there is any). * If the given flag is true and if there are only local bounds, * the minimum of the local bounds is returned. */ - ValueType getLowerBound(bool convertLocalBounds) const; + SolutionType getLowerBound(bool convertLocalBounds) const; /*! * Retrieves the upper bound (if there is any). */ - ValueType const& getUpperBound() const; + SolutionType const& getUpperBound() const; /*! * Retrieves the upper bound for the variable with the given index (if there is any upper bound). * @pre some upper bound (local or global) has been specified * @return the smallest upper bound known for the given row */ - ValueType const& getUpperBound(uint64_t const& index) const; + SolutionType const& getUpperBound(uint64_t const& index) const; /*! * Retrieves the upper bound (if there is any). * If the given flag is true and if there are only local bounds, * the maximum of the local bounds is returned. */ - ValueType getUpperBound(bool convertLocalBounds) const; + SolutionType getUpperBound(bool convertLocalBounds) const; /*! * Retrieves a vector containing the lower bounds (if there are any). */ - std::vector const& getLowerBounds() const; + std::vector const& getLowerBounds() const; /*! * Retrieves a vector containing the upper bounds (if there are any). */ - std::vector const& getUpperBounds() const; + std::vector const& getUpperBounds() const; /*! * Sets lower bounds for the solution that can potentially be used by the solver. */ - void setLowerBounds(std::vector const& values); + void setLowerBounds(std::vector const& values); /*! * Sets lower bounds for the solution that can potentially be used by the solver. */ - void setLowerBounds(std::vector&& values); + void setLowerBounds(std::vector&& values); /*! * Sets upper bounds for the solution that can potentially be used by the solver. */ - void setUpperBounds(std::vector const& values); + void setUpperBounds(std::vector const& values); /*! * Sets upper bounds for the solution that can potentially be used by the solver. */ - void setUpperBounds(std::vector&& values); + void setUpperBounds(std::vector&& values); /*! * Sets bounds for the solution that can potentially be used by the solver. */ - void setBounds(std::vector const& lower, std::vector const& upper); + void setBounds(std::vector const& lower, std::vector const& upper); - void setBoundsFromOtherSolver(AbstractEquationSolver const& other); + void setBoundsFromOtherSolver(AbstractEquationSolver const& other); /*! * Removes all specified solution bounds @@ -194,8 +194,8 @@ class AbstractEquationSolver { * Retrieves sound bounds on the solution that the last call to this solver computed. * @pre The respective bound was computed, see hasSolutionLowerBounds() resp. hasSolutionUpperBounds(). */ - std::vector const& getSolutionLowerBounds() const; - std::vector const& getSolutionUpperBounds() const; + std::vector const& getSolutionLowerBounds() const; + std::vector const& getSolutionUpperBounds() const; /*! * Retrieves whether progress is to be shown. @@ -223,21 +223,21 @@ class AbstractEquationSolver { * * @return The custom termination condition. */ - TerminationCondition const& getTerminationCondition() const; - std::unique_ptr> const& getTerminationConditionPointer() const; + TerminationCondition const& getTerminationCondition() const; + std::unique_ptr> const& getTerminationConditionPointer() const; /*! * Stores sound bounds on the solution that were obtained while solving. Note that solving is const, so * that this is as well. */ - void setSolutionBounds(SolutionBounds bounds) const; + void setSolutionBounds(SolutionBounds bounds) const; /*! * Reports the given solution as an exact one, i.e. stores it as both the lower and the upper bound. * * @param x The computed solution. */ - void setSolutionBoundsExact(std::vector const& x) const; + void setSolutionBoundsExact(std::vector const& x) const; /*! * Discards any bounds on the solution obtained by a previous call. This must happen whenever solving @@ -245,9 +245,9 @@ class AbstractEquationSolver { */ void clearSolutionBounds() const; - void createUpperBoundsVector(std::vector& upperBoundsVector) const; - void createUpperBoundsVector(std::unique_ptr>& upperBoundsVector, uint64_t length) const; - void createLowerBoundsVector(std::vector& lowerBoundsVector) const; + void createUpperBoundsVector(std::vector& upperBoundsVector) const; + void createUpperBoundsVector(std::unique_ptr>& upperBoundsVector, uint64_t length) const; + void createLowerBoundsVector(std::vector& lowerBoundsVector) const; /*! * Report the current status of the solver. @@ -265,7 +265,7 @@ class AbstractEquationSolver { * @param maximalNumberOfIterations Maximal number of iterations. * @return New status. */ - SolverStatus updateStatus(SolverStatus status, std::vector const& x, SolverGuarantee const& guarantee, uint64_t iterations, + SolverStatus updateStatus(SolverStatus status, std::vector const& x, SolverGuarantee const& guarantee, uint64_t iterations, uint64_t maximalNumberOfIterations) const; /*! @@ -279,26 +279,26 @@ class AbstractEquationSolver { SolverStatus updateStatus(SolverStatus status, bool earlyTermination, uint64_t iterations, uint64_t maximalNumberOfIterations) const; // A termination condition to be used (can be unset). - std::unique_ptr> terminationCondition; + std::unique_ptr> terminationCondition; // A bit vector containing the indices of the relevant values if they were set. boost::optional relevantValues; // A lower bound if one was set. - boost::optional lowerBound; + boost::optional lowerBound; // An upper bound if one was set. - boost::optional upperBound; + boost::optional upperBound; // Lower bounds if they were set. - boost::optional> lowerBounds; + boost::optional> lowerBounds; // Lower bounds if they were set. - boost::optional> upperBounds; + boost::optional> upperBounds; private: // Sound bounds on the solution, if the last call to this solver produced any. - mutable SolutionBounds solutionBounds; + mutable SolutionBounds solutionBounds; // Indicates the progress of this solver. mutable boost::optional progressMeasurement;