Storm 1.14.0.1
A Modern Probabilistic Model Checker
Loading...
Searching...
No Matches
StrongBisimulationTest.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdint>
5#include <numeric>
6#include <random>
7#include <set>
8
13
14namespace {
15
24
25using ValueType = double;
26
31void checkChoiceMapping(storm::models::sparse::Model<ValueType> const& model, storm::bisimulation::ReturnType<ValueType> const& result) {
32 ASSERT_TRUE(result.toQuotientChoiceMapping.has_value());
33 ASSERT_EQ(model.getNumberOfChoices(), result.toQuotientChoiceMapping->size());
34 for (uint64_t state = 0; state < model.getNumberOfStates(); ++state) {
35 auto const quotientChoices = result.quotient->getTransitionMatrix().getRowGroupIndices(result.toQuotientStateMapping[state]);
36 std::set<uint64_t> const expectedChoices(quotientChoices.begin(), quotientChoices.end());
37 std::set<uint64_t> mappedChoices;
38 for (uint64_t const choice : model.getTransitionMatrix().getRowGroupIndices(state)) {
39 mappedChoices.insert((*result.toQuotientChoiceMapping)[choice]);
40 }
41 EXPECT_EQ(expectedChoices, mappedChoices) << "unexpected quotient choices for the choices of state " << state;
42 }
43}
44
52void testQuotient(std::string const& prismFile, std::string const& formulaString, uint64_t expectedModelStates, uint64_t expectedStates,
53 uint64_t expectedTransitions, uint64_t expectedChoices, Options const options = strongOptions(), BuildOptions buildOptions = {}) {
54#ifndef STORM_HAVE_Z3
55 GTEST_SKIP() << "Z3 not available.";
56#endif
57 if (options.stateLabelPreservation == StateLabelPreservation::All) {
58 buildOptions.allLabels = true;
59 }
60 auto const input = buildFromPrism<ValueType>(prismFile, formulaString, buildOptions);
61 ASSERT_EQ(expectedModelStates, input.model->getNumberOfStates());
62
63 Options quotientOptions = options;
64 quotientOptions.createQuotientChoiceMapping = true; // Does not affect the quotient, but lets us check the mapping.
65 auto const result = storm::bisimulation::performBisimulationMinimization<ValueType>(*input.model, input.formulas, quotientOptions);
66 auto const& quotient = result.quotient;
67 checkChoiceMapping(*input.model, result);
68 EXPECT_EQ(input.model->getType(), quotient->getType());
69 EXPECT_EQ(expectedStates, quotient->getNumberOfStates());
70 EXPECT_EQ(expectedTransitions, quotient->getNumberOfTransitions());
71 EXPECT_EQ(expectedChoices, quotient->getNumberOfChoices());
72 // The quotient keeps the choice labels and the choice origins of the model (but not necessarily an empty choice labeling).
73 auto const choiceLabels = [](auto const& model) {
74 std::set<std::string> result;
75 if (model->hasChoiceLabeling()) {
76 auto const labels = model->getChoiceLabeling().getLabels();
77 result.insert(labels.begin(), labels.end());
78 }
79 return result;
80 };
81 EXPECT_EQ(choiceLabels(input.model), choiceLabels(quotient));
82 EXPECT_EQ(input.model->hasChoiceOrigins(), quotient->hasChoiceOrigins());
83
84 // The formulas were parsed for the PRISM program, so unlike the formula string they can be checked even if they contain atomic expressions.
85 for (auto const& formula : input.formulas) {
86 EXPECT_NEAR(checkFormula<ValueType>(quotient, formula), checkFormula<ValueType>(input.model, formula), 1e-9) << *formula;
87 }
88}
89
93Options allLabelOptions() {
94 Options options = strongOptions();
95 options.stateLabelPreservation = StateLabelPreservation::All;
96 return options;
97}
98
103Options approximateOptions() {
104 Options options = strongOptions();
106 return options;
107}
108
109// ------------------------------------------------------------
110// Deterministic models
111// ------------------------------------------------------------
112
116TEST(StrongBisimulationTest, SilentChainIsNotCollapsed) {
117 // 0 -> 1 -> 2 -> {3, 4}; 3 and 4 absorbing, 3 labeled "goal".
119 builder.addNextValue(0, 1, 1.0);
120 builder.addNextValue(1, 2, 1.0);
121 builder.addNextValue(2, 3, 0.5);
122 builder.addNextValue(2, 4, 0.5);
123 builder.addNextValue(3, 3, 1.0);
124 builder.addNextValue(4, 4, 1.0);
125 auto const model = buildModel<storm::models::sparse::Dtmc<ValueType>>(builder.build(), {{"goal", {3}}});
126
127 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient;
128 EXPECT_EQ(5ull, quotient->getNumberOfStates());
129 EXPECT_NEAR(checkFormula<ValueType>(quotient, "P=? [F \"goal\"]"), 0.5, 1e-12);
130}
131
135TEST(StrongBisimulationTest, IdenticalDistributions) {
136 // 1 and 2 have the same distribution over {3} and {4}, so they are merged; 0 keeps them apart only through its own distribution.
138 builder.addNextValue(0, 1, 0.5);
139 builder.addNextValue(0, 2, 0.5);
140 builder.addNextValue(1, 3, 0.25);
141 builder.addNextValue(1, 4, 0.75);
142 builder.addNextValue(2, 3, 0.25);
143 builder.addNextValue(2, 4, 0.75);
144 builder.addNextValue(3, 3, 1.0);
145 builder.addNextValue(4, 4, 1.0);
146 auto const model = buildModel<storm::models::sparse::Dtmc<ValueType>>(builder.build(), {{"goal", {3}}});
147
148 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient;
149 EXPECT_EQ(4ull, quotient->getNumberOfStates()); // {0}, {1,2}, {3}, {4}
150 EXPECT_NEAR(checkFormula<ValueType>(quotient, "P=? [F \"goal\"]"), 0.25, 1e-12);
151}
152
156TEST(StrongBisimulationTest, CtmcInternalRateIsObservable) {
157 // 0 -> 1 with rate 100 and 0 -> 2 with rate 1; 1 -> 2 with rate 1; 2 absorbing and labeled "goal".
159 builder.addNextValue(0, 1, 100.0);
160 builder.addNextValue(0, 2, 1.0);
161 builder.addNextValue(1, 2, 1.0);
162 builder.addNextValue(2, 2, 1.0);
163 auto const model = buildModel<storm::models::sparse::Ctmc<ValueType>>(builder.build(), {{"goal", {2}}});
164
165 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient;
166 EXPECT_EQ(storm::models::ModelType::Ctmc, quotient->getType());
167 EXPECT_EQ(3ull, quotient->getNumberOfStates()); // weak bisimulation merges 0 and 1 here, strong bisimulation does not
168}
169
173TEST(StrongBisimulationTest, CtmcExitRateIsObservable) {
174 // 0 and 1 both move to 2 only, but at different speeds.
176 builder.addNextValue(0, 2, 1.0);
177 builder.addNextValue(1, 2, 2.0);
178 builder.addNextValue(2, 2, 1.0);
179 auto const model = buildModel<storm::models::sparse::Ctmc<ValueType>>(builder.build(), {{"goal", {2}}});
180
181 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient;
182 EXPECT_EQ(3ull, quotient->getNumberOfStates());
183}
184
189TEST(StrongBisimulationTest, PropositionalSubformulas) {
190 // 0 moves uniformly to 1, 2 and 3, which are absorbing. 1 is labeled with "a" and "b", 2 is unlabeled and 3 is labeled with "a".
192 builder.addNextValue(0, 1, 1.0 / 3);
193 builder.addNextValue(0, 2, 1.0 / 3);
194 builder.addNextValue(0, 3, 1.0 / 3);
195 builder.addNextValue(1, 1, 1.0);
196 builder.addNextValue(2, 2, 1.0);
197 builder.addNextValue(3, 3, 1.0);
198 auto const model = buildModel<storm::models::sparse::Dtmc<ValueType>>(builder.build(), {{"a", {1, 3}}, {"b", {1}}});
199 Options individualOptions = strongOptions();
200 individualOptions.stateLabelPreservation = StateLabelPreservation::FormulaIndividual;
201
202 storm::parser::FormulaParser formulaParser;
203 for (std::string const formulaString : {"P=? [F \"a\" & !\"b\"]", "P=? [F P>=0.5 [F \"a\" & !\"b\"]]"}) {
204 std::vector<std::shared_ptr<storm::logic::Formula const>> const formulas{formulaParser.parseSingleFormulaFromString(formulaString)};
205 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, formulas, strongOptions()).quotient;
206 EXPECT_EQ(3ull, quotient->getNumberOfStates()) << formulaString; // {0}, {1, 2} and {3}
207 EXPECT_NEAR(checkFormula<ValueType>(quotient, formulaString), 1.0 / 3, 1e-12) << formulaString;
208
209 auto const individualQuotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, formulas, individualOptions).quotient;
210 EXPECT_EQ(4ull, individualQuotient->getNumberOfStates()) << formulaString;
211 EXPECT_NEAR(checkFormula<ValueType>(individualQuotient, formulaString), 1.0 / 3, 1e-12) << formulaString;
212 }
213}
214
219TEST(StrongBisimulationTest, InitLabelInPropositionalSubformula) {
220 // 0 and 1 both move to the absorbing state 2, which is labeled "a". Only 1 is initial, so merging 0 and 1 would make 0 the representative of the initial
221 // block and thereby lose the initial state.
223 builder.addNextValue(0, 2, 1.0);
224 builder.addNextValue(1, 2, 1.0);
225 builder.addNextValue(2, 2, 1.0);
226 auto const model = buildModel<storm::models::sparse::Dtmc<ValueType>>(builder.build(), {{"a", {2}}});
227 model->getStateLabeling().removeLabelFromState("init", 0);
228 model->getStateLabeling().addLabelToState("init", 1);
229
230 std::string const formulaString = "P=? [F \"a\" & !\"init\"]";
231 storm::parser::FormulaParser formulaParser;
232 std::vector<std::shared_ptr<storm::logic::Formula const>> const formulas{formulaParser.parseSingleFormulaFromString(formulaString)};
233 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, formulas, strongOptions()).quotient;
234 EXPECT_EQ(3ull, quotient->getNumberOfStates());
235 ASSERT_EQ(1ull, quotient->getInitialStates().getNumberOfSetBits());
236 EXPECT_NEAR(checkFormula<ValueType>(quotient, formulaString), 1.0, 1e-12);
237}
238
243TEST(StrongBisimulationTest, InitLabelCombinedWithOtherLabel) {
244 // 0 (labeled "a") and 1 (the only initial state) both move to the absorbing state 2. No state satisfies "init" & "a", but a block {0, 1, 2} would be
245 // represented by state 0 and thus yield a quotient state that is both initial and labeled "a".
247 builder.addNextValue(0, 2, 1.0);
248 builder.addNextValue(1, 2, 1.0);
249 builder.addNextValue(2, 2, 1.0);
250 auto const model = buildModel<storm::models::sparse::Dtmc<ValueType>>(builder.build(), {{"a", {0}}});
251 model->getStateLabeling().removeLabelFromState("init", 0);
252 model->getStateLabeling().addLabelToState("init", 1);
253
254 std::string const formulaString = "P=? [F \"init\" & \"a\"]";
255 storm::parser::FormulaParser formulaParser;
256 std::vector<std::shared_ptr<storm::logic::Formula const>> const formulas{formulaParser.parseSingleFormulaFromString(formulaString)};
257 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, formulas, strongOptions()).quotient;
258 EXPECT_EQ(2ull, quotient->getNumberOfStates()); // {1} and {0, 2}
259 EXPECT_NEAR(checkFormula<ValueType>(quotient, formulaString), 0.0, 1e-12);
260}
261
265TEST(StrongBisimulationTest, FloatingPointRoundingNeedsTolerance) {
266 // 1 reaches {3, 4} via two transitions (0.1 and 0.2), 2 via a single one (0.3); both sums equal 0.3 mathematically but not as doubles.
268 builder.addNextValue(0, 1, 0.5);
269 builder.addNextValue(0, 2, 0.5);
270 builder.addNextValue(1, 3, 0.1);
271 builder.addNextValue(1, 4, 0.2);
272 builder.addNextValue(1, 5, 0.7);
273 builder.addNextValue(2, 3, 0.3);
274 builder.addNextValue(2, 5, 0.7);
275 builder.addNextValue(3, 3, 1.0);
276 builder.addNextValue(4, 4, 1.0);
277 builder.addNextValue(5, 5, 1.0);
278 auto const model = buildModel<storm::models::sparse::Dtmc<ValueType>>(builder.build(), {{"target", {3, 4}}});
279
280 EXPECT_EQ(5ull, storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient->getNumberOfStates());
281 EXPECT_EQ(4ull, storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, approximateOptions()).quotient->getNumberOfStates());
282 // When no tolerance is set, the model's stochastic tolerance is used -- the default model tolerance (see
283 // storm::Environment::DefaultModelTolerance), which also identifies the two states.
285 EXPECT_EQ(4ull, storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, Options{}).quotient->getNumberOfStates());
286}
287
288// ------------------------------------------------------------
289// Nondeterministic models
290// ------------------------------------------------------------
291
295TEST(StrongBisimulationTest, ChoiceSetsAreCompared) {
296 // States 0 and 1 offer the same two distributions over {2} and {3}, but in a different order, and 1 offers one of them twice.
297 storm::storage::SparseMatrixBuilder<ValueType> builder(7, 4, 0, false, true, 4);
298 builder.newRowGroup(0);
299 builder.addNextValue(0, 2, 1.0); // state 0, choice 0
300 builder.addNextValue(1, 2, 0.5); // state 0, choice 1
301 builder.addNextValue(1, 3, 0.5);
302 builder.newRowGroup(2);
303 builder.addNextValue(2, 2, 0.5); // state 1, choice 0
304 builder.addNextValue(2, 3, 0.5);
305 builder.addNextValue(3, 2, 1.0); // state 1, choice 1
306 builder.addNextValue(4, 2, 1.0); // state 1, choice 2 (a duplicate of choice 1)
307 builder.newRowGroup(5);
308 builder.addNextValue(5, 2, 1.0); // state 2, absorbing and labeled "goal"
309 builder.newRowGroup(6);
310 builder.addNextValue(6, 3, 1.0); // state 3, absorbing
311 auto const model = buildModel<storm::models::sparse::Mdp<ValueType>>(builder.build(), {{"goal", {2}}});
312
313 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient;
314 EXPECT_EQ(3ull, quotient->getNumberOfStates()); // {0,1}, {2}, {3}
315 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Pmin=? [F \"goal\"]"), 0.5, 1e-12);
316 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Pmax=? [F \"goal\"]"), 1.0, 1e-12);
317}
318
323TEST(StrongBisimulationTest, ChoiceOrderMattersIfActionSensitive) {
324 // States 0 and 1 offer the same two distributions over {2} and {3}, but in a different order.
325 storm::storage::SparseMatrixBuilder<ValueType> builder(6, 4, 0, false, true, 4);
326 builder.newRowGroup(0);
327 builder.addNextValue(0, 2, 1.0); // state 0, choice 0
328 builder.addNextValue(1, 2, 0.5); // state 0, choice 1
329 builder.addNextValue(1, 3, 0.5);
330 builder.newRowGroup(2);
331 builder.addNextValue(2, 2, 0.5); // state 1, choice 0
332 builder.addNextValue(2, 3, 0.5);
333 builder.addNextValue(3, 2, 1.0); // state 1, choice 1
334 builder.newRowGroup(4);
335 builder.addNextValue(4, 2, 1.0); // state 2, absorbing and labeled "goal"
336 builder.newRowGroup(5);
337 builder.addNextValue(5, 3, 1.0); // state 3, absorbing
338 auto const model = buildModel<storm::models::sparse::Mdp<ValueType>>(builder.build(), {{"goal", {2}}});
339
340 EXPECT_EQ(3ull, storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient->getNumberOfStates());
341
342 Options options = strongOptions();
343 options.actionSensitive = true;
344 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, options).quotient;
345 EXPECT_EQ(4ull, quotient->getNumberOfStates());
346 EXPECT_EQ(6ull, quotient->getNumberOfChoices());
347}
348
353TEST(StrongBisimulationTest, ApproximateChoicePartnersAtDifferentPositions) {
354 // States 0 and 1 have three choices each over "goal" (2) and "sink" (3). Every choice has a partner within 0.01 in the other state, but the middle
355 // choices are 0.011 apart.
356 storm::storage::SparseMatrixBuilder<ValueType> builder(8, 4, 0, false, true, 4);
357 builder.newRowGroup(0);
358 builder.addNextValue(0, 2, 0.001);
359 builder.addNextValue(0, 3, 0.999);
360 builder.addNextValue(1, 2, 0.019);
361 builder.addNextValue(1, 3, 0.981);
362 builder.addNextValue(2, 2, 0.039);
363 builder.addNextValue(2, 3, 0.961);
364 builder.newRowGroup(3);
365 builder.addNextValue(3, 2, 0.010);
366 builder.addNextValue(3, 3, 0.990);
367 builder.addNextValue(4, 2, 0.030);
368 builder.addNextValue(4, 3, 0.970);
369 builder.addNextValue(5, 2, 0.048);
370 builder.addNextValue(5, 3, 0.952);
371 builder.newRowGroup(6);
372 builder.addNextValue(6, 2, 1.0);
373 builder.newRowGroup(7);
374 builder.addNextValue(7, 3, 1.0);
375 auto const model = buildModel<storm::models::sparse::Mdp<ValueType>>(builder.build(), {{"goal", {2}}, {"sink", {3}}});
376
377 EXPECT_EQ(4ull, storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient->getNumberOfStates());
378 Options options = strongOptions();
379 options.tolerance = storm::utility::convertNumber<storm::RationalNumber>(0.02); // i.e., halfTolerance 0.01
380 EXPECT_EQ(3ull, storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, options).quotient->getNumberOfStates());
381
382 // The choices 0.001 and 0.019 of state 0 both only have the partner 0.010 in state 1, so the quotient choices cannot be matched with the choices of
383 // state 1 one-to-one.
384 options.createQuotientChoiceMapping = true;
385 STORM_SILENT_EXPECT_THROW(storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, options), storm::exceptions::UnexpectedException);
386}
387
392TEST(StrongBisimulationTest, ApproximateChoiceMappingIsOneToOne) {
393 // The states 0 and 1 (and, analogously, 4 and 5) have three choices each over "goal" (2) and "sink" (3). With tolerance 0.2, the two states of such a
394 // pair are bisimilar (their choices are pairwise 0.05 apart), while the choices of a single state are more than 0.1 apart. Some choices are within the
395 // tolerance of two choices of the other state, e.g. 0.15 of state 1 is within 0.2 of both, 0.1 and 0.32 of state 0. The second pair mirrors the first one
396 // (p vs. 1-p), so that one of them is affected no matter how the signatures are sorted and which state of a pair is the representative.
397 storm::storage::SparseMatrixBuilder<ValueType> builder(14, 6, 0, true, true, 6); // States 4 and 5 have no predecessors, so we force the dimensions.
398 auto const addChoice = [&builder](uint64_t const row, ValueType const goalProbability) {
399 builder.addNextValue(row, 2, goalProbability);
400 builder.addNextValue(row, 3, storm::utility::one<ValueType>() - goalProbability);
401 };
402 builder.newRowGroup(0); // state 0
403 addChoice(0, 0.1);
404 addChoice(1, 0.32);
405 addChoice(2, 0.5);
406 builder.newRowGroup(3); // state 1
407 addChoice(3, 0.15);
408 addChoice(4, 0.27);
409 addChoice(5, 0.45);
410 builder.newRowGroup(6);
411 builder.addNextValue(6, 2, 1.0); // state 2, absorbing and labeled "goal"
412 builder.newRowGroup(7);
413 builder.addNextValue(7, 3, 1.0); // state 3, absorbing and labeled "sink"
414 builder.newRowGroup(8); // state 4
415 addChoice(8, 0.5);
416 addChoice(9, 0.68);
417 addChoice(10, 0.9);
418 builder.newRowGroup(11); // state 5
419 addChoice(11, 0.55);
420 addChoice(12, 0.73);
421 addChoice(13, 0.85);
422 auto const model = buildModel<storm::models::sparse::Mdp<ValueType>>(builder.build(), {{"goal", {2}}, {"sink", {3}}});
423
424 Options options = strongOptions();
425 options.tolerance = storm::utility::convertNumber<storm::RationalNumber>(0.2); // i.e., halfTolerance 0.1
426 options.createQuotientChoiceMapping = true;
427 auto const result = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, options);
428 ASSERT_EQ(4ull, result.quotient->getNumberOfStates()); // {0,1}, {2}, {3}, {4,5}
429 ASSERT_EQ(8ull, result.quotient->getNumberOfChoices());
430 checkChoiceMapping(*model, result);
431
432 // Within a pair, the i'th choices of the two states are only close to each other, so they share their quotient choice.
433 auto const& mapping = *result.toQuotientChoiceMapping;
434 for (uint64_t i = 0; i < 3; ++i) {
435 EXPECT_EQ(mapping[i], mapping[3 + i]) << "unexpected quotient choice for the choices " << i << " and " << (3 + i);
436 EXPECT_EQ(mapping[8 + i], mapping[11 + i]) << "unexpected quotient choice for the choices " << (8 + i) << " and " << (11 + i);
437 }
438}
439
448TEST(StrongBisimulationTest, MdpQuotientIsNotMeasureDriven) {
449 // A chain 0 -> 1 -> 2 -> 2 in which the goal is reached with probability one, plus a second choice in state 0 that takes a detour through state 3.
450 storm::storage::SparseMatrixBuilder<ValueType> builder(5, 4, 0, false, true, 4);
451 builder.newRowGroup(0);
452 builder.addNextValue(0, 1, 1.0); // state 0, choice 0
453 builder.addNextValue(1, 3, 1.0); // state 0, choice 1
454 builder.newRowGroup(2);
455 builder.addNextValue(2, 2, 1.0); // state 1
456 builder.newRowGroup(3);
457 builder.addNextValue(3, 2, 1.0); // state 2, absorbing and labeled "goal"
458 builder.newRowGroup(4);
459 builder.addNextValue(4, 1, 1.0); // state 3
460 auto const model = buildModel<storm::models::sparse::Mdp<ValueType>>(builder.build(), {{"goal", {2}}});
461
462 storm::parser::FormulaParser formulaParser;
463 std::vector<std::shared_ptr<storm::logic::Formula const>> const formulas{formulaParser.parseSingleFormulaFromString("Pmax=? [F \"goal\"]")};
464 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, formulas, strongOptions()).quotient;
465
466 // Every state reaches the goal with probability one, so a measure-driven initial partition would have merged all four states into a single one.
467 EXPECT_EQ(4ull, quotient->getNumberOfStates());
468 // No quotient state may carry both "init" and "goal", because no state of the original model does.
469 auto const& labeling = quotient->getStateLabeling();
470 ASSERT_TRUE(labeling.containsLabel("init"));
471 ASSERT_TRUE(labeling.containsLabel("goal"));
472 EXPECT_TRUE(labeling.getStates("init").isDisjointFrom(labeling.getStates("goal")));
473 // A step-bounded property distinguishes the states that a measure-driven partition would have merged.
474 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Pmax=? [F<=1 \"goal\"]"), checkFormula<ValueType>(model, "Pmax=? [F<=1 \"goal\"]"), 1e-12);
475 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Pmax=? [F<=2 \"goal\"]"), checkFormula<ValueType>(model, "Pmax=? [F<=2 \"goal\"]"), 1e-12);
476}
477
482TEST(StrongBisimulationTest, MarkovAutomaton) {
483 auto const model = storm::api::buildExplicitDRNModel<ValueType>(STORM_TEST_RESOURCES_DIR "/ma/jobscheduler.drn");
485
486 storm::parser::FormulaParser formulaParser;
487 std::vector<std::shared_ptr<storm::logic::Formula const>> const formulas{formulaParser.parseSingleFormulaFromString("Tmin=? [F \"all_jobs_finished\"]")};
488
489 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, formulas, strongOptions()).quotient;
490 EXPECT_EQ(storm::models::ModelType::MarkovAutomaton, quotient->getType());
491 EXPECT_LE(quotient->getNumberOfStates(), model->getNumberOfStates());
492 auto const markovAutomaton = quotient->template as<storm::models::sparse::MarkovAutomaton<ValueType>>();
493 // Markovian and probabilistic states must not be mixed, and a Markovian state has exactly one choice.
494 for (uint64_t state = 0; state < quotient->getNumberOfStates(); ++state) {
495 if (markovAutomaton->isMarkovianState(state)) {
496 EXPECT_EQ(1ull, quotient->getTransitionMatrix().getRowGroupSize(state));
497 }
498 }
499 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Tmin=? [F \"all_jobs_finished\"]"), checkFormula<ValueType>(model, "Tmin=? [F \"all_jobs_finished\"]"),
500 1e-9);
501 // Expected times are preserved in both optimization directions, cf. https://github.com/stormchecker/storm/issues/498.
502 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Tmax=? [F \"all_jobs_finished\"]"), checkFormula<ValueType>(model, "Tmax=? [F \"all_jobs_finished\"]"),
503 1e-9);
504}
505
512TEST(StrongBisimulationTest, MarkovAutomatonExpectedTime) {
513 // The probabilistic state 0 chooses between the Markovian states 1 and 2, which both move on to the "done" state 3 but at different rates.
514 storm::storage::SparseMatrixBuilder<ValueType> builder(5, 4, 0, false, true, 4);
515 builder.newRowGroup(0);
516 builder.addNextValue(0, 1, 1.0); // state 0 is probabilistic, so its rows hold probabilities: choice 0
517 builder.addNextValue(1, 2, 1.0); // state 0, choice 1
518 builder.newRowGroup(2);
519 builder.addNextValue(2, 3, 1.0); // state 1, Markovian with exit rate 1
520 builder.newRowGroup(3);
521 builder.addNextValue(3, 3, 2.0); // state 2, Markovian with exit rate 2
522 builder.newRowGroup(4);
523 builder.addNextValue(4, 3, 1.0); // state 3, Markovian, absorbing and labeled "done"
524 auto const model = buildMarkovAutomaton<ValueType>(builder.build(), storm::storage::BitVector(4, {1, 2, 3}), {{"done", {3}}});
525 ASSERT_NEAR(0.5, checkFormula<ValueType>(model, "Tmin=? [F \"done\"]"), 1e-12);
526 ASSERT_NEAR(1.0, checkFormula<ValueType>(model, "Tmax=? [F \"done\"]"), 1e-12);
527
528 storm::parser::FormulaParser formulaParser;
529 std::vector<std::shared_ptr<storm::logic::Formula const>> const formulas{formulaParser.parseSingleFormulaFromString("Tmin=? [F \"done\"]")};
530 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, formulas, strongOptions()).quotient;
531
532 // States 1 and 2 have the same distribution over the blocks, so only their exit rates keep them apart.
533 EXPECT_EQ(4ull, quotient->getNumberOfStates());
534 EXPECT_NEAR(0.5, checkFormula<ValueType>(quotient, "Tmin=? [F \"done\"]"), 1e-12);
535 EXPECT_NEAR(1.0, checkFormula<ValueType>(quotient, "Tmax=? [F \"done\"]"), 1e-12);
536}
537
542TEST(StrongBisimulationTest, MarkovAutomatonHybridStates) {
543 // The hybrid states 0 and 1 behave the same: both move to the Markovian state 2 (which moves on to the "goal" state 3), either through their Markovian
544 // choice or, without letting time pass, through their probabilistic one.
545 storm::storage::SparseMatrixBuilder<ValueType> builder(6, 4, 0, false, true, 4);
546 builder.newRowGroup(0);
547 builder.addNextValue(0, 2, 2.0); // The Markovian choice of state 0, holding a rate.
548 builder.addNextValue(1, 2, 1.0); // The probabilistic choice of state 0, with the same distribution.
549 builder.newRowGroup(2);
550 builder.addNextValue(2, 2, 2.0);
551 builder.addNextValue(3, 2, 1.0);
552 builder.newRowGroup(4);
553 builder.addNextValue(4, 3, 4.0);
554 builder.newRowGroup(5);
555 builder.addNextValue(5, 3, 1.0);
556 storm::storage::BitVector markovianStates(4);
557 markovianStates.set(0);
558 markovianStates.set(1);
559 markovianStates.set(2);
560 auto const model = buildMarkovAutomaton<ValueType>(builder.build(), std::move(markovianStates), {{"goal", {3}}});
561 ASSERT_FALSE(model->isClosed());
562
563 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, strongOptions()).quotient;
564 EXPECT_EQ(3ull, quotient->getNumberOfStates()); // {0, 1}, {2} and {3}
565 EXPECT_EQ(4ull, quotient->getNumberOfChoices()); // The merged hybrid state keeps both of its choices.
566 auto const markovAutomaton = quotient->template as<storm::models::sparse::MarkovAutomaton<ValueType>>();
567 ASSERT_FALSE(markovAutomaton->isClosed());
568 auto const hybridState = *quotient->getInitialStates().begin();
569 EXPECT_TRUE(markovAutomaton->isMarkovianState(hybridState));
570 EXPECT_EQ(2ull, quotient->getTransitionMatrix().getRowGroupSize(hybridState));
571 EXPECT_EQ(2.0, markovAutomaton->getExitRate(hybridState));
572
573 // Closing both models applies the maximal progress assumption, i.e. it keeps the probabilistic choice of a hybrid state. This also checks that the
574 // Markovian choice is the first choice of the quotient state, since that is the one close() removes.
575 model->close();
576 markovAutomaton->close();
577 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Tmin=? [F \"goal\"]"), 0.25, 1e-9);
578 EXPECT_NEAR(checkFormula<ValueType>(model, "Tmin=? [F \"goal\"]"), 0.25, 1e-9);
579}
580
581// ------------------------------------------------------------
582// Benchmark models
583// ------------------------------------------------------------
584
585TEST(StrongBisimulationTest, Die) {
586 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "P=? [F \"one\"]", 13ull, 5ull, 8ull, 5ull);
587}
588
589TEST(StrongBisimulationTest, DieAllLabels) {
590 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "P=? [F \"one\"]", 13ull, 11ull, 17ull, 11ull, allLabelOptions());
591}
592
596TEST(StrongBisimulationTest, DieUnnamedRewardModel) {
597 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "R=? [F \"done\"]", 13ull, 5ull, 7ull, 5ull);
598}
599
603TEST(StrongBisimulationTest, DiePropositionalSubformulas) {
604 std::string const formulaString = "P=? [F (s=7 & d=1) | \"two\"]";
605 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", formulaString, 13ull, 6ull, 10ull, 6ull);
606 Options options = strongOptions();
607 options.stateLabelPreservation = StateLabelPreservation::FormulaIndividual;
608 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", formulaString, 13ull, 7ull, 11ull, 7ull, options);
609}
610
614TEST(StrongBisimulationTest, DieStateValuations) {
615#ifndef STORM_HAVE_Z3
616 GTEST_SKIP() << "Z3 not available.";
617#endif
618 auto const input = buildFromPrism<ValueType>(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "P=? [F \"one\"]", {.stateValuations = true});
619 ASSERT_TRUE(input.model->hasStateValuations());
620
621 auto const result = storm::bisimulation::performBisimulationMinimization<ValueType>(*input.model, input.formulas, strongOptions());
622 ASSERT_TRUE(result.quotient->hasStateValuations());
623 ASSERT_EQ(result.quotient->getNumberOfStates(), result.quotient->getStateValuations().getNumberOfEntities());
624 // Every quotient state carries the valuation of one of the states that it represents.
625 std::set<uint64_t> quotientStatesWithRepresentedValuation;
626 for (uint64_t state = 0; state < input.model->getNumberOfStates(); ++state) {
627 uint64_t const quotientState = result.toQuotientStateMapping[state];
628 if (input.model->getStateValuations().toString(state) == result.quotient->getStateValuations().toString(quotientState)) {
629 quotientStatesWithRepresentedValuation.insert(quotientState);
630 }
631 }
632 EXPECT_EQ(result.quotient->getNumberOfStates(), quotientStatesWithRepresentedValuation.size());
633}
634
635TEST(StrongBisimulationTest, Crowds) {
636 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/crowds5_5.pm", "P=? [F \"observe0Greater1\"]", 7403ull, 65ull, 105ull, 65ull);
637}
638
639TEST(StrongBisimulationTest, CrowdsAllLabels) {
640 // The model is larger than above because the target states of the formula are no longer made absorbing.
641 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/crowds5_5.pm", "P=? [F \"observe0Greater1\"]", 8607ull, 2149ull, 3912ull, 2149ull, allLabelOptions());
642}
643
644TEST(StrongBisimulationTest, CtmcEmbedded) {
645 testQuotient(STORM_TEST_RESOURCES_DIR "/ctmc/embedded2.sm", "P=? [F<=10000 \"down\"]", 2076ull, 310ull, 1752ull, 310ull, approximateOptions());
646}
647
648TEST(StrongBisimulationTest, CtmcCluster) {
649 testQuotient(STORM_TEST_RESOURCES_DIR "/ctmc/cluster2.sm", "P=? [F<=100 !\"minimum\"]", 276ull, 147ull, 569ull, 147ull);
650}
651
652TEST(StrongBisimulationTest, CtmcClusterChoiceLabelsAndOrigins) {
653 // The actions of the left and the right cluster have different names and stem from different modules, which rules out any reduction.
654 testQuotient(STORM_TEST_RESOURCES_DIR "/ctmc/cluster2.sm", "P=? [F<=100 !\"minimum\"]", 276ull, 276ull, 1120ull, 276ull, strongOptions(),
655 {.choiceLabels = true, .choiceOrigins = true});
656}
657
658TEST(StrongBisimulationTest, DtmcBrpChoiceLabelsAndOrigins) {
659 testQuotient(STORM_TEST_RESOURCES_DIR "/dtmc/brp-16-2.pm", "P=? [F \"target\"]", 613ull, 412ull, 572ull, 412ull, strongOptions(),
660 {.choiceLabels = true, .choiceOrigins = true});
661}
662
668TEST(StrongBisimulationTest, CtmcEmbeddedExact) {
669#ifndef STORM_HAVE_Z3
670 GTEST_SKIP() << "Z3 not available.";
671#endif
672 std::string const formulaString = "P=? [F<=10000 \"down\"]";
673 auto const exactInput = buildFromPrism<storm::RationalNumber>(STORM_TEST_RESOURCES_DIR "/ctmc/embedded2.sm", formulaString, {.allLabels = true});
674 ASSERT_EQ(3478ull, exactInput.model->getNumberOfStates());
675 ASSERT_EQ(14639ull, exactInput.model->getNumberOfTransitions());
676
677 Options options = strongOptions();
678 options.stateLabelPreservation = StateLabelPreservation::All;
679 auto const labeledQuotient =
680 storm::bisimulation::performBisimulationMinimization<storm::RationalNumber>(*exactInput.model, exactInput.formulas, options).quotient;
681 EXPECT_EQ(1127ull, labeledQuotient->getNumberOfStates());
682 EXPECT_EQ(5730ull, labeledQuotient->getNumberOfTransitions());
683
684 // Without the labels the quotient is much coarser. This is the configuration in which the reported sizes differed the most (136 on macOS vs 180 on
685 // Linux), since the initial partition consists of a single block there.
686 options.stateLabelPreservation = StateLabelPreservation::None;
687 auto const unlabeledQuotient = storm::bisimulation::performBisimulationMinimization<storm::RationalNumber>(*exactInput.model, {}, options).quotient;
688 EXPECT_EQ(98ull, unlabeledQuotient->getNumberOfStates());
689 EXPECT_EQ(539ull, unlabeledQuotient->getNumberOfTransitions());
690}
691
696TEST(StrongBisimulationTest, CtmcEmbeddedWithTolerance) {
697#ifndef STORM_HAVE_Z3
698 GTEST_SKIP() << "Z3 not available.";
699#endif
700 std::string const formulaString = "P=? [F<=10000 \"down\"]";
701 auto const input = buildFromPrism<double>(STORM_TEST_RESOURCES_DIR "/ctmc/embedded2.sm", formulaString, {.allLabels = true});
702
703 Options options = approximateOptions();
704 options.stateLabelPreservation = StateLabelPreservation::All;
705 auto const labeledQuotient = storm::bisimulation::performBisimulationMinimization<double>(*input.model, input.formulas, options).quotient;
706 EXPECT_EQ(1127ull, labeledQuotient->getNumberOfStates());
707 EXPECT_EQ(5730ull, labeledQuotient->getNumberOfTransitions());
708
709 options.stateLabelPreservation = StateLabelPreservation::None;
710 auto const unlabeledQuotient = storm::bisimulation::performBisimulationMinimization<double>(*input.model, {}, options).quotient;
711 EXPECT_EQ(98ull, unlabeledQuotient->getNumberOfStates());
712 EXPECT_EQ(539ull, unlabeledQuotient->getNumberOfTransitions());
713}
714
715TEST(StrongBisimulationTest, MdpTwoDice) {
716 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/two_dice.nm", "Pmin=? [F \"two\"]", 169ull, 11ull, 26ull, 14ull);
717}
718
719TEST(StrongBisimulationTest, MdpTwoDiceAllLabels) {
720 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/two_dice.nm", "Pmin=? [F \"two\"]", 169ull, 77ull, 183ull, 97ull, allLabelOptions());
721}
722
723TEST(StrongBisimulationTest, MdpCoin) {
724 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/coin2-2.nm", "Pmin=? [F \"finished\"]", 272ull, 55ull, 96ull, 78ull);
725}
726
731TEST(StrongBisimulationTest, MdpCoinQuotientIsNotMeasureDriven) {
732#ifndef STORM_HAVE_Z3
733 GTEST_SKIP() << "Z3 not available.";
734#endif
735 std::string const formulaString = "Pmax=? [F \"finished\"]";
736 auto const input = buildFromPrism<ValueType>(STORM_TEST_RESOURCES_DIR "/mdp/coin2-2.nm", formulaString);
737 ASSERT_NEAR(1.0, checkFormula<ValueType>(input.model, formulaString), 1e-9);
738
739 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*input.model, input.formulas, strongOptions()).quotient;
740 EXPECT_EQ(55ull, quotient->getNumberOfStates());
741 EXPECT_NEAR(checkFormula<ValueType>(quotient, formulaString), 1.0, 1e-9);
742 // The quotient preserves all of PCTL, in particular the step-bounded variant of the formula it was built for.
743 EXPECT_NEAR(checkFormula<ValueType>(quotient, "Pmax=? [F<=10 \"finished\"]"), checkFormula<ValueType>(input.model, "Pmax=? [F<=10 \"finished\"]"), 1e-9);
744}
745
746TEST(StrongBisimulationTest, MdpTwoDiceActionSensitive) {
747 Options options = strongOptions();
748 options.actionSensitive = true;
749 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/two_dice.nm", "Pmin=? [F \"two\"]", 169ull, 33ull, 97ull, 58ull, options);
750}
751
752TEST(StrongBisimulationTest, MdpCoinChoiceOrigins) {
753 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/coin2-2.nm", "Pmin=? [F \"finished\"]", 272ull, 195ull, 399ull, 321ull, strongOptions(),
754 {.choiceOrigins = true});
755}
756
757TEST(StrongBisimulationTest, MdpLeaderChoiceLabels) {
758 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/leader3.nm", "Pmin=? [F \"elected\"]", 364ull, 169ull, 325ull, 262ull, strongOptions(), {.choiceLabels = true});
759}
760
761TEST(StrongBisimulationTest, MdpLeaderChoiceLabelsAndOriginsActionSensitive) {
762 Options options = strongOptions();
763 options.actionSensitive = true;
764 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/leader3.nm", "Pmin=? [F \"elected\"]", 364ull, 169ull, 325ull, 262ull, options,
765 {.choiceLabels = true, .choiceOrigins = true});
766}
767
771TEST(StrongBisimulationTest, MdpTwoDiceApproximative) {
772 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/two_dice.nm", "Pmin=? [F \"two\"]", 169ull, 11ull, 26ull, 14ull, approximateOptions());
773}
774
775TEST(StrongBisimulationTest, MdpCoinApproximative) {
776 testQuotient(STORM_TEST_RESOURCES_DIR "/mdp/coin2-2.nm", "Pmin=? [F \"finished\"]", 272ull, 55ull, 96ull, 78ull, approximateOptions());
777}
778
779// ------------------------------------------------------------
780// Consistency between the refinement strategies and the value types
781// ------------------------------------------------------------
782
786template<typename VT>
787void testRefinementStrategiesAgree(std::string const& prismFile, std::string const& formulaString) {
788#ifndef STORM_HAVE_Z3
789 GTEST_SKIP() << "Z3 not available.";
790#endif
791 auto const input = buildFromPrism<VT>(prismFile, formulaString);
792
793 auto splitterOptions = strongOptions();
794 splitterOptions.preferSignatureRefinement = false;
795 auto const splitterQuotient = storm::bisimulation::performBisimulationMinimization<VT>(*input.model, input.formulas, splitterOptions).quotient;
796
797 auto signatureOptions = splitterOptions;
798 signatureOptions.preferSignatureRefinement = true;
799 auto const signatureQuotient = storm::bisimulation::performBisimulationMinimization<VT>(*input.model, input.formulas, signatureOptions).quotient;
800
801 EXPECT_EQ(splitterQuotient->getNumberOfStates(), signatureQuotient->getNumberOfStates());
802 EXPECT_EQ(splitterQuotient->getNumberOfTransitions(), signatureQuotient->getNumberOfTransitions());
803}
804
805TEST(StrongBisimulationTest, RefinementStrategiesAgreeOnDie) {
806 testRefinementStrategiesAgree<ValueType>(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "P=? [F \"one\"]");
807}
808
809TEST(StrongBisimulationTest, RefinementStrategiesAgreeOnCrowds) {
810 testRefinementStrategiesAgree<ValueType>(STORM_TEST_RESOURCES_DIR "/dtmc/crowds5_5.pm", "P=? [F \"observe0Greater1\"]");
811}
812
817TEST(StrongBisimulationTest, RefinementStrategiesAgreeOnCtmc) {
818 testRefinementStrategiesAgree<storm::RationalNumber>(STORM_TEST_RESOURCES_DIR "/ctmc/embedded2.sm", "P=? [F<=10000 \"down\"]");
819}
820
824void testExactAgreesWithApproximate(std::string const& prismFile, std::string const& formulaString) {
825#ifndef STORM_HAVE_Z3
826 GTEST_SKIP() << "Z3 not available.";
827#endif
828 auto const doubleInput = buildFromPrism<double>(prismFile, formulaString);
829 auto const exactInput = buildFromPrism<storm::RationalNumber>(prismFile, formulaString);
830
831 auto const doubleQuotient =
832 storm::bisimulation::performBisimulationMinimization<double>(*doubleInput.model, doubleInput.formulas, strongOptions()).quotient;
833 auto const exactQuotient =
834 storm::bisimulation::performBisimulationMinimization<storm::RationalNumber>(*exactInput.model, exactInput.formulas, strongOptions()).quotient;
835
836 EXPECT_EQ(doubleQuotient->getNumberOfStates(), exactQuotient->getNumberOfStates());
837 EXPECT_EQ(doubleQuotient->getNumberOfTransitions(), exactQuotient->getNumberOfTransitions());
838}
839
840TEST(StrongBisimulationTest, ExactAgreesWithApproximateOnDie) {
841 testExactAgreesWithApproximate(STORM_TEST_RESOURCES_DIR "/dtmc/die.pm", "P=? [F \"one\"]");
842}
843
844TEST(StrongBisimulationTest, ExactAgreesWithApproximateOnTwoDice) {
845 testExactAgreesWithApproximate(STORM_TEST_RESOURCES_DIR "/mdp/two_dice.nm", "Pmin=? [F \"two\"]");
846}
847
853template<typename VT>
854void testPermutationInvariance(std::string const& prismFile, std::string const& formulaString, uint64_t const numSeeds, Options options = strongOptions()) {
855#ifndef STORM_HAVE_Z3
856 GTEST_SKIP() << "Z3 not available.";
857#endif
858 options.stateLabelPreservation = StateLabelPreservation::All;
859 auto const input = buildFromPrism<VT>(prismFile, formulaString, {.allLabels = true});
860 auto const quotient = storm::bisimulation::performBisimulationMinimization<VT>(*input.model, input.formulas, options).quotient;
861
862 std::vector<uint64_t> permutation(input.model->getNumberOfStates());
863 std::iota(permutation.begin(), permutation.end(), 0ull);
864 for (uint64_t seed = 0; seed < numSeeds; ++seed) {
865 std::mt19937_64 rng(seed);
866 std::shuffle(permutation.begin(), permutation.end(), rng);
867 auto const permutedModel = storm::transformer::permuteStates(*input.model, permutation);
868 auto const permutedQuotient = storm::bisimulation::performBisimulationMinimization<VT>(*permutedModel, input.formulas, options).quotient;
869 EXPECT_EQ(quotient->getNumberOfStates(), permutedQuotient->getNumberOfStates()) << "Seed " << seed << ".";
870 EXPECT_EQ(quotient->getNumberOfTransitions(), permutedQuotient->getNumberOfTransitions()) << "Seed " << seed << ".";
871 }
872}
873
874TEST(StrongBisimulationTest, PermutationInvarianceExactCtmc) {
875 testPermutationInvariance<storm::RationalNumber>(STORM_TEST_RESOURCES_DIR "/ctmc/embedded2.sm", "P=? [F<=10000 \"down\"]", 3ull);
876}
877
878TEST(StrongBisimulationTest, PermutationInvarianceCtmc) {
879 testPermutationInvariance<double>(STORM_TEST_RESOURCES_DIR "/ctmc/embedded2.sm", "P=? [F<=10000 \"down\"]", 3ull, approximateOptions());
880}
881
882TEST(StrongBisimulationTest, PermutationInvarianceMdp) {
883 testPermutationInvariance<double>(STORM_TEST_RESOURCES_DIR "/mdp/two_dice.nm", "Pmin=? [F \"two\"]", 3ull, approximateOptions());
884}
885
886// ------------------------------------------------------------
887// Randomized self-checks
888// ------------------------------------------------------------
889
894template<typename ModelType>
895std::shared_ptr<ModelType> buildRandomModel(uint64_t const numStates, uint64_t const seed) {
896 bool constexpr isNondeterministic = std::is_same_v<ModelType, storm::models::sparse::Mdp<ValueType>>;
897 std::mt19937_64 rng(seed);
898 storm::storage::SparseMatrixBuilder<ValueType> builder(0, numStates, 0, false, isNondeterministic, isNondeterministic ? numStates : 0);
899 uint64_t row = 0;
900 for (uint64_t state = 0; state < numStates; ++state) {
901 if constexpr (isNondeterministic) {
902 builder.newRowGroup(row);
903 }
904 uint64_t const numChoices = isNondeterministic ? 1 + rng() % 2 : 1;
905 for (uint64_t choice = 0; choice < numChoices; ++choice, ++row) {
906 std::map<uint64_t, ValueType> distribution;
907 uint64_t const numSuccessors = 1 + rng() % 3;
908 for (uint64_t i = 0; i < numSuccessors; ++i) {
909 // Small values keep the rows short and make coinciding distributions (and thus actual merges) reasonably likely.
910 distribution[rng() % numStates] += static_cast<ValueType>(1 + rng() % 4);
911 }
913 for (auto const& [_, value] : distribution) {
914 sum += value;
915 }
916 for (auto const& [column, value] : distribution) {
917 builder.addNextValue(row, column, value / sum);
918 }
919 }
920 }
921 std::map<std::string, std::vector<uint64_t>> labels{{"a", {}}, {"b", {}}};
922 for (uint64_t state = 0; state < numStates; ++state) {
923 if (rng() % 3 == 0) {
924 labels["a"].push_back(state);
925 }
926 if (rng() % 4 == 0) {
927 labels["b"].push_back(state);
928 }
929 }
930 return buildModel<ModelType>(builder.build(row, numStates, numStates), labels);
931}
932
940template<typename ModelType>
941void testIdempotence(uint64_t const numStates, uint64_t const numSeeds, std::string const& formulaString) {
942 for (uint64_t seed = 0; seed < numSeeds; ++seed) {
943 auto const model = buildRandomModel<ModelType>(numStates, seed);
944 Options const options = strongOptions();
945 auto const quotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, options).quotient;
946 ASSERT_LE(quotient->getNumberOfStates(), model->getNumberOfStates()) << "Seed " << seed << ".";
947 auto const quotientOfQuotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*quotient, {}, options).quotient;
948 EXPECT_EQ(quotient->getNumberOfStates(), quotientOfQuotient->getNumberOfStates()) << "Not idempotent for seed " << seed << ".";
949 EXPECT_EQ(quotient->getNumberOfTransitions(), quotientOfQuotient->getNumberOfTransitions()) << "Not idempotent for seed " << seed << ".";
950 // The comparison is against the default precision of the underlying equation solver, not against the exactness of the quotient.
951 EXPECT_NEAR(checkFormula<ValueType>(quotient, formulaString), checkFormula<ValueType>(model, formulaString), 1e-4) << "Seed " << seed << ".";
952 }
953}
954
955TEST(StrongBisimulationTest, RandomDtmcs) {
956 testIdempotence<storm::models::sparse::Dtmc<ValueType>>(12, 500, "P=? [F \"a\"]");
957 testIdempotence<storm::models::sparse::Dtmc<ValueType>>(60, 200, "P=? [(!\"b\") U \"a\"]");
958}
959
960TEST(StrongBisimulationTest, RandomCtmcs) {
961 testIdempotence<storm::models::sparse::Ctmc<ValueType>>(12, 500, "P=? [F \"a\"]");
962 testIdempotence<storm::models::sparse::Ctmc<ValueType>>(60, 200, "P=? [(!\"b\") U \"a\"]");
963}
964
965TEST(StrongBisimulationTest, RandomMdps) {
966 testIdempotence<storm::models::sparse::Mdp<ValueType>>(12, 500, "Pmin=? [F \"a\"]");
967 testIdempotence<storm::models::sparse::Mdp<ValueType>>(60, 200, "Pmax=? [(!\"b\") U \"a\"]");
968}
969
973template<typename ModelType>
974void testRefinementStrategiesAgreeOnRandomModels(uint64_t const numStates, uint64_t const numSeeds) {
975 for (uint64_t seed = 0; seed < numSeeds; ++seed) {
976 auto const model = buildRandomModel<ModelType>(numStates, seed);
977 auto splitterOptions = strongOptions();
978 splitterOptions.preferSignatureRefinement = false;
979 auto const splitterQuotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, splitterOptions).quotient;
980 auto signatureOptions = splitterOptions;
981 signatureOptions.preferSignatureRefinement = true;
982 auto const signatureQuotient = storm::bisimulation::performBisimulationMinimization<ValueType>(*model, {}, signatureOptions).quotient;
983 EXPECT_EQ(splitterQuotient->getNumberOfStates(), signatureQuotient->getNumberOfStates()) << "Seed " << seed << ".";
984 EXPECT_EQ(splitterQuotient->getNumberOfTransitions(), signatureQuotient->getNumberOfTransitions()) << "Seed " << seed << ".";
985 }
986}
987
988TEST(StrongBisimulationTest, RefinementStrategiesAgreeOnRandomDtmcs) {
989 testRefinementStrategiesAgreeOnRandomModels<storm::models::sparse::Dtmc<ValueType>>(12, 500);
990 testRefinementStrategiesAgreeOnRandomModels<storm::models::sparse::Dtmc<ValueType>>(60, 200);
991}
992
993TEST(StrongBisimulationTest, RefinementStrategiesAgreeOnRandomCtmcs) {
994 testRefinementStrategiesAgreeOnRandomModels<storm::models::sparse::Ctmc<ValueType>>(12, 500);
995 testRefinementStrategiesAgreeOnRandomModels<storm::models::sparse::Ctmc<ValueType>>(60, 200);
996}
997
998} // namespace
TEST(RegionConstraintCheckerTest, AffineConstraintHoldsThroughout)
static constexpr double DefaultModelTolerance
The default tolerance up to which probabilities and rates are considered equal.
Definition Environment.h:42
virtual ModelType getType() const
Return the actual type of the model.
Definition ModelBase.cpp:7
std::set< std::string > getLabels() const
Retrieves the set of labels contained in this labeling.
Base class for all sparse models.
Definition Model.h:34
storm::models::sparse::ChoiceLabeling const & getChoiceLabeling() const
Retrieves the labels for the choices of the model.
Definition Model.cpp:363
storm::storage::SparseMatrix< ValueType > const & getTransitionMatrix() const
Retrieves the matrix representing the transitions of the model.
Definition Model.cpp:226
virtual uint_fast64_t getNumberOfChoices() const override
Returns the number of choices ine the model.
Definition Model.cpp:201
ValueType const & getStochasticTolerance() const
Retrieves the tolerance up to which the model components are considered valid.
Definition Model.cpp:713
bool hasChoiceLabeling() const
Retrieves whether this model has a labeling of the choices.
Definition Model.cpp:358
storm::models::sparse::StateLabeling const & getStateLabeling() const
Returns the state labeling associated with this model.
Definition Model.cpp:348
virtual uint_fast64_t getNumberOfStates() const override
Returns the number of states of the model.
Definition Model.cpp:191
void addLabelToState(std::string const &label, storm::storage::sparse::state_type state)
Adds a label to a given state.
void removeLabelFromState(std::string const &label, storm::storage::sparse::state_type state)
Removes a label from a given state.
std::shared_ptr< storm::logic::Formula const > parseSingleFormulaFromString(std::string const &formulaString) const
Parses the formula given by the provided string.
A bit vector that is internally represented as a vector of 64-bit values.
Definition BitVector.h:16
A class that can be used to build a sparse matrix by adding value by value.
std::shared_ptr< storm::models::sparse::Model< ValueType > > buildExplicitDRNModel(std::string const &drnFile, storm::parser::DirectEncodingParserOptions const &options=storm::parser::DirectEncodingParserOptions())
ReturnType< ValueType > performBisimulationMinimization(storm::models::sparse::Model< ValueType > const &model, std::vector< std::shared_ptr< storm::logic::Formula const > > const &formulas, Options options)
Computes the bisimulation quotient of the given model.
SFTBDDChecker::ValueType ValueType
Expression sum(std::vector< storm::expressions::Expression > const &expressions)
storm::bisimulation::StateLabelPreservation StateLabelPreservation
ValueType checkFormula(std::shared_ptr< storm::models::sparse::Model< ValueType > > const &model, std::shared_ptr< storm::logic::Formula const > const &formula)
storm::bisimulation::Options Options
std::shared_ptr< storm::models::sparse::MarkovAutomaton< ValueType > > buildMarkovAutomaton(storm::storage::SparseMatrix< ValueType > matrix, storm::storage::BitVector markovianStates, std::map< std::string, std::vector< uint64_t > > const &labels)
Builds a Markov automaton.
std::shared_ptr< ModelType > buildModel(storm::storage::SparseMatrix< ValueType > matrix, std::map< std::string, std::vector< uint64_t > > const &labels, std::vector< ValueType > const &stateRewards={}, std::vector< ValueType > const &stateActionRewards={})
Builds a deterministic model with the given (probability or rate) matrix.
PrismInput< ValueType > buildFromPrism(std::string const &prismFile, std::string const &formulaString, BuildOptions const &buildOptions={})
std::shared_ptr< storm::models::sparse::Model< ValueType > > permuteStates(storm::models::sparse::Model< ValueType > const &originalModel, std::vector< uint64_t > const &permutation)
Applies the given permutation to the states of the given model.
ValueType zero()
Definition constants.cpp:26
ValueType one()
Definition constants.cpp:21
TargetType convertNumber(SourceType const &number)
#define STORM_SILENT_EXPECT_THROW(statement, expected_exception)
Definition storm_gtest.h:21
std::optional< std::vector< uint64_t > > toQuotientChoiceMapping
std::shared_ptr< storm::models::sparse::Model< ValueType > > quotient
std::vector< uint64_t > toQuotientStateMapping
What to build from a PRISM program in addition to what the formulas require.