Storm 1.13.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
21
22namespace storm {
23namespace storage {
24
25using namespace bisimulation;
26
27template<typename ModelType, typename BlockDataType>
29 this->preserveSingleFormula(model, formula);
30}
31
32template<typename ModelType, typename BlockDataType>
34 std::vector<std::shared_ptr<storm::logic::Formula const>> const& formulas)
35 : Options() {
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>
52 phiStates(),
53 psiStates(),
55 buildQuotient(true),
56 keepRewards(false),
58 bounded(false),
59 discounted(false) {
60 // Intentionally left empty.
61}
62
63template<typename ModelType, typename BlockDataType>
65 // Disable the measure driven initial partition.
67 phiStates.reset();
68 psiStates.reset();
69
70 // Retrieve information about formula.
72
73 // Preserve rewards if necessary.
74 keepRewards = keepRewards || info.containsRewardOperator() || info.containsRewardBoundedFormula();
75
76 // Preserve bounded properties if necessary.
77 bounded = bounded || (info.containsBoundedUntilFormula() || info.containsNextFormula() || info.containsCumulativeRewardFormula());
78
79 // Preserve discounted properties if necessary.
80 discounted = discounted || info.containsDiscountFormula();
81
82 // Compute the relevant labels and expressions.
83 this->addToRespectedAtomicPropositions(formula.getAtomicExpressionFormulas(), formula.getAtomicLabelFormulas());
84}
85
86template<typename ModelType, typename BlockDataType>
87void BisimulationDecomposition<ModelType, BlockDataType>::Options::preserveSingleFormula(ModelType const& model, storm::logic::Formula const& formula) {
88 // Retrieve information about formula.
90
91 keepRewards = info.containsRewardOperator() || info.containsRewardBoundedFormula();
92
93 // We need to preserve bounded properties iff the formula contains a bounded until or a next subformula.
95
96 // We need to preserve discounting iff the formula contains a discounted subformula
97 discounted = info.containsDiscountFormula();
98
99 // Compute the relevant labels and expressions.
100 this->addToRespectedAtomicPropositions(formula.getAtomicExpressionFormulas(), formula.getAtomicLabelFormulas());
101
102 // Check whether measure driven initial partition is possible and, if so, set it.
103 this->checkAndSetMeasureDrivenInitialPartition(model, formula);
104}
105
106template<typename ModelType, typename BlockDataType>
107void BisimulationDecomposition<ModelType, BlockDataType>::Options::checkAndSetMeasureDrivenInitialPartition(ModelType const& model,
108 storm::logic::Formula const& formula) {
109 std::shared_ptr<storm::logic::Formula const> newFormula = formula.asSharedPointer();
110
111 if (formula.isProbabilityOperatorFormula()) {
113 optimalityType = formula.asProbabilityOperatorFormula().getOptimalityType();
114 } else if (formula.asProbabilityOperatorFormula().hasBound()) {
116 if (comparisonType == storm::logic::ComparisonType::Less || comparisonType == storm::logic::ComparisonType::LessEqual) {
117 optimalityType = OptimizationDirection::Maximize;
118 } else {
119 optimalityType = OptimizationDirection::Minimize;
120 }
121 }
123 } else if (formula.isRewardOperatorFormula()) {
125 optimalityType = formula.asRewardOperatorFormula().getOptimalityType();
126 } else if (formula.asRewardOperatorFormula().hasBound()) {
128 if (comparisonType == storm::logic::ComparisonType::Less || comparisonType == storm::logic::ComparisonType::LessEqual) {
129 optimalityType = OptimizationDirection::Maximize;
130 } else {
131 optimalityType = OptimizationDirection::Minimize;
132 }
133 }
134 newFormula = formula.asRewardOperatorFormula().getSubformula().asSharedPointer();
135 }
136
137 std::shared_ptr<storm::logic::Formula const> leftSubformula = std::make_shared<storm::logic::BooleanLiteralFormula>(true);
138 std::shared_ptr<storm::logic::Formula const> rightSubformula;
139 if (newFormula->isUntilFormula()) {
140 leftSubformula = newFormula->asUntilFormula().getLeftSubformula().asSharedPointer();
141 rightSubformula = newFormula->asUntilFormula().getRightSubformula().asSharedPointer();
142 if (leftSubformula->isInFragment(storm::logic::propositional()) && rightSubformula->isInFragment(storm::logic::propositional())) {
143 measureDrivenInitialPartition = true;
144 }
145 } else if (newFormula->isEventuallyFormula()) {
146 rightSubformula = newFormula->asEventuallyFormula().getSubformula().asSharedPointer();
147 if (rightSubformula->isInFragment(storm::logic::propositional())) {
148 measureDrivenInitialPartition = true;
149 }
150 }
151
152 if (measureDrivenInitialPartition) {
153 storm::modelchecker::SparsePropositionalModelChecker<ModelType> checker(model);
154 std::unique_ptr<storm::modelchecker::CheckResult> phiStatesCheckResult = checker.check(*leftSubformula);
155 std::unique_ptr<storm::modelchecker::CheckResult> psiStatesCheckResult = checker.check(*rightSubformula);
156
157 using SolutionType = storm::IntervalBaseType<ValueType>;
158 phiStates = phiStatesCheckResult->template asExplicitQualitativeCheckResult<SolutionType>().getTruthValuesVector();
159 psiStates = psiStatesCheckResult->template asExplicitQualitativeCheckResult<SolutionType>().getTruthValuesVector();
160 } else {
161 optimalityType.reset();
162 }
163}
164
165template<typename ModelType, typename BlockDataType>
167 std::vector<std::shared_ptr<storm::logic::AtomicExpressionFormula const>> const& expressions,
168 std::vector<std::shared_ptr<storm::logic::AtomicLabelFormula const>> const& labels) {
169 std::set<std::string> labelsToRespect;
170 for (auto const& labelFormula : labels) {
171 labelsToRespect.insert(labelFormula->getLabel());
172 }
173 for (auto const& expressionFormula : expressions) {
174 labelsToRespect.insert(expressionFormula->toString());
175 }
176 if (!respectedAtomicPropositions) {
177 respectedAtomicPropositions = labelsToRespect;
178 } else {
179 respectedAtomicPropositions.value().insert(labelsToRespect.begin(), labelsToRespect.end());
180 }
181}
182
183template<typename ModelType, typename BlockDataType>
185 : BisimulationDecomposition(model, model.getBackwardTransitions(), options) {
186 // Intentionally left empty.
187}
188
189template<typename ModelType, typename BlockDataType>
192 Options const& options)
194 STORM_LOG_THROW(!options.getKeepRewards() || !model.hasRewardModel() || model.hasUniqueRewardModel(), storm::exceptions::IllegalFunctionCallException,
195 "Bisimulation currently only supports models with at most one reward model.");
196 STORM_LOG_THROW(!options.getKeepRewards() || !model.hasRewardModel() || !model.getUniqueRewardModel().hasTransitionRewards(),
197 storm::exceptions::IllegalFunctionCallException,
198 "Bisimulation is currently supported for models with state or action rewards only. Consider converting the transition rewards to state "
199 "rewards (via suitable function calls).");
200 STORM_LOG_THROW(options.getType() != BisimulationType::Weak || !options.getBounded(), storm::exceptions::IllegalFunctionCallException,
201 "Weak bisimulation cannot preserve bounded properties.");
202 STORM_LOG_THROW(options.getType() != BisimulationType::Weak || !options.getDiscounted(), storm::exceptions::IllegalFunctionCallException,
203 "Weak bisimulation cannot preserve discounted properties.");
204
205 // Fix the respected atomic propositions if they were not explicitly given.
206 if (!this->options.respectedAtomicPropositions) {
207 this->options.respectedAtomicPropositions = model.getStateLabeling().getLabels();
208 }
210
211template<typename ModelType, typename BlockDataType>
213 std::chrono::high_resolution_clock::time_point totalStart = std::chrono::high_resolution_clock::now();
214
215 std::chrono::high_resolution_clock::time_point initialPartitionStart = std::chrono::high_resolution_clock::now();
216 // initialize the initial partition.
217 if (options.measureDrivenInitialPartition) {
218 STORM_LOG_THROW(options.phiStates, storm::exceptions::InvalidOptionException, "Unable to compute measure-driven initial partition without phi states.");
219 STORM_LOG_THROW(options.psiStates, storm::exceptions::InvalidOptionException, "Unable to compute measure-driven initial partition without psi states.");
221 } else {
223 }
224 STORM_LOG_WARN_COND(partition.size() > 1, "Initial partition consists only of a single block.");
225 std::chrono::high_resolution_clock::duration initialPartitionTime = std::chrono::high_resolution_clock::now() - initialPartitionStart;
227 this->initialize();
228
229 std::chrono::high_resolution_clock::time_point refinementStart = std::chrono::high_resolution_clock::now();
231 std::chrono::high_resolution_clock::duration refinementTime = std::chrono::high_resolution_clock::now() - refinementStart;
232
233 std::chrono::high_resolution_clock::time_point extractionStart = std::chrono::high_resolution_clock::now();
235 std::chrono::high_resolution_clock::duration extractionTime = std::chrono::high_resolution_clock::now() - extractionStart;
236
237 std::chrono::high_resolution_clock::time_point quotientBuildStart = std::chrono::high_resolution_clock::now();
238 if (options.buildQuotient) {
239 this->buildQuotient();
240 }
241 std::chrono::high_resolution_clock::duration quotientBuildTime = std::chrono::high_resolution_clock::now() - quotientBuildStart;
242
243 std::chrono::high_resolution_clock::duration totalTime = std::chrono::high_resolution_clock::now() - totalStart;
244
246 std::chrono::milliseconds initialPartitionTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(initialPartitionTime);
247 std::chrono::milliseconds refinementTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(refinementTime);
248 std::chrono::milliseconds extractionTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(extractionTime);
249 std::chrono::milliseconds quotientBuildTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(quotientBuildTime);
250 std::chrono::milliseconds totalTimeInMilliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(totalTime);
251 std::cout << "\nTime breakdown:\n";
252 std::cout << " * time for initial partition: " << initialPartitionTimeInMilliseconds.count() << "ms\n";
253 std::cout << " * time for partitioning: " << refinementTimeInMilliseconds.count() << "ms\n";
254 std::cout << " * time for extraction: " << extractionTimeInMilliseconds.count() << "ms\n";
255 std::cout << " * time for building quotient: " << quotientBuildTimeInMilliseconds.count() << "ms\n";
256 std::cout << "------------------------------------------\n";
257 std::cout << " * total time: " << totalTimeInMilliseconds.count() << "ms\n\n";
258 }
259}
260
261template<typename ModelType, typename BlockDataType>
263 // Insert all blocks into the splitter queue as a (potential) splitter.
264 std::vector<Block<BlockDataType>*> splitterQueue;
265 std::for_each(partition.getBlocks().begin(), partition.getBlocks().end(), [&](std::unique_ptr<Block<BlockDataType>> const& block) {
266 block->data().setSplitter();
267 splitterQueue.push_back(block.get());
268 });
270 // Then perform the actual splitting until there are no more splitters.
271 uint_fast64_t iterations = 0;
272 while (!splitterQueue.empty()) {
273 ++iterations;
274
275 // Get and prepare the next splitter.
276 // Sort the splitters according to their sizes to prefer small splitters. That is just a heuristic, but
277 // tends to work well.
278 std::sort(splitterQueue.begin(), splitterQueue.end(),
279 [](Block<BlockDataType> const* b1, Block<BlockDataType> const* b2) { return b1->getNumberOfStates() > b2->getNumberOfStates(); });
280 Block<BlockDataType>* splitter = splitterQueue.back();
281 splitterQueue.pop_back();
282 splitter->data().setSplitter(false);
283
284 // Now refine the partition using the current splitter.
285 refinePartitionBasedOnSplitter(*splitter, splitterQueue);
286
288 std::cout << "Performed " << iterations << " iterations of partition refinement before abort.\n";
289 STORM_LOG_THROW(false, storm::exceptions::AbortException, "Aborted in bisimulation computation.");
290 break;
291 }
292 }
293}
294
295template<typename ModelType, typename BlockDataType>
297 STORM_LOG_THROW(this->quotient != nullptr, storm::exceptions::IllegalFunctionCallException,
298 "Unable to retrieve quotient model from bisimulation decomposition, because it was not built.");
299 return this->quotient;
300}
301
302template<typename ModelType, typename BlockDataType>
304 auto const& rewardModel = model.getUniqueRewardModel();
305 if (rewardModel.hasStateRewards()) {
306 this->splitInitialPartitionBasedOnRewards(rewardModel.getStateRewardVector());
307 }
308 if (rewardModel.hasStateActionRewards()) {
309 if (model.isNondeterministicModel()) {
310 std::vector<std::set<ValueType>> actionRewards;
311 actionRewards.reserve(model.getNumberOfStates());
312 for (storm::storage::sparse::state_type state = 0; state < model.getNumberOfStates(); ++state) {
313 std::set<ValueType> rewardsAtState;
314 for (auto choice = model.getTransitionMatrix().getRowGroupIndices()[state];
315 choice < model.getTransitionMatrix().getRowGroupIndices()[state + 1]; ++choice) {
316 rewardsAtState.insert(rewardModel.getStateActionReward(choice));
317 }
318 actionRewards.push_back(std::move(rewardsAtState));
319 }
321 } else {
322 this->splitInitialPartitionBasedOnRewards(rewardModel.getStateActionRewardVector());
323 }
324 }
325}
326
327template<typename ModelType, typename BlockDataType>
330 return rewardVector[a] < rewardVector[b];
331 });
332}
333
334template<typename ModelType, typename BlockDataType>
336 partition.split([&actionRewards](storm::storage::sparse::state_type const& a, storm::storage::sparse::state_type const& b) {
337 return actionRewards[a] < actionRewards[b];
338 });
339}
340
341template<typename ModelType, typename BlockDataType>
344
345 for (auto const& label : options.respectedAtomicPropositions.value()) {
346 if (label == "init") {
347 continue;
348 }
349 partition.splitStates(model.getStates(label));
350 }
351
352 // If the model has state rewards, we need to consider them, because otherwise reward properties are not
353 // preserved.
354 if (options.getKeepRewards() && model.hasRewardModel()) {
356 }
357}
358
359template<typename ModelType, typename BlockDataType>
361 std::pair<storm::storage::BitVector, storm::storage::BitVector> statesWithProbability01 = this->getStatesWithProbability01();
362
363 std::optional<storm::storage::sparse::state_type> representativePsiState;
364 if (!options.psiStates.value().empty()) {
365 representativePsiState = *options.psiStates.value().begin();
366 }
367
369 model.getNumberOfStates(), statesWithProbability01.first,
370 options.getBounded() || options.getKeepRewards() ? options.psiStates.value() : statesWithProbability01.second, representativePsiState);
371
372 // If the model has state rewards, we need to consider them, because otherwise reward properties are not
373 // preserved.
374 if (options.getKeepRewards() && model.hasRewardModel()) {
376 }
377}
378
379template<typename ModelType, typename BlockDataType>
381 // Intentionally left empty.
382}
383
384template<typename ModelType, typename BlockDataType>
386 // Now move the states from the internal partition into their final place in the decomposition. We do so in
387 // a way that maintains the block IDs as indices.
388 this->blocks.resize(partition.size());
389 for (auto const& blockPtr : partition.getBlocks()) {
390 // We need to sort the states to allow for rapid construction of the blocks.
391 partition.sortBlock(*blockPtr);
392
393 // Convert the state-value-pairs to states only.
394 this->blocks[blockPtr->getId()] = block_type(partition.begin(*blockPtr), partition.end(*blockPtr), true);
395 }
396}
397
401
405
409} // namespace storage
410} // 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.
A class that holds a possibly non-square matrix in the compressed row storage format.
#define STORM_LOG_WARN_COND(cond, message)
Definition macros.h:38
#define STORM_LOG_THROW(cond, exception, message)
Definition macros.h:30
FragmentSpecification propositional()
SettingsType const & getModule()
Get module.
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.
std::optional< storm::storage::BitVector > phiStates
bool buildQuotient
A flag that governs whether the quotient model is actually built or only the decomposition is compute...
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...
std::optional< storm::storage::BitVector > psiStates