From 119f73edd513225c836fa08d23e204dc3410993d Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:18:43 +0200 Subject: [PATCH 01/65] Add initial CLI support for CVaR queries --- src/storm-cli-utilities/model-handling.h | 12 ++++++++++++ src/storm/settings/modules/IOSettings.cpp | 16 ++++++++++++++++ src/storm/settings/modules/IOSettings.h | 15 +++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/src/storm-cli-utilities/model-handling.h b/src/storm-cli-utilities/model-handling.h index 42b8ccbbd5..fded66cc07 100644 --- a/src/storm-cli-utilities/model-handling.h +++ b/src/storm-cli-utilities/model-handling.h @@ -427,6 +427,9 @@ inline std::pair preprocessSymbolicIn SymbolicInput output = input; // Preprocess properties (if requested) + STORM_LOG_THROW(!(ioSettings.isPropertiesAsMultiSet() && ioSettings.isCvarSet()), storm::exceptions::InvalidArgumentException, + "Options '--propsasmulti' and '--cvar' can not be combined."); + if (ioSettings.isPropertiesAsMultiSet()) { STORM_LOG_THROW(!input.properties.empty(), storm::exceptions::InvalidArgumentException, "Can not translate properties to multi-objective formula because no properties were specified."); @@ -435,6 +438,15 @@ inline std::pair preprocessSymbolicIn output.properties = {storm::api::createMultiObjectiveProperty(output.properties, multiObjSettings.isLexicographicModelCheckingSet())}; } + if (ioSettings.isCvarSet()) { + STORM_LOG_THROW(!input.properties.empty(), storm::exceptions::InvalidArgumentException, + "Can not translate properties to a CVaR formula because no properties were specified."); + STORM_LOG_THROW(output.properties.size() == 1, storm::exceptions::InvalidArgumentException, + "The '--cvar' option currently requires exactly one selected property."); + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, + "The '--cvar' option is recognized, but rewriting the selected property to a dedicated CVaR formula is not implemented yet."); + } + // Substitute constant definitions in symbolic input. std::string constantDefinitionString = ioSettings.getConstantDefinitionString(); std::map constantDefinitions; diff --git a/src/storm/settings/modules/IOSettings.cpp b/src/storm/settings/modules/IOSettings.cpp index b0faff8343..1bda82a702 100644 --- a/src/storm/settings/modules/IOSettings.cpp +++ b/src/storm/settings/modules/IOSettings.cpp @@ -59,6 +59,7 @@ const std::string IOSettings::qvbsInputOptionName = "qvbs"; const std::string IOSettings::qvbsInputOptionShortName = "qvbs"; const std::string IOSettings::qvbsRootOptionName = "qvbsroot"; const std::string IOSettings::propertiesAsMultiOptionName = "propsasmulti"; +const std::string IOSettings::cvarOptionName = "cvar"; const std::string IOSettings::uncertaintyResolutionModeName = "uncertainty-resolution"; @@ -281,6 +282,13 @@ IOSettings::IOSettings() : ModuleSettings(moduleName) { .setIsAdvanced() .build()); + this->addOption(storm::settings::OptionBuilder(moduleName, cvarOptionName, false, + "Computes the conditional value-at-risk for the selected property.") + .addArgument(storm::settings::ArgumentBuilder::createDoubleArgument("alpha", "The size of the lower tail.") + .addValidatorDouble(storm::settings::ArgumentValidatorFactory::createDoubleRangeValidatorExcluding(0.0, 1.0)) + .build()) + .build()); + std::vector uncertaintyResolutionModes = {"minimize", "maximize", "robust", "cooperative", "min", "max"}; this->addOption(storm::settings::OptionBuilder(moduleName, uncertaintyResolutionModeName, false, "Mode to resolve the uncertainty (intervals)") .addArgument(storm::settings::ArgumentBuilder::createStringArgument("mode", "Mode to resolve the uncertainty (intervals) by nature.") @@ -518,6 +526,14 @@ std::string IOSettings::getPropertyFilter() const { return this->getOption(propertyOptionName).getArgumentByName("filter").getValueAsString(); } +bool IOSettings::isCvarSet() const { + return this->getOption(cvarOptionName).getHasOptionBeenSet(); +} + +double IOSettings::getCvarAlpha() const { + return this->getOption(cvarOptionName).getArgumentByName("alpha").getValueAsDouble(); +} + bool IOSettings::isComputeSteadyStateDistributionSet() const { return this->getOption(steadyStateDistrOptionName).getHasOptionBeenSet(); } diff --git a/src/storm/settings/modules/IOSettings.h b/src/storm/settings/modules/IOSettings.h index df2219f573..15acb3fe70 100644 --- a/src/storm/settings/modules/IOSettings.h +++ b/src/storm/settings/modules/IOSettings.h @@ -367,6 +367,20 @@ class IOSettings : public ModuleSettings { */ std::string getPropertyFilter() const; + /*! + * Retrieves whether the CVaR option was set. + * + * @return True if the CVaR option was set. + */ + bool isCvarSet() const; + + /*! + * Retrieves the alpha value specified with the CVaR option. + * + * @return The alpha value specified with the CVaR option. + */ + double getCvarAlpha() const; + /*! * Retrieves whether the steady-state distribution is to be computed. */ @@ -470,6 +484,7 @@ class IOSettings : public ModuleSettings { static const std::string qvbsInputOptionShortName; static const std::string qvbsRootOptionName; static const std::string propertiesAsMultiOptionName; + static const std::string cvarOptionName; static const std::string uncertaintyResolutionModeName; }; From 3d1308aa1a6ccaa4f0a5f4af12d5fb55392778e1 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:54:43 +0200 Subject: [PATCH 02/65] logic: Add CvarFormula --- src/storm/logic/CloneVisitor.cpp | 5 ++ src/storm/logic/CloneVisitor.h | 1 + src/storm/logic/CvarFormula.cpp | 72 +++++++++++++++++++ src/storm/logic/CvarFormula.h | 37 ++++++++++ .../ExtractMaximalStateFormulasVisitor.cpp | 7 ++ .../ExtractMaximalStateFormulasVisitor.h | 1 + src/storm/logic/Formula.cpp | 12 ++++ src/storm/logic/Formula.h | 4 ++ src/storm/logic/FormulaInformationVisitor.cpp | 4 ++ src/storm/logic/FormulaInformationVisitor.h | 1 + src/storm/logic/FormulaVisitor.h | 1 + src/storm/logic/Formulas.h | 1 + src/storm/logic/FormulasForwardDeclarations.h | 1 + src/storm/logic/FragmentChecker.cpp | 11 +++ src/storm/logic/FragmentChecker.h | 1 + src/storm/logic/FragmentSpecification.cpp | 33 ++++++++- src/storm/logic/FragmentSpecification.h | 11 +++ .../LiftableTransitionRewardsVisitor.cpp | 4 ++ .../logic/LiftableTransitionRewardsVisitor.h | 1 + src/storm/logic/ToExpressionVisitor.cpp | 4 ++ src/storm/logic/ToExpressionVisitor.h | 1 + src/storm/logic/ToPrefixStringVisitor.cpp | 4 ++ src/storm/logic/ToPrefixStringVisitor.h | 1 + .../storage/jani/visitor/JSONExporter.cpp | 4 ++ src/storm/storage/jani/visitor/JSONExporter.h | 1 + 25 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/storm/logic/CvarFormula.cpp create mode 100644 src/storm/logic/CvarFormula.h diff --git a/src/storm/logic/CloneVisitor.cpp b/src/storm/logic/CloneVisitor.cpp index f3531aa60c..a0141c0add 100644 --- a/src/storm/logic/CloneVisitor.cpp +++ b/src/storm/logic/CloneVisitor.cpp @@ -76,6 +76,11 @@ boost::any CloneVisitor::visit(CumulativeRewardFormula const& f, boost::any cons return std::static_pointer_cast(std::make_shared(f)); } +boost::any CloneVisitor::visit(CvarFormula const& f, boost::any const& data) const { + std::shared_ptr subformula = boost::any_cast>(f.getSubformula().accept(*this, data)); + return std::static_pointer_cast(std::make_shared(f.getAlpha(), subformula)); +} + boost::any CloneVisitor::visit(EventuallyFormula const& f, boost::any const& data) const { std::shared_ptr subformula = boost::any_cast>(f.getSubformula().accept(*this, data)); if (f.hasRewardAccumulation()) { diff --git a/src/storm/logic/CloneVisitor.h b/src/storm/logic/CloneVisitor.h index 82a2b6b4cc..d031faf7c4 100644 --- a/src/storm/logic/CloneVisitor.h +++ b/src/storm/logic/CloneVisitor.h @@ -20,6 +20,7 @@ class CloneVisitor : public FormulaVisitor { virtual boost::any visit(BoundedUntilFormula const& f, boost::any const& data) const override; virtual boost::any visit(ConditionalFormula const& f, boost::any const& data) const override; virtual boost::any visit(CumulativeRewardFormula const& f, boost::any const& data) const override; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const override; virtual boost::any visit(EventuallyFormula const& f, boost::any const& data) const override; virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(GloballyFormula const& f, boost::any const& data) const override; diff --git a/src/storm/logic/CvarFormula.cpp b/src/storm/logic/CvarFormula.cpp new file mode 100644 index 0000000000..21e24503d4 --- /dev/null +++ b/src/storm/logic/CvarFormula.cpp @@ -0,0 +1,72 @@ +#include "storm/logic/CvarFormula.h" + +#include +#include +#include + +#include "storm/logic/FormulaVisitor.h" + +namespace storm { +namespace logic { + +CvarFormula::CvarFormula(double alpha, std::shared_ptr subformula) : alpha(alpha), subformula(std::move(subformula)) { + // Intentionally left empty. +} + +CvarFormula::~CvarFormula() { + // Intentionally left empty. +} + +bool CvarFormula::isCvarFormula() const { + return true; +} + +bool CvarFormula::hasQuantitativeResult() const { + return true; +} + +bool CvarFormula::hasNumericalResult() const { + return true; +} + +bool CvarFormula::hasMultiDimensionalResult() const { + return false; +} + +double CvarFormula::getAlpha() const { + return alpha; +} + +Formula const& CvarFormula::getSubformula() const { + return *subformula; +} + +boost::any CvarFormula::accept(FormulaVisitor const& visitor, boost::any const& data) const { + return visitor.visit(*this, data); +} + +void CvarFormula::gatherAtomicExpressionFormulas(std::vector>& atomicExpressionFormulas) const { + subformula->gatherAtomicExpressionFormulas(atomicExpressionFormulas); +} + +void CvarFormula::gatherAtomicLabelFormulas(std::vector>& atomicLabelFormulas) const { + subformula->gatherAtomicLabelFormulas(atomicLabelFormulas); +} + +void CvarFormula::gatherReferencedRewardModels(std::set& referencedRewardModels) const { + subformula->gatherReferencedRewardModels(referencedRewardModels); +} + +void CvarFormula::gatherUsedVariables(std::set& usedVariables) const { + subformula->gatherUsedVariables(usedVariables); +} + +std::ostream& CvarFormula::writeToStream(std::ostream& out, bool /* allowParentheses */) const { + out << "cvar(" << alpha << ", "; + subformula->writeToStream(out); + out << ")"; + return out; +} + +} // namespace logic +} // namespace storm diff --git a/src/storm/logic/CvarFormula.h b/src/storm/logic/CvarFormula.h new file mode 100644 index 0000000000..9550a16f8c --- /dev/null +++ b/src/storm/logic/CvarFormula.h @@ -0,0 +1,37 @@ +#pragma once + +#include "storm/logic/StateFormula.h" + +namespace storm { +namespace logic { + +class CvarFormula : public StateFormula { + public: + CvarFormula(double alpha, std::shared_ptr subformula); + + virtual ~CvarFormula(); + + virtual bool isCvarFormula() const override; + + virtual bool hasQuantitativeResult() const override; + virtual bool hasNumericalResult() const; + virtual bool hasMultiDimensionalResult() const; + + double getAlpha() const; + Formula const& getSubformula() const; + + virtual boost::any accept(FormulaVisitor const& visitor, boost::any const& data) const override; + virtual void gatherAtomicExpressionFormulas(std::vector>& atomicExpressionFormulas) const override; + virtual void gatherAtomicLabelFormulas(std::vector>& atomicLabelFormulas) const override; + virtual void gatherReferencedRewardModels(std::set& referencedRewardModels) const override; + virtual void gatherUsedVariables(std::set& usedVariables) const override; + + virtual std::ostream& writeToStream(std::ostream& out, bool allowParentheses = false) const override; + + private: + double alpha; + std::shared_ptr subformula; +}; + +} // namespace logic +} // namespace storm diff --git a/src/storm/logic/ExtractMaximalStateFormulasVisitor.cpp b/src/storm/logic/ExtractMaximalStateFormulasVisitor.cpp index 37ceab54e1..11b97516d6 100644 --- a/src/storm/logic/ExtractMaximalStateFormulasVisitor.cpp +++ b/src/storm/logic/ExtractMaximalStateFormulasVisitor.cpp @@ -164,6 +164,13 @@ boost::any ExtractMaximalStateFormulasVisitor::visit(MultiObjectiveFormula const return result; } +boost::any ExtractMaximalStateFormulasVisitor::visit(CvarFormula const& f, boost::any const& data) const { + incrementNestingLevel(); + boost::any result = CloneVisitor::visit(f, data); + decrementNestingLevel(); + return result; +} + boost::any ExtractMaximalStateFormulasVisitor::visit(ProbabilityOperatorFormula const& f, boost::any const& data) const { incrementNestingLevel(); boost::any result = CloneVisitor::visit(f, data); diff --git a/src/storm/logic/ExtractMaximalStateFormulasVisitor.h b/src/storm/logic/ExtractMaximalStateFormulasVisitor.h index 82bffc792d..6723199a6c 100644 --- a/src/storm/logic/ExtractMaximalStateFormulasVisitor.h +++ b/src/storm/logic/ExtractMaximalStateFormulasVisitor.h @@ -30,6 +30,7 @@ class ExtractMaximalStateFormulasVisitor : public CloneVisitor { virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(LongRunAverageOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(MultiObjectiveFormula const& f, boost::any const& data) const override; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const override; virtual boost::any visit(ProbabilityOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(RewardOperatorFormula const& f, boost::any const& data) const override; diff --git a/src/storm/logic/Formula.cpp b/src/storm/logic/Formula.cpp index 440d9139d4..49de2b7300 100644 --- a/src/storm/logic/Formula.cpp +++ b/src/storm/logic/Formula.cpp @@ -33,6 +33,10 @@ bool Formula::isQuantileFormula() const { return false; } +bool Formula::isCvarFormula() const { + return false; +} + bool Formula::isBinaryStateFormula() const { return false; } @@ -250,6 +254,14 @@ QuantileFormula const& Formula::asQuantileFormula() const { return dynamic_cast(*this); } +CvarFormula& Formula::asCvarFormula() { + return dynamic_cast(*this); +} + +CvarFormula const& Formula::asCvarFormula() const { + return dynamic_cast(*this); +} + BinaryStateFormula& Formula::asBinaryStateFormula() { return dynamic_cast(*this); } diff --git a/src/storm/logic/Formula.h b/src/storm/logic/Formula.h index 57863e1235..86ce9823a4 100644 --- a/src/storm/logic/Formula.h +++ b/src/storm/logic/Formula.h @@ -54,6 +54,7 @@ class Formula : public std::enable_shared_from_this { virtual bool isMultiObjectiveFormula() const; virtual bool isQuantileFormula() const; + virtual bool isCvarFormula() const; // Operator formulas. virtual bool isOperatorFormula() const; @@ -126,6 +127,9 @@ class Formula : public std::enable_shared_from_this { QuantileFormula& asQuantileFormula(); QuantileFormula const& asQuantileFormula() const; + CvarFormula& asCvarFormula(); + CvarFormula const& asCvarFormula() const; + BinaryStateFormula& asBinaryStateFormula(); BinaryStateFormula const& asBinaryStateFormula() const; diff --git a/src/storm/logic/FormulaInformationVisitor.cpp b/src/storm/logic/FormulaInformationVisitor.cpp index 61eef9e4a4..c3015fab7f 100644 --- a/src/storm/logic/FormulaInformationVisitor.cpp +++ b/src/storm/logic/FormulaInformationVisitor.cpp @@ -79,6 +79,10 @@ boost::any FormulaInformationVisitor::visit(CumulativeRewardFormula const& f, bo return result; } +boost::any FormulaInformationVisitor::visit(CvarFormula const& f, boost::any const& data) const { + return f.getSubformula().accept(*this, data); +} + boost::any FormulaInformationVisitor::visit(EventuallyFormula const& f, boost::any const& data) const { FormulaInformation result = boost::any_cast(f.getSubformula().accept(*this, data)); if (f.getSubformula().isPathFormula()) { diff --git a/src/storm/logic/FormulaInformationVisitor.h b/src/storm/logic/FormulaInformationVisitor.h index 9721fdfc5b..eb67175e6d 100644 --- a/src/storm/logic/FormulaInformationVisitor.h +++ b/src/storm/logic/FormulaInformationVisitor.h @@ -25,6 +25,7 @@ class FormulaInformationVisitor : public FormulaVisitor { virtual boost::any visit(BoundedUntilFormula const& f, boost::any const& data) const override; virtual boost::any visit(ConditionalFormula const& f, boost::any const& data) const override; virtual boost::any visit(CumulativeRewardFormula const& f, boost::any const& data) const override; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const override; virtual boost::any visit(EventuallyFormula const& f, boost::any const& data) const override; virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(GloballyFormula const& f, boost::any const& data) const override; diff --git a/src/storm/logic/FormulaVisitor.h b/src/storm/logic/FormulaVisitor.h index 816fd48006..e943ea9aa1 100644 --- a/src/storm/logic/FormulaVisitor.h +++ b/src/storm/logic/FormulaVisitor.h @@ -21,6 +21,7 @@ class FormulaVisitor { virtual boost::any visit(BoundedUntilFormula const& f, boost::any const& data) const = 0; virtual boost::any visit(ConditionalFormula const& f, boost::any const& data) const = 0; virtual boost::any visit(CumulativeRewardFormula const& f, boost::any const& data) const = 0; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const = 0; virtual boost::any visit(EventuallyFormula const& f, boost::any const& data) const = 0; virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const = 0; virtual boost::any visit(GloballyFormula const& f, boost::any const& data) const = 0; diff --git a/src/storm/logic/Formulas.h b/src/storm/logic/Formulas.h index e2937bc7ce..eddd5fcaf5 100644 --- a/src/storm/logic/Formulas.h +++ b/src/storm/logic/Formulas.h @@ -11,6 +11,7 @@ #include "storm/logic/ComparisonType.h" #include "storm/logic/ConditionalFormula.h" #include "storm/logic/CumulativeRewardFormula.h" +#include "storm/logic/CvarFormula.h" #include "storm/logic/DiscountedCumulativeRewardFormula.h" #include "storm/logic/DiscountedTotalRewardFormula.h" #include "storm/logic/EventuallyFormula.h" diff --git a/src/storm/logic/FormulasForwardDeclarations.h b/src/storm/logic/FormulasForwardDeclarations.h index f5de501b64..6f42d9249e 100644 --- a/src/storm/logic/FormulasForwardDeclarations.h +++ b/src/storm/logic/FormulasForwardDeclarations.h @@ -15,6 +15,7 @@ class BooleanLiteralFormula; class BoundedUntilFormula; class ConditionalFormula; class CumulativeRewardFormula; +class CvarFormula; class EventuallyFormula; class TimeOperatorFormula; class GloballyFormula; diff --git a/src/storm/logic/FragmentChecker.cpp b/src/storm/logic/FragmentChecker.cpp index 8dc07f5f2d..a151cecea8 100644 --- a/src/storm/logic/FragmentChecker.cpp +++ b/src/storm/logic/FragmentChecker.cpp @@ -31,6 +31,9 @@ bool FragmentChecker::conformsToSpecification(Formula const& f, FragmentSpecific if (specification.isQuantileFormulaAtTopLevelRequired()) { result &= f.isQuantileFormula(); } + if (specification.isCvarFormulaAtTopLevelRequired()) { + result &= f.isCvarFormula(); + } return result; } @@ -151,6 +154,14 @@ boost::any FragmentChecker::visit(CumulativeRewardFormula const& f, boost::any c return result; } +boost::any FragmentChecker::visit(CvarFormula const& f, boost::any const& data) const { + InheritedInformation const& inherited = boost::any_cast(data); + if (!inherited.getSpecification().areCvarFormulasAllowed()) { + return false; + } + return f.getSubformula().accept(*this, data); +} + boost::any FragmentChecker::visit(EventuallyFormula const& f, boost::any const& data) const { InheritedInformation const& inherited = boost::any_cast(data); bool result = true; diff --git a/src/storm/logic/FragmentChecker.h b/src/storm/logic/FragmentChecker.h index 114782d980..9538b40404 100644 --- a/src/storm/logic/FragmentChecker.h +++ b/src/storm/logic/FragmentChecker.h @@ -20,6 +20,7 @@ class FragmentChecker : public FormulaVisitor { virtual boost::any visit(BoundedUntilFormula const& f, boost::any const& data) const override; virtual boost::any visit(ConditionalFormula const& f, boost::any const& data) const override; virtual boost::any visit(CumulativeRewardFormula const& f, boost::any const& data) const override; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const override; virtual boost::any visit(EventuallyFormula const& f, boost::any const& data) const override; virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(GloballyFormula const& f, boost::any const& data) const override; diff --git a/src/storm/logic/FragmentSpecification.cpp b/src/storm/logic/FragmentSpecification.cpp index 63a546f6b9..3076518b61 100644 --- a/src/storm/logic/FragmentSpecification.cpp +++ b/src/storm/logic/FragmentSpecification.cpp @@ -239,6 +239,17 @@ FragmentSpecification quantiles() { return quantiles; } +FragmentSpecification cvars() { + FragmentSpecification cvars = propositional(); + + cvars.setCvarFormulasAllowed(true); + cvars.setCvarFormulaAtTopLevelRequired(true); + cvars.setRewardOperatorsAllowed(true); + cvars.setReachabilityRewardFormulasAllowed(true); + + return cvars; +} + FragmentSpecification::FragmentSpecification() { probabilityOperator = false; rewardOperator = false; @@ -247,6 +258,7 @@ FragmentSpecification::FragmentSpecification() { multiObjectiveFormula = false; quantileFormula = false; + cvarFormula = false; globallyFormula = false; reachabilityProbabilityFormula = false; @@ -295,8 +307,9 @@ FragmentSpecification::FragmentSpecification() { operatorAtTopLevelRequired = false; multiObjectiveFormulaAtTopLevelRequired = false; - operatorsAtTopLevelOfMultiObjectiveFormulasRequired = false; quantileFormulaAtTopLevelRequired = false; + cvarFormulaAtTopLevelRequired = false; + operatorsAtTopLevelOfMultiObjectiveFormulasRequired = false; rewardAccumulation = false; @@ -362,6 +375,15 @@ FragmentSpecification& FragmentSpecification::setQuantileFormulasAllowed(bool ne return *this; } +bool FragmentSpecification::areCvarFormulasAllowed() const { + return cvarFormula; +} + +FragmentSpecification& FragmentSpecification::setCvarFormulasAllowed(bool newValue) { + this->cvarFormula = newValue; + return *this; +} + bool FragmentSpecification::areGloballyFormulasAllowed() const { return globallyFormula; } @@ -741,6 +763,15 @@ FragmentSpecification& FragmentSpecification::setQuantileFormulaAtTopLevelRequir return *this; } +bool FragmentSpecification::isCvarFormulaAtTopLevelRequired() const { + return cvarFormulaAtTopLevelRequired; +} + +FragmentSpecification& FragmentSpecification::setCvarFormulaAtTopLevelRequired(bool newValue) { + cvarFormulaAtTopLevelRequired = newValue; + return *this; +} + bool FragmentSpecification::isRewardAccumulationAllowed() const { return rewardAccumulation; } diff --git a/src/storm/logic/FragmentSpecification.h b/src/storm/logic/FragmentSpecification.h index 9ec6d1708e..827ef9dfcb 100644 --- a/src/storm/logic/FragmentSpecification.h +++ b/src/storm/logic/FragmentSpecification.h @@ -36,6 +36,9 @@ class FragmentSpecification { bool areQuantileFormulasAllowed() const; FragmentSpecification& setQuantileFormulasAllowed(bool newValue); + bool areCvarFormulasAllowed() const; + FragmentSpecification& setCvarFormulasAllowed(bool newValue); + bool areGloballyFormulasAllowed() const; FragmentSpecification& setGloballyFormulasAllowed(bool newValue); @@ -156,6 +159,9 @@ class FragmentSpecification { bool isQuantileFormulaAtTopLevelRequired() const; FragmentSpecification& setQuantileFormulaAtTopLevelRequired(bool newValue); + bool isCvarFormulaAtTopLevelRequired() const; + FragmentSpecification& setCvarFormulaAtTopLevelRequired(bool newValue); + bool isRewardAccumulationAllowed() const; FragmentSpecification& setRewardAccumulationAllowed(bool newValue); @@ -181,6 +187,7 @@ class FragmentSpecification { bool multiObjectiveFormula; bool quantileFormula; + bool cvarFormula; bool globallyFormula; bool reachabilityProbabilityFormula; @@ -229,6 +236,7 @@ class FragmentSpecification { bool operatorAtTopLevelRequired; bool multiObjectiveFormulaAtTopLevelRequired; bool quantileFormulaAtTopLevelRequired; + bool cvarFormulaAtTopLevelRequired; bool operatorsAtTopLevelOfMultiObjectiveFormulasRequired; bool rewardAccumulation; @@ -281,5 +289,8 @@ FragmentSpecification lexObjective(); // Quantile formulas. FragmentSpecification quantiles(); +// CVaR formulas. +FragmentSpecification cvars(); + } // namespace logic } // namespace storm diff --git a/src/storm/logic/LiftableTransitionRewardsVisitor.cpp b/src/storm/logic/LiftableTransitionRewardsVisitor.cpp index 9bd6ec5d37..e35590b9d6 100644 --- a/src/storm/logic/LiftableTransitionRewardsVisitor.cpp +++ b/src/storm/logic/LiftableTransitionRewardsVisitor.cpp @@ -70,6 +70,10 @@ boost::any LiftableTransitionRewardsVisitor::visit(CumulativeRewardFormula const return true; } +boost::any LiftableTransitionRewardsVisitor::visit(CvarFormula const& f, boost::any const& data) const { + return f.getSubformula().accept(*this, data); +} + boost::any LiftableTransitionRewardsVisitor::visit(EventuallyFormula const& f, boost::any const& data) const { return f.getSubformula().accept(*this, data); } diff --git a/src/storm/logic/LiftableTransitionRewardsVisitor.h b/src/storm/logic/LiftableTransitionRewardsVisitor.h index 27cf39bb20..cd300aab0d 100644 --- a/src/storm/logic/LiftableTransitionRewardsVisitor.h +++ b/src/storm/logic/LiftableTransitionRewardsVisitor.h @@ -25,6 +25,7 @@ class LiftableTransitionRewardsVisitor : public FormulaVisitor { virtual boost::any visit(BoundedUntilFormula const& f, boost::any const& data) const override; virtual boost::any visit(ConditionalFormula const& f, boost::any const& data) const override; virtual boost::any visit(CumulativeRewardFormula const& f, boost::any const& data) const override; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const override; virtual boost::any visit(EventuallyFormula const& f, boost::any const& data) const override; virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(GloballyFormula const& f, boost::any const& data) const override; diff --git a/src/storm/logic/ToExpressionVisitor.cpp b/src/storm/logic/ToExpressionVisitor.cpp index e9db5ed866..bbc7f0609b 100644 --- a/src/storm/logic/ToExpressionVisitor.cpp +++ b/src/storm/logic/ToExpressionVisitor.cpp @@ -65,6 +65,10 @@ boost::any ToExpressionVisitor::visit(CumulativeRewardFormula const&, boost::any STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "Cannot assemble expression from formula that contains illegal elements."); } +boost::any ToExpressionVisitor::visit(CvarFormula const&, boost::any const&) const { + STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "Cannot assemble expression from formula that contains illegal elements."); +} + boost::any ToExpressionVisitor::visit(EventuallyFormula const&, boost::any const&) const { STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "Cannot assemble expression from formula that contains illegal elements."); } diff --git a/src/storm/logic/ToExpressionVisitor.h b/src/storm/logic/ToExpressionVisitor.h index 76248f31eb..e10c04d69d 100644 --- a/src/storm/logic/ToExpressionVisitor.h +++ b/src/storm/logic/ToExpressionVisitor.h @@ -20,6 +20,7 @@ class ToExpressionVisitor : public FormulaVisitor { virtual boost::any visit(BoundedUntilFormula const& f, boost::any const& data) const override; virtual boost::any visit(ConditionalFormula const& f, boost::any const& data) const override; virtual boost::any visit(CumulativeRewardFormula const& f, boost::any const& data) const override; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const override; virtual boost::any visit(EventuallyFormula const& f, boost::any const& data) const override; virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(GloballyFormula const& f, boost::any const& data) const override; diff --git a/src/storm/logic/ToPrefixStringVisitor.cpp b/src/storm/logic/ToPrefixStringVisitor.cpp index f33e52cc73..5bc0baccf9 100644 --- a/src/storm/logic/ToPrefixStringVisitor.cpp +++ b/src/storm/logic/ToPrefixStringVisitor.cpp @@ -109,6 +109,10 @@ boost::any ToPrefixStringVisitor::visit(CumulativeRewardFormula const&, boost::a STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "Can not convert to prefix string"); } +boost::any ToPrefixStringVisitor::visit(CvarFormula const&, boost::any const&) const { + STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "Can not convert to prefix string"); +} + boost::any ToPrefixStringVisitor::visit(EventuallyFormula const& f, boost::any const& data) const { std::string subexpression = boost::any_cast(f.getSubformula().accept(*this, data)); return std::string("F ") + subexpression; diff --git a/src/storm/logic/ToPrefixStringVisitor.h b/src/storm/logic/ToPrefixStringVisitor.h index 2fe860e01c..0f3a1bd498 100644 --- a/src/storm/logic/ToPrefixStringVisitor.h +++ b/src/storm/logic/ToPrefixStringVisitor.h @@ -19,6 +19,7 @@ class ToPrefixStringVisitor : public FormulaVisitor { virtual boost::any visit(BoundedUntilFormula const& f, boost::any const& data) const override; virtual boost::any visit(ConditionalFormula const& f, boost::any const& data) const override; virtual boost::any visit(CumulativeRewardFormula const& f, boost::any const& data) const override; + virtual boost::any visit(CvarFormula const& f, boost::any const& data) const override; virtual boost::any visit(EventuallyFormula const& f, boost::any const& data) const override; virtual boost::any visit(TimeOperatorFormula const& f, boost::any const& data) const override; virtual boost::any visit(GloballyFormula const& f, boost::any const& data) const override; diff --git a/src/storm/storage/jani/visitor/JSONExporter.cpp b/src/storm/storage/jani/visitor/JSONExporter.cpp index e4d8db050d..467e691bac 100644 --- a/src/storm/storage/jani/visitor/JSONExporter.cpp +++ b/src/storm/storage/jani/visitor/JSONExporter.cpp @@ -468,6 +468,10 @@ boost::any FormulaToJaniJson::visit(storm::logic::QuantileFormula const&, boost: STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Jani currently does not support conversion of a Quantile formula"); } +boost::any FormulaToJaniJson::visit(storm::logic::CvarFormula const&, boost::any const&) const { + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Jani currently does not support conversion of a CVaR formula"); +} + boost::any FormulaToJaniJson::visit(storm::logic::NextFormula const& f, boost::any const& data) const { ExportJsonType opDecl; opDecl["op"] = "U"; diff --git a/src/storm/storage/jani/visitor/JSONExporter.h b/src/storm/storage/jani/visitor/JSONExporter.h index 6fe16eb5e3..cb5ad50f70 100644 --- a/src/storm/storage/jani/visitor/JSONExporter.h +++ b/src/storm/storage/jani/visitor/JSONExporter.h @@ -59,6 +59,7 @@ class FormulaToJaniJson : public storm::logic::FormulaVisitor { virtual boost::any visit(storm::logic::BoundedUntilFormula const& f, boost::any const& data) const; virtual boost::any visit(storm::logic::ConditionalFormula const& f, boost::any const& data) const; virtual boost::any visit(storm::logic::CumulativeRewardFormula const& f, boost::any const& data) const; + virtual boost::any visit(storm::logic::CvarFormula const& f, boost::any const& data) const; virtual boost::any visit(storm::logic::EventuallyFormula const& f, boost::any const& data) const; virtual boost::any visit(storm::logic::TimeOperatorFormula const& f, boost::any const& data) const; virtual boost::any visit(storm::logic::GameFormula const& f, boost::any const& data) const; From 1764219e449fb5a49d2e660d3761beaf7e0c7762 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:41:12 +0200 Subject: [PATCH 03/65] api: Rewrite CVaR queries to CvarFormula --- src/storm-cli-utilities/model-handling.h | 3 +-- src/storm/api/properties.cpp | 7 +++++++ src/storm/api/properties.h | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/storm-cli-utilities/model-handling.h b/src/storm-cli-utilities/model-handling.h index fded66cc07..7e9c2b4589 100644 --- a/src/storm-cli-utilities/model-handling.h +++ b/src/storm-cli-utilities/model-handling.h @@ -443,8 +443,7 @@ inline std::pair preprocessSymbolicIn "Can not translate properties to a CVaR formula because no properties were specified."); STORM_LOG_THROW(output.properties.size() == 1, storm::exceptions::InvalidArgumentException, "The '--cvar' option currently requires exactly one selected property."); - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "The '--cvar' option is recognized, but rewriting the selected property to a dedicated CVaR formula is not implemented yet."); + output.properties = {storm::api::createCvarProperty(output.properties.front(), ioSettings.getCvarAlpha())}; } // Substitute constant definitions in symbolic input. diff --git a/src/storm/api/properties.cpp b/src/storm/api/properties.cpp index 41b963032b..4f2003777b 100644 --- a/src/storm/api/properties.cpp +++ b/src/storm/api/properties.cpp @@ -69,6 +69,13 @@ std::vector> extractFormulasFromPro return formulas; } +storm::jani::Property createCvarProperty(storm::jani::Property const& property, double alpha) { + STORM_LOG_THROW(property.getFilter().isDefault(), storm::exceptions::InvalidArgumentException, + "Non-default property filter of property " << property.getName() << " is not supported for CVaR queries."); + auto cvarFormula = std::make_shared(alpha, property.getRawFormula()); + return storm::jani::Property(property.getName(), cvarFormula, property.getUndefinedConstants(), property.getComment()); +} + storm::jani::Property createMultiObjectiveProperty(std::vector const& properties, bool lexicographic) { std::set undefConstants; std::string name = ""; diff --git a/src/storm/api/properties.h b/src/storm/api/properties.h index a852b523ed..60d5c8a8d1 100644 --- a/src/storm/api/properties.h +++ b/src/storm/api/properties.h @@ -36,6 +36,7 @@ std::vector substituteTranscendentalNumbersInProperties(s std::vector filterProperties(std::vector const& properties, boost::optional> const& propertyFilter); std::vector> extractFormulasFromProperties(std::vector const& properties); +storm::jani::Property createCvarProperty(storm::jani::Property const& property, double alpha); storm::jani::Property createMultiObjectiveProperty(std::vector const& properties, bool lexicographic); } // namespace api From 963b8b05445a960c99c4cecb1e9e3340e33113a3 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:25:18 +0200 Subject: [PATCH 04/65] cvar: Add formula validation and extraction helper --- .../cvar/CvarFormulaInformation.cpp | 39 +++++++++++++++++++ .../cvar/CvarFormulaInformation.h | 24 ++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/storm/modelchecker/cvar/CvarFormulaInformation.cpp create mode 100644 src/storm/modelchecker/cvar/CvarFormulaInformation.h diff --git a/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp b/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp new file mode 100644 index 0000000000..1416ffdcf4 --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp @@ -0,0 +1,39 @@ +#include "storm/modelchecker/cvar/CvarFormulaInformation.h" + +#include "storm/exceptions/InvalidPropertyException.h" +#include "storm/logic/EventuallyFormula.h" +#include "storm/logic/RewardOperatorFormula.h" +#include "storm/utility/macros.h" +#include "storm/utility/logging.h" + + +namespace storm { +namespace modelchecker { +namespace cvar { +CvarFormulaInformation extractCvarFormulaInformation(storm::logic::CvarFormula const& formula) { + storm::logic::Formula const& embeddedFormula = formula.getSubformula(); + STORM_LOG_THROW(embeddedFormula.isRewardOperatorFormula(), storm::exceptions::InvalidPropertyException, + "CVaR formulas currently require an embedded reward operator formula."); + + auto const& rewardOperator = embeddedFormula.asRewardOperatorFormula(); + STORM_LOG_THROW(rewardOperator.hasOptimalityType(), storm::exceptions::InvalidPropertyException, + "The embedded reward operator formula of a CVaR query must specify whether to minimize or maximize."); + STORM_LOG_THROW(!rewardOperator.hasBound(), storm::exceptions::InvalidPropertyException, + "The embedded reward operator formula of a CVaR query must not specify a threshold."); + + storm::logic::Formula const& rewardPathFormula = rewardOperator.getSubformula(); + STORM_LOG_THROW(rewardPathFormula.isReachabilityRewardFormula(), storm::exceptions::InvalidPropertyException, + "CVaR queries currently only support weighted reachability objectives."); + STORM_LOG_THROW(rewardPathFormula.isEventuallyFormula(), storm::exceptions::InvalidPropertyException, + "CVaR queries currently only support reachability reward formulas of the form Rmin/max=? [ F target ]."); + + auto const& eventuallyFormula = rewardPathFormula.asEventuallyFormula(); + STORM_LOG_THROW(eventuallyFormula.getSubformula().isStateFormula(), storm::exceptions::InvalidPropertyException, + "The target of the embedded reachability reward formula of a CVaR query must be a state formula."); + + return {formula.getAlpha(), rewardOperator.getOptimalityType(), rewardOperator.getOptionalRewardModelName(), + eventuallyFormula.getSubformula().asSharedPointer()}; +} +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/CvarFormulaInformation.h b/src/storm/modelchecker/cvar/CvarFormulaInformation.h new file mode 100644 index 0000000000..ae806fd7f2 --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarFormulaInformation.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include "storm/logic/CvarFormula.h" +#include "storm/solver/OptimizationDirection.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +struct CvarFormulaInformation { + double alpha; + storm::solver::OptimizationDirection optimizationDirection; + boost::optional rewardModelName; + std::shared_ptr targetFormula; +}; + +CvarFormulaInformation extractCvarFormulaInformation(storm::logic::CvarFormula const& formula); + +} // namespace cvar +} // namespace modelchecker +} // namespace storm From fa8ac2e526759a9d7c52e100350a55069df69246 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:36:52 +0200 Subject: [PATCH 05/65] modelchecker: Wired CVaR formulas into sparse MDP checking --- .../modelchecker/AbstractModelChecker.cpp | 9 +++++++++ src/storm/modelchecker/AbstractModelChecker.h | 3 +++ .../prctl/SparseMdpPrctlModelChecker.cpp | 18 +++++++++++++++++- .../prctl/SparseMdpPrctlModelChecker.h | 2 ++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/storm/modelchecker/AbstractModelChecker.cpp b/src/storm/modelchecker/AbstractModelChecker.cpp index 1a02acc28e..49d942de2e 100644 --- a/src/storm/modelchecker/AbstractModelChecker.cpp +++ b/src/storm/modelchecker/AbstractModelChecker.cpp @@ -296,6 +296,8 @@ std::unique_ptr AbstractModelChecker::checkStateFormula( STORM_LOG_ASSERT(mof.isTradeoff(), "Unexpected multi-objective formula type."); return this->checkMultiObjectiveFormula(env, checkTask.substituteFormula(mof)); } + } else if (stateFormula.isCvarFormula()) { + return this->checkCvarFormula(env, checkTask.substituteFormula(stateFormula.asCvarFormula())); } else if (stateFormula.isQuantileFormula()) { return this->checkQuantileFormula(env, checkTask.substituteFormula(stateFormula.asQuantileFormula())); } @@ -444,6 +446,13 @@ std::unique_ptr AbstractModelChecker::checkMultiObjectiv "This model checker (" << getClassName() << ") does not support the formula: " << checkTask.getFormula() << "."); } +template +std::unique_ptr AbstractModelChecker::checkCvarFormula(Environment const&, + CheckTask const& checkTask) { + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, + "This model checker (" << getClassName() << ") does not support the formula: " << checkTask.getFormula() << "."); +} + template std::unique_ptr AbstractModelChecker::checkQuantileFormula(Environment const&, CheckTask const& checkTask) { diff --git a/src/storm/modelchecker/AbstractModelChecker.h b/src/storm/modelchecker/AbstractModelChecker.h index 51261bddce..e2e9ce1f3c 100644 --- a/src/storm/modelchecker/AbstractModelChecker.h +++ b/src/storm/modelchecker/AbstractModelChecker.h @@ -138,6 +138,9 @@ class AbstractModelChecker { virtual std::unique_ptr checkMultiObjectiveFormula(Environment const& env, CheckTask const& checkTask); + // The methods to check CVaR formulas. + virtual std::unique_ptr checkCvarFormula(Environment const& env, CheckTask const& checkTask); + // The methods to check quantile formulas. virtual std::unique_ptr checkQuantileFormula(Environment const& env, CheckTask const& checkTask); diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 562c7fbbad..9e9c28ce50 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -6,6 +6,7 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/logic/FragmentSpecification.h" +#include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" #include "storm/modelchecker/helper/infinitehorizon/SparseNondeterministicInfiniteHorizonHelper.h" @@ -90,7 +91,8 @@ bool SparseMdpPrctlModelChecker::canHandleStatic(CheckTask SparseMdpPrctlModelChecker::che } } +template +std::unique_ptr SparseMdpPrctlModelChecker::checkCvarFormula( + Environment const&, CheckTask const& checkTask) { + STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, + "Computing CVaR is only supported for the initial states of a model."); + STORM_LOG_THROW(this->getModel().getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, + "CVaR is not supported on models with multiple initial states."); + + auto cvarFormulaInformation = storm::modelchecker::cvar::extractCvarFormulaInformation(checkTask.getFormula()); + static_cast(cvarFormulaInformation); + + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR model checking for sparse MDPs is not implemented yet."); +} + template std::unique_ptr SparseMdpPrctlModelChecker::checkQuantileFormula( Environment const& env, CheckTask const& checkTask) { diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h index 5a7f10a1fe..d0e9949cb2 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h @@ -62,6 +62,8 @@ class SparseMdpPrctlModelChecker : public SparsePropositionalModelChecker const& checkTask) override; virtual std::unique_ptr checkLexObjectiveFormula(Environment const& env, CheckTask const& checkTask) override; + virtual std::unique_ptr checkCvarFormula(Environment const& env, + CheckTask const& checkTask) override; virtual std::unique_ptr checkQuantileFormula(Environment const& env, CheckTask const& checkTask) override; }; From beda4135893a6ad41b906a18b0b527a251268584 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:55:14 +0200 Subject: [PATCH 06/65] cvar: Validate terminal rewards for sparse MDP queries --- .../WeightedReachabilityModelInformation.h | 58 +++++++++++++++++++ .../prctl/SparseMdpPrctlModelChecker.cpp | 14 +++-- 2 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h new file mode 100644 index 0000000000..7d219e3306 --- /dev/null +++ b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include "storm/exceptions/InvalidPropertyException.h" +#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/storage/BitVector.h" +#include "storm/utility/constants.h" +#include "storm/utility/logging.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +struct WeightedReachabilityModelInformation { + std::string rewardModelName; + storm::storage::BitVector targetStates; + std::vector terminalRewards; +}; + +template +WeightedReachabilityModelInformation extractWeightedReachabilityModelInformation( + SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, storm::storage::BitVector const& targetStates) { + using ValueType = typename SparseMdpModelType::ValueType; + + std::string rewardModelName = formulaInformation.rewardModelName ? formulaInformation.rewardModelName.get() : ""; + auto const& rewardModel = model.getRewardModel(rewardModelName); + if (rewardModelName.empty()) { + rewardModelName = model.getUniqueRewardModelName(); + } + + STORM_LOG_THROW(rewardModel.hasOnlyStateRewards(), storm::exceptions::InvalidPropertyException, + "CVaR queries currently only support weighted reachability with state-based terminal rewards."); + + std::vector const& stateRewards = rewardModel.getStateRewardVector(); + bool hasNonZeroTargetReward = false; + for (uint64_t state = 0; state < stateRewards.size(); ++state) { + if (targetStates[state]) { + hasNonZeroTargetReward |= !storm::utility::isZero(stateRewards[state]); + } else { + STORM_LOG_THROW(storm::utility::isZero(stateRewards[state]), storm::exceptions::InvalidPropertyException, + "CVaR queries currently require terminal rewards, i.e. non-target states must have reward 0."); + } + } + + if (!hasNonZeroTargetReward) { + STORM_LOG_WARN("All target states have terminal reward 0 in reward model '" << rewardModelName << "'."); + } + + return {rewardModelName, targetStates, stateRewards}; +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 9e9c28ce50..7ecc2cd9a0 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -7,6 +7,7 @@ #include "storm/exceptions/NotImplementedException.h" #include "storm/logic/FragmentSpecification.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" #include "storm/modelchecker/helper/infinitehorizon/SparseNondeterministicInfiniteHorizonHelper.h" @@ -524,16 +525,21 @@ std::unique_ptr SparseMdpPrctlModelChecker::che template std::unique_ptr SparseMdpPrctlModelChecker::checkCvarFormula( - Environment const&, CheckTask const& checkTask) { + Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, "Computing CVaR is only supported for the initial states of a model."); STORM_LOG_THROW(this->getModel().getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, "CVaR is not supported on models with multiple initial states."); auto cvarFormulaInformation = storm::modelchecker::cvar::extractCvarFormulaInformation(checkTask.getFormula()); - static_cast(cvarFormulaInformation); - - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR model checking for sparse MDPs is not implemented yet."); + auto targetStates = + this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); + auto weightedReachabilityModelInformation = + storm::modelchecker::cvar::extractWeightedReachabilityModelInformation(this->getModel(), cvarFormulaInformation, targetStates); + + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, + "CVaR model checking for sparse MDPs is not implemented yet after validating terminal reward model '" + << weightedReachabilityModelInformation.rewardModelName << "'."); } template From cc4b624bc340a16f73b2c672c3fae5763511be09 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:22:44 +0200 Subject: [PATCH 07/65] cvar: Add backend input data for sparse MDP checking --- .../modelchecker/cvar/CvarModelCheckingData.h | 43 +++++++ .../prctl/SparseMdpPrctlModelChecker.cpp | 110 +++++++++--------- 2 files changed, 101 insertions(+), 52 deletions(-) create mode 100644 src/storm/modelchecker/cvar/CvarModelCheckingData.h diff --git a/src/storm/modelchecker/cvar/CvarModelCheckingData.h b/src/storm/modelchecker/cvar/CvarModelCheckingData.h new file mode 100644 index 0000000000..bdcd7037d9 --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarModelCheckingData.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" +#include "storm/storage/SparseMatrix.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +struct CvarModelCheckingData { + using ValueType = typename SparseMdpModelType::ValueType; + + double alpha; + storm::solver::OptimizationDirection optimizationDirection; + uint64_t initialState; + std::string rewardModelName; + storm::storage::BitVector targetStates; + std::vector terminalRewards; + storm::storage::SparseMatrix const& transitionMatrix; + SparseMdpModelType const& model; +}; + +template +CvarModelCheckingData createCvarModelCheckingData( + SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, + WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { + return {formulaInformation.alpha, + formulaInformation.optimizationDirection, + *model.getInitialStates().begin(), + weightedReachabilityModelInformation.rewardModelName, + weightedReachabilityModelInformation.targetStates, + weightedReachabilityModelInformation.terminalRewards, + model.getTransitionMatrix(), + model}; +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 7ecc2cd9a0..ab1ae9bb51 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -5,6 +5,7 @@ #include "storm/adapters/RationalNumberAdapter.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" +#include "storm/modelchecker/cvar/CvarModelCheckingData.h" #include "storm/logic/FragmentSpecification.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" @@ -51,46 +52,46 @@ bool SparseMdpPrctlModelChecker::canHandleStatic(CheckTask SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(numericResult))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique>(helper.extractScheduler(this->getModel()))); + std::make_unique >(helper.extractScheduler(this->getModel()))); } return result; @@ -257,7 +258,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(numericResult))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique>(helper.extractScheduler(this->getModel()))); + std::make_unique >(helper.extractScheduler(this->getModel()))); } return result; @@ -327,13 +328,13 @@ std::unique_ptr SparseMdpPrctlModelChecker::com } template<> -std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedCumulativeRewards( +std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedCumulativeRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } template<> -std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedCumulativeRewards( +std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedCumulativeRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } @@ -422,13 +423,13 @@ std::unique_ptr SparseMdpPrctlModelChecker::com } template<> -std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedTotalRewards( +std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedTotalRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } template<> -std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedTotalRewards( +std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedTotalRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } @@ -471,7 +472,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(values))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique>(helper.extractScheduler())); + std::make_unique >(helper.extractScheduler())); } return result; } @@ -492,7 +493,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(values))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique>(helper.extractScheduler())); + std::make_unique >(helper.extractScheduler())); } return result; } @@ -531,15 +532,20 @@ std::unique_ptr SparseMdpPrctlModelChecker::che STORM_LOG_THROW(this->getModel().getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, "CVaR is not supported on models with multiple initial states."); + // check if query fits specified format auto cvarFormulaInformation = storm::modelchecker::cvar::extractCvarFormulaInformation(checkTask.getFormula()); auto targetStates = this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); + // check if model fits terminal reward auto weightedReachabilityModelInformation = storm::modelchecker::cvar::extractWeightedReachabilityModelInformation(this->getModel(), cvarFormulaInformation, targetStates); + // combine info into 1 simplified object + auto cvarModelCheckingData = + storm::modelchecker::cvar::createCvarModelCheckingData(this->getModel(), cvarFormulaInformation, weightedReachabilityModelInformation); STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "CVaR model checking for sparse MDPs is not implemented yet after validating terminal reward model '" - << weightedReachabilityModelInformation.rewardModelName << "'."); + "CVaR model checking for sparse MDPs is not implemented yet after validating terminal reward model '" + << weightedReachabilityModelInformation.rewardModelName << "'."); } template @@ -565,9 +571,9 @@ std::unique_ptr SparseMdpPrctlModelChecker::che } } -template class SparseMdpPrctlModelChecker>; -template class SparseMdpPrctlModelChecker>; -template class SparseMdpPrctlModelChecker>; -template class SparseMdpPrctlModelChecker>; -} // namespace modelchecker -} // namespace storm +template class SparseMdpPrctlModelChecker >; +template class SparseMdpPrctlModelChecker >; +template class SparseMdpPrctlModelChecker >; +template class SparseMdpPrctlModelChecker >; +} // namespace modelchecker +} // namespace storm From e522f3b5622b21a483ce3562419dd684e70962b7 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:47:53 +0200 Subject: [PATCH 08/65] cvar: Add sparse backend helper stub --- .../modelchecker/cvar/SparseCvarHelper.h | 31 +++++++++++++++++++ .../prctl/SparseMdpPrctlModelChecker.cpp | 7 +++-- 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/storm/modelchecker/cvar/SparseCvarHelper.h diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h new file mode 100644 index 0000000000..a8de288fb3 --- /dev/null +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -0,0 +1,31 @@ +#pragma once + +#include "storm/environment/Environment.h" +#include "storm/exceptions/NotImplementedException.h" +#include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +class SparseCvarHelper { + public: + explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) : modelCheckingData(modelCheckingData) { + // Intentionally left empty. + } + + typename SparseMdpModelType::ValueType computeCvar(Environment const&) const { + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, + "CVaR model checking for sparse MDPs is not implemented yet after validating terminal reward model '" + << modelCheckingData.rewardModelName << "'."); + } + + private: + CvarModelCheckingData const& modelCheckingData; +}; + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index ab1ae9bb51..fbb4e40119 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -6,6 +6,7 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/modelchecker/cvar/SparseCvarHelper.h" #include "storm/logic/FragmentSpecification.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" @@ -543,9 +544,9 @@ std::unique_ptr SparseMdpPrctlModelChecker::che auto cvarModelCheckingData = storm::modelchecker::cvar::createCvarModelCheckingData(this->getModel(), cvarFormulaInformation, weightedReachabilityModelInformation); - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "CVaR model checking for sparse MDPs is not implemented yet after validating terminal reward model '" - << weightedReachabilityModelInformation.rewardModelName << "'."); + storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); + auto cvarValue = cvarHelper.computeCvar(env); + return std::unique_ptr(new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarValue))); } template From a1c693d9bf03669aac8e73f25c91bdd6bb5b0f05 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:13:13 +0200 Subject: [PATCH 09/65] cvar: Add thresholds and target partitions to preprocessing --- .../modelchecker/cvar/CvarModelCheckingData.h | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/storm/modelchecker/cvar/CvarModelCheckingData.h b/src/storm/modelchecker/cvar/CvarModelCheckingData.h index bdcd7037d9..129b6e755c 100644 --- a/src/storm/modelchecker/cvar/CvarModelCheckingData.h +++ b/src/storm/modelchecker/cvar/CvarModelCheckingData.h @@ -1,15 +1,28 @@ #pragma once +#include +#include #include +#include #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" +#include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" +#include "storm/utility/constants.h" namespace storm { namespace modelchecker { namespace cvar { +template +struct CvarThresholdData { + ValueType threshold; + storm::storage::BitVector targetStatesBelowThreshold; + storm::storage::BitVector targetStatesAtThreshold; + storm::storage::BitVector targetStatesBelowOrAtThreshold; +}; + template struct CvarModelCheckingData { using ValueType = typename SparseMdpModelType::ValueType; @@ -20,20 +33,61 @@ struct CvarModelCheckingData { std::string rewardModelName; storm::storage::BitVector targetStates; std::vector terminalRewards; + std::vector candidateThresholds; storm::storage::SparseMatrix const& transitionMatrix; SparseMdpModelType const& model; }; +template +std::vector collectCandidateThresholds(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards) { + std::vector candidateThresholds; + candidateThresholds.reserve(targetStates.getNumberOfSetBits()); + for (uint64_t state = 0; state < terminalRewards.size(); ++state) { + if (targetStates[state]) { + candidateThresholds.push_back(terminalRewards[state]); + } + } + std::sort(candidateThresholds.begin(), candidateThresholds.end()); + candidateThresholds.erase(std::unique(candidateThresholds.begin(), candidateThresholds.end()), candidateThresholds.end()); + return candidateThresholds; +} + +template +CvarThresholdData createCvarThresholdData(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards, + ValueType const& threshold) { + storm::storage::BitVector targetStatesBelowThreshold(targetStates.size(), false); + storm::storage::BitVector targetStatesAtThreshold(targetStates.size(), false); + storm::storage::BitVector targetStatesBelowOrAtThreshold(targetStates.size(), false); + + for (uint64_t state = 0; state < terminalRewards.size(); ++state) { + if (!targetStates[state]) { + continue; + } + if (terminalRewards[state] < threshold) { + targetStatesBelowThreshold.set(state, true); + targetStatesBelowOrAtThreshold.set(state, true); + } else if (terminalRewards[state] == threshold) { + targetStatesAtThreshold.set(state, true); + targetStatesBelowOrAtThreshold.set(state, true); + } + } + + return {threshold, targetStatesBelowThreshold, targetStatesAtThreshold, targetStatesBelowOrAtThreshold}; +} + template CvarModelCheckingData createCvarModelCheckingData( SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { + auto candidateThresholds = + collectCandidateThresholds(weightedReachabilityModelInformation.targetStates, weightedReachabilityModelInformation.terminalRewards); return {formulaInformation.alpha, formulaInformation.optimizationDirection, *model.getInitialStates().begin(), weightedReachabilityModelInformation.rewardModelName, weightedReachabilityModelInformation.targetStates, weightedReachabilityModelInformation.terminalRewards, + std::move(candidateThresholds), model.getTransitionMatrix(), model}; } From fd5d94beb201f33e21d7f0151cec4eca9d7c279e Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:32:25 +0200 Subject: [PATCH 10/65] cvar: add helper to build LP for given threshold --- .../modelchecker/cvar/SparseCvarHelper.h | 134 ++++++++++++++++-- 1 file changed, 124 insertions(+), 10 deletions(-) diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index a8de288fb3..b0f619b94f 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -1,31 +1,145 @@ #pragma once +#include +#include + #include "storm/environment/Environment.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/storage/expressions/BinaryRelationType.h" +#include "storm/utility/constants.h" #include "storm/utility/macros.h" +#include "storm/utility/solver.h" +#include "storm/solver/LpSolver.h" namespace storm { namespace modelchecker { namespace cvar { - +/*! + * Solves an LP for Conditional Value-at-Risk on an MDP with a terminal reward objective. + * @see https://doi.org/10.1145/3209108.3209176 Fig. 4 for a description of the algorithm as implemented (and slightly altered) here. + */ template class SparseCvarHelper { - public: - explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) : modelCheckingData(modelCheckingData) { +public: + explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) + : modelCheckingData(modelCheckingData) { // Intentionally left empty. } typename SparseMdpModelType::ValueType computeCvar(Environment const&) const { + using ValueType = typename SparseMdpModelType::ValueType; + STORM_LOG_THROW(!modelCheckingData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, + "CVaR model checking requires at least one target reward threshold candidate."); + + for (auto const& threshold : modelCheckingData.candidateThresholds) { + auto thresholdData = createCvarThresholdData(modelCheckingData.targetStates, modelCheckingData.terminalRewards, threshold); + buildLpForThreshold(thresholdData); + } + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "CVaR model checking for sparse MDPs is not implemented yet after validating terminal reward model '" - << modelCheckingData.rewardModelName << "'."); + "CVaR LP scaffolding created for reward model '" << modelCheckingData.rewardModelName << "' with " + << modelCheckingData.candidateThresholds.size() + << " threshold candidates."); + } + +private: + template + void buildLpForThreshold(CvarThresholdData const& thresholdData) const { + using RawLpSolver = storm::solver::LpSolver; + using RawLpConstraint = storm::solver::RawLpConstraint; + + auto lpSolverFactory = storm::utility::solver::getLpSolverFactory(); + auto solver = lpSolverFactory->createRaw("cvar"); + solver->setOptimizationDirection(modelCheckingData.optimizationDirection); + auto backwardTransitions = modelCheckingData.model.getBackwardTransitions(); + + std::vector actionFlowVariables; + actionFlowVariables.reserve(modelCheckingData.transitionMatrix.getRowCount()); + for (uint64_t row = 0; row < modelCheckingData.transitionMatrix.getRowCount(); ++row) { + actionFlowVariables.push_back( + solver->addLowerBoundedContinuousVariable("y_" + std::to_string(row), storm::utility::zero())); + } + + std::vector > recurrentFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); + for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { + if (modelCheckingData.targetStates[state]) { + recurrentFlowVariables[state] = + solver->addLowerBoundedContinuousVariable("x_" + std::to_string(state), storm::utility::zero()); + } + } + + std::vector > splitFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); + for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { + if (thresholdData.targetStatesBelowOrAtThreshold[state]) { + splitFlowVariables[state] = + solver->addLowerBoundedContinuousVariable("xb_" + std::to_string(state), storm::utility::zero(), + modelCheckingData.terminalRewards[state]); + } + } + + solver->update(); + + static_cast(splitFlowVariables); + + // Equation (2) from Fig. 4: + for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { + auto outgoingActions = modelCheckingData.transitionMatrix.getRowGroupIndices(state); + auto incomingActions = backwardTransitions.getRow(state); + uint64_t reservedSize = outgoingActions.size() + incomingActions.getNumberOfEntries() + (recurrentFlowVariables[state].has_value() ? 1 : 0); + RawLpConstraint constraint(storm::expressions::RelationType::Equal, + state == modelCheckingData.initialState ? storm::utility::one() : storm::utility::zero(), + reservedSize); + + for (auto const& incomingAction : incomingActions) { + constraint.addToLhs(actionFlowVariables[incomingAction.getColumn()], -incomingAction.getValue()); + } + for (auto const& action : outgoingActions) { + constraint.addToLhs(actionFlowVariables[action], storm::utility::one()); + } + if (recurrentFlowVariables[state].has_value()) { + constraint.addToLhs(recurrentFlowVariables[state].value(), storm::utility::one()); + } + + solver->addConstraint("transient_flow_" + std::to_string(state), constraint); + } + + // Equation (3): + RawLpConstraint recurrentConstraint(storm::expressions::RelationType::Equal, + storm::utility::one(), + modelCheckingData.targetStates.getNumberOfSetBits()); + + for (auto state : modelCheckingData.targetStates) { + recurrentConstraint.addToLhs(recurrentFlowVariables[state].value(), storm::utility::one()); + } + solver->addConstraint("recurrent_behaviour", recurrentConstraint); + + // Equation (4): + for (auto state : thresholdData.targetStatesBelowThreshold) { + RawLpConstraint splitEqualityConstraint(storm::expressions::RelationType::Equal, storm::utility::zero(), 2); + splitEqualityConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); + splitEqualityConstraint.addToLhs(recurrentFlowVariables[state].value(), -storm::utility::one()); + solver->addConstraint("split_eq_" + std::to_string(state), splitEqualityConstraint); + } + for (auto state : thresholdData.targetStatesAtThreshold) { + RawLpConstraint splitInequalityConstraint(storm::expressions::RelationType::LessOrEqual, storm::utility::zero(), 2); + splitInequalityConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); + splitInequalityConstraint.addToLhs(recurrentFlowVariables[state].value(), -storm::utility::one()); + solver->addConstraint("split_le_" + std::to_string(state), splitInequalityConstraint); + } + + // Equation (5): + RawLpConstraint probabilityConsistentSplitConstraint(storm::expressions::RelationType::Equal, + storm::utility::convertNumber(modelCheckingData.alpha), + thresholdData.targetStatesBelowOrAtThreshold.getNumberOfSetBits()); + for (auto state : thresholdData.targetStatesBelowOrAtThreshold) { + probabilityConsistentSplitConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); + } + solver->addConstraint("probability_consistent_split", probabilityConsistentSplitConstraint); } - private: CvarModelCheckingData const& modelCheckingData; }; - -} // namespace cvar -} // namespace modelchecker -} // namespace storm +} // namespace cvar +} // namespace modelchecker +} // namespace storm From abd37244af048878e91f00f80f7f68780ed35ba4 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:46:19 +0200 Subject: [PATCH 11/65] cvar: Solve threshold LPs and aggregate objective values --- .../modelchecker/cvar/SparseCvarHelper.h | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index b0f619b94f..e88f7c524f 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -4,6 +4,7 @@ #include #include "storm/environment/Environment.h" +#include "storm/exceptions/UnexpectedException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarModelCheckingData.h" #include "storm/storage/expressions/BinaryRelationType.h" @@ -32,20 +33,29 @@ class SparseCvarHelper { STORM_LOG_THROW(!modelCheckingData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, "CVaR model checking requires at least one target reward threshold candidate."); + std::optional bestValue; for (auto const& threshold : modelCheckingData.candidateThresholds) { auto thresholdData = createCvarThresholdData(modelCheckingData.targetStates, modelCheckingData.terminalRewards, threshold); - buildLpForThreshold(thresholdData); + auto thresholdValue = buildLpForThreshold(thresholdData); + if (!thresholdValue.has_value()) { + continue; + } + if (!bestValue.has_value()) { + bestValue = thresholdValue.value(); + } else if ((storm::solver::minimize(modelCheckingData.optimizationDirection) && thresholdValue.value() < bestValue.value()) || + (storm::solver::maximize(modelCheckingData.optimizationDirection) && thresholdValue.value() > bestValue.value())) { + bestValue = thresholdValue.value(); + } } - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "CVaR LP scaffolding created for reward model '" << modelCheckingData.rewardModelName << "' with " - << modelCheckingData.candidateThresholds.size() - << " threshold candidates."); + STORM_LOG_THROW(bestValue.has_value(), storm::exceptions::UnexpectedException, + "CVaR model checking did not find a feasible LP for any threshold candidate."); + return bestValue.value(); } private: template - void buildLpForThreshold(CvarThresholdData const& thresholdData) const { + std::optional buildLpForThreshold(CvarThresholdData const& thresholdData) const { using RawLpSolver = storm::solver::LpSolver; using RawLpConstraint = storm::solver::RawLpConstraint; @@ -136,6 +146,16 @@ class SparseCvarHelper { probabilityConsistentSplitConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); } solver->addConstraint("probability_consistent_split", probabilityConsistentSplitConstraint); + + solver->optimize(); + if (solver->isInfeasible()) { + return std::nullopt; + } + STORM_LOG_THROW(!solver->isUnbounded(), storm::exceptions::UnexpectedException, + "The CVaR LP for threshold " << thresholdData.threshold << " is unbounded."); + STORM_LOG_THROW(solver->isOptimal(), storm::exceptions::UnexpectedException, + "The CVaR LP for threshold " << thresholdData.threshold << " did not reach an optimal solution."); + return solver->getObjectiveValue(); } CvarModelCheckingData const& modelCheckingData; From ab360c63648413fe12e6cf0e8122b68acb3dc44b Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:52:08 +0200 Subject: [PATCH 12/65] Add CVaR query test for sparse MDPs --- .../examples/testfiles/mdp/cvar_simple_mdp.nm | 19 ++++++ .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 58 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 resources/examples/testfiles/mdp/cvar_simple_mdp.nm create mode 100644 src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp diff --git a/resources/examples/testfiles/mdp/cvar_simple_mdp.nm b/resources/examples/testfiles/mdp/cvar_simple_mdp.nm new file mode 100644 index 0000000000..cbdd275071 --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_simple_mdp.nm @@ -0,0 +1,19 @@ +mdp + +module main + s : [0..3] init 0; + + [] s=0 -> 1/2 : (s'=1) + 1/2 : (s'=2); + [] s=0 -> 1 : (s'=3); + [] s=1 -> 1 : (s'=1); + [] s=2 -> 1 : (s'=2); + [] s=3 -> 1 : (s'=3); +endmodule + +label "target" = s=1 | s=2 | s=3; + +rewards "term" + s=1 : 1; + s=2 : 3; + s=3 : 2; +endrewards diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp new file mode 100644 index 0000000000..878eff8de9 --- /dev/null +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -0,0 +1,58 @@ +#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/Environment.h" +#include "storm/modelchecker/CheckTask.h" +#include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" +#include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" +#include "storm/models/sparse/Mdp.h" +#include "storm/utility/constants.h" + +namespace { + +template +std::shared_ptr> buildCvarModel(std::string const& propertyString, double alpha) { + storm::prism::Program program = storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"); + auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, program); + std::vector cvarProperties = {storm::api::createCvarProperty(properties.front(), alpha)}; + auto formulas = storm::api::extractFormulasFromProperties(cvarProperties); + return storm::api::buildSparseModel(program, formulas)->template as>(); +} + +template +ValueType checkInitialStateValue(std::shared_ptr> const& mdp, + std::shared_ptr const& formula) { + storm::Environment env; + storm::modelchecker::SparseMdpPrctlModelChecker> checker(*mdp); + storm::modelchecker::CheckTask task(*formula, true); + auto result = checker.check(env, task); + return result->template asExplicitQuantitativeCheckResult().getMax(); +} + +TEST(CvarQueryTest, SimpleMdp) { +#if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) + GTEST_SKIP() << "No LP solver available."; +#endif + + double alpha = 0.75; + + auto maxMdp = buildCvarModel("R{\"term\"}max=? [ F \"target\" ];", alpha); + auto maxProperties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}max=? [ F \"target\" ];", + storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm")); + auto maxFormula = storm::api::createCvarProperty(maxProperties.front(), alpha).getRawFormula(); + double maxValue = checkInitialStateValue(maxMdp, maxFormula); + EXPECT_NEAR(maxValue, 2.0, 1e-10); + + auto minMdp = buildCvarModel("R{\"term\"}min=? [ F \"target\" ];", alpha); + auto minProperties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}min=? [ F \"target\" ];", + storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm")); + auto minFormula = storm::api::createCvarProperty(minProperties.front(), alpha).getRawFormula(); + double minValue = checkInitialStateValue(minMdp, minFormula); + EXPECT_NEAR(minValue, 5.0 / 3.0, 1e-10); +} + +} // namespace From 7d15fab15671c96d03b2ddcdc990695f7929eace Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:52:26 +0200 Subject: [PATCH 13/65] Fix CVaR flow LP encoding for sparse MDPs --- .../modelchecker/cvar/SparseCvarHelper.h | 20 +++++---- .../prctl/SparseMdpPrctlModelChecker.cpp | 42 ++++++++++--------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index e88f7c524f..c6dfb72bac 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -33,6 +34,7 @@ class SparseCvarHelper { STORM_LOG_THROW(!modelCheckingData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, "CVaR model checking requires at least one target reward threshold candidate."); + // checking all possible threshold values for VaR iteratively. Could possibly improved by e.g. creating a product MDP or similar. std::optional bestValue; for (auto const& threshold : modelCheckingData.candidateThresholds) { auto thresholdData = createCvarThresholdData(modelCheckingData.targetStates, modelCheckingData.terminalRewards, threshold); @@ -50,7 +52,7 @@ class SparseCvarHelper { STORM_LOG_THROW(bestValue.has_value(), storm::exceptions::UnexpectedException, "CVaR model checking did not find a feasible LP for any threshold candidate."); - return bestValue.value(); + return bestValue.value() / storm::utility::convertNumber(modelCheckingData.alpha); } private: @@ -62,7 +64,7 @@ class SparseCvarHelper { auto lpSolverFactory = storm::utility::solver::getLpSolverFactory(); auto solver = lpSolverFactory->createRaw("cvar"); solver->setOptimizationDirection(modelCheckingData.optimizationDirection); - auto backwardTransitions = modelCheckingData.model.getBackwardTransitions(); + auto backwardChoices = modelCheckingData.transitionMatrix.transpose(); std::vector actionFlowVariables; actionFlowVariables.reserve(modelCheckingData.transitionMatrix.getRowCount()); @@ -90,22 +92,26 @@ class SparseCvarHelper { solver->update(); - static_cast(splitFlowVariables); - // Equation (2) from Fig. 4: for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { auto outgoingActions = modelCheckingData.transitionMatrix.getRowGroupIndices(state); - auto incomingActions = backwardTransitions.getRow(state); + auto incomingActions = backwardChoices.getRow(state); uint64_t reservedSize = outgoingActions.size() + incomingActions.getNumberOfEntries() + (recurrentFlowVariables[state].has_value() ? 1 : 0); RawLpConstraint constraint(storm::expressions::RelationType::Equal, state == modelCheckingData.initialState ? storm::utility::one() : storm::utility::zero(), reservedSize); + std::map actionCoefficients; for (auto const& incomingAction : incomingActions) { - constraint.addToLhs(actionFlowVariables[incomingAction.getColumn()], -incomingAction.getValue()); + actionCoefficients[actionFlowVariables[incomingAction.getColumn()]] -= incomingAction.getValue(); } for (auto const& action : outgoingActions) { - constraint.addToLhs(actionFlowVariables[action], storm::utility::one()); + actionCoefficients[actionFlowVariables[action]] += storm::utility::one(); + } + for (auto const& actionCoefficient : actionCoefficients) { + if (!storm::utility::isZero(actionCoefficient.second)) { + constraint.addToLhs(actionCoefficient.first, actionCoefficient.second); + } } if (recurrentFlowVariables[state].has_value()) { constraint.addToLhs(recurrentFlowVariables[state].value(), storm::utility::one()); diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index fbb4e40119..ae2fa0a8e5 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -528,25 +528,29 @@ std::unique_ptr SparseMdpPrctlModelChecker::che template std::unique_ptr SparseMdpPrctlModelChecker::checkCvarFormula( Environment const& env, CheckTask const& checkTask) { - STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, - "Computing CVaR is only supported for the initial states of a model."); - STORM_LOG_THROW(this->getModel().getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, - "CVaR is not supported on models with multiple initial states."); - - // check if query fits specified format - auto cvarFormulaInformation = storm::modelchecker::cvar::extractCvarFormulaInformation(checkTask.getFormula()); - auto targetStates = - this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); - // check if model fits terminal reward - auto weightedReachabilityModelInformation = - storm::modelchecker::cvar::extractWeightedReachabilityModelInformation(this->getModel(), cvarFormulaInformation, targetStates); - // combine info into 1 simplified object - auto cvarModelCheckingData = - storm::modelchecker::cvar::createCvarModelCheckingData(this->getModel(), cvarFormulaInformation, weightedReachabilityModelInformation); - - storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); - auto cvarValue = cvarHelper.computeCvar(env); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarValue))); + if constexpr (storm::IsIntervalType) { + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR formulas are not supported for interval models."); + } else { + STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, + "Computing CVaR is only supported for the initial states of a model."); + STORM_LOG_THROW(this->getModel().getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, + "CVaR is not supported on models with multiple initial states."); + + // check if query fits specified format + auto cvarFormulaInformation = storm::modelchecker::cvar::extractCvarFormulaInformation(checkTask.getFormula()); + auto targetStates = + this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); + // check if model fits terminal reward + auto weightedReachabilityModelInformation = + storm::modelchecker::cvar::extractWeightedReachabilityModelInformation(this->getModel(), cvarFormulaInformation, targetStates); + // combine info into 1 simplified object + auto cvarModelCheckingData = + storm::modelchecker::cvar::createCvarModelCheckingData(this->getModel(), cvarFormulaInformation, weightedReachabilityModelInformation); + + storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); + auto cvarValue = cvarHelper.computeCvar(env); + return std::unique_ptr(new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarValue))); + } } template From 06911bb5ce144733cecd94972fb57b34573f1be3 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:08:28 +0200 Subject: [PATCH 14/65] cvar: Add preprocessing of unreachable-target MECs --- .../modelchecker/cvar/CvarModelCheckingData.h | 16 ++-- .../modelchecker/cvar/SparseCvarHelper.h | 10 +-- .../WeightedReachabilityModelInformation.h | 74 +++++++++++++++++-- .../prctl/SparseMdpPrctlModelChecker.cpp | 2 +- 4 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/storm/modelchecker/cvar/CvarModelCheckingData.h b/src/storm/modelchecker/cvar/CvarModelCheckingData.h index 129b6e755c..302380e922 100644 --- a/src/storm/modelchecker/cvar/CvarModelCheckingData.h +++ b/src/storm/modelchecker/cvar/CvarModelCheckingData.h @@ -23,10 +23,8 @@ struct CvarThresholdData { storm::storage::BitVector targetStatesBelowOrAtThreshold; }; -template +template struct CvarModelCheckingData { - using ValueType = typename SparseMdpModelType::ValueType; - double alpha; storm::solver::OptimizationDirection optimizationDirection; uint64_t initialState; @@ -34,8 +32,7 @@ struct CvarModelCheckingData { storm::storage::BitVector targetStates; std::vector terminalRewards; std::vector candidateThresholds; - storm::storage::SparseMatrix const& transitionMatrix; - SparseMdpModelType const& model; + storm::storage::SparseMatrix transitionMatrix; }; template @@ -76,20 +73,19 @@ CvarThresholdData createCvarThresholdData(storm::storage::BitVector c } template -CvarModelCheckingData createCvarModelCheckingData( +CvarModelCheckingData createCvarModelCheckingData( SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { auto candidateThresholds = - collectCandidateThresholds(weightedReachabilityModelInformation.targetStates, weightedReachabilityModelInformation.terminalRewards); + collectCandidateThresholds(weightedReachabilityModelInformation.effectiveTargetStates, weightedReachabilityModelInformation.terminalRewards); return {formulaInformation.alpha, formulaInformation.optimizationDirection, *model.getInitialStates().begin(), weightedReachabilityModelInformation.rewardModelName, - weightedReachabilityModelInformation.targetStates, + weightedReachabilityModelInformation.effectiveTargetStates, weightedReachabilityModelInformation.terminalRewards, std::move(candidateThresholds), - model.getTransitionMatrix(), - model}; + weightedReachabilityModelInformation.transitionMatrix}; } } // namespace cvar diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index c6dfb72bac..7f6f24a7ec 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -21,16 +21,15 @@ namespace cvar { * Solves an LP for Conditional Value-at-Risk on an MDP with a terminal reward objective. * @see https://doi.org/10.1145/3209108.3209176 Fig. 4 for a description of the algorithm as implemented (and slightly altered) here. */ -template +template class SparseCvarHelper { public: - explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) + explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) : modelCheckingData(modelCheckingData) { // Intentionally left empty. } - typename SparseMdpModelType::ValueType computeCvar(Environment const&) const { - using ValueType = typename SparseMdpModelType::ValueType; + ValueType computeCvar(Environment const&) const { STORM_LOG_THROW(!modelCheckingData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, "CVaR model checking requires at least one target reward threshold candidate."); @@ -56,7 +55,6 @@ class SparseCvarHelper { } private: - template std::optional buildLpForThreshold(CvarThresholdData const& thresholdData) const { using RawLpSolver = storm::solver::LpSolver; using RawLpConstraint = storm::solver::RawLpConstraint; @@ -164,7 +162,7 @@ class SparseCvarHelper { return solver->getObjectiveValue(); } - CvarModelCheckingData const& modelCheckingData; + CvarModelCheckingData const& modelCheckingData; }; } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h index 7d219e3306..3cadea093c 100644 --- a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h +++ b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h @@ -6,21 +6,61 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/storage/BitVector.h" +#include "storm/storage/MaximalEndComponentDecomposition.h" +#include "storm/storage/SparseMatrix.h" #include "storm/utility/constants.h" +#include "storm/utility/graph.h" #include "storm/utility/logging.h" #include "storm/utility/macros.h" namespace storm { namespace modelchecker { namespace cvar { - template struct WeightedReachabilityModelInformation { std::string rewardModelName; - storm::storage::BitVector targetStates; + storm::storage::BitVector originalTargetStates; + storm::storage::BitVector effectiveTargetStates; + storm::storage::BitVector badMecStates; std::vector terminalRewards; + storm::storage::SparseMatrix transitionMatrix; }; +template +void validateTargetStatesAreAbsorbing(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& targetStates) { + for (auto targetState : targetStates) { + for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; + ++row) { + for (auto const& entry : transitionMatrix.getRow(row)) { + STORM_LOG_THROW(entry.getColumn() == targetState, storm::exceptions::InvalidPropertyException, + "CVaR queries currently require all original target states to be absorbing."); + } + } + } +} + +template +storm::storage::BitVector computeBadMecStates(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::BitVector const& initialStates, + storm::storage::BitVector const& targetStates) { + storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); + storm::storage::BitVector noStates(transitionMatrix.getRowGroupCount(), false); + auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, initialStates, allStates, noStates); + auto backwardTransitions = transitionMatrix.transpose(true); + auto statesThatCanReachTarget = storm::utility::graph::performProbGreater0(backwardTransitions, allStates, targetStates); + storm::storage::MaximalEndComponentDecomposition mecs(transitionMatrix, backwardTransitions, reachableStates); + + storm::storage::BitVector badMecStates(transitionMatrix.getRowGroupCount(), false); + for (auto const& mec : mecs) { + if (!mec.containsAnyState(statesThatCanReachTarget)) { + for (auto const& stateChoices : mec) { + badMecStates.set(stateChoices.first, true); + } + } + } + return badMecStates; +} + template WeightedReachabilityModelInformation extractWeightedReachabilityModelInformation( SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, storm::storage::BitVector const& targetStates) { @@ -36,6 +76,8 @@ WeightedReachabilityModelInformation ext "CVaR queries currently only support weighted reachability with state-based terminal rewards."); std::vector const& stateRewards = rewardModel.getStateRewardVector(); + validateTargetStatesAreAbsorbing(model.getTransitionMatrix(), targetStates); + bool hasNonZeroTargetReward = false; for (uint64_t state = 0; state < stateRewards.size(); ++state) { if (targetStates[state]) { @@ -50,9 +92,27 @@ WeightedReachabilityModelInformation ext STORM_LOG_WARN("All target states have terminal reward 0 in reward model '" << rewardModelName << "'."); } - return {rewardModelName, targetStates, stateRewards}; -} + auto badMecStates = computeBadMecStates(model.getTransitionMatrix(), model.getInitialStates(), targetStates); + auto effectiveTargetStates = targetStates | badMecStates; + auto terminalRewards = stateRewards; + auto transitionMatrix = model.getTransitionMatrix(); -} // namespace cvar -} // namespace modelchecker -} // namespace storm + if (!badMecStates.empty()) { + STORM_LOG_INFO( + "CVaR preprocessing converted reachable end components that cannot reach the original target set into zero-reward absorbing terminal behaviour."); + transitionMatrix.makeRowGroupsAbsorbing(badMecStates, true); + for (auto state : badMecStates) { + terminalRewards[state] = storm::utility::zero(); + } + } + + return {rewardModelName, + targetStates, + effectiveTargetStates, + badMecStates, + std::move(terminalRewards), + std::move(transitionMatrix)}; +} +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index ae2fa0a8e5..7032a574f2 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -547,7 +547,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::che auto cvarModelCheckingData = storm::modelchecker::cvar::createCvarModelCheckingData(this->getModel(), cvarFormulaInformation, weightedReachabilityModelInformation); - storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); + storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); auto cvarValue = cvarHelper.computeCvar(env); return std::unique_ptr(new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarValue))); } From 34f50d8d20e442f86c4f07697a8d9bb908b58a8b Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:28:21 +0200 Subject: [PATCH 15/65] Add CVaR preprocessing tests --- .../testfiles/mdp/cvar_bad_mec_mdp.nm | 16 ++++++ .../mdp/cvar_nonabsorbing_target_mdp.nm | 15 +++++ .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 55 +++++++++++++++---- 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 resources/examples/testfiles/mdp/cvar_bad_mec_mdp.nm create mode 100644 resources/examples/testfiles/mdp/cvar_nonabsorbing_target_mdp.nm diff --git a/resources/examples/testfiles/mdp/cvar_bad_mec_mdp.nm b/resources/examples/testfiles/mdp/cvar_bad_mec_mdp.nm new file mode 100644 index 0000000000..529ef1306a --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_bad_mec_mdp.nm @@ -0,0 +1,16 @@ +mdp + +module main + s : [0..3] init 0; + + [] s=0 -> 1/2 : (s'=1) + 1/2 : (s'=2); + [] s=1 -> 1 : (s'=1); + [] s=2 -> 1 : (s'=3); + [] s=3 -> 1 : (s'=2); +endmodule + +label "target" = s=1; + +rewards "term" + s=1 : 4; +endrewards diff --git a/resources/examples/testfiles/mdp/cvar_nonabsorbing_target_mdp.nm b/resources/examples/testfiles/mdp/cvar_nonabsorbing_target_mdp.nm new file mode 100644 index 0000000000..7dd60967b8 --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_nonabsorbing_target_mdp.nm @@ -0,0 +1,15 @@ +mdp + +module main + s : [0..2] init 0; + + [] s=0 -> 1 : (s'=1); + [] s=1 -> 1 : (s'=2); + [] s=2 -> 1 : (s'=2); +endmodule + +label "target" = s=1; + +rewards "term" + s=1 : 5; +endrewards diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 878eff8de9..67cb086448 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -6,6 +6,7 @@ #include "storm/api/builder.h" #include "storm/api/properties.h" #include "storm/environment/Environment.h" +#include "storm/exceptions/InvalidPropertyException.h" #include "storm/modelchecker/CheckTask.h" #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" @@ -15,14 +16,19 @@ namespace { template -std::shared_ptr> buildCvarModel(std::string const& propertyString, double alpha) { - storm::prism::Program program = storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"); +std::shared_ptr> buildCvarModel(std::string const& modelPath, std::string const& propertyString, double alpha) { + storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, program); std::vector cvarProperties = {storm::api::createCvarProperty(properties.front(), alpha)}; auto formulas = storm::api::extractFormulasFromProperties(cvarProperties); return storm::api::buildSparseModel(program, formulas)->template as>(); } +std::shared_ptr buildCvarFormula(std::string const& modelPath, std::string const& propertyString, double alpha) { + auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, storm::api::parseProgram(modelPath)); + return storm::api::createCvarProperty(properties.front(), alpha).getRawFormula(); +} + template ValueType checkInitialStateValue(std::shared_ptr> const& mdp, std::shared_ptr const& formula) { @@ -39,20 +45,49 @@ TEST(CvarQueryTest, SimpleMdp) { #endif double alpha = 0.75; + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; - auto maxMdp = buildCvarModel("R{\"term\"}max=? [ F \"target\" ];", alpha); - auto maxProperties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}max=? [ F \"target\" ];", - storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm")); - auto maxFormula = storm::api::createCvarProperty(maxProperties.front(), alpha).getRawFormula(); + auto maxMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + auto maxFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); double maxValue = checkInitialStateValue(maxMdp, maxFormula); EXPECT_NEAR(maxValue, 2.0, 1e-10); - auto minMdp = buildCvarModel("R{\"term\"}min=? [ F \"target\" ];", alpha); - auto minProperties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}min=? [ F \"target\" ];", - storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm")); - auto minFormula = storm::api::createCvarProperty(minProperties.front(), alpha).getRawFormula(); + auto minMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); + auto minFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); double minValue = checkInitialStateValue(minMdp, minFormula); EXPECT_NEAR(minValue, 5.0 / 3.0, 1e-10); } +TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { +#if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) + GTEST_SKIP() << "No LP solver available."; +#endif + + double alpha = 0.5; + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_bad_mec_mdp.nm"; + + auto maxMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + auto maxFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + double maxValue = checkInitialStateValue(maxMdp, maxFormula); + EXPECT_NEAR(maxValue, 0.0, 1e-10); + + auto minMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); + auto minFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); + double minValue = checkInitialStateValue(minMdp, minFormula); + EXPECT_NEAR(minValue, 0.0, 1e-10); +} + +TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { + double alpha = 0.5; + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_nonabsorbing_target_mdp.nm"; + + auto mdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + auto formula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + + storm::Environment env; + storm::modelchecker::SparseMdpPrctlModelChecker> checker(*mdp); + storm::modelchecker::CheckTask task(*formula, true); + STORM_SILENT_EXPECT_THROW(checker.check(env, task), storm::exceptions::InvalidPropertyException); +} + } // namespace From de2c0de17cbf91bd61dc7a3e1c0253933a3937e8 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:37:55 +0200 Subject: [PATCH 16/65] Add CVaR test MDP with alpha-sensitive and adaptive scheduling cases --- .../mdp/cvar_branching_tradeoff_mdp.nm | 41 +++++++++++++++++++ .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 37 +++++++++++++---- 2 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 resources/examples/testfiles/mdp/cvar_branching_tradeoff_mdp.nm diff --git a/resources/examples/testfiles/mdp/cvar_branching_tradeoff_mdp.nm b/resources/examples/testfiles/mdp/cvar_branching_tradeoff_mdp.nm new file mode 100644 index 0000000000..2c2738887f --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_branching_tradeoff_mdp.nm @@ -0,0 +1,41 @@ +mdp + +module main + s : [0..10] init 0; + + // Start: direct safe/balanced/risky options, plus an adaptive branch. + [safe] s=0 -> 1 : (s'=3); + [balanced] s=0 -> 3/4 : (s'=1) + 1/4 : (s'=5); + [adaptive] s=0 -> 1/2 : (s'=8) + 1/2 : (s'=9); + [risky] s=0 -> 1/2 : (s'=10) + 1/2 : (s'=7); + + // Duplicate terminal rewards at 6 and 12 make threshold splitting observable. + [] s=1 -> 1 : (s'=1); + [] s=2 -> 1 : (s'=2); + [] s=3 -> 1 : (s'=3); + [] s=4 -> 1 : (s'=4); + [] s=5 -> 1 : (s'=5); + [] s=6 -> 1 : (s'=6); + [] s=7 -> 1 : (s'=7); + + // After the adaptive probabilistic split, the scheduler can react to good/bad news. + [cash] s=8 -> 1 : (s'=4); + [push] s=8 -> 1/2 : (s'=6) + 1/2 : (s'=7); + + [cash] s=9 -> 1 : (s'=2); + [push] s=9 -> 1/2 : (s'=10) + 1/2 : (s'=4); + + [] s=10 -> 1 : (s'=10); +endmodule + +label "target" = s=1 | s=2 | s=3 | s=4 | s=5 | s=6 | s=7 | s=10; + +rewards "term" + s=1 : 6; + s=2 : 6; + s=3 : 7; + s=4 : 10; + s=5 : 12; + s=6 : 12; + s=7 : 20; +endrewards diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 67cb086448..df0a755c9c 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -14,14 +14,13 @@ #include "storm/utility/constants.h" namespace { - template -std::shared_ptr> buildCvarModel(std::string const& modelPath, std::string const& propertyString, double alpha) { +std::shared_ptr > buildCvarModel(std::string const& modelPath, std::string const& propertyString, double alpha) { storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, program); std::vector cvarProperties = {storm::api::createCvarProperty(properties.front(), alpha)}; auto formulas = storm::api::extractFormulasFromProperties(cvarProperties); - return storm::api::buildSparseModel(program, formulas)->template as>(); + return storm::api::buildSparseModel(program, formulas)->template as >(); } std::shared_ptr buildCvarFormula(std::string const& modelPath, std::string const& propertyString, double alpha) { @@ -30,10 +29,10 @@ std::shared_ptr buildCvarFormula(std::string const& } template -ValueType checkInitialStateValue(std::shared_ptr> const& mdp, +ValueType checkInitialStateValue(std::shared_ptr > const& mdp, std::shared_ptr const& formula) { storm::Environment env; - storm::modelchecker::SparseMdpPrctlModelChecker> checker(*mdp); + storm::modelchecker::SparseMdpPrctlModelChecker > checker(*mdp); storm::modelchecker::CheckTask task(*formula, true); auto result = checker.check(env, task); return result->template asExplicitQuantitativeCheckResult().getMax(); @@ -85,9 +84,33 @@ TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { auto formula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); storm::Environment env; - storm::modelchecker::SparseMdpPrctlModelChecker> checker(*mdp); + storm::modelchecker::SparseMdpPrctlModelChecker > checker(*mdp); storm::modelchecker::CheckTask task(*formula, true); STORM_SILENT_EXPECT_THROW(checker.check(env, task), storm::exceptions::InvalidPropertyException); } -} // namespace +TEST(CvarQueryTest, BranchingTradeoffMdp) { +#if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) + GTEST_SKIP() << "No LP solver available."; +#endif + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; + + auto maxHalfMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.5); + auto maxHalfFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.5); + EXPECT_NEAR(checkInitialStateValue(maxHalfMdp, maxHalfFormula), 7.0, 1e-10); + + auto maxThreeQuarterMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + auto maxThreeQuarterFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + EXPECT_NEAR(checkInitialStateValue(maxThreeQuarterMdp, maxThreeQuarterFormula), 8.0, 1e-10); + + auto minHalfMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.5); + auto minHalfFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.5); + EXPECT_NEAR(checkInitialStateValue(minHalfMdp, minHalfFormula), 0.0, 1e-10); + + // this requires randomization of the strategy + auto minThreeQuarterMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + auto minThreeQuarterFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + EXPECT_NEAR(checkInitialStateValue(minThreeQuarterMdp, minThreeQuarterFormula), 14.0 / 3.0, 1e-10); +} +} // namespace From b33ac31f789f681a0b5e68d2884030fa04bc9609 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:02:12 +0200 Subject: [PATCH 17/65] Apply code formatting --- .../cvar/CvarFormulaInformation.cpp | 9 +- .../modelchecker/cvar/SparseCvarHelper.h | 3 +- .../WeightedReachabilityModelInformation.h | 16 +-- .../prctl/SparseMdpPrctlModelChecker.cpp | 104 +++++++++--------- .../prctl/SparseMdpPrctlModelChecker.h | 3 +- src/storm/settings/modules/IOSettings.cpp | 3 +- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 8 +- 7 files changed, 69 insertions(+), 77 deletions(-) diff --git a/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp b/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp index 1416ffdcf4..ed68ed4d59 100644 --- a/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp +++ b/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp @@ -3,9 +3,8 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/logic/EventuallyFormula.h" #include "storm/logic/RewardOperatorFormula.h" -#include "storm/utility/macros.h" #include "storm/utility/logging.h" - +#include "storm/utility/macros.h" namespace storm { namespace modelchecker { @@ -34,6 +33,6 @@ CvarFormulaInformation extractCvarFormulaInformation(storm::logic::CvarFormula c return {formula.getAlpha(), rewardOperator.getOptimalityType(), rewardOperator.getOptionalRewardModelName(), eventuallyFormula.getSubformula().asSharedPointer()}; } -} // namespace cvar -} // namespace modelchecker -} // namespace storm +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index 7f6f24a7ec..c140f715a1 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -119,8 +119,7 @@ class SparseCvarHelper { } // Equation (3): - RawLpConstraint recurrentConstraint(storm::expressions::RelationType::Equal, - storm::utility::one(), + RawLpConstraint recurrentConstraint(storm::expressions::RelationType::Equal, storm::utility::one(), modelCheckingData.targetStates.getNumberOfSetBits()); for (auto state : modelCheckingData.targetStates) { diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h index 3cadea093c..d01d8a784a 100644 --- a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h +++ b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h @@ -40,8 +40,7 @@ void validateTargetStatesAreAbsorbing(storm::storage::SparseMatrix co } template -storm::storage::BitVector computeBadMecStates(storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::BitVector const& initialStates, +storm::storage::BitVector computeBadMecStates(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& initialStates, storm::storage::BitVector const& targetStates) { storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); storm::storage::BitVector noStates(transitionMatrix.getRowGroupCount(), false); @@ -106,13 +105,8 @@ WeightedReachabilityModelInformation ext } } - return {rewardModelName, - targetStates, - effectiveTargetStates, - badMecStates, - std::move(terminalRewards), - std::move(transitionMatrix)}; + return {rewardModelName, targetStates, effectiveTargetStates, badMecStates, std::move(terminalRewards), std::move(transitionMatrix)}; } -} // namespace cvar -} // namespace modelchecker -} // namespace storm +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 7032a574f2..3e9f40d509 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -5,10 +5,10 @@ #include "storm/adapters/RationalNumberAdapter.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" -#include "storm/modelchecker/cvar/CvarModelCheckingData.h" -#include "storm/modelchecker/cvar/SparseCvarHelper.h" #include "storm/logic/FragmentSpecification.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/modelchecker/cvar/SparseCvarHelper.h" #include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" @@ -53,46 +53,46 @@ bool SparseMdpPrctlModelChecker::canHandleStatic(CheckTask SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(numericResult))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique >(helper.extractScheduler(this->getModel()))); + std::make_unique>(helper.extractScheduler(this->getModel()))); } return result; @@ -259,7 +259,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(numericResult))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique >(helper.extractScheduler(this->getModel()))); + std::make_unique>(helper.extractScheduler(this->getModel()))); } return result; @@ -329,13 +329,13 @@ std::unique_ptr SparseMdpPrctlModelChecker::com } template<> -std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedCumulativeRewards( +std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedCumulativeRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } template<> -std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedCumulativeRewards( +std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedCumulativeRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } @@ -424,13 +424,13 @@ std::unique_ptr SparseMdpPrctlModelChecker::com } template<> -std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedTotalRewards( +std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedTotalRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } template<> -std::unique_ptr SparseMdpPrctlModelChecker >::computeDiscountedTotalRewards( +std::unique_ptr SparseMdpPrctlModelChecker>::computeDiscountedTotalRewards( Environment const& env, CheckTask const& checkTask) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Discounted properties are not implemented for interval models."); } @@ -473,7 +473,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(values))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique >(helper.extractScheduler())); + std::make_unique>(helper.extractScheduler())); } return result; } @@ -494,7 +494,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::com std::unique_ptr result(new ExplicitQuantitativeCheckResult(std::move(values))); if (checkTask.isProduceSchedulersSet()) { result->asExplicitQuantitativeCheckResult().setScheduler( - std::make_unique >(helper.extractScheduler())); + std::make_unique>(helper.extractScheduler())); } return result; } @@ -576,9 +576,9 @@ std::unique_ptr SparseMdpPrctlModelChecker::che } } -template class SparseMdpPrctlModelChecker >; -template class SparseMdpPrctlModelChecker >; -template class SparseMdpPrctlModelChecker >; -template class SparseMdpPrctlModelChecker >; -} // namespace modelchecker -} // namespace storm +template class SparseMdpPrctlModelChecker>; +template class SparseMdpPrctlModelChecker>; +template class SparseMdpPrctlModelChecker>; +template class SparseMdpPrctlModelChecker>; +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h index d0e9949cb2..987209b8e4 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h @@ -62,8 +62,7 @@ class SparseMdpPrctlModelChecker : public SparsePropositionalModelChecker const& checkTask) override; virtual std::unique_ptr checkLexObjectiveFormula(Environment const& env, CheckTask const& checkTask) override; - virtual std::unique_ptr checkCvarFormula(Environment const& env, - CheckTask const& checkTask) override; + virtual std::unique_ptr checkCvarFormula(Environment const& env, CheckTask const& checkTask) override; virtual std::unique_ptr checkQuantileFormula(Environment const& env, CheckTask const& checkTask) override; }; diff --git a/src/storm/settings/modules/IOSettings.cpp b/src/storm/settings/modules/IOSettings.cpp index 1bda82a702..48b83079ab 100644 --- a/src/storm/settings/modules/IOSettings.cpp +++ b/src/storm/settings/modules/IOSettings.cpp @@ -282,8 +282,7 @@ IOSettings::IOSettings() : ModuleSettings(moduleName) { .setIsAdvanced() .build()); - this->addOption(storm::settings::OptionBuilder(moduleName, cvarOptionName, false, - "Computes the conditional value-at-risk for the selected property.") + this->addOption(storm::settings::OptionBuilder(moduleName, cvarOptionName, false, "Computes the conditional value-at-risk for the selected property.") .addArgument(storm::settings::ArgumentBuilder::createDoubleArgument("alpha", "The size of the lower tail.") .addValidatorDouble(storm::settings::ArgumentValidatorFactory::createDoubleRangeValidatorExcluding(0.0, 1.0)) .build()) diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index df0a755c9c..d76fe121ed 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -13,14 +13,16 @@ #include "storm/models/sparse/Mdp.h" #include "storm/utility/constants.h" +#include + namespace { template -std::shared_ptr > buildCvarModel(std::string const& modelPath, std::string const& propertyString, double alpha) { +std::shared_ptr> buildCvarModel(std::string const& modelPath, std::string const& propertyString, double alpha) { storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, program); std::vector cvarProperties = {storm::api::createCvarProperty(properties.front(), alpha)}; auto formulas = storm::api::extractFormulasFromProperties(cvarProperties); - return storm::api::buildSparseModel(program, formulas)->template as >(); + return storm::api::buildSparseModel(program, formulas)->template as>(); } std::shared_ptr buildCvarFormula(std::string const& modelPath, std::string const& propertyString, double alpha) { @@ -84,7 +86,7 @@ TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { auto formula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); storm::Environment env; - storm::modelchecker::SparseMdpPrctlModelChecker > checker(*mdp); + storm::modelchecker::SparseMdpPrctlModelChecker> checker(*mdp); storm::modelchecker::CheckTask task(*formula, true); STORM_SILENT_EXPECT_THROW(checker.check(env, task), storm::exceptions::InvalidPropertyException); } From 2001c17bc837246145c070e3cb9d08b9ad921f36 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:11:06 +0200 Subject: [PATCH 18/65] cvar: Add scheduler reconstruction via LP flow variables --- .../modelchecker/cvar/SparseCvarHelper.h | 98 ++++++++++++++----- .../prctl/SparseMdpPrctlModelChecker.cpp | 9 +- 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index c140f715a1..ea96a6101a 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -1,61 +1,75 @@ #pragma once #include +#include #include #include #include "storm/environment/Environment.h" -#include "storm/exceptions/UnexpectedException.h" #include "storm/exceptions/NotImplementedException.h" +#include "storm/exceptions/UnexpectedException.h" #include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/solver/LpSolver.h" +#include "storm/storage/Scheduler.h" #include "storm/storage/expressions/BinaryRelationType.h" #include "storm/utility/constants.h" #include "storm/utility/macros.h" #include "storm/utility/solver.h" -#include "storm/solver/LpSolver.h" namespace storm { namespace modelchecker { namespace cvar { + +template +struct CvarComputationResult { + ValueType value; + std::unique_ptr> scheduler; +}; /*! * Solves an LP for Conditional Value-at-Risk on an MDP with a terminal reward objective. * @see https://doi.org/10.1145/3209108.3209176 Fig. 4 for a description of the algorithm as implemented (and slightly altered) here. */ template class SparseCvarHelper { -public: - explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) - : modelCheckingData(modelCheckingData) { + public: + explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) : modelCheckingData(modelCheckingData) { // Intentionally left empty. } - ValueType computeCvar(Environment const&) const { + CvarComputationResult computeCvar(Environment const&, bool produceScheduler = false) const { STORM_LOG_THROW(!modelCheckingData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, "CVaR model checking requires at least one target reward threshold candidate."); // checking all possible threshold values for VaR iteratively. Could possibly improved by e.g. creating a product MDP or similar. std::optional bestValue; + std::unique_ptr> bestScheduler; for (auto const& threshold : modelCheckingData.candidateThresholds) { auto thresholdData = createCvarThresholdData(modelCheckingData.targetStates, modelCheckingData.terminalRewards, threshold); - auto thresholdValue = buildLpForThreshold(thresholdData); - if (!thresholdValue.has_value()) { + auto thresholdResult = buildLpForThreshold(thresholdData, produceScheduler); + if (!thresholdResult.has_value()) { continue; } if (!bestValue.has_value()) { - bestValue = thresholdValue.value(); - } else if ((storm::solver::minimize(modelCheckingData.optimizationDirection) && thresholdValue.value() < bestValue.value()) || - (storm::solver::maximize(modelCheckingData.optimizationDirection) && thresholdValue.value() > bestValue.value())) { - bestValue = thresholdValue.value(); + bestValue = thresholdResult->value; + if (produceScheduler) { + bestScheduler = std::move(thresholdResult->scheduler); + } + } else if ((storm::solver::minimize(modelCheckingData.optimizationDirection) && thresholdResult->value < bestValue.value()) || + (storm::solver::maximize(modelCheckingData.optimizationDirection) && thresholdResult->value > bestValue.value())) { + bestValue = thresholdResult->value; + if (produceScheduler) { + bestScheduler = std::move(thresholdResult->scheduler); + } } } STORM_LOG_THROW(bestValue.has_value(), storm::exceptions::UnexpectedException, "CVaR model checking did not find a feasible LP for any threshold candidate."); - return bestValue.value() / storm::utility::convertNumber(modelCheckingData.alpha); + return {bestValue.value() / storm::utility::convertNumber(modelCheckingData.alpha), std::move(bestScheduler)}; } private: - std::optional buildLpForThreshold(CvarThresholdData const& thresholdData) const { + std::optional> buildLpForThreshold(CvarThresholdData const& thresholdData, bool produceScheduler) const { using RawLpSolver = storm::solver::LpSolver; using RawLpConstraint = storm::solver::RawLpConstraint; @@ -67,24 +81,21 @@ class SparseCvarHelper { std::vector actionFlowVariables; actionFlowVariables.reserve(modelCheckingData.transitionMatrix.getRowCount()); for (uint64_t row = 0; row < modelCheckingData.transitionMatrix.getRowCount(); ++row) { - actionFlowVariables.push_back( - solver->addLowerBoundedContinuousVariable("y_" + std::to_string(row), storm::utility::zero())); + actionFlowVariables.push_back(solver->addLowerBoundedContinuousVariable("y_" + std::to_string(row), storm::utility::zero())); } - std::vector > recurrentFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); + std::vector> recurrentFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { if (modelCheckingData.targetStates[state]) { - recurrentFlowVariables[state] = - solver->addLowerBoundedContinuousVariable("x_" + std::to_string(state), storm::utility::zero()); + recurrentFlowVariables[state] = solver->addLowerBoundedContinuousVariable("x_" + std::to_string(state), storm::utility::zero()); } } - std::vector > splitFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); + std::vector> splitFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { if (thresholdData.targetStatesBelowOrAtThreshold[state]) { - splitFlowVariables[state] = - solver->addLowerBoundedContinuousVariable("xb_" + std::to_string(state), storm::utility::zero(), - modelCheckingData.terminalRewards[state]); + splitFlowVariables[state] = solver->addLowerBoundedContinuousVariable("xb_" + std::to_string(state), storm::utility::zero(), + modelCheckingData.terminalRewards[state]); } } @@ -158,11 +169,44 @@ class SparseCvarHelper { "The CVaR LP for threshold " << thresholdData.threshold << " is unbounded."); STORM_LOG_THROW(solver->isOptimal(), storm::exceptions::UnexpectedException, "The CVaR LP for threshold " << thresholdData.threshold << " did not reach an optimal solution."); - return solver->getObjectiveValue(); + + std::unique_ptr> scheduler; + if (produceScheduler) { + scheduler = std::make_unique>(modelCheckingData.transitionMatrix.getRowGroupCount()); + for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { + if (modelCheckingData.targetStates[state]) { + scheduler->setDontCare(state); + continue; + } + + uint64_t firstRow = modelCheckingData.transitionMatrix.getRowGroupIndices()[state]; + uint64_t lastRow = modelCheckingData.transitionMatrix.getRowGroupIndices()[state + 1]; + storm::storage::Distribution actionDistribution; + actionDistribution.reserve(lastRow - firstRow); + + for (uint64_t row = firstRow; row < lastRow; ++row) { + auto flow = solver->getContinuousValue(actionFlowVariables[row]); + if (storm::utility::isAlmostZero(flow)) { + continue; + } + actionDistribution.addProbability(row - firstRow, flow); + } + + if (actionDistribution.size() == 0) { + scheduler->setDontCare(state); + continue; + } + + actionDistribution.normalize(); + scheduler->setChoice(storm::storage::SchedulerChoice(std::move(actionDistribution)), state); + } + } + + return CvarComputationResult{solver->getObjectiveValue(), std::move(scheduler)}; } CvarModelCheckingData const& modelCheckingData; }; -} // namespace cvar -} // namespace modelchecker -} // namespace storm +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 3e9f40d509..eb03165953 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -548,8 +548,13 @@ std::unique_ptr SparseMdpPrctlModelChecker::che storm::modelchecker::cvar::createCvarModelCheckingData(this->getModel(), cvarFormulaInformation, weightedReachabilityModelInformation); storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); - auto cvarValue = cvarHelper.computeCvar(env); - return std::unique_ptr(new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarValue))); + auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); + std::unique_ptr result( + new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarResult.value))); + if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { + result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); + } + return result; } } From f962d59f933d2e54509ce6520400bb4bf0ebbc79 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:10:09 +0200 Subject: [PATCH 19/65] Add CVaR scheduler tests and simplify CVaR test setup --- .../modelchecker/cvar/SparseCvarHelper.h | 2 +- .../prctl/SparseMdpPrctlModelChecker.cpp | 3 +- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 183 +++++++++++++----- 3 files changed, 135 insertions(+), 53 deletions(-) diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index ea96a6101a..036a3ea699 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -68,7 +68,7 @@ class SparseCvarHelper { return {bestValue.value() / storm::utility::convertNumber(modelCheckingData.alpha), std::move(bestScheduler)}; } -private: + private: std::optional> buildLpForThreshold(CvarThresholdData const& thresholdData, bool produceScheduler) const { using RawLpSolver = storm::solver::LpSolver; using RawLpConstraint = storm::solver::RawLpConstraint; diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index eb03165953..b0369ebb00 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -549,8 +549,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::che storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); - std::unique_ptr result( - new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarResult.value))); + std::unique_ptr result(new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarResult.value))); if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); } diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index d76fe121ed..c32c409f8a 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -11,70 +11,97 @@ #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" #include "storm/models/sparse/Mdp.h" -#include "storm/utility/constants.h" #include namespace { +constexpr uint64_t safeChoice = 0; +constexpr uint64_t balancedChoice = 1; +constexpr uint64_t adaptiveChoice = 2; +constexpr uint64_t riskyChoice = 3; +constexpr uint64_t cashChoice = 0; +constexpr uint64_t pushChoice = 1; + +bool hasLpSolver() { +#if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) + return false; +#else + return true; +#endif +} + template -std::shared_ptr> buildCvarModel(std::string const& modelPath, std::string const& propertyString, double alpha) { +struct CvarTestInput { + std::shared_ptr> mdp; + std::shared_ptr formula; +}; + +template +CvarTestInput buildCvarInput(std::string const& modelPath, std::string const& propertyString, double alpha) { storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, program); std::vector cvarProperties = {storm::api::createCvarProperty(properties.front(), alpha)}; auto formulas = storm::api::extractFormulasFromProperties(cvarProperties); - return storm::api::buildSparseModel(program, formulas)->template as>(); + auto mdp = storm::api::buildSparseModel(program, formulas)->template as>(); + return {mdp, cvarProperties.front().getRawFormula()}; } -std::shared_ptr buildCvarFormula(std::string const& modelPath, std::string const& propertyString, double alpha) { - auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, storm::api::parseProgram(modelPath)); - return storm::api::createCvarProperty(properties.front(), alpha).getRawFormula(); +template +std::unique_ptr checkInitialStateResult(CvarTestInput const& input, bool produceScheduler = false) { + storm::Environment env; + storm::modelchecker::SparseMdpPrctlModelChecker> checker(*input.mdp); + storm::modelchecker::CheckTask task(*input.formula, true); + task.setProduceSchedulers(produceScheduler); + return checker.check(env, task); } template -ValueType checkInitialStateValue(std::shared_ptr > const& mdp, - std::shared_ptr const& formula) { - storm::Environment env; - storm::modelchecker::SparseMdpPrctlModelChecker > checker(*mdp); - storm::modelchecker::CheckTask task(*formula, true); - auto result = checker.check(env, task); +ValueType checkInitialStateValue(CvarTestInput const& input) { + auto result = checkInitialStateResult(input); return result->template asExplicitQuantitativeCheckResult().getMax(); } +template +std::vector getChoiceSuccessors(std::shared_ptr> const& mdp, uint64_t state, uint64_t localChoice) { + std::vector result; + uint64_t row = mdp->getTransitionMatrix().getRowGroupIndices()[state] + localChoice; + for (auto const& entry : mdp->getTransitionMatrix().getRow(row)) { + result.push_back(entry.getColumn()); + } + return result; +} + TEST(CvarQueryTest, SimpleMdp) { -#if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) - GTEST_SKIP() << "No LP solver available."; -#endif + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } double alpha = 0.75; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; - auto maxMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); - auto maxFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); - double maxValue = checkInitialStateValue(maxMdp, maxFormula); + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + double maxValue = checkInitialStateValue(maxInput); EXPECT_NEAR(maxValue, 2.0, 1e-10); - auto minMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); - auto minFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); - double minValue = checkInitialStateValue(minMdp, minFormula); + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); + double minValue = checkInitialStateValue(minInput); EXPECT_NEAR(minValue, 5.0 / 3.0, 1e-10); } TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { -#if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) - GTEST_SKIP() << "No LP solver available."; -#endif + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } double alpha = 0.5; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_bad_mec_mdp.nm"; - auto maxMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); - auto maxFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); - double maxValue = checkInitialStateValue(maxMdp, maxFormula); + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + double maxValue = checkInitialStateValue(maxInput); EXPECT_NEAR(maxValue, 0.0, 1e-10); - auto minMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); - auto minFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); - double minValue = checkInitialStateValue(minMdp, minFormula); + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); + double minValue = checkInitialStateValue(minInput); EXPECT_NEAR(minValue, 0.0, 1e-10); } @@ -82,37 +109,93 @@ TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { double alpha = 0.5; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_nonabsorbing_target_mdp.nm"; - auto mdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); - auto formula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); + auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); storm::Environment env; - storm::modelchecker::SparseMdpPrctlModelChecker> checker(*mdp); - storm::modelchecker::CheckTask task(*formula, true); + storm::modelchecker::SparseMdpPrctlModelChecker> checker(*input.mdp); + storm::modelchecker::CheckTask task(*input.formula, true); STORM_SILENT_EXPECT_THROW(checker.check(env, task), storm::exceptions::InvalidPropertyException); } TEST(CvarQueryTest, BranchingTradeoffMdp) { -#if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) - GTEST_SKIP() << "No LP solver available."; -#endif + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; - auto maxHalfMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.5); - auto maxHalfFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.5); - EXPECT_NEAR(checkInitialStateValue(maxHalfMdp, maxHalfFormula), 7.0, 1e-10); + auto maxHalfInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.5); + EXPECT_NEAR(checkInitialStateValue(maxHalfInput), 7.0, 1e-10); - auto maxThreeQuarterMdp = buildCvarModel(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); - auto maxThreeQuarterFormula = buildCvarFormula(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); - EXPECT_NEAR(checkInitialStateValue(maxThreeQuarterMdp, maxThreeQuarterFormula), 8.0, 1e-10); + auto maxThreeQuarterInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + EXPECT_NEAR(checkInitialStateValue(maxThreeQuarterInput), 8.0, 1e-10); - auto minHalfMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.5); - auto minHalfFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.5); - EXPECT_NEAR(checkInitialStateValue(minHalfMdp, minHalfFormula), 0.0, 1e-10); + auto minHalfInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.5); + EXPECT_NEAR(checkInitialStateValue(minHalfInput), 0.0, 1e-10); // this requires randomization of the strategy - auto minThreeQuarterMdp = buildCvarModel(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); - auto minThreeQuarterFormula = buildCvarFormula(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); - EXPECT_NEAR(checkInitialStateValue(minThreeQuarterMdp, minThreeQuarterFormula), 14.0 / 3.0, 1e-10); + auto minThreeQuarterInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + EXPECT_NEAR(checkInitialStateValue(minThreeQuarterInput), 14.0 / 3.0, 1e-10); +} + +TEST(CvarQueryTest, ProducesDeterministicSchedulerForMaxBranchingTradeoffMdp) { + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + auto result = checkInitialStateResult(input, true); + + ASSERT_TRUE(result->isExplicitQuantitativeCheckResult()); + auto const& quantitativeResult = result->template asExplicitQuantitativeCheckResult(); + ASSERT_TRUE(quantitativeResult.hasScheduler()); + EXPECT_NEAR(quantitativeResult.getMax(), 8.0, 1e-10); + + storm::storage::Scheduler const& scheduler = quantitativeResult.getScheduler(); + uint64_t initialState = *input.mdp->getInitialStates().begin(); + auto adaptiveSuccessors = getChoiceSuccessors(input.mdp, initialState, adaptiveChoice); + ASSERT_EQ(2, adaptiveSuccessors.size()); + EXPECT_TRUE(scheduler.isDeterministicScheduler()); + EXPECT_TRUE(scheduler.isMemorylessScheduler()); + EXPECT_FALSE(scheduler.isPartialScheduler()); + EXPECT_EQ(adaptiveChoice, scheduler.getChoice(initialState).getDeterministicChoice()); + std::set branchChoices = {scheduler.getChoice(adaptiveSuccessors[0]).getDeterministicChoice(), + scheduler.getChoice(adaptiveSuccessors[1]).getDeterministicChoice()}; + EXPECT_EQ(std::set({cashChoice, pushChoice}), branchChoices); +} + +TEST(CvarQueryTest, ProducesRandomizedSchedulerForMinBranchingTradeoffMdp) { + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + auto result = checkInitialStateResult(input, true); + + ASSERT_TRUE(result->isExplicitQuantitativeCheckResult()); + auto const& quantitativeResult = result->template asExplicitQuantitativeCheckResult(); + ASSERT_TRUE(quantitativeResult.hasScheduler()); + EXPECT_NEAR(quantitativeResult.getMax(), 14.0 / 3.0, 1e-10); + + storm::storage::Scheduler const& scheduler = quantitativeResult.getScheduler(); + EXPECT_FALSE(scheduler.isDeterministicScheduler()); + EXPECT_TRUE(scheduler.isMemorylessScheduler()); + EXPECT_FALSE(scheduler.isPartialScheduler()); + + uint64_t initialState = *input.mdp->getInitialStates().begin(); + auto adaptiveSuccessors = getChoiceSuccessors(input.mdp, initialState, adaptiveChoice); + ASSERT_EQ(2, adaptiveSuccessors.size()); + auto const& initialChoice = scheduler.getChoice(initialState); + ASSERT_TRUE(initialChoice.isDefined()); + ASSERT_FALSE(initialChoice.isDeterministic()); + auto const& initialDistribution = initialChoice.getChoiceAsDistribution(); + EXPECT_NEAR(initialDistribution.getProbability(safeChoice), 0.5, 1e-10); + EXPECT_NEAR(initialDistribution.getProbability(riskyChoice), 0.5, 1e-10); + EXPECT_NEAR(initialDistribution.getProbability(balancedChoice), 0.0, 1e-10); + EXPECT_NEAR(initialDistribution.getProbability(adaptiveChoice), 0.0, 1e-10); + EXPECT_TRUE(scheduler.isDontCare(adaptiveSuccessors[0])); + EXPECT_TRUE(scheduler.isDontCare(adaptiveSuccessors[1])); } -} // namespace +} // namespace From cbb1978a175b55d1716e48d0aa964153ef08e94a Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:04:26 +0200 Subject: [PATCH 20/65] Add CVaR rational test --- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index c32c409f8a..6d4d2cf2e0 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -1,6 +1,7 @@ #include "storm-config.h" #include "test/storm_gtest.h" +#include "storm/adapters/RationalNumberAdapter.h" #include "storm-parsers/api/model_descriptions.h" #include "storm-parsers/api/properties.h" #include "storm/api/builder.h" @@ -30,6 +31,14 @@ bool hasLpSolver() { #endif } +bool hasExactLpSolver() { +#if !defined(STORM_HAVE_Z3) + return false; +#else + return true; +#endif +} + template struct CvarTestInput { std::shared_ptr> mdp; @@ -138,6 +147,20 @@ TEST(CvarQueryTest, BranchingTradeoffMdp) { EXPECT_NEAR(checkInitialStateValue(minThreeQuarterInput), 14.0 / 3.0, 1e-10); } +TEST(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { + if (!hasExactLpSolver()) { + GTEST_SKIP() << "No exact LP solver available."; + } + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; + + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + EXPECT_EQ(storm::RationalNumber(8), checkInitialStateValue(maxInput)); + + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + EXPECT_EQ(storm::RationalNumber("14/3"), checkInitialStateValue(minInput)); +} + TEST(CvarQueryTest, ProducesDeterministicSchedulerForMaxBranchingTradeoffMdp) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; From f32a3594b5515d275eb6fa7980c4cecd58238bf3 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:19:26 +0200 Subject: [PATCH 21/65] cvar: Added collapse for transient MEC --- .../mdp/cvar_target_reaching_mec_mdp.nm | 23 ++++ .../modelchecker/cvar/CvarModelCheckingData.h | 9 +- .../modelchecker/cvar/SparseCvarHelper.h | 11 ++ .../WeightedReachabilityModelInformation.h | 121 ++++++++++++++++-- .../prctl/SparseMdpPrctlModelChecker.cpp | 10 +- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 30 ++++- 6 files changed, 184 insertions(+), 20 deletions(-) create mode 100644 resources/examples/testfiles/mdp/cvar_target_reaching_mec_mdp.nm diff --git a/resources/examples/testfiles/mdp/cvar_target_reaching_mec_mdp.nm b/resources/examples/testfiles/mdp/cvar_target_reaching_mec_mdp.nm new file mode 100644 index 0000000000..3563c87290 --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_target_reaching_mec_mdp.nm @@ -0,0 +1,23 @@ +mdp + +module main + s : [0..4] init 0; + + [] s=0 -> 1 : (s'=1); + + [cycle12] s=1 -> 1 : (s'=2); + [takeLow] s=1 -> 1 : (s'=3); + + [cycle21] s=2 -> 1 : (s'=1); + [takeRisk] s=2 -> 1/2 : (s'=3) + 1/2 : (s'=4); + + [] s=3 -> 1 : (s'=3); + [] s=4 -> 1 : (s'=4); +endmodule + +label "target" = s=3 | s=4; + +rewards "term" + s=3 : 4; + s=4 : 10; +endrewards diff --git a/src/storm/modelchecker/cvar/CvarModelCheckingData.h b/src/storm/modelchecker/cvar/CvarModelCheckingData.h index 302380e922..9e04bab176 100644 --- a/src/storm/modelchecker/cvar/CvarModelCheckingData.h +++ b/src/storm/modelchecker/cvar/CvarModelCheckingData.h @@ -72,15 +72,14 @@ CvarThresholdData createCvarThresholdData(storm::storage::BitVector c return {threshold, targetStatesBelowThreshold, targetStatesAtThreshold, targetStatesBelowOrAtThreshold}; } -template -CvarModelCheckingData createCvarModelCheckingData( - SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, - WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { +template +CvarModelCheckingData createCvarModelCheckingData(CvarFormulaInformation const& formulaInformation, + WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { auto candidateThresholds = collectCandidateThresholds(weightedReachabilityModelInformation.effectiveTargetStates, weightedReachabilityModelInformation.terminalRewards); return {formulaInformation.alpha, formulaInformation.optimizationDirection, - *model.getInitialStates().begin(), + weightedReachabilityModelInformation.initialState, weightedReachabilityModelInformation.rewardModelName, weightedReachabilityModelInformation.effectiveTargetStates, weightedReachabilityModelInformation.terminalRewards, diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index 036a3ea699..a6c0307f57 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -27,6 +27,17 @@ struct CvarComputationResult { }; /*! * Solves an LP for Conditional Value-at-Risk on an MDP with a terminal reward objective. + * + * Supported CLI shape: + * storm --prism model.nm --prop 'R{"reward"}min/max=? [ F "target" ]' --cvar + * + * The --cvar option requires exactly one selected property. That property must be unfiltered and must be an unbounded + * reward query with an optimization direction (min or max) and an eventually formula F phi whose target phi is a state + * formula. The reward model may be named explicitly (R{"reward"}...) or omitted if the model has a unique reward model. + * + * Requirements: sparse MDP, 0 < alpha < 1, exactly one initial state, state-based terminal rewards, + * reward 0 on non-target states, and absorbing original target states. + * * @see https://doi.org/10.1145/3209108.3209176 Fig. 4 for a description of the algorithm as implemented (and slightly altered) here. */ template diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h index d01d8a784a..775782d146 100644 --- a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h +++ b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h @@ -3,11 +3,13 @@ #include #include +#include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/storage/BitVector.h" #include "storm/storage/MaximalEndComponentDecomposition.h" #include "storm/storage/SparseMatrix.h" +#include "storm/transformer/EndComponentEliminator.h" #include "storm/utility/constants.h" #include "storm/utility/graph.h" #include "storm/utility/logging.h" @@ -16,12 +18,24 @@ namespace storm { namespace modelchecker { namespace cvar { + +/*! + * Collects and preprocesses the model information needed by the CVaR LP. + * + * The LP implemented in SparseCvarHelper follows the weighted-reachability setting from the referenced CVaR paper: + * a single initial state, terminal rewards on absorbing target states, and no reward before reaching such a terminal + * state. This helper enforces these assumptions on the input model and rewrites the transition structure where needed: + * end components that cannot reach the original target set become zero-reward terminal targets, while target-reaching + * end components are collapsed before the LP is built. + */ template struct WeightedReachabilityModelInformation { std::string rewardModelName; + uint64_t initialState; storm::storage::BitVector originalTargetStates; storm::storage::BitVector effectiveTargetStates; storm::storage::BitVector badMecStates; + uint64_t collapsedTargetReachingMecCount; std::vector terminalRewards; storm::storage::SparseMatrix transitionMatrix; }; @@ -40,16 +54,27 @@ void validateTargetStatesAreAbsorbing(storm::storage::SparseMatrix co } template -storm::storage::BitVector computeBadMecStates(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& initialStates, - storm::storage::BitVector const& targetStates) { +storm::storage::MaximalEndComponentDecomposition computeReachableMecs(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::BitVector const& initialStates) { storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); storm::storage::BitVector noStates(transitionMatrix.getRowGroupCount(), false); auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, initialStates, allStates, noStates); auto backwardTransitions = transitionMatrix.transpose(true); - auto statesThatCanReachTarget = storm::utility::graph::performProbGreater0(backwardTransitions, allStates, targetStates); - storm::storage::MaximalEndComponentDecomposition mecs(transitionMatrix, backwardTransitions, reachableStates); + return storm::storage::MaximalEndComponentDecomposition(transitionMatrix, backwardTransitions, reachableStates); +} + +template +storm::storage::BitVector computeStatesThatCanReachTarget(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::BitVector const& targetStates) { + storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); + auto backwardTransitions = transitionMatrix.transpose(true); + return storm::utility::graph::performProbGreater0(backwardTransitions, allStates, targetStates); +} - storm::storage::BitVector badMecStates(transitionMatrix.getRowGroupCount(), false); +template +storm::storage::BitVector computeBadMecStates(storm::storage::MaximalEndComponentDecomposition const& mecs, + storm::storage::BitVector const& statesThatCanReachTarget, uint64_t numberOfStates) { + storm::storage::BitVector badMecStates(numberOfStates, false); for (auto const& mec : mecs) { if (!mec.containsAnyState(statesThatCanReachTarget)) { for (auto const& stateChoices : mec) { @@ -60,9 +85,66 @@ storm::storage::BitVector computeBadMecStates(storm::storage::SparseMatrix +storm::storage::MaximalEndComponentDecomposition computeTargetReachingMecs(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::BitVector const& initialStates, + storm::storage::BitVector const& effectiveTargetStates) { + storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); + storm::storage::BitVector nonTargetStates = ~effectiveTargetStates; + storm::storage::BitVector noStates(allStates.size(), false); + auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, initialStates, allStates, noStates); + auto subsystemStates = reachableStates & nonTargetStates; + + storm::storage::BitVector possibleEcRows(transitionMatrix.getRowCount(), false); + for (auto state : subsystemStates) { + for (uint64_t row = transitionMatrix.getRowGroupIndices()[state], endRow = transitionMatrix.getRowGroupIndices()[state + 1]; row < endRow; ++row) { + possibleEcRows.set(row, true); + } + } + + auto backwardTransitions = transitionMatrix.transpose(true); + auto statesThatCanReachTarget = storm::utility::graph::performProbGreater0(backwardTransitions, allStates, effectiveTargetStates); + auto targetReachingStates = statesThatCanReachTarget & subsystemStates; + return storm::storage::MaximalEndComponentDecomposition(transitionMatrix, backwardTransitions, targetReachingStates, possibleEcRows); +} + +template +void applyTargetReachingMecCollapse(storm::storage::SparseMatrix& transitionMatrix, storm::storage::BitVector& effectiveTargetStates, + storm::storage::BitVector& badMecStates, std::vector& terminalRewards, uint64_t& initialState, + storm::storage::MaximalEndComponentDecomposition const& targetReachingMecs) { + storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); + storm::storage::BitVector noSinkRows(transitionMatrix.getRowGroupCount(), false); + // Preserve the eliminated end component as a single representative state with a self-loop choice, plus the original exits. + auto eliminationResult = storm::transformer::EndComponentEliminator::transform(transitionMatrix, targetReachingMecs, allStates, noSinkRows); + + storm::storage::BitVector newEffectiveTargetStates(eliminationResult.matrix.getRowGroupCount(), false); + storm::storage::BitVector newBadMecStates(eliminationResult.matrix.getRowGroupCount(), false); + std::vector newTerminalRewards(eliminationResult.matrix.getRowGroupCount(), storm::utility::zero()); + for (auto oldTargetState : effectiveTargetStates) { + auto newTargetState = eliminationResult.oldToNewStateMapping[oldTargetState]; + if (newTargetState < eliminationResult.matrix.getRowGroupCount()) { + newEffectiveTargetStates.set(newTargetState, true); + newTerminalRewards[newTargetState] = terminalRewards[oldTargetState]; + } + } + for (auto oldBadMecState : badMecStates) { + auto newBadMecState = eliminationResult.oldToNewStateMapping[oldBadMecState]; + if (newBadMecState < eliminationResult.matrix.getRowGroupCount()) { + newBadMecStates.set(newBadMecState, true); + } + } + + initialState = eliminationResult.oldToNewStateMapping[initialState]; + effectiveTargetStates = std::move(newEffectiveTargetStates); + badMecStates = std::move(newBadMecStates); + terminalRewards = std::move(newTerminalRewards); + transitionMatrix = std::move(eliminationResult.matrix); +} + template WeightedReachabilityModelInformation extractWeightedReachabilityModelInformation( - SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, storm::storage::BitVector const& targetStates) { + SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, storm::storage::BitVector const& targetStates, + bool produceScheduler = false) { using ValueType = typename SparseMdpModelType::ValueType; std::string rewardModelName = formulaInformation.rewardModelName ? formulaInformation.rewardModelName.get() : ""; @@ -91,10 +173,13 @@ WeightedReachabilityModelInformation ext STORM_LOG_WARN("All target states have terminal reward 0 in reward model '" << rewardModelName << "'."); } - auto badMecStates = computeBadMecStates(model.getTransitionMatrix(), model.getInitialStates(), targetStates); + auto reachableMecs = computeReachableMecs(model.getTransitionMatrix(), model.getInitialStates()); + auto statesThatCanReachTarget = computeStatesThatCanReachTarget(model.getTransitionMatrix(), targetStates); + auto badMecStates = computeBadMecStates(reachableMecs, statesThatCanReachTarget, model.getNumberOfStates()); auto effectiveTargetStates = targetStates | badMecStates; auto terminalRewards = stateRewards; auto transitionMatrix = model.getTransitionMatrix(); + uint64_t initialState = *model.getInitialStates().begin(); if (!badMecStates.empty()) { STORM_LOG_INFO( @@ -105,7 +190,27 @@ WeightedReachabilityModelInformation ext } } - return {rewardModelName, targetStates, effectiveTargetStates, badMecStates, std::move(terminalRewards), std::move(transitionMatrix)}; + uint64_t collapsedTargetReachingMecCount = 0; + auto targetReachingMecs = computeTargetReachingMecs(transitionMatrix, model.getInitialStates(), effectiveTargetStates); + if (!targetReachingMecs.empty()) { + STORM_LOG_THROW(!produceScheduler, storm::exceptions::InvalidOperationException, + "Cannot produce a CVaR scheduler because preprocessing has to collapse target-reaching end components."); + collapsedTargetReachingMecCount = targetReachingMecs.size(); + auto oldStateCount = transitionMatrix.getRowGroupCount(); + applyTargetReachingMecCollapse(transitionMatrix, effectiveTargetStates, badMecStates, terminalRewards, initialState, targetReachingMecs); + STORM_PRINT_AND_LOG("CVaR preprocessing collapsed " << collapsedTargetReachingMecCount + << " target-reaching end component(s), reducing the transition matrix from " << oldStateCount + << " to " << transitionMatrix.getRowGroupCount() << " states.\n"); + } + + return {rewardModelName, + initialState, + targetStates, + effectiveTargetStates, + badMecStates, + collapsedTargetReachingMecCount, + std::move(terminalRewards), + std::move(transitionMatrix)}; } } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index b0369ebb00..30f1f86594 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -541,15 +541,15 @@ std::unique_ptr SparseMdpPrctlModelChecker::che auto targetStates = this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); // check if model fits terminal reward - auto weightedReachabilityModelInformation = - storm::modelchecker::cvar::extractWeightedReachabilityModelInformation(this->getModel(), cvarFormulaInformation, targetStates); + auto weightedReachabilityModelInformation = storm::modelchecker::cvar::extractWeightedReachabilityModelInformation( + this->getModel(), cvarFormulaInformation, targetStates, checkTask.isProduceSchedulersSet()); // combine info into 1 simplified object - auto cvarModelCheckingData = - storm::modelchecker::cvar::createCvarModelCheckingData(this->getModel(), cvarFormulaInformation, weightedReachabilityModelInformation); + auto cvarModelCheckingData = storm::modelchecker::cvar::createCvarModelCheckingData(cvarFormulaInformation, weightedReachabilityModelInformation); storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); - std::unique_ptr result(new ExplicitQuantitativeCheckResult(cvarModelCheckingData.initialState, std::move(cvarResult.value))); + std::unique_ptr result( + new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); } diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 6d4d2cf2e0..4e5596cd37 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -1,12 +1,13 @@ #include "storm-config.h" #include "test/storm_gtest.h" -#include "storm/adapters/RationalNumberAdapter.h" #include "storm-parsers/api/model_descriptions.h" #include "storm-parsers/api/properties.h" +#include "storm/adapters/RationalNumberAdapter.h" #include "storm/api/builder.h" #include "storm/api/properties.h" #include "storm/environment/Environment.h" +#include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/modelchecker/CheckTask.h" #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" @@ -32,7 +33,7 @@ bool hasLpSolver() { } bool hasExactLpSolver() { -#if !defined(STORM_HAVE_Z3) +#if !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) return false; #else return true; @@ -114,6 +115,31 @@ TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { EXPECT_NEAR(minValue, 0.0, 1e-10); } +TEST(CvarQueryTest, TargetReachingMecIsCollapsed) { + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_target_reaching_mec_mdp.nm"; + + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + EXPECT_NEAR(checkInitialStateValue(maxInput), 6.0, 1e-10); + + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + EXPECT_NEAR(checkInitialStateValue(minInput), 4.0, 1e-10); +} + +TEST(CvarQueryTest, RejectsSchedulerForTargetReachingMecCollapse) { + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_target_reaching_mec_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + + STORM_SILENT_EXPECT_THROW(checkInitialStateResult(input, true), storm::exceptions::InvalidOperationException); +} + TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { double alpha = 0.5; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_nonabsorbing_target_mdp.nm"; From ce9819015601ed4540b74d7a4e041dd0fd6bc5c3 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:34:47 +0200 Subject: [PATCH 22/65] Refactor CVaR dispatch into query and problem classification --- .../modelchecker/cvar/CvarClassification.h | 57 +++++++++++++++++++ .../prctl/SparseMdpPrctlModelChecker.cpp | 38 +++++++++---- 2 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 src/storm/modelchecker/cvar/CvarClassification.h diff --git a/src/storm/modelchecker/cvar/CvarClassification.h b/src/storm/modelchecker/cvar/CvarClassification.h new file mode 100644 index 0000000000..556909db15 --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarClassification.h @@ -0,0 +1,57 @@ +#pragma once + +#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/storage/BitVector.h" + +namespace storm { +namespace modelchecker { +namespace cvar { +/*! + * Classifies the embedded CVaR query at the formula level. + * + * This is intentionally separate from the concrete problem kind below: + * multiple concrete problem kinds may share the same surface query syntax. + */ +enum class CvarQueryKind { + ReachabilityReward +}; + +/*! + * Classifies the concrete solver problem induced by a CVaR query on a given + * model and reward structure. + * + * Weighted reachability is the currently implemented LP-based terminal-reward + * setting. SSP will be used by the future value-iteration implementation for + * accumulated state-action costs until reaching the goal. + */ +enum class CvarProblemKind { + WeightedReachability, + Ssp +}; + +/*! + * Determines the formula-level CVaR query kind. + * + * The current front-end only admits reachability reward CVaR queries, but this + * explicit classification provides the extension point for future CVaR query + * families. + */ +inline CvarQueryKind classifyCvarQuery(CvarFormulaInformation const&) { + return CvarQueryKind::ReachabilityReward; +} + +/*! + * Classifies the concrete CVaR problem kind to use. + * + * This first version is intentionally conservative and preserves the current + * behavior by routing all supported CVaR queries through the existing weighted + * reachability implementation. The SSP branch will be enabled in follow-up + * commits once its preprocessing and solver path are introduced. + */ +template +CvarProblemKind classifyCvarProblem(SparseMdpModelType const&, CvarFormulaInformation const&, CvarQueryKind, storm::storage::BitVector const&) { + return CvarProblemKind::WeightedReachability; +} +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 30f1f86594..17df45a0b5 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -6,6 +6,7 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/logic/FragmentSpecification.h" +#include "storm/modelchecker/cvar/CvarClassification.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/modelchecker/cvar/CvarModelCheckingData.h" #include "storm/modelchecker/cvar/SparseCvarHelper.h" @@ -540,18 +541,31 @@ std::unique_ptr SparseMdpPrctlModelChecker::che auto cvarFormulaInformation = storm::modelchecker::cvar::extractCvarFormulaInformation(checkTask.getFormula()); auto targetStates = this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); - // check if model fits terminal reward - auto weightedReachabilityModelInformation = storm::modelchecker::cvar::extractWeightedReachabilityModelInformation( - this->getModel(), cvarFormulaInformation, targetStates, checkTask.isProduceSchedulersSet()); - // combine info into 1 simplified object - auto cvarModelCheckingData = storm::modelchecker::cvar::createCvarModelCheckingData(cvarFormulaInformation, weightedReachabilityModelInformation); - - storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); - auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); - std::unique_ptr result( - new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); - if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { - result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); + auto queryKind = storm::modelchecker::cvar::classifyCvarQuery(cvarFormulaInformation); + auto problemKind = storm::modelchecker::cvar::classifyCvarProblem(this->getModel(), cvarFormulaInformation, queryKind, targetStates); + + std::unique_ptr result; + switch (problemKind) { + case storm::modelchecker::cvar::CvarProblemKind::WeightedReachability: { + // check if model fits terminal reward + auto weightedReachabilityModelInformation = storm::modelchecker::cvar::extractWeightedReachabilityModelInformation( + this->getModel(), cvarFormulaInformation, targetStates, checkTask.isProduceSchedulersSet()); + // combine info into 1 simplified object + auto cvarModelCheckingData = + storm::modelchecker::cvar::createCvarModelCheckingData(cvarFormulaInformation, weightedReachabilityModelInformation); + + storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); + auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); + result = std::unique_ptr( + new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); + if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { + result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); + } + break; + } + case storm::modelchecker::cvar::CvarProblemKind::Ssp: + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, + "CVaR for stochastic shortest path objectives is not implemented yet."); } return result; } From 7a23e1ea6c3376ef20afba074b4180b225aba63c Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:59:45 +0200 Subject: [PATCH 23/65] Refactor CVaR method selection and dispatch for SSP support --- src/storm/environment/SubEnvironment.cpp | 1 + .../AllModelCheckerEnvironments.h | 3 +- .../CvarModelCheckerEnvironment.cpp | 25 +++++++++ .../CvarModelCheckerEnvironment.h | 19 +++++++ .../modelchecker/ModelCheckerEnvironment.cpp | 9 ++++ .../modelchecker/ModelCheckerEnvironment.h | 5 ++ .../modelchecker/cvar/CvarClassification.h | 52 +++++++++++++------ src/storm/modelchecker/cvar/CvarMethod.cpp | 21 ++++++++ src/storm/modelchecker/cvar/CvarMethod.h | 13 +++++ .../modelchecker/cvar/SparseCvarHelper.h | 1 + .../prctl/SparseMdpPrctlModelChecker.cpp | 7 +-- src/storm/settings/SettingsManager.cpp | 2 + src/storm/settings/modules/CvarSettings.cpp | 42 +++++++++++++++ src/storm/settings/modules/CvarSettings.h | 38 ++++++++++++++ 14 files changed, 219 insertions(+), 19 deletions(-) create mode 100644 src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp create mode 100644 src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h create mode 100644 src/storm/modelchecker/cvar/CvarMethod.cpp create mode 100644 src/storm/modelchecker/cvar/CvarMethod.h create mode 100644 src/storm/settings/modules/CvarSettings.cpp create mode 100644 src/storm/settings/modules/CvarSettings.h diff --git a/src/storm/environment/SubEnvironment.cpp b/src/storm/environment/SubEnvironment.cpp index ab2a7afbd6..d69e9baf57 100644 --- a/src/storm/environment/SubEnvironment.cpp +++ b/src/storm/environment/SubEnvironment.cpp @@ -48,6 +48,7 @@ void SubEnvironment::assertInitialized() const { template class SubEnvironment; +template class SubEnvironment; template class SubEnvironment; template class SubEnvironment; diff --git a/src/storm/environment/modelchecker/AllModelCheckerEnvironments.h b/src/storm/environment/modelchecker/AllModelCheckerEnvironments.h index f37921b606..a4b396024b 100644 --- a/src/storm/environment/modelchecker/AllModelCheckerEnvironments.h +++ b/src/storm/environment/modelchecker/AllModelCheckerEnvironments.h @@ -1,4 +1,5 @@ #pragma once +#include "storm/environment/modelchecker/CvarModelCheckerEnvironment.h" #include "storm/environment/modelchecker/ModelCheckerEnvironment.h" -#include "storm/environment/modelchecker/MultiObjectiveModelCheckerEnvironment.h" \ No newline at end of file +#include "storm/environment/modelchecker/MultiObjectiveModelCheckerEnvironment.h" diff --git a/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp new file mode 100644 index 0000000000..99618a375a --- /dev/null +++ b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp @@ -0,0 +1,25 @@ +#include "storm/environment/modelchecker/CvarModelCheckerEnvironment.h" + +#include "storm/settings/SettingsManager.h" +#include "storm/settings/modules/CvarSettings.h" + +namespace storm { + +CvarModelCheckerEnvironment::CvarModelCheckerEnvironment() { + auto const& cvarSettings = storm::settings::getModule(); + method = cvarSettings.getCvarMethod(); +} + +CvarModelCheckerEnvironment::~CvarModelCheckerEnvironment() { + // Intentionally left empty +} + +storm::modelchecker::cvar::CvarMethod const& CvarModelCheckerEnvironment::getMethod() const { + return method; +} + +void CvarModelCheckerEnvironment::setMethod(storm::modelchecker::cvar::CvarMethod value) { + method = value; +} + +} // namespace storm diff --git a/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h new file mode 100644 index 0000000000..3c9f142382 --- /dev/null +++ b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h @@ -0,0 +1,19 @@ +#pragma once + +#include "storm/modelchecker/cvar/CvarMethod.h" + +namespace storm { + +class CvarModelCheckerEnvironment { + public: + CvarModelCheckerEnvironment(); + ~CvarModelCheckerEnvironment(); + + storm::modelchecker::cvar::CvarMethod const& getMethod() const; + void setMethod(storm::modelchecker::cvar::CvarMethod value); + + private: + storm::modelchecker::cvar::CvarMethod method; +}; + +} // namespace storm diff --git a/src/storm/environment/modelchecker/ModelCheckerEnvironment.cpp b/src/storm/environment/modelchecker/ModelCheckerEnvironment.cpp index 5ff79fd9a3..08e3d6670d 100644 --- a/src/storm/environment/modelchecker/ModelCheckerEnvironment.cpp +++ b/src/storm/environment/modelchecker/ModelCheckerEnvironment.cpp @@ -1,5 +1,6 @@ #include "storm/environment/modelchecker/ModelCheckerEnvironment.h" +#include "storm/environment/modelchecker/CvarModelCheckerEnvironment.h" #include "storm/environment/modelchecker/MultiObjectiveModelCheckerEnvironment.h" #include "storm/settings/SettingsManager.h" @@ -27,6 +28,14 @@ ModelCheckerEnvironment::~ModelCheckerEnvironment() { // Intentionally left empty } +CvarModelCheckerEnvironment& ModelCheckerEnvironment::cvar() { + return cvarModelCheckerEnvironment.get(); +} + +CvarModelCheckerEnvironment const& ModelCheckerEnvironment::cvar() const { + return cvarModelCheckerEnvironment.get(); +} + SteadyStateDistributionAlgorithm ModelCheckerEnvironment::getSteadyStateDistributionAlgorithm() const { return steadyStateDistributionAlgorithm; } diff --git a/src/storm/environment/modelchecker/ModelCheckerEnvironment.h b/src/storm/environment/modelchecker/ModelCheckerEnvironment.h index fce3e9e337..3c995c23e8 100644 --- a/src/storm/environment/modelchecker/ModelCheckerEnvironment.h +++ b/src/storm/environment/modelchecker/ModelCheckerEnvironment.h @@ -6,6 +6,7 @@ #include "storm/environment/Environment.h" #include "storm/environment/SubEnvironment.h" +#include "storm/environment/modelchecker/CvarModelCheckerEnvironment.h" #include "storm/modelchecker/helper/conditional/ConditionalAlgorithmSetting.h" #include "storm/modelchecker/helper/infinitehorizon/SteadyStateDistributionAlgorithm.h" @@ -19,6 +20,9 @@ class ModelCheckerEnvironment { ModelCheckerEnvironment(); ~ModelCheckerEnvironment(); + CvarModelCheckerEnvironment& cvar(); + CvarModelCheckerEnvironment const& cvar() const; + MultiObjectiveModelCheckerEnvironment& multi(); MultiObjectiveModelCheckerEnvironment const& multi() const; @@ -34,6 +38,7 @@ class ModelCheckerEnvironment { void unsetLtl2daTool(); private: + SubEnvironment cvarModelCheckerEnvironment; SubEnvironment multiObjectiveModelCheckerEnvironment; boost::optional ltl2daTool; SteadyStateDistributionAlgorithm steadyStateDistributionAlgorithm; diff --git a/src/storm/modelchecker/cvar/CvarClassification.h b/src/storm/modelchecker/cvar/CvarClassification.h index 556909db15..5b62da492d 100644 --- a/src/storm/modelchecker/cvar/CvarClassification.h +++ b/src/storm/modelchecker/cvar/CvarClassification.h @@ -1,7 +1,11 @@ #pragma once +#include "storm/exceptions/InvalidPropertyException.h" +#include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/storage/BitVector.h" +#include "storm/utility/macros.h" namespace storm { namespace modelchecker { @@ -12,9 +16,7 @@ namespace cvar { * This is intentionally separate from the concrete problem kind below: * multiple concrete problem kinds may share the same surface query syntax. */ -enum class CvarQueryKind { - ReachabilityReward -}; +enum class CvarQueryKind { ReachabilityReward }; /*! * Classifies the concrete solver problem induced by a CVaR query on a given @@ -24,10 +26,7 @@ enum class CvarQueryKind { * setting. SSP will be used by the future value-iteration implementation for * accumulated state-action costs until reaching the goal. */ -enum class CvarProblemKind { - WeightedReachability, - Ssp -}; +enum class CvarProblemKind { WeightedReachability, Ssp }; /*! * Determines the formula-level CVaR query kind. @@ -43,15 +42,38 @@ inline CvarQueryKind classifyCvarQuery(CvarFormulaInformation const&) { /*! * Classifies the concrete CVaR problem kind to use. * - * This first version is intentionally conservative and preserves the current - * behavior by routing all supported CVaR queries through the existing weighted - * reachability implementation. The SSP branch will be enabled in follow-up - * commits once its preprocessing and solver path are introduced. + * The selection can be overridden explicitly via the CVaR method setting. + * Otherwise, classification stays conservative: state-action reward models are + * routed to the SSP branch, while state-only reward models remain on the + * weighted-reachability path until SSP preprocessing is introduced. */ template -CvarProblemKind classifyCvarProblem(SparseMdpModelType const&, CvarFormulaInformation const&, CvarQueryKind, storm::storage::BitVector const&) { +CvarProblemKind classifyCvarProblem(SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, CvarQueryKind, + storm::storage::BitVector const&, CvarMethod method) { + std::string rewardModelName = formulaInformation.rewardModelName ? formulaInformation.rewardModelName.get() : ""; + auto const& rewardModel = model.getRewardModel(rewardModelName); + if (rewardModelName.empty()) { + rewardModelName = model.getUniqueRewardModelName(); + } + + if (method == CvarMethod::WeightedReachability) { + STORM_LOG_THROW(!rewardModel.hasStateActionRewards() && !rewardModel.hasTransitionRewards(), storm::exceptions::InvalidPropertyException, + "The weighted-reachability CVaR method requires state-based terminal rewards only."); + return CvarProblemKind::WeightedReachability; + } + + STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, + "CVaR queries with transition rewards are not supported yet."); + + if (method == CvarMethod::SspParetoVi) { + return CvarProblemKind::Ssp; + } + + if (rewardModel.hasStateActionRewards()) { + return CvarProblemKind::Ssp; + } return CvarProblemKind::WeightedReachability; } -} // namespace cvar -} // namespace modelchecker -} // namespace storm +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/CvarMethod.cpp b/src/storm/modelchecker/cvar/CvarMethod.cpp new file mode 100644 index 0000000000..19ab82b457 --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarMethod.cpp @@ -0,0 +1,21 @@ +#include "storm/modelchecker/cvar/CvarMethod.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +std::string toString(CvarMethod method) { + switch (method) { + case CvarMethod::Auto: + return "auto"; + case CvarMethod::WeightedReachability: + return "weighted-reachability"; + case CvarMethod::SspParetoVi: + return "ssp-pareto-vi"; + } + return "unknown"; +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/CvarMethod.h b/src/storm/modelchecker/cvar/CvarMethod.h new file mode 100644 index 0000000000..dc40e7a46b --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarMethod.h @@ -0,0 +1,13 @@ +#pragma once + +#include "storm/utility/ExtendSettingEnumWithSelectionField.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +ExtendEnumsWithSelectionField(CvarMethod, Auto, WeightedReachability, SspParetoVi) std::string toString(CvarMethod method); + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseCvarHelper.h index a6c0307f57..af9905fd49 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseCvarHelper.h @@ -30,6 +30,7 @@ struct CvarComputationResult { * * Supported CLI shape: * storm --prism model.nm --prop 'R{"reward"}min/max=? [ F "target" ]' --cvar + * storm --prism model.nm --prop 'R{"reward"}min/max=? [ F "target" ]' --cvar --cvar:method wr * * The --cvar option requires exactly one selected property. That property must be unfiltered and must be an unbounded * reward query with an optimization direction (min or max) and an eventually formula F phi whose target phi is a state diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 17df45a0b5..6c564e307c 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -3,6 +3,7 @@ #include "storm/adapters/IntervalAdapter.h" #include "storm/adapters/RationalFunctionAdapter.h" #include "storm/adapters/RationalNumberAdapter.h" +#include "storm/environment/modelchecker/ModelCheckerEnvironment.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/logic/FragmentSpecification.h" @@ -542,7 +543,8 @@ std::unique_ptr SparseMdpPrctlModelChecker::che auto targetStates = this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); auto queryKind = storm::modelchecker::cvar::classifyCvarQuery(cvarFormulaInformation); - auto problemKind = storm::modelchecker::cvar::classifyCvarProblem(this->getModel(), cvarFormulaInformation, queryKind, targetStates); + auto problemKind = storm::modelchecker::cvar::classifyCvarProblem(this->getModel(), cvarFormulaInformation, queryKind, targetStates, + env.modelchecker().cvar().getMethod()); std::unique_ptr result; switch (problemKind) { @@ -564,8 +566,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::che break; } case storm::modelchecker::cvar::CvarProblemKind::Ssp: - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "CVaR for stochastic shortest path objectives is not implemented yet."); + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR for stochastic shortest path objectives is not implemented yet."); } return result; } diff --git a/src/storm/settings/SettingsManager.cpp b/src/storm/settings/SettingsManager.cpp index 9602f0a67d..96518d089f 100644 --- a/src/storm/settings/SettingsManager.cpp +++ b/src/storm/settings/SettingsManager.cpp @@ -18,6 +18,7 @@ #include "storm/settings/modules/BuildSettings.h" #include "storm/settings/modules/CoreSettings.h" #include "storm/settings/modules/CuddSettings.h" +#include "storm/settings/modules/CvarSettings.h" #include "storm/settings/modules/DebugSettings.h" #include "storm/settings/modules/EigenEquationSolverSettings.h" #include "storm/settings/modules/EliminationSettings.h" @@ -681,6 +682,7 @@ void initializeAll(std::string const& name, std::string const& executableName) { // Register all known settings modules. storm::settings::addModule(); storm::settings::addModule(); + storm::settings::addModule(); storm::settings::addModule(); storm::settings::addModule(); storm::settings::addModule(); diff --git a/src/storm/settings/modules/CvarSettings.cpp b/src/storm/settings/modules/CvarSettings.cpp new file mode 100644 index 0000000000..c0490fe615 --- /dev/null +++ b/src/storm/settings/modules/CvarSettings.cpp @@ -0,0 +1,42 @@ +#include "storm/settings/modules/CvarSettings.h" + +#include + +#include "storm/exceptions/IllegalArgumentValueException.h" +#include "storm/settings/ArgumentBuilder.h" +#include "storm/settings/OptionBuilder.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace settings { +namespace modules { + +std::string const CvarSettings::moduleName = "cvar"; +std::string const CvarSettings::methodOptionName = "method"; + +CvarSettings::CvarSettings() : ModuleSettings(moduleName) { + std::vector methods = {"auto", "wr", "weighted-reachability", "ssp", "ssp-vi", "pareto-vi"}; + this->addOption(storm::settings::OptionBuilder(moduleName, methodOptionName, true, "The method to be used for CVaR model checking.") + .setIsAdvanced() + .addArgument(storm::settings::ArgumentBuilder::createStringArgument("name", "The name of the method to use.") + .addValidatorString(ArgumentValidatorFactory::createMultipleChoiceValidator(methods)) + .setDefaultValueString("auto") + .build()) + .build()); +} + +storm::modelchecker::cvar::CvarMethod CvarSettings::getCvarMethod() const { + std::string methodAsString = this->getOption(methodOptionName).getArgumentByName("name").getValueAsString(); + if (methodAsString == "auto") { + return storm::modelchecker::cvar::CvarMethod::Auto; + } else if (methodAsString == "wr" || methodAsString == "weighted-reachability") { + return storm::modelchecker::cvar::CvarMethod::WeightedReachability; + } else if (methodAsString == "ssp" || methodAsString == "ssp-vi" || methodAsString == "pareto-vi") { + return storm::modelchecker::cvar::CvarMethod::SspParetoVi; + } + STORM_LOG_THROW(false, storm::exceptions::IllegalArgumentValueException, "Unknown CVaR method '" << methodAsString << "'."); +} + +} // namespace modules +} // namespace settings +} // namespace storm diff --git a/src/storm/settings/modules/CvarSettings.h b/src/storm/settings/modules/CvarSettings.h new file mode 100644 index 0000000000..0d5718efca --- /dev/null +++ b/src/storm/settings/modules/CvarSettings.h @@ -0,0 +1,38 @@ +#ifndef STORM_SETTINGS_MODULES_CVARSETTINGS_H_ +#define STORM_SETTINGS_MODULES_CVARSETTINGS_H_ + +#include "storm/modelchecker/cvar/CvarMethod.h" +#include "storm/settings/modules/ModuleSettings.h" + +namespace storm { +namespace settings { +namespace modules { + +/*! + * This class represents the settings for CVaR model checking. + */ +class CvarSettings : public ModuleSettings { + public: + /*! + * Creates a new set of CVaR model checking settings. + */ + CvarSettings(); + + /*! + * Retrieves the selected CVaR method. + * + * @return The selected CVaR method. + */ + storm::modelchecker::cvar::CvarMethod getCvarMethod() const; + + static std::string const moduleName; + + private: + static std::string const methodOptionName; +}; + +} // namespace modules +} // namespace settings +} // namespace storm + +#endif /* STORM_SETTINGS_MODULES_CVARSETTINGS_H_ */ From 1d1b2db24a9ea494af148e0f2afed0c14d385b93 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:50:38 +0200 Subject: [PATCH 24/65] Add SSP CVaR model extraction with choice-cost lifting --- .../modelchecker/cvar/SspModelInformation.h | 67 +++++++++++++++++++ .../prctl/SparseMdpPrctlModelChecker.cpp | 6 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/storm/modelchecker/cvar/SspModelInformation.h diff --git a/src/storm/modelchecker/cvar/SspModelInformation.h b/src/storm/modelchecker/cvar/SspModelInformation.h new file mode 100644 index 0000000000..1ada6f0dfd --- /dev/null +++ b/src/storm/modelchecker/cvar/SspModelInformation.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include "storm/exceptions/NotImplementedException.h" +#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/storage/BitVector.h" +#include "storm/storage/SparseMatrix.h" +#include "storm/utility/logging.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +/*! + * Collects the normalized SSP model information needed by the future CVaR VI. + * + * The SSP CVaR algorithm works with per-choice costs. If the input reward model + * only uses state rewards, we lift those rewards to equivalent state-action + * costs by copying the state reward to each outgoing choice of the state. + */ +template +struct SspModelInformation { + std::string rewardModelName; + uint64_t initialState; + storm::storage::BitVector targetStates; + bool liftedStateRewardsToChoiceCosts; + std::vector choiceCosts; + storm::storage::SparseMatrix transitionMatrix; +}; + +template +SspModelInformation extractSspModelInformation(SparseMdpModelType const& model, + CvarFormulaInformation const& formulaInformation, + storm::storage::BitVector const& targetStates) { + using ValueType = typename SparseMdpModelType::ValueType; + + std::string rewardModelName = formulaInformation.rewardModelName ? formulaInformation.rewardModelName.get() : ""; + auto const& rewardModel = model.getRewardModel(rewardModelName); + if (rewardModelName.empty()) { + rewardModelName = model.getUniqueRewardModelName(); + } + + STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, + "CVaR SSP preprocessing does not support transition rewards."); + + bool liftedStateRewardsToChoiceCosts = rewardModel.hasStateRewards() && !rewardModel.hasStateActionRewards(); + if (liftedStateRewardsToChoiceCosts) { + STORM_LOG_INFO("CVaR SSP preprocessing lifts state rewards to equivalent per-choice costs."); + } + + auto transitionMatrix = model.getTransitionMatrix(); + auto choiceCosts = rewardModel.getTotalRewardVector(transitionMatrix); + + return {rewardModelName, + *model.getInitialStates().begin(), + targetStates, + liftedStateRewardsToChoiceCosts, + std::move(choiceCosts), + std::move(transitionMatrix)}; +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 6c564e307c..db195e3bea 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -10,6 +10,7 @@ #include "storm/modelchecker/cvar/CvarClassification.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/modelchecker/cvar/SspModelInformation.h" #include "storm/modelchecker/cvar/SparseCvarHelper.h" #include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" @@ -565,8 +566,11 @@ std::unique_ptr SparseMdpPrctlModelChecker::che } break; } - case storm::modelchecker::cvar::CvarProblemKind::Ssp: + case storm::modelchecker::cvar::CvarProblemKind::Ssp: { + auto sspModelInformation = storm::modelchecker::cvar::extractSspModelInformation(this->getModel(), cvarFormulaInformation, targetStates); + static_cast(sspModelInformation); STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR for stochastic shortest path objectives is not implemented yet."); + } } return result; } From c3f8c47d044391feee1a4206c480c14b8287805a Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:51:42 +0200 Subject: [PATCH 25/65] Extract shared CVaR preprocessing utilities and added SSP preprocessing --- .../cvar/CvarPreprocessingUtilities.h | 61 +++++++++++++++++ .../modelchecker/cvar/SspModelInformation.h | 68 ++++++++++++++++++- .../WeightedReachabilityModelInformation.h | 46 +------------ 3 files changed, 129 insertions(+), 46 deletions(-) create mode 100644 src/storm/modelchecker/cvar/CvarPreprocessingUtilities.h diff --git a/src/storm/modelchecker/cvar/CvarPreprocessingUtilities.h b/src/storm/modelchecker/cvar/CvarPreprocessingUtilities.h new file mode 100644 index 0000000000..9f66df31ad --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarPreprocessingUtilities.h @@ -0,0 +1,61 @@ +#pragma once + +#include "storm/exceptions/InvalidPropertyException.h" +#include "storm/storage/BitVector.h" +#include "storm/storage/MaximalEndComponentDecomposition.h" +#include "storm/storage/SparseMatrix.h" +#include "storm/utility/graph.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +void validateTargetStatesAreAbsorbing(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& targetStates) { + for (auto targetState : targetStates) { + for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; + ++row) { + for (auto const& entry : transitionMatrix.getRow(row)) { + STORM_LOG_THROW(entry.getColumn() == targetState, storm::exceptions::InvalidPropertyException, + "CVaR query currently requires all original target states to be absorbing."); + } + } + } +} + +template +storm::storage::MaximalEndComponentDecomposition computeReachableMecs(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::BitVector const& initialStates) { + storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); + storm::storage::BitVector noStates(transitionMatrix.getRowGroupCount(), false); + auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, initialStates, allStates, noStates); + auto backwardTransitions = transitionMatrix.transpose(true); + return storm::storage::MaximalEndComponentDecomposition(transitionMatrix, backwardTransitions, reachableStates); +} + +template +storm::storage::BitVector computeStatesThatCanReachTarget(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::BitVector const& targetStates) { + storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); + auto backwardTransitions = transitionMatrix.transpose(true); + return storm::utility::graph::performProbGreater0(backwardTransitions, allStates, targetStates); +} + +template +storm::storage::BitVector computeBadMecStates(storm::storage::MaximalEndComponentDecomposition const& mecs, + storm::storage::BitVector const& statesThatCanReachTarget, uint64_t numberOfStates) { + storm::storage::BitVector badMecStates(numberOfStates, false); + for (auto const& mec : mecs) { + if (!mec.containsAnyState(statesThatCanReachTarget)) { + for (auto const& stateChoices : mec) { + badMecStates.set(stateChoices.first, true); + } + } + } + return badMecStates; +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/SspModelInformation.h b/src/storm/modelchecker/cvar/SspModelInformation.h index 1ada6f0dfd..6e3cdae8b8 100644 --- a/src/storm/modelchecker/cvar/SspModelInformation.h +++ b/src/storm/modelchecker/cvar/SspModelInformation.h @@ -4,9 +4,11 @@ #include #include "storm/exceptions/NotImplementedException.h" +#include "storm/modelchecker/cvar/CvarPreprocessingUtilities.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" +#include "storm/utility/constants.h" #include "storm/utility/logging.h" #include "storm/utility/macros.h" @@ -26,11 +28,43 @@ struct SspModelInformation { std::string rewardModelName; uint64_t initialState; storm::storage::BitVector targetStates; + storm::storage::BitVector reachableStates; + storm::storage::BitVector statesThatCanReachTarget; + storm::storage::BitVector badMecStates; bool liftedStateRewardsToChoiceCosts; + bool normalizedTargetStatesToAbsorbing; std::vector choiceCosts; storm::storage::SparseMatrix transitionMatrix; }; +template +std::vector extractChoiceCostsForSsp( + SparseMdpModelType const& model, typename SparseMdpModelType::RewardModelType const& rewardModel, storm::storage::BitVector const& targetStates) { + using ValueType = typename SparseMdpModelType::ValueType; + + std::vector choiceCosts(model.getNumberOfChoices(), storm::utility::zero()); + bool hasStateRewards = rewardModel.hasStateRewards(); + bool hasStateActionRewards = rewardModel.hasStateActionRewards(); + + for (uint64_t state = 0; state < model.getNumberOfStates(); ++state) { + if (targetStates[state]) { + // Costs stop once the goal state is reached. + continue; + } + + ValueType stateReward = hasStateRewards ? rewardModel.getStateReward(state) : storm::utility::zero(); + for (uint64_t row = model.getTransitionMatrix().getRowGroupIndices()[state], endRow = model.getTransitionMatrix().getRowGroupIndices()[state + 1]; row < endRow; + ++row) { + choiceCosts[row] = stateReward; + if (hasStateActionRewards) { + choiceCosts[row] += rewardModel.getStateActionReward(row); + } + } + } + + return choiceCosts; +} + template SspModelInformation extractSspModelInformation(SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, @@ -52,12 +86,44 @@ SspModelInformation extractSspModelInfor } auto transitionMatrix = model.getTransitionMatrix(); - auto choiceCosts = rewardModel.getTotalRewardVector(transitionMatrix); + bool normalizedTargetStatesToAbsorbing = false; + for (auto targetState : targetStates) { + for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; ++row) { + for (auto const& entry : transitionMatrix.getRow(row)) { + if (entry.getColumn() != targetState) { + normalizedTargetStatesToAbsorbing = true; + break; + } + } + if (normalizedTargetStatesToAbsorbing) { + break; + } + } + if (normalizedTargetStatesToAbsorbing) { + break; + } + } + if (normalizedTargetStatesToAbsorbing) { + STORM_LOG_INFO("CVaR SSP preprocessing makes target states absorbing to match terminal-goal semantics."); + transitionMatrix.makeRowGroupsAbsorbing(targetStates, true); + } + + auto reachableStates = storm::utility::graph::getReachableStates( + transitionMatrix, model.getInitialStates(), storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), false)); + auto statesThatCanReachTarget = computeStatesThatCanReachTarget(transitionMatrix, targetStates); + auto badMecStates = + computeBadMecStates(computeReachableMecs(transitionMatrix, model.getInitialStates()), statesThatCanReachTarget, transitionMatrix.getRowGroupCount()); + auto choiceCosts = extractChoiceCostsForSsp(model, rewardModel, targetStates); return {rewardModelName, *model.getInitialStates().begin(), targetStates, + std::move(reachableStates), + std::move(statesThatCanReachTarget), + std::move(badMecStates), liftedStateRewardsToChoiceCosts, + normalizedTargetStatesToAbsorbing, std::move(choiceCosts), std::move(transitionMatrix)}; } diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h index 775782d146..dd55f2f4e7 100644 --- a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h +++ b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h @@ -5,6 +5,7 @@ #include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/InvalidPropertyException.h" +#include "storm/modelchecker/cvar/CvarPreprocessingUtilities.h" #include "storm/modelchecker/cvar/CvarFormulaInformation.h" #include "storm/storage/BitVector.h" #include "storm/storage/MaximalEndComponentDecomposition.h" @@ -40,51 +41,6 @@ struct WeightedReachabilityModelInformation { storm::storage::SparseMatrix transitionMatrix; }; -template -void validateTargetStatesAreAbsorbing(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& targetStates) { - for (auto targetState : targetStates) { - for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; - ++row) { - for (auto const& entry : transitionMatrix.getRow(row)) { - STORM_LOG_THROW(entry.getColumn() == targetState, storm::exceptions::InvalidPropertyException, - "CVaR queries currently require all original target states to be absorbing."); - } - } - } -} - -template -storm::storage::MaximalEndComponentDecomposition computeReachableMecs(storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::BitVector const& initialStates) { - storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); - storm::storage::BitVector noStates(transitionMatrix.getRowGroupCount(), false); - auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, initialStates, allStates, noStates); - auto backwardTransitions = transitionMatrix.transpose(true); - return storm::storage::MaximalEndComponentDecomposition(transitionMatrix, backwardTransitions, reachableStates); -} - -template -storm::storage::BitVector computeStatesThatCanReachTarget(storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::BitVector const& targetStates) { - storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); - auto backwardTransitions = transitionMatrix.transpose(true); - return storm::utility::graph::performProbGreater0(backwardTransitions, allStates, targetStates); -} - -template -storm::storage::BitVector computeBadMecStates(storm::storage::MaximalEndComponentDecomposition const& mecs, - storm::storage::BitVector const& statesThatCanReachTarget, uint64_t numberOfStates) { - storm::storage::BitVector badMecStates(numberOfStates, false); - for (auto const& mec : mecs) { - if (!mec.containsAnyState(statesThatCanReachTarget)) { - for (auto const& stateChoices : mec) { - badMecStates.set(stateChoices.first, true); - } - } - } - return badMecStates; -} - template storm::storage::MaximalEndComponentDecomposition computeTargetReachingMecs(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& initialStates, From 06513988a8724f66db45ba2f824064cafed7e443 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:01:34 +0200 Subject: [PATCH 26/65] Clarify CVaR query and WR backend naming --- .../modelchecker/cvar/CvarClassification.h | 8 +-- ...formation.cpp => CvarQueryInformation.cpp} | 4 +- ...laInformation.h => CvarQueryInformation.h} | 4 +- ... SparseWeightedReachabilityCvarLpHelper.h} | 70 +++++++++---------- .../modelchecker/cvar/SspModelInformation.h | 6 +- ...ata.h => WeightedReachabilityCvarLpData.h} | 12 ++-- .../WeightedReachabilityModelInformation.h | 8 +-- .../prctl/SparseMdpPrctlModelChecker.cpp | 24 +++---- 8 files changed, 65 insertions(+), 71 deletions(-) rename src/storm/modelchecker/cvar/{CvarFormulaInformation.cpp => CvarQueryInformation.cpp} (93%) rename src/storm/modelchecker/cvar/{CvarFormulaInformation.h => CvarQueryInformation.h} (78%) rename src/storm/modelchecker/cvar/{SparseCvarHelper.h => SparseWeightedReachabilityCvarLpHelper.h} (73%) rename src/storm/modelchecker/cvar/{CvarModelCheckingData.h => WeightedReachabilityCvarLpData.h} (88%) diff --git a/src/storm/modelchecker/cvar/CvarClassification.h b/src/storm/modelchecker/cvar/CvarClassification.h index 5b62da492d..80ffb87177 100644 --- a/src/storm/modelchecker/cvar/CvarClassification.h +++ b/src/storm/modelchecker/cvar/CvarClassification.h @@ -2,7 +2,7 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" -#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/storage/BitVector.h" #include "storm/utility/macros.h" @@ -35,7 +35,7 @@ enum class CvarProblemKind { WeightedReachability, Ssp }; * explicit classification provides the extension point for future CVaR query * families. */ -inline CvarQueryKind classifyCvarQuery(CvarFormulaInformation const&) { +inline CvarQueryKind classifyCvarQuery(CvarQueryInformation const&) { return CvarQueryKind::ReachabilityReward; } @@ -48,9 +48,9 @@ inline CvarQueryKind classifyCvarQuery(CvarFormulaInformation const&) { * weighted-reachability path until SSP preprocessing is introduced. */ template -CvarProblemKind classifyCvarProblem(SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, CvarQueryKind, +CvarProblemKind classifyCvarProblem(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, CvarQueryKind, storm::storage::BitVector const&, CvarMethod method) { - std::string rewardModelName = formulaInformation.rewardModelName ? formulaInformation.rewardModelName.get() : ""; + std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; auto const& rewardModel = model.getRewardModel(rewardModelName); if (rewardModelName.empty()) { rewardModelName = model.getUniqueRewardModelName(); diff --git a/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp b/src/storm/modelchecker/cvar/CvarQueryInformation.cpp similarity index 93% rename from src/storm/modelchecker/cvar/CvarFormulaInformation.cpp rename to src/storm/modelchecker/cvar/CvarQueryInformation.cpp index ed68ed4d59..36153a6708 100644 --- a/src/storm/modelchecker/cvar/CvarFormulaInformation.cpp +++ b/src/storm/modelchecker/cvar/CvarQueryInformation.cpp @@ -1,4 +1,4 @@ -#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/logic/EventuallyFormula.h" @@ -9,7 +9,7 @@ namespace storm { namespace modelchecker { namespace cvar { -CvarFormulaInformation extractCvarFormulaInformation(storm::logic::CvarFormula const& formula) { +CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const& formula) { storm::logic::Formula const& embeddedFormula = formula.getSubformula(); STORM_LOG_THROW(embeddedFormula.isRewardOperatorFormula(), storm::exceptions::InvalidPropertyException, "CVaR formulas currently require an embedded reward operator formula."); diff --git a/src/storm/modelchecker/cvar/CvarFormulaInformation.h b/src/storm/modelchecker/cvar/CvarQueryInformation.h similarity index 78% rename from src/storm/modelchecker/cvar/CvarFormulaInformation.h rename to src/storm/modelchecker/cvar/CvarQueryInformation.h index ae806fd7f2..8157317f99 100644 --- a/src/storm/modelchecker/cvar/CvarFormulaInformation.h +++ b/src/storm/modelchecker/cvar/CvarQueryInformation.h @@ -10,14 +10,14 @@ namespace storm { namespace modelchecker { namespace cvar { -struct CvarFormulaInformation { +struct CvarQueryInformation { double alpha; storm::solver::OptimizationDirection optimizationDirection; boost::optional rewardModelName; std::shared_ptr targetFormula; }; -CvarFormulaInformation extractCvarFormulaInformation(storm::logic::CvarFormula const& formula); +CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const& formula); } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/cvar/SparseCvarHelper.h b/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h similarity index 73% rename from src/storm/modelchecker/cvar/SparseCvarHelper.h rename to src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h index af9905fd49..c595de6b85 100644 --- a/src/storm/modelchecker/cvar/SparseCvarHelper.h +++ b/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h @@ -8,7 +8,7 @@ #include "storm/environment/Environment.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/exceptions/UnexpectedException.h" -#include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" #include "storm/solver/LpSolver.h" #include "storm/storage/Scheduler.h" #include "storm/storage/expressions/BinaryRelationType.h" @@ -42,21 +42,20 @@ struct CvarComputationResult { * @see https://doi.org/10.1145/3209108.3209176 Fig. 4 for a description of the algorithm as implemented (and slightly altered) here. */ template -class SparseCvarHelper { +class SparseWeightedReachabilityCvarLpHelper { public: - explicit SparseCvarHelper(CvarModelCheckingData const& modelCheckingData) : modelCheckingData(modelCheckingData) { + explicit SparseWeightedReachabilityCvarLpHelper(WeightedReachabilityCvarLpData const& lpData) : lpData(lpData) { // Intentionally left empty. } CvarComputationResult computeCvar(Environment const&, bool produceScheduler = false) const { - STORM_LOG_THROW(!modelCheckingData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, + STORM_LOG_THROW(!lpData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, "CVaR model checking requires at least one target reward threshold candidate."); - // checking all possible threshold values for VaR iteratively. Could possibly improved by e.g. creating a product MDP or similar. std::optional bestValue; std::unique_ptr> bestScheduler; - for (auto const& threshold : modelCheckingData.candidateThresholds) { - auto thresholdData = createCvarThresholdData(modelCheckingData.targetStates, modelCheckingData.terminalRewards, threshold); + for (auto const& threshold : lpData.candidateThresholds) { + auto thresholdData = createCvarThresholdData(lpData.targetStates, lpData.terminalRewards, threshold); auto thresholdResult = buildLpForThreshold(thresholdData, produceScheduler); if (!thresholdResult.has_value()) { continue; @@ -66,8 +65,8 @@ class SparseCvarHelper { if (produceScheduler) { bestScheduler = std::move(thresholdResult->scheduler); } - } else if ((storm::solver::minimize(modelCheckingData.optimizationDirection) && thresholdResult->value < bestValue.value()) || - (storm::solver::maximize(modelCheckingData.optimizationDirection) && thresholdResult->value > bestValue.value())) { + } else if ((storm::solver::minimize(lpData.optimizationDirection) && thresholdResult->value < bestValue.value()) || + (storm::solver::maximize(lpData.optimizationDirection) && thresholdResult->value > bestValue.value())) { bestValue = thresholdResult->value; if (produceScheduler) { bestScheduler = std::move(thresholdResult->scheduler); @@ -77,7 +76,7 @@ class SparseCvarHelper { STORM_LOG_THROW(bestValue.has_value(), storm::exceptions::UnexpectedException, "CVaR model checking did not find a feasible LP for any threshold candidate."); - return {bestValue.value() / storm::utility::convertNumber(modelCheckingData.alpha), std::move(bestScheduler)}; + return {bestValue.value() / storm::utility::convertNumber(lpData.alpha), std::move(bestScheduler)}; } private: @@ -87,40 +86,38 @@ class SparseCvarHelper { auto lpSolverFactory = storm::utility::solver::getLpSolverFactory(); auto solver = lpSolverFactory->createRaw("cvar"); - solver->setOptimizationDirection(modelCheckingData.optimizationDirection); - auto backwardChoices = modelCheckingData.transitionMatrix.transpose(); + solver->setOptimizationDirection(lpData.optimizationDirection); + auto backwardChoices = lpData.transitionMatrix.transpose(); std::vector actionFlowVariables; - actionFlowVariables.reserve(modelCheckingData.transitionMatrix.getRowCount()); - for (uint64_t row = 0; row < modelCheckingData.transitionMatrix.getRowCount(); ++row) { + actionFlowVariables.reserve(lpData.transitionMatrix.getRowCount()); + for (uint64_t row = 0; row < lpData.transitionMatrix.getRowCount(); ++row) { actionFlowVariables.push_back(solver->addLowerBoundedContinuousVariable("y_" + std::to_string(row), storm::utility::zero())); } - std::vector> recurrentFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); - for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { - if (modelCheckingData.targetStates[state]) { + std::vector> recurrentFlowVariables(lpData.transitionMatrix.getRowGroupCount(), std::nullopt); + for (uint64_t state = 0; state < lpData.transitionMatrix.getRowGroupCount(); ++state) { + if (lpData.targetStates[state]) { recurrentFlowVariables[state] = solver->addLowerBoundedContinuousVariable("x_" + std::to_string(state), storm::utility::zero()); } } - std::vector> splitFlowVariables(modelCheckingData.transitionMatrix.getRowGroupCount(), std::nullopt); - for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { + std::vector> splitFlowVariables(lpData.transitionMatrix.getRowGroupCount(), std::nullopt); + for (uint64_t state = 0; state < lpData.transitionMatrix.getRowGroupCount(); ++state) { if (thresholdData.targetStatesBelowOrAtThreshold[state]) { - splitFlowVariables[state] = solver->addLowerBoundedContinuousVariable("xb_" + std::to_string(state), storm::utility::zero(), - modelCheckingData.terminalRewards[state]); + splitFlowVariables[state] = + solver->addLowerBoundedContinuousVariable("xb_" + std::to_string(state), storm::utility::zero(), lpData.terminalRewards[state]); } } solver->update(); - // Equation (2) from Fig. 4: - for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { - auto outgoingActions = modelCheckingData.transitionMatrix.getRowGroupIndices(state); + for (uint64_t state = 0; state < lpData.transitionMatrix.getRowGroupCount(); ++state) { + auto outgoingActions = lpData.transitionMatrix.getRowGroupIndices(state); auto incomingActions = backwardChoices.getRow(state); uint64_t reservedSize = outgoingActions.size() + incomingActions.getNumberOfEntries() + (recurrentFlowVariables[state].has_value() ? 1 : 0); RawLpConstraint constraint(storm::expressions::RelationType::Equal, - state == modelCheckingData.initialState ? storm::utility::one() : storm::utility::zero(), - reservedSize); + state == lpData.initialState ? storm::utility::one() : storm::utility::zero(), reservedSize); std::map actionCoefficients; for (auto const& incomingAction : incomingActions) { @@ -141,16 +138,14 @@ class SparseCvarHelper { solver->addConstraint("transient_flow_" + std::to_string(state), constraint); } - // Equation (3): RawLpConstraint recurrentConstraint(storm::expressions::RelationType::Equal, storm::utility::one(), - modelCheckingData.targetStates.getNumberOfSetBits()); + lpData.targetStates.getNumberOfSetBits()); - for (auto state : modelCheckingData.targetStates) { + for (auto state : lpData.targetStates) { recurrentConstraint.addToLhs(recurrentFlowVariables[state].value(), storm::utility::one()); } solver->addConstraint("recurrent_behaviour", recurrentConstraint); - // Equation (4): for (auto state : thresholdData.targetStatesBelowThreshold) { RawLpConstraint splitEqualityConstraint(storm::expressions::RelationType::Equal, storm::utility::zero(), 2); splitEqualityConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); @@ -164,9 +159,8 @@ class SparseCvarHelper { solver->addConstraint("split_le_" + std::to_string(state), splitInequalityConstraint); } - // Equation (5): RawLpConstraint probabilityConsistentSplitConstraint(storm::expressions::RelationType::Equal, - storm::utility::convertNumber(modelCheckingData.alpha), + storm::utility::convertNumber(lpData.alpha), thresholdData.targetStatesBelowOrAtThreshold.getNumberOfSetBits()); for (auto state : thresholdData.targetStatesBelowOrAtThreshold) { probabilityConsistentSplitConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); @@ -184,15 +178,15 @@ class SparseCvarHelper { std::unique_ptr> scheduler; if (produceScheduler) { - scheduler = std::make_unique>(modelCheckingData.transitionMatrix.getRowGroupCount()); - for (uint64_t state = 0; state < modelCheckingData.transitionMatrix.getRowGroupCount(); ++state) { - if (modelCheckingData.targetStates[state]) { + scheduler = std::make_unique>(lpData.transitionMatrix.getRowGroupCount()); + for (uint64_t state = 0; state < lpData.transitionMatrix.getRowGroupCount(); ++state) { + if (lpData.targetStates[state]) { scheduler->setDontCare(state); continue; } - uint64_t firstRow = modelCheckingData.transitionMatrix.getRowGroupIndices()[state]; - uint64_t lastRow = modelCheckingData.transitionMatrix.getRowGroupIndices()[state + 1]; + uint64_t firstRow = lpData.transitionMatrix.getRowGroupIndices()[state]; + uint64_t lastRow = lpData.transitionMatrix.getRowGroupIndices()[state + 1]; storm::storage::Distribution actionDistribution; actionDistribution.reserve(lastRow - firstRow); @@ -217,7 +211,7 @@ class SparseCvarHelper { return CvarComputationResult{solver->getObjectiveValue(), std::move(scheduler)}; } - CvarModelCheckingData const& modelCheckingData; + WeightedReachabilityCvarLpData const& lpData; }; } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/cvar/SspModelInformation.h b/src/storm/modelchecker/cvar/SspModelInformation.h index 6e3cdae8b8..e20fdf4ce5 100644 --- a/src/storm/modelchecker/cvar/SspModelInformation.h +++ b/src/storm/modelchecker/cvar/SspModelInformation.h @@ -5,7 +5,7 @@ #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarPreprocessingUtilities.h" -#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" #include "storm/utility/constants.h" @@ -67,11 +67,11 @@ std::vector extractChoiceCostsForSsp( template SspModelInformation extractSspModelInformation(SparseMdpModelType const& model, - CvarFormulaInformation const& formulaInformation, + CvarQueryInformation const& queryInformation, storm::storage::BitVector const& targetStates) { using ValueType = typename SparseMdpModelType::ValueType; - std::string rewardModelName = formulaInformation.rewardModelName ? formulaInformation.rewardModelName.get() : ""; + std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; auto const& rewardModel = model.getRewardModel(rewardModelName); if (rewardModelName.empty()) { rewardModelName = model.getUniqueRewardModelName(); diff --git a/src/storm/modelchecker/cvar/CvarModelCheckingData.h b/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h similarity index 88% rename from src/storm/modelchecker/cvar/CvarModelCheckingData.h rename to src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h index 9e04bab176..47bc5727c4 100644 --- a/src/storm/modelchecker/cvar/CvarModelCheckingData.h +++ b/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h @@ -5,7 +5,7 @@ #include #include -#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" @@ -24,7 +24,7 @@ struct CvarThresholdData { }; template -struct CvarModelCheckingData { +struct WeightedReachabilityCvarLpData { double alpha; storm::solver::OptimizationDirection optimizationDirection; uint64_t initialState; @@ -73,12 +73,12 @@ CvarThresholdData createCvarThresholdData(storm::storage::BitVector c } template -CvarModelCheckingData createCvarModelCheckingData(CvarFormulaInformation const& formulaInformation, - WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { +WeightedReachabilityCvarLpData createWeightedReachabilityCvarLpData( + CvarQueryInformation const& queryInformation, WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { auto candidateThresholds = collectCandidateThresholds(weightedReachabilityModelInformation.effectiveTargetStates, weightedReachabilityModelInformation.terminalRewards); - return {formulaInformation.alpha, - formulaInformation.optimizationDirection, + return {queryInformation.alpha, + queryInformation.optimizationDirection, weightedReachabilityModelInformation.initialState, weightedReachabilityModelInformation.rewardModelName, weightedReachabilityModelInformation.effectiveTargetStates, diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h index dd55f2f4e7..9da81f49c4 100644 --- a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h +++ b/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h @@ -6,7 +6,7 @@ #include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/modelchecker/cvar/CvarPreprocessingUtilities.h" -#include "storm/modelchecker/cvar/CvarFormulaInformation.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/storage/BitVector.h" #include "storm/storage/MaximalEndComponentDecomposition.h" #include "storm/storage/SparseMatrix.h" @@ -23,7 +23,7 @@ namespace cvar { /*! * Collects and preprocesses the model information needed by the CVaR LP. * - * The LP implemented in SparseCvarHelper follows the weighted-reachability setting from the referenced CVaR paper: + * The LP implemented in SparseWeightedReachabilityCvarLpHelper follows the weighted-reachability setting from the referenced CVaR paper: * a single initial state, terminal rewards on absorbing target states, and no reward before reaching such a terminal * state. This helper enforces these assumptions on the input model and rewrites the transition structure where needed: * end components that cannot reach the original target set become zero-reward terminal targets, while target-reaching @@ -99,11 +99,11 @@ void applyTargetReachingMecCollapse(storm::storage::SparseMatrix& tra template WeightedReachabilityModelInformation extractWeightedReachabilityModelInformation( - SparseMdpModelType const& model, CvarFormulaInformation const& formulaInformation, storm::storage::BitVector const& targetStates, + SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const& targetStates, bool produceScheduler = false) { using ValueType = typename SparseMdpModelType::ValueType; - std::string rewardModelName = formulaInformation.rewardModelName ? formulaInformation.rewardModelName.get() : ""; + std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; auto const& rewardModel = model.getRewardModel(rewardModelName); if (rewardModelName.empty()) { rewardModelName = model.getUniqueRewardModelName(); diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index db195e3bea..abe106d809 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -8,10 +8,10 @@ #include "storm/exceptions/NotImplementedException.h" #include "storm/logic/FragmentSpecification.h" #include "storm/modelchecker/cvar/CvarClassification.h" -#include "storm/modelchecker/cvar/CvarFormulaInformation.h" -#include "storm/modelchecker/cvar/CvarModelCheckingData.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h" #include "storm/modelchecker/cvar/SspModelInformation.h" -#include "storm/modelchecker/cvar/SparseCvarHelper.h" +#include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" #include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" @@ -540,11 +540,11 @@ std::unique_ptr SparseMdpPrctlModelChecker::che "CVaR is not supported on models with multiple initial states."); // check if query fits specified format - auto cvarFormulaInformation = storm::modelchecker::cvar::extractCvarFormulaInformation(checkTask.getFormula()); + auto cvarQueryInformation = storm::modelchecker::cvar::extractCvarQueryInformation(checkTask.getFormula()); auto targetStates = - this->check(env, *cvarFormulaInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); - auto queryKind = storm::modelchecker::cvar::classifyCvarQuery(cvarFormulaInformation); - auto problemKind = storm::modelchecker::cvar::classifyCvarProblem(this->getModel(), cvarFormulaInformation, queryKind, targetStates, + this->check(env, *cvarQueryInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); + auto queryKind = storm::modelchecker::cvar::classifyCvarQuery(cvarQueryInformation); + auto problemKind = storm::modelchecker::cvar::classifyCvarProblem(this->getModel(), cvarQueryInformation, queryKind, targetStates, env.modelchecker().cvar().getMethod()); std::unique_ptr result; @@ -552,12 +552,12 @@ std::unique_ptr SparseMdpPrctlModelChecker::che case storm::modelchecker::cvar::CvarProblemKind::WeightedReachability: { // check if model fits terminal reward auto weightedReachabilityModelInformation = storm::modelchecker::cvar::extractWeightedReachabilityModelInformation( - this->getModel(), cvarFormulaInformation, targetStates, checkTask.isProduceSchedulersSet()); + this->getModel(), cvarQueryInformation, targetStates, checkTask.isProduceSchedulersSet()); // combine info into 1 simplified object - auto cvarModelCheckingData = - storm::modelchecker::cvar::createCvarModelCheckingData(cvarFormulaInformation, weightedReachabilityModelInformation); + auto weightedReachabilityCvarLpData = + storm::modelchecker::cvar::createWeightedReachabilityCvarLpData(cvarQueryInformation, weightedReachabilityModelInformation); - storm::modelchecker::cvar::SparseCvarHelper cvarHelper(cvarModelCheckingData); + storm::modelchecker::cvar::SparseWeightedReachabilityCvarLpHelper cvarHelper(weightedReachabilityCvarLpData); auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); result = std::unique_ptr( new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); @@ -567,7 +567,7 @@ std::unique_ptr SparseMdpPrctlModelChecker::che break; } case storm::modelchecker::cvar::CvarProblemKind::Ssp: { - auto sspModelInformation = storm::modelchecker::cvar::extractSspModelInformation(this->getModel(), cvarFormulaInformation, targetStates); + auto sspModelInformation = storm::modelchecker::cvar::extractSspModelInformation(this->getModel(), cvarQueryInformation, targetStates); static_cast(sspModelInformation); STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR for stochastic shortest path objectives is not implemented yet."); } From a0ab33580f322c2639882d4ea393b50e816aa986 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:18:01 +0200 Subject: [PATCH 27/65] Extract SSP & WR CVaR preprocessing --- .../cvar/WeightedReachabilityCvarLpData.h | 17 +++++----- .../SspCvarPreprocessingResult.h | 31 +++++++++++++++++ .../SspCvarPreprocessor.h} | 31 +++++------------ ...ghtedReachabilityCvarPreprocessingResult.h | 29 ++++++++++++++++ .../WeightedReachabilityCvarPreprocessor.h} | 34 +++++-------------- .../prctl/SparseMdpPrctlModelChecker.cpp | 14 ++++---- 6 files changed, 93 insertions(+), 63 deletions(-) create mode 100644 src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h rename src/storm/modelchecker/cvar/{SspModelInformation.h => preprocessing/SspCvarPreprocessor.h} (80%) create mode 100644 src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h rename src/storm/modelchecker/cvar/{WeightedReachabilityModelInformation.h => preprocessing/WeightedReachabilityCvarPreprocessor.h} (85%) diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h b/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h index 47bc5727c4..9cc3e8686c 100644 --- a/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h +++ b/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h @@ -6,7 +6,7 @@ #include #include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" +#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" #include "storm/utility/constants.h" @@ -74,17 +74,18 @@ CvarThresholdData createCvarThresholdData(storm::storage::BitVector c template WeightedReachabilityCvarLpData createWeightedReachabilityCvarLpData( - CvarQueryInformation const& queryInformation, WeightedReachabilityModelInformation const& weightedReachabilityModelInformation) { + CvarQueryInformation const& queryInformation, + preprocessing::WeightedReachabilityCvarPreprocessingResult const& weightedReachabilityPreprocessingResult) { auto candidateThresholds = - collectCandidateThresholds(weightedReachabilityModelInformation.effectiveTargetStates, weightedReachabilityModelInformation.terminalRewards); + collectCandidateThresholds(weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards); return {queryInformation.alpha, queryInformation.optimizationDirection, - weightedReachabilityModelInformation.initialState, - weightedReachabilityModelInformation.rewardModelName, - weightedReachabilityModelInformation.effectiveTargetStates, - weightedReachabilityModelInformation.terminalRewards, + weightedReachabilityPreprocessingResult.initialState, + weightedReachabilityPreprocessingResult.rewardModelName, + weightedReachabilityPreprocessingResult.effectiveTargetStates, + weightedReachabilityPreprocessingResult.terminalRewards, std::move(candidateThresholds), - weightedReachabilityModelInformation.transitionMatrix}; + weightedReachabilityPreprocessingResult.transitionMatrix}; } } // namespace cvar diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h new file mode 100644 index 0000000000..03df1d4f01 --- /dev/null +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "storm/storage/BitVector.h" +#include "storm/storage/SparseMatrix.h" + +namespace storm { +namespace modelchecker { +namespace cvar { +namespace preprocessing { + +template +struct SspCvarPreprocessingResult { + std::string rewardModelName; + uint64_t initialState; + storm::storage::BitVector targetStates; + storm::storage::BitVector reachableStates; + storm::storage::BitVector statesThatCanReachTarget; + storm::storage::BitVector badMecStates; + bool liftedStateRewardsToChoiceCosts; + bool normalizedTargetStatesToAbsorbing; + std::vector choiceCosts; + storm::storage::SparseMatrix transitionMatrix; +}; + +} // namespace preprocessing +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/SspModelInformation.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h similarity index 80% rename from src/storm/modelchecker/cvar/SspModelInformation.h rename to src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index e20fdf4ce5..164254344c 100644 --- a/src/storm/modelchecker/cvar/SspModelInformation.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -6,6 +6,7 @@ #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarPreprocessingUtilities.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" #include "storm/utility/constants.h" @@ -15,28 +16,14 @@ namespace storm { namespace modelchecker { namespace cvar { +namespace preprocessing { /*! - * Collects the normalized SSP model information needed by the future CVaR VI. + * Preprocesses a sparse MDP for the SSP CVaR backend. * - * The SSP CVaR algorithm works with per-choice costs. If the input reward model - * only uses state rewards, we lift those rewards to equivalent state-action - * costs by copying the state reward to each outgoing choice of the state. + * This normalizes the model to terminal-goal semantics and extracts the + * choice-based costs and graph information needed by the future Pareto-front VI. */ -template -struct SspModelInformation { - std::string rewardModelName; - uint64_t initialState; - storm::storage::BitVector targetStates; - storm::storage::BitVector reachableStates; - storm::storage::BitVector statesThatCanReachTarget; - storm::storage::BitVector badMecStates; - bool liftedStateRewardsToChoiceCosts; - bool normalizedTargetStatesToAbsorbing; - std::vector choiceCosts; - storm::storage::SparseMatrix transitionMatrix; -}; - template std::vector extractChoiceCostsForSsp( SparseMdpModelType const& model, typename SparseMdpModelType::RewardModelType const& rewardModel, storm::storage::BitVector const& targetStates) { @@ -48,7 +35,6 @@ std::vector extractChoiceCostsForSsp( for (uint64_t state = 0; state < model.getNumberOfStates(); ++state) { if (targetStates[state]) { - // Costs stop once the goal state is reached. continue; } @@ -66,9 +52,9 @@ std::vector extractChoiceCostsForSsp( } template -SspModelInformation extractSspModelInformation(SparseMdpModelType const& model, - CvarQueryInformation const& queryInformation, - storm::storage::BitVector const& targetStates) { +SspCvarPreprocessingResult preprocessSspCvar(SparseMdpModelType const& model, + CvarQueryInformation const& queryInformation, + storm::storage::BitVector const& targetStates) { using ValueType = typename SparseMdpModelType::ValueType; std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; @@ -128,6 +114,7 @@ SspModelInformation extractSspModelInfor std::move(transitionMatrix)}; } +} // namespace preprocessing } // namespace cvar } // namespace modelchecker } // namespace storm diff --git a/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h b/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h new file mode 100644 index 0000000000..0a63d03762 --- /dev/null +++ b/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "storm/storage/BitVector.h" +#include "storm/storage/SparseMatrix.h" + +namespace storm { +namespace modelchecker { +namespace cvar { +namespace preprocessing { + +template +struct WeightedReachabilityCvarPreprocessingResult { + std::string rewardModelName; + uint64_t initialState; + storm::storage::BitVector originalTargetStates; + storm::storage::BitVector effectiveTargetStates; + storm::storage::BitVector badMecStates; + uint64_t collapsedTargetReachingMecCount; + std::vector terminalRewards; + storm::storage::SparseMatrix transitionMatrix; +}; + +} // namespace preprocessing +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h b/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h similarity index 85% rename from src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h rename to src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h index 9da81f49c4..639e72c299 100644 --- a/src/storm/modelchecker/cvar/WeightedReachabilityModelInformation.h +++ b/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h @@ -1,15 +1,11 @@ #pragma once -#include -#include - #include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/InvalidPropertyException.h" #include "storm/modelchecker/cvar/CvarPreprocessingUtilities.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/storage/BitVector.h" +#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h" #include "storm/storage/MaximalEndComponentDecomposition.h" -#include "storm/storage/SparseMatrix.h" #include "storm/transformer/EndComponentEliminator.h" #include "storm/utility/constants.h" #include "storm/utility/graph.h" @@ -19,28 +15,15 @@ namespace storm { namespace modelchecker { namespace cvar { +namespace preprocessing { /*! - * Collects and preprocesses the model information needed by the CVaR LP. + * Preprocesses a sparse MDP for the weighted-reachability CVaR LP backend. * - * The LP implemented in SparseWeightedReachabilityCvarLpHelper follows the weighted-reachability setting from the referenced CVaR paper: - * a single initial state, terminal rewards on absorbing target states, and no reward before reaching such a terminal - * state. This helper enforces these assumptions on the input model and rewrites the transition structure where needed: - * end components that cannot reach the original target set become zero-reward terminal targets, while target-reaching - * end components are collapsed before the LP is built. + * This enforces the weighted-reachability assumptions on the reward model and + * transition structure and returns the normalized terminal-reward instance that + * can be consumed by the LP helper. */ -template -struct WeightedReachabilityModelInformation { - std::string rewardModelName; - uint64_t initialState; - storm::storage::BitVector originalTargetStates; - storm::storage::BitVector effectiveTargetStates; - storm::storage::BitVector badMecStates; - uint64_t collapsedTargetReachingMecCount; - std::vector terminalRewards; - storm::storage::SparseMatrix transitionMatrix; -}; - template storm::storage::MaximalEndComponentDecomposition computeTargetReachingMecs(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& initialStates, @@ -70,7 +53,6 @@ void applyTargetReachingMecCollapse(storm::storage::SparseMatrix& tra storm::storage::MaximalEndComponentDecomposition const& targetReachingMecs) { storm::storage::BitVector allStates(transitionMatrix.getRowGroupCount(), true); storm::storage::BitVector noSinkRows(transitionMatrix.getRowGroupCount(), false); - // Preserve the eliminated end component as a single representative state with a self-loop choice, plus the original exits. auto eliminationResult = storm::transformer::EndComponentEliminator::transform(transitionMatrix, targetReachingMecs, allStates, noSinkRows); storm::storage::BitVector newEffectiveTargetStates(eliminationResult.matrix.getRowGroupCount(), false); @@ -98,7 +80,7 @@ void applyTargetReachingMecCollapse(storm::storage::SparseMatrix& tra } template -WeightedReachabilityModelInformation extractWeightedReachabilityModelInformation( +WeightedReachabilityCvarPreprocessingResult preprocessWeightedReachabilityCvar( SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const& targetStates, bool produceScheduler = false) { using ValueType = typename SparseMdpModelType::ValueType; @@ -168,6 +150,8 @@ WeightedReachabilityModelInformation ext std::move(terminalRewards), std::move(transitionMatrix)}; } + +} // namespace preprocessing } // namespace cvar } // namespace modelchecker } // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index abe106d809..4066567db2 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -9,10 +9,10 @@ #include "storm/logic/FragmentSpecification.h" #include "storm/modelchecker/cvar/CvarClassification.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h" +#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h" #include "storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h" -#include "storm/modelchecker/cvar/SspModelInformation.h" #include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" -#include "storm/modelchecker/cvar/WeightedReachabilityModelInformation.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" #include "storm/modelchecker/helper/infinitehorizon/SparseNondeterministicInfiniteHorizonHelper.h" @@ -550,12 +550,10 @@ std::unique_ptr SparseMdpPrctlModelChecker::che std::unique_ptr result; switch (problemKind) { case storm::modelchecker::cvar::CvarProblemKind::WeightedReachability: { - // check if model fits terminal reward - auto weightedReachabilityModelInformation = storm::modelchecker::cvar::extractWeightedReachabilityModelInformation( + auto weightedReachabilityPreprocessingResult = storm::modelchecker::cvar::preprocessing::preprocessWeightedReachabilityCvar( this->getModel(), cvarQueryInformation, targetStates, checkTask.isProduceSchedulersSet()); - // combine info into 1 simplified object auto weightedReachabilityCvarLpData = - storm::modelchecker::cvar::createWeightedReachabilityCvarLpData(cvarQueryInformation, weightedReachabilityModelInformation); + storm::modelchecker::cvar::createWeightedReachabilityCvarLpData(cvarQueryInformation, weightedReachabilityPreprocessingResult); storm::modelchecker::cvar::SparseWeightedReachabilityCvarLpHelper cvarHelper(weightedReachabilityCvarLpData); auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); @@ -567,8 +565,8 @@ std::unique_ptr SparseMdpPrctlModelChecker::che break; } case storm::modelchecker::cvar::CvarProblemKind::Ssp: { - auto sspModelInformation = storm::modelchecker::cvar::extractSspModelInformation(this->getModel(), cvarQueryInformation, targetStates); - static_cast(sspModelInformation); + auto sspPreprocessingResult = storm::modelchecker::cvar::preprocessing::preprocessSspCvar(this->getModel(), cvarQueryInformation, targetStates); + static_cast(sspPreprocessingResult); STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR for stochastic shortest path objectives is not implemented yet."); } } From 2be56a7a50e1c376ba1ce02ac40ab5d333663584 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:30:44 +0200 Subject: [PATCH 28/65] Refactor CVaR backend selection and dispatch --- .../modelchecker/cvar/CvarClassification.h | 22 ++++++------- .../modelchecker/cvar/CvarComputationResult.h | 19 ++++++++++++ .../SparseWeightedReachabilityCvarLpHelper.h | 6 +--- src/storm/modelchecker/cvar/SspCvarBackend.h | 28 +++++++++++++++++ .../cvar/WeightedReachabilityCvarBackend.h | 29 +++++++++++++++++ .../prctl/SparseMdpPrctlModelChecker.cpp | 31 +++++++------------ 6 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 src/storm/modelchecker/cvar/CvarComputationResult.h create mode 100644 src/storm/modelchecker/cvar/SspCvarBackend.h create mode 100644 src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h diff --git a/src/storm/modelchecker/cvar/CvarClassification.h b/src/storm/modelchecker/cvar/CvarClassification.h index 80ffb87177..f4f95099a3 100644 --- a/src/storm/modelchecker/cvar/CvarClassification.h +++ b/src/storm/modelchecker/cvar/CvarClassification.h @@ -13,20 +13,20 @@ namespace cvar { /*! * Classifies the embedded CVaR query at the formula level. * - * This is intentionally separate from the concrete problem kind below: - * multiple concrete problem kinds may share the same surface query syntax. + * This is intentionally separate from the concrete backend selection below: + * multiple concrete backends may share the same surface query syntax. */ enum class CvarQueryKind { ReachabilityReward }; /*! - * Classifies the concrete solver problem induced by a CVaR query on a given + * Selects the concrete CVaR backend induced by a query on a given * model and reward structure. * * Weighted reachability is the currently implemented LP-based terminal-reward * setting. SSP will be used by the future value-iteration implementation for * accumulated state-action costs until reaching the goal. */ -enum class CvarProblemKind { WeightedReachability, Ssp }; +enum class CvarBackendKind { WeightedReachability, Ssp }; /*! * Determines the formula-level CVaR query kind. @@ -40,7 +40,7 @@ inline CvarQueryKind classifyCvarQuery(CvarQueryInformation const&) { } /*! - * Classifies the concrete CVaR problem kind to use. + * Selects the concrete CVaR backend to use. * * The selection can be overridden explicitly via the CVaR method setting. * Otherwise, classification stays conservative: state-action reward models are @@ -48,8 +48,8 @@ inline CvarQueryKind classifyCvarQuery(CvarQueryInformation const&) { * weighted-reachability path until SSP preprocessing is introduced. */ template -CvarProblemKind classifyCvarProblem(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, CvarQueryKind, - storm::storage::BitVector const&, CvarMethod method) { +CvarBackendKind selectCvarBackend(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, CvarQueryKind, + storm::storage::BitVector const&, CvarMethod method) { std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; auto const& rewardModel = model.getRewardModel(rewardModelName); if (rewardModelName.empty()) { @@ -59,20 +59,20 @@ CvarProblemKind classifyCvarProblem(SparseMdpModelType const& model, CvarQueryIn if (method == CvarMethod::WeightedReachability) { STORM_LOG_THROW(!rewardModel.hasStateActionRewards() && !rewardModel.hasTransitionRewards(), storm::exceptions::InvalidPropertyException, "The weighted-reachability CVaR method requires state-based terminal rewards only."); - return CvarProblemKind::WeightedReachability; + return CvarBackendKind::WeightedReachability; } STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, "CVaR queries with transition rewards are not supported yet."); if (method == CvarMethod::SspParetoVi) { - return CvarProblemKind::Ssp; + return CvarBackendKind::Ssp; } if (rewardModel.hasStateActionRewards()) { - return CvarProblemKind::Ssp; + return CvarBackendKind::Ssp; } - return CvarProblemKind::WeightedReachability; + return CvarBackendKind::WeightedReachability; } } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/cvar/CvarComputationResult.h b/src/storm/modelchecker/cvar/CvarComputationResult.h new file mode 100644 index 0000000000..706e8d89dc --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarComputationResult.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "storm/storage/Scheduler.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +struct CvarComputationResult { + ValueType value; + std::unique_ptr> scheduler; +}; + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h b/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h index c595de6b85..72c2572740 100644 --- a/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h +++ b/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h @@ -8,6 +8,7 @@ #include "storm/environment/Environment.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/exceptions/UnexpectedException.h" +#include "storm/modelchecker/cvar/CvarComputationResult.h" #include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" #include "storm/solver/LpSolver.h" #include "storm/storage/Scheduler.h" @@ -20,11 +21,6 @@ namespace storm { namespace modelchecker { namespace cvar { -template -struct CvarComputationResult { - ValueType value; - std::unique_ptr> scheduler; -}; /*! * Solves an LP for Conditional Value-at-Risk on an MDP with a terminal reward objective. * diff --git a/src/storm/modelchecker/cvar/SspCvarBackend.h b/src/storm/modelchecker/cvar/SspCvarBackend.h new file mode 100644 index 0000000000..5df601bc39 --- /dev/null +++ b/src/storm/modelchecker/cvar/SspCvarBackend.h @@ -0,0 +1,28 @@ +#pragma once + +#include "storm/environment/Environment.h" +#include "storm/exceptions/NotImplementedException.h" +#include "storm/modelchecker/cvar/CvarComputationResult.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h" +#include "storm/storage/BitVector.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +CvarComputationResult computeSspCvar(Environment const&, SparseMdpModelType const& model, + CvarQueryInformation const& queryInformation, + storm::storage::BitVector const& targetStates, + bool produceScheduler = false) { + auto sspPreprocessingResult = preprocessing::preprocessSspCvar(model, queryInformation, targetStates); + static_cast(sspPreprocessingResult); + static_cast(produceScheduler); + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR for stochastic shortest path objectives is not implemented yet."); +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h b/src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h new file mode 100644 index 0000000000..a6aa84ad6c --- /dev/null +++ b/src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h @@ -0,0 +1,29 @@ +#pragma once + +#include "storm/environment/Environment.h" +#include "storm/modelchecker/cvar/CvarComputationResult.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h" +#include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" +#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h" +#include "storm/storage/BitVector.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +CvarComputationResult computeWeightedReachabilityCvar( + Environment const& env, SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const& targetStates, + bool produceScheduler = false) { + auto weightedReachabilityPreprocessingResult = + preprocessing::preprocessWeightedReachabilityCvar(model, queryInformation, targetStates, produceScheduler); + auto weightedReachabilityCvarLpData = createWeightedReachabilityCvarLpData(queryInformation, weightedReachabilityPreprocessingResult); + + SparseWeightedReachabilityCvarLpHelper cvarHelper(weightedReachabilityCvarLpData); + return cvarHelper.computeCvar(env, produceScheduler); +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 4066567db2..d27ba8e0f2 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -9,10 +9,8 @@ #include "storm/logic/FragmentSpecification.h" #include "storm/modelchecker/cvar/CvarClassification.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h" -#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h" -#include "storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h" -#include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" +#include "storm/modelchecker/cvar/SspCvarBackend.h" +#include "storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" #include "storm/modelchecker/helper/infinitehorizon/SparseNondeterministicInfiniteHorizonHelper.h" @@ -544,19 +542,15 @@ std::unique_ptr SparseMdpPrctlModelChecker::che auto targetStates = this->check(env, *cvarQueryInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); auto queryKind = storm::modelchecker::cvar::classifyCvarQuery(cvarQueryInformation); - auto problemKind = storm::modelchecker::cvar::classifyCvarProblem(this->getModel(), cvarQueryInformation, queryKind, targetStates, - env.modelchecker().cvar().getMethod()); + auto backendKind = + storm::modelchecker::cvar::selectCvarBackend(this->getModel(), cvarQueryInformation, queryKind, targetStates, env.modelchecker().cvar().getMethod()); std::unique_ptr result; - switch (problemKind) { - case storm::modelchecker::cvar::CvarProblemKind::WeightedReachability: { - auto weightedReachabilityPreprocessingResult = storm::modelchecker::cvar::preprocessing::preprocessWeightedReachabilityCvar( - this->getModel(), cvarQueryInformation, targetStates, checkTask.isProduceSchedulersSet()); - auto weightedReachabilityCvarLpData = - storm::modelchecker::cvar::createWeightedReachabilityCvarLpData(cvarQueryInformation, weightedReachabilityPreprocessingResult); - - storm::modelchecker::cvar::SparseWeightedReachabilityCvarLpHelper cvarHelper(weightedReachabilityCvarLpData); - auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); + switch (backendKind) { + case storm::modelchecker::cvar::CvarBackendKind::WeightedReachability: { + auto cvarResult = + storm::modelchecker::cvar::computeWeightedReachabilityCvar(env, this->getModel(), cvarQueryInformation, targetStates, + checkTask.isProduceSchedulersSet()); result = std::unique_ptr( new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { @@ -564,10 +558,9 @@ std::unique_ptr SparseMdpPrctlModelChecker::che } break; } - case storm::modelchecker::cvar::CvarProblemKind::Ssp: { - auto sspPreprocessingResult = storm::modelchecker::cvar::preprocessing::preprocessSspCvar(this->getModel(), cvarQueryInformation, targetStates); - static_cast(sspPreprocessingResult); - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR for stochastic shortest path objectives is not implemented yet."); + case storm::modelchecker::cvar::CvarBackendKind::Ssp: { + storm::modelchecker::cvar::computeSspCvar(env, this->getModel(), cvarQueryInformation, targetStates, checkTask.isProduceSchedulersSet()); + break; } } return result; From 90881705890095a018b2208f8aa62926e8a734ac Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:12:49 +0200 Subject: [PATCH 29/65] Enforce SSP paper assumptions (including no 0 reward steps) --- src/storm/modelchecker/cvar/SspCvarBackend.h | 28 ------ .../cvar/WeightedReachabilityCvarBackend.h | 29 ------ .../cvar/WeightedReachabilityCvarLpData.h | 93 ------------------- .../cvar/helper/SparseCvarComputationHelper.h | 59 ++++++++++++ .../SparseWeightedReachabilityCvarLpHelper.h | 83 ++++++++++++++++- .../SspCvarPreprocessingResult.h | 2 - .../cvar/preprocessing/SspCvarPreprocessor.h | 30 ++++-- .../prctl/SparseMdpPrctlModelChecker.cpp | 33 ++----- 8 files changed, 170 insertions(+), 187 deletions(-) delete mode 100644 src/storm/modelchecker/cvar/SspCvarBackend.h delete mode 100644 src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h delete mode 100644 src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h create mode 100644 src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h rename src/storm/modelchecker/cvar/{ => helper}/SparseWeightedReachabilityCvarLpHelper.h (74%) diff --git a/src/storm/modelchecker/cvar/SspCvarBackend.h b/src/storm/modelchecker/cvar/SspCvarBackend.h deleted file mode 100644 index 5df601bc39..0000000000 --- a/src/storm/modelchecker/cvar/SspCvarBackend.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "storm/environment/Environment.h" -#include "storm/exceptions/NotImplementedException.h" -#include "storm/modelchecker/cvar/CvarComputationResult.h" -#include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h" -#include "storm/storage/BitVector.h" -#include "storm/utility/macros.h" - -namespace storm { -namespace modelchecker { -namespace cvar { - -template -CvarComputationResult computeSspCvar(Environment const&, SparseMdpModelType const& model, - CvarQueryInformation const& queryInformation, - storm::storage::BitVector const& targetStates, - bool produceScheduler = false) { - auto sspPreprocessingResult = preprocessing::preprocessSspCvar(model, queryInformation, targetStates); - static_cast(sspPreprocessingResult); - static_cast(produceScheduler); - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR for stochastic shortest path objectives is not implemented yet."); -} - -} // namespace cvar -} // namespace modelchecker -} // namespace storm diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h b/src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h deleted file mode 100644 index a6aa84ad6c..0000000000 --- a/src/storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "storm/environment/Environment.h" -#include "storm/modelchecker/cvar/CvarComputationResult.h" -#include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h" -#include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" -#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h" -#include "storm/storage/BitVector.h" - -namespace storm { -namespace modelchecker { -namespace cvar { - -template -CvarComputationResult computeWeightedReachabilityCvar( - Environment const& env, SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const& targetStates, - bool produceScheduler = false) { - auto weightedReachabilityPreprocessingResult = - preprocessing::preprocessWeightedReachabilityCvar(model, queryInformation, targetStates, produceScheduler); - auto weightedReachabilityCvarLpData = createWeightedReachabilityCvarLpData(queryInformation, weightedReachabilityPreprocessingResult); - - SparseWeightedReachabilityCvarLpHelper cvarHelper(weightedReachabilityCvarLpData); - return cvarHelper.computeCvar(env, produceScheduler); -} - -} // namespace cvar -} // namespace modelchecker -} // namespace storm diff --git a/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h b/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h deleted file mode 100644 index 9cc3e8686c..0000000000 --- a/src/storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h" -#include "storm/storage/BitVector.h" -#include "storm/storage/SparseMatrix.h" -#include "storm/utility/constants.h" - -namespace storm { -namespace modelchecker { -namespace cvar { - -template -struct CvarThresholdData { - ValueType threshold; - storm::storage::BitVector targetStatesBelowThreshold; - storm::storage::BitVector targetStatesAtThreshold; - storm::storage::BitVector targetStatesBelowOrAtThreshold; -}; - -template -struct WeightedReachabilityCvarLpData { - double alpha; - storm::solver::OptimizationDirection optimizationDirection; - uint64_t initialState; - std::string rewardModelName; - storm::storage::BitVector targetStates; - std::vector terminalRewards; - std::vector candidateThresholds; - storm::storage::SparseMatrix transitionMatrix; -}; - -template -std::vector collectCandidateThresholds(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards) { - std::vector candidateThresholds; - candidateThresholds.reserve(targetStates.getNumberOfSetBits()); - for (uint64_t state = 0; state < terminalRewards.size(); ++state) { - if (targetStates[state]) { - candidateThresholds.push_back(terminalRewards[state]); - } - } - std::sort(candidateThresholds.begin(), candidateThresholds.end()); - candidateThresholds.erase(std::unique(candidateThresholds.begin(), candidateThresholds.end()), candidateThresholds.end()); - return candidateThresholds; -} - -template -CvarThresholdData createCvarThresholdData(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards, - ValueType const& threshold) { - storm::storage::BitVector targetStatesBelowThreshold(targetStates.size(), false); - storm::storage::BitVector targetStatesAtThreshold(targetStates.size(), false); - storm::storage::BitVector targetStatesBelowOrAtThreshold(targetStates.size(), false); - - for (uint64_t state = 0; state < terminalRewards.size(); ++state) { - if (!targetStates[state]) { - continue; - } - if (terminalRewards[state] < threshold) { - targetStatesBelowThreshold.set(state, true); - targetStatesBelowOrAtThreshold.set(state, true); - } else if (terminalRewards[state] == threshold) { - targetStatesAtThreshold.set(state, true); - targetStatesBelowOrAtThreshold.set(state, true); - } - } - - return {threshold, targetStatesBelowThreshold, targetStatesAtThreshold, targetStatesBelowOrAtThreshold}; -} - -template -WeightedReachabilityCvarLpData createWeightedReachabilityCvarLpData( - CvarQueryInformation const& queryInformation, - preprocessing::WeightedReachabilityCvarPreprocessingResult const& weightedReachabilityPreprocessingResult) { - auto candidateThresholds = - collectCandidateThresholds(weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards); - return {queryInformation.alpha, - queryInformation.optimizationDirection, - weightedReachabilityPreprocessingResult.initialState, - weightedReachabilityPreprocessingResult.rewardModelName, - weightedReachabilityPreprocessingResult.effectiveTargetStates, - weightedReachabilityPreprocessingResult.terminalRewards, - std::move(candidateThresholds), - weightedReachabilityPreprocessingResult.transitionMatrix}; -} - -} // namespace cvar -} // namespace modelchecker -} // namespace storm diff --git a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h new file mode 100644 index 0000000000..795c04cff2 --- /dev/null +++ b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h @@ -0,0 +1,59 @@ +#pragma once + +#include "storm/environment/Environment.h" +#include "storm/exceptions/NotImplementedException.h" +#include "storm/exceptions/UnexpectedException.h" +#include "storm/modelchecker/cvar/CvarClassification.h" +#include "storm/modelchecker/cvar/CvarComputationResult.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h" +#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h" +#include "storm/storage/BitVector.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +class SparseCvarComputationHelper { + public: + using ValueType = typename SparseMdpModelType::ValueType; + + SparseCvarComputationHelper(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, + storm::storage::BitVector const& targetStates) + : model(model), queryInformation(queryInformation), targetStates(targetStates) { + // Intentionally left empty. + } + + CvarComputationResult computeCvar(Environment const& env, bool produceScheduler = false) const { + auto queryKind = classifyCvarQuery(queryInformation); + auto backendKind = selectCvarBackend(model, queryInformation, queryKind, targetStates, env.modelchecker().cvar().getMethod()); + + switch (backendKind) { + case CvarBackendKind::WeightedReachability: { + auto weightedReachabilityPreprocessingResult = + preprocessing::preprocessWeightedReachabilityCvar(model, queryInformation, targetStates, produceScheduler); + SparseWeightedReachabilityCvarLpHelper cvarHelper(queryInformation, weightedReachabilityPreprocessingResult); + return cvarHelper.computeCvar(env, produceScheduler); + } + case CvarBackendKind::Ssp: { + auto sspPreprocessingResult = preprocessing::preprocessSspCvar(model, queryInformation, targetStates); + static_cast(sspPreprocessingResult); + static_cast(produceScheduler); + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, + "CVaR for stochastic shortest path objectives is not implemented yet."); + } + } + STORM_LOG_THROW(false, storm::exceptions::UnexpectedException, "Encountered an unknown CVaR backend."); + } + + private: + SparseMdpModelType const& model; + CvarQueryInformation const& queryInformation; + storm::storage::BitVector const& targetStates; +}; + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h similarity index 74% rename from src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h rename to src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h index 72c2572740..47f742957f 100644 --- a/src/storm/modelchecker/cvar/SparseWeightedReachabilityCvarLpHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h @@ -9,7 +9,8 @@ #include "storm/exceptions/NotImplementedException.h" #include "storm/exceptions/UnexpectedException.h" #include "storm/modelchecker/cvar/CvarComputationResult.h" -#include "storm/modelchecker/cvar/WeightedReachabilityCvarLpData.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h" #include "storm/solver/LpSolver.h" #include "storm/storage/Scheduler.h" #include "storm/storage/expressions/BinaryRelationType.h" @@ -21,6 +22,63 @@ namespace storm { namespace modelchecker { namespace cvar { +template +struct CvarThresholdData { + ValueType threshold; + storm::storage::BitVector targetStatesBelowThreshold; + storm::storage::BitVector targetStatesAtThreshold; + storm::storage::BitVector targetStatesBelowOrAtThreshold; +}; + +template +struct WeightedReachabilityCvarLpData { + double alpha; + storm::solver::OptimizationDirection optimizationDirection; + uint64_t initialState; + std::string rewardModelName; + storm::storage::BitVector targetStates; + std::vector terminalRewards; + std::vector candidateThresholds; + storm::storage::SparseMatrix transitionMatrix; +}; + +template +std::vector collectCandidateThresholds(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards) { + std::vector candidateThresholds; + candidateThresholds.reserve(targetStates.getNumberOfSetBits()); + for (uint64_t state = 0; state < terminalRewards.size(); ++state) { + if (targetStates[state]) { + candidateThresholds.push_back(terminalRewards[state]); + } + } + std::sort(candidateThresholds.begin(), candidateThresholds.end()); + candidateThresholds.erase(std::unique(candidateThresholds.begin(), candidateThresholds.end()), candidateThresholds.end()); + return candidateThresholds; +} + +template +CvarThresholdData createCvarThresholdData(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards, + ValueType const& threshold) { + storm::storage::BitVector targetStatesBelowThreshold(targetStates.size(), false); + storm::storage::BitVector targetStatesAtThreshold(targetStates.size(), false); + storm::storage::BitVector targetStatesBelowOrAtThreshold(targetStates.size(), false); + + for (uint64_t state = 0; state < terminalRewards.size(); ++state) { + if (!targetStates[state]) { + continue; + } + if (terminalRewards[state] < threshold) { + targetStatesBelowThreshold.set(state, true); + targetStatesBelowOrAtThreshold.set(state, true); + } else if (terminalRewards[state] == threshold) { + targetStatesAtThreshold.set(state, true); + targetStatesBelowOrAtThreshold.set(state, true); + } + } + + return {threshold, targetStatesBelowThreshold, targetStatesAtThreshold, targetStatesBelowOrAtThreshold}; +} + /*! * Solves an LP for Conditional Value-at-Risk on an MDP with a terminal reward objective. * @@ -40,9 +98,9 @@ namespace cvar { template class SparseWeightedReachabilityCvarLpHelper { public: - explicit SparseWeightedReachabilityCvarLpHelper(WeightedReachabilityCvarLpData const& lpData) : lpData(lpData) { - // Intentionally left empty. - } + SparseWeightedReachabilityCvarLpHelper(CvarQueryInformation const& queryInformation, + preprocessing::WeightedReachabilityCvarPreprocessingResult const& weightedReachabilityPreprocessingResult) + : lpData(createLpData(queryInformation, weightedReachabilityPreprocessingResult)) {} CvarComputationResult computeCvar(Environment const&, bool produceScheduler = false) const { STORM_LOG_THROW(!lpData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, @@ -76,6 +134,21 @@ class SparseWeightedReachabilityCvarLpHelper { } private: + static WeightedReachabilityCvarLpData createLpData( + CvarQueryInformation const& queryInformation, + preprocessing::WeightedReachabilityCvarPreprocessingResult const& weightedReachabilityPreprocessingResult) { + auto candidateThresholds = + collectCandidateThresholds(weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards); + return {queryInformation.alpha, + queryInformation.optimizationDirection, + weightedReachabilityPreprocessingResult.initialState, + weightedReachabilityPreprocessingResult.rewardModelName, + weightedReachabilityPreprocessingResult.effectiveTargetStates, + weightedReachabilityPreprocessingResult.terminalRewards, + std::move(candidateThresholds), + weightedReachabilityPreprocessingResult.transitionMatrix}; + } + std::optional> buildLpForThreshold(CvarThresholdData const& thresholdData, bool produceScheduler) const { using RawLpSolver = storm::solver::LpSolver; using RawLpConstraint = storm::solver::RawLpConstraint; @@ -207,7 +280,7 @@ class SparseWeightedReachabilityCvarLpHelper { return CvarComputationResult{solver->getObjectiveValue(), std::move(scheduler)}; } - WeightedReachabilityCvarLpData const& lpData; + WeightedReachabilityCvarLpData lpData; }; } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h index 03df1d4f01..69146c56f5 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h @@ -17,8 +17,6 @@ struct SspCvarPreprocessingResult { uint64_t initialState; storm::storage::BitVector targetStates; storm::storage::BitVector reachableStates; - storm::storage::BitVector statesThatCanReachTarget; - storm::storage::BitVector badMecStates; bool liftedStateRewardsToChoiceCosts; bool normalizedTargetStatesToAbsorbing; std::vector choiceCosts; diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index 164254344c..62ab71b6c6 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -3,13 +3,14 @@ #include #include +#include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" -#include "storm/modelchecker/cvar/CvarPreprocessingUtilities.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" #include "storm/utility/constants.h" +#include "storm/utility/graph.h" #include "storm/utility/logging.h" #include "storm/utility/macros.h" @@ -51,6 +52,21 @@ std::vector extractChoiceCostsForSsp( return choiceCosts; } +template +void validatePositiveChoiceCostsOutsideGoals(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& targetStates, + std::vector const& choiceCosts) { + ValueType const zero = storm::utility::zero(); + for (uint64_t state = 0; state < transitionMatrix.getRowGroupCount(); ++state) { + if (targetStates[state]) { + continue; + } + for (uint64_t row = transitionMatrix.getRowGroupIndices()[state], endRow = transitionMatrix.getRowGroupIndices()[state + 1]; row < endRow; ++row) { + STORM_LOG_THROW(choiceCosts[row] > zero, storm::exceptions::InvalidPropertyException, + "CVaR SSP preprocessing currently requires strictly positive choice costs outside goal states."); + } + } +} + template SspCvarPreprocessingResult preprocessSspCvar(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, @@ -94,20 +110,22 @@ SspCvarPreprocessingResult preprocessSsp transitionMatrix.makeRowGroupsAbsorbing(targetStates, true); } + auto backwardTransitions = transitionMatrix.transpose(true); auto reachableStates = storm::utility::graph::getReachableStates( transitionMatrix, model.getInitialStates(), storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), storm::storage::BitVector(transitionMatrix.getRowGroupCount(), false)); - auto statesThatCanReachTarget = computeStatesThatCanReachTarget(transitionMatrix, targetStates); - auto badMecStates = - computeBadMecStates(computeReachableMecs(transitionMatrix, model.getInitialStates()), statesThatCanReachTarget, transitionMatrix.getRowGroupCount()); + auto properStates = + storm::utility::graph::performProb1E(transitionMatrix, transitionMatrix.getRowGroupIndices(), backwardTransitions, + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), targetStates); + STORM_LOG_THROW(reachableStates.isSubsetOf(properStates), storm::exceptions::InvalidPropertyException, + "CVaR SSP preprocessing currently requires a proper policy from every reachable state."); auto choiceCosts = extractChoiceCostsForSsp(model, rewardModel, targetStates); + validatePositiveChoiceCostsOutsideGoals(transitionMatrix, targetStates, choiceCosts); return {rewardModelName, *model.getInitialStates().begin(), targetStates, std::move(reachableStates), - std::move(statesThatCanReachTarget), - std::move(badMecStates), liftedStateRewardsToChoiceCosts, normalizedTargetStatesToAbsorbing, std::move(choiceCosts), diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index d27ba8e0f2..8beac3c8f7 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -7,10 +7,8 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/logic/FragmentSpecification.h" -#include "storm/modelchecker/cvar/CvarClassification.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/SspCvarBackend.h" -#include "storm/modelchecker/cvar/WeightedReachabilityCvarBackend.h" +#include "storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" #include "storm/modelchecker/helper/infinitehorizon/SparseNondeterministicInfiniteHorizonHelper.h" @@ -541,27 +539,14 @@ std::unique_ptr SparseMdpPrctlModelChecker::che auto cvarQueryInformation = storm::modelchecker::cvar::extractCvarQueryInformation(checkTask.getFormula()); auto targetStates = this->check(env, *cvarQueryInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); - auto queryKind = storm::modelchecker::cvar::classifyCvarQuery(cvarQueryInformation); - auto backendKind = - storm::modelchecker::cvar::selectCvarBackend(this->getModel(), cvarQueryInformation, queryKind, targetStates, env.modelchecker().cvar().getMethod()); - - std::unique_ptr result; - switch (backendKind) { - case storm::modelchecker::cvar::CvarBackendKind::WeightedReachability: { - auto cvarResult = - storm::modelchecker::cvar::computeWeightedReachabilityCvar(env, this->getModel(), cvarQueryInformation, targetStates, - checkTask.isProduceSchedulersSet()); - result = std::unique_ptr( - new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); - if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { - result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); - } - break; - } - case storm::modelchecker::cvar::CvarBackendKind::Ssp: { - storm::modelchecker::cvar::computeSspCvar(env, this->getModel(), cvarQueryInformation, targetStates, checkTask.isProduceSchedulersSet()); - break; - } + + storm::modelchecker::cvar::SparseCvarComputationHelper cvarHelper(this->getModel(), cvarQueryInformation, targetStates); + auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); + + std::unique_ptr result( + new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); + if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { + result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); } return result; } From ad82326ebebce9af8cfc37fe1cdaf868b44be7d1 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:15:13 +0200 Subject: [PATCH 30/65] Add SSP expected cost-to-go preprocessing --- .../cvar/helper/SparseCvarComputationHelper.h | 2 +- .../preprocessing/SspCvarPreprocessingResult.h | 1 + .../cvar/preprocessing/SspCvarPreprocessor.h | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h index 795c04cff2..9746fdfab6 100644 --- a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h @@ -38,7 +38,7 @@ class SparseCvarComputationHelper { return cvarHelper.computeCvar(env, produceScheduler); } case CvarBackendKind::Ssp: { - auto sspPreprocessingResult = preprocessing::preprocessSspCvar(model, queryInformation, targetStates); + auto sspPreprocessingResult = preprocessing::preprocessSspCvar(env, model, queryInformation, targetStates); static_cast(sspPreprocessingResult); static_cast(produceScheduler); STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h index 69146c56f5..2ebf414e08 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h @@ -20,6 +20,7 @@ struct SspCvarPreprocessingResult { bool liftedStateRewardsToChoiceCosts; bool normalizedTargetStatesToAbsorbing; std::vector choiceCosts; + std::vector expectedCostsToGoal; storm::storage::SparseMatrix transitionMatrix; }; diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index 62ab71b6c6..b260263e54 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -6,6 +6,9 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h" +#include "storm/models/sparse/StandardRewardModel.h" +#include "storm/solver/SolveGoal.h" #include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" @@ -67,8 +70,19 @@ void validatePositiveChoiceCostsOutsideGoals(storm::storage::SparseMatrix +std::vector computeExpectedCostsToGoal(Environment const& env, storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::SparseMatrix const& backwardTransitions, + storm::storage::BitVector const& targetStates, std::vector const& choiceCosts) { + storm::models::sparse::StandardRewardModel rewardModel(std::nullopt, std::vector(choiceCosts), std::nullopt); + auto result = storm::modelchecker::helper::SparseMdpPrctlHelper::computeReachabilityRewards( + env, storm::solver::SolveGoal(storm::OptimizationDirection::Minimize), transitionMatrix, backwardTransitions, rewardModel, + targetStates, false, false); + return std::move(result.values); +} + template -SspCvarPreprocessingResult preprocessSspCvar(SparseMdpModelType const& model, +SspCvarPreprocessingResult preprocessSspCvar(Environment const& env, SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const& targetStates) { using ValueType = typename SparseMdpModelType::ValueType; @@ -121,6 +135,7 @@ SspCvarPreprocessingResult preprocessSsp "CVaR SSP preprocessing currently requires a proper policy from every reachable state."); auto choiceCosts = extractChoiceCostsForSsp(model, rewardModel, targetStates); validatePositiveChoiceCostsOutsideGoals(transitionMatrix, targetStates, choiceCosts); + auto expectedCostsToGoal = computeExpectedCostsToGoal(env, transitionMatrix, backwardTransitions, targetStates, choiceCosts); return {rewardModelName, *model.getInitialStates().begin(), @@ -129,6 +144,7 @@ SspCvarPreprocessingResult preprocessSsp liftedStateRewardsToChoiceCosts, normalizedTargetStatesToAbsorbing, std::move(choiceCosts), + std::move(expectedCostsToGoal), std::move(transitionMatrix)}; } From 77ecc32a9ac5247154e9eae0d0ccdb9d03d5cbd3 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:10:54 +0200 Subject: [PATCH 31/65] Add SSP Pareto front operations scaffolding --- .../cvar/helper/SparseCvarComputationHelper.h | 7 +- .../cvar/helper/SparseSspCvarParetoViHelper.h | 38 +++ .../modelchecker/cvar/helper/SspParetoFront.h | 221 ++++++++++++++++++ 3 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h create mode 100644 src/storm/modelchecker/cvar/helper/SspParetoFront.h diff --git a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h index 9746fdfab6..1503549633 100644 --- a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h @@ -6,6 +6,7 @@ #include "storm/modelchecker/cvar/CvarClassification.h" #include "storm/modelchecker/cvar/CvarComputationResult.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h" #include "storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h" #include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h" #include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h" @@ -39,10 +40,8 @@ class SparseCvarComputationHelper { } case CvarBackendKind::Ssp: { auto sspPreprocessingResult = preprocessing::preprocessSspCvar(env, model, queryInformation, targetStates); - static_cast(sspPreprocessingResult); - static_cast(produceScheduler); - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "CVaR for stochastic shortest path objectives is not implemented yet."); + SparseSspCvarParetoViHelper cvarHelper(queryInformation, sspPreprocessingResult); + return cvarHelper.computeCvar(env, produceScheduler); } } STORM_LOG_THROW(false, storm::exceptions::UnexpectedException, "Encountered an unknown CVaR backend."); diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h new file mode 100644 index 0000000000..8ae92c0d2c --- /dev/null +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -0,0 +1,38 @@ +#pragma once + +#include "storm/environment/Environment.h" +#include "storm/exceptions/NotImplementedException.h" +#include "storm/modelchecker/cvar/CvarComputationResult.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/helper/SspParetoFront.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +class SparseSspCvarParetoViHelper { + public: + SparseSspCvarParetoViHelper(CvarQueryInformation const& queryInformation, + preprocessing::SspCvarPreprocessingResult const& preprocessingResult) + : queryInformation(queryInformation), preprocessingResult(preprocessingResult) { + // Intentionally left empty. + } + + CvarComputationResult computeCvar(Environment const&, bool produceScheduler = false) const { + static_cast(produceScheduler); + static_assert(sizeof(SspParetoFront) > 0, "Expected SSP Pareto front type to be available."); + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, + "CVaR for stochastic shortest path objectives is not implemented yet."); + } + + private: + CvarQueryInformation const& queryInformation; + preprocessing::SspCvarPreprocessingResult const& preprocessingResult; +}; + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h new file mode 100644 index 0000000000..3a4ebca65e --- /dev/null +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -0,0 +1,221 @@ +#pragma once + +#include +#include +#include +#include + +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +class SspParetoFront { + public: + struct Point { + ValueType probability; + ValueType expectedCost; + + enum class DominanceResult { Incomparable, Dominates, Dominated, Equal }; + + DominanceResult getDominance(Point const& other) const { + if (probability == other.probability && expectedCost == other.expectedCost) { + return DominanceResult::Equal; + } + if (probability >= other.probability && expectedCost <= other.expectedCost) { + return DominanceResult::Dominates; + } + if (probability <= other.probability && expectedCost >= other.expectedCost) { + return DominanceResult::Dominated; + } + return DominanceResult::Incomparable; + } + }; + + using container_type = std::vector; + using const_iterator = typename container_type::const_iterator; + + SspParetoFront() = default; + + explicit SspParetoFront(container_type points) : points(std::move(points)) { + canonicalize(); + } + + static SspParetoFront singleton(ValueType const& probability, ValueType const& expectedCost) { + return SspParetoFront(container_type{{probability, expectedCost}}); + } + + bool empty() const { + return points.empty(); + } + + std::size_t size() const { + return points.size(); + } + + container_type const& getPoints() const { + return points; + } + + const_iterator begin() const { + return points.begin(); + } + + const_iterator end() const { + return points.end(); + } + + void clear() { + points.clear(); + } + + void addPoint(Point const& point) { + addPoint(Point{point}); + } + + void addPoint(Point&& point) { + auto it = points.begin(); + while (it != points.end()) { + switch (point.getDominance(*it)) { + case Point::DominanceResult::Equal: + case Point::DominanceResult::Dominated: + return; + case Point::DominanceResult::Dominates: + it = points.erase(it); + break; + case Point::DominanceResult::Incomparable: + ++it; + break; + } + } + points.push_back(std::move(point)); + canonicalize(); + } + + void addPoints(container_type const& additionalPoints) { + points.insert(points.end(), additionalPoints.begin(), additionalPoints.end()); + canonicalize(); + } + + SspParetoFront scaled(ValueType const& factor) const { + if (empty()) { + return SspParetoFront(); + } + container_type scaledPoints; + scaledPoints.reserve(points.size()); + for (auto const& point : points) { + scaledPoints.push_back(Point{factor * point.probability, factor * point.expectedCost}); + } + return SspParetoFront(std::move(scaledPoints)); + } + + SspParetoFront minkowskiSum(SspParetoFront const& other) const { + if (empty() || other.empty()) { + return SspParetoFront(); + } + container_type sumPoints; + sumPoints.reserve(points.size() * other.points.size()); + for (auto const& left : points) { + for (auto const& right : other.points) { + sumPoints.push_back(Point{left.probability + right.probability, left.expectedCost + right.expectedCost}); + } + } + return SspParetoFront(std::move(sumPoints)); + } + + static SspParetoFront convexUnion(std::vector const& fronts) { + container_type unionPoints; + std::size_t totalPointCount = 0; + for (auto const& front : fronts) { + totalPointCount += front.size(); + } + unionPoints.reserve(totalPointCount); + for (auto const& front : fronts) { + unionPoints.insert(unionPoints.end(), front.begin(), front.end()); + } + return SspParetoFront(std::move(unionPoints)); + } + + std::string toString() const { + std::stringstream stream; + stream << "{"; + bool first = true; + for (auto const& point : points) { + if (!first) { + stream << ", "; + } + first = false; + stream << "(" << point.probability << ", " << point.expectedCost << ")"; + } + stream << "}"; + return stream.str(); + } + + private: + void canonicalize() { + if (points.empty()) { + return; + } + sortPoints(); + removeDuplicateProbabilityPoints(); + removeNonExtremeConvexPoints(); + } + + void sortPoints() { + std::sort(points.begin(), points.end(), [](Point const& left, Point const& right) { + if (left.probability == right.probability) { + return left.expectedCost < right.expectedCost; + } + return left.probability < right.probability; + }); + } + + void removeDuplicateProbabilityPoints() { + container_type uniquePoints; + uniquePoints.reserve(points.size()); + for (auto const& point : points) { + if (!uniquePoints.empty() && uniquePoints.back().probability == point.probability) { + continue; + } + uniquePoints.push_back(point); + } + points = std::move(uniquePoints); + } + + void removeNonExtremeConvexPoints() { + if (points.size() < 3) { + return; + } + container_type hullPoints; + hullPoints.reserve(points.size()); + for (auto const& point : points) { + hullPoints.push_back(point); + while (hullPoints.size() >= 3 && liesOnOrAboveSegment(hullPoints[hullPoints.size() - 3], hullPoints[hullPoints.size() - 2], + hullPoints[hullPoints.size() - 1])) { + hullPoints.erase(hullPoints.end() - 2); + } + } + points = std::move(hullPoints); + STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), [](Point const& left, Point const& right) { + return left.probability >= right.probability || left.expectedCost <= right.expectedCost; + }) == points.end(), + "Expected SSP Pareto front points to be strictly ordered by increasing probability and decreasing expected cost."); + } + + static bool liesOnOrAboveSegment(Point const& left, Point const& middle, Point const& right) { + ValueType const leftToRightProbabilityDelta = right.probability - left.probability; + ValueType const leftToMiddleProbabilityDelta = middle.probability - left.probability; + STORM_LOG_ASSERT(leftToRightProbabilityDelta > 0 && leftToMiddleProbabilityDelta > 0, + "Expected SSP Pareto front probabilities to be strictly increasing."); + return (middle.expectedCost - left.expectedCost) * leftToRightProbabilityDelta >= + (right.expectedCost - left.expectedCost) * leftToMiddleProbabilityDelta; + } + + container_type points; +}; + +} // namespace cvar +} // namespace modelchecker +} // namespace storm From b13c16f837154155071114d68f43919c9e491ea4 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 4 May 2026 14:49:13 +0200 Subject: [PATCH 32/65] Add SSP Pareto front iteration logic --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 102 ++++++++++++++++++ .../modelchecker/cvar/helper/SspParetoFront.h | 44 ++++++++ .../SspCvarPreprocessingResult.h | 1 + .../cvar/preprocessing/SspCvarPreprocessor.h | 20 ++++ 4 files changed, 167 insertions(+) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 8ae92c0d2c..58c96e48d4 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -1,20 +1,41 @@ #pragma once +#include +#include +#include + #include "storm/environment/Environment.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarComputationResult.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/helper/SspParetoFront.h" #include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" +#include "storm/utility/constants.h" #include "storm/utility/macros.h" namespace storm { namespace modelchecker { namespace cvar { +/*! + * Implements the Pareto-front value iteration for Conditional Value-at-Risk on an SSP-style total-cost objective. + * + * Supported CLI shape: + * storm --prism model.nm --prop 'R{"reward"}min/max=? [ F "target" ]' --cvar --cvar:method ssp + * + * The helper assumes that SSP-specific preprocessing has already normalized the sparse MDP to terminal-goal + * semantics, validated integer-valued positive choice costs outside goal states, and computed the classical + * expected cost-to-go vector used in the base layer. + * + * @see https://doi.org/10.1609/aaai.v36i9.21222 for the Pareto-front value iteration on which this implementation is based. + */ template class SparseSspCvarParetoViHelper { public: + using ParetoFront = SspParetoFront; + using FrontierLayer = std::vector; + using FrontierWindow = std::vector; + SparseSspCvarParetoViHelper(CvarQueryInformation const& queryInformation, preprocessing::SspCvarPreprocessingResult const& preprocessingResult) : queryInformation(queryInformation), preprocessingResult(preprocessingResult) { @@ -29,6 +50,87 @@ class SparseSspCvarParetoViHelper { } private: + FrontierLayer createBaseFrontierLayer() const { + FrontierLayer baseLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); + for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { + if (preprocessingResult.targetStates[state]) { + baseLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + } else if (preprocessingResult.reachableStates[state]) { + baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state]); + } + } + return baseLayer; + } + + FrontierWindow initializeFrontierWindow() const { + uint64_t const windowSize = preprocessingResult.maximalChoiceCost + 1; + FrontierWindow frontierWindow(windowSize, FrontierLayer(preprocessingResult.transitionMatrix.getRowGroupCount())); + frontierWindow[0] = createBaseFrontierLayer(); + return frontierWindow; + } + + ParetoFront computeActionFront(uint64_t actionRow, uint64_t costBound, FrontierWindow const& frontierWindow) const { + uint64_t const actionCost = getChoiceCostBoundOffset(actionRow); + if (costBound < actionCost) { + return ParetoFront(); + } + + ParetoFront actionFront = ParetoFront::singleton(storm::utility::zero(), storm::utility::zero()); + uint64_t const predecessorBound = costBound - actionCost; + FrontierLayer const& predecessorLayer = frontierWindow[predecessorBound % frontierWindow.size()]; + + for (auto const& transition : preprocessingResult.transitionMatrix.getRow(actionRow)) { + actionFront = actionFront.minkowskiSum(predecessorLayer[transition.getColumn()].scaled(transition.getValue())); + } + return actionFront; + } + + FrontierLayer computeFrontierLayerForCostBound(uint64_t costBound, FrontierWindow const& frontierWindow) const { + FrontierLayer currentLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); + for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { + if (!preprocessingResult.reachableStates[state]) { + continue; + } + if (preprocessingResult.targetStates[state]) { + currentLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + continue; + } + + std::vector actionFronts; + for (uint64_t actionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state], + endRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state + 1]; + actionRow < endRow; ++actionRow) { + auto actionFront = computeActionFront(actionRow, costBound, frontierWindow); + if (!actionFront.empty()) { + actionFronts.push_back(std::move(actionFront)); + } + } + currentLayer[state] = ParetoFront::convexUnion(actionFronts); + } + return currentLayer; + } + + /*! + * Evaluates one initial-state frontier for a fixed cost bound n. + * + * Following Algorithm 1 and the total-cost extension in the paper, a frontier for bound n induces the CVaR + * candidate n + E / t, where E is the minimal continuation cost on the frontier at probability 1 - t. + */ + static std::optional extractCvarCandidateFromInitialFrontier(SspParetoFront const& initialFrontier, uint64_t costBound, + double alpha) { + ValueType const targetProbability = storm::utility::one() - storm::utility::convertNumber(alpha); + auto continuationCost = initialFrontier.getMinimalContinuationCostAtProbability(targetProbability); + if (!continuationCost.has_value()) { + return std::nullopt; + } + return storm::utility::convertNumber(costBound) + + continuationCost.value() / storm::utility::convertNumber(alpha); + } + + uint64_t getChoiceCostBoundOffset(uint64_t actionRow) const { + return storm::utility::convertNumber(preprocessingResult.choiceCosts[actionRow]); + } + CvarQueryInformation const& queryInformation; preprocessing::SspCvarPreprocessingResult const& preprocessingResult; }; diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index 3a4ebca65e..4010946f61 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -11,11 +12,23 @@ namespace storm { namespace modelchecker { namespace cvar { +/*! + * Represents the lower-right boundary of one SSP CVaR Pareto set from the paper. + * + * For a fixed state and cost bound n, each point (p, E) represents an achievable tradeoff where: + * - p is the probability of reaching the goal within the current cost bound, and + * - E is the expected continuation cost beyond that bound. + * + * The class only stores extremal boundary points. The upward/leftward closed polygon itself is induced by + * these points together with convex closure. + */ template class SspParetoFront { public: struct Point { + //! Probability of reaching the goal within the current cost bound. ValueType probability; + //! Expected continuation cost beyond the current cost bound. ValueType expectedCost; enum class DominanceResult { Incomparable, Dominates, Dominated, Equal }; @@ -138,6 +151,37 @@ class SspParetoFront { return SspParetoFront(std::move(unionPoints)); } + /*! + * Returns the minimal continuation cost E on the lower boundary at the given probability p. + * + * This is the paper-specific query used to evaluate a frontier for a fixed bound n: + * it yields the value min{E | (p, E) is contained in the convex Pareto set}. + */ + std::optional getMinimalContinuationCostAtProbability(ValueType const& probability) const { + if (empty()) { + return std::nullopt; + } + if (probability > points.back().probability) { + return std::nullopt; + } + if (probability <= points.front().probability) { + return points.front().expectedCost; + } + + for (std::size_t index = 1; index < points.size(); ++index) { + Point const& left = points[index - 1]; + Point const& right = points[index]; + if (probability <= right.probability) { + ValueType const probabilityDelta = right.probability - left.probability; + STORM_LOG_ASSERT(probabilityDelta > 0, "Expected SSP Pareto front points to be strictly sorted by probability."); + ValueType const interpolationFactor = (probability - left.probability) / probabilityDelta; + return left.expectedCost + interpolationFactor * (right.expectedCost - left.expectedCost); + } + } + + return points.back().expectedCost; + } + std::string toString() const { std::stringstream stream; stream << "{"; diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h index 2ebf414e08..e586798dc3 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h @@ -19,6 +19,7 @@ struct SspCvarPreprocessingResult { storm::storage::BitVector reachableStates; bool liftedStateRewardsToChoiceCosts; bool normalizedTargetStatesToAbsorbing; + uint64_t maximalChoiceCost; std::vector choiceCosts; std::vector expectedCostsToGoal; storm::storage::SparseMatrix transitionMatrix; diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index b260263e54..efef8b4f04 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -70,6 +71,23 @@ void validatePositiveChoiceCostsOutsideGoals(storm::storage::SparseMatrix +uint64_t validateAndComputeMaximalChoiceCostOutsideGoals(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::BitVector const& targetStates, std::vector const& choiceCosts) { + uint64_t maximalChoiceCost = 0; + for (uint64_t state = 0; state < transitionMatrix.getRowGroupCount(); ++state) { + if (targetStates[state]) { + continue; + } + for (uint64_t row = transitionMatrix.getRowGroupIndices()[state], endRow = transitionMatrix.getRowGroupIndices()[state + 1]; row < endRow; ++row) { + STORM_LOG_THROW(storm::utility::isInteger(choiceCosts[row]), storm::exceptions::InvalidPropertyException, + "CVaR SSP preprocessing currently requires integer-valued choice costs."); + maximalChoiceCost = std::max(maximalChoiceCost, storm::utility::convertNumber(choiceCosts[row])); + } + } + return maximalChoiceCost; +} + template std::vector computeExpectedCostsToGoal(Environment const& env, storm::storage::SparseMatrix const& transitionMatrix, storm::storage::SparseMatrix const& backwardTransitions, @@ -135,6 +153,7 @@ SspCvarPreprocessingResult preprocessSsp "CVaR SSP preprocessing currently requires a proper policy from every reachable state."); auto choiceCosts = extractChoiceCostsForSsp(model, rewardModel, targetStates); validatePositiveChoiceCostsOutsideGoals(transitionMatrix, targetStates, choiceCosts); + uint64_t maximalChoiceCost = validateAndComputeMaximalChoiceCostOutsideGoals(transitionMatrix, targetStates, choiceCosts); auto expectedCostsToGoal = computeExpectedCostsToGoal(env, transitionMatrix, backwardTransitions, targetStates, choiceCosts); return {rewardModelName, @@ -143,6 +162,7 @@ SspCvarPreprocessingResult preprocessSsp std::move(reachableStates), liftedStateRewardsToChoiceCosts, normalizedTargetStatesToAbsorbing, + maximalChoiceCost, std::move(choiceCosts), std::move(expectedCostsToGoal), std::move(transitionMatrix)}; From 21fca7d6aee156fc736d6498db5fcba84e6d7811 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 4 May 2026 15:48:45 +0200 Subject: [PATCH 33/65] Implement SSP Pareto VI full loop --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 35 +++++++++++++++---- .../cvar/preprocessing/SspCvarPreprocessor.h | 4 +++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 58c96e48d4..7b6fa86a11 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -2,10 +2,12 @@ #include #include +#include #include #include "storm/environment/Environment.h" #include "storm/exceptions/NotImplementedException.h" +#include "storm/exceptions/UnexpectedException.h" #include "storm/modelchecker/cvar/CvarComputationResult.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/helper/SspParetoFront.h" @@ -43,10 +45,26 @@ class SparseSspCvarParetoViHelper { } CvarComputationResult computeCvar(Environment const&, bool produceScheduler = false) const { - static_cast(produceScheduler); - static_assert(sizeof(SspParetoFront) > 0, "Expected SSP Pareto front type to be available."); - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, - "CVaR for stochastic shortest path objectives is not implemented yet."); + STORM_LOG_THROW(!produceScheduler, storm::exceptions::NotImplementedException, + "Scheduler extraction for CVaR SSP value iteration is not implemented yet."); + + FrontierWindow frontierWindow = initializeFrontierWindow(); + std::optional bestCandidate = + extractCvarCandidateFromInitialFrontier(frontierWindow[0][preprocessingResult.initialState], 0, queryInformation.alpha); + + for (uint64_t costBound = 1; + !bestCandidate.has_value() || storm::utility::convertNumber(costBound) <= bestCandidate.value(); ++costBound) { + auto currentLayer = computeFrontierLayerForCostBound(costBound, frontierWindow); + auto currentCandidate = extractCvarCandidateFromInitialFrontier(currentLayer[preprocessingResult.initialState], costBound, queryInformation.alpha); + if (currentCandidate.has_value() && (!bestCandidate.has_value() || currentCandidate.value() < bestCandidate.value())) { + bestCandidate = currentCandidate; + } + writeFrontierLayerToWindow(costBound, std::move(currentLayer), frontierWindow); + } + + STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, + "CVaR SSP value iteration did not find a feasible candidate."); + return {bestCandidate.value(), nullptr}; } private: @@ -63,12 +81,17 @@ class SparseSspCvarParetoViHelper { } FrontierWindow initializeFrontierWindow() const { - uint64_t const windowSize = preprocessingResult.maximalChoiceCost + 1; - FrontierWindow frontierWindow(windowSize, FrontierLayer(preprocessingResult.transitionMatrix.getRowGroupCount())); + STORM_LOG_ASSERT(preprocessingResult.maximalChoiceCost > 0, + "Expected a strictly positive maximal choice cost."); + FrontierWindow frontierWindow(preprocessingResult.maximalChoiceCost, FrontierLayer(preprocessingResult.transitionMatrix.getRowGroupCount())); frontierWindow[0] = createBaseFrontierLayer(); return frontierWindow; } + static void writeFrontierLayerToWindow(uint64_t costBound, FrontierLayer layer, FrontierWindow& frontierWindow) { + frontierWindow[costBound % frontierWindow.size()] = std::move(layer); + } + ParetoFront computeActionFront(uint64_t actionRow, uint64_t costBound, FrontierWindow const& frontierWindow) const { uint64_t const actionCost = getChoiceCostBoundOffset(actionRow); if (costBound < actionCost) { diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index efef8b4f04..898489c83b 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -111,6 +111,10 @@ SspCvarPreprocessingResult preprocessSsp rewardModelName = model.getUniqueRewardModelName(); } + STORM_LOG_THROW(queryInformation.optimizationDirection == storm::solver::OptimizationDirection::Minimize, + storm::exceptions::InvalidPropertyException, + "CVaR SSP preprocessing currently only supports minimizing total costs."); + STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, "CVaR SSP preprocessing does not support transition rewards."); From 18af168a7a3fb883e928bf2ee82ac01cb0b5d674 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 4 May 2026 17:14:21 +0200 Subject: [PATCH 34/65] Add deterministic SSP CVaR smoke test --- .../mdp/cvar_ssp_deterministic_mdp.nm | 16 +++++++++++++++ .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 resources/examples/testfiles/mdp/cvar_ssp_deterministic_mdp.nm diff --git a/resources/examples/testfiles/mdp/cvar_ssp_deterministic_mdp.nm b/resources/examples/testfiles/mdp/cvar_ssp_deterministic_mdp.nm new file mode 100644 index 0000000000..4fbbc8dcfe --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_ssp_deterministic_mdp.nm @@ -0,0 +1,16 @@ +mdp + +module main + s : [0..2] init 0; + + [step0] s=0 -> 1 : (s'=1); + [step1] s=1 -> 1 : (s'=2); + [] s=2 -> 1 : (s'=2); +endmodule + +label "goal" = s=2; + +rewards "cost" + [step0] true : 2; + [step1] true : 3; +endrewards diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 4e5596cd37..68ab515343 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -7,8 +7,10 @@ #include "storm/api/builder.h" #include "storm/api/properties.h" #include "storm/environment/Environment.h" +#include "storm/environment/modelchecker/ModelCheckerEnvironment.h" #include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/InvalidPropertyException.h" +#include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/modelchecker/CheckTask.h" #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" @@ -71,6 +73,16 @@ ValueType checkInitialStateValue(CvarTestInput const& input) { return result->template asExplicitQuantitativeCheckResult().getMax(); } +template +ValueType checkInitialStateValueWithMethod(CvarTestInput const& input, storm::modelchecker::cvar::CvarMethod method) { + storm::Environment env; + env.modelchecker().cvar().setMethod(method); + storm::modelchecker::SparseMdpPrctlModelChecker> checker(*input.mdp); + storm::modelchecker::CheckTask task(*input.formula, true); + auto result = checker.check(env, task); + return result->template asExplicitQuantitativeCheckResult().getMax(); +} + template std::vector getChoiceSuccessors(std::shared_ptr> const& mdp, uint64_t state, uint64_t localChoice) { std::vector result; @@ -247,4 +259,12 @@ TEST(CvarQueryTest, ProducesRandomizedSchedulerForMinBranchingTradeoffMdp) { EXPECT_TRUE(scheduler.isDontCare(adaptiveSuccessors[0])); EXPECT_TRUE(scheduler.isDontCare(adaptiveSuccessors[1])); } + +TEST(CvarQueryTest, DeterministicSspPathMdp) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", 0.5); + + double value = checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi); + EXPECT_NEAR(value, 5.0, 1e-10); +} } // namespace From 8f1291a524bf1f04f4ac4df7f035cecdbf595e5a Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 6 May 2026 13:48:57 +0200 Subject: [PATCH 35/65] Implement and validate SSP CVaR Pareto VI semantics --- .../mdp/cvar_ssp_branching_tradeoff_mdp.nm | 21 +++++++ .../cvar/helper/SparseSspCvarParetoViHelper.h | 55 ++++++++++++++----- .../modelchecker/cvar/helper/SspParetoFront.h | 24 +++++++- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 10 ++++ 4 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 resources/examples/testfiles/mdp/cvar_ssp_branching_tradeoff_mdp.nm diff --git a/resources/examples/testfiles/mdp/cvar_ssp_branching_tradeoff_mdp.nm b/resources/examples/testfiles/mdp/cvar_ssp_branching_tradeoff_mdp.nm new file mode 100644 index 0000000000..4471601b7d --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_ssp_branching_tradeoff_mdp.nm @@ -0,0 +1,21 @@ + +mdp + +module main + s : [0..3] init 0; + + [safe] s=0 -> 1 : (s'=3); + [risky] s=0 -> 1/2 : (s'=1) + 1/2 : (s'=2); + [low] s=1 -> 1 : (s'=3); + [high] s=2 -> 1 : (s'=3); + [] s=3 -> 1 : (s'=3); +endmodule + +label "goal" = s=3; + +rewards "cost" + [safe] true : 6; + [risky] true : 1; + [low] true : 1; + [high] true : 7; +endrewards diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 7b6fa86a11..45253344a3 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -68,13 +68,26 @@ class SparseSspCvarParetoViHelper { } private: - FrontierLayer createBaseFrontierLayer() const { + /*! + * Creates the initial frontier layer for a given bound n. + * + * For n < 0 we intentionally deviate from the paper's P_n = empty convention and instead use the + * mathematically direct excess-cost semantics induced by E[(X-n)^+]: because all costs are nonnegative, + * Pr[X <= n] = 0 and E[(X-n)^+] = E[X] - n. Hence the Pareto set collapses to the singleton + * (0, e*(s) - n), where e*(s) is the minimal expected cost-to-go from s. + */ + FrontierLayer createInitialFrontierLayer(int64_t costBound) const { FrontierLayer baseLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); + ValueType const boundValue = storm::utility::convertNumber(costBound); for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { - if (preprocessingResult.targetStates[state]) { + if (!preprocessingResult.reachableStates[state]) { + continue; + } + if (costBound >= 0 && preprocessingResult.targetStates[state]) { baseLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); - } else if (preprocessingResult.reachableStates[state]) { - baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state]); + } else { + baseLayer[state] = + ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); } } return baseLayer; @@ -84,23 +97,25 @@ class SparseSspCvarParetoViHelper { STORM_LOG_ASSERT(preprocessingResult.maximalChoiceCost > 0, "Expected a strictly positive maximal choice cost."); FrontierWindow frontierWindow(preprocessingResult.maximalChoiceCost, FrontierLayer(preprocessingResult.transitionMatrix.getRowGroupCount())); - frontierWindow[0] = createBaseFrontierLayer(); + for (int64_t costBound = 1 - static_cast(preprocessingResult.maximalChoiceCost); costBound <= 0; ++costBound) { + frontierWindow[getWindowIndex(costBound)] = createInitialFrontierLayer(costBound); + } return frontierWindow; } - static void writeFrontierLayerToWindow(uint64_t costBound, FrontierLayer layer, FrontierWindow& frontierWindow) { - frontierWindow[costBound % frontierWindow.size()] = std::move(layer); + static void writeFrontierLayerToWindow(int64_t costBound, FrontierLayer layer, FrontierWindow& frontierWindow) { + frontierWindow[getWindowIndex(costBound, frontierWindow.size())] = std::move(layer); + } + + FrontierLayer const& getFrontierLayerForBound(int64_t costBound, FrontierWindow const& frontierWindow) const { + return frontierWindow[getWindowIndex(costBound, frontierWindow.size())]; } ParetoFront computeActionFront(uint64_t actionRow, uint64_t costBound, FrontierWindow const& frontierWindow) const { uint64_t const actionCost = getChoiceCostBoundOffset(actionRow); - if (costBound < actionCost) { - return ParetoFront(); - } - ParetoFront actionFront = ParetoFront::singleton(storm::utility::zero(), storm::utility::zero()); - uint64_t const predecessorBound = costBound - actionCost; - FrontierLayer const& predecessorLayer = frontierWindow[predecessorBound % frontierWindow.size()]; + int64_t const predecessorBound = static_cast(costBound) - static_cast(actionCost); + FrontierLayer const& predecessorLayer = getFrontierLayerForBound(predecessorBound, frontierWindow); for (auto const& transition : preprocessingResult.transitionMatrix.getRow(actionRow)) { actionFront = actionFront.minkowskiSum(predecessorLayer[transition.getColumn()].scaled(transition.getValue())); @@ -154,6 +169,20 @@ class SparseSspCvarParetoViHelper { return storm::utility::convertNumber(preprocessingResult.choiceCosts[actionRow]); } + static std::size_t getWindowIndex(int64_t costBound, std::size_t windowSize) { + STORM_LOG_ASSERT(windowSize > 0, "Expected a non-empty SSP frontier window."); + int64_t const signedWindowSize = static_cast(windowSize); + int64_t index = costBound % signedWindowSize; + if (index < 0) { + index += signedWindowSize; + } + return static_cast(index); + } + + std::size_t getWindowIndex(int64_t costBound) const { + return getWindowIndex(costBound, preprocessingResult.maximalChoiceCost); + } + CvarQueryInformation const& queryInformation; preprocessing::SspCvarPreprocessingResult const& preprocessingResult; }; diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index 4010946f61..c9201945e1 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -204,6 +204,7 @@ class SspParetoFront { } sortPoints(); removeDuplicateProbabilityPoints(); + removeDominatedPoints(); removeNonExtremeConvexPoints(); } @@ -228,6 +229,25 @@ class SspParetoFront { points = std::move(uniquePoints); } + void removeDominatedPoints() { + if (points.size() < 2) { + return; + } + container_type nonDominatedPoints; + nonDominatedPoints.reserve(points.size()); + ValueType bestExpectedCostSeenFromRight = points.back().expectedCost; + nonDominatedPoints.push_back(points.back()); + for (std::size_t index = points.size() - 1; index > 0; --index) { + Point const& point = points[index - 1]; + if (point.expectedCost < bestExpectedCostSeenFromRight) { + nonDominatedPoints.push_back(point); + bestExpectedCostSeenFromRight = point.expectedCost; + } + } + std::reverse(nonDominatedPoints.begin(), nonDominatedPoints.end()); + points = std::move(nonDominatedPoints); + } + void removeNonExtremeConvexPoints() { if (points.size() < 3) { return; @@ -243,9 +263,9 @@ class SspParetoFront { } points = std::move(hullPoints); STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), [](Point const& left, Point const& right) { - return left.probability >= right.probability || left.expectedCost <= right.expectedCost; + return left.probability >= right.probability || left.expectedCost >= right.expectedCost; }) == points.end(), - "Expected SSP Pareto front points to be strictly ordered by increasing probability and decreasing expected cost."); + "Expected SSP Pareto front points to be strictly ordered by increasing probability and increasing expected cost."); } static bool liesOnOrAboveSegment(Point const& left, Point const& middle, Point const& right) { diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 68ab515343..0493a84a87 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -267,4 +267,14 @@ TEST(CvarQueryTest, DeterministicSspPathMdp) { double value = checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi); EXPECT_NEAR(value, 5.0, 1e-10); } + +TEST(CvarQueryTest, BranchingSspTradeoffMdp) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_branching_tradeoff_mdp.nm"; + + auto halfInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", 0.5); + EXPECT_NEAR(checkInitialStateValueWithMethod(halfInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 6.0, 1e-10); + + auto nineTenthsInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", 0.9); + EXPECT_NEAR(checkInitialStateValueWithMethod(nineTenthsInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 16.0 / 3.0, 1e-10); +} } // namespace From 5cbc512a34d36903649128d51701d8d2b4b13d07 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 6 May 2026 13:57:18 +0200 Subject: [PATCH 36/65] Corrected tail semantics --- src/storm/settings/modules/IOSettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storm/settings/modules/IOSettings.cpp b/src/storm/settings/modules/IOSettings.cpp index 48b83079ab..a96f136625 100644 --- a/src/storm/settings/modules/IOSettings.cpp +++ b/src/storm/settings/modules/IOSettings.cpp @@ -283,7 +283,7 @@ IOSettings::IOSettings() : ModuleSettings(moduleName) { .build()); this->addOption(storm::settings::OptionBuilder(moduleName, cvarOptionName, false, "Computes the conditional value-at-risk for the selected property.") - .addArgument(storm::settings::ArgumentBuilder::createDoubleArgument("alpha", "The size of the lower tail.") + .addArgument(storm::settings::ArgumentBuilder::createDoubleArgument("alpha", "The size of the tail.") .addValidatorDouble(storm::settings::ArgumentValidatorFactory::createDoubleRangeValidatorExcluding(0.0, 1.0)) .build()) .build()); From a5f518e2970a4825637308371e8a0aa082761506 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 6 May 2026 16:50:03 +0200 Subject: [PATCH 37/65] Add binary search to find valid threshold range --- .../SparseWeightedReachabilityCvarLpHelper.h | 192 +++++++++++++----- 1 file changed, 141 insertions(+), 51 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h index 47f742957f..1a53aa33c6 100644 --- a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h @@ -3,17 +3,23 @@ #include #include #include +#include #include #include "storm/environment/Environment.h" +#include "storm/environment/solver/MinMaxSolverEnvironment.h" +#include "storm/environment/solver/SolverEnvironment.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/exceptions/UnexpectedException.h" #include "storm/modelchecker/cvar/CvarComputationResult.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessingResult.h" +#include "storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h" #include "storm/solver/LpSolver.h" +#include "storm/solver/SolveGoal.h" #include "storm/storage/Scheduler.h" #include "storm/storage/expressions/BinaryRelationType.h" +#include "storm/utility/ConstantsComparator.h" #include "storm/utility/constants.h" #include "storm/utility/macros.h" #include "storm/utility/solver.h" @@ -30,6 +36,12 @@ struct CvarThresholdData { storm::storage::BitVector targetStatesBelowOrAtThreshold; }; +template +struct CvarRewardBucket { + ValueType reward; + std::vector targetStates; +}; + template struct WeightedReachabilityCvarLpData { double alpha; @@ -38,45 +50,25 @@ struct WeightedReachabilityCvarLpData { std::string rewardModelName; storm::storage::BitVector targetStates; std::vector terminalRewards; - std::vector candidateThresholds; + std::vector> rewardBuckets; storm::storage::SparseMatrix transitionMatrix; + storm::storage::SparseMatrix backwardChoices; + storm::storage::SparseMatrix backwardTransitions; }; template -std::vector collectCandidateThresholds(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards) { - std::vector candidateThresholds; - candidateThresholds.reserve(targetStates.getNumberOfSetBits()); - for (uint64_t state = 0; state < terminalRewards.size(); ++state) { - if (targetStates[state]) { - candidateThresholds.push_back(terminalRewards[state]); - } +std::vector> collectRewardBuckets(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards) { + std::map> buckets; + for (auto state : targetStates) { + buckets[terminalRewards[state]].push_back(state); } - std::sort(candidateThresholds.begin(), candidateThresholds.end()); - candidateThresholds.erase(std::unique(candidateThresholds.begin(), candidateThresholds.end()), candidateThresholds.end()); - return candidateThresholds; -} -template -CvarThresholdData createCvarThresholdData(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards, - ValueType const& threshold) { - storm::storage::BitVector targetStatesBelowThreshold(targetStates.size(), false); - storm::storage::BitVector targetStatesAtThreshold(targetStates.size(), false); - storm::storage::BitVector targetStatesBelowOrAtThreshold(targetStates.size(), false); - - for (uint64_t state = 0; state < terminalRewards.size(); ++state) { - if (!targetStates[state]) { - continue; - } - if (terminalRewards[state] < threshold) { - targetStatesBelowThreshold.set(state, true); - targetStatesBelowOrAtThreshold.set(state, true); - } else if (terminalRewards[state] == threshold) { - targetStatesAtThreshold.set(state, true); - targetStatesBelowOrAtThreshold.set(state, true); - } + std::vector> result; + result.reserve(buckets.size()); + for (auto& bucket : buckets) { + result.push_back({bucket.first, std::move(bucket.second)}); } - - return {threshold, targetStatesBelowThreshold, targetStatesAtThreshold, targetStatesBelowOrAtThreshold}; + return result; } /*! @@ -102,34 +94,43 @@ class SparseWeightedReachabilityCvarLpHelper { preprocessing::WeightedReachabilityCvarPreprocessingResult const& weightedReachabilityPreprocessingResult) : lpData(createLpData(queryInformation, weightedReachabilityPreprocessingResult)) {} - CvarComputationResult computeCvar(Environment const&, bool produceScheduler = false) const { - STORM_LOG_THROW(!lpData.candidateThresholds.empty(), storm::exceptions::NotImplementedException, + CvarComputationResult computeCvar(Environment const& env, bool produceScheduler = false) const { + STORM_LOG_THROW(!lpData.rewardBuckets.empty(), storm::exceptions::NotImplementedException, "CVaR model checking requires at least one target reward threshold candidate."); std::optional bestValue; + std::optional bestThresholdIndex; std::unique_ptr> bestScheduler; - for (auto const& threshold : lpData.candidateThresholds) { - auto thresholdData = createCvarThresholdData(lpData.targetStates, lpData.terminalRewards, threshold); - auto thresholdResult = buildLpForThreshold(thresholdData, produceScheduler); + auto candidateRange = computeCandidateRange(env); + storm::storage::BitVector targetStatesBelowThreshold = createPrefixTargetStates(candidateRange.first); + for (uint64_t thresholdIndex = candidateRange.first; thresholdIndex < candidateRange.second; ++thresholdIndex) { + auto thresholdData = createThresholdData(thresholdIndex, targetStatesBelowThreshold); + auto thresholdResult = buildLpForThreshold(thresholdData, false); if (!thresholdResult.has_value()) { + addBucketStates(targetStatesBelowThreshold, thresholdIndex); continue; } if (!bestValue.has_value()) { bestValue = thresholdResult->value; - if (produceScheduler) { - bestScheduler = std::move(thresholdResult->scheduler); - } + bestThresholdIndex = thresholdIndex; } else if ((storm::solver::minimize(lpData.optimizationDirection) && thresholdResult->value < bestValue.value()) || (storm::solver::maximize(lpData.optimizationDirection) && thresholdResult->value > bestValue.value())) { bestValue = thresholdResult->value; - if (produceScheduler) { - bestScheduler = std::move(thresholdResult->scheduler); - } + bestThresholdIndex = thresholdIndex; } + addBucketStates(targetStatesBelowThreshold, thresholdIndex); } STORM_LOG_THROW(bestValue.has_value(), storm::exceptions::UnexpectedException, "CVaR model checking did not find a feasible LP for any threshold candidate."); + if (produceScheduler) { + STORM_LOG_ASSERT(bestThresholdIndex.has_value(), "Expected a threshold index for the best CVaR LP value."); + auto thresholdData = createThresholdData(bestThresholdIndex.value()); + auto thresholdResult = buildLpForThreshold(thresholdData, true); + STORM_LOG_THROW(thresholdResult.has_value(), storm::exceptions::UnexpectedException, + "The previously optimal CVaR LP threshold became infeasible when extracting a scheduler."); + bestScheduler = std::move(thresholdResult->scheduler); + } return {bestValue.value() / storm::utility::convertNumber(lpData.alpha), std::move(bestScheduler)}; } @@ -137,16 +138,107 @@ class SparseWeightedReachabilityCvarLpHelper { static WeightedReachabilityCvarLpData createLpData( CvarQueryInformation const& queryInformation, preprocessing::WeightedReachabilityCvarPreprocessingResult const& weightedReachabilityPreprocessingResult) { - auto candidateThresholds = - collectCandidateThresholds(weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards); + auto rewardBuckets = + collectRewardBuckets(weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards); return {queryInformation.alpha, queryInformation.optimizationDirection, weightedReachabilityPreprocessingResult.initialState, weightedReachabilityPreprocessingResult.rewardModelName, weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards, - std::move(candidateThresholds), - weightedReachabilityPreprocessingResult.transitionMatrix}; + std::move(rewardBuckets), + weightedReachabilityPreprocessingResult.transitionMatrix, + weightedReachabilityPreprocessingResult.transitionMatrix.transpose(), + weightedReachabilityPreprocessingResult.transitionMatrix.transpose(true)}; + } + + storm::storage::BitVector createInitialStateBitVector() const { + storm::storage::BitVector initialStates(lpData.transitionMatrix.getRowGroupCount(), false); + initialStates.set(lpData.initialState, true); + return initialStates; + } + + void addBucketStates(storm::storage::BitVector& states, uint64_t bucketIndex) const { + for (auto state : lpData.rewardBuckets[bucketIndex].targetStates) { + states.set(state, true); + } + } + + storm::storage::BitVector createBucketTargetStates(uint64_t bucketIndex) const { + storm::storage::BitVector states(lpData.transitionMatrix.getRowGroupCount(), false); + addBucketStates(states, bucketIndex); + return states; + } + + storm::storage::BitVector createPrefixTargetStates(uint64_t endBucketIndex) const { + storm::storage::BitVector states(lpData.transitionMatrix.getRowGroupCount(), false); + for (uint64_t bucketIndex = 0; bucketIndex < endBucketIndex; ++bucketIndex) { + addBucketStates(states, bucketIndex); + } + return states; + } + + CvarThresholdData createThresholdData(uint64_t thresholdIndex) const { + auto targetStatesBelowThreshold = createPrefixTargetStates(thresholdIndex); + return createThresholdData(thresholdIndex, targetStatesBelowThreshold); + } + + CvarThresholdData createThresholdData(uint64_t thresholdIndex, storm::storage::BitVector const& targetStatesBelowThreshold) const { + auto targetStatesAtThreshold = createBucketTargetStates(thresholdIndex); + auto targetStatesBelowOrAtThreshold = targetStatesBelowThreshold | targetStatesAtThreshold; + return {lpData.rewardBuckets[thresholdIndex].reward, targetStatesBelowThreshold, std::move(targetStatesAtThreshold), + std::move(targetStatesBelowOrAtThreshold)}; + } + + ValueType computeReachabilityProbability(Environment const& env, storm::solver::OptimizationDirection direction, + storm::storage::BitVector const& targetStates) const { + if (targetStates.empty()) { + return storm::utility::zero(); + } + if (targetStates[lpData.initialState]) { + return storm::utility::one(); + } + + storm::storage::BitVector allStates(lpData.transitionMatrix.getRowGroupCount(), true); + auto result = storm::modelchecker::helper::SparseMdpPrctlHelper::computeUntilProbabilities( + env, storm::solver::SolveGoal(direction, createInitialStateBitVector()), lpData.transitionMatrix, lpData.backwardTransitions, + allStates, targetStates, false, false); + return result.values[lpData.initialState]; + } + + std::pair computeCandidateRange(Environment const& env) const { + uint64_t const bucketCount = lpData.rewardBuckets.size(); + ValueType const alpha = storm::utility::convertNumber(lpData.alpha); + storm::utility::ConstantsComparator comparator(storm::utility::convertNumber(env.solver().minMax().getPrecision())); + + uint64_t lower = 0; + uint64_t upper = bucketCount; + while (lower < upper) { + uint64_t const mid = lower + (upper - lower) / 2; + auto targetStatesBelowOrAtThreshold = createPrefixTargetStates(mid + 1); + auto maxReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Maximize, targetStatesBelowOrAtThreshold); + if (comparator.isLess(maxReachability, alpha)) { + lower = mid + 1; + } else { + upper = mid; + } + } + uint64_t const firstNotTooLow = lower; + + lower = firstNotTooLow; + upper = bucketCount; + while (lower < upper) { + uint64_t const mid = lower + (upper - lower) / 2; + auto targetStatesBelowThreshold = createPrefixTargetStates(mid); + auto minReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Minimize, targetStatesBelowThreshold); + if (comparator.isLess(alpha, minReachability)) { + upper = mid; + } else { + lower = mid + 1; + } + } + + return {firstNotTooLow, lower}; } std::optional> buildLpForThreshold(CvarThresholdData const& thresholdData, bool produceScheduler) const { @@ -156,7 +248,6 @@ class SparseWeightedReachabilityCvarLpHelper { auto lpSolverFactory = storm::utility::solver::getLpSolverFactory(); auto solver = lpSolverFactory->createRaw("cvar"); solver->setOptimizationDirection(lpData.optimizationDirection); - auto backwardChoices = lpData.transitionMatrix.transpose(); std::vector actionFlowVariables; actionFlowVariables.reserve(lpData.transitionMatrix.getRowCount()); @@ -183,7 +274,7 @@ class SparseWeightedReachabilityCvarLpHelper { for (uint64_t state = 0; state < lpData.transitionMatrix.getRowGroupCount(); ++state) { auto outgoingActions = lpData.transitionMatrix.getRowGroupIndices(state); - auto incomingActions = backwardChoices.getRow(state); + auto incomingActions = lpData.backwardChoices.getRow(state); uint64_t reservedSize = outgoingActions.size() + incomingActions.getNumberOfEntries() + (recurrentFlowVariables[state].has_value() ? 1 : 0); RawLpConstraint constraint(storm::expressions::RelationType::Equal, state == lpData.initialState ? storm::utility::one() : storm::utility::zero(), reservedSize); @@ -228,8 +319,7 @@ class SparseWeightedReachabilityCvarLpHelper { solver->addConstraint("split_le_" + std::to_string(state), splitInequalityConstraint); } - RawLpConstraint probabilityConsistentSplitConstraint(storm::expressions::RelationType::Equal, - storm::utility::convertNumber(lpData.alpha), + RawLpConstraint probabilityConsistentSplitConstraint(storm::expressions::RelationType::Equal, storm::utility::convertNumber(lpData.alpha), thresholdData.targetStatesBelowOrAtThreshold.getNumberOfSetBits()); for (auto state : thresholdData.targetStatesBelowOrAtThreshold) { probabilityConsistentSplitConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); From d6c45b8b905a236d763476dc65fd9a7157193aa3 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 6 May 2026 16:50:12 +0200 Subject: [PATCH 38/65] format --- .../modelchecker/cvar/CvarClassification.h | 2 +- .../cvar/helper/SparseCvarComputationHelper.h | 3 +- .../cvar/helper/SparseSspCvarParetoViHelper.h | 18 ++++------- .../modelchecker/cvar/helper/SspParetoFront.h | 11 ++++--- .../cvar/preprocessing/SspCvarPreprocessor.h | 32 +++++++++---------- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 2 +- 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/storm/modelchecker/cvar/CvarClassification.h b/src/storm/modelchecker/cvar/CvarClassification.h index f4f95099a3..acbc1cf62f 100644 --- a/src/storm/modelchecker/cvar/CvarClassification.h +++ b/src/storm/modelchecker/cvar/CvarClassification.h @@ -2,8 +2,8 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" -#include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/CvarMethod.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/storage/BitVector.h" #include "storm/utility/macros.h" diff --git a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h index 1503549633..7cbe80b418 100644 --- a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h @@ -21,8 +21,7 @@ class SparseCvarComputationHelper { public: using ValueType = typename SparseMdpModelType::ValueType; - SparseCvarComputationHelper(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, - storm::storage::BitVector const& targetStates) + SparseCvarComputationHelper(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const& targetStates) : model(model), queryInformation(queryInformation), targetStates(targetStates) { // Intentionally left empty. } diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 45253344a3..397f20cada 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -38,8 +38,7 @@ class SparseSspCvarParetoViHelper { using FrontierLayer = std::vector; using FrontierWindow = std::vector; - SparseSspCvarParetoViHelper(CvarQueryInformation const& queryInformation, - preprocessing::SspCvarPreprocessingResult const& preprocessingResult) + SparseSspCvarParetoViHelper(CvarQueryInformation const& queryInformation, preprocessing::SspCvarPreprocessingResult const& preprocessingResult) : queryInformation(queryInformation), preprocessingResult(preprocessingResult) { // Intentionally left empty. } @@ -52,8 +51,7 @@ class SparseSspCvarParetoViHelper { std::optional bestCandidate = extractCvarCandidateFromInitialFrontier(frontierWindow[0][preprocessingResult.initialState], 0, queryInformation.alpha); - for (uint64_t costBound = 1; - !bestCandidate.has_value() || storm::utility::convertNumber(costBound) <= bestCandidate.value(); ++costBound) { + for (uint64_t costBound = 1; !bestCandidate.has_value() || storm::utility::convertNumber(costBound) <= bestCandidate.value(); ++costBound) { auto currentLayer = computeFrontierLayerForCostBound(costBound, frontierWindow); auto currentCandidate = extractCvarCandidateFromInitialFrontier(currentLayer[preprocessingResult.initialState], costBound, queryInformation.alpha); if (currentCandidate.has_value() && (!bestCandidate.has_value() || currentCandidate.value() < bestCandidate.value())) { @@ -62,8 +60,7 @@ class SparseSspCvarParetoViHelper { writeFrontierLayerToWindow(costBound, std::move(currentLayer), frontierWindow); } - STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, - "CVaR SSP value iteration did not find a feasible candidate."); + STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, "CVaR SSP value iteration did not find a feasible candidate."); return {bestCandidate.value(), nullptr}; } @@ -86,16 +83,14 @@ class SparseSspCvarParetoViHelper { if (costBound >= 0 && preprocessingResult.targetStates[state]) { baseLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); } else { - baseLayer[state] = - ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); + baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); } } return baseLayer; } FrontierWindow initializeFrontierWindow() const { - STORM_LOG_ASSERT(preprocessingResult.maximalChoiceCost > 0, - "Expected a strictly positive maximal choice cost."); + STORM_LOG_ASSERT(preprocessingResult.maximalChoiceCost > 0, "Expected a strictly positive maximal choice cost."); FrontierWindow frontierWindow(preprocessingResult.maximalChoiceCost, FrontierLayer(preprocessingResult.transitionMatrix.getRowGroupCount())); for (int64_t costBound = 1 - static_cast(preprocessingResult.maximalChoiceCost); costBound <= 0; ++costBound) { frontierWindow[getWindowIndex(costBound)] = createInitialFrontierLayer(costBound); @@ -161,8 +156,7 @@ class SparseSspCvarParetoViHelper { if (!continuationCost.has_value()) { return std::nullopt; } - return storm::utility::convertNumber(costBound) + - continuationCost.value() / storm::utility::convertNumber(alpha); + return storm::utility::convertNumber(costBound) + continuationCost.value() / storm::utility::convertNumber(alpha); } uint64_t getChoiceCostBoundOffset(uint64_t actionRow) const { diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index c9201945e1..d13f87a0cb 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -256,15 +256,16 @@ class SspParetoFront { hullPoints.reserve(points.size()); for (auto const& point : points) { hullPoints.push_back(point); - while (hullPoints.size() >= 3 && liesOnOrAboveSegment(hullPoints[hullPoints.size() - 3], hullPoints[hullPoints.size() - 2], - hullPoints[hullPoints.size() - 1])) { + while (hullPoints.size() >= 3 && + liesOnOrAboveSegment(hullPoints[hullPoints.size() - 3], hullPoints[hullPoints.size() - 2], hullPoints[hullPoints.size() - 1])) { hullPoints.erase(hullPoints.end() - 2); } } points = std::move(hullPoints); - STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), [](Point const& left, Point const& right) { - return left.probability >= right.probability || left.expectedCost >= right.expectedCost; - }) == points.end(), + STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), + [](Point const& left, Point const& right) { + return left.probability >= right.probability || left.expectedCost >= right.expectedCost; + }) == points.end(), "Expected SSP Pareto front points to be strictly ordered by increasing probability and increasing expected cost."); } diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index 898489c83b..df373ba02a 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -7,10 +7,10 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" #include "storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h" #include "storm/models/sparse/StandardRewardModel.h" #include "storm/solver/SolveGoal.h" -#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" #include "storm/storage/BitVector.h" #include "storm/storage/SparseMatrix.h" #include "storm/utility/constants.h" @@ -30,8 +30,9 @@ namespace preprocessing { * choice-based costs and graph information needed by the future Pareto-front VI. */ template -std::vector extractChoiceCostsForSsp( - SparseMdpModelType const& model, typename SparseMdpModelType::RewardModelType const& rewardModel, storm::storage::BitVector const& targetStates) { +std::vector extractChoiceCostsForSsp(SparseMdpModelType const& model, + typename SparseMdpModelType::RewardModelType const& rewardModel, + storm::storage::BitVector const& targetStates) { using ValueType = typename SparseMdpModelType::ValueType; std::vector choiceCosts(model.getNumberOfChoices(), storm::utility::zero()); @@ -44,8 +45,8 @@ std::vector extractChoiceCostsForSsp( } ValueType stateReward = hasStateRewards ? rewardModel.getStateReward(state) : storm::utility::zero(); - for (uint64_t row = model.getTransitionMatrix().getRowGroupIndices()[state], endRow = model.getTransitionMatrix().getRowGroupIndices()[state + 1]; row < endRow; - ++row) { + for (uint64_t row = model.getTransitionMatrix().getRowGroupIndices()[state], endRow = model.getTransitionMatrix().getRowGroupIndices()[state + 1]; + row < endRow; ++row) { choiceCosts[row] = stateReward; if (hasStateActionRewards) { choiceCosts[row] += rewardModel.getStateActionReward(row); @@ -101,8 +102,8 @@ std::vector computeExpectedCostsToGoal(Environment const& env, storm: template SspCvarPreprocessingResult preprocessSspCvar(Environment const& env, SparseMdpModelType const& model, - CvarQueryInformation const& queryInformation, - storm::storage::BitVector const& targetStates) { + CvarQueryInformation const& queryInformation, + storm::storage::BitVector const& targetStates) { using ValueType = typename SparseMdpModelType::ValueType; std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; @@ -111,8 +112,7 @@ SspCvarPreprocessingResult preprocessSsp rewardModelName = model.getUniqueRewardModelName(); } - STORM_LOG_THROW(queryInformation.optimizationDirection == storm::solver::OptimizationDirection::Minimize, - storm::exceptions::InvalidPropertyException, + STORM_LOG_THROW(queryInformation.optimizationDirection == storm::solver::OptimizationDirection::Minimize, storm::exceptions::InvalidPropertyException, "CVaR SSP preprocessing currently only supports minimizing total costs."); STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, @@ -126,7 +126,8 @@ SspCvarPreprocessingResult preprocessSsp auto transitionMatrix = model.getTransitionMatrix(); bool normalizedTargetStatesToAbsorbing = false; for (auto targetState : targetStates) { - for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; ++row) { + for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; + ++row) { for (auto const& entry : transitionMatrix.getRow(row)) { if (entry.getColumn() != targetState) { normalizedTargetStatesToAbsorbing = true; @@ -147,12 +148,11 @@ SspCvarPreprocessingResult preprocessSsp } auto backwardTransitions = transitionMatrix.transpose(true); - auto reachableStates = storm::utility::graph::getReachableStates( - transitionMatrix, model.getInitialStates(), storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), - storm::storage::BitVector(transitionMatrix.getRowGroupCount(), false)); - auto properStates = - storm::utility::graph::performProb1E(transitionMatrix, transitionMatrix.getRowGroupIndices(), backwardTransitions, - storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), targetStates); + auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, model.getInitialStates(), + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), false)); + auto properStates = storm::utility::graph::performProb1E(transitionMatrix, transitionMatrix.getRowGroupIndices(), backwardTransitions, + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), targetStates); STORM_LOG_THROW(reachableStates.isSubsetOf(properStates), storm::exceptions::InvalidPropertyException, "CVaR SSP preprocessing currently requires a proper policy from every reachable state."); auto choiceCosts = extractChoiceCostsForSsp(model, rewardModel, targetStates); diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 0493a84a87..6534ff91f1 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -10,8 +10,8 @@ #include "storm/environment/modelchecker/ModelCheckerEnvironment.h" #include "storm/exceptions/InvalidOperationException.h" #include "storm/exceptions/InvalidPropertyException.h" -#include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/modelchecker/CheckTask.h" +#include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" #include "storm/models/sparse/Mdp.h" From 8b05116f0989e2f4c614a071f9ae85ac0ad6ec2c Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 7 May 2026 11:06:25 +0200 Subject: [PATCH 39/65] Optimize CVaR threshold pruning Bucket terminal rewards, cache transposes and prefix sets, prune impossible thresholds via reachability checks, and extract schedulers only for the winning LP. --- .../SparseWeightedReachabilityCvarLpHelper.h | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h index 1a53aa33c6..9ccefd90df 100644 --- a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h @@ -21,6 +21,7 @@ #include "storm/storage/expressions/BinaryRelationType.h" #include "storm/utility/ConstantsComparator.h" #include "storm/utility/constants.h" +#include "storm/utility/graph.h" #include "storm/utility/macros.h" #include "storm/utility/solver.h" @@ -47,6 +48,7 @@ struct WeightedReachabilityCvarLpData { double alpha; storm::solver::OptimizationDirection optimizationDirection; uint64_t initialState; + storm::storage::BitVector initialStates; std::string rewardModelName; storm::storage::BitVector targetStates; std::vector terminalRewards; @@ -57,9 +59,13 @@ struct WeightedReachabilityCvarLpData { }; template -std::vector> collectRewardBuckets(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards) { +std::vector> collectRewardBuckets(storm::storage::BitVector const& targetStates, std::vector const& terminalRewards, + storm::storage::BitVector const& reachableStates) { std::map> buckets; for (auto state : targetStates) { + if (!reachableStates[state]) { + continue; + } buckets[terminalRewards[state]].push_back(state); } @@ -138,11 +144,18 @@ class SparseWeightedReachabilityCvarLpHelper { static WeightedReachabilityCvarLpData createLpData( CvarQueryInformation const& queryInformation, preprocessing::WeightedReachabilityCvarPreprocessingResult const& weightedReachabilityPreprocessingResult) { - auto rewardBuckets = - collectRewardBuckets(weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards); + auto initialStates = createInitialStateBitVector(weightedReachabilityPreprocessingResult.transitionMatrix.getRowGroupCount(), + weightedReachabilityPreprocessingResult.initialState); + storm::storage::BitVector allStates(weightedReachabilityPreprocessingResult.transitionMatrix.getRowGroupCount(), true); + storm::storage::BitVector noStates(weightedReachabilityPreprocessingResult.transitionMatrix.getRowGroupCount(), false); + auto reachableStates = + storm::utility::graph::getReachableStates(weightedReachabilityPreprocessingResult.transitionMatrix, initialStates, allStates, noStates); + auto rewardBuckets = collectRewardBuckets(weightedReachabilityPreprocessingResult.effectiveTargetStates, + weightedReachabilityPreprocessingResult.terminalRewards, reachableStates); return {queryInformation.alpha, queryInformation.optimizationDirection, weightedReachabilityPreprocessingResult.initialState, + std::move(initialStates), weightedReachabilityPreprocessingResult.rewardModelName, weightedReachabilityPreprocessingResult.effectiveTargetStates, weightedReachabilityPreprocessingResult.terminalRewards, @@ -152,9 +165,9 @@ class SparseWeightedReachabilityCvarLpHelper { weightedReachabilityPreprocessingResult.transitionMatrix.transpose(true)}; } - storm::storage::BitVector createInitialStateBitVector() const { - storm::storage::BitVector initialStates(lpData.transitionMatrix.getRowGroupCount(), false); - initialStates.set(lpData.initialState, true); + static storm::storage::BitVector createInitialStateBitVector(uint64_t stateCount, uint64_t initialState) { + storm::storage::BitVector initialStates(stateCount, false); + initialStates.set(initialState, true); return initialStates; } @@ -178,6 +191,14 @@ class SparseWeightedReachabilityCvarLpHelper { return states; } + storm::storage::BitVector const& getCachedPrefixTargetStates(uint64_t endBucketIndex, std::map& cache) const { + auto cachedPrefix = cache.find(endBucketIndex); + if (cachedPrefix != cache.end()) { + return cachedPrefix->second; + } + return cache.emplace(endBucketIndex, createPrefixTargetStates(endBucketIndex)).first->second; + } + CvarThresholdData createThresholdData(uint64_t thresholdIndex) const { auto targetStatesBelowThreshold = createPrefixTargetStates(thresholdIndex); return createThresholdData(thresholdIndex, targetStatesBelowThreshold); @@ -201,7 +222,7 @@ class SparseWeightedReachabilityCvarLpHelper { storm::storage::BitVector allStates(lpData.transitionMatrix.getRowGroupCount(), true); auto result = storm::modelchecker::helper::SparseMdpPrctlHelper::computeUntilProbabilities( - env, storm::solver::SolveGoal(direction, createInitialStateBitVector()), lpData.transitionMatrix, lpData.backwardTransitions, + env, storm::solver::SolveGoal(direction, lpData.initialStates), lpData.transitionMatrix, lpData.backwardTransitions, allStates, targetStates, false, false); return result.values[lpData.initialState]; } @@ -210,12 +231,13 @@ class SparseWeightedReachabilityCvarLpHelper { uint64_t const bucketCount = lpData.rewardBuckets.size(); ValueType const alpha = storm::utility::convertNumber(lpData.alpha); storm::utility::ConstantsComparator comparator(storm::utility::convertNumber(env.solver().minMax().getPrecision())); + std::map prefixTargetStateCache; uint64_t lower = 0; uint64_t upper = bucketCount; while (lower < upper) { uint64_t const mid = lower + (upper - lower) / 2; - auto targetStatesBelowOrAtThreshold = createPrefixTargetStates(mid + 1); + auto const& targetStatesBelowOrAtThreshold = getCachedPrefixTargetStates(mid + 1, prefixTargetStateCache); auto maxReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Maximize, targetStatesBelowOrAtThreshold); if (comparator.isLess(maxReachability, alpha)) { lower = mid + 1; @@ -229,7 +251,7 @@ class SparseWeightedReachabilityCvarLpHelper { upper = bucketCount; while (lower < upper) { uint64_t const mid = lower + (upper - lower) / 2; - auto targetStatesBelowThreshold = createPrefixTargetStates(mid); + auto const& targetStatesBelowThreshold = getCachedPrefixTargetStates(mid, prefixTargetStateCache); auto minReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Minimize, targetStatesBelowThreshold); if (comparator.isLess(alpha, minReachability)) { upper = mid; From 4cbc79698c5d75d992a78be62f31383b1b19cddb Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 7 May 2026 13:04:09 +0200 Subject: [PATCH 40/65] Add singleton and empty fast paths for pareto set operations --- .../modelchecker/cvar/helper/SspParetoFront.h | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index d13f87a0cb..94e58c2ed3 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -6,6 +6,7 @@ #include #include +#include "storm/utility/constants.h" #include "storm/utility/macros.h" namespace storm { @@ -50,6 +51,14 @@ class SspParetoFront { using container_type = std::vector; using const_iterator = typename container_type::const_iterator; + private: + struct AlreadyCanonicalTag {}; + + explicit SspParetoFront(container_type points, AlreadyCanonicalTag) : points(std::move(points)) { + // Intentionally left empty. + } + + public: SspParetoFront() = default; explicit SspParetoFront(container_type points) : points(std::move(points)) { @@ -57,7 +66,7 @@ class SspParetoFront { } static SspParetoFront singleton(ValueType const& probability, ValueType const& expectedCost) { - return SspParetoFront(container_type{{probability, expectedCost}}); + return SspParetoFront(container_type{{probability, expectedCost}}, AlreadyCanonicalTag{}); } bool empty() const { @@ -68,6 +77,10 @@ class SspParetoFront { return points.size(); } + bool isSingleton() const { + return points.size() == 1; + } + container_type const& getPoints() const { return points; } @@ -116,11 +129,24 @@ class SspParetoFront { if (empty()) { return SspParetoFront(); } + if (storm::utility::isOne(factor)) { + return *this; + } + if (storm::utility::isZero(factor)) { + return singleton(storm::utility::zero(), storm::utility::zero()); + } + if (isSingleton()) { + auto const& point = points.front(); + return singleton(factor * point.probability, factor * point.expectedCost); + } container_type scaledPoints; scaledPoints.reserve(points.size()); for (auto const& point : points) { scaledPoints.push_back(Point{factor * point.probability, factor * point.expectedCost}); } + if (storm::utility::isPositive(factor)) { + return SspParetoFront(std::move(scaledPoints), AlreadyCanonicalTag{}); + } return SspParetoFront(std::move(scaledPoints)); } @@ -128,6 +154,12 @@ class SspParetoFront { if (empty() || other.empty()) { return SspParetoFront(); } + if (isSingleton()) { + return other.translated(points.front()); + } + if (other.isSingleton()) { + return translated(other.points.front()); + } container_type sumPoints; sumPoints.reserve(points.size() * other.points.size()); for (auto const& left : points) { @@ -139,11 +171,22 @@ class SspParetoFront { } static SspParetoFront convexUnion(std::vector const& fronts) { + SspParetoFront const* onlyNonEmptyFront = nullptr; container_type unionPoints; std::size_t totalPointCount = 0; for (auto const& front : fronts) { + if (front.empty()) { + continue; + } + onlyNonEmptyFront = onlyNonEmptyFront == nullptr ? &front : onlyNonEmptyFront; totalPointCount += front.size(); } + if (totalPointCount == 0) { + return SspParetoFront(); + } + if (onlyNonEmptyFront != nullptr && onlyNonEmptyFront->size() == totalPointCount) { + return *onlyNonEmptyFront; + } unionPoints.reserve(totalPointCount); for (auto const& front : fronts) { unionPoints.insert(unionPoints.end(), front.begin(), front.end()); @@ -198,6 +241,26 @@ class SspParetoFront { } private: + SspParetoFront translated(Point const& offset) const { + if (empty()) { + return SspParetoFront(); + } + if (storm::utility::isZero(offset.probability) && storm::utility::isZero(offset.expectedCost)) { + return *this; + } + if (isSingleton()) { + Point const& point = points.front(); + return singleton(point.probability + offset.probability, point.expectedCost + offset.expectedCost); + } + + container_type translatedPoints; + translatedPoints.reserve(points.size()); + for (auto const& point : points) { + translatedPoints.push_back(Point{point.probability + offset.probability, point.expectedCost + offset.expectedCost}); + } + return SspParetoFront(std::move(translatedPoints), AlreadyCanonicalTag{}); + } + void canonicalize() { if (points.empty()) { return; From a5929bd900ba114babcd3c05c761cb4eb9c1bcb8 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 7 May 2026 14:11:33 +0200 Subject: [PATCH 41/65] Skip SSP pareto union for single action states --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 397f20cada..8ec1551d7d 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -129,10 +129,15 @@ class SparseSspCvarParetoViHelper { continue; } + uint64_t const firstActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state]; + uint64_t const endActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state + 1]; + if (firstActionRow + 1 == endActionRow) { + currentLayer[state] = computeActionFront(firstActionRow, costBound, frontierWindow); + continue; + } + std::vector actionFronts; - for (uint64_t actionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state], - endRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state + 1]; - actionRow < endRow; ++actionRow) { + for (uint64_t actionRow = firstActionRow; actionRow < endActionRow; ++actionRow) { auto actionFront = computeActionFront(actionRow, costBound, frontierWindow); if (!actionFront.empty()) { actionFronts.push_back(std::move(actionFront)); From 9f1b07081b626c5ee9241adc0da3cc133e9e8b90 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 7 May 2026 14:32:52 +0200 Subject: [PATCH 42/65] Optimize SSP Pareto VI hot loops Cache choice-cost offsets and reachable state lists, reserve action-front storage, use binary-search frontier lookup, and avoid vector erase during hull pruning. --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 51 ++++++++++++++----- .../modelchecker/cvar/helper/SspParetoFront.h | 25 +++++---- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 8ec1551d7d..bf218366ef 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -39,7 +39,11 @@ class SparseSspCvarParetoViHelper { using FrontierWindow = std::vector; SparseSspCvarParetoViHelper(CvarQueryInformation const& queryInformation, preprocessing::SspCvarPreprocessingResult const& preprocessingResult) - : queryInformation(queryInformation), preprocessingResult(preprocessingResult) { + : queryInformation(queryInformation), + preprocessingResult(preprocessingResult), + choiceCostOffsets(createChoiceCostOffsets(preprocessingResult)), + reachableTargetStates(collectReachableStates(preprocessingResult, true)), + reachableNonTargetStates(collectReachableStates(preprocessingResult, false)) { // Intentionally left empty. } @@ -76,16 +80,16 @@ class SparseSspCvarParetoViHelper { FrontierLayer createInitialFrontierLayer(int64_t costBound) const { FrontierLayer baseLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); ValueType const boundValue = storm::utility::convertNumber(costBound); - for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { - if (!preprocessingResult.reachableStates[state]) { - continue; - } + for (auto state : reachableTargetStates) { if (costBound >= 0 && preprocessingResult.targetStates[state]) { baseLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); } else { baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); } } + for (auto state : reachableNonTargetStates) { + baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); + } return baseLayer; } @@ -120,14 +124,10 @@ class SparseSspCvarParetoViHelper { FrontierLayer computeFrontierLayerForCostBound(uint64_t costBound, FrontierWindow const& frontierWindow) const { FrontierLayer currentLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); - for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { - if (!preprocessingResult.reachableStates[state]) { - continue; - } - if (preprocessingResult.targetStates[state]) { - currentLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); - continue; - } + for (auto state : reachableTargetStates) { + currentLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + } + for (auto state : reachableNonTargetStates) { uint64_t const firstActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state]; uint64_t const endActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state + 1]; @@ -137,6 +137,7 @@ class SparseSspCvarParetoViHelper { } std::vector actionFronts; + actionFronts.reserve(endActionRow - firstActionRow); for (uint64_t actionRow = firstActionRow; actionRow < endActionRow; ++actionRow) { auto actionFront = computeActionFront(actionRow, costBound, frontierWindow); if (!actionFront.empty()) { @@ -165,7 +166,7 @@ class SparseSspCvarParetoViHelper { } uint64_t getChoiceCostBoundOffset(uint64_t actionRow) const { - return storm::utility::convertNumber(preprocessingResult.choiceCosts[actionRow]); + return choiceCostOffsets[actionRow]; } static std::size_t getWindowIndex(int64_t costBound, std::size_t windowSize) { @@ -182,8 +183,30 @@ class SparseSspCvarParetoViHelper { return getWindowIndex(costBound, preprocessingResult.maximalChoiceCost); } + static std::vector createChoiceCostOffsets(preprocessing::SspCvarPreprocessingResult const& preprocessingResult) { + std::vector result; + result.reserve(preprocessingResult.choiceCosts.size()); + for (auto const& choiceCost : preprocessingResult.choiceCosts) { + result.push_back(storm::utility::convertNumber(choiceCost)); + } + return result; + } + + static std::vector collectReachableStates(preprocessing::SspCvarPreprocessingResult const& preprocessingResult, bool targetStates) { + std::vector result; + for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { + if (preprocessingResult.reachableStates[state] && preprocessingResult.targetStates[state] == targetStates) { + result.push_back(state); + } + } + return result; + } + CvarQueryInformation const& queryInformation; preprocessing::SspCvarPreprocessingResult const& preprocessingResult; + std::vector choiceCostOffsets; + std::vector reachableTargetStates; + std::vector reachableNonTargetStates; }; } // namespace cvar diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index 94e58c2ed3..ba147cbfd9 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -211,18 +211,16 @@ class SspParetoFront { return points.front().expectedCost; } - for (std::size_t index = 1; index < points.size(); ++index) { - Point const& left = points[index - 1]; - Point const& right = points[index]; - if (probability <= right.probability) { - ValueType const probabilityDelta = right.probability - left.probability; - STORM_LOG_ASSERT(probabilityDelta > 0, "Expected SSP Pareto front points to be strictly sorted by probability."); - ValueType const interpolationFactor = (probability - left.probability) / probabilityDelta; - return left.expectedCost + interpolationFactor * (right.expectedCost - left.expectedCost); - } - } - - return points.back().expectedCost; + auto rightIt = std::lower_bound(points.begin(), points.end(), probability, + [](Point const& point, ValueType const& value) { return point.probability < value; }); + STORM_LOG_ASSERT(rightIt != points.begin() && rightIt != points.end(), "Expected probability to lie inside the SSP Pareto-front range."); + + Point const& left = *(rightIt - 1); + Point const& right = *rightIt; + ValueType const probabilityDelta = right.probability - left.probability; + STORM_LOG_ASSERT(probabilityDelta > 0, "Expected SSP Pareto front points to be strictly sorted by probability."); + ValueType const interpolationFactor = (probability - left.probability) / probabilityDelta; + return left.expectedCost + interpolationFactor * (right.expectedCost - left.expectedCost); } std::string toString() const { @@ -321,7 +319,8 @@ class SspParetoFront { hullPoints.push_back(point); while (hullPoints.size() >= 3 && liesOnOrAboveSegment(hullPoints[hullPoints.size() - 3], hullPoints[hullPoints.size() - 2], hullPoints[hullPoints.size() - 1])) { - hullPoints.erase(hullPoints.end() - 2); + hullPoints[hullPoints.size() - 2] = hullPoints.back(); + hullPoints.pop_back(); } } points = std::move(hullPoints); From bbdace7edf6160cd8523b82b2fb3f93efefeb549 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 7 May 2026 15:22:25 +0200 Subject: [PATCH 43/65] Fuse scaled SSP Pareto Minkowski sums Remove a redundant target-state check and add a fused scaled Minkowski operation so SSP action-front computation avoids creating temporary scaled frontiers. --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 4 +- .../modelchecker/cvar/helper/SspParetoFront.h | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index bf218366ef..6251b0db36 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -81,7 +81,7 @@ class SparseSspCvarParetoViHelper { FrontierLayer baseLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); ValueType const boundValue = storm::utility::convertNumber(costBound); for (auto state : reachableTargetStates) { - if (costBound >= 0 && preprocessingResult.targetStates[state]) { + if (costBound >= 0) { baseLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); } else { baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); @@ -117,7 +117,7 @@ class SparseSspCvarParetoViHelper { FrontierLayer const& predecessorLayer = getFrontierLayerForBound(predecessorBound, frontierWindow); for (auto const& transition : preprocessingResult.transitionMatrix.getRow(actionRow)) { - actionFront = actionFront.minkowskiSum(predecessorLayer[transition.getColumn()].scaled(transition.getValue())); + actionFront = actionFront.minkowskiSumScaled(predecessorLayer[transition.getColumn()], transition.getValue()); } return actionFront; } diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index ba147cbfd9..abd08bdefe 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -170,6 +170,34 @@ class SspParetoFront { return SspParetoFront(std::move(sumPoints)); } + SspParetoFront minkowskiSumScaled(SspParetoFront const& other, ValueType const& factor) const { + if (empty() || other.empty()) { + return SspParetoFront(); + } + if (storm::utility::isZero(factor)) { + return *this; + } + if (storm::utility::isOne(factor)) { + return minkowskiSum(other); + } + if (isSingleton()) { + return other.scaledTranslated(factor, points.front()); + } + if (other.isSingleton()) { + Point const& point = other.points.front(); + return translated(Point{factor * point.probability, factor * point.expectedCost}); + } + + container_type sumPoints; + sumPoints.reserve(points.size() * other.points.size()); + for (auto const& left : points) { + for (auto const& right : other.points) { + sumPoints.push_back(Point{left.probability + factor * right.probability, left.expectedCost + factor * right.expectedCost}); + } + } + return SspParetoFront(std::move(sumPoints)); + } + static SspParetoFront convexUnion(std::vector const& fronts) { SspParetoFront const* onlyNonEmptyFront = nullptr; container_type unionPoints; @@ -259,6 +287,29 @@ class SspParetoFront { return SspParetoFront(std::move(translatedPoints), AlreadyCanonicalTag{}); } + SspParetoFront scaledTranslated(ValueType const& factor, Point const& offset) const { + if (empty()) { + return SspParetoFront(); + } + if (storm::utility::isZero(factor)) { + return singleton(offset.probability, offset.expectedCost); + } + if (isSingleton()) { + Point const& point = points.front(); + return singleton(offset.probability + factor * point.probability, offset.expectedCost + factor * point.expectedCost); + } + + container_type scaledTranslatedPoints; + scaledTranslatedPoints.reserve(points.size()); + for (auto const& point : points) { + scaledTranslatedPoints.push_back(Point{offset.probability + factor * point.probability, offset.expectedCost + factor * point.expectedCost}); + } + if (storm::utility::isPositive(factor)) { + return SspParetoFront(std::move(scaledTranslatedPoints), AlreadyCanonicalTag{}); + } + return SspParetoFront(std::move(scaledTranslatedPoints)); + } + void canonicalize() { if (points.empty()) { return; From f35cdf18f1b96600371e7fb2d46eab97b0b0ed24 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 7 May 2026 15:30:45 +0200 Subject: [PATCH 44/65] Initialize SSP action fronts from first transition --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 6251b0db36..4b57a353b0 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -112,12 +112,19 @@ class SparseSspCvarParetoViHelper { ParetoFront computeActionFront(uint64_t actionRow, uint64_t costBound, FrontierWindow const& frontierWindow) const { uint64_t const actionCost = getChoiceCostBoundOffset(actionRow); - ParetoFront actionFront = ParetoFront::singleton(storm::utility::zero(), storm::utility::zero()); + ParetoFront actionFront; int64_t const predecessorBound = static_cast(costBound) - static_cast(actionCost); FrontierLayer const& predecessorLayer = getFrontierLayerForBound(predecessorBound, frontierWindow); + bool initialized = false; for (auto const& transition : preprocessingResult.transitionMatrix.getRow(actionRow)) { - actionFront = actionFront.minkowskiSumScaled(predecessorLayer[transition.getColumn()], transition.getValue()); + auto const& successorFront = predecessorLayer[transition.getColumn()]; + if (initialized) { + actionFront = actionFront.minkowskiSumScaled(successorFront, transition.getValue()); + } else { + actionFront = successorFront.scaled(transition.getValue()); + initialized = true; + } } return actionFront; } From 666a857d3b90019b8d4e572161bf2e8991519c03 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 7 May 2026 15:55:01 +0200 Subject: [PATCH 45/65] Build SSP action unions from point buffers --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 4b57a353b0..fde8300b2c 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -143,15 +143,26 @@ class SparseSspCvarParetoViHelper { continue; } - std::vector actionFronts; - actionFronts.reserve(endActionRow - firstActionRow); + ParetoFront firstNonEmptyActionFront; + typename ParetoFront::container_type actionFrontPoints; + bool hasNonEmptyActionFront = false; for (uint64_t actionRow = firstActionRow; actionRow < endActionRow; ++actionRow) { auto actionFront = computeActionFront(actionRow, costBound, frontierWindow); - if (!actionFront.empty()) { - actionFronts.push_back(std::move(actionFront)); + if (actionFront.empty()) { + continue; } + if (!hasNonEmptyActionFront) { + firstNonEmptyActionFront = std::move(actionFront); + hasNonEmptyActionFront = true; + continue; + } + if (actionFrontPoints.empty()) { + actionFrontPoints.reserve(firstNonEmptyActionFront.size() + actionFront.size()); + actionFrontPoints.insert(actionFrontPoints.end(), firstNonEmptyActionFront.begin(), firstNonEmptyActionFront.end()); + } + actionFrontPoints.insert(actionFrontPoints.end(), actionFront.begin(), actionFront.end()); } - currentLayer[state] = ParetoFront::convexUnion(actionFronts); + currentLayer[state] = actionFrontPoints.empty() ? std::move(firstNonEmptyActionFront) : ParetoFront(std::move(actionFrontPoints)); } return currentLayer; } From 436a43df468ccc42120b8288458e04b83057ea34 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Fri, 8 May 2026 13:14:37 +0200 Subject: [PATCH 46/65] Optimize SSP Pareto-front canonicalization Merge sorted action frontiers, canonicalize already-sorted point sets without resorting, and compact duplicate, dominated, and convex-redundant points in place. --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 21 +--- .../modelchecker/cvar/helper/SspParetoFront.h | 112 ++++++++++++------ 2 files changed, 80 insertions(+), 53 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index fde8300b2c..4b57a353b0 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -143,26 +143,15 @@ class SparseSspCvarParetoViHelper { continue; } - ParetoFront firstNonEmptyActionFront; - typename ParetoFront::container_type actionFrontPoints; - bool hasNonEmptyActionFront = false; + std::vector actionFronts; + actionFronts.reserve(endActionRow - firstActionRow); for (uint64_t actionRow = firstActionRow; actionRow < endActionRow; ++actionRow) { auto actionFront = computeActionFront(actionRow, costBound, frontierWindow); - if (actionFront.empty()) { - continue; + if (!actionFront.empty()) { + actionFronts.push_back(std::move(actionFront)); } - if (!hasNonEmptyActionFront) { - firstNonEmptyActionFront = std::move(actionFront); - hasNonEmptyActionFront = true; - continue; - } - if (actionFrontPoints.empty()) { - actionFrontPoints.reserve(firstNonEmptyActionFront.size() + actionFront.size()); - actionFrontPoints.insert(actionFrontPoints.end(), firstNonEmptyActionFront.begin(), firstNonEmptyActionFront.end()); - } - actionFrontPoints.insert(actionFrontPoints.end(), actionFront.begin(), actionFront.end()); } - currentLayer[state] = actionFrontPoints.empty() ? std::move(firstNonEmptyActionFront) : ParetoFront(std::move(actionFrontPoints)); + currentLayer[state] = ParetoFront::convexUnion(actionFronts); } return currentLayer; } diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index abd08bdefe..12916b2a04 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -53,11 +53,16 @@ class SspParetoFront { private: struct AlreadyCanonicalTag {}; + struct AlreadySortedTag {}; explicit SspParetoFront(container_type points, AlreadyCanonicalTag) : points(std::move(points)) { // Intentionally left empty. } + explicit SspParetoFront(container_type points, AlreadySortedTag) : points(std::move(points)) { + canonicalizeSorted(); + } + public: SspParetoFront() = default; @@ -200,7 +205,6 @@ class SspParetoFront { static SspParetoFront convexUnion(std::vector const& fronts) { SspParetoFront const* onlyNonEmptyFront = nullptr; - container_type unionPoints; std::size_t totalPointCount = 0; for (auto const& front : fronts) { if (front.empty()) { @@ -215,11 +219,7 @@ class SspParetoFront { if (onlyNonEmptyFront != nullptr && onlyNonEmptyFront->size() == totalPointCount) { return *onlyNonEmptyFront; } - unionPoints.reserve(totalPointCount); - for (auto const& front : fronts) { - unionPoints.insert(unionPoints.end(), front.begin(), front.end()); - } - return SspParetoFront(std::move(unionPoints)); + return SspParetoFront(mergeSortedFrontPoints(fronts, totalPointCount), AlreadySortedTag{}); } /*! @@ -315,8 +315,14 @@ class SspParetoFront { return; } sortPoints(); - removeDuplicateProbabilityPoints(); - removeDominatedPoints(); + canonicalizeSorted(); + } + + void canonicalizeSorted() { + if (points.empty()) { + return; + } + removeDuplicateAndDominatedPoints(); removeNonExtremeConvexPoints(); } @@ -329,52 +335,84 @@ class SspParetoFront { }); } - void removeDuplicateProbabilityPoints() { - container_type uniquePoints; - uniquePoints.reserve(points.size()); - for (auto const& point : points) { - if (!uniquePoints.empty() && uniquePoints.back().probability == point.probability) { - continue; + static bool pointLess(Point const& left, Point const& right) { + if (left.probability == right.probability) { + return left.expectedCost < right.expectedCost; + } + return left.probability < right.probability; + } + + static container_type mergeSortedFrontPoints(std::vector const& fronts, std::size_t totalPointCount) { + std::vector iterators; + std::vector ends; + iterators.reserve(fronts.size()); + ends.reserve(fronts.size()); + for (auto const& front : fronts) { + if (!front.empty()) { + iterators.push_back(front.begin()); + ends.push_back(front.end()); } - uniquePoints.push_back(point); } - points = std::move(uniquePoints); + + container_type mergedPoints; + mergedPoints.reserve(totalPointCount); + while (mergedPoints.size() < totalPointCount) { + std::size_t bestFront = iterators.size(); + for (std::size_t index = 0; index < iterators.size(); ++index) { + if (iterators[index] == ends[index]) { + continue; + } + if (bestFront == iterators.size() || pointLess(*iterators[index], *iterators[bestFront])) { + bestFront = index; + } + } + STORM_LOG_ASSERT(bestFront < iterators.size(), "Expected at least one non-exhausted SSP Pareto front during sorted merge."); + mergedPoints.push_back(*iterators[bestFront]); + ++iterators[bestFront]; + } + return mergedPoints; } - void removeDominatedPoints() { + void removeDuplicateAndDominatedPoints() { if (points.size() < 2) { return; } - container_type nonDominatedPoints; - nonDominatedPoints.reserve(points.size()); - ValueType bestExpectedCostSeenFromRight = points.back().expectedCost; - nonDominatedPoints.push_back(points.back()); - for (std::size_t index = points.size() - 1; index > 0; --index) { - Point const& point = points[index - 1]; - if (point.expectedCost < bestExpectedCostSeenFromRight) { - nonDominatedPoints.push_back(point); - bestExpectedCostSeenFromRight = point.expectedCost; + + std::size_t writeIndex = points.size(); + std::size_t index = points.size(); + bool hasBestExpectedCostSeenFromRight = false; + ValueType bestExpectedCostSeenFromRight{}; + while (index > 0) { + std::size_t const groupEnd = index; + ValueType const probability = points[groupEnd - 1].probability; + while (index > 0 && points[index - 1].probability == probability) { + --index; + } + + Point const& bestPointForProbability = points[index]; + if (!hasBestExpectedCostSeenFromRight || bestPointForProbability.expectedCost < bestExpectedCostSeenFromRight) { + --writeIndex; + points[writeIndex] = bestPointForProbability; + bestExpectedCostSeenFromRight = bestPointForProbability.expectedCost; + hasBestExpectedCostSeenFromRight = true; } } - std::reverse(nonDominatedPoints.begin(), nonDominatedPoints.end()); - points = std::move(nonDominatedPoints); + points.erase(points.begin(), points.begin() + static_cast(writeIndex)); } void removeNonExtremeConvexPoints() { if (points.size() < 3) { return; } - container_type hullPoints; - hullPoints.reserve(points.size()); - for (auto const& point : points) { - hullPoints.push_back(point); - while (hullPoints.size() >= 3 && - liesOnOrAboveSegment(hullPoints[hullPoints.size() - 3], hullPoints[hullPoints.size() - 2], hullPoints[hullPoints.size() - 1])) { - hullPoints[hullPoints.size() - 2] = hullPoints.back(); - hullPoints.pop_back(); + std::size_t hullSize = 0; + for (std::size_t index = 0, endIndex = points.size(); index < endIndex; ++index) { + points[hullSize++] = points[index]; + while (hullSize >= 3 && liesOnOrAboveSegment(points[hullSize - 3], points[hullSize - 2], points[hullSize - 1])) { + points[hullSize - 2] = points[hullSize - 1]; + --hullSize; } } - points = std::move(hullPoints); + points.resize(hullSize); STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), [](Point const& left, Point const& right) { return left.probability >= right.probability || left.expectedCost >= right.expectedCost; From cef009a5945554c4e78dbce0e3f0b2e0882ce0f5 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Fri, 8 May 2026 13:40:50 +0200 Subject: [PATCH 47/65] Reduce SSP Pareto VI allocation overhead --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 24 +++++++++++++------ .../modelchecker/cvar/helper/SspParetoFront.h | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 4b57a353b0..f11b3dcae7 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -80,10 +80,13 @@ class SparseSspCvarParetoViHelper { FrontierLayer createInitialFrontierLayer(int64_t costBound) const { FrontierLayer baseLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); ValueType const boundValue = storm::utility::convertNumber(costBound); - for (auto state : reachableTargetStates) { - if (costBound >= 0) { - baseLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); - } else { + if (costBound >= 0) { + ParetoFront const targetFront = createTargetFrontier(); + for (auto state : reachableTargetStates) { + baseLayer[state] = targetFront; + } + } else { + for (auto state : reachableTargetStates) { baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); } } @@ -131,9 +134,12 @@ class SparseSspCvarParetoViHelper { FrontierLayer computeFrontierLayerForCostBound(uint64_t costBound, FrontierWindow const& frontierWindow) const { FrontierLayer currentLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); + ParetoFront const targetFront = createTargetFrontier(); for (auto state : reachableTargetStates) { - currentLayer[state] = ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + currentLayer[state] = targetFront; } + + std::vector actionFronts; for (auto state : reachableNonTargetStates) { uint64_t const firstActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state]; @@ -143,7 +149,7 @@ class SparseSspCvarParetoViHelper { continue; } - std::vector actionFronts; + actionFronts.clear(); actionFronts.reserve(endActionRow - firstActionRow); for (uint64_t actionRow = firstActionRow; actionRow < endActionRow; ++actionRow) { auto actionFront = computeActionFront(actionRow, costBound, frontierWindow); @@ -151,7 +157,7 @@ class SparseSspCvarParetoViHelper { actionFronts.push_back(std::move(actionFront)); } } - currentLayer[state] = ParetoFront::convexUnion(actionFronts); + currentLayer[state] = ParetoFront::convexUnionDestructive(actionFronts); } return currentLayer; } @@ -190,6 +196,10 @@ class SparseSspCvarParetoViHelper { return getWindowIndex(costBound, preprocessingResult.maximalChoiceCost); } + static ParetoFront createTargetFrontier() { + return ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + } + static std::vector createChoiceCostOffsets(preprocessing::SspCvarPreprocessingResult const& preprocessingResult) { std::vector result; result.reserve(preprocessingResult.choiceCosts.size()); diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index 12916b2a04..37095f83cd 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "storm/utility/constants.h" @@ -222,6 +223,29 @@ class SspParetoFront { return SspParetoFront(mergeSortedFrontPoints(fronts, totalPointCount), AlreadySortedTag{}); } + static SspParetoFront convexUnion(std::vector&& fronts) { + return convexUnionDestructive(fronts); + } + + static SspParetoFront convexUnionDestructive(std::vector& fronts) { + SspParetoFront* onlyNonEmptyFront = nullptr; + std::size_t totalPointCount = 0; + for (auto& front : fronts) { + if (front.empty()) { + continue; + } + onlyNonEmptyFront = onlyNonEmptyFront == nullptr ? &front : onlyNonEmptyFront; + totalPointCount += front.size(); + } + if (totalPointCount == 0) { + return SspParetoFront(); + } + if (onlyNonEmptyFront != nullptr && onlyNonEmptyFront->size() == totalPointCount) { + return std::move(*onlyNonEmptyFront); + } + return SspParetoFront(mergeSortedFrontPoints(fronts, totalPointCount), AlreadySortedTag{}); + } + /*! * Returns the minimal continuation cost E on the lower boundary at the given probability p. * From ba94231b519f1d279405feea0d470b5a0d0b4916 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Fri, 8 May 2026 16:11:30 +0200 Subject: [PATCH 48/65] Optimize SSP Pareto frontier construction Use convex-chain Minkowski sums, reuse frontier-layer scratch storage, and switch sorted Pareto-front union merging to a heap-based merge. --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 24 ++-- .../modelchecker/cvar/helper/SspParetoFront.h | 120 ++++++++++++++---- 2 files changed, 108 insertions(+), 36 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index f11b3dcae7..3ba3ddb928 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -54,14 +54,15 @@ class SparseSspCvarParetoViHelper { FrontierWindow frontierWindow = initializeFrontierWindow(); std::optional bestCandidate = extractCvarCandidateFromInitialFrontier(frontierWindow[0][preprocessingResult.initialState], 0, queryInformation.alpha); + FrontierLayer currentLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); for (uint64_t costBound = 1; !bestCandidate.has_value() || storm::utility::convertNumber(costBound) <= bestCandidate.value(); ++costBound) { - auto currentLayer = computeFrontierLayerForCostBound(costBound, frontierWindow); + computeFrontierLayerForCostBound(costBound, frontierWindow, currentLayer); auto currentCandidate = extractCvarCandidateFromInitialFrontier(currentLayer[preprocessingResult.initialState], costBound, queryInformation.alpha); if (currentCandidate.has_value() && (!bestCandidate.has_value() || currentCandidate.value() < bestCandidate.value())) { bestCandidate = currentCandidate; } - writeFrontierLayerToWindow(costBound, std::move(currentLayer), frontierWindow); + swapFrontierLayerIntoWindow(costBound, currentLayer, frontierWindow); } STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, "CVaR SSP value iteration did not find a feasible candidate."); @@ -105,8 +106,8 @@ class SparseSspCvarParetoViHelper { return frontierWindow; } - static void writeFrontierLayerToWindow(int64_t costBound, FrontierLayer layer, FrontierWindow& frontierWindow) { - frontierWindow[getWindowIndex(costBound, frontierWindow.size())] = std::move(layer); + static void swapFrontierLayerIntoWindow(int64_t costBound, FrontierLayer& layer, FrontierWindow& frontierWindow) { + std::swap(frontierWindow[getWindowIndex(costBound, frontierWindow.size())], layer); } FrontierLayer const& getFrontierLayerForBound(int64_t costBound, FrontierWindow const& frontierWindow) const { @@ -132,8 +133,8 @@ class SparseSspCvarParetoViHelper { return actionFront; } - FrontierLayer computeFrontierLayerForCostBound(uint64_t costBound, FrontierWindow const& frontierWindow) const { - FrontierLayer currentLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); + void computeFrontierLayerForCostBound(uint64_t costBound, FrontierWindow const& frontierWindow, FrontierLayer& currentLayer) const { + prepareFrontierLayer(currentLayer); ParetoFront const targetFront = createTargetFrontier(); for (auto state : reachableTargetStates) { currentLayer[state] = targetFront; @@ -141,7 +142,6 @@ class SparseSspCvarParetoViHelper { std::vector actionFronts; for (auto state : reachableNonTargetStates) { - uint64_t const firstActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state]; uint64_t const endActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state + 1]; if (firstActionRow + 1 == endActionRow) { @@ -159,7 +159,6 @@ class SparseSspCvarParetoViHelper { } currentLayer[state] = ParetoFront::convexUnionDestructive(actionFronts); } - return currentLayer; } /*! @@ -200,6 +199,15 @@ class SparseSspCvarParetoViHelper { return ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); } + void prepareFrontierLayer(FrontierLayer& layer) const { + if (layer.size() != preprocessingResult.transitionMatrix.getRowGroupCount()) { + layer.resize(preprocessingResult.transitionMatrix.getRowGroupCount()); + } + for (auto& front : layer) { + front.clear(); + } + } + static std::vector createChoiceCostOffsets(preprocessing::SspCvarPreprocessingResult const& preprocessingResult) { std::vector result; result.reserve(preprocessingResult.choiceCosts.size()); diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index 37095f83cd..1406ee0825 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include #include #include @@ -166,14 +168,7 @@ class SspParetoFront { if (other.isSingleton()) { return translated(other.points.front()); } - container_type sumPoints; - sumPoints.reserve(points.size() * other.points.size()); - for (auto const& left : points) { - for (auto const& right : other.points) { - sumPoints.push_back(Point{left.probability + right.probability, left.expectedCost + right.expectedCost}); - } - } - return SspParetoFront(std::move(sumPoints)); + return minkowskiSumConvexChain(other, storm::utility::one()); } SspParetoFront minkowskiSumScaled(SspParetoFront const& other, ValueType const& factor) const { @@ -193,6 +188,9 @@ class SspParetoFront { Point const& point = other.points.front(); return translated(Point{factor * point.probability, factor * point.expectedCost}); } + if (storm::utility::isPositive(factor)) { + return minkowskiSumConvexChain(other, factor); + } container_type sumPoints; sumPoints.reserve(points.size() * other.points.size()); @@ -263,8 +261,8 @@ class SspParetoFront { return points.front().expectedCost; } - auto rightIt = std::lower_bound(points.begin(), points.end(), probability, - [](Point const& point, ValueType const& value) { return point.probability < value; }); + auto rightIt = + std::lower_bound(points.begin(), points.end(), probability, [](Point const& point, ValueType const& value) { return point.probability < value; }); STORM_LOG_ASSERT(rightIt != points.begin() && rightIt != points.end(), "Expected probability to lie inside the SSP Pareto-front range."); Point const& left = *(rightIt - 1); @@ -334,6 +332,47 @@ class SspParetoFront { return SspParetoFront(std::move(scaledTranslatedPoints)); } + SspParetoFront minkowskiSumConvexChain(SspParetoFront const& other, ValueType const& factor) const { + STORM_LOG_ASSERT(storm::utility::isPositive(factor), "Expected a positive scaling factor for convex-chain Minkowski sum."); + + container_type sumPoints; + sumPoints.reserve(points.size() + other.points.size() - 1); + + Point current{points.front().probability + factor * other.points.front().probability, + points.front().expectedCost + factor * other.points.front().expectedCost}; + sumPoints.push_back(current); + + std::size_t leftEdge = 0; + std::size_t rightEdge = 0; + while (leftEdge + 1 < points.size() || rightEdge + 1 < other.points.size()) { + if (rightEdge + 1 == other.points.size()) { + addEdge(current, points[leftEdge], points[leftEdge + 1]); + ++leftEdge; + } else if (leftEdge + 1 == points.size()) { + addScaledEdge(current, other.points[rightEdge], other.points[rightEdge + 1], factor); + ++rightEdge; + } else { + int_fast8_t const slopeComparison = + compareEdgeSlopes(points[leftEdge], points[leftEdge + 1], other.points[rightEdge], other.points[rightEdge + 1]); + if (slopeComparison < 0) { + addEdge(current, points[leftEdge], points[leftEdge + 1]); + ++leftEdge; + } else if (slopeComparison > 0) { + addScaledEdge(current, other.points[rightEdge], other.points[rightEdge + 1], factor); + ++rightEdge; + } else { + addEdge(current, points[leftEdge], points[leftEdge + 1]); + addScaledEdge(current, other.points[rightEdge], other.points[rightEdge + 1], factor); + ++leftEdge; + ++rightEdge; + } + } + sumPoints.push_back(current); + } + + return SspParetoFront(std::move(sumPoints), AlreadySortedTag{}); + } + void canonicalize() { if (points.empty()) { return; @@ -366,34 +405,59 @@ class SspParetoFront { return left.probability < right.probability; } + static void addEdge(Point& point, Point const& edgeStart, Point const& edgeEnd) { + point.probability += edgeEnd.probability - edgeStart.probability; + point.expectedCost += edgeEnd.expectedCost - edgeStart.expectedCost; + } + + static void addScaledEdge(Point& point, Point const& edgeStart, Point const& edgeEnd, ValueType const& factor) { + point.probability += factor * (edgeEnd.probability - edgeStart.probability); + point.expectedCost += factor * (edgeEnd.expectedCost - edgeStart.expectedCost); + } + + static int_fast8_t compareEdgeSlopes(Point const& leftStart, Point const& leftEnd, Point const& rightStart, Point const& rightEnd) { + ValueType const leftProbabilityDelta = leftEnd.probability - leftStart.probability; + ValueType const rightProbabilityDelta = rightEnd.probability - rightStart.probability; + STORM_LOG_ASSERT(leftProbabilityDelta > 0 && rightProbabilityDelta > 0, "Expected strictly increasing probabilities in SSP Pareto front edges."); + + ValueType const leftScaledCostDelta = (leftEnd.expectedCost - leftStart.expectedCost) * rightProbabilityDelta; + ValueType const rightScaledCostDelta = (rightEnd.expectedCost - rightStart.expectedCost) * leftProbabilityDelta; + if (leftScaledCostDelta < rightScaledCostDelta) { + return -1; + } + if (rightScaledCostDelta < leftScaledCostDelta) { + return 1; + } + return 0; + } + static container_type mergeSortedFrontPoints(std::vector const& fronts, std::size_t totalPointCount) { - std::vector iterators; - std::vector ends; - iterators.reserve(fronts.size()); - ends.reserve(fronts.size()); + struct MergeCursor { + const_iterator iterator; + const_iterator end; + }; + auto cursorGreater = [](MergeCursor const& left, MergeCursor const& right) { return pointLess(*right.iterator, *left.iterator); }; + std::vector initialCursors; + initialCursors.reserve(fronts.size()); for (auto const& front : fronts) { if (!front.empty()) { - iterators.push_back(front.begin()); - ends.push_back(front.end()); + initialCursors.push_back(MergeCursor{front.begin(), front.end()}); } } + std::priority_queue, decltype(cursorGreater)> cursors(cursorGreater, std::move(initialCursors)); container_type mergedPoints; mergedPoints.reserve(totalPointCount); - while (mergedPoints.size() < totalPointCount) { - std::size_t bestFront = iterators.size(); - for (std::size_t index = 0; index < iterators.size(); ++index) { - if (iterators[index] == ends[index]) { - continue; - } - if (bestFront == iterators.size() || pointLess(*iterators[index], *iterators[bestFront])) { - bestFront = index; - } + while (!cursors.empty()) { + MergeCursor cursor = cursors.top(); + cursors.pop(); + mergedPoints.push_back(*cursor.iterator); + ++cursor.iterator; + if (cursor.iterator != cursor.end) { + cursors.push(cursor); } - STORM_LOG_ASSERT(bestFront < iterators.size(), "Expected at least one non-exhausted SSP Pareto front during sorted merge."); - mergedPoints.push_back(*iterators[bestFront]); - ++iterators[bestFront]; } + STORM_LOG_ASSERT(mergedPoints.size() == totalPointCount, "Unexpected number of points produced by sorted SSP Pareto-front merge."); return mergedPoints; } From ca393dcb8cafb1a81f7c37dcabae71b8307c3ed0 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 11 May 2026 14:49:22 +0200 Subject: [PATCH 49/65] Extract SSP Pareto value-iteration operator Move SSP Pareto layer application into a dedicated operator with backend-style action reduction, keeping the CVaR helper focused on frontier-window initialization, cost-bound iteration, and candidate extraction. --- .../cvar/helper/SparseSspCvarParetoViHelper.h | 134 ++---------- .../helper/SspParetoValueIterationOperator.h | 203 ++++++++++++++++++ 2 files changed, 217 insertions(+), 120 deletions(-) create mode 100644 src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 3ba3ddb928..0db240fca8 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -10,7 +10,7 @@ #include "storm/exceptions/UnexpectedException.h" #include "storm/modelchecker/cvar/CvarComputationResult.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/helper/SspParetoFront.h" +#include "storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h" #include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" #include "storm/utility/constants.h" #include "storm/utility/macros.h" @@ -35,15 +35,12 @@ template class SparseSspCvarParetoViHelper { public: using ParetoFront = SspParetoFront; + using ParetoViOperator = SspParetoValueIterationOperator; using FrontierLayer = std::vector; using FrontierWindow = std::vector; SparseSspCvarParetoViHelper(CvarQueryInformation const& queryInformation, preprocessing::SspCvarPreprocessingResult const& preprocessingResult) - : queryInformation(queryInformation), - preprocessingResult(preprocessingResult), - choiceCostOffsets(createChoiceCostOffsets(preprocessingResult)), - reachableTargetStates(collectReachableStates(preprocessingResult, true)), - reachableNonTargetStates(collectReachableStates(preprocessingResult, false)) { + : queryInformation(queryInformation), preprocessingResult(preprocessingResult), paretoViOperator(preprocessingResult) { // Intentionally left empty. } @@ -54,10 +51,10 @@ class SparseSspCvarParetoViHelper { FrontierWindow frontierWindow = initializeFrontierWindow(); std::optional bestCandidate = extractCvarCandidateFromInitialFrontier(frontierWindow[0][preprocessingResult.initialState], 0, queryInformation.alpha); - FrontierLayer currentLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); + FrontierLayer currentLayer(paretoViOperator.getStateCount()); for (uint64_t costBound = 1; !bestCandidate.has_value() || storm::utility::convertNumber(costBound) <= bestCandidate.value(); ++costBound) { - computeFrontierLayerForCostBound(costBound, frontierWindow, currentLayer); + paretoViOperator.apply(costBound, frontierWindow, currentLayer); auto currentCandidate = extractCvarCandidateFromInitialFrontier(currentLayer[preprocessingResult.initialState], costBound, queryInformation.alpha); if (currentCandidate.has_value() && (!bestCandidate.has_value() || currentCandidate.value() < bestCandidate.value())) { bestCandidate = currentCandidate; @@ -79,19 +76,19 @@ class SparseSspCvarParetoViHelper { * (0, e*(s) - n), where e*(s) is the minimal expected cost-to-go from s. */ FrontierLayer createInitialFrontierLayer(int64_t costBound) const { - FrontierLayer baseLayer(preprocessingResult.transitionMatrix.getRowGroupCount()); + FrontierLayer baseLayer(paretoViOperator.getStateCount()); ValueType const boundValue = storm::utility::convertNumber(costBound); if (costBound >= 0) { - ParetoFront const targetFront = createTargetFrontier(); - for (auto state : reachableTargetStates) { + ParetoFront const targetFront = ParetoViOperator::createTargetFrontier(); + for (auto state : paretoViOperator.getReachableTargetStates()) { baseLayer[state] = targetFront; } } else { - for (auto state : reachableTargetStates) { + for (auto state : paretoViOperator.getReachableTargetStates()) { baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); } } - for (auto state : reachableNonTargetStates) { + for (auto state : paretoViOperator.getReachableNonTargetStates()) { baseLayer[state] = ParetoFront::singleton(storm::utility::zero(), preprocessingResult.expectedCostsToGoal[state] - boundValue); } return baseLayer; @@ -99,66 +96,15 @@ class SparseSspCvarParetoViHelper { FrontierWindow initializeFrontierWindow() const { STORM_LOG_ASSERT(preprocessingResult.maximalChoiceCost > 0, "Expected a strictly positive maximal choice cost."); - FrontierWindow frontierWindow(preprocessingResult.maximalChoiceCost, FrontierLayer(preprocessingResult.transitionMatrix.getRowGroupCount())); + FrontierWindow frontierWindow(preprocessingResult.maximalChoiceCost, FrontierLayer(paretoViOperator.getStateCount())); for (int64_t costBound = 1 - static_cast(preprocessingResult.maximalChoiceCost); costBound <= 0; ++costBound) { - frontierWindow[getWindowIndex(costBound)] = createInitialFrontierLayer(costBound); + frontierWindow[ParetoViOperator::getWindowIndex(costBound, frontierWindow.size())] = createInitialFrontierLayer(costBound); } return frontierWindow; } static void swapFrontierLayerIntoWindow(int64_t costBound, FrontierLayer& layer, FrontierWindow& frontierWindow) { - std::swap(frontierWindow[getWindowIndex(costBound, frontierWindow.size())], layer); - } - - FrontierLayer const& getFrontierLayerForBound(int64_t costBound, FrontierWindow const& frontierWindow) const { - return frontierWindow[getWindowIndex(costBound, frontierWindow.size())]; - } - - ParetoFront computeActionFront(uint64_t actionRow, uint64_t costBound, FrontierWindow const& frontierWindow) const { - uint64_t const actionCost = getChoiceCostBoundOffset(actionRow); - ParetoFront actionFront; - int64_t const predecessorBound = static_cast(costBound) - static_cast(actionCost); - FrontierLayer const& predecessorLayer = getFrontierLayerForBound(predecessorBound, frontierWindow); - - bool initialized = false; - for (auto const& transition : preprocessingResult.transitionMatrix.getRow(actionRow)) { - auto const& successorFront = predecessorLayer[transition.getColumn()]; - if (initialized) { - actionFront = actionFront.minkowskiSumScaled(successorFront, transition.getValue()); - } else { - actionFront = successorFront.scaled(transition.getValue()); - initialized = true; - } - } - return actionFront; - } - - void computeFrontierLayerForCostBound(uint64_t costBound, FrontierWindow const& frontierWindow, FrontierLayer& currentLayer) const { - prepareFrontierLayer(currentLayer); - ParetoFront const targetFront = createTargetFrontier(); - for (auto state : reachableTargetStates) { - currentLayer[state] = targetFront; - } - - std::vector actionFronts; - for (auto state : reachableNonTargetStates) { - uint64_t const firstActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state]; - uint64_t const endActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state + 1]; - if (firstActionRow + 1 == endActionRow) { - currentLayer[state] = computeActionFront(firstActionRow, costBound, frontierWindow); - continue; - } - - actionFronts.clear(); - actionFronts.reserve(endActionRow - firstActionRow); - for (uint64_t actionRow = firstActionRow; actionRow < endActionRow; ++actionRow) { - auto actionFront = computeActionFront(actionRow, costBound, frontierWindow); - if (!actionFront.empty()) { - actionFronts.push_back(std::move(actionFront)); - } - } - currentLayer[state] = ParetoFront::convexUnionDestructive(actionFronts); - } + std::swap(frontierWindow[ParetoViOperator::getWindowIndex(costBound, frontierWindow.size())], layer); } /*! @@ -177,61 +123,9 @@ class SparseSspCvarParetoViHelper { return storm::utility::convertNumber(costBound) + continuationCost.value() / storm::utility::convertNumber(alpha); } - uint64_t getChoiceCostBoundOffset(uint64_t actionRow) const { - return choiceCostOffsets[actionRow]; - } - - static std::size_t getWindowIndex(int64_t costBound, std::size_t windowSize) { - STORM_LOG_ASSERT(windowSize > 0, "Expected a non-empty SSP frontier window."); - int64_t const signedWindowSize = static_cast(windowSize); - int64_t index = costBound % signedWindowSize; - if (index < 0) { - index += signedWindowSize; - } - return static_cast(index); - } - - std::size_t getWindowIndex(int64_t costBound) const { - return getWindowIndex(costBound, preprocessingResult.maximalChoiceCost); - } - - static ParetoFront createTargetFrontier() { - return ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); - } - - void prepareFrontierLayer(FrontierLayer& layer) const { - if (layer.size() != preprocessingResult.transitionMatrix.getRowGroupCount()) { - layer.resize(preprocessingResult.transitionMatrix.getRowGroupCount()); - } - for (auto& front : layer) { - front.clear(); - } - } - - static std::vector createChoiceCostOffsets(preprocessing::SspCvarPreprocessingResult const& preprocessingResult) { - std::vector result; - result.reserve(preprocessingResult.choiceCosts.size()); - for (auto const& choiceCost : preprocessingResult.choiceCosts) { - result.push_back(storm::utility::convertNumber(choiceCost)); - } - return result; - } - - static std::vector collectReachableStates(preprocessing::SspCvarPreprocessingResult const& preprocessingResult, bool targetStates) { - std::vector result; - for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { - if (preprocessingResult.reachableStates[state] && preprocessingResult.targetStates[state] == targetStates) { - result.push_back(state); - } - } - return result; - } - CvarQueryInformation const& queryInformation; preprocessing::SspCvarPreprocessingResult const& preprocessingResult; - std::vector choiceCostOffsets; - std::vector reachableTargetStates; - std::vector reachableNonTargetStates; + ParetoViOperator paretoViOperator; }; } // namespace cvar diff --git a/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h b/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h new file mode 100644 index 0000000000..235927a21a --- /dev/null +++ b/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h @@ -0,0 +1,203 @@ +#pragma once + +#include +#include +#include +#include + +#include "storm/modelchecker/cvar/helper/SspParetoFront.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" +#include "storm/utility/constants.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +/*! + * Applies one cost-bound layer update for SSP CVaR Pareto-front value iteration. + * + * This class follows the role of Storm's generic value-iteration operators, but keeps the SSP-specific + * cost-window semantics explicit: each action row reads from the predecessor layer determined by the current + * cost bound minus that action's integer cost. + */ +template +class SspParetoValueIterationOperator { + public: + using ParetoFront = SspParetoFront; + using FrontierLayer = std::vector; + using FrontierWindow = std::vector; + + explicit SspParetoValueIterationOperator(preprocessing::SspCvarPreprocessingResult const& preprocessingResult) + : preprocessingResult(preprocessingResult), + choiceCostOffsets(createChoiceCostOffsets(preprocessingResult)), + reachableTargetStates(collectReachableStates(preprocessingResult, true)), + reachableNonTargetStates(collectReachableStates(preprocessingResult, false)) { + } + + void apply(uint64_t costBound, FrontierWindow const& frontierWindow, FrontierLayer& outputLayer) const { + prepareOutputLayer(outputLayer); + + ParetoFront const targetFront = createTargetFrontier(); + for (auto state : reachableTargetStates) { + outputLayer[state] = targetFront; + } + + std::vector actionFrontStorage; + ActionFrontReducer actionFrontReducer(actionFrontStorage); + for (auto state : reachableNonTargetStates) { + applyRowGroup(state, costBound, frontierWindow, outputLayer, actionFrontReducer); + } + } + + std::size_t getStateCount() const { + return preprocessingResult.transitionMatrix.getRowGroupCount(); + } + + std::vector const& getReachableTargetStates() const { + return reachableTargetStates; + } + + std::vector const& getReachableNonTargetStates() const { + return reachableNonTargetStates; + } + + static std::size_t getWindowIndex(int64_t costBound, std::size_t windowSize) { + STORM_LOG_ASSERT(windowSize > 0, "Expected a non-empty SSP frontier window."); + int64_t const signedWindowSize = static_cast(windowSize); + int64_t index = costBound % signedWindowSize; + if (index < 0) { + index += signedWindowSize; + } + return static_cast(index); + } + + static ParetoFront createTargetFrontier() { + return ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + } + + private: + class ActionFrontReducer { + public: + explicit ActionFrontReducer(std::vector& actionFrontStorage) : actionFrontStorage(actionFrontStorage) {} + + void reset(std::size_t rowCount) { + actionFrontStorage.clear(); + rowCountHint = rowCount; + firstNonEmptyActionFront.clear(); + hasNonEmptyActionFront = false; + usesActionFrontStorage = false; + } + + void add(ParetoFront&& actionFront) { + addActionFront(std::move(actionFront)); + } + + void reduceInto(ParetoFront& outputFront) { + if (!hasNonEmptyActionFront) { + outputFront.clear(); + } else if (!usesActionFrontStorage) { + outputFront = std::move(firstNonEmptyActionFront); + } else { + outputFront = ParetoFront::convexUnionDestructive(actionFrontStorage); + } + } + + private: + void addActionFront(ParetoFront&& actionFront) { + if (actionFront.empty()) { + return; + } + if (!hasNonEmptyActionFront) { + firstNonEmptyActionFront = std::move(actionFront); + hasNonEmptyActionFront = true; + return; + } + if (!usesActionFrontStorage) { + actionFrontStorage.reserve(rowCountHint); + actionFrontStorage.push_back(std::move(firstNonEmptyActionFront)); + usesActionFrontStorage = true; + } + actionFrontStorage.push_back(std::move(actionFront)); + } + + std::vector& actionFrontStorage; + ParetoFront firstNonEmptyActionFront; + std::size_t rowCountHint{0}; + bool hasNonEmptyActionFront{false}; + bool usesActionFrontStorage{false}; + }; + + void prepareOutputLayer(FrontierLayer& layer) const { + if (layer.size() != getStateCount()) { + layer.resize(getStateCount()); + } + for (auto& front : layer) { + front.clear(); + } + } + + void applyRowGroup(uint64_t state, uint64_t costBound, FrontierWindow const& frontierWindow, FrontierLayer& outputLayer, + ActionFrontReducer& actionFrontReducer) const { + uint64_t const firstActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state]; + uint64_t const endActionRow = preprocessingResult.transitionMatrix.getRowGroupIndices()[state + 1]; + STORM_LOG_ASSERT(firstActionRow < endActionRow, "Expected at least one action row in each reachable non-target SSP state."); + + actionFrontReducer.reset(endActionRow - firstActionRow); + for (uint64_t actionRow = firstActionRow; actionRow < endActionRow; ++actionRow) { + actionFrontReducer.add(computeActionFront(actionRow, costBound, frontierWindow)); + } + actionFrontReducer.reduceInto(outputLayer[state]); + } + + ParetoFront computeActionFront(uint64_t actionRow, uint64_t costBound, FrontierWindow const& frontierWindow) const { + uint64_t const actionCost = choiceCostOffsets[actionRow]; + ParetoFront actionFront; + int64_t const predecessorBound = static_cast(costBound) - static_cast(actionCost); + FrontierLayer const& predecessorLayer = getFrontierLayerForBound(predecessorBound, frontierWindow); + + bool initialized = false; + for (auto const& transition : preprocessingResult.transitionMatrix.getRow(actionRow)) { + auto const& successorFront = predecessorLayer[transition.getColumn()]; + if (initialized) { + actionFront = actionFront.minkowskiSumScaled(successorFront, transition.getValue()); + } else { + actionFront = successorFront.scaled(transition.getValue()); + initialized = true; + } + } + return actionFront; + } + + FrontierLayer const& getFrontierLayerForBound(int64_t costBound, FrontierWindow const& frontierWindow) const { + return frontierWindow[getWindowIndex(costBound, frontierWindow.size())]; + } + + static std::vector createChoiceCostOffsets(preprocessing::SspCvarPreprocessingResult const& preprocessingResult) { + std::vector result; + result.reserve(preprocessingResult.choiceCosts.size()); + for (auto const& choiceCost : preprocessingResult.choiceCosts) { + result.push_back(storm::utility::convertNumber(choiceCost)); + } + return result; + } + + static std::vector collectReachableStates(preprocessing::SspCvarPreprocessingResult const& preprocessingResult, bool targetStates) { + std::vector result; + for (uint64_t state = 0; state < preprocessingResult.transitionMatrix.getRowGroupCount(); ++state) { + if (preprocessingResult.reachableStates[state] && preprocessingResult.targetStates[state] == targetStates) { + result.push_back(state); + } + } + return result; + } + + preprocessing::SspCvarPreprocessingResult const& preprocessingResult; + std::vector choiceCostOffsets; + std::vector reachableTargetStates; + std::vector reachableNonTargetStates; +}; + +} // namespace cvar +} // namespace modelchecker +} // namespace storm From 30bab6bf1c18bd80fa2ad4f40e115b3330a9e8c6 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Mon, 11 May 2026 15:47:21 +0200 Subject: [PATCH 50/65] Add focused SSP Pareto VI tests --- .../helper/SspParetoValueIterationOperator.h | 3 +- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h b/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h index 235927a21a..fba620a715 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h @@ -32,8 +32,7 @@ class SspParetoValueIterationOperator { : preprocessingResult(preprocessingResult), choiceCostOffsets(createChoiceCostOffsets(preprocessingResult)), reachableTargetStates(collectReachableStates(preprocessingResult, true)), - reachableNonTargetStates(collectReachableStates(preprocessingResult, false)) { - } + reachableNonTargetStates(collectReachableStates(preprocessingResult, false)) {} void apply(uint64_t costBound, FrontierWindow const& frontierWindow, FrontierLayer& outputLayer) const { prepareOutputLayer(outputLayer); diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 6534ff91f1..4c664d776c 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -12,9 +12,13 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/modelchecker/CheckTask.h" #include "storm/modelchecker/cvar/CvarMethod.h" +#include "storm/modelchecker/cvar/helper/SspParetoFront.h" +#include "storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" #include "storm/models/sparse/Mdp.h" +#include "storm/storage/SparseMatrix.h" #include @@ -93,6 +97,82 @@ std::vector getChoiceSuccessors(std::shared_ptr const& front, std::vector> const& expectedPoints) { + auto const& actualPoints = front.getPoints(); + ASSERT_EQ(expectedPoints.size(), actualPoints.size()); + for (std::size_t index = 0; index < expectedPoints.size(); ++index) { + EXPECT_NEAR(expectedPoints[index].first, actualPoints[index].probability, 1e-10); + EXPECT_NEAR(expectedPoints[index].second, actualPoints[index].expectedCost, 1e-10); + } +} + +storm::modelchecker::cvar::preprocessing::SspCvarPreprocessingResult buildTinySspPreprocessingResult() { + storm::storage::SparseMatrixBuilder builder(4, 3, 4, true, true, 3); + builder.newRowGroup(0); + builder.addNextValue(0, 1, 1.0); + builder.addNextValue(1, 1, 1.0); + builder.newRowGroup(2); + builder.addNextValue(2, 2, 1.0); + builder.newRowGroup(3); + builder.addNextValue(3, 2, 1.0); + + storm::modelchecker::cvar::preprocessing::SspCvarPreprocessingResult result; + result.rewardModelName = "cost"; + result.initialState = 0; + result.targetStates = storm::storage::BitVector(3, false); + result.targetStates.set(2, true); + result.reachableStates = storm::storage::BitVector(3, true); + result.liftedStateRewardsToChoiceCosts = true; + result.normalizedTargetStatesToAbsorbing = true; + result.maximalChoiceCost = 2; + result.choiceCosts = {1.0, 2.0, 1.0, 0.0}; + result.expectedCostsToGoal = {0.0, 0.0, 0.0}; + result.transitionMatrix = builder.build(); + return result; +} + +TEST(CvarSspParetoFrontTest, CanonicalizesDuplicateDominatedAndConvexRedundantPoints) { + using ParetoFront = storm::modelchecker::cvar::SspParetoFront; + + ParetoFront front({{0.4, 5.0}, {0.2, 4.0}, {0.2, 3.0}, {0.6, 6.0}, {0.8, 9.0}, {0.5, 7.0}}); + + expectParetoFrontPoints(front, {{0.2, 3.0}, {0.6, 6.0}, {0.8, 9.0}}); +} + +TEST(CvarSspParetoFrontTest, ScaledMinkowskiSumMergesConvexChains) { + using ParetoFront = storm::modelchecker::cvar::SspParetoFront; + + ParetoFront left({{0.0, 0.0}, {0.25, 1.0}, {0.5, 3.0}}); + ParetoFront right({{0.0, 0.0}, {0.25, 0.5}, {0.5, 2.0}}); + + auto result = left.minkowskiSumScaled(right, 0.5); + + expectParetoFrontPoints(result, {{0.0, 0.0}, {0.125, 0.25}, {0.375, 1.25}, {0.5, 2.0}, {0.75, 4.0}}); +} + +TEST(CvarSspParetoValueIterationOperatorTest, AppliesActionCostsAndUnionsActionFronts) { + using ParetoFront = storm::modelchecker::cvar::SspParetoFront; + using ParetoViOperator = storm::modelchecker::cvar::SspParetoValueIterationOperator; + using FrontierLayer = std::vector; + using FrontierWindow = std::vector; + + auto preprocessingResult = buildTinySspPreprocessingResult(); + ParetoViOperator paretoViOperator(preprocessingResult); + FrontierWindow frontierWindow(3, FrontierLayer(3)); + for (auto& layer : frontierWindow) { + layer[2] = ParetoViOperator::createTargetFrontier(); + } + frontierWindow[0][1] = ParetoFront::singleton(0.8, 4.0); + frontierWindow[1][1] = ParetoFront::singleton(0.5, 1.0); + + FrontierLayer outputLayer; + paretoViOperator.apply(2, frontierWindow, outputLayer); + + expectParetoFrontPoints(outputLayer[0], {{0.5, 1.0}, {0.8, 4.0}}); + expectParetoFrontPoints(outputLayer[1], {{1.0, 0.0}}); + expectParetoFrontPoints(outputLayer[2], {{1.0, 0.0}}); +} + TEST(CvarQueryTest, SimpleMdp) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; From aa57589247094cf47a61369fa5d15fd485e54609 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:37:18 +0200 Subject: [PATCH 51/65] Refactor CVaR model-checking dispatch --- .../modelchecker/cvar/CvarClassification.h | 27 ++-------- src/storm/modelchecker/cvar/CvarMethod.cpp | 2 +- .../modelchecker/cvar/CvarModelChecking.cpp | 53 +++++++++++++++++++ .../modelchecker/cvar/CvarModelChecking.h | 31 +++++++++++ .../modelchecker/cvar/CvarQueryInformation.h | 3 +- .../cvar/helper/SparseCvarComputationHelper.h | 4 +- .../cvar/helper/SparseSspCvarParetoViHelper.h | 2 +- .../SparseWeightedReachabilityCvarLpHelper.h | 5 +- .../prctl/SparseMdpPrctlModelChecker.cpp | 26 ++------- 9 files changed, 102 insertions(+), 51 deletions(-) create mode 100644 src/storm/modelchecker/cvar/CvarModelChecking.cpp create mode 100644 src/storm/modelchecker/cvar/CvarModelChecking.h diff --git a/src/storm/modelchecker/cvar/CvarClassification.h b/src/storm/modelchecker/cvar/CvarClassification.h index acbc1cf62f..bf7d5a68f0 100644 --- a/src/storm/modelchecker/cvar/CvarClassification.h +++ b/src/storm/modelchecker/cvar/CvarClassification.h @@ -10,35 +10,16 @@ namespace storm { namespace modelchecker { namespace cvar { -/*! - * Classifies the embedded CVaR query at the formula level. - * - * This is intentionally separate from the concrete backend selection below: - * multiple concrete backends may share the same surface query syntax. - */ -enum class CvarQueryKind { ReachabilityReward }; - /*! * Selects the concrete CVaR backend induced by a query on a given * model and reward structure. * * Weighted reachability is the currently implemented LP-based terminal-reward - * setting. SSP will be used by the future value-iteration implementation for - * accumulated state-action costs until reaching the goal. + * setting. SSP uses Pareto value iteration for accumulated costs until + * reaching the goal. */ enum class CvarBackendKind { WeightedReachability, Ssp }; -/*! - * Determines the formula-level CVaR query kind. - * - * The current front-end only admits reachability reward CVaR queries, but this - * explicit classification provides the extension point for future CVaR query - * families. - */ -inline CvarQueryKind classifyCvarQuery(CvarQueryInformation const&) { - return CvarQueryKind::ReachabilityReward; -} - /*! * Selects the concrete CVaR backend to use. * @@ -48,8 +29,8 @@ inline CvarQueryKind classifyCvarQuery(CvarQueryInformation const&) { * weighted-reachability path until SSP preprocessing is introduced. */ template -CvarBackendKind selectCvarBackend(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, CvarQueryKind, - storm::storage::BitVector const&, CvarMethod method) { +CvarBackendKind selectCvarBackend(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const&, + CvarMethod method) { std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; auto const& rewardModel = model.getRewardModel(rewardModelName); if (rewardModelName.empty()) { diff --git a/src/storm/modelchecker/cvar/CvarMethod.cpp b/src/storm/modelchecker/cvar/CvarMethod.cpp index 19ab82b457..7fd0f06a1d 100644 --- a/src/storm/modelchecker/cvar/CvarMethod.cpp +++ b/src/storm/modelchecker/cvar/CvarMethod.cpp @@ -11,7 +11,7 @@ std::string toString(CvarMethod method) { case CvarMethod::WeightedReachability: return "weighted-reachability"; case CvarMethod::SspParetoVi: - return "ssp-pareto-vi"; + return "ssp-vi"; } return "unknown"; } diff --git a/src/storm/modelchecker/cvar/CvarModelChecking.cpp b/src/storm/modelchecker/cvar/CvarModelChecking.cpp new file mode 100644 index 0000000000..f067cdab2c --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarModelChecking.cpp @@ -0,0 +1,53 @@ +#include "storm/modelchecker/cvar/CvarModelChecking.h" + +#include "storm/adapters/RationalNumberAdapter.h" +#include "storm/environment/Environment.h" +#include "storm/exceptions/InvalidOperationException.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h" +#include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" +#include "storm/models/sparse/Mdp.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +template +std::unique_ptr performCvarModelChecking( + Environment const& env, SparseMdpModelType const& model, + CheckTask> const& checkTask, + std::function const& formulaChecker) { + using ValueType = typename SparseMdpModelType::ValueType; + using SolutionType = storm::IntervalBaseType; + + STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, + "Computing CVaR is only supported for the initial states of a model."); + STORM_LOG_THROW(model.getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, + "CVaR is not supported on models with multiple initial states."); + + auto cvarQueryInformation = extractCvarQueryInformation(checkTask.getFormula()); + auto targetStates = formulaChecker(*cvarQueryInformation.targetFormula); + + SparseCvarComputationHelper cvarHelper(model, cvarQueryInformation, targetStates); + auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); + + std::unique_ptr result(new ExplicitQuantitativeCheckResult(*model.getInitialStates().begin(), std::move(cvarResult.value))); + if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { + result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); + } + return result; +} + +template std::unique_ptr performCvarModelChecking>( + Environment const& env, storm::models::sparse::Mdp const& model, CheckTask const& checkTask, + std::function const& formulaChecker); + +template std::unique_ptr performCvarModelChecking>( + Environment const& env, storm::models::sparse::Mdp const& model, + CheckTask const& checkTask, + std::function const& formulaChecker); + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/CvarModelChecking.h b/src/storm/modelchecker/cvar/CvarModelChecking.h new file mode 100644 index 0000000000..9f025f6487 --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarModelChecking.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "storm/adapters/IntervalForward.h" +#include "storm/logic/CvarFormula.h" +#include "storm/modelchecker/CheckTask.h" +#include "storm/modelchecker/results/CheckResult.h" +#include "storm/storage/BitVector.h" + +namespace storm { + +class Environment; + +namespace logic { +class Formula; +} + +namespace modelchecker { +namespace cvar { + +template +std::unique_ptr performCvarModelChecking( + Environment const& env, SparseMdpModelType const& model, + CheckTask> const& checkTask, + std::function const& formulaChecker); + +} // namespace cvar +} // namespace modelchecker +} // namespace storm diff --git a/src/storm/modelchecker/cvar/CvarQueryInformation.h b/src/storm/modelchecker/cvar/CvarQueryInformation.h index 8157317f99..1a2a8fc896 100644 --- a/src/storm/modelchecker/cvar/CvarQueryInformation.h +++ b/src/storm/modelchecker/cvar/CvarQueryInformation.h @@ -3,6 +3,7 @@ #include #include +#include "storm/adapters/RationalNumberAdapter.h" #include "storm/logic/CvarFormula.h" #include "storm/solver/OptimizationDirection.h" @@ -11,7 +12,7 @@ namespace modelchecker { namespace cvar { struct CvarQueryInformation { - double alpha; + storm::RationalNumber alpha; storm::solver::OptimizationDirection optimizationDirection; boost::optional rewardModelName; std::shared_ptr targetFormula; diff --git a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h index 7cbe80b418..27eb3437a5 100644 --- a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h @@ -1,6 +1,7 @@ #pragma once #include "storm/environment/Environment.h" +#include "storm/environment/modelchecker/ModelCheckerEnvironment.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/exceptions/UnexpectedException.h" #include "storm/modelchecker/cvar/CvarClassification.h" @@ -27,8 +28,7 @@ class SparseCvarComputationHelper { } CvarComputationResult computeCvar(Environment const& env, bool produceScheduler = false) const { - auto queryKind = classifyCvarQuery(queryInformation); - auto backendKind = selectCvarBackend(model, queryInformation, queryKind, targetStates, env.modelchecker().cvar().getMethod()); + auto backendKind = selectCvarBackend(model, queryInformation, targetStates, env.modelchecker().cvar().getMethod()); switch (backendKind) { case CvarBackendKind::WeightedReachability: { diff --git a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h index 0db240fca8..ffb0471005 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h @@ -114,7 +114,7 @@ class SparseSspCvarParetoViHelper { * candidate n + E / t, where E is the minimal continuation cost on the frontier at probability 1 - t. */ static std::optional extractCvarCandidateFromInitialFrontier(SspParetoFront const& initialFrontier, uint64_t costBound, - double alpha) { + storm::RationalNumber const& alpha) { ValueType const targetProbability = storm::utility::one() - storm::utility::convertNumber(alpha); auto continuationCost = initialFrontier.getMinimalContinuationCostAtProbability(targetProbability); if (!continuationCost.has_value()) { diff --git a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h index 9ccefd90df..db87d93d65 100644 --- a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h @@ -6,6 +6,7 @@ #include #include +#include "storm/adapters/RationalNumberAdapter.h" #include "storm/environment/Environment.h" #include "storm/environment/solver/MinMaxSolverEnvironment.h" #include "storm/environment/solver/SolverEnvironment.h" @@ -45,7 +46,7 @@ struct CvarRewardBucket { template struct WeightedReachabilityCvarLpData { - double alpha; + storm::RationalNumber alpha; storm::solver::OptimizationDirection optimizationDirection; uint64_t initialState; storm::storage::BitVector initialStates; @@ -84,7 +85,7 @@ std::vector> collectRewardBuckets(storm::storage::Bi * storm --prism model.nm --prop 'R{"reward"}min/max=? [ F "target" ]' --cvar * storm --prism model.nm --prop 'R{"reward"}min/max=? [ F "target" ]' --cvar --cvar:method wr * - * The --cvar option requires exactly one selected property. That property must be unfiltered and must be an unbounded + * The --cvar option requires exactly one selected property. That property must be an unbounded * reward query with an optimization direction (min or max) and an eventually formula F phi whose target phi is a state * formula. The reward model may be named explicitly (R{"reward"}...) or omitted if the model has a unique reward model. * diff --git a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp index 8f66436263..4b2a7be42b 100644 --- a/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp +++ b/src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp @@ -7,8 +7,7 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/logic/FragmentSpecification.h" -#include "storm/modelchecker/cvar/CvarQueryInformation.h" -#include "storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h" +#include "storm/modelchecker/cvar/CvarModelChecking.h" #include "storm/modelchecker/helper/conditional/ConditionalHelper.h" #include "storm/modelchecker/helper/finitehorizon/SparseNondeterministicStepBoundedHorizonHelper.h" #include "storm/modelchecker/helper/infinitehorizon/SparseNondeterministicInfiniteHorizonHelper.h" @@ -531,25 +530,10 @@ std::unique_ptr SparseMdpPrctlModelChecker::che if constexpr (storm::IsIntervalType) { STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "CVaR formulas are not supported for interval models."); } else { - STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, - "Computing CVaR is only supported for the initial states of a model."); - STORM_LOG_THROW(this->getModel().getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, - "CVaR is not supported on models with multiple initial states."); - - // check if query fits specified format - auto cvarQueryInformation = storm::modelchecker::cvar::extractCvarQueryInformation(checkTask.getFormula()); - auto targetStates = - this->check(env, *cvarQueryInformation.targetFormula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); - - storm::modelchecker::cvar::SparseCvarComputationHelper cvarHelper(this->getModel(), cvarQueryInformation, targetStates); - auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); - - std::unique_ptr result( - new ExplicitQuantitativeCheckResult(*this->getModel().getInitialStates().begin(), std::move(cvarResult.value))); - if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { - result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); - } - return result; + auto formulaChecker = [&](storm::logic::Formula const& formula) { + return this->check(env, formula)->template asExplicitQualitativeCheckResult().getTruthValuesVector(); + }; + return cvar::performCvarModelChecking(env, this->getModel(), checkTask, formulaChecker); } } From b722deaf344862d9366adb69d74b6754aab3bbd3 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:45:49 +0200 Subject: [PATCH 52/65] Use exact rational CVaR alpha and align wrapper integration --- src/storm/api/properties.cpp | 146 +++++++++++++++++- src/storm/api/properties.h | 5 +- src/storm/logic/CvarFormula.cpp | 11 +- src/storm/logic/CvarFormula.h | 7 +- src/storm/logic/FormulaInformationVisitor.cpp | 6 +- src/storm/modelchecker/CheckTask.h | 19 +-- src/storm/settings/modules/IOSettings.cpp | 8 +- src/storm/settings/modules/IOSettings.h | 2 +- .../storage/jani/visitor/JSONExporter.cpp | 2 +- 9 files changed, 178 insertions(+), 28 deletions(-) diff --git a/src/storm/api/properties.cpp b/src/storm/api/properties.cpp index 4f2003777b..0df50ae839 100644 --- a/src/storm/api/properties.cpp +++ b/src/storm/api/properties.cpp @@ -1,17 +1,151 @@ #include "storm/api/properties.h" +#include #include +#include +#include +#include + +#include "storm/exceptions/InvalidArgumentException.h" #include "storm/storage/SymbolicModelDescription.h" #include "storm/storage/jani/Model.h" #include "storm/storage/jani/Property.h" #include "storm/storage/prism/Program.h" -#include "storm/logic/Formula.h" +#include "storm/logic/Formulas.h" #include "storm/utility/cli.h" +#include "storm/utility/constants.h" +#include "storm/utility/macros.h" namespace storm { namespace api { +namespace { + +std::string trimAndStripLeadingPlus(std::string const& input) { + std::string result = boost::algorithm::trim_copy(input); + if (!result.empty() && result.front() == '+') { + result.erase(result.begin()); + } + return result; +} + +bool isNonEmptyUnsignedDecimalInteger(std::string const& input) { + return !input.empty() && std::all_of(input.begin(), input.end(), [](unsigned char c) { return std::isdigit(c); }); +} + +storm::RationalNumber parseUnsignedIntegerAsRational(std::string const& input, std::string const& originalInput) { + std::string strippedInput = trimAndStripLeadingPlus(input); + STORM_LOG_THROW(isNonEmptyUnsignedDecimalInteger(strippedInput), storm::exceptions::InvalidArgumentException, + "Unable to parse CVaR alpha '" << originalInput << "'."); + return storm::utility::convertNumber(strippedInput); +} + +storm::RationalNumber powerOfTen(uint64_t exponent) { + storm::RationalNumber result = storm::utility::one(); + storm::RationalNumber const ten = storm::utility::convertNumber(10); + for (uint64_t i = 0; i < exponent; ++i) { + result *= ten; + } + return result; +} + +int64_t parseSignedExponent(std::string const& input, std::string const& originalInput) { + STORM_LOG_THROW(!input.empty(), storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); + std::string exponentString = input; + bool negative = false; + if (exponentString.front() == '+' || exponentString.front() == '-') { + negative = exponentString.front() == '-'; + exponentString.erase(exponentString.begin()); + } + STORM_LOG_THROW(isNonEmptyUnsignedDecimalInteger(exponentString), storm::exceptions::InvalidArgumentException, + "Unable to parse CVaR alpha '" << originalInput << "'."); + + uint64_t exponent = 0; + try { + exponent = std::stoull(exponentString); + } catch (std::exception const&) { + STORM_LOG_THROW(false, storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); + } + STORM_LOG_THROW(exponent <= static_cast(std::numeric_limits::max()), storm::exceptions::InvalidArgumentException, + "Unable to parse CVaR alpha '" << originalInput << "'."); + return negative ? -static_cast(exponent) : static_cast(exponent); +} + +storm::RationalNumber parseDecimalOrScientificCvarAlpha(std::string const& input, std::string const& originalInput) { + std::string mantissa = input; + int64_t exponent = 0; + + auto exponentPosition = mantissa.find_first_of("eE"); + if (exponentPosition != std::string::npos) { + STORM_LOG_THROW(mantissa.find_first_of("eE", exponentPosition + 1) == std::string::npos, storm::exceptions::InvalidArgumentException, + "Unable to parse CVaR alpha '" << originalInput << "'."); + exponent = parseSignedExponent(mantissa.substr(exponentPosition + 1), originalInput); + mantissa = mantissa.substr(0, exponentPosition); + } + + STORM_LOG_THROW(!mantissa.empty(), storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); + STORM_LOG_THROW(mantissa.front() != '-', storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); + if (mantissa.front() == '+') { + mantissa.erase(mantissa.begin()); + } + + auto decimalPosition = mantissa.find('.'); + STORM_LOG_THROW(decimalPosition == std::string::npos || mantissa.find('.', decimalPosition + 1) == std::string::npos, + storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); + + std::string digitsBeforeDecimal; + std::string digitsAfterDecimal; + if (decimalPosition == std::string::npos) { + digitsBeforeDecimal = mantissa; + } else { + digitsBeforeDecimal = mantissa.substr(0, decimalPosition); + digitsAfterDecimal = mantissa.substr(decimalPosition + 1); + } + + STORM_LOG_THROW((digitsBeforeDecimal.empty() || isNonEmptyUnsignedDecimalInteger(digitsBeforeDecimal)) && + (digitsAfterDecimal.empty() || isNonEmptyUnsignedDecimalInteger(digitsAfterDecimal)) && + !(digitsBeforeDecimal.empty() && digitsAfterDecimal.empty()), + storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); + STORM_LOG_THROW(digitsAfterDecimal.size() <= static_cast(std::numeric_limits::max()), storm::exceptions::InvalidArgumentException, + "Unable to parse CVaR alpha '" << originalInput << "'."); + + std::string digits = digitsBeforeDecimal + digitsAfterDecimal; + storm::RationalNumber value = storm::utility::convertNumber(digits); + int64_t decimalScale = static_cast(digitsAfterDecimal.size()) - exponent; + if (decimalScale >= 0) { + value /= powerOfTen(static_cast(decimalScale)); + } else { + value *= powerOfTen(static_cast(-decimalScale)); + } + return value; +} + +storm::RationalNumber parseCvarAlpha(std::string const& input) { + std::string strippedInput = trimAndStripLeadingPlus(input); + STORM_LOG_THROW(!strippedInput.empty(), storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << input << "'."); + STORM_LOG_THROW(strippedInput.front() != '-', storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << input << "'."); + + storm::RationalNumber alpha; + auto fractionSeparator = strippedInput.find('/'); + if (fractionSeparator != std::string::npos) { + STORM_LOG_THROW(strippedInput.find('/', fractionSeparator + 1) == std::string::npos, storm::exceptions::InvalidArgumentException, + "Unable to parse CVaR alpha '" << input << "'."); + auto numerator = parseUnsignedIntegerAsRational(strippedInput.substr(0, fractionSeparator), input); + auto denominator = parseUnsignedIntegerAsRational(strippedInput.substr(fractionSeparator + 1), input); + STORM_LOG_THROW(denominator != storm::utility::zero(), storm::exceptions::InvalidArgumentException, + "Unable to parse CVaR alpha '" << input << "' because the denominator is zero."); + alpha = numerator / denominator; + } else { + alpha = parseDecimalOrScientificCvarAlpha(strippedInput, input); + } + + STORM_LOG_THROW(storm::utility::zero() < alpha && alpha < storm::utility::one(), + storm::exceptions::InvalidArgumentException, "The CVaR alpha must be in the open interval (0, 1)."); + return alpha; +} + +} // namespace std::vector substituteConstantsInProperties(std::vector const& properties, std::map const& substitution) { @@ -69,13 +203,17 @@ std::vector> extractFormulasFromPro return formulas; } -storm::jani::Property createCvarProperty(storm::jani::Property const& property, double alpha) { - STORM_LOG_THROW(property.getFilter().isDefault(), storm::exceptions::InvalidArgumentException, - "Non-default property filter of property " << property.getName() << " is not supported for CVaR queries."); +storm::jani::Property createCvarProperty(storm::jani::Property const& property, storm::RationalNumber const& alpha) { + STORM_LOG_WARN_COND(property.getFilter().isDefault(), + "Non-default property filter of property " << property.getName() << " will be dropped during conversion to CVaR property."); auto cvarFormula = std::make_shared(alpha, property.getRawFormula()); return storm::jani::Property(property.getName(), cvarFormula, property.getUndefinedConstants(), property.getComment()); } +storm::jani::Property createCvarProperty(storm::jani::Property const& property, std::string const& alpha) { + return createCvarProperty(property, parseCvarAlpha(alpha)); +} + storm::jani::Property createMultiObjectiveProperty(std::vector const& properties, bool lexicographic) { std::set undefConstants; std::string name = ""; diff --git a/src/storm/api/properties.h b/src/storm/api/properties.h index 60d5c8a8d1..c2dbd19394 100644 --- a/src/storm/api/properties.h +++ b/src/storm/api/properties.h @@ -7,6 +7,8 @@ #include #include +#include "storm/adapters/RationalNumberAdapter.h" + namespace storm { namespace jani { @@ -36,7 +38,8 @@ std::vector substituteTranscendentalNumbersInProperties(s std::vector filterProperties(std::vector const& properties, boost::optional> const& propertyFilter); std::vector> extractFormulasFromProperties(std::vector const& properties); -storm::jani::Property createCvarProperty(storm::jani::Property const& property, double alpha); +storm::jani::Property createCvarProperty(storm::jani::Property const& property, storm::RationalNumber const& alpha); +storm::jani::Property createCvarProperty(storm::jani::Property const& property, std::string const& alpha); storm::jani::Property createMultiObjectiveProperty(std::vector const& properties, bool lexicographic); } // namespace api diff --git a/src/storm/logic/CvarFormula.cpp b/src/storm/logic/CvarFormula.cpp index 21e24503d4..5853aaf588 100644 --- a/src/storm/logic/CvarFormula.cpp +++ b/src/storm/logic/CvarFormula.cpp @@ -4,13 +4,18 @@ #include #include +#include "storm/exceptions/InvalidArgumentException.h" #include "storm/logic/FormulaVisitor.h" +#include "storm/utility/constants.h" +#include "storm/utility/macros.h" namespace storm { namespace logic { -CvarFormula::CvarFormula(double alpha, std::shared_ptr subformula) : alpha(alpha), subformula(std::move(subformula)) { - // Intentionally left empty. +CvarFormula::CvarFormula(storm::RationalNumber const& alpha, std::shared_ptr subformula) : alpha(alpha), subformula(std::move(subformula)) { + STORM_LOG_THROW(this->subformula != nullptr, storm::exceptions::InvalidArgumentException, "A CVaR formula requires a subformula."); + STORM_LOG_THROW(storm::utility::zero() < this->alpha && this->alpha < storm::utility::one(), + storm::exceptions::InvalidArgumentException, "The CVaR alpha must be in the open interval (0, 1)."); } CvarFormula::~CvarFormula() { @@ -33,7 +38,7 @@ bool CvarFormula::hasMultiDimensionalResult() const { return false; } -double CvarFormula::getAlpha() const { +storm::RationalNumber const& CvarFormula::getAlpha() const { return alpha; } diff --git a/src/storm/logic/CvarFormula.h b/src/storm/logic/CvarFormula.h index 9550a16f8c..e4e66c2f41 100644 --- a/src/storm/logic/CvarFormula.h +++ b/src/storm/logic/CvarFormula.h @@ -1,5 +1,6 @@ #pragma once +#include "storm/adapters/RationalNumberAdapter.h" #include "storm/logic/StateFormula.h" namespace storm { @@ -7,7 +8,7 @@ namespace logic { class CvarFormula : public StateFormula { public: - CvarFormula(double alpha, std::shared_ptr subformula); + CvarFormula(storm::RationalNumber const& alpha, std::shared_ptr subformula); virtual ~CvarFormula(); @@ -17,7 +18,7 @@ class CvarFormula : public StateFormula { virtual bool hasNumericalResult() const; virtual bool hasMultiDimensionalResult() const; - double getAlpha() const; + storm::RationalNumber const& getAlpha() const; Formula const& getSubformula() const; virtual boost::any accept(FormulaVisitor const& visitor, boost::any const& data) const override; @@ -29,7 +30,7 @@ class CvarFormula : public StateFormula { virtual std::ostream& writeToStream(std::ostream& out, bool allowParentheses = false) const override; private: - double alpha; + storm::RationalNumber alpha; std::shared_ptr subformula; }; diff --git a/src/storm/logic/FormulaInformationVisitor.cpp b/src/storm/logic/FormulaInformationVisitor.cpp index c3015fab7f..1b8d4ef9ab 100644 --- a/src/storm/logic/FormulaInformationVisitor.cpp +++ b/src/storm/logic/FormulaInformationVisitor.cpp @@ -80,7 +80,11 @@ boost::any FormulaInformationVisitor::visit(CumulativeRewardFormula const& f, bo } boost::any FormulaInformationVisitor::visit(CvarFormula const& f, boost::any const& data) const { - return f.getSubformula().accept(*this, data); + if (recurseIntoOperators) { + return f.getSubformula().accept(*this, data); + } else { + return FormulaInformation(); + } } boost::any FormulaInformationVisitor::visit(EventuallyFormula const& f, boost::any const& data) const { diff --git a/src/storm/modelchecker/CheckTask.h b/src/storm/modelchecker/CheckTask.h index 16cd6ca63a..2c43e520fc 100644 --- a/src/storm/modelchecker/CheckTask.h +++ b/src/storm/modelchecker/CheckTask.h @@ -64,12 +64,13 @@ class CheckTask { * Calling this method has no effect if the provided formula is not an operator formula. */ void updateOperatorInformation() { - if (formula.get().isOperatorFormula()) { - storm::logic::OperatorFormula const& operatorFormula = formula.get().asOperatorFormula(); - if (operatorFormula.hasOptimalityType()) { - this->optimizationDirection = operatorFormula.getOptimalityType(); - } + storm::logic::Formula const* formulaForOperatorInformation = &formula.get(); + if (formulaForOperatorInformation->isCvarFormula()) { + formulaForOperatorInformation = &formulaForOperatorInformation->asCvarFormula().getSubformula(); + } + if (formulaForOperatorInformation->isOperatorFormula()) { + storm::logic::OperatorFormula const& operatorFormula = formulaForOperatorInformation->asOperatorFormula(); if (operatorFormula.hasBound()) { this->bound = operatorFormula.getBound(); } @@ -83,8 +84,8 @@ class CheckTask { : OptimizationDirection::Minimize; } - if (formula.get().isProbabilityOperatorFormula()) { - storm::logic::ProbabilityOperatorFormula const& probabilityOperatorFormula = formula.get().asProbabilityOperatorFormula(); + if (formulaForOperatorInformation->isProbabilityOperatorFormula()) { + storm::logic::ProbabilityOperatorFormula const& probabilityOperatorFormula = formulaForOperatorInformation->asProbabilityOperatorFormula(); if (probabilityOperatorFormula.hasBound()) { if (storm::utility::isZero(probabilityOperatorFormula.template getThresholdAs()) || @@ -92,8 +93,8 @@ class CheckTask { this->qualitative = true; } } - } else if (formula.get().isRewardOperatorFormula()) { - storm::logic::RewardOperatorFormula const& rewardOperatorFormula = formula.get().asRewardOperatorFormula(); + } else if (formulaForOperatorInformation->isRewardOperatorFormula()) { + storm::logic::RewardOperatorFormula const& rewardOperatorFormula = formulaForOperatorInformation->asRewardOperatorFormula(); this->rewardModel = rewardOperatorFormula.getOptionalRewardModelName(); if (rewardOperatorFormula.hasBound()) { diff --git a/src/storm/settings/modules/IOSettings.cpp b/src/storm/settings/modules/IOSettings.cpp index a96f136625..d26bb9b035 100644 --- a/src/storm/settings/modules/IOSettings.cpp +++ b/src/storm/settings/modules/IOSettings.cpp @@ -283,9 +283,7 @@ IOSettings::IOSettings() : ModuleSettings(moduleName) { .build()); this->addOption(storm::settings::OptionBuilder(moduleName, cvarOptionName, false, "Computes the conditional value-at-risk for the selected property.") - .addArgument(storm::settings::ArgumentBuilder::createDoubleArgument("alpha", "The size of the tail.") - .addValidatorDouble(storm::settings::ArgumentValidatorFactory::createDoubleRangeValidatorExcluding(0.0, 1.0)) - .build()) + .addArgument(storm::settings::ArgumentBuilder::createStringArgument("alpha", "The size of the tail.").build()) .build()); std::vector uncertaintyResolutionModes = {"minimize", "maximize", "robust", "cooperative", "min", "max"}; @@ -529,8 +527,8 @@ bool IOSettings::isCvarSet() const { return this->getOption(cvarOptionName).getHasOptionBeenSet(); } -double IOSettings::getCvarAlpha() const { - return this->getOption(cvarOptionName).getArgumentByName("alpha").getValueAsDouble(); +std::string IOSettings::getCvarAlpha() const { + return this->getOption(cvarOptionName).getArgumentByName("alpha").getValueAsString(); } bool IOSettings::isComputeSteadyStateDistributionSet() const { diff --git a/src/storm/settings/modules/IOSettings.h b/src/storm/settings/modules/IOSettings.h index 15acb3fe70..eeab0fee4e 100644 --- a/src/storm/settings/modules/IOSettings.h +++ b/src/storm/settings/modules/IOSettings.h @@ -379,7 +379,7 @@ class IOSettings : public ModuleSettings { * * @return The alpha value specified with the CVaR option. */ - double getCvarAlpha() const; + std::string getCvarAlpha() const; /*! * Retrieves whether the steady-state distribution is to be computed. diff --git a/src/storm/storage/jani/visitor/JSONExporter.cpp b/src/storm/storage/jani/visitor/JSONExporter.cpp index 467e691bac..6ae2e9b349 100644 --- a/src/storm/storage/jani/visitor/JSONExporter.cpp +++ b/src/storm/storage/jani/visitor/JSONExporter.cpp @@ -469,7 +469,7 @@ boost::any FormulaToJaniJson::visit(storm::logic::QuantileFormula const&, boost: } boost::any FormulaToJaniJson::visit(storm::logic::CvarFormula const&, boost::any const&) const { - STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "Jani currently does not support conversion of a CVaR formula"); + STORM_LOG_THROW(false, storm::exceptions::NotImplementedException, "JANI export does not support Storm's CLI-only CVaR property wrapper."); } boost::any FormulaToJaniJson::visit(storm::logic::NextFormula const& f, boost::any const& data) const { From 5f69b78f9e0370f6fb33610bea70560f27df861d Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:46:01 +0200 Subject: [PATCH 53/65] Add focused tests for CVaR refactor --- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 161 +++++++----------- 1 file changed, 61 insertions(+), 100 deletions(-) diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 4c664d776c..f8a9b00bb6 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -8,8 +8,9 @@ #include "storm/api/properties.h" #include "storm/environment/Environment.h" #include "storm/environment/modelchecker/ModelCheckerEnvironment.h" -#include "storm/exceptions/InvalidOperationException.h" +#include "storm/exceptions/InvalidArgumentException.h" #include "storm/exceptions/InvalidPropertyException.h" +#include "storm/logic/CvarFormula.h" #include "storm/modelchecker/CheckTask.h" #include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/modelchecker/cvar/helper/SspParetoFront.h" @@ -18,18 +19,9 @@ #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" #include "storm/models/sparse/Mdp.h" -#include "storm/storage/SparseMatrix.h" - -#include +#include "storm/utility/constants.h" namespace { -constexpr uint64_t safeChoice = 0; -constexpr uint64_t balancedChoice = 1; -constexpr uint64_t adaptiveChoice = 2; -constexpr uint64_t riskyChoice = 3; -constexpr uint64_t cashChoice = 0; -constexpr uint64_t pushChoice = 1; - bool hasLpSolver() { #if !defined(STORM_HAVE_GLPK) && !defined(STORM_HAVE_GUROBI) && !defined(STORM_HAVE_Z3) && !defined(STORM_HAVE_SOPLEX) return false; @@ -53,7 +45,7 @@ struct CvarTestInput { }; template -CvarTestInput buildCvarInput(std::string const& modelPath, std::string const& propertyString, double alpha) { +CvarTestInput buildCvarInput(std::string const& modelPath, std::string const& propertyString, std::string const& alpha) { storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram(propertyString, program); std::vector cvarProperties = {storm::api::createCvarProperty(properties.front(), alpha)}; @@ -63,11 +55,10 @@ CvarTestInput buildCvarInput(std::string const& modelPath, std::strin } template -std::unique_ptr checkInitialStateResult(CvarTestInput const& input, bool produceScheduler = false) { +std::unique_ptr checkInitialStateResult(CvarTestInput const& input) { storm::Environment env; storm::modelchecker::SparseMdpPrctlModelChecker> checker(*input.mdp); storm::modelchecker::CheckTask task(*input.formula, true); - task.setProduceSchedulers(produceScheduler); return checker.check(env, task); } @@ -87,16 +78,6 @@ ValueType checkInitialStateValueWithMethod(CvarTestInput const& input return result->template asExplicitQuantitativeCheckResult().getMax(); } -template -std::vector getChoiceSuccessors(std::shared_ptr> const& mdp, uint64_t state, uint64_t localChoice) { - std::vector result; - uint64_t row = mdp->getTransitionMatrix().getRowGroupIndices()[state] + localChoice; - for (auto const& entry : mdp->getTransitionMatrix().getRow(row)) { - result.push_back(entry.getColumn()); - } - return result; -} - void expectParetoFrontPoints(storm::modelchecker::cvar::SspParetoFront const& front, std::vector> const& expectedPoints) { auto const& actualPoints = front.getPoints(); ASSERT_EQ(expectedPoints.size(), actualPoints.size()); @@ -178,7 +159,7 @@ TEST(CvarQueryTest, SimpleMdp) { GTEST_SKIP() << "No LP solver available."; } - double alpha = 0.75; + std::string alpha = "0.75"; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); @@ -195,7 +176,7 @@ TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { GTEST_SKIP() << "No LP solver available."; } - double alpha = 0.5; + std::string alpha = "0.5"; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_bad_mec_mdp.nm"; auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); @@ -214,26 +195,15 @@ TEST(CvarQueryTest, TargetReachingMecIsCollapsed) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_target_reaching_mec_mdp.nm"; - auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "0.75"); EXPECT_NEAR(checkInitialStateValue(maxInput), 6.0, 1e-10); - auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.75"); EXPECT_NEAR(checkInitialStateValue(minInput), 4.0, 1e-10); } -TEST(CvarQueryTest, RejectsSchedulerForTargetReachingMecCollapse) { - if (!hasLpSolver()) { - GTEST_SKIP() << "No LP solver available."; - } - - std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_target_reaching_mec_mdp.nm"; - auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); - - STORM_SILENT_EXPECT_THROW(checkInitialStateResult(input, true), storm::exceptions::InvalidOperationException); -} - TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { - double alpha = 0.5; + std::string alpha = "0.5"; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_nonabsorbing_target_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", alpha); @@ -251,17 +221,17 @@ TEST(CvarQueryTest, BranchingTradeoffMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; - auto maxHalfInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.5); + auto maxHalfInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "0.5"); EXPECT_NEAR(checkInitialStateValue(maxHalfInput), 7.0, 1e-10); - auto maxThreeQuarterInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + auto maxThreeQuarterInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "0.75"); EXPECT_NEAR(checkInitialStateValue(maxThreeQuarterInput), 8.0, 1e-10); - auto minHalfInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.5); + auto minHalfInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.5"); EXPECT_NEAR(checkInitialStateValue(minHalfInput), 0.0, 1e-10); // this requires randomization of the strategy - auto minThreeQuarterInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + auto minThreeQuarterInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.75"); EXPECT_NEAR(checkInitialStateValue(minThreeQuarterInput), 14.0 / 3.0, 1e-10); } @@ -272,77 +242,68 @@ TEST(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; - auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "0.75"); EXPECT_EQ(storm::RationalNumber(8), checkInitialStateValue(maxInput)); - auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.75"); EXPECT_EQ(storm::RationalNumber("14/3"), checkInitialStateValue(minInput)); } -TEST(CvarQueryTest, ProducesDeterministicSchedulerForMaxBranchingTradeoffMdp) { +TEST(CvarQueryTest, EquivalentExactAlphaSyntaxes) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; } - std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; - auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", 0.75); - auto result = checkInitialStateResult(input, true); - - ASSERT_TRUE(result->isExplicitQuantitativeCheckResult()); - auto const& quantitativeResult = result->template asExplicitQuantitativeCheckResult(); - ASSERT_TRUE(quantitativeResult.hasScheduler()); - EXPECT_NEAR(quantitativeResult.getMax(), 8.0, 1e-10); - - storm::storage::Scheduler const& scheduler = quantitativeResult.getScheduler(); - uint64_t initialState = *input.mdp->getInitialStates().begin(); - auto adaptiveSuccessors = getChoiceSuccessors(input.mdp, initialState, adaptiveChoice); - ASSERT_EQ(2, adaptiveSuccessors.size()); - EXPECT_TRUE(scheduler.isDeterministicScheduler()); - EXPECT_TRUE(scheduler.isMemorylessScheduler()); - EXPECT_FALSE(scheduler.isPartialScheduler()); - EXPECT_EQ(adaptiveChoice, scheduler.getChoice(initialState).getDeterministicChoice()); - std::set branchChoices = {scheduler.getChoice(adaptiveSuccessors[0]).getDeterministicChoice(), - scheduler.getChoice(adaptiveSuccessors[1]).getDeterministicChoice()}; - EXPECT_EQ(std::set({cashChoice, pushChoice}), branchChoices); + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; + + auto decimalInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "0.75"); + auto fractionInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "3/4"); + auto scientificInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "7.5e-1"); + + EXPECT_NEAR(checkInitialStateValue(decimalInput), 2.0, 1e-10); + EXPECT_NEAR(checkInitialStateValue(fractionInput), 2.0, 1e-10); + EXPECT_NEAR(checkInitialStateValue(scientificInput), 2.0, 1e-10); } -TEST(CvarQueryTest, ProducesRandomizedSchedulerForMinBranchingTradeoffMdp) { - if (!hasLpSolver()) { - GTEST_SKIP() << "No LP solver available."; +TEST(CvarQueryTest, RejectsInvalidAlphaSyntaxes) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; + storm::prism::Program program = storm::api::parseProgram(modelPath); + auto properties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}max=? [ F \"target\" ];", program); + + for (auto const& alpha : {"0", "1", "-0.1", "abc", "nan", "inf", "1/0", "0/1", "1/1", "1e"}) { + STORM_SILENT_EXPECT_THROW(storm::api::createCvarProperty(properties.front(), std::string(alpha)), storm::exceptions::InvalidArgumentException); } +} - std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_branching_tradeoff_mdp.nm"; - auto input = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", 0.75); - auto result = checkInitialStateResult(input, true); - - ASSERT_TRUE(result->isExplicitQuantitativeCheckResult()); - auto const& quantitativeResult = result->template asExplicitQuantitativeCheckResult(); - ASSERT_TRUE(quantitativeResult.hasScheduler()); - EXPECT_NEAR(quantitativeResult.getMax(), 14.0 / 3.0, 1e-10); - - storm::storage::Scheduler const& scheduler = quantitativeResult.getScheduler(); - EXPECT_FALSE(scheduler.isDeterministicScheduler()); - EXPECT_TRUE(scheduler.isMemorylessScheduler()); - EXPECT_FALSE(scheduler.isPartialScheduler()); - - uint64_t initialState = *input.mdp->getInitialStates().begin(); - auto adaptiveSuccessors = getChoiceSuccessors(input.mdp, initialState, adaptiveChoice); - ASSERT_EQ(2, adaptiveSuccessors.size()); - auto const& initialChoice = scheduler.getChoice(initialState); - ASSERT_TRUE(initialChoice.isDefined()); - ASSERT_FALSE(initialChoice.isDeterministic()); - auto const& initialDistribution = initialChoice.getChoiceAsDistribution(); - EXPECT_NEAR(initialDistribution.getProbability(safeChoice), 0.5, 1e-10); - EXPECT_NEAR(initialDistribution.getProbability(riskyChoice), 0.5, 1e-10); - EXPECT_NEAR(initialDistribution.getProbability(balancedChoice), 0.0, 1e-10); - EXPECT_NEAR(initialDistribution.getProbability(adaptiveChoice), 0.0, 1e-10); - EXPECT_TRUE(scheduler.isDontCare(adaptiveSuccessors[0])); - EXPECT_TRUE(scheduler.isDontCare(adaptiveSuccessors[1])); +TEST(CvarQueryTest, CvarFormulaValidatesAlphaAndSubformula) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; + storm::prism::Program program = storm::api::parseProgram(modelPath); + auto properties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}max=? [ F \"target\" ];", program); + auto formula = properties.front().getRawFormula(); + + EXPECT_NO_THROW(storm::logic::CvarFormula(storm::RationalNumber("3/4"), formula)); + STORM_SILENT_EXPECT_THROW(storm::logic::CvarFormula(storm::utility::zero(), formula), storm::exceptions::InvalidArgumentException); + STORM_SILENT_EXPECT_THROW(storm::logic::CvarFormula(storm::utility::one(), formula), storm::exceptions::InvalidArgumentException); + STORM_SILENT_EXPECT_THROW(storm::logic::CvarFormula(storm::RationalNumber("1/2"), nullptr), storm::exceptions::InvalidArgumentException); +} + +TEST(CvarQueryTest, CheckTaskExtractsWrappedRewardMetadata) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "3/4"); + + auto const& cvarFormula = input.formula->asCvarFormula(); + storm::modelchecker::CheckTask task(cvarFormula, true); + + ASSERT_TRUE(task.isOptimizationDirectionSet()); + EXPECT_EQ(storm::solver::OptimizationDirection::Maximize, task.getOptimizationDirection()); + ASSERT_TRUE(task.isRewardModelSet()); + EXPECT_EQ("term", task.getRewardModel()); + EXPECT_FALSE(task.isBoundSet()); } TEST(CvarQueryTest, DeterministicSspPathMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; - auto input = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", 0.5); + auto input = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.5"); double value = checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi); EXPECT_NEAR(value, 5.0, 1e-10); @@ -351,10 +312,10 @@ TEST(CvarQueryTest, DeterministicSspPathMdp) { TEST(CvarQueryTest, BranchingSspTradeoffMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_branching_tradeoff_mdp.nm"; - auto halfInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", 0.5); + auto halfInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.5"); EXPECT_NEAR(checkInitialStateValueWithMethod(halfInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 6.0, 1e-10); - auto nineTenthsInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", 0.9); + auto nineTenthsInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.9"); EXPECT_NEAR(checkInitialStateValueWithMethod(nineTenthsInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 16.0 / 3.0, 1e-10); } } // namespace From 9325b7415f0d13b32e1b7ee14696749fc176473a Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:06:11 +0200 Subject: [PATCH 54/65] Simplified Storm rational parsing for CVaR alpha and type plumbing --- src/storm/api/properties.cpp | 122 +----------------- .../modelchecker/cvar/CvarModelChecking.cpp | 7 +- .../modelchecker/cvar/CvarModelChecking.h | 3 +- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 4 +- 4 files changed, 9 insertions(+), 127 deletions(-) diff --git a/src/storm/api/properties.cpp b/src/storm/api/properties.cpp index 0df50ae839..885c5f00f4 100644 --- a/src/storm/api/properties.cpp +++ b/src/storm/api/properties.cpp @@ -1,10 +1,6 @@ #include "storm/api/properties.h" -#include #include -#include -#include -#include #include "storm/exceptions/InvalidArgumentException.h" #include "storm/storage/SymbolicModelDescription.h" @@ -22,123 +18,11 @@ namespace storm { namespace api { namespace { -std::string trimAndStripLeadingPlus(std::string const& input) { - std::string result = boost::algorithm::trim_copy(input); - if (!result.empty() && result.front() == '+') { - result.erase(result.begin()); - } - return result; -} - -bool isNonEmptyUnsignedDecimalInteger(std::string const& input) { - return !input.empty() && std::all_of(input.begin(), input.end(), [](unsigned char c) { return std::isdigit(c); }); -} - -storm::RationalNumber parseUnsignedIntegerAsRational(std::string const& input, std::string const& originalInput) { - std::string strippedInput = trimAndStripLeadingPlus(input); - STORM_LOG_THROW(isNonEmptyUnsignedDecimalInteger(strippedInput), storm::exceptions::InvalidArgumentException, - "Unable to parse CVaR alpha '" << originalInput << "'."); - return storm::utility::convertNumber(strippedInput); -} - -storm::RationalNumber powerOfTen(uint64_t exponent) { - storm::RationalNumber result = storm::utility::one(); - storm::RationalNumber const ten = storm::utility::convertNumber(10); - for (uint64_t i = 0; i < exponent; ++i) { - result *= ten; - } - return result; -} - -int64_t parseSignedExponent(std::string const& input, std::string const& originalInput) { - STORM_LOG_THROW(!input.empty(), storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); - std::string exponentString = input; - bool negative = false; - if (exponentString.front() == '+' || exponentString.front() == '-') { - negative = exponentString.front() == '-'; - exponentString.erase(exponentString.begin()); - } - STORM_LOG_THROW(isNonEmptyUnsignedDecimalInteger(exponentString), storm::exceptions::InvalidArgumentException, - "Unable to parse CVaR alpha '" << originalInput << "'."); - - uint64_t exponent = 0; - try { - exponent = std::stoull(exponentString); - } catch (std::exception const&) { - STORM_LOG_THROW(false, storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); - } - STORM_LOG_THROW(exponent <= static_cast(std::numeric_limits::max()), storm::exceptions::InvalidArgumentException, - "Unable to parse CVaR alpha '" << originalInput << "'."); - return negative ? -static_cast(exponent) : static_cast(exponent); -} - -storm::RationalNumber parseDecimalOrScientificCvarAlpha(std::string const& input, std::string const& originalInput) { - std::string mantissa = input; - int64_t exponent = 0; - - auto exponentPosition = mantissa.find_first_of("eE"); - if (exponentPosition != std::string::npos) { - STORM_LOG_THROW(mantissa.find_first_of("eE", exponentPosition + 1) == std::string::npos, storm::exceptions::InvalidArgumentException, - "Unable to parse CVaR alpha '" << originalInput << "'."); - exponent = parseSignedExponent(mantissa.substr(exponentPosition + 1), originalInput); - mantissa = mantissa.substr(0, exponentPosition); - } - - STORM_LOG_THROW(!mantissa.empty(), storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); - STORM_LOG_THROW(mantissa.front() != '-', storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); - if (mantissa.front() == '+') { - mantissa.erase(mantissa.begin()); - } - - auto decimalPosition = mantissa.find('.'); - STORM_LOG_THROW(decimalPosition == std::string::npos || mantissa.find('.', decimalPosition + 1) == std::string::npos, - storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); - - std::string digitsBeforeDecimal; - std::string digitsAfterDecimal; - if (decimalPosition == std::string::npos) { - digitsBeforeDecimal = mantissa; - } else { - digitsBeforeDecimal = mantissa.substr(0, decimalPosition); - digitsAfterDecimal = mantissa.substr(decimalPosition + 1); - } - - STORM_LOG_THROW((digitsBeforeDecimal.empty() || isNonEmptyUnsignedDecimalInteger(digitsBeforeDecimal)) && - (digitsAfterDecimal.empty() || isNonEmptyUnsignedDecimalInteger(digitsAfterDecimal)) && - !(digitsBeforeDecimal.empty() && digitsAfterDecimal.empty()), - storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << originalInput << "'."); - STORM_LOG_THROW(digitsAfterDecimal.size() <= static_cast(std::numeric_limits::max()), storm::exceptions::InvalidArgumentException, - "Unable to parse CVaR alpha '" << originalInput << "'."); - - std::string digits = digitsBeforeDecimal + digitsAfterDecimal; - storm::RationalNumber value = storm::utility::convertNumber(digits); - int64_t decimalScale = static_cast(digitsAfterDecimal.size()) - exponent; - if (decimalScale >= 0) { - value /= powerOfTen(static_cast(decimalScale)); - } else { - value *= powerOfTen(static_cast(-decimalScale)); - } - return value; -} - storm::RationalNumber parseCvarAlpha(std::string const& input) { - std::string strippedInput = trimAndStripLeadingPlus(input); + std::string strippedInput = boost::algorithm::trim_copy(input); STORM_LOG_THROW(!strippedInput.empty(), storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << input << "'."); - STORM_LOG_THROW(strippedInput.front() != '-', storm::exceptions::InvalidArgumentException, "Unable to parse CVaR alpha '" << input << "'."); - - storm::RationalNumber alpha; - auto fractionSeparator = strippedInput.find('/'); - if (fractionSeparator != std::string::npos) { - STORM_LOG_THROW(strippedInput.find('/', fractionSeparator + 1) == std::string::npos, storm::exceptions::InvalidArgumentException, - "Unable to parse CVaR alpha '" << input << "'."); - auto numerator = parseUnsignedIntegerAsRational(strippedInput.substr(0, fractionSeparator), input); - auto denominator = parseUnsignedIntegerAsRational(strippedInput.substr(fractionSeparator + 1), input); - STORM_LOG_THROW(denominator != storm::utility::zero(), storm::exceptions::InvalidArgumentException, - "Unable to parse CVaR alpha '" << input << "' because the denominator is zero."); - alpha = numerator / denominator; - } else { - alpha = parseDecimalOrScientificCvarAlpha(strippedInput, input); - } + + storm::RationalNumber alpha = storm::utility::convertNumber(strippedInput); STORM_LOG_THROW(storm::utility::zero() < alpha && alpha < storm::utility::one(), storm::exceptions::InvalidArgumentException, "The CVaR alpha must be in the open interval (0, 1)."); diff --git a/src/storm/modelchecker/cvar/CvarModelChecking.cpp b/src/storm/modelchecker/cvar/CvarModelChecking.cpp index f067cdab2c..a49c318beb 100644 --- a/src/storm/modelchecker/cvar/CvarModelChecking.cpp +++ b/src/storm/modelchecker/cvar/CvarModelChecking.cpp @@ -16,10 +16,9 @@ namespace cvar { template std::unique_ptr performCvarModelChecking( Environment const& env, SparseMdpModelType const& model, - CheckTask> const& checkTask, + CheckTask const& checkTask, std::function const& formulaChecker) { using ValueType = typename SparseMdpModelType::ValueType; - using SolutionType = storm::IntervalBaseType; STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, "Computing CVaR is only supported for the initial states of a model."); @@ -32,9 +31,9 @@ std::unique_ptr performCvarModelChecking( SparseCvarComputationHelper cvarHelper(model, cvarQueryInformation, targetStates); auto cvarResult = cvarHelper.computeCvar(env, checkTask.isProduceSchedulersSet()); - std::unique_ptr result(new ExplicitQuantitativeCheckResult(*model.getInitialStates().begin(), std::move(cvarResult.value))); + std::unique_ptr result(new ExplicitQuantitativeCheckResult(*model.getInitialStates().begin(), std::move(cvarResult.value))); if (checkTask.isProduceSchedulersSet() && cvarResult.scheduler) { - result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); + result->asExplicitQuantitativeCheckResult().setScheduler(std::move(cvarResult.scheduler)); } return result; } diff --git a/src/storm/modelchecker/cvar/CvarModelChecking.h b/src/storm/modelchecker/cvar/CvarModelChecking.h index 9f025f6487..74021dd677 100644 --- a/src/storm/modelchecker/cvar/CvarModelChecking.h +++ b/src/storm/modelchecker/cvar/CvarModelChecking.h @@ -3,7 +3,6 @@ #include #include -#include "storm/adapters/IntervalForward.h" #include "storm/logic/CvarFormula.h" #include "storm/modelchecker/CheckTask.h" #include "storm/modelchecker/results/CheckResult.h" @@ -23,7 +22,7 @@ namespace cvar { template std::unique_ptr performCvarModelChecking( Environment const& env, SparseMdpModelType const& model, - CheckTask> const& checkTask, + CheckTask const& checkTask, std::function const& formulaChecker); } // namespace cvar diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index f8a9b00bb6..ffc17d2588 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -270,8 +270,8 @@ TEST(CvarQueryTest, RejectsInvalidAlphaSyntaxes) { storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}max=? [ F \"target\" ];", program); - for (auto const& alpha : {"0", "1", "-0.1", "abc", "nan", "inf", "1/0", "0/1", "1/1", "1e"}) { - STORM_SILENT_EXPECT_THROW(storm::api::createCvarProperty(properties.front(), std::string(alpha)), storm::exceptions::InvalidArgumentException); + for (auto const& alpha : {"0", "1", "-0.1", "abc", "0/1", "1/1"}) { + STORM_SILENT_EXPECT_THROW(storm::api::createCvarProperty(properties.front(), std::string(alpha)), storm::exceptions::BaseException); } } From 9e1e802eca7d3271ffad14275c510d04c7830144 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:40:00 +0200 Subject: [PATCH 55/65] Support min-cost and max-reward tails in weighted CVaR LP --- .../cvar/CvarQueryInformation.cpp | 7 +- .../modelchecker/cvar/CvarQueryInformation.h | 3 + .../SparseWeightedReachabilityCvarLpHelper.h | 100 +++++++++++++----- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 11 +- 4 files changed, 90 insertions(+), 31 deletions(-) diff --git a/src/storm/modelchecker/cvar/CvarQueryInformation.cpp b/src/storm/modelchecker/cvar/CvarQueryInformation.cpp index 36153a6708..72ee57c09a 100644 --- a/src/storm/modelchecker/cvar/CvarQueryInformation.cpp +++ b/src/storm/modelchecker/cvar/CvarQueryInformation.cpp @@ -9,6 +9,7 @@ namespace storm { namespace modelchecker { namespace cvar { + CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const& formula) { storm::logic::Formula const& embeddedFormula = formula.getSubformula(); STORM_LOG_THROW(embeddedFormula.isRewardOperatorFormula(), storm::exceptions::InvalidPropertyException, @@ -30,7 +31,11 @@ CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const STORM_LOG_THROW(eventuallyFormula.getSubformula().isStateFormula(), storm::exceptions::InvalidPropertyException, "The target of the embedded reachability reward formula of a CVaR query must be a state formula."); - return {formula.getAlpha(), rewardOperator.getOptimalityType(), rewardOperator.getOptionalRewardModelName(), + auto const optimizationDirection = rewardOperator.getOptimalityType(); + return {formula.getAlpha(), + optimizationDirection, + storm::solver::minimize(optimizationDirection) ? CvarInterpretation::Cost : CvarInterpretation::Reward, + rewardOperator.getOptionalRewardModelName(), eventuallyFormula.getSubformula().asSharedPointer()}; } } // namespace cvar diff --git a/src/storm/modelchecker/cvar/CvarQueryInformation.h b/src/storm/modelchecker/cvar/CvarQueryInformation.h index 1a2a8fc896..634b65d47b 100644 --- a/src/storm/modelchecker/cvar/CvarQueryInformation.h +++ b/src/storm/modelchecker/cvar/CvarQueryInformation.h @@ -11,9 +11,12 @@ namespace storm { namespace modelchecker { namespace cvar { +enum class CvarInterpretation { Cost, Reward }; + struct CvarQueryInformation { storm::RationalNumber alpha; storm::solver::OptimizationDirection optimizationDirection; + CvarInterpretation interpretation; boost::optional rewardModelName; std::shared_ptr targetFormula; }; diff --git a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h index db87d93d65..7660d8512f 100644 --- a/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h @@ -33,9 +33,9 @@ namespace cvar { template struct CvarThresholdData { ValueType threshold; - storm::storage::BitVector targetStatesBelowThreshold; + storm::storage::BitVector targetStatesInStrictTail; storm::storage::BitVector targetStatesAtThreshold; - storm::storage::BitVector targetStatesBelowOrAtThreshold; + storm::storage::BitVector targetStatesInTail; }; template @@ -48,6 +48,7 @@ template struct WeightedReachabilityCvarLpData { storm::RationalNumber alpha; storm::solver::OptimizationDirection optimizationDirection; + CvarInterpretation interpretation; uint64_t initialState; storm::storage::BitVector initialStates; std::string rewardModelName; @@ -109,12 +110,10 @@ class SparseWeightedReachabilityCvarLpHelper { std::optional bestThresholdIndex; std::unique_ptr> bestScheduler; auto candidateRange = computeCandidateRange(env); - storm::storage::BitVector targetStatesBelowThreshold = createPrefixTargetStates(candidateRange.first); for (uint64_t thresholdIndex = candidateRange.first; thresholdIndex < candidateRange.second; ++thresholdIndex) { - auto thresholdData = createThresholdData(thresholdIndex, targetStatesBelowThreshold); + auto thresholdData = createThresholdData(thresholdIndex); auto thresholdResult = buildLpForThreshold(thresholdData, false); if (!thresholdResult.has_value()) { - addBucketStates(targetStatesBelowThreshold, thresholdIndex); continue; } if (!bestValue.has_value()) { @@ -125,7 +124,6 @@ class SparseWeightedReachabilityCvarLpHelper { bestValue = thresholdResult->value; bestThresholdIndex = thresholdIndex; } - addBucketStates(targetStatesBelowThreshold, thresholdIndex); } STORM_LOG_THROW(bestValue.has_value(), storm::exceptions::UnexpectedException, @@ -155,6 +153,7 @@ class SparseWeightedReachabilityCvarLpHelper { weightedReachabilityPreprocessingResult.terminalRewards, reachableStates); return {queryInformation.alpha, queryInformation.optimizationDirection, + queryInformation.interpretation, weightedReachabilityPreprocessingResult.initialState, std::move(initialStates), weightedReachabilityPreprocessingResult.rewardModelName, @@ -192,6 +191,14 @@ class SparseWeightedReachabilityCvarLpHelper { return states; } + storm::storage::BitVector createSuffixTargetStates(uint64_t startBucketIndex) const { + storm::storage::BitVector states(lpData.transitionMatrix.getRowGroupCount(), false); + for (uint64_t bucketIndex = startBucketIndex; bucketIndex < lpData.rewardBuckets.size(); ++bucketIndex) { + addBucketStates(states, bucketIndex); + } + return states; + } + storm::storage::BitVector const& getCachedPrefixTargetStates(uint64_t endBucketIndex, std::map& cache) const { auto cachedPrefix = cache.find(endBucketIndex); if (cachedPrefix != cache.end()) { @@ -200,16 +207,27 @@ class SparseWeightedReachabilityCvarLpHelper { return cache.emplace(endBucketIndex, createPrefixTargetStates(endBucketIndex)).first->second; } - CvarThresholdData createThresholdData(uint64_t thresholdIndex) const { - auto targetStatesBelowThreshold = createPrefixTargetStates(thresholdIndex); - return createThresholdData(thresholdIndex, targetStatesBelowThreshold); + storm::storage::BitVector const& getCachedSuffixTargetStates(uint64_t startBucketIndex, std::map& cache) const { + auto cachedSuffix = cache.find(startBucketIndex); + if (cachedSuffix != cache.end()) { + return cachedSuffix->second; + } + return cache.emplace(startBucketIndex, createSuffixTargetStates(startBucketIndex)).first->second; } - CvarThresholdData createThresholdData(uint64_t thresholdIndex, storm::storage::BitVector const& targetStatesBelowThreshold) const { + CvarThresholdData createThresholdData(uint64_t thresholdIndex) const { auto targetStatesAtThreshold = createBucketTargetStates(thresholdIndex); - auto targetStatesBelowOrAtThreshold = targetStatesBelowThreshold | targetStatesAtThreshold; - return {lpData.rewardBuckets[thresholdIndex].reward, targetStatesBelowThreshold, std::move(targetStatesAtThreshold), - std::move(targetStatesBelowOrAtThreshold)}; + if (lpData.interpretation == CvarInterpretation::Reward) { + auto targetStatesInStrictTail = createPrefixTargetStates(thresholdIndex); + auto targetStatesInTail = targetStatesInStrictTail | targetStatesAtThreshold; + return {lpData.rewardBuckets[thresholdIndex].reward, std::move(targetStatesInStrictTail), std::move(targetStatesAtThreshold), + std::move(targetStatesInTail)}; + } + + auto targetStatesInStrictTail = createSuffixTargetStates(thresholdIndex + 1); + auto targetStatesInTail = targetStatesInStrictTail | targetStatesAtThreshold; + return {lpData.rewardBuckets[thresholdIndex].reward, std::move(targetStatesInStrictTail), std::move(targetStatesAtThreshold), + std::move(targetStatesInTail)}; } ValueType computeReachabilityProbability(Environment const& env, storm::solver::OptimizationDirection direction, @@ -232,15 +250,49 @@ class SparseWeightedReachabilityCvarLpHelper { uint64_t const bucketCount = lpData.rewardBuckets.size(); ValueType const alpha = storm::utility::convertNumber(lpData.alpha); storm::utility::ConstantsComparator comparator(storm::utility::convertNumber(env.solver().minMax().getPrecision())); - std::map prefixTargetStateCache; + + if (lpData.interpretation == CvarInterpretation::Reward) { + std::map prefixTargetStateCache; + + uint64_t lower = 0; + uint64_t upper = bucketCount; + while (lower < upper) { + uint64_t const mid = lower + (upper - lower) / 2; + auto const& targetStatesInTail = getCachedPrefixTargetStates(mid + 1, prefixTargetStateCache); + auto maxReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Maximize, targetStatesInTail); + if (comparator.isLess(maxReachability, alpha)) { + lower = mid + 1; + } else { + upper = mid; + } + } + uint64_t const firstNotTooLow = lower; + + lower = firstNotTooLow; + upper = bucketCount; + while (lower < upper) { + uint64_t const mid = lower + (upper - lower) / 2; + auto const& targetStatesInStrictTail = getCachedPrefixTargetStates(mid, prefixTargetStateCache); + auto minReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Minimize, targetStatesInStrictTail); + if (comparator.isLess(alpha, minReachability)) { + upper = mid; + } else { + lower = mid + 1; + } + } + + return {firstNotTooLow, lower}; + } + + std::map suffixTargetStateCache; uint64_t lower = 0; uint64_t upper = bucketCount; while (lower < upper) { uint64_t const mid = lower + (upper - lower) / 2; - auto const& targetStatesBelowOrAtThreshold = getCachedPrefixTargetStates(mid + 1, prefixTargetStateCache); - auto maxReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Maximize, targetStatesBelowOrAtThreshold); - if (comparator.isLess(maxReachability, alpha)) { + auto const& targetStatesInStrictTail = getCachedSuffixTargetStates(mid + 1, suffixTargetStateCache); + auto minReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Minimize, targetStatesInStrictTail); + if (comparator.isLess(alpha, minReachability)) { lower = mid + 1; } else { upper = mid; @@ -252,9 +304,9 @@ class SparseWeightedReachabilityCvarLpHelper { upper = bucketCount; while (lower < upper) { uint64_t const mid = lower + (upper - lower) / 2; - auto const& targetStatesBelowThreshold = getCachedPrefixTargetStates(mid, prefixTargetStateCache); - auto minReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Minimize, targetStatesBelowThreshold); - if (comparator.isLess(alpha, minReachability)) { + auto const& targetStatesInTail = getCachedSuffixTargetStates(mid, suffixTargetStateCache); + auto maxReachability = computeReachabilityProbability(env, storm::solver::OptimizationDirection::Maximize, targetStatesInTail); + if (comparator.isLess(maxReachability, alpha)) { upper = mid; } else { lower = mid + 1; @@ -287,7 +339,7 @@ class SparseWeightedReachabilityCvarLpHelper { std::vector> splitFlowVariables(lpData.transitionMatrix.getRowGroupCount(), std::nullopt); for (uint64_t state = 0; state < lpData.transitionMatrix.getRowGroupCount(); ++state) { - if (thresholdData.targetStatesBelowOrAtThreshold[state]) { + if (thresholdData.targetStatesInTail[state]) { splitFlowVariables[state] = solver->addLowerBoundedContinuousVariable("xb_" + std::to_string(state), storm::utility::zero(), lpData.terminalRewards[state]); } @@ -329,7 +381,7 @@ class SparseWeightedReachabilityCvarLpHelper { } solver->addConstraint("recurrent_behaviour", recurrentConstraint); - for (auto state : thresholdData.targetStatesBelowThreshold) { + for (auto state : thresholdData.targetStatesInStrictTail) { RawLpConstraint splitEqualityConstraint(storm::expressions::RelationType::Equal, storm::utility::zero(), 2); splitEqualityConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); splitEqualityConstraint.addToLhs(recurrentFlowVariables[state].value(), -storm::utility::one()); @@ -343,8 +395,8 @@ class SparseWeightedReachabilityCvarLpHelper { } RawLpConstraint probabilityConsistentSplitConstraint(storm::expressions::RelationType::Equal, storm::utility::convertNumber(lpData.alpha), - thresholdData.targetStatesBelowOrAtThreshold.getNumberOfSetBits()); - for (auto state : thresholdData.targetStatesBelowOrAtThreshold) { + thresholdData.targetStatesInTail.getNumberOfSetBits()); + for (auto state : thresholdData.targetStatesInTail) { probabilityConsistentSplitConstraint.addToLhs(splitFlowVariables[state].value(), storm::utility::one()); } solver->addConstraint("probability_consistent_split", probabilityConsistentSplitConstraint); diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index ffc17d2588..625913f0d7 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -168,7 +168,7 @@ TEST(CvarQueryTest, SimpleMdp) { auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); double minValue = checkInitialStateValue(minInput); - EXPECT_NEAR(minValue, 5.0 / 3.0, 1e-10); + EXPECT_NEAR(minValue, 2.0, 1e-10); } TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { @@ -185,7 +185,7 @@ TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", alpha); double minValue = checkInitialStateValue(minInput); - EXPECT_NEAR(minValue, 0.0, 1e-10); + EXPECT_NEAR(minValue, 4.0, 1e-10); } TEST(CvarQueryTest, TargetReachingMecIsCollapsed) { @@ -228,11 +228,10 @@ TEST(CvarQueryTest, BranchingTradeoffMdp) { EXPECT_NEAR(checkInitialStateValue(maxThreeQuarterInput), 8.0, 1e-10); auto minHalfInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.5"); - EXPECT_NEAR(checkInitialStateValue(minHalfInput), 0.0, 1e-10); + EXPECT_NEAR(checkInitialStateValue(minHalfInput), 7.0, 1e-10); - // this requires randomization of the strategy auto minThreeQuarterInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.75"); - EXPECT_NEAR(checkInitialStateValue(minThreeQuarterInput), 14.0 / 3.0, 1e-10); + EXPECT_NEAR(checkInitialStateValue(minThreeQuarterInput), 7.0, 1e-10); } TEST(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { @@ -246,7 +245,7 @@ TEST(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { EXPECT_EQ(storm::RationalNumber(8), checkInitialStateValue(maxInput)); auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.75"); - EXPECT_EQ(storm::RationalNumber("14/3"), checkInitialStateValue(minInput)); + EXPECT_EQ(storm::RationalNumber(7), checkInitialStateValue(minInput)); } TEST(CvarQueryTest, EquivalentExactAlphaSyntaxes) { From 6a23e478837b9aa3fada494baeda3612a3a0e230 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:14:58 +0200 Subject: [PATCH 56/65] Reduced option names --- src/storm/settings/modules/CvarSettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storm/settings/modules/CvarSettings.cpp b/src/storm/settings/modules/CvarSettings.cpp index c0490fe615..444e610f45 100644 --- a/src/storm/settings/modules/CvarSettings.cpp +++ b/src/storm/settings/modules/CvarSettings.cpp @@ -15,7 +15,7 @@ std::string const CvarSettings::moduleName = "cvar"; std::string const CvarSettings::methodOptionName = "method"; CvarSettings::CvarSettings() : ModuleSettings(moduleName) { - std::vector methods = {"auto", "wr", "weighted-reachability", "ssp", "ssp-vi", "pareto-vi"}; + std::vector methods = {"auto", "wr", "weighted-reachability", "ssp"}; this->addOption(storm::settings::OptionBuilder(moduleName, methodOptionName, true, "The method to be used for CVaR model checking.") .setIsAdvanced() .addArgument(storm::settings::ArgumentBuilder::createStringArgument("name", "The name of the method to use.") From 1e06960334224f15ccbe9c349521b4be28f3f4ba Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:24:05 +0200 Subject: [PATCH 57/65] Add explicit CVaR reward interpretation selection --- .../CvarModelCheckerEnvironment.cpp | 9 +++ .../CvarModelCheckerEnvironment.h | 4 ++ .../modelchecker/cvar/CvarClassification.h | 2 +- .../modelchecker/cvar/CvarModelChecking.cpp | 14 +++-- .../modelchecker/cvar/CvarModelChecking.h | 7 +-- .../cvar/CvarQueryInformation.cpp | 9 ++- .../modelchecker/cvar/CvarQueryInformation.h | 8 +-- .../cvar/preprocessing/SspCvarPreprocessor.h | 4 +- .../WeightedReachabilityCvarPreprocessor.h | 2 +- src/storm/settings/modules/CvarSettings.cpp | 21 +++++++ src/storm/settings/modules/CvarSettings.h | 9 +++ .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 62 +++++++++++++++++++ 12 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp index 99618a375a..5046fea488 100644 --- a/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp +++ b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.cpp @@ -8,6 +8,7 @@ namespace storm { CvarModelCheckerEnvironment::CvarModelCheckerEnvironment() { auto const& cvarSettings = storm::settings::getModule(); method = cvarSettings.getCvarMethod(); + interpretationSelection = cvarSettings.getInterpretationSelection(); } CvarModelCheckerEnvironment::~CvarModelCheckerEnvironment() { @@ -22,4 +23,12 @@ void CvarModelCheckerEnvironment::setMethod(storm::modelchecker::cvar::CvarMetho method = value; } +storm::modelchecker::cvar::CvarInterpretationSelection const& CvarModelCheckerEnvironment::getInterpretationSelection() const { + return interpretationSelection; +} + +void CvarModelCheckerEnvironment::setInterpretationSelection(storm::modelchecker::cvar::CvarInterpretationSelection value) { + interpretationSelection = value; +} + } // namespace storm diff --git a/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h index 3c9f142382..ffea10b5f9 100644 --- a/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h +++ b/src/storm/environment/modelchecker/CvarModelCheckerEnvironment.h @@ -1,5 +1,6 @@ #pragma once +#include "storm/modelchecker/cvar/CvarInterpretation.h" #include "storm/modelchecker/cvar/CvarMethod.h" namespace storm { @@ -11,9 +12,12 @@ class CvarModelCheckerEnvironment { storm::modelchecker::cvar::CvarMethod const& getMethod() const; void setMethod(storm::modelchecker::cvar::CvarMethod value); + storm::modelchecker::cvar::CvarInterpretationSelection const& getInterpretationSelection() const; + void setInterpretationSelection(storm::modelchecker::cvar::CvarInterpretationSelection value); private: storm::modelchecker::cvar::CvarMethod method; + storm::modelchecker::cvar::CvarInterpretationSelection interpretationSelection; }; } // namespace storm diff --git a/src/storm/modelchecker/cvar/CvarClassification.h b/src/storm/modelchecker/cvar/CvarClassification.h index bf7d5a68f0..8de9d1203c 100644 --- a/src/storm/modelchecker/cvar/CvarClassification.h +++ b/src/storm/modelchecker/cvar/CvarClassification.h @@ -31,7 +31,7 @@ enum class CvarBackendKind { WeightedReachability, Ssp }; template CvarBackendKind selectCvarBackend(SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, storm::storage::BitVector const&, CvarMethod method) { - std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; + std::string rewardModelName = queryInformation.rewardModelName.value_or(""); auto const& rewardModel = model.getRewardModel(rewardModelName); if (rewardModelName.empty()) { rewardModelName = model.getUniqueRewardModelName(); diff --git a/src/storm/modelchecker/cvar/CvarModelChecking.cpp b/src/storm/modelchecker/cvar/CvarModelChecking.cpp index a49c318beb..641dea840d 100644 --- a/src/storm/modelchecker/cvar/CvarModelChecking.cpp +++ b/src/storm/modelchecker/cvar/CvarModelChecking.cpp @@ -3,6 +3,8 @@ #include "storm/adapters/RationalNumberAdapter.h" #include "storm/environment/Environment.h" #include "storm/exceptions/InvalidOperationException.h" +#include "storm/exceptions/InvalidPropertyException.h" +#include "storm/modelchecker/cvar/CvarInterpretation.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" @@ -14,18 +16,20 @@ namespace modelchecker { namespace cvar { template -std::unique_ptr performCvarModelChecking( - Environment const& env, SparseMdpModelType const& model, - CheckTask const& checkTask, - std::function const& formulaChecker) { +std::unique_ptr performCvarModelChecking(Environment const& env, SparseMdpModelType const& model, + CheckTask const& checkTask, + std::function const& formulaChecker) { using ValueType = typename SparseMdpModelType::ValueType; STORM_LOG_THROW(checkTask.isOnlyInitialStatesRelevantSet(), storm::exceptions::InvalidOperationException, "Computing CVaR is only supported for the initial states of a model."); STORM_LOG_THROW(model.getInitialStates().getNumberOfSetBits() == 1, storm::exceptions::InvalidOperationException, "CVaR is not supported on models with multiple initial states."); + STORM_LOG_THROW(checkTask.isOptimizationDirectionSet(), storm::exceptions::InvalidPropertyException, + "The embedded reward operator formula of a CVaR query must specify whether to minimize or maximize."); - auto cvarQueryInformation = extractCvarQueryInformation(checkTask.getFormula()); + auto interpretation = resolveCvarInterpretation(env.modelchecker().cvar().getInterpretationSelection(), checkTask.getOptimizationDirection()); + auto cvarQueryInformation = extractCvarQueryInformation(checkTask.getFormula(), interpretation); auto targetStates = formulaChecker(*cvarQueryInformation.targetFormula); SparseCvarComputationHelper cvarHelper(model, cvarQueryInformation, targetStates); diff --git a/src/storm/modelchecker/cvar/CvarModelChecking.h b/src/storm/modelchecker/cvar/CvarModelChecking.h index 74021dd677..33795430b5 100644 --- a/src/storm/modelchecker/cvar/CvarModelChecking.h +++ b/src/storm/modelchecker/cvar/CvarModelChecking.h @@ -20,10 +20,9 @@ namespace modelchecker { namespace cvar { template -std::unique_ptr performCvarModelChecking( - Environment const& env, SparseMdpModelType const& model, - CheckTask const& checkTask, - std::function const& formulaChecker); +std::unique_ptr performCvarModelChecking(Environment const& env, SparseMdpModelType const& model, + CheckTask const& checkTask, + std::function const& formulaChecker); } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/cvar/CvarQueryInformation.cpp b/src/storm/modelchecker/cvar/CvarQueryInformation.cpp index 72ee57c09a..63c47fed4b 100644 --- a/src/storm/modelchecker/cvar/CvarQueryInformation.cpp +++ b/src/storm/modelchecker/cvar/CvarQueryInformation.cpp @@ -10,7 +10,7 @@ namespace storm { namespace modelchecker { namespace cvar { -CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const& formula) { +CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const& formula, CvarInterpretation interpretation) { storm::logic::Formula const& embeddedFormula = formula.getSubformula(); STORM_LOG_THROW(embeddedFormula.isRewardOperatorFormula(), storm::exceptions::InvalidPropertyException, "CVaR formulas currently require an embedded reward operator formula."); @@ -32,10 +32,9 @@ CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const "The target of the embedded reachability reward formula of a CVaR query must be a state formula."); auto const optimizationDirection = rewardOperator.getOptimalityType(); - return {formula.getAlpha(), - optimizationDirection, - storm::solver::minimize(optimizationDirection) ? CvarInterpretation::Cost : CvarInterpretation::Reward, - rewardOperator.getOptionalRewardModelName(), + auto const& optionalRewardModelName = rewardOperator.getOptionalRewardModelName(); + return {formula.getAlpha(), optimizationDirection, interpretation, + optionalRewardModelName ? std::optional(optionalRewardModelName.get()) : std::nullopt, eventuallyFormula.getSubformula().asSharedPointer()}; } } // namespace cvar diff --git a/src/storm/modelchecker/cvar/CvarQueryInformation.h b/src/storm/modelchecker/cvar/CvarQueryInformation.h index 634b65d47b..2b92a99865 100644 --- a/src/storm/modelchecker/cvar/CvarQueryInformation.h +++ b/src/storm/modelchecker/cvar/CvarQueryInformation.h @@ -1,27 +1,27 @@ #pragma once #include +#include #include #include "storm/adapters/RationalNumberAdapter.h" #include "storm/logic/CvarFormula.h" +#include "storm/modelchecker/cvar/CvarInterpretation.h" #include "storm/solver/OptimizationDirection.h" namespace storm { namespace modelchecker { namespace cvar { -enum class CvarInterpretation { Cost, Reward }; - struct CvarQueryInformation { storm::RationalNumber alpha; storm::solver::OptimizationDirection optimizationDirection; CvarInterpretation interpretation; - boost::optional rewardModelName; + std::optional rewardModelName; std::shared_ptr targetFormula; }; -CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const& formula); +CvarQueryInformation extractCvarQueryInformation(storm::logic::CvarFormula const& formula, CvarInterpretation interpretation); } // namespace cvar } // namespace modelchecker diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index df373ba02a..559ead6ef0 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -106,7 +106,7 @@ SspCvarPreprocessingResult preprocessSsp storm::storage::BitVector const& targetStates) { using ValueType = typename SparseMdpModelType::ValueType; - std::string rewardModelName = queryInformation.rewardModelName ? queryInformation.rewardModelName.get() : ""; + std::string rewardModelName = queryInformation.rewardModelName.value_or(""); auto const& rewardModel = model.getRewardModel(rewardModelName); if (rewardModelName.empty()) { rewardModelName = model.getUniqueRewardModelName(); @@ -114,6 +114,8 @@ SspCvarPreprocessingResult preprocessSsp STORM_LOG_THROW(queryInformation.optimizationDirection == storm::solver::OptimizationDirection::Minimize, storm::exceptions::InvalidPropertyException, "CVaR SSP preprocessing currently only supports minimizing total costs."); + STORM_LOG_THROW(queryInformation.interpretation == CvarInterpretation::Cost, storm::exceptions::InvalidPropertyException, + "CVaR SSP preprocessing currently only supports the cost interpretation."); STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, "CVaR SSP preprocessing does not support transition rewards."); diff --git a/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h index 639e72c299..fcb9181b61 100644 --- a/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h @@ -85,7 +85,7 @@ WeightedReachabilityCvarPreprocessingResult methods = {"auto", "wr", "weighted-reachability", "ssp"}; + std::vector interpretations = {"auto", "cost", "reward"}; this->addOption(storm::settings::OptionBuilder(moduleName, methodOptionName, true, "The method to be used for CVaR model checking.") .setIsAdvanced() .addArgument(storm::settings::ArgumentBuilder::createStringArgument("name", "The name of the method to use.") @@ -23,6 +25,13 @@ CvarSettings::CvarSettings() : ModuleSettings(moduleName) { .setDefaultValueString("auto") .build()) .build()); + this->addOption(storm::settings::OptionBuilder(moduleName, interpretationOptionName, true, "The interpretation to be used for CVaR model checking.") + .setIsAdvanced() + .addArgument(storm::settings::ArgumentBuilder::createStringArgument("name", "The name of the interpretation to use.") + .addValidatorString(ArgumentValidatorFactory::createMultipleChoiceValidator(interpretations)) + .setDefaultValueString("auto") + .build()) + .build()); } storm::modelchecker::cvar::CvarMethod CvarSettings::getCvarMethod() const { @@ -37,6 +46,18 @@ storm::modelchecker::cvar::CvarMethod CvarSettings::getCvarMethod() const { STORM_LOG_THROW(false, storm::exceptions::IllegalArgumentValueException, "Unknown CVaR method '" << methodAsString << "'."); } +storm::modelchecker::cvar::CvarInterpretationSelection CvarSettings::getInterpretationSelection() const { + std::string interpretationAsString = this->getOption(interpretationOptionName).getArgumentByName("name").getValueAsString(); + if (interpretationAsString == "auto") { + return storm::modelchecker::cvar::CvarInterpretationSelection::Auto; + } else if (interpretationAsString == "cost") { + return storm::modelchecker::cvar::CvarInterpretationSelection::Cost; + } else if (interpretationAsString == "reward") { + return storm::modelchecker::cvar::CvarInterpretationSelection::Reward; + } + STORM_LOG_THROW(false, storm::exceptions::IllegalArgumentValueException, "Unknown CVaR interpretation '" << interpretationAsString << "'."); +} + } // namespace modules } // namespace settings } // namespace storm diff --git a/src/storm/settings/modules/CvarSettings.h b/src/storm/settings/modules/CvarSettings.h index 0d5718efca..2e7f223004 100644 --- a/src/storm/settings/modules/CvarSettings.h +++ b/src/storm/settings/modules/CvarSettings.h @@ -1,6 +1,7 @@ #ifndef STORM_SETTINGS_MODULES_CVARSETTINGS_H_ #define STORM_SETTINGS_MODULES_CVARSETTINGS_H_ +#include "storm/modelchecker/cvar/CvarInterpretation.h" #include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/settings/modules/ModuleSettings.h" @@ -25,10 +26,18 @@ class CvarSettings : public ModuleSettings { */ storm::modelchecker::cvar::CvarMethod getCvarMethod() const; + /*! + * Retrieves the selected CVaR interpretation policy. + * + * @return The selected CVaR interpretation policy. + */ + storm::modelchecker::cvar::CvarInterpretationSelection getInterpretationSelection() const; + static std::string const moduleName; private: static std::string const methodOptionName; + static std::string const interpretationOptionName; }; } // namespace modules diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 625913f0d7..5309828d79 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -12,6 +12,7 @@ #include "storm/exceptions/InvalidPropertyException.h" #include "storm/logic/CvarFormula.h" #include "storm/modelchecker/CheckTask.h" +#include "storm/modelchecker/cvar/CvarInterpretation.h" #include "storm/modelchecker/cvar/CvarMethod.h" #include "storm/modelchecker/cvar/helper/SspParetoFront.h" #include "storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h" @@ -72,6 +73,19 @@ template ValueType checkInitialStateValueWithMethod(CvarTestInput const& input, storm::modelchecker::cvar::CvarMethod method) { storm::Environment env; env.modelchecker().cvar().setMethod(method); + env.modelchecker().cvar().setInterpretationSelection(storm::modelchecker::cvar::CvarInterpretationSelection::Auto); + storm::modelchecker::SparseMdpPrctlModelChecker> checker(*input.mdp); + storm::modelchecker::CheckTask task(*input.formula, true); + auto result = checker.check(env, task); + return result->template asExplicitQuantitativeCheckResult().getMax(); +} + +template +ValueType checkInitialStateValueWithMethodAndInterpretationSelection(CvarTestInput const& input, storm::modelchecker::cvar::CvarMethod method, + storm::modelchecker::cvar::CvarInterpretationSelection interpretationSelection) { + storm::Environment env; + env.modelchecker().cvar().setMethod(method); + env.modelchecker().cvar().setInterpretationSelection(interpretationSelection); storm::modelchecker::SparseMdpPrctlModelChecker> checker(*input.mdp); storm::modelchecker::CheckTask task(*input.formula, true); auto result = checker.check(env, task); @@ -248,6 +262,54 @@ TEST(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { EXPECT_EQ(storm::RationalNumber(7), checkInitialStateValue(minInput)); } +TEST(CvarQueryTest, InterpretationOverridesOnSimpleMdp) { + if (!hasLpSolver()) { + GTEST_SKIP() << "No LP solver available."; + } + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; + + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "0.75"); + EXPECT_NEAR(checkInitialStateValueWithMethodAndInterpretationSelection(maxInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Reward), + 2.0, 1e-10); + EXPECT_NEAR(checkInitialStateValueWithMethodAndInterpretationSelection(maxInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Cost), + 7.0 / 3.0, 1e-10); + + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.75"); + EXPECT_NEAR(checkInitialStateValueWithMethodAndInterpretationSelection(minInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Cost), + 2.0, 1e-10); + EXPECT_NEAR(checkInitialStateValueWithMethodAndInterpretationSelection(minInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Reward), + 5.0 / 3.0, 1e-10); +} + +TEST(CvarQueryTest, InterpretationOverridesOnSimpleMdpRationalNumbers) { + if (!hasExactLpSolver()) { + GTEST_SKIP() << "No exact LP solver available."; + } + + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; + + auto maxInput = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "0.75"); + EXPECT_EQ(storm::RationalNumber(2), + checkInitialStateValueWithMethodAndInterpretationSelection(maxInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Reward)); + EXPECT_EQ(storm::RationalNumber("7/3"), + checkInitialStateValueWithMethodAndInterpretationSelection(maxInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Cost)); + + auto minInput = buildCvarInput(modelPath, "R{\"term\"}min=? [ F \"target\" ];", "0.75"); + EXPECT_EQ(storm::RationalNumber(2), + checkInitialStateValueWithMethodAndInterpretationSelection(minInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Cost)); + EXPECT_EQ(storm::RationalNumber("5/3"), + checkInitialStateValueWithMethodAndInterpretationSelection(minInput, storm::modelchecker::cvar::CvarMethod::WeightedReachability, + storm::modelchecker::cvar::CvarInterpretationSelection::Reward)); +} + TEST(CvarQueryTest, EquivalentExactAlphaSyntaxes) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; From 11585987487f7af82b85d05473fd8c69711a361e Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:24:05 +0200 Subject: [PATCH 58/65] Add CVaR interpretation type header --- .../modelchecker/cvar/CvarInterpretation.h | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/storm/modelchecker/cvar/CvarInterpretation.h diff --git a/src/storm/modelchecker/cvar/CvarInterpretation.h b/src/storm/modelchecker/cvar/CvarInterpretation.h new file mode 100644 index 0000000000..eaed7778ee --- /dev/null +++ b/src/storm/modelchecker/cvar/CvarInterpretation.h @@ -0,0 +1,29 @@ +#pragma once + +#include "storm/solver/OptimizationDirection.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +enum class CvarInterpretation { Cost, Reward }; + +enum class CvarInterpretationSelection { Auto, Cost, Reward }; + +inline CvarInterpretation resolveCvarInterpretation(CvarInterpretationSelection selection, storm::solver::OptimizationDirection optimizationDirection) { + switch (selection) { + case CvarInterpretationSelection::Auto: + return storm::solver::minimize(optimizationDirection) ? CvarInterpretation::Cost : CvarInterpretation::Reward; + case CvarInterpretationSelection::Cost: + return CvarInterpretation::Cost; + case CvarInterpretationSelection::Reward: + return CvarInterpretation::Reward; + } + STORM_LOG_ASSERT(false, "Encountered an unknown CVaR interpretation selection."); + return CvarInterpretation::Cost; +} + +} // namespace cvar +} // namespace modelchecker +} // namespace storm From f4306412fd51881e0acc3d0ed7ddb3620d76f6e4 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:36:18 +0200 Subject: [PATCH 59/65] Generalize SSP Pareto frontier orientation --- .../modelchecker/cvar/helper/SspParetoFront.h | 110 +++++++++++++----- .../helper/SspParetoValueIterationOperator.h | 16 ++- 2 files changed, 91 insertions(+), 35 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index 1406ee0825..3868ae2807 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -16,17 +16,23 @@ namespace storm { namespace modelchecker { namespace cvar { +enum class SspParetoFrontKind { CostUpperTail, RewardLowerTail }; + /*! - * Represents the lower-right boundary of one SSP CVaR Pareto set from the paper. + * Represents one SSP CVaR Pareto frontier. * - * For a fixed state and cost bound n, each point (p, E) represents an achievable tradeoff where: + * For the cost-side upper-tail algorithm, each point (p, E) represents an achievable tradeoff where: * - p is the probability of reaching the goal within the current cost bound, and * - E is the expected continuation cost beyond that bound. * - * The class only stores extremal boundary points. The upward/leftward closed polygon itself is induced by - * these points together with convex closure. + * For the reward-side lower-tail algorithm, each point (p, e) represents an achievable tradeoff where: + * - p is the probability of accumulating reward at most the current threshold, and + * - e is the expected shortfall below that threshold. + * + * The class only stores extremal boundary points. The closed polygon itself is induced by these points together + * with convex closure. Cost frontiers use the original lower-right order; reward frontiers use lower-left order. */ -template +template class SspParetoFront { public: struct Point { @@ -41,11 +47,20 @@ class SspParetoFront { if (probability == other.probability && expectedCost == other.expectedCost) { return DominanceResult::Equal; } - if (probability >= other.probability && expectedCost <= other.expectedCost) { - return DominanceResult::Dominates; - } - if (probability <= other.probability && expectedCost >= other.expectedCost) { - return DominanceResult::Dominated; + if constexpr (FrontKind == SspParetoFrontKind::CostUpperTail) { + if (probability >= other.probability && expectedCost <= other.expectedCost) { + return DominanceResult::Dominates; + } + if (probability <= other.probability && expectedCost >= other.expectedCost) { + return DominanceResult::Dominated; + } + } else { + if (probability <= other.probability && expectedCost <= other.expectedCost) { + return DominanceResult::Dominates; + } + if (probability >= other.probability && expectedCost >= other.expectedCost) { + return DominanceResult::Dominated; + } } return DominanceResult::Incomparable; } @@ -466,26 +481,48 @@ class SspParetoFront { return; } - std::size_t writeIndex = points.size(); - std::size_t index = points.size(); - bool hasBestExpectedCostSeenFromRight = false; - ValueType bestExpectedCostSeenFromRight{}; - while (index > 0) { - std::size_t const groupEnd = index; - ValueType const probability = points[groupEnd - 1].probability; - while (index > 0 && points[index - 1].probability == probability) { - --index; + if constexpr (FrontKind == SspParetoFrontKind::CostUpperTail) { + std::size_t writeIndex = points.size(); + std::size_t index = points.size(); + bool hasBestExpectedCostSeenFromRight = false; + ValueType bestExpectedCostSeenFromRight{}; + while (index > 0) { + std::size_t const groupEnd = index; + ValueType const probability = points[groupEnd - 1].probability; + while (index > 0 && points[index - 1].probability == probability) { + --index; + } + + Point const& bestPointForProbability = points[index]; + if (!hasBestExpectedCostSeenFromRight || bestPointForProbability.expectedCost < bestExpectedCostSeenFromRight) { + --writeIndex; + points[writeIndex] = bestPointForProbability; + bestExpectedCostSeenFromRight = bestPointForProbability.expectedCost; + hasBestExpectedCostSeenFromRight = true; + } } + points.erase(points.begin(), points.begin() + static_cast(writeIndex)); + } else { + std::size_t writeIndex = 0; + std::size_t index = 0; + bool hasBestExpectedCostSeenFromLeft = false; + ValueType bestExpectedCostSeenFromLeft{}; + while (index < points.size()) { + ValueType const probability = points[index].probability; + Point const& bestPointForProbability = points[index]; + while (index < points.size() && points[index].probability == probability) { + ++index; + } - Point const& bestPointForProbability = points[index]; - if (!hasBestExpectedCostSeenFromRight || bestPointForProbability.expectedCost < bestExpectedCostSeenFromRight) { - --writeIndex; - points[writeIndex] = bestPointForProbability; - bestExpectedCostSeenFromRight = bestPointForProbability.expectedCost; - hasBestExpectedCostSeenFromRight = true; + if (!hasBestExpectedCostSeenFromLeft || bestPointForProbability.expectedCost < bestExpectedCostSeenFromLeft) { + points[writeIndex] = bestPointForProbability; + ++writeIndex; + bestExpectedCostSeenFromLeft = bestPointForProbability.expectedCost; + hasBestExpectedCostSeenFromLeft = true; + } } + points.resize(writeIndex); } - points.erase(points.begin(), points.begin() + static_cast(writeIndex)); } void removeNonExtremeConvexPoints() { @@ -501,11 +538,19 @@ class SspParetoFront { } } points.resize(hullSize); - STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), - [](Point const& left, Point const& right) { - return left.probability >= right.probability || left.expectedCost >= right.expectedCost; - }) == points.end(), - "Expected SSP Pareto front points to be strictly ordered by increasing probability and increasing expected cost."); + if constexpr (FrontKind == SspParetoFrontKind::CostUpperTail) { + STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), + [](Point const& left, Point const& right) { + return left.probability >= right.probability || left.expectedCost >= right.expectedCost; + }) == points.end(), + "Expected cost SSP Pareto front points to be strictly ordered by increasing probability and increasing expected cost."); + } else { + STORM_LOG_ASSERT(std::adjacent_find(points.begin(), points.end(), + [](Point const& left, Point const& right) { + return left.probability >= right.probability || left.expectedCost <= right.expectedCost; + }) == points.end(), + "Expected reward SSP Pareto front points to be strictly ordered by increasing probability and decreasing expected shortfall."); + } } static bool liesOnOrAboveSegment(Point const& left, Point const& middle, Point const& right) { @@ -520,6 +565,9 @@ class SspParetoFront { container_type points; }; +template +using SspRewardParetoFront = SspParetoFront; + } // namespace cvar } // namespace modelchecker } // namespace storm diff --git a/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h b/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h index fba620a715..307f4583ed 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h @@ -21,10 +21,10 @@ namespace cvar { * cost-window semantics explicit: each action row reads from the predecessor layer determined by the current * cost bound minus that action's integer cost. */ -template +template class SspParetoValueIterationOperator { public: - using ParetoFront = SspParetoFront; + using ParetoFront = SspParetoFront; using FrontierLayer = std::vector; using FrontierWindow = std::vector; @@ -37,7 +37,7 @@ class SspParetoValueIterationOperator { void apply(uint64_t costBound, FrontierWindow const& frontierWindow, FrontierLayer& outputLayer) const { prepareOutputLayer(outputLayer); - ParetoFront const targetFront = createTargetFrontier(); + ParetoFront const targetFront = createTargetFrontier(costBound); for (auto state : reachableTargetStates) { outputLayer[state] = targetFront; } @@ -72,7 +72,15 @@ class SspParetoValueIterationOperator { } static ParetoFront createTargetFrontier() { - return ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + return createTargetFrontier(0); + } + + static ParetoFront createTargetFrontier(uint64_t costBound) { + if constexpr (FrontKind == SspParetoFrontKind::CostUpperTail) { + return ParetoFront::singleton(storm::utility::one(), storm::utility::zero()); + } else { + return ParetoFront::singleton(storm::utility::one(), storm::utility::convertNumber(costBound)); + } } private: From c64e70a8dae98874e14108ea43172c5da11c7cf8 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:36:43 +0200 Subject: [PATCH 60/65] Add SSP reward preprocessing checks --- .../cvar/helper/SparseCvarComputationHelper.h | 19 ++- .../cvar/preprocessing/SspCvarPreprocessor.h | 121 ++++++++++++++---- 2 files changed, 111 insertions(+), 29 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h index 27eb3437a5..999c2b0cb2 100644 --- a/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseCvarComputationHelper.h @@ -2,12 +2,14 @@ #include "storm/environment/Environment.h" #include "storm/environment/modelchecker/ModelCheckerEnvironment.h" +#include "storm/exceptions/InvalidPropertyException.h" #include "storm/exceptions/NotImplementedException.h" #include "storm/exceptions/UnexpectedException.h" #include "storm/modelchecker/cvar/CvarClassification.h" #include "storm/modelchecker/cvar/CvarComputationResult.h" #include "storm/modelchecker/cvar/CvarQueryInformation.h" #include "storm/modelchecker/cvar/helper/SparseSspCvarParetoViHelper.h" +#include "storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h" #include "storm/modelchecker/cvar/helper/SparseWeightedReachabilityCvarLpHelper.h" #include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h" #include "storm/modelchecker/cvar/preprocessing/WeightedReachabilityCvarPreprocessor.h" @@ -38,9 +40,20 @@ class SparseCvarComputationHelper { return cvarHelper.computeCvar(env, produceScheduler); } case CvarBackendKind::Ssp: { - auto sspPreprocessingResult = preprocessing::preprocessSspCvar(env, model, queryInformation, targetStates); - SparseSspCvarParetoViHelper cvarHelper(queryInformation, sspPreprocessingResult); - return cvarHelper.computeCvar(env, produceScheduler); + if (queryInformation.optimizationDirection == storm::solver::OptimizationDirection::Minimize && + queryInformation.interpretation == CvarInterpretation::Cost) { + auto sspPreprocessingResult = preprocessing::preprocessSspCvar(env, model, queryInformation, targetStates); + SparseSspCvarParetoViHelper cvarHelper(queryInformation, sspPreprocessingResult); + return cvarHelper.computeCvar(env, produceScheduler); + } + if (queryInformation.optimizationDirection == storm::solver::OptimizationDirection::Maximize && + queryInformation.interpretation == CvarInterpretation::Reward) { + auto sspPreprocessingResult = preprocessing::preprocessSspRewardCvar(env, model, queryInformation, targetStates); + SparseSspRewardCvarParetoViHelper cvarHelper(queryInformation, sspPreprocessingResult); + return cvarHelper.computeCvar(env, produceScheduler); + } + STORM_LOG_THROW(false, storm::exceptions::InvalidPropertyException, + "CVaR SSP value iteration currently supports only minimizing costs and maximizing rewards."); } } STORM_LOG_THROW(false, storm::exceptions::UnexpectedException, "Encountered an unknown CVaR backend."); diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index 559ead6ef0..940a8b66f2 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -59,7 +59,7 @@ std::vector extractChoiceCostsForSsp(Spa template void validatePositiveChoiceCostsOutsideGoals(storm::storage::SparseMatrix const& transitionMatrix, storm::storage::BitVector const& targetStates, - std::vector const& choiceCosts) { + std::vector const& choiceCosts, std::string const& valueName = "choice costs") { ValueType const zero = storm::utility::zero(); for (uint64_t state = 0; state < transitionMatrix.getRowGroupCount(); ++state) { if (targetStates[state]) { @@ -67,14 +67,15 @@ void validatePositiveChoiceCostsOutsideGoals(storm::storage::SparseMatrix zero, storm::exceptions::InvalidPropertyException, - "CVaR SSP preprocessing currently requires strictly positive choice costs outside goal states."); + "CVaR SSP preprocessing currently requires strictly positive " << valueName << " outside goal states."); } } } template uint64_t validateAndComputeMaximalChoiceCostOutsideGoals(storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::BitVector const& targetStates, std::vector const& choiceCosts) { + storm::storage::BitVector const& targetStates, std::vector const& choiceCosts, + std::string const& valueName = "choice costs") { uint64_t maximalChoiceCost = 0; for (uint64_t state = 0; state < transitionMatrix.getRowGroupCount(); ++state) { if (targetStates[state]) { @@ -82,7 +83,7 @@ uint64_t validateAndComputeMaximalChoiceCostOutsideGoals(storm::storage::SparseM } for (uint64_t row = transitionMatrix.getRowGroupIndices()[state], endRow = transitionMatrix.getRowGroupIndices()[state + 1]; row < endRow; ++row) { STORM_LOG_THROW(storm::utility::isInteger(choiceCosts[row]), storm::exceptions::InvalidPropertyException, - "CVaR SSP preprocessing currently requires integer-valued choice costs."); + "CVaR SSP preprocessing currently requires integer-valued " << valueName << "."); maximalChoiceCost = std::max(maximalChoiceCost, storm::utility::convertNumber(choiceCosts[row])); } } @@ -100,6 +101,44 @@ std::vector computeExpectedCostsToGoal(Environment const& env, storm: return std::move(result.values); } +template +bool normalizeTargetStatesToAbsorbing(storm::storage::SparseMatrix& transitionMatrix, storm::storage::BitVector const& targetStates) { + bool normalizedTargetStatesToAbsorbing = false; + for (auto targetState : targetStates) { + for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; + ++row) { + for (auto const& entry : transitionMatrix.getRow(row)) { + if (entry.getColumn() != targetState) { + normalizedTargetStatesToAbsorbing = true; + break; + } + } + if (normalizedTargetStatesToAbsorbing) { + break; + } + } + if (normalizedTargetStatesToAbsorbing) { + break; + } + } + if (normalizedTargetStatesToAbsorbing) { + STORM_LOG_INFO("CVaR SSP preprocessing makes target states absorbing to match terminal-goal semantics."); + transitionMatrix.makeRowGroupsAbsorbing(targetStates, true); + } + return normalizedTargetStatesToAbsorbing; +} + +template +void validateRewardAlmostSureReachability(storm::storage::SparseMatrix const& transitionMatrix, + storm::storage::SparseMatrix const& backwardTransitions, + storm::storage::BitVector const& targetStates, storm::storage::BitVector const& reachableStates) { + storm::storage::BitVector prob1AStates = + storm::utility::graph::performProb1A(transitionMatrix, transitionMatrix.getRowGroupIndices(), backwardTransitions, + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), targetStates); + STORM_LOG_THROW(reachableStates.isSubsetOf(prob1AStates), storm::exceptions::InvalidPropertyException, + "CVaR SSP reward preprocessing currently requires all reachable states to reach a goal almost surely under all schedulers."); +} + template SspCvarPreprocessingResult preprocessSspCvar(Environment const& env, SparseMdpModelType const& model, CvarQueryInformation const& queryInformation, @@ -126,28 +165,7 @@ SspCvarPreprocessingResult preprocessSsp } auto transitionMatrix = model.getTransitionMatrix(); - bool normalizedTargetStatesToAbsorbing = false; - for (auto targetState : targetStates) { - for (uint64_t row = transitionMatrix.getRowGroupIndices()[targetState], endRow = transitionMatrix.getRowGroupIndices()[targetState + 1]; row < endRow; - ++row) { - for (auto const& entry : transitionMatrix.getRow(row)) { - if (entry.getColumn() != targetState) { - normalizedTargetStatesToAbsorbing = true; - break; - } - } - if (normalizedTargetStatesToAbsorbing) { - break; - } - } - if (normalizedTargetStatesToAbsorbing) { - break; - } - } - if (normalizedTargetStatesToAbsorbing) { - STORM_LOG_INFO("CVaR SSP preprocessing makes target states absorbing to match terminal-goal semantics."); - transitionMatrix.makeRowGroupsAbsorbing(targetStates, true); - } + bool normalizedTargetStatesToAbsorbing = normalizeTargetStatesToAbsorbing(transitionMatrix, targetStates); auto backwardTransitions = transitionMatrix.transpose(true); auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, model.getInitialStates(), @@ -174,6 +192,57 @@ SspCvarPreprocessingResult preprocessSsp std::move(transitionMatrix)}; } +template +SspCvarPreprocessingResult preprocessSspRewardCvar(Environment const&, SparseMdpModelType const& model, + CvarQueryInformation const& queryInformation, + storm::storage::BitVector const& targetStates) { + using ValueType = typename SparseMdpModelType::ValueType; + + std::string rewardModelName = queryInformation.rewardModelName.value_or(""); + auto const& rewardModel = model.getRewardModel(rewardModelName); + if (rewardModelName.empty()) { + rewardModelName = model.getUniqueRewardModelName(); + } + + STORM_LOG_THROW(queryInformation.optimizationDirection == storm::solver::OptimizationDirection::Maximize, storm::exceptions::InvalidPropertyException, + "CVaR SSP reward preprocessing currently only supports maximizing total rewards."); + STORM_LOG_THROW(queryInformation.interpretation == CvarInterpretation::Reward, storm::exceptions::InvalidPropertyException, + "CVaR SSP reward preprocessing requires the reward interpretation."); + + STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, + "CVaR SSP preprocessing does not support transition rewards."); + + bool liftedStateRewardsToChoiceCosts = rewardModel.hasStateRewards() && !rewardModel.hasStateActionRewards(); + if (liftedStateRewardsToChoiceCosts) { + STORM_LOG_INFO("CVaR SSP preprocessing lifts state rewards to equivalent per-choice rewards."); + } + + auto transitionMatrix = model.getTransitionMatrix(); + bool normalizedTargetStatesToAbsorbing = normalizeTargetStatesToAbsorbing(transitionMatrix, targetStates); + auto backwardTransitions = transitionMatrix.transpose(true); + auto reachableStates = storm::utility::graph::getReachableStates(transitionMatrix, model.getInitialStates(), + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), + storm::storage::BitVector(transitionMatrix.getRowGroupCount(), false)); + + uint64_t const initialState = *model.getInitialStates().begin(); + validateRewardAlmostSureReachability(transitionMatrix, backwardTransitions, targetStates, reachableStates); + + auto choiceRewards = extractChoiceCostsForSsp(model, rewardModel, targetStates); + validatePositiveChoiceCostsOutsideGoals(transitionMatrix, targetStates, choiceRewards, "choice rewards"); + uint64_t maximalChoiceReward = validateAndComputeMaximalChoiceCostOutsideGoals(transitionMatrix, targetStates, choiceRewards, "choice rewards"); + + return {rewardModelName, + initialState, + targetStates, + std::move(reachableStates), + liftedStateRewardsToChoiceCosts, + normalizedTargetStatesToAbsorbing, + maximalChoiceReward, + std::move(choiceRewards), + std::vector(), + std::move(transitionMatrix)}; +} + } // namespace preprocessing } // namespace cvar } // namespace modelchecker From cac44df7018fab4afd595a1b68ada405275b8aae Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:36:55 +0200 Subject: [PATCH 61/65] Implement SSP reward lower-tail value iteration --- .../SparseSspRewardCvarParetoViHelper.h | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h diff --git a/src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h new file mode 100644 index 0000000000..a8ac7519c6 --- /dev/null +++ b/src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include + +#include "storm/environment/Environment.h" +#include "storm/exceptions/NotImplementedException.h" +#include "storm/exceptions/UnexpectedException.h" +#include "storm/modelchecker/cvar/CvarComputationResult.h" +#include "storm/modelchecker/cvar/CvarQueryInformation.h" +#include "storm/modelchecker/cvar/helper/SspParetoValueIterationOperator.h" +#include "storm/modelchecker/cvar/preprocessing/SspCvarPreprocessingResult.h" +#include "storm/utility/constants.h" +#include "storm/utility/macros.h" + +namespace storm { +namespace modelchecker { +namespace cvar { + +/*! + * Implements Pareto-front value iteration for lower-tail CVaR on an SSP-style total-reward objective. + * + * The helper assumes that preprocessing has normalized the model to terminal-goal semantics and validated + * strictly positive integer per-choice rewards outside goal states. The computed frontier stores tuples + * (p, e), where p is the probability of accumulating reward at most the current threshold and e is the + * expected shortfall below that threshold. + */ +template +class SparseSspRewardCvarParetoViHelper { + public: + using ParetoFront = SspRewardParetoFront; + using ParetoViOperator = SspParetoValueIterationOperator; + using FrontierLayer = std::vector; + using FrontierWindow = std::vector; + + SparseSspRewardCvarParetoViHelper(CvarQueryInformation const& queryInformation, + preprocessing::SspCvarPreprocessingResult const& preprocessingResult) + : queryInformation(queryInformation), preprocessingResult(preprocessingResult), paretoViOperator(preprocessingResult) { + // Intentionally left empty. + } + + CvarComputationResult computeCvar(Environment const&, bool produceScheduler = false) const { + STORM_LOG_THROW(!produceScheduler, storm::exceptions::NotImplementedException, + "Scheduler extraction for CVaR SSP value iteration is not implemented yet."); + + ValueType const alpha = storm::utility::convertNumber(queryInformation.alpha); + FrontierWindow frontierWindow = initializeFrontierWindow(); + auto bestCandidate = extractBestCvarCandidateFromInitialFrontier(frontierWindow[0][preprocessingResult.initialState], 0, alpha); + if (initialFrontierHasReachedAlpha(frontierWindow[0][preprocessingResult.initialState], alpha)) { + STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, + "CVaR SSP reward value iteration reached the stopping criterion without a candidate."); + return {bestCandidate.value(), nullptr}; + } + + FrontierLayer currentLayer(paretoViOperator.getStateCount()); + for (uint64_t rewardThreshold = 1;; ++rewardThreshold) { + paretoViOperator.apply(rewardThreshold, frontierWindow, currentLayer); + auto currentCandidate = extractBestCvarCandidateFromInitialFrontier(currentLayer[preprocessingResult.initialState], rewardThreshold, alpha); + if (currentCandidate.has_value() && (!bestCandidate.has_value() || bestCandidate.value() < currentCandidate.value())) { + bestCandidate = currentCandidate; + } + + bool const reachedAlpha = initialFrontierHasReachedAlpha(currentLayer[preprocessingResult.initialState], alpha); + swapFrontierLayerIntoWindow(rewardThreshold, currentLayer, frontierWindow); + if (reachedAlpha) { + break; + } + } + + STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, "CVaR SSP reward value iteration did not find a feasible candidate."); + return {bestCandidate.value(), nullptr}; + } + + private: + FrontierLayer createInitialFrontierLayer(int64_t rewardThreshold) const { + FrontierLayer baseLayer(paretoViOperator.getStateCount()); + if (rewardThreshold >= 0) { + ParetoFront const targetFront = + ParetoFront::singleton(storm::utility::one(), storm::utility::convertNumber(rewardThreshold)); + for (auto state : paretoViOperator.getReachableTargetStates()) { + baseLayer[state] = targetFront; + } + } else { + ParetoFront const targetFront = ParetoFront::singleton(storm::utility::zero(), storm::utility::zero()); + for (auto state : paretoViOperator.getReachableTargetStates()) { + baseLayer[state] = targetFront; + } + } + ParetoFront const nonTargetFront = ParetoFront::singleton(storm::utility::zero(), storm::utility::zero()); + for (auto state : paretoViOperator.getReachableNonTargetStates()) { + baseLayer[state] = nonTargetFront; + } + return baseLayer; + } + + FrontierWindow initializeFrontierWindow() const { + STORM_LOG_ASSERT(preprocessingResult.maximalChoiceCost > 0, "Expected a strictly positive maximal choice reward."); + FrontierWindow frontierWindow(preprocessingResult.maximalChoiceCost, FrontierLayer(paretoViOperator.getStateCount())); + for (int64_t rewardThreshold = 1 - static_cast(preprocessingResult.maximalChoiceCost); rewardThreshold <= 0; ++rewardThreshold) { + frontierWindow[ParetoViOperator::getWindowIndex(rewardThreshold, frontierWindow.size())] = createInitialFrontierLayer(rewardThreshold); + } + return frontierWindow; + } + + static void swapFrontierLayerIntoWindow(int64_t rewardThreshold, FrontierLayer& layer, FrontierWindow& frontierWindow) { + std::swap(frontierWindow[ParetoViOperator::getWindowIndex(rewardThreshold, frontierWindow.size())], layer); + } + + static std::optional extractBestCvarCandidateFromInitialFrontier(ParetoFront const& initialFrontier, uint64_t rewardThreshold, + ValueType const& alpha) { + if (initialFrontier.empty()) { + return std::nullopt; + } + ValueType const thresholdValue = storm::utility::convertNumber(rewardThreshold); + std::optional result; + for (auto const& point : initialFrontier) { + ValueType const candidate = thresholdValue - point.expectedCost / alpha; + if (!result.has_value() || result.value() < candidate) { + result = candidate; + } + } + return result; + } + + static bool initialFrontierHasReachedAlpha(ParetoFront const& initialFrontier, ValueType const& alpha) { + if (initialFrontier.empty()) { + return false; + } + storm::utility::ElementLess less; + for (auto const& point : initialFrontier) { + if (less(point.probability, alpha)) { + return false; + } + } + return true; + } + + CvarQueryInformation const& queryInformation; + preprocessing::SspCvarPreprocessingResult const& preprocessingResult; + ParetoViOperator paretoViOperator; +}; + +} // namespace cvar +} // namespace modelchecker +} // namespace storm From 5f6c30d8bf0e3b9ab3bf3e8e516bf9bc5845dea5 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:37:05 +0200 Subject: [PATCH 62/65] Add SSP reward CVaR model tests --- .../cvar_ssp_reward_delayed_challenger_mdp.nm | 20 +++ .../mdp/cvar_ssp_reward_geometric_mdp.nm | 14 +++ ..._ssp_reward_prob1a_choice_violation_mdp.nm | 16 +++ ...ward_prob1a_probabilistic_violation_mdp.nm | 16 +++ .../mdp/cvar_ssp_reward_safe_risky_mdp.nm | 20 +++ .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 115 +++++++++++++++++- 6 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 resources/examples/testfiles/mdp/cvar_ssp_reward_delayed_challenger_mdp.nm create mode 100644 resources/examples/testfiles/mdp/cvar_ssp_reward_geometric_mdp.nm create mode 100644 resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_choice_violation_mdp.nm create mode 100644 resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_probabilistic_violation_mdp.nm create mode 100644 resources/examples/testfiles/mdp/cvar_ssp_reward_safe_risky_mdp.nm diff --git a/resources/examples/testfiles/mdp/cvar_ssp_reward_delayed_challenger_mdp.nm b/resources/examples/testfiles/mdp/cvar_ssp_reward_delayed_challenger_mdp.nm new file mode 100644 index 0000000000..9eaec9708d --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_ssp_reward_delayed_challenger_mdp.nm @@ -0,0 +1,20 @@ +mdp + +module main + s : [0..3] init 0; + + [det] s=0 -> 1 : (s'=3); + [lottery] s=0 -> 1/2 : (s'=1) + 1/2 : (s'=2); + [low] s=1 -> 1 : (s'=3); + [high] s=2 -> 1 : (s'=3); + [] s=3 -> 1 : (s'=3); +endmodule + +label "goal" = s=3; + +rewards "reward" + [det] true : 6; + [lottery] true : 1; + [low] true : 7; + [high] true : 99; +endrewards diff --git a/resources/examples/testfiles/mdp/cvar_ssp_reward_geometric_mdp.nm b/resources/examples/testfiles/mdp/cvar_ssp_reward_geometric_mdp.nm new file mode 100644 index 0000000000..fccc3fde34 --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_ssp_reward_geometric_mdp.nm @@ -0,0 +1,14 @@ +mdp + +module main + s : [0..1] init 0; + + [step] s=0 -> 1/2 : (s'=0) + 1/2 : (s'=1); + [] s=1 -> 1 : (s'=1); +endmodule + +label "goal" = s=1; + +rewards "reward" + [step] true : 1; +endrewards diff --git a/resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_choice_violation_mdp.nm b/resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_choice_violation_mdp.nm new file mode 100644 index 0000000000..dafa87e386 --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_choice_violation_mdp.nm @@ -0,0 +1,16 @@ +mdp + +module main + s : [0..1] init 0; + + [loop] s=0 -> 1 : (s'=0); + [exit] s=0 -> 1 : (s'=1); + [] s=1 -> 1 : (s'=1); +endmodule + +label "goal" = s=1; + +rewards "reward" + [loop] true : 1; + [exit] true : 1; +endrewards diff --git a/resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_probabilistic_violation_mdp.nm b/resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_probabilistic_violation_mdp.nm new file mode 100644 index 0000000000..293c61f115 --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_ssp_reward_prob1a_probabilistic_violation_mdp.nm @@ -0,0 +1,16 @@ +mdp + +module main + s : [0..2] init 0; + + [split] s=0 -> 1/2 : (s'=1) + 1/2 : (s'=2); + [loop] s=1 -> 1 : (s'=1); + [] s=2 -> 1 : (s'=2); +endmodule + +label "goal" = s=2; + +rewards "reward" + [split] true : 1; + [loop] true : 1; +endrewards diff --git a/resources/examples/testfiles/mdp/cvar_ssp_reward_safe_risky_mdp.nm b/resources/examples/testfiles/mdp/cvar_ssp_reward_safe_risky_mdp.nm new file mode 100644 index 0000000000..a4c957f93d --- /dev/null +++ b/resources/examples/testfiles/mdp/cvar_ssp_reward_safe_risky_mdp.nm @@ -0,0 +1,20 @@ +mdp + +module main + s : [0..3] init 0; + + [safe] s=0 -> 1 : (s'=3); + [risky] s=0 -> 1/5 : (s'=1) + 4/5 : (s'=2); + [low] s=1 -> 1 : (s'=3); + [high] s=2 -> 1 : (s'=3); + [] s=3 -> 1 : (s'=3); +endmodule + +label "goal" = s=3; + +rewards "reward" + [safe] true : 6; + [risky] true : 1; + [low] true : 1; + [high] true : 9; +endrewards diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 5309828d79..a0df471677 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -1,6 +1,8 @@ #include "storm-config.h" #include "test/storm_gtest.h" +#include + #include "storm-parsers/api/model_descriptions.h" #include "storm-parsers/api/properties.h" #include "storm/adapters/RationalNumberAdapter.h" @@ -10,6 +12,7 @@ #include "storm/environment/modelchecker/ModelCheckerEnvironment.h" #include "storm/exceptions/InvalidArgumentException.h" #include "storm/exceptions/InvalidPropertyException.h" +#include "storm/exceptions/NotImplementedException.h" #include "storm/logic/CvarFormula.h" #include "storm/modelchecker/CheckTask.h" #include "storm/modelchecker/cvar/CvarInterpretation.h" @@ -20,6 +23,7 @@ #include "storm/modelchecker/prctl/SparseMdpPrctlModelChecker.h" #include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h" #include "storm/models/sparse/Mdp.h" +#include "storm/models/sparse/StandardRewardModel.h" #include "storm/utility/constants.h" namespace { @@ -92,7 +96,8 @@ ValueType checkInitialStateValueWithMethodAndInterpretationSelection(CvarTestInp return result->template asExplicitQuantitativeCheckResult().getMax(); } -void expectParetoFrontPoints(storm::modelchecker::cvar::SspParetoFront const& front, std::vector> const& expectedPoints) { +template +void expectParetoFrontPoints(ParetoFront const& front, std::vector> const& expectedPoints) { auto const& actualPoints = front.getPoints(); ASSERT_EQ(expectedPoints.size(), actualPoints.size()); for (std::size_t index = 0; index < expectedPoints.size(); ++index) { @@ -168,6 +173,38 @@ TEST(CvarSspParetoValueIterationOperatorTest, AppliesActionCostsAndUnionsActionF expectParetoFrontPoints(outputLayer[2], {{1.0, 0.0}}); } +TEST(CvarSspRewardParetoFrontTest, KeepsLowerLeftIncomparablePoints) { + using ParetoFront = storm::modelchecker::cvar::SspRewardParetoFront; + + ParetoFront front({{0.1, 0.9}, {0.5, 0.5}, {0.7, 0.8}, {0.1, 1.1}, {0.9, 0.2}}); + + expectParetoFrontPoints(front, {{0.1, 0.9}, {0.5, 0.5}, {0.9, 0.2}}); +} + +TEST(CvarSspRewardParetoValueIterationOperatorTest, AppliesRewardShiftsAndUnionsActionFronts) { + using ParetoFront = storm::modelchecker::cvar::SspRewardParetoFront; + using ParetoViOperator = + storm::modelchecker::cvar::SspParetoValueIterationOperator; + using FrontierLayer = std::vector; + using FrontierWindow = std::vector; + + auto preprocessingResult = buildTinySspPreprocessingResult(); + ParetoViOperator paretoViOperator(preprocessingResult); + FrontierWindow frontierWindow(3, FrontierLayer(3)); + for (uint64_t threshold = 0; threshold < frontierWindow.size(); ++threshold) { + frontierWindow[threshold][2] = ParetoViOperator::createTargetFrontier(threshold); + } + frontierWindow[2][1] = ParetoFront::singleton(0.5, 0.1); + frontierWindow[1][1] = ParetoFront::singleton(0.2, 0.5); + + FrontierLayer outputLayer; + paretoViOperator.apply(3, frontierWindow, outputLayer); + + expectParetoFrontPoints(outputLayer[0], {{0.2, 0.5}, {0.5, 0.1}}); + expectParetoFrontPoints(outputLayer[1], {{1.0, 2.0}}); + expectParetoFrontPoints(outputLayer[2], {{1.0, 3.0}}); +} + TEST(CvarQueryTest, SimpleMdp) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; @@ -370,6 +407,16 @@ TEST(CvarQueryTest, DeterministicSspPathMdp) { EXPECT_NEAR(value, 5.0, 1e-10); } +TEST(CvarQueryTest, DeterministicRewardSspPathMdp) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"cost\"}max=? [ F \"goal\" ];", "0.5"); + + EXPECT_NEAR(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 5.0, 1e-10); + EXPECT_NEAR(checkInitialStateValueWithMethodAndInterpretationSelection(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi, + storm::modelchecker::cvar::CvarInterpretationSelection::Reward), + 5.0, 1e-10); +} + TEST(CvarQueryTest, BranchingSspTradeoffMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_branching_tradeoff_mdp.nm"; @@ -379,4 +426,70 @@ TEST(CvarQueryTest, BranchingSspTradeoffMdp) { auto nineTenthsInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.9"); EXPECT_NEAR(checkInitialStateValueWithMethod(nineTenthsInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 16.0 / 3.0, 1e-10); } + +TEST(CvarQueryTest, SafeRiskyRewardSspMdp) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_safe_risky_mdp.nm"; + + auto quarterInput = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.25"); + EXPECT_NEAR(checkInitialStateValueWithMethod(quarterInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 6.0, 1e-10); + + auto fourFifthsInput = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.8"); + EXPECT_NEAR(checkInitialStateValueWithMethod(fourFifthsInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 8.0, 1e-10); +} + +TEST(CvarQueryTest, DelayedChallengerRewardSspMdp) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_delayed_challenger_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.5"); + + EXPECT_NEAR(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 8.0, 1e-10); +} + +TEST(CvarQueryTest, GeometricRewardSspMdp) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_geometric_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.8"); + + EXPECT_NEAR(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 1.4375, 1e-10); +} + +TEST(CvarQueryTest, RejectsRewardSspProb1AChoiceViolation) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_prob1a_choice_violation_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.5"); + + STORM_SILENT_EXPECT_THROW(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), + storm::exceptions::InvalidPropertyException); +} + +TEST(CvarQueryTest, RejectsRewardSspProb1AProbabilisticViolation) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_prob1a_probabilistic_violation_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.5"); + + STORM_SILENT_EXPECT_THROW(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), + storm::exceptions::InvalidPropertyException); +} + +TEST(CvarQueryTest, RejectsUnsupportedSspInterpretationCombinations) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; + + auto minRewardInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.5"); + STORM_SILENT_EXPECT_THROW(checkInitialStateValueWithMethodAndInterpretationSelection(minRewardInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi, + storm::modelchecker::cvar::CvarInterpretationSelection::Reward), + storm::exceptions::InvalidPropertyException); + + auto maxCostInput = buildCvarInput(modelPath, "R{\"cost\"}max=? [ F \"goal\" ];", "0.5"); + STORM_SILENT_EXPECT_THROW(checkInitialStateValueWithMethodAndInterpretationSelection(maxCostInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi, + storm::modelchecker::cvar::CvarInterpretationSelection::Cost), + storm::exceptions::InvalidPropertyException); +} + +TEST(CvarQueryTest, RejectsSspTransitionRewards) { + std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; + auto input = buildCvarInput(modelPath, "R{\"cost\"}max=? [ F \"goal\" ];", "0.5"); + + storm::models::sparse::StandardRewardModel transitionRewardModel( + std::nullopt, std::make_optional(std::vector(input.mdp->getNumberOfChoices(), 1.0)), std::make_optional(input.mdp->getTransitionMatrix())); + input.mdp->getRewardModels()["cost"] = std::move(transitionRewardModel); + + STORM_SILENT_EXPECT_THROW(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), + storm::exceptions::NotImplementedException); +} } // namespace From e41587a3643ccd350d2349b4d8da4a307b788d47 Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:05:08 +0200 Subject: [PATCH 63/65] Format --- .../cvar/helper/SparseSspRewardCvarParetoViHelper.h | 6 +++--- .../modelchecker/cvar/preprocessing/SspCvarPreprocessor.h | 4 ++-- src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h b/src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h index a8ac7519c6..358d2da58c 100644 --- a/src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h +++ b/src/storm/modelchecker/cvar/helper/SparseSspRewardCvarParetoViHelper.h @@ -69,7 +69,8 @@ class SparseSspRewardCvarParetoViHelper { } } - STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, "CVaR SSP reward value iteration did not find a feasible candidate."); + STORM_LOG_THROW(bestCandidate.has_value(), storm::exceptions::UnexpectedException, + "CVaR SSP reward value iteration did not find a feasible candidate."); return {bestCandidate.value(), nullptr}; } @@ -77,8 +78,7 @@ class SparseSspRewardCvarParetoViHelper { FrontierLayer createInitialFrontierLayer(int64_t rewardThreshold) const { FrontierLayer baseLayer(paretoViOperator.getStateCount()); if (rewardThreshold >= 0) { - ParetoFront const targetFront = - ParetoFront::singleton(storm::utility::one(), storm::utility::convertNumber(rewardThreshold)); + ParetoFront const targetFront = ParetoFront::singleton(storm::utility::one(), storm::utility::convertNumber(rewardThreshold)); for (auto state : paretoViOperator.getReachableTargetStates()) { baseLayer[state] = targetFront; } diff --git a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h index 940a8b66f2..75fc6f5d56 100644 --- a/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h +++ b/src/storm/modelchecker/cvar/preprocessing/SspCvarPreprocessor.h @@ -130,8 +130,8 @@ bool normalizeTargetStatesToAbsorbing(storm::storage::SparseMatrix& t template void validateRewardAlmostSureReachability(storm::storage::SparseMatrix const& transitionMatrix, - storm::storage::SparseMatrix const& backwardTransitions, - storm::storage::BitVector const& targetStates, storm::storage::BitVector const& reachableStates) { + storm::storage::SparseMatrix const& backwardTransitions, storm::storage::BitVector const& targetStates, + storm::storage::BitVector const& reachableStates) { storm::storage::BitVector prob1AStates = storm::utility::graph::performProb1A(transitionMatrix, transitionMatrix.getRowGroupIndices(), backwardTransitions, storm::storage::BitVector(transitionMatrix.getRowGroupCount(), true), targetStates); diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index a0df471677..ef1f9bf0f2 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -183,8 +183,7 @@ TEST(CvarSspRewardParetoFrontTest, KeepsLowerLeftIncomparablePoints) { TEST(CvarSspRewardParetoValueIterationOperatorTest, AppliesRewardShiftsAndUnionsActionFronts) { using ParetoFront = storm::modelchecker::cvar::SspRewardParetoFront; - using ParetoViOperator = - storm::modelchecker::cvar::SspParetoValueIterationOperator; + using ParetoViOperator = storm::modelchecker::cvar::SspParetoValueIterationOperator; using FrontierLayer = std::vector; using FrontierWindow = std::vector; From 32ce646c5212d67b62ecefe74f926215c1e16eca Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:41:08 +0200 Subject: [PATCH 64/65] Canonicalize SSP Pareto fronts after affine transforms --- src/storm/modelchecker/cvar/helper/SspParetoFront.h | 6 +++--- .../storm/modelchecker/prctl/mdp/CvarQueryTest.cpp | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/storm/modelchecker/cvar/helper/SspParetoFront.h b/src/storm/modelchecker/cvar/helper/SspParetoFront.h index 3868ae2807..2ec90c04e8 100644 --- a/src/storm/modelchecker/cvar/helper/SspParetoFront.h +++ b/src/storm/modelchecker/cvar/helper/SspParetoFront.h @@ -168,7 +168,7 @@ class SspParetoFront { scaledPoints.push_back(Point{factor * point.probability, factor * point.expectedCost}); } if (storm::utility::isPositive(factor)) { - return SspParetoFront(std::move(scaledPoints), AlreadyCanonicalTag{}); + return SspParetoFront(std::move(scaledPoints), AlreadySortedTag{}); } return SspParetoFront(std::move(scaledPoints)); } @@ -321,7 +321,7 @@ class SspParetoFront { for (auto const& point : points) { translatedPoints.push_back(Point{point.probability + offset.probability, point.expectedCost + offset.expectedCost}); } - return SspParetoFront(std::move(translatedPoints), AlreadyCanonicalTag{}); + return SspParetoFront(std::move(translatedPoints), AlreadySortedTag{}); } SspParetoFront scaledTranslated(ValueType const& factor, Point const& offset) const { @@ -342,7 +342,7 @@ class SspParetoFront { scaledTranslatedPoints.push_back(Point{offset.probability + factor * point.probability, offset.expectedCost + factor * point.expectedCost}); } if (storm::utility::isPositive(factor)) { - return SspParetoFront(std::move(scaledTranslatedPoints), AlreadyCanonicalTag{}); + return SspParetoFront(std::move(scaledTranslatedPoints), AlreadySortedTag{}); } return SspParetoFront(std::move(scaledTranslatedPoints)); } diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index ef1f9bf0f2..115060fcf5 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -150,6 +150,16 @@ TEST(CvarSspParetoFrontTest, ScaledMinkowskiSumMergesConvexChains) { expectParetoFrontPoints(result, {{0.0, 0.0}, {0.125, 0.25}, {0.375, 1.25}, {0.5, 2.0}, {0.75, 4.0}}); } +TEST(CvarSspParetoFrontTest, CanonicalizesAfterFloatingPointTranslationCollapse) { + using ParetoFront = storm::modelchecker::cvar::SspParetoFront; + + ParetoFront front({{0.0, 0.0}, {1e-17, 1.0}}); + + auto result = front.minkowskiSum(ParetoFront::singleton(1.0, 0.0)); + + expectParetoFrontPoints(result, {{1.0, 0.0}}); +} + TEST(CvarSspParetoValueIterationOperatorTest, AppliesActionCostsAndUnionsActionFronts) { using ParetoFront = storm::modelchecker::cvar::SspParetoFront; using ParetoViOperator = storm::modelchecker::cvar::SspParetoValueIterationOperator; From 42aaf884b58724b47ad0149a5127d6766de7429a Mon Sep 17 00:00:00 2001 From: Patric <129106694+pjtimm@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:54:13 +0200 Subject: [PATCH 65/65] Skip CVaR model tests without Z3 --- .../modelchecker/prctl/mdp/CvarQueryTest.cpp | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp index 115060fcf5..11f1cafa57 100644 --- a/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp +++ b/src/test/storm/modelchecker/prctl/mdp/CvarQueryTest.cpp @@ -43,6 +43,15 @@ bool hasExactLpSolver() { #endif } +class CvarQueryTest : public ::testing::Test { + public: + void SetUp() override { +#ifndef STORM_HAVE_Z3 + GTEST_SKIP() << "Z3 not available."; +#endif + } +}; + template struct CvarTestInput { std::shared_ptr> mdp; @@ -214,7 +223,7 @@ TEST(CvarSspRewardParetoValueIterationOperatorTest, AppliesRewardShiftsAndUnions expectParetoFrontPoints(outputLayer[2], {{1.0, 3.0}}); } -TEST(CvarQueryTest, SimpleMdp) { +TEST_F(CvarQueryTest, SimpleMdp) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; } @@ -231,7 +240,7 @@ TEST(CvarQueryTest, SimpleMdp) { EXPECT_NEAR(minValue, 2.0, 1e-10); } -TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { +TEST_F(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; } @@ -248,7 +257,7 @@ TEST(CvarQueryTest, ReachableBadMecIsPreprocessedToZeroTerminalReward) { EXPECT_NEAR(minValue, 4.0, 1e-10); } -TEST(CvarQueryTest, TargetReachingMecIsCollapsed) { +TEST_F(CvarQueryTest, TargetReachingMecIsCollapsed) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; } @@ -262,7 +271,7 @@ TEST(CvarQueryTest, TargetReachingMecIsCollapsed) { EXPECT_NEAR(checkInitialStateValue(minInput), 4.0, 1e-10); } -TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { +TEST_F(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { std::string alpha = "0.5"; std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_nonabsorbing_target_mdp.nm"; @@ -274,7 +283,7 @@ TEST(CvarQueryTest, RejectsNonAbsorbingOriginalTargetStates) { STORM_SILENT_EXPECT_THROW(checker.check(env, task), storm::exceptions::InvalidPropertyException); } -TEST(CvarQueryTest, BranchingTradeoffMdp) { +TEST_F(CvarQueryTest, BranchingTradeoffMdp) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; } @@ -294,7 +303,7 @@ TEST(CvarQueryTest, BranchingTradeoffMdp) { EXPECT_NEAR(checkInitialStateValue(minThreeQuarterInput), 7.0, 1e-10); } -TEST(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { +TEST_F(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { if (!hasExactLpSolver()) { GTEST_SKIP() << "No exact LP solver available."; } @@ -308,7 +317,7 @@ TEST(CvarQueryTest, BranchingTradeoffMdpRationalNumbers) { EXPECT_EQ(storm::RationalNumber(7), checkInitialStateValue(minInput)); } -TEST(CvarQueryTest, InterpretationOverridesOnSimpleMdp) { +TEST_F(CvarQueryTest, InterpretationOverridesOnSimpleMdp) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; } @@ -332,7 +341,7 @@ TEST(CvarQueryTest, InterpretationOverridesOnSimpleMdp) { 5.0 / 3.0, 1e-10); } -TEST(CvarQueryTest, InterpretationOverridesOnSimpleMdpRationalNumbers) { +TEST_F(CvarQueryTest, InterpretationOverridesOnSimpleMdpRationalNumbers) { if (!hasExactLpSolver()) { GTEST_SKIP() << "No exact LP solver available."; } @@ -356,7 +365,7 @@ TEST(CvarQueryTest, InterpretationOverridesOnSimpleMdpRationalNumbers) { storm::modelchecker::cvar::CvarInterpretationSelection::Reward)); } -TEST(CvarQueryTest, EquivalentExactAlphaSyntaxes) { +TEST_F(CvarQueryTest, EquivalentExactAlphaSyntaxes) { if (!hasLpSolver()) { GTEST_SKIP() << "No LP solver available."; } @@ -372,7 +381,7 @@ TEST(CvarQueryTest, EquivalentExactAlphaSyntaxes) { EXPECT_NEAR(checkInitialStateValue(scientificInput), 2.0, 1e-10); } -TEST(CvarQueryTest, RejectsInvalidAlphaSyntaxes) { +TEST(CvarFormulaTest, RejectsInvalidAlphaSyntaxes) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}max=? [ F \"target\" ];", program); @@ -382,7 +391,7 @@ TEST(CvarQueryTest, RejectsInvalidAlphaSyntaxes) { } } -TEST(CvarQueryTest, CvarFormulaValidatesAlphaAndSubformula) { +TEST(CvarFormulaTest, CvarFormulaValidatesAlphaAndSubformula) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; storm::prism::Program program = storm::api::parseProgram(modelPath); auto properties = storm::api::parsePropertiesForPrismProgram("R{\"term\"}max=? [ F \"target\" ];", program); @@ -394,7 +403,7 @@ TEST(CvarQueryTest, CvarFormulaValidatesAlphaAndSubformula) { STORM_SILENT_EXPECT_THROW(storm::logic::CvarFormula(storm::RationalNumber("1/2"), nullptr), storm::exceptions::InvalidArgumentException); } -TEST(CvarQueryTest, CheckTaskExtractsWrappedRewardMetadata) { +TEST_F(CvarQueryTest, CheckTaskExtractsWrappedRewardMetadata) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_simple_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"term\"}max=? [ F \"target\" ];", "3/4"); @@ -408,7 +417,7 @@ TEST(CvarQueryTest, CheckTaskExtractsWrappedRewardMetadata) { EXPECT_FALSE(task.isBoundSet()); } -TEST(CvarQueryTest, DeterministicSspPathMdp) { +TEST_F(CvarQueryTest, DeterministicSspPathMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.5"); @@ -416,7 +425,7 @@ TEST(CvarQueryTest, DeterministicSspPathMdp) { EXPECT_NEAR(value, 5.0, 1e-10); } -TEST(CvarQueryTest, DeterministicRewardSspPathMdp) { +TEST_F(CvarQueryTest, DeterministicRewardSspPathMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"cost\"}max=? [ F \"goal\" ];", "0.5"); @@ -426,7 +435,7 @@ TEST(CvarQueryTest, DeterministicRewardSspPathMdp) { 5.0, 1e-10); } -TEST(CvarQueryTest, BranchingSspTradeoffMdp) { +TEST_F(CvarQueryTest, BranchingSspTradeoffMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_branching_tradeoff_mdp.nm"; auto halfInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.5"); @@ -436,7 +445,7 @@ TEST(CvarQueryTest, BranchingSspTradeoffMdp) { EXPECT_NEAR(checkInitialStateValueWithMethod(nineTenthsInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 16.0 / 3.0, 1e-10); } -TEST(CvarQueryTest, SafeRiskyRewardSspMdp) { +TEST_F(CvarQueryTest, SafeRiskyRewardSspMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_safe_risky_mdp.nm"; auto quarterInput = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.25"); @@ -446,21 +455,21 @@ TEST(CvarQueryTest, SafeRiskyRewardSspMdp) { EXPECT_NEAR(checkInitialStateValueWithMethod(fourFifthsInput, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 8.0, 1e-10); } -TEST(CvarQueryTest, DelayedChallengerRewardSspMdp) { +TEST_F(CvarQueryTest, DelayedChallengerRewardSspMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_delayed_challenger_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.5"); EXPECT_NEAR(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 8.0, 1e-10); } -TEST(CvarQueryTest, GeometricRewardSspMdp) { +TEST_F(CvarQueryTest, GeometricRewardSspMdp) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_geometric_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.8"); EXPECT_NEAR(checkInitialStateValueWithMethod(input, storm::modelchecker::cvar::CvarMethod::SspParetoVi), 1.4375, 1e-10); } -TEST(CvarQueryTest, RejectsRewardSspProb1AChoiceViolation) { +TEST_F(CvarQueryTest, RejectsRewardSspProb1AChoiceViolation) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_prob1a_choice_violation_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.5"); @@ -468,7 +477,7 @@ TEST(CvarQueryTest, RejectsRewardSspProb1AChoiceViolation) { storm::exceptions::InvalidPropertyException); } -TEST(CvarQueryTest, RejectsRewardSspProb1AProbabilisticViolation) { +TEST_F(CvarQueryTest, RejectsRewardSspProb1AProbabilisticViolation) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_reward_prob1a_probabilistic_violation_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"reward\"}max=? [ F \"goal\" ];", "0.5"); @@ -476,7 +485,7 @@ TEST(CvarQueryTest, RejectsRewardSspProb1AProbabilisticViolation) { storm::exceptions::InvalidPropertyException); } -TEST(CvarQueryTest, RejectsUnsupportedSspInterpretationCombinations) { +TEST_F(CvarQueryTest, RejectsUnsupportedSspInterpretationCombinations) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; auto minRewardInput = buildCvarInput(modelPath, "R{\"cost\"}min=? [ F \"goal\" ];", "0.5"); @@ -490,7 +499,7 @@ TEST(CvarQueryTest, RejectsUnsupportedSspInterpretationCombinations) { storm::exceptions::InvalidPropertyException); } -TEST(CvarQueryTest, RejectsSspTransitionRewards) { +TEST_F(CvarQueryTest, RejectsSspTransitionRewards) { std::string modelPath = STORM_TEST_RESOURCES_DIR "/mdp/cvar_ssp_deterministic_mdp.nm"; auto input = buildCvarInput(modelPath, "R{\"cost\"}max=? [ F \"goal\" ];", "0.5");