Storm 1.14.0.1
A Modern Probabilistic Model Checker
Loading...
Searching...
No Matches
BisimulationDecomposition.cpp
Go to the documentation of this file.
2
3#include <chrono>
4
19
20namespace storm {
21namespace storage {
22
23using namespace bisimulation;
24
25template<typename ModelType, typename BlockDataType>
27 : Options(tolerance) {
28 this->preserveSingleFormula(model, formula);
29}
30
31template<typename ModelType, typename BlockDataType>
33 std::vector<std::shared_ptr<storm::logic::Formula const>> const& formulas,
34 ValueType const& tolerance)
35 : Options(tolerance) {
36 if (formulas.empty()) {
37 this->respectedAtomicPropositions = model.getStateLabeling().getLabels();
38 this->keepRewards = true;
39 }
40 if (formulas.size() == 1) {
41 this->preserveSingleFormula(model, *formulas.front());
42 } else {
43 for (auto const& formula : formulas) {
44 preserveFormula(*formula);
45 }
46 }
47}
48
49template<typename ModelType, typename BlockDataType>
54
55template<typename ModelType, typename BlockDataType>
57 // Intentionally left empty.
58}
59
60template<typename ModelType, typename BlockDataType>
62 // Disable the measure driven initial partition.
64 phiStates.reset();
65 psiStates.reset();
66
67 // Retrieve information about formula.
69
70 // Preserve rewards if necessary.
71 keepRewards = keepRewards || info.containsRewardOperator() || info.containsRewardBoundedFormula();
72
73 // Preserve bounded properties if necessary.
74 bounded = bounded || (info.containsBoundedUntilFormula() || info.containsNextFormula() || info.containsCumulativeRewardFormula());
75
76 // Preserve discounted properties if necessary.
77 discounted = discounted || info.containsDiscountFormula();
78
79 // Compute the relevant labels and expressions.
80 this->addToRespectedAtomicPropositions(formula.getAtomicExpressionFormulas(), formula.getAtomicLabelFormulas());
81}
82
83template<typename ModelType, typename BlockDataType>
84void BisimulationDecomposition<ModelType, BlockDataType>::Options::preserveSingleFormula(ModelType const& model, storm::logic::Formula const& formula) {
85 // Retrieve information about formula.
87
88 keepRewards = info.containsRewardOperator() || info.containsRewardBoundedFormula();
89
90 // We need to preserve bounded properties iff the formula contains a bounded until or a next subformula.
92
93 // We need to preserve discounting iff the formula contains a discounted subformula
94 discounted = info.containsDiscountFormula();
95
96 // Compute the relevant labels and expressions.
97 this->addToRespectedAtomicPropositions(formula.getAtomicExpressionFormulas(), formula.getAtomicLabelFormulas());
98
99 // Check whether measure driven initial partition is possible and, if so, set it.
100 this->checkAndSetMeasureDrivenInitialPartition(model, formula);
101}
102
103template<typename ModelType, typename BlockDataType>
104void BisimulationDecomposition<ModelType, BlockDataType>::Options::checkAndSetMeasureDrivenInitialPartition(ModelType const& model,
105 storm::logic::Formula const& formula) {
106 std::shared_ptr<storm::logic::Formula const> newFormula = formula.asSharedPointer();
107
108 if (formula.isProbabilityOperatorFormula()) {
110 optimalityType = formula.asProbabilityOperatorFormula().getOptimalityType();
111 } else if (formula.asProbabilityOperatorFormula().hasBound()) {
113 if (comparisonType == storm::logic::ComparisonType::Less || comparisonType == storm::logic::ComparisonType::LessEqual) {
114 optimalityType = OptimizationDirection::Maximize;
115 } else {
116 optimalityType = OptimizationDirection::Minimize;
117 }
118 }
120 } else if (formula.isRewardOperatorFormula()) {
122 optimalityType = formula.asRewardOperatorFormula().getOptimalityType();
123 } else if (formula.asRewardOperatorFormula().hasBound()) {
125 if (comparisonType == storm::logic::ComparisonType::Less || comparisonType == storm::logic::ComparisonType::LessEqual) {
126 optimalityType = OptimizationDirection::Maximize;
127 } else {
128 optimalityType = OptimizationDirection::Minimize;
129 }
130 }
131 newFormula = formula.asRewardOperatorFormula().getSubformula().asSharedPointer();
132 }
133
134 std::shared_ptr<storm::logic::Formula const> leftSubformula = std::make_shared<storm::logic::BooleanLiteralFormula>(true);
135 std::shared_ptr<storm::logic::Formula const> rightSubformula;
136 if (newFormula->isUntilFormula()) {
137 leftSubformula = newFormula->asUntilFormula().getLeftSubformula().asSharedPointer();
138 rightSubformula = newFormula->asUntilFormula().getRightSubformula().asSharedPointer();
139 if (leftSubformula->isInFragment(storm::logic::propositional()) && rightSubformula->isInFragment(storm::logic::propositional())) {
140 measureDrivenInitialPartition = true;
141 }
142 } else if (newFormula->isEventuallyFormula()) {
143 rightSubformula = newFormula->asEventuallyFormula().getSubformula().asSharedPointer();
144 if (rightSubformula->isInFragment(storm::logic::propositional())) {
145 measureDrivenInitialPartition = true;
146 }
147 }
148
149 if (measureDrivenInitialPartition) {
150 storm::modelchecker::SparsePropositionalModelChecker<ModelType> checker(model);
151 std::unique_ptr<storm::modelchecker::CheckResult> phiStatesCheckResult = checker.check(*leftSubformula);
152 std::unique_ptr<storm::modelchecker::CheckResult> psiStatesCheckResult = checker.check(*rightSubformula);
153
154 using SolutionType = storm::IntervalBaseType<ValueType>;
155 phiStates = phiStatesCheckResult->template asExplicitQualitativeCheckResult<SolutionType>().getTruthValuesVector();
156 psiStates = psiStatesCheckResult->template asExplicitQualitativeCheckResult<SolutionType>().getTruthValuesVector();
157 } else {
158 optimalityType.reset();
159 }
160}
161
162template<typename ModelType, typename BlockDataType>
164 std::vector<std::shared_ptr<storm::logic::AtomicExpressionFormula const>> const& expressions,
165 std::vector<std::shared_ptr<storm::logic::AtomicLabelFormula const>> const& labels) {
166 std::set<std::string> labelsToRespect;
167 for (auto const& labelFormula : labels) {
168 labelsToRespect.insert(labelFormula->getLabel());
169 }
170 for (auto const& expressionFormula : expressions) {
171 labelsToRespect.insert(expressionFormula->toString());
172 }
173 if (!respectedAtomicPropositions) {
174 respectedAtomicPropositions = labelsToRespect;
175 } else {
176 respectedAtomicPropositions.value().insert(labelsToRespect.begin(), labelsToRespect.end());
177 }
178}
179
180template<typename ModelType, typename BlockDataType>
182 : BisimulationDecomposition(model, model.getBackwardTransitions(), options) {
183 // Intentionally left empty.
184}
185
186template<typename ModelType, typename BlockDataType>
189 Options const& options)
191 STORM_LOG_THROW(!options.getKeepRewards() || !model.hasRewardModel() || model.hasUniqueRewardModel(), storm::exceptions::IllegalFunctionCallException,
192 "Bisimulation currently only supports models with at most one reward model.");
193 STORM_LOG_THROW(!options.getKeepRewards() || !model.hasRewardModel() || !model.getUniqueRewardModel().hasTransitionRewards(),
194 storm::exceptions::IllegalFunctionCallException,
195 "Bisimulation is currently supported for models with state or action rewards only. Consider converting the transition rewards to state "
196 "rewards (via suitable function calls).");
197 STORM_LOG_THROW(options.getType() != BisimulationType::Weak || !options.getBounded(), storm::exceptions::IllegalFunctionCallException,
198 "Weak bisimulation cannot preserve bounded properties.");
199 STORM_LOG_THROW(options.getType() != BisimulationType::Weak || !options.getDiscounted(), storm::exceptions::IllegalFunctionCallException,
200 "Weak bisimulation cannot preserve discounted properties.");
201
202 // Fix the respected atomic propositions if they were not explicitly given.
203 if (!this->options.respectedAtomicPropositions) {
204 this->options.respectedAtomicPropositions = model.getStateLabeling().getLabels();
205 }
206}
207
208template<typename ModelType, typename BlockDataType>
210 std::chrono::high_resolution_clock::time_point totalStart = std::chrono::high_resolution_clock::now();
211
212 std::chrono::high_resolution_clock::time_point initialPartitionStart = std::chrono::high_resolution_clock::now();
213 // initialize the initial partition.
214 if (options.measureDrivenInitialPartition) {
215 STORM_LOG_THROW(options.phiStates, storm::exceptions::InvalidOptionException, "Unable to compute measure-driven initial partition without phi states.");
216 STORM_LOG_THROW(options.psiStates, storm::exceptions::InvalidOptionException, "Unable to compute measure-driven initial partition without psi states.");
218 } else {
220 }
221 STORM_LOG_WARN_COND(partition.size() > 1, "Initial partition consists only of a single block.");
222 std::chrono::high_resolution_clock::duration initialPartitionTime = std::chrono::high_resolution_clock::now() - initialPartitionStart;
223
224 this->initialize();
226 std::chrono::high_resolution_clock::time_point refinementStart = std::chrono::high_resolution_clock::now();
228 std::chrono::high_resolution_clock::duration refinementTime = std::chrono::high_resolution_clock::now() - refinementStart;
229
230 std::chrono::high_resolution_clock::time_point extractionStart = std::chrono::high_resolution_clock::now();
232 std::chrono::high_resolution_clock::duration extractionTime = std::chrono::high_resolution_clock::now() - extractionStart;
233
234 std::chrono::high_resolution_clock::time_point quotientBuildStart = std::chrono::high_resolution_clock::now();
235 if (options.buildQuotient) {
236 this->buildQuotient();
237 }
238 std::chrono::high_resolution_clock::duration quotientBuildTime = std::chrono::high_resolution_clock::now() - quotientBuildStart;
239
240 std::chrono::high_resolution_clock::duration totalTime = std::chrono::high_resolution_clock::now() - totalStart;
241
242 {
243 std::chrono::milliseconds initialPartitionTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(initialPartitionTime);
244 std::chrono::milliseconds refinementTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(refinementTime);
245 std::chrono::milliseconds extractionTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(extractionTime);
246 std::chrono::milliseconds quotientBuildTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(quotientBuildTime);
247 std::chrono::milliseconds totalTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(totalTime);
248 STORM_LOG_STATISTICS("\nTime breakdown:\n");
249 STORM_LOG_STATISTICS(" * time for initial partition: " << initialPartitionTimeInMilliseconds.count() << "ms\n");
250 STORM_LOG_STATISTICS(" * time for partitioning: " << refinementTimeInMilliseconds.count() << "ms\n");
251 STORM_LOG_STATISTICS(" * time for extraction: " << extractionTimeInMilliseconds.count() << "ms\n");
252 STORM_LOG_STATISTICS(" * time for building quotient: " << quotientBuildTimeInMilliseconds.count() << "ms\n");
253 STORM_LOG_STATISTICS("------------------------------------------\n");
254 STORM_LOG_STATISTICS(" * total time: " << totalTimeInMilliseconds.count() << "ms\n\n");
256}
257
258template<typename ModelType, typename BlockDataType>
260 // Insert all blocks into the splitter queue as a (potential) splitter.
261 std::vector<Block<BlockDataType>*> splitterQueue;
262 std::for_each(partition.getBlocks().begin(), partition.getBlocks().end(), [&](std::unique_ptr<Block<BlockDataType>> const& block) {
263 block->data().setSplitter();
264 splitterQueue.push_back(block.get());
265 });
266
267 // Then perform the actual splitting until there are no more splitters.
268 uint_fast64_t iterations = 0;
269 while (!splitterQueue.empty()) {
270 ++iterations;
271
272 // Get and prepare the next splitter.
273 // Sort the splitters according to their sizes to prefer small splitters. That is just a heuristic, but
274 // tends to work well.
275 std::sort(splitterQueue.begin(), splitterQueue.end(),
276 [](Block<BlockDataType> const* b1, Block<BlockDataType> const* b2) { return b1->getNumberOfStates() > b2->getNumberOfStates(); });
277 Block<BlockDataType>* splitter = splitterQueue.back();
278 splitterQueue.pop_back();
279 splitter->data().setSplitter(false);
280
281 // Now refine the partition using the current splitter.
282 refinePartitionBasedOnSplitter(*splitter, splitterQueue);
285 STORM_LOG_INFO("Performed " << iterations << " iterations of partition refinement before abort.\n");
286 STORM_LOG_THROW(false, storm::exceptions::AbortException, "Aborted in bisimulation computation.");
287 break;
288 }
289 }
290}
291
292template<typename ModelType, typename BlockDataType>
294 STORM_LOG_THROW(this->quotient != nullptr, storm::exceptions::IllegalFunctionCallException,
295 "Unable to retrieve quotient model from bisimulation decomposition, because it was not built.");
296 return this->quotient;
297}
298
299template<typename ModelType, typename BlockDataType>
301 auto const& rewardModel = model.getUniqueRewardModel();
302 if (rewardModel.hasStateRewards()) {
303 this->splitInitialPartitionBasedOnRewards(rewardModel.getStateRewardVector());
304 }
305 if (rewardModel.hasStateActionRewards()) {
306 if (model.isNondeterministicModel()) {
307 std::vector<std::set<ValueType>> actionRewards;
308 actionRewards.reserve(model.getNumberOfStates());
309 for (storm::storage::sparse::state_type state = 0; state < model.getNumberOfStates(); ++state) {
310 std::set<ValueType> rewardsAtState;
311 for (auto choice = model.getTransitionMatrix().getRowGroupIndices()[state];
312 choice < model.getTransitionMatrix().getRowGroupIndices()[state + 1]; ++choice) {
313 rewardsAtState.insert(rewardModel.getStateActionReward(choice));
314 }
315 actionRewards.push_back(std::move(rewardsAtState));
316 }
318 } else {
319 this->splitInitialPartitionBasedOnRewards(rewardModel.getStateActionRewardVector());
320 }
321 }
322}
323
324template<typename ModelType, typename BlockDataType>
327 return rewardVector[a] < rewardVector[b];
328 });
329}
330
331template<typename ModelType, typename BlockDataType>
333 partition.split([&actionRewards](storm::storage::sparse::state_type const& a, storm::storage::sparse::state_type const& b) {
334 return actionRewards[a] < actionRewards[b];
335 });
336}
337
338template<typename ModelType, typename BlockDataType>
341
342 for (auto const& label : options.respectedAtomicPropositions.value()) {
343 if (label == "init") {
344 continue;
345 }
346 partition.splitStates(model.getStates(label));
347 }
348
349 // If the model has state rewards, we need to consider them, because otherwise reward properties are not
350 // preserved.
351 if (options.getKeepRewards() && model.hasRewardModel()) {
353 }
354}
355
356template<typename ModelType, typename BlockDataType>
358 std::pair<storm::storage::BitVector, storm::storage::BitVector> statesWithProbability01 = this->getStatesWithProbability01();
359
360 std::optional<storm::storage::sparse::state_type> representativePsiState;
361 if (!options.psiStates.value().empty()) {
362 representativePsiState = *options.psiStates.value().begin();
363 }
364
366 model.getNumberOfStates(), statesWithProbability01.first,
367 options.getBounded() || options.getKeepRewards() ? options.psiStates.value() : statesWithProbability01.second, representativePsiState);
368
369 // If the model has state rewards, we need to consider them, because otherwise reward properties are not
370 // preserved.
371 if (options.getKeepRewards() && model.hasRewardModel()) {
373 }
374}
375
376template<typename ModelType, typename BlockDataType>
378 // Intentionally left empty.
379}
380
381template<typename ModelType, typename BlockDataType>
383 // Now move the states from the internal partition into their final place in the decomposition. We do so in
384 // a way that maintains the block IDs as indices.
385 this->blocks.resize(partition.size());
386 for (auto const& blockPtr : partition.getBlocks()) {
387 // We need to sort the states to allow for rapid construction of the blocks.
388 partition.sortBlock(*blockPtr);
389
390 // Convert the state-value-pairs to states only.
391 this->blocks[blockPtr->getId()] = block_type(partition.begin(*blockPtr), partition.end(*blockPtr), true);
392 }
393}
394
398
402
406} // namespace storage
407} // namespace storm
RewardOperatorFormula & asRewardOperatorFormula()
Definition Formula.cpp:484
std::vector< std::shared_ptr< AtomicExpressionFormula const > > getAtomicExpressionFormulas() const
Definition Formula.cpp:500
virtual bool isProbabilityOperatorFormula() const
Definition Formula.cpp:180
ProbabilityOperatorFormula & asProbabilityOperatorFormula()
Definition Formula.cpp:476
std::vector< std::shared_ptr< AtomicLabelFormula const > > getAtomicLabelFormulas() const
Definition Formula.cpp:506
virtual bool isRewardOperatorFormula() const
Definition Formula.cpp:184
std::shared_ptr< Formula const > asSharedPointer()
Definition Formula.cpp:571
FormulaInformation info(bool recurseIntoOperators=true) const
Definition Formula.cpp:209
ComparisonType getComparisonType() const
storm::solver::OptimizationDirection const & getOptimalityType() const
Formula const & getSubformula() const
This class is the superclass of all decompositions of a sparse model into its bisimulation quotient.
storm::storage::SparseMatrix< ValueType > backwardTransitions
virtual void refinePartitionBasedOnSplitter(bisimulation::Block< bisimulation::DeterministicBlockData > &splitter, std::vector< bisimulation::Block< bisimulation::DeterministicBlockData > * > &splitterVector)=0
storm::utility::ConstantsComparator< ValueType > comparator
std::shared_ptr< ModelType > getQuotient() const
Retrieves the quotient of the model under the computed bisimulation.
virtual void splitInitialPartitionBasedOnRewards()
Splits the initial partition based on the (unique) reward model of the current model.
virtual void splitInitialPartitionBasedOnActionRewards(std::vector< std::set< ValueType > > const &rewardVector)
Splits the initial partition based on the given vector of action rewards.
BisimulationDecomposition(ModelType const &model, Options const &options)
Decomposes the given model into equivalence classes of a bisimulation.
storm::storage::bisimulation::Partition< BlockDataType > partition
void computeBisimulationDecomposition()
Computes the decomposition of the model into bisimulation equivalence classes.
virtual std::pair< storm::storage::BitVector, storm::storage::BitVector > getStatesWithProbability01()=0
Computes the set of states with probability 0/1 for satisfying phi until psi.
virtual void initializeMeasureDrivenPartition()
Creates the measure-driven initial partition for reaching psi states from phi states.
virtual void initializeLabelBasedPartition()
Initializes the initial partition based on all respected labels.
virtual void initialize()
A function that can initialize auxiliary data structures.
A class that holds a possibly non-square matrix in the compressed row storage format.
#define STORM_LOG_INFO(message)
Definition logging.h:27
#define STORM_LOG_STATISTICS(message)
Definition logging.h:41
#define STORM_LOG_WARN_COND(cond, message)
Definition macros.h:36
#define STORM_LOG_THROW(cond, exception, message)
Definition macros.h:28
FragmentSpecification propositional()
bool isTerminate()
Check whether the program should terminate (due to some abort signal).
typename detail::IntervalMetaProgrammingHelper< ValueType >::BaseType IntervalBaseType
Helper to access the type in which interval boundaries are stored.
void preserveFormula(storm::logic::Formula const &formula)
Changes the options in a way that the given formula is preserved.
static Options preservingAllLabels(ValueType const &tolerance)
Creates an object representing the options necessary to obtain the quotient that respects all atomic ...
Options(ModelType const &model, storm::logic::Formula const &formula, ValueType const &tolerance)
Creates an object representing the options necessary to obtain the quotient while still preserving th...
std::optional< std::set< std::string > > respectedAtomicPropositions
An optional set of strings that indicate which of the atomic propositions of the model are to be resp...