Storm 1.14.0.1
A Modern Probabilistic Model Checker
Loading...
Searching...
No Matches
PartitionTest.cpp
Go to the documentation of this file.
1#include <sstream>
2#include <vector>
4
6#include "test/storm_gtest.h"
7
8namespace {
9
10std::string blockToString(auto const& block) {
11 std::ostringstream oss;
12 oss << "{";
13 for (bool first{true}; auto const e : block) {
14 if (first) {
15 first = false;
16 } else {
17 oss << ", ";
18 }
19 oss << e;
20 }
21 oss << "}";
22 return oss.str();
23}
24
25bool equalBlocks(storm::bisimulation::Partition const& partition, std::initializer_list<uint64_t> const& expected,
27 bool const expectedContainsActual =
28 std::all_of(actual.begin(), actual.end(), [&expected](auto const& e) { return std::find(expected.begin(), expected.end(), e) != expected.end(); });
29 EXPECT_TRUE(expectedContainsActual) << "Content mismatch: " << blockToString(expected) << " vs. " << blockToString(actual);
30 bool const actualContainsExpected =
31 std::all_of(expected.begin(), expected.end(), [&actual](auto const& e) { return std::find(actual.begin(), actual.end(), e) != actual.end(); });
32 EXPECT_TRUE(actualContainsExpected) << "Content mismatch: " << blockToString(expected) << " vs. " << blockToString(actual);
33
34 // Also test the contains method
35 for (auto const& e : expected) {
36 EXPECT_TRUE(partition.contains(actual, e)) << "Element " << e << " not in block " << blockToString(actual);
37 }
38
39 return expectedContainsActual && actualContainsExpected;
40}
41
42TEST(PartitionTest, EmptyPartition) {
43 STORM_SILENT_EXPECT_THROW(storm::bisimulation::Partition(0), storm::exceptions::InvalidArgumentException);
44}
45
46TEST(PartitionTest, Basic) {
48
49 // Check initial partition
50 EXPECT_EQ(10ul, partition.getNumberOfElements());
51 EXPECT_EQ(1ul, partition.getNumberOfBlocks());
52 for (auto i = 0ul; i < 10; ++i) {
53 auto block = partition.getBlockOfElement(i);
54 ASSERT_TRUE(partition.checkBlockValidity(block)) << "Block " << blockToString(block) << " is not valid.";
55 EXPECT_TRUE(equalBlocks(partition, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, block));
56 }
57 auto initBlock = partition.getBlockOfElement(0);
58 ASSERT_FALSE(partition.isProperSuperBlock(initBlock));
59
60 // Split the partition into two blocks (odd and even)
61 partition.forEachBlock([&partition](auto const& block) {
62 auto [odd, even] = partition.splitBlockByPredicate(block, [](auto const& e) { return e % 2 == 0; });
63 ASSERT_TRUE(partition.checkBlockValidity(odd)) << "Block " << blockToString(odd) << " is not valid.";
64 ASSERT_TRUE(partition.checkBlockValidity(even)) << "Block " << blockToString(even) << " is not valid.";
65 EXPECT_TRUE(equalBlocks(partition, {1, 3, 5, 7, 9}, odd));
66 EXPECT_TRUE(equalBlocks(partition, {0, 2, 4, 6, 8}, even));
67 });
68 ASSERT_TRUE(partition.checkBlockValidity(initBlock)) << "Block " << blockToString(initBlock) << " is not valid.";
69 EXPECT_EQ(2ul, partition.getNumberOfBlocks());
70 ASSERT_TRUE(partition.isProperSuperBlock(initBlock));
71 auto oddBlock = partition.getBlockOfElement(1);
72 auto evenBlock = partition.getBlockOfElement(0);
73
74 // Split the odd block w.r.t. their remainder when divided by 3
75 EXPECT_TRUE(partition.splitBlockByOrder(oddBlock, [](auto const& a, auto const& b) { return a % 3 < b % 3; }));
76 uint64_t seenSubBlocks = 0; // very "smart" (aka lazy) encoding of the seen subsets
77 partition.forEachSubBlock(oddBlock, [&partition, &seenSubBlocks](auto const& block) {
78 ASSERT_TRUE(partition.checkBlockValidity(block)) << "Block " << blockToString(block) << " is not valid.";
79 if (partition.contains(block, 1)) {
80 EXPECT_TRUE(equalBlocks(partition, {1, 7}, block));
81 seenSubBlocks += 1;
82 } else if (partition.contains(block, 3)) {
83 EXPECT_TRUE(equalBlocks(partition, {3, 9}, block));
84 seenSubBlocks += 10;
85 } else if (partition.contains(block, 5)) {
86 EXPECT_TRUE(equalBlocks(partition, {5}, block));
87 seenSubBlocks += 100;
88 } else {
89 FAIL() << "Block " << blockToString(block) << " does not contain any expected elements.";
90 }
91 });
92 EXPECT_EQ(111ul, seenSubBlocks) << "Not all sub blocks have been seen.";
93 EXPECT_EQ(4ul, partition.getNumberOfBlocks());
94
95 // Check if the contains method works as expected
96 for (auto i = 0ul; i < 10; ++i) {
97 EXPECT_EQ(partition.contains(evenBlock, i), i % 2 == 0) << "Element " << i << " not in block " << blockToString(i % 2 == 0 ? evenBlock : oddBlock);
98 EXPECT_EQ(partition.contains(oddBlock, i), i % 2 != 0) << "Element " << i << " in block " << blockToString(i % 2 != 0 ? oddBlock : evenBlock);
99 }
100
101 EXPECT_TRUE(partition.isProperSuperBlock(oddBlock));
102 EXPECT_TRUE(equalBlocks(partition, {1, 3, 5, 7, 9}, oddBlock));
103
104 // Perform splits that have no effect, i.e., do not change the number of blocks
105 auto [evenAndOddBlock, evenAndEvenBlock] = partition.splitBlockByPredicate(evenBlock, [](auto const& e) { return e % 2 == 0; });
106 EXPECT_TRUE(evenAndOddBlock.empty());
107 EXPECT_TRUE(equalBlocks(partition, {0, 2, 4, 6, 8}, evenAndEvenBlock));
108 EXPECT_FALSE(partition.isProperSuperBlock(evenBlock));
109
110 auto [less10, geq10] = partition.splitBlockByPredicate(evenBlock, [](auto const& e) { return e >= 10; });
111 EXPECT_TRUE(geq10.empty());
112 EXPECT_TRUE(equalBlocks(partition, {0, 2, 4, 6, 8}, less10));
113 EXPECT_FALSE(partition.isProperSuperBlock(evenBlock));
114
115 EXPECT_FALSE(partition.splitBlockByOrder(evenBlock, [](auto const& a, auto const& b) { return a % 2 < b % 2; }));
116 EXPECT_FALSE(partition.isProperSuperBlock(evenBlock));
117
119 blockSet.insert(evenBlock);
120 blockSet.insert(oddBlock);
121 EXPECT_EQ(blockSet.size(), 2);
122 blockSet.insert(evenAndEvenBlock); // (re-)inserts the even block
123 EXPECT_EQ(blockSet.size(), 2);
124 blockSet.insert(partition.getBlockOfElement(0)); // (re-)inserts the even block
125 EXPECT_EQ(blockSet.size(), 2);
126 EXPECT_EQ(blockSet.size(), 2);
127 blockSet.insert(partition.getBlockOfElement(1)); // inserts a sub-block of oddBlock
128 EXPECT_EQ(blockSet.size(), 3);
129};
130
131TEST(PartitionTest, SplitByRange) {
132 storm::bisimulation::Partition partition(10);
133
134 // Split into "not in range" and "in range" parts
135 auto [no, yes] = partition.splitBlockByRange(partition.getUniversalBlock(), std::vector<uint64_t>{1, 3, 5, 7, 9});
136 ASSERT_TRUE(partition.checkBlockValidity(no)) << "Block " << blockToString(no) << " is not valid.";
137 ASSERT_TRUE(partition.checkBlockValidity(yes)) << "Block " << blockToString(yes) << " is not valid.";
138 EXPECT_TRUE(equalBlocks(partition, {0, 2, 4, 6, 8}, no));
139 EXPECT_TRUE(equalBlocks(partition, {1, 3, 5, 7, 9}, yes));
140 EXPECT_EQ(2ul, partition.getNumberOfBlocks());
141
142 // Duplicates in the range as well as elements that are not part of the block being split (but are valid elements of the partition) must be handled
143 // gracefully
144 auto [notInRange, inRange] = partition.splitBlockByRange(no, std::vector<uint64_t>{0, 4, 4, 8, 1, 3}); // 1 and 3 do not belong to `no`
145 ASSERT_TRUE(partition.checkBlockValidity(notInRange)) << "Block " << blockToString(notInRange) << " is not valid.";
146 ASSERT_TRUE(partition.checkBlockValidity(inRange)) << "Block " << blockToString(inRange) << " is not valid.";
147 EXPECT_TRUE(equalBlocks(partition, {2, 6}, notInRange));
148 EXPECT_TRUE(equalBlocks(partition, {0, 4, 8}, inRange));
149 EXPECT_EQ(3ul, partition.getNumberOfBlocks());
150
151 // Splitting with an empty range has no effect
152 auto [allOfYes, emptyBlock] = partition.splitBlockByRange(yes, std::vector<uint64_t>{});
153 EXPECT_TRUE(emptyBlock.empty());
154 EXPECT_TRUE(equalBlocks(partition, {1, 3, 5, 7, 9}, allOfYes));
155 EXPECT_FALSE(partition.isProperSuperBlock(yes));
156
157 // Splitting with a range that contains all elements of the block has no effect
158 auto [emptyBlock2, allOfYes2] = partition.splitBlockByRange(yes, std::vector<uint64_t>{1, 3, 5, 7, 9});
159 EXPECT_TRUE(emptyBlock2.empty());
160 EXPECT_TRUE(equalBlocks(partition, {1, 3, 5, 7, 9}, allOfYes2));
161 EXPECT_FALSE(partition.isProperSuperBlock(yes));
162 EXPECT_EQ(3ul, partition.getNumberOfBlocks());
163}
164
165TEST(PartitionTest, SplitByClustering) {
166 // Values are deliberately interleaved so that same-valued elements are not adjacent; splitBlockByClustering must not rely on any pre-existing order.
167 std::vector<int> const value = {0, 5, 0, 5, 0, 9};
168 storm::bisimulation::Partition partition(value.size());
169 auto const condition = [&value](uint64_t const a, uint64_t const b) { return value[a] != value[b]; };
170
171 EXPECT_TRUE(partition.splitBlockByClustering(partition.getUniversalBlock(), condition));
172 EXPECT_EQ(3ul, partition.getNumberOfBlocks());
173 partition.forEachBlock([&partition](auto const& block) {
174 ASSERT_TRUE(partition.checkBlockValidity(block)) << "Block " << blockToString(block) << " is not valid.";
175 if (partition.contains(block, 0)) {
176 EXPECT_TRUE(equalBlocks(partition, {0, 2, 4}, block));
177 } else if (partition.contains(block, 1)) {
178 EXPECT_TRUE(equalBlocks(partition, {1, 3}, block));
179 } else if (partition.contains(block, 5)) {
180 EXPECT_TRUE(equalBlocks(partition, {5}, block));
181 } else {
182 FAIL() << "Block " << blockToString(block) << " does not contain any expected elements.";
183 }
184 });
185
186 // A condition that never holds must not split anything.
187 {
188 storm::bisimulation::Partition trivialPartition(4);
189 EXPECT_FALSE(trivialPartition.splitBlockByClustering(trivialPartition.getUniversalBlock(), [](uint64_t const, uint64_t const) { return false; }));
190 EXPECT_EQ(1ul, trivialPartition.getNumberOfBlocks());
191 }
192
193 // Unlike a transitive-closure-based clustering, splitBlockByClustering only compares every element against the (fixed) anchor of its group, not
194 // against each other: with values 0, 4, 8 and a "must split" threshold of 5, element 1 (value 4) is within reach of anchor 0 (|4-0|=4) and gets
195 // grouped with it, even though it would also be within reach of element 2 (value 8, |8-4|=4) - once claimed by the first anchor, it is not
196 // reconsidered, so element 2 (|8-0|=8, exceeds the threshold) ends up alone rather than transitively merged with 0 via 1.
197 {
198 std::vector<int> const chainValue = {0, 4, 8};
199 storm::bisimulation::Partition chainPartition(chainValue.size());
200 auto const chainCondition = [&chainValue](uint64_t const a, uint64_t const b) { return std::abs(chainValue[a] - chainValue[b]) > 5; };
201 EXPECT_TRUE(chainPartition.splitBlockByClustering(chainPartition.getUniversalBlock(), chainCondition));
202 EXPECT_EQ(2ul, chainPartition.getNumberOfBlocks());
203 EXPECT_TRUE(equalBlocks(chainPartition, {0, 1}, chainPartition.getBlockOfElement(0)));
204 EXPECT_TRUE(equalBlocks(chainPartition, {2}, chainPartition.getBlockOfElement(2)));
205 }
206}
207
208TEST(PartitionTest, UniversalBlockAndSubBlockRelation) {
210 auto universal = partition.getUniversalBlock();
211 EXPECT_TRUE(equalBlocks(partition, {0, 1, 2, 3, 4, 5, 6, 7}, universal));
212 EXPECT_TRUE(partition.isSubBlockOf(universal, universal)); // a block is a sub-block of itself
213 EXPECT_TRUE(partition.isBlockOfElement(universal, 3));
214
215 auto [odd, even] = partition.splitBlockByPredicate(universal, [](auto const& e) { return e % 2 == 0; });
216
217 // getUniversalBlock always spans all elements, even though the partition now consists of multiple blocks
218 auto stillUniversal = partition.getUniversalBlock();
219 EXPECT_TRUE(equalBlocks(partition, {0, 1, 2, 3, 4, 5, 6, 7}, stillUniversal));
220 EXPECT_TRUE(partition.isProperSuperBlock(stillUniversal));
221
222 EXPECT_TRUE(partition.isSubBlockOf(odd, stillUniversal));
223 EXPECT_TRUE(partition.isSubBlockOf(even, stillUniversal));
224 EXPECT_FALSE(partition.isSubBlockOf(stillUniversal, odd));
225 EXPECT_FALSE(partition.isSubBlockOf(odd, even));
226
227 // `odd` is the finest currently known block for element 1, but the (now proper super-) universal block is not
228 EXPECT_TRUE(partition.isBlockOfElement(odd, 1));
229 EXPECT_FALSE(partition.isBlockOfElement(stillUniversal, 1));
230 EXPECT_FALSE(partition.isBlockOfElement(even, 1));
231}
232
233TEST(PartitionTest, SingletonBlockSplits) {
234 // A partition consisting of a single element: the sole block always starts at index 0.
235 {
237 auto block = partition.getBlockOfElement(0);
238 EXPECT_FALSE(partition.splitBlockByOrder(block, [](auto const& a, auto const& b) { return a < b; }));
239
240 auto [emptyFalsePart, allTrue] = partition.splitBlockByPredicate(block, [](auto const&) { return true; });
241 EXPECT_TRUE(emptyFalsePart.empty());
242 EXPECT_TRUE(equalBlocks(partition, {0}, allTrue));
243
244 auto [allFalse, emptyTruePart] = partition.splitBlockByPredicate(block, [](auto const&) { return false; });
245 EXPECT_TRUE(emptyTruePart.empty());
246 EXPECT_TRUE(equalBlocks(partition, {0}, allFalse));
247
248 EXPECT_FALSE(partition.isProperSuperBlock(block));
249 ASSERT_TRUE(partition.checkBlockValidity(block));
250 }
251
252 // A singleton block resulting from splitting a larger partition, i.e., one that does not start at index 0
253 {
255 auto [odd, even] = partition.splitBlockByPredicate(partition.getUniversalBlock(), [](auto const& e) { return e % 2 == 0; });
256 auto [rest, isolated] = partition.splitBlockByPredicate(even, [](auto const& e) { return e == 0; });
257 EXPECT_TRUE(equalBlocks(partition, {0}, isolated));
258
259 EXPECT_FALSE(partition.splitBlockByOrder(isolated, [](auto const& a, auto const& b) { return a < b; }));
260
261 auto [notInRange, inRange] = partition.splitBlockByRange(isolated, std::vector<uint64_t>{0});
262 EXPECT_TRUE(notInRange.empty());
263 EXPECT_TRUE(equalBlocks(partition, {0}, inRange));
264
265 auto [notInRange2, inRange2] = partition.splitBlockByRange(isolated, std::vector<uint64_t>{});
266 EXPECT_TRUE(inRange2.empty());
267 EXPECT_TRUE(equalBlocks(partition, {0}, notInRange2));
268
269 ASSERT_TRUE(partition.checkBlockValidity(isolated));
270 }
271}
272
273TEST(PartitionTest, NonSuperBlockSet) {
275 storm::bisimulation::Partition::NonSuperBlockSet nonSuperBlockSet(partition);
276
277 EXPECT_TRUE(nonSuperBlockSet.empty());
278 EXPECT_EQ(0ul, nonSuperBlockSet.size());
279
280 auto universalBlock = partition.getUniversalBlock();
281 nonSuperBlockSet.insert(universalBlock);
282 EXPECT_FALSE(nonSuperBlockSet.empty());
283 EXPECT_EQ(1ul, nonSuperBlockSet.size());
284 EXPECT_TRUE(nonSuperBlockSet.contains(universalBlock));
285
286 // Inserting the same (leaf) block again has no effect
287 nonSuperBlockSet.insert(partition.getBlockOfElement(3));
288 EXPECT_EQ(1ul, nonSuperBlockSet.size());
289
290 auto poppedBlock = nonSuperBlockSet.pop();
291 EXPECT_TRUE(equalBlocks(partition, {0, 1, 2, 3, 4, 5}, poppedBlock));
292 EXPECT_TRUE(nonSuperBlockSet.empty());
293
294 auto [odd, even] = partition.splitBlockByPredicate(poppedBlock, [](auto const& e) { return e % 2 == 0; });
295 nonSuperBlockSet.insert(odd);
296 nonSuperBlockSet.insert(even);
297 EXPECT_EQ(2ul, nonSuperBlockSet.size());
298 EXPECT_TRUE(nonSuperBlockSet.contains(odd));
299 EXPECT_TRUE(nonSuperBlockSet.contains(even));
300
301 // Popping removes an arbitrary (but existing) block from the set; here we don't rely on a particular order
302 auto b0 = nonSuperBlockSet.pop();
303 auto b1 = nonSuperBlockSet.pop();
304 EXPECT_TRUE(nonSuperBlockSet.empty());
305 if (partition.contains(b0, 1)) {
306 EXPECT_TRUE(equalBlocks(partition, {1, 3, 5}, b0));
307 EXPECT_TRUE(equalBlocks(partition, {0, 2, 4}, b1));
308 } else {
309 EXPECT_TRUE(equalBlocks(partition, {0, 2, 4}, b0));
310 EXPECT_TRUE(equalBlocks(partition, {1, 3, 5}, b1));
311 }
312 EXPECT_FALSE(nonSuperBlockSet.contains(odd));
313 EXPECT_FALSE(nonSuperBlockSet.contains(even));
314}
315} // namespace
TEST(RegionConstraintCheckerTest, AffineConstraintHoldsThroughout)
Set of blocks that are no proper superblock.
Definition Partition.h:62
Represents a partition of a set of consecutive indices.
Definition Partition.h:20
bool contains(Block const &block, ElementIndex element) const
Definition Partition.cpp:79
bool splitBlockByClustering(Block const &block, Condition const &condition)
Splits the given block by anchor-based clustering: repeatedly picks the first not-yet-grouped element...
Definition Partition.h:288
void forEachBlock(Func const &f) const
Applies the given function on each block in the partition.
Definition Partition.h:184
std::size_t getNumberOfBlocks() const
Retrieves the number of blocks.
Definition Partition.cpp:61
void forEachSubBlock(Block const &superBlock, Func const &f) const
Applies the given function on each block in the given super block.
Definition Partition.h:196
bool isBlockOfElement(Block const &block, ElementIndex const &element) const
std::size_t getNumberOfElements() const
Retrieves the number of elements.
Definition Partition.cpp:66
bool splitBlockByOrder(Block const &block, SplittingOrder const &less)
Splits the given block according to the given order.
Definition Partition.h:215
std::span< ElementIndex const > Block
A contiguous range of elements that form a (possibly proper super-) block of the partition.
Definition Partition.h:27
bool isSubBlockOf(Block const &subblock, Block const &superblock) const
Definition Partition.cpp:84
Block getBlockOfElement(ElementIndex element) const
Definition Partition.cpp:74
std::pair< Block, Block > splitBlockByRange(Block const &block, SplitRange &&r)
Splits the given block according to the given range of elements.
Definition Partition.h:362
std::set< Block, BlockCompare > OrderedBlockSet
A set of blocks, ordered by their size (smallest first).
Definition Partition.h:48
bool isProperSuperBlock(Block const &block) const
bool checkBlockValidity(Block const &block) const
Checks if the given block is a valid block in the partition.
std::pair< Block, Block > splitBlockByPredicate(Block const &block, SplittingPredicate const &f)
Splits the given block according to the given predicate.
Definition Partition.h:329
#define STORM_SILENT_EXPECT_THROW(statement, expected_exception)
Definition storm_gtest.h:21