Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
tpl_ca_parallel_engine_test.cc
Go to the documentation of this file.
1/*
2 Aleph_w
3
4 Data structures & Algorithms
5 version 2.0.0b
6 https://github.com/lrleon/Aleph-w
7
8 This file is part of Aleph-w library
9
10 Copyright (c) 2002-2026 Leandro Rabindranath Leon
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the "Software"), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all
20 copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 SOFTWARE.
29*/
30
53#include <array>
54#include <cstdint>
55#include <random>
56#include <vector>
57
58#include <gtest/gtest.h>
59
60#include <thread_pool.H>
61
62#include <ca-tiling.H>
63#include <ca-traits.H>
64#include <tpl_ca_concepts.H>
65#include <tpl_ca_bit_storage.H>
66#include <tpl_ca_engine.H>
68#include <tpl_ca_lattice.H>
69#include <tpl_ca_neighborhood.H>
71#include <tpl_ca_rule.H>
72#include <tpl_ca_storage.H>
73
74using namespace Aleph;
75using namespace Aleph::CA;
76
77// ---------------------------------------------------------------------------
78// Tiling primitives.
79// ---------------------------------------------------------------------------
80
82{
83 // Five threads dividing 50 indices: every range gets ten, no remainder.
84 for (std::size_t i = 0; i < 5; ++i)
85 {
86 const auto r = split_range_balanced(50, 5, i);
87 EXPECT_EQ(r.size(), 10u);
88 EXPECT_EQ(r.begin, i * 10u);
89 EXPECT_EQ(r.end, (i + 1) * 10u);
90 }
91}
92
94{
95 // Three threads dividing 10 indices: 4, 3, 3.
96 const auto a = split_range_balanced(10, 3, 0);
97 const auto b = split_range_balanced(10, 3, 1);
98 const auto c = split_range_balanced(10, 3, 2);
99 EXPECT_EQ(a.size(), 4u);
100 EXPECT_EQ(b.size(), 3u);
101 EXPECT_EQ(c.size(), 3u);
102 EXPECT_EQ(a.begin, 0u);
103 EXPECT_EQ(b.begin, a.end);
104 EXPECT_EQ(c.begin, b.end);
105 EXPECT_EQ(c.end, 10u);
106}
107
109{
110 // Zero parts collapses to the full range.
111 EXPECT_EQ(split_range_balanced(7, 0, 0).end, 7u);
112 // Out-of-range index produces an empty trailing range at `n`.
113 const auto out = split_range_balanced(7, 3, 5);
114 EXPECT_TRUE(out.empty());
115 EXPECT_EQ(out.begin, 7u);
116}
117
119{
120 std::array<ca_size_t, 2> ext{17, 9};
121 std::vector<int> hits(ext[0], 0);
122 for (std::size_t p = 0; p < 5; ++p)
123 {
124 const auto r = Row_Partition<2>::slab(ext, 5, p);
125 for (ca_size_t i = r.begin; i < r.end; ++i)
126 ++hits[i];
127 }
128 for (int h : hits)
129 EXPECT_EQ(h, 1);
130}
131
133{
134 std::array<ca_size_t, 2> ext{12, 18};
135 const auto factor = Block_Partition_2D::factor_partitions(ext, 6);
136 EXPECT_EQ(factor[0] * factor[1], 6u);
137
138 std::vector<std::vector<int>> hits(ext[0], std::vector<int>(ext[1], 0));
139 for (std::size_t t = 0; t < 6; ++t)
140 {
141 const auto tile = Block_Partition_2D::tile(ext, factor[0], factor[1], t);
142 for (ca_size_t i = tile.rows.begin; i < tile.rows.end; ++i)
143 for (ca_size_t j = tile.cols.begin; j < tile.cols.end; ++j)
144 ++hits[i][j];
145 }
146 for (const auto &row : hits)
147 for (int h : row)
148 EXPECT_EQ(h, 1);
149}
150
152{
153 // Bit interleaving: morton_2d(0b101, 0b010) == 0b011001.
154 EXPECT_EQ(morton_encode_2d(0b101u, 0b010u), 0b011001u);
155 // Sanity: identical inputs put each bit pair as 11 or 00.
156 EXPECT_EQ(morton_encode_2d(0b1111u, 0b1111u), 0b11111111u);
157 // Pure-x and pure-y inputs occupy alternating bits.
158 EXPECT_EQ(morton_encode_2d(0xFFu, 0u), 0x5555u);
159 EXPECT_EQ(morton_encode_2d(0u, 0xFFu), 0xAAAAu);
160}
161
163{
164 EXPECT_TRUE(should_run_sequential(/*cells=*/0, /*parts=*/8, /*min=*/100));
166 EXPECT_TRUE(should_run_sequential(1000, 1, 100));
167 EXPECT_FALSE(should_run_sequential(1000, 8, 100));
168}
169
170// ---------------------------------------------------------------------------
171// Helpers for the engine equivalence tests.
172// ---------------------------------------------------------------------------
173
174template <typename L>
175static void seed_random(L &lat, std::uint32_t seed, double density = 0.4)
176{
177 std::mt19937 rng(seed);
178 std::bernoulli_distribution flip(density);
179 using coord_t = typename L::coord_type;
180 if constexpr (L::rank == 1)
181 {
182 const ca_size_t n0 = lat.size(0);
183 for (ca_size_t i = 0; i < n0; ++i)
184 lat.set(coord_t{static_cast<ca_index_t>(i)}, flip(rng) ? 1 : 0);
185 }
186 else if constexpr (L::rank == 2)
187 {
188 const ca_size_t n0 = lat.size(0);
189 const ca_size_t n1 = lat.size(1);
190 for (ca_size_t i = 0; i < n0; ++i)
191 for (ca_size_t j = 0; j < n1; ++j)
192 lat.set(coord_t{static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)},
193 flip(rng) ? 1 : 0);
194 }
195 else if constexpr (L::rank == 3)
196 {
197 const ca_size_t n0 = lat.size(0);
198 const ca_size_t n1 = lat.size(1);
199 const ca_size_t n2 = lat.size(2);
200 for (ca_size_t i = 0; i < n0; ++i)
201 for (ca_size_t j = 0; j < n1; ++j)
202 for (ca_size_t k = 0; k < n2; ++k)
203 lat.set(coord_t{static_cast<ca_index_t>(i),
204 static_cast<ca_index_t>(j),
205 static_cast<ca_index_t>(k)},
206 flip(rng) ? 1 : 0);
207 }
208}
209
210template <typename L>
211static bool frames_equal(const L &a, const L &b)
212{
213 if (a.extents() != b.extents())
214 return false;
215 using coord_t = typename L::coord_type;
216 if constexpr (L::rank == 1)
217 {
218 const ca_size_t n0 = a.size(0);
219 for (ca_size_t i = 0; i < n0; ++i)
220 if (a.at(coord_t{static_cast<ca_index_t>(i)})
221 != b.at(coord_t{static_cast<ca_index_t>(i)}))
222 return false;
223 }
224 else if constexpr (L::rank == 2)
225 {
226 const ca_size_t n0 = a.size(0);
227 const ca_size_t n1 = a.size(1);
228 for (ca_size_t i = 0; i < n0; ++i)
229 for (ca_size_t j = 0; j < n1; ++j)
230 {
231 const coord_t c{static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)};
232 if (a.at(c) != b.at(c))
233 return false;
234 }
235 }
236 else if constexpr (L::rank == 3)
237 {
238 const ca_size_t n0 = a.size(0);
239 const ca_size_t n1 = a.size(1);
240 const ca_size_t n2 = a.size(2);
241 for (ca_size_t i = 0; i < n0; ++i)
242 for (ca_size_t j = 0; j < n1; ++j)
243 for (ca_size_t k = 0; k < n2; ++k)
244 {
245 const coord_t c{static_cast<ca_index_t>(i),
246 static_cast<ca_index_t>(j),
247 static_cast<ca_index_t>(k)};
248 if (a.at(c) != b.at(c))
249 return false;
250 }
251 }
252 return true;
253}
254
255// Helper to drive both engines through `steps` iterations and assert
256// the resulting frames are bit-equal.
257template <typename Lattice, typename Rule, typename Neighborhood>
258static void
260 Rule rule,
262 std::size_t steps,
263 std::size_t partitions,
264 std::size_t min_cells = 0)
265{
267
269 cfg.num_partitions = partitions;
270 cfg.min_parallel_cells = min_cells;
272
273 seq.run(steps);
274 par.run(steps);
275
276 EXPECT_EQ(seq.steps_run(), steps);
277 EXPECT_EQ(par.steps_run(), steps);
278 EXPECT_TRUE(frames_equal(seq.frame(), par.frame()))
279 << "Parallel engine diverged from sequential at " << steps << " steps with "
280 << partitions << " partitions";
281}
282
283// ---------------------------------------------------------------------------
284// Equivalence with the sequential engine.
285// ---------------------------------------------------------------------------
286
288{
290 L seed({64, 64}, 0);
291 seed_random(seed, /*seed=*/0xC0FFEEu);
292
293 for (std::size_t parts : {1u, 2u, 4u, 8u})
295}
296
298{
300 L seed({40, 80}, 0);
301 seed_random(seed, /*seed=*/123u);
302
303 for (std::size_t parts : {1u, 2u, 4u})
305}
306
308{
310 L seed({32, 48}, 0);
311 seed_random(seed, /*seed=*/7777u);
312
313 for (std::size_t parts : {1u, 3u, 8u})
315}
316
318{
320 L seed({24, 24}, 0);
321 seed_random(seed, /*seed=*/42u, /*density=*/0.25);
322
323 for (std::size_t parts : {1u, 2u, 4u})
325}
326
328{
330 L seed({36, 28}, 0);
331 seed_random(seed, /*seed=*/1u);
332
333 for (std::size_t parts : {1u, 4u, 8u})
335}
336
338{
340 L seed({48, 48});
341 seed_random(seed, /*seed=*/9090u);
342
343 for (std::size_t parts : {1u, 2u, 4u, 8u})
345}
346
356
358{
359 // A simple totalistic rule that depends on a non-trivial neighbourhood.
360 // Sum of the centre and 12 Von-Neumann radius-2 neighbours: keep the
361 // last bit. Plays nicely with our `int` cells.
363 L seed({32, 32}, 0);
364 seed_random(seed, /*seed=*/24601u);
365
366 auto totalistic = Totalistic_Rule<int (*)(int)>(+[](int sum) -> int { return sum & 1; });
367 for (std::size_t parts : {1u, 2u, 8u})
369}
370
372{
373 // The parallel engine respects the `Order` template just like the
374 // sequential one; equivalence must hold under tiled iteration too.
376 using Order = Tile<8, 8>;
377 L seed({64, 64}, 0);
378 seed_random(seed, /*seed=*/42424242u);
379
382
385 cfg.min_parallel_cells = 0;
388
389 seq.run(100);
390 par.run(100);
391 EXPECT_TRUE(frames_equal(seq.frame(), par.frame()));
392}
393
395{
398 std::array<Offset_Vec<1>, 2>{Offset_Vec<1>{-1}, Offset_Vec<1>{1}});
399
400 for (std::uint8_t rule_no : {std::uint8_t{30}, std::uint8_t{90}, std::uint8_t{110}})
401 {
402 L seed({257}, 0);
403 seed.set(typename L::coord_type{128}, 1);
405
406 for (std::size_t parts : {1u, 2u, 4u, 8u})
408 }
409}
410
412{
414 L seed({12, 12, 12}, 0);
415 seed_random(seed, /*seed=*/2024u, /*density=*/0.3);
416
417 // 3D Life-like rule: B5/S45.
419 +[](int current, std::size_t alive) -> int {
420 const bool alive_now = current != 0;
421 const bool next_alive = (alive_now and (alive == 4 or alive == 5))
422 or (not alive_now and alive == 5);
423 return next_alive ? 1 : 0;
424 });
425
426 for (std::size_t parts : {1u, 2u, 4u})
428}
429
430// ---------------------------------------------------------------------------
431// Engine-level behaviour.
432// ---------------------------------------------------------------------------
433
435{
436 // A very small lattice with a high threshold should bypass the pool
437 // entirely and produce the same trajectory as the sequential engine.
439 L seed({4, 4}, 0);
440 seed.set({0, 1}, 1);
441 seed.set({1, 2}, 1);
442 seed.set({2, 0}, 1);
443 seed.set({2, 1}, 1);
444 seed.set({2, 2}, 1); // glider
445
448 cfg.min_parallel_cells = 1024; // threshold above the 16-cell lattice
451
454
455 par.run(20);
456 seq.run(20);
457 EXPECT_TRUE(frames_equal(seq.frame(), par.frame()));
458}
459
461{
463 L seed({16, 16}, 0);
464 seed_random(seed, /*seed=*/1234u);
465
468 cfg.min_parallel_cells = 0;
471
472 std::size_t pre_count = 0;
473 std::size_t post_count = 0;
474 std::size_t last_pre_idx = 0;
475 std::size_t last_post_idx = 0;
476 par.on_pre_step([&](std::size_t step_idx, const L &) {
477 ++pre_count;
479 });
480 par.on_post_step([&](std::size_t step_idx, const L &) {
481 ++post_count;
483 });
484
485 par.run(7);
486 EXPECT_EQ(pre_count, 7u);
488 EXPECT_EQ(last_pre_idx, 6u); // pre fires before swap, so last index is 6
489 EXPECT_EQ(last_post_idx, 7u); // post fires after swap, so last index is 7
490}
491
493{
495 L seed({32, 32}, 0);
496 seed_random(seed, /*seed=*/8675309u);
497
499
502 cfg.num_partitions = 4; // oversubscribe the 2-worker pool
503 cfg.min_parallel_cells = 0;
506
509
510 par.run(50);
511 seq.run(50);
512 EXPECT_TRUE(frames_equal(seq.frame(), par.frame()));
513}
514
516{
517 // Same seed, same configuration, multiple independent runs: identical
518 // final frames.
520 L seed({40, 40}, 0);
521 seed_random(seed, /*seed=*/0xDEADBEEFu);
522
525 cfg.min_parallel_cells = 0;
526
531
532 a.run(80);
533 b.run(80);
534 EXPECT_TRUE(frames_equal(a.frame(), b.frame()));
535}
536
538{
539 // A glider on an 8x8 toroidal grid returns to a translated copy of
540 // itself after exactly four steps, both sequentially and in parallel.
542 L seed({8, 8}, 0);
543 seed.set({0, 1}, 1);
544 seed.set({1, 2}, 1);
545 seed.set({2, 0}, 1);
546 seed.set({2, 1}, 1);
547 seed.set({2, 2}, 1);
548
551 cfg.min_parallel_cells = 0;
554
555 par.run(4);
556
557 // After 4 steps the glider has moved by (+1, +1).
558 L expected({8, 8}, 0);
559 expected.set({1, 2}, 1);
560 expected.set({2, 3}, 1);
561 expected.set({3, 1}, 1);
562 expected.set({3, 2}, 1);
563 expected.set({3, 3}, 1);
564 EXPECT_TRUE(frames_equal(par.frame(), expected));
565}
566
568{
569 // A blinker has period 2: B at step 0, B' at step 1, B at step 2.
571 L seed({5, 5}, 0);
572 seed.set({2, 1}, 1);
573 seed.set({2, 2}, 1);
574 seed.set({2, 3}, 1);
575
576 for (std::size_t parts : {1u, 2u, 5u, 8u})
577 {
580 cfg.min_parallel_cells = 0;
583 par.run(2);
584 EXPECT_TRUE(frames_equal(par.frame(), seed))
585 << "Blinker did not return to seed with " << parts << " partitions";
586 }
587}
588
590{
591 // A small 1D lattice still works correctly when partitions exceed
592 // the number of cells along axis 0; any extra partitions should be
593 // empty no-ops.
596 std::array<Offset_Vec<1>, 2>{Offset_Vec<1>{-1}, Offset_Vec<1>{1}});
597
598 L seed({5}, 0);
599 seed.set({2}, 1);
600
602}
603
long double h
Definition btreepic.C:154
size_t steps
Definition ca-c-api.h:126
size_t size_t int32_t * out
Definition ca-c-api.h:120
size_t row
Definition ca-c-api.h:115
Domain-decomposition utilities for the parallel CA engine.
Common typedefs and tag types for the Cellular Automata module.
User-supplied list of offsets for arbitrary connectivity.
Lattice with Halo ghost layers around the user-visible cells.
Lattice that adds boundary-aware access on top of a storage.
Moore (Chebyshev) neighborhood of radius R in N dimensions.
Parallel synchronous double-buffered engine.
std::size_t steps_run() const noexcept
Return the number of completed steps.
void run(const std::size_t steps)
Run several synchronous steps.
const Lattice & frame() const noexcept
Return the current frame.
void on_pre_step(F &&f)
Register a hook fired before every step().
Synchronous double-buffered engine.
void run(const std::size_t steps)
Run several synchronous steps.
std::size_t steps_run() const noexcept
Return the number of completed steps.
const Lattice & frame() const noexcept
Return the current frame.
Von Neumann (L1) neighborhood of radius R in N dimensions.
A reusable thread pool for efficient parallel task execution.
Minimal std::expected-style result type for C++20.
#define TEST(name)
static mt19937 rng
size_t blossom_maximum_cardinality_matching(const GT &g, DynDlist< typename GT::Arc * > &matching, SA sa=SA())
Alias of compute_maximum_cardinality_general_matching().
Definition Blossom.H:466
constexpr Game_Of_Life_Rule make_game_of_life_rule() noexcept
Build the canonical Game of Life rule.
std::ptrdiff_t ca_index_t
Signed coordinate component used by lattices and neighborhoods.
Definition ca-traits.H:60
constexpr Range1D split_range_balanced(const ca_size_t n, const ca_size_t parts, const ca_size_t idx) noexcept
Balanced split of [0, n) into parts contiguous ranges.
Definition ca-tiling.H:127
double density(const Lattice &lat, const typename Lattice::state_type &s)
Definition ca-metrics.H:212
bool frames_equal(const Lattice &a, const Lattice &b)
Definition ca-metrics.H:323
constexpr bool should_run_sequential(const ca_size_t cells, const ca_size_t num_partitions, const ca_size_t min_cells) noexcept
Decide whether a workload should run sequentially.
Definition ca-tiling.H:163
std::size_t ca_size_t
Unsigned size component used for extents and counts.
Definition ca-traits.H:63
Coord_Vec< N > Offset_Vec
Default offset vector (aliases Coord_Vec).
Definition ca-traits.H:79
constexpr std::uint64_t morton_encode_2d(const std::uint32_t x, const std::uint32_t y) noexcept
Morton (Z-order) encoding of (x, y).
Definition ca-tiling.H:375
constexpr Lookup_Rule< 2, 2 > make_wolfram_elementary_rule(std::uint8_t rule_no) noexcept
Build the elementary 1D Wolfram rule rule_no (0..255) as a Lookup_Rule<2, 2> over neighbourhood {-1,...
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
and
Check uniqueness with explicit hash + equality functors.
T sum(const Container &container, const T &init=T{})
Compute sum of all elements.
static constexpr Tile2D_Range tile(const std::array< ca_size_t, 2 > &extents, const ca_size_t parts_y, const ca_size_t parts_x, const ca_size_t tile_index) noexcept
Tile owned by tile_index over the parts_y * parts_x grid.
Definition ca-tiling.H:266
static constexpr std::array< ca_size_t, 2 > factor_partitions(const std::array< ca_size_t, 2 > &extents, const ca_size_t parts) noexcept
Pick a balanced (parts_y, parts_x) factorisation of parts.
Definition ca-tiling.H:292
Constant-value boundary.
Definition ca-traits.H:138
Zero-gradient (Neumann) boundary.
Definition ca-traits.H:152
Out-of-range neighbours behave as if the lattice ended.
Definition ca-traits.H:119
Configuration for Parallel_Synchronous_Engine.
std::size_t num_partitions
Number of partitions per step.
ThreadPool * pool
Thread pool to schedule on. nullptr means Aleph::default_pool().
Out-of-range coordinates mirror back into the lattice.
Definition ca-traits.H:129
static constexpr Range1D slab(const std::array< ca_size_t, Rank > &extents, const ca_size_t parts, const ca_size_t idx) noexcept
Number of partitions along axis 0 with a balanced split.
Definition ca-tiling.H:190
Iterate a 2D lattice in W x H tiles.
Definition ca-traits.H:200
The lattice wraps around on every axis.
Definition ca-traits.H:124
ValueArg< size_t > seed
Definition testHash.C:53
static int * k
gsl_rng * r
A modern, efficient thread pool for parallel task execution.
Bit-packed dense storage for boolean cellular automata.
C++20 concepts for the Cellular Automata module.
Synchronous double-buffered engine for cellular automata.
Ghost-layer lattice for high-performance boundary handling.
Cellular automata lattice with pluggable boundary policies.
Neighborhoods catalogue for Aleph::CA.
Parallel synchronous engine for cellular automata (Phase 5).
static void seed_random(L &lat, std::uint32_t seed, double density=0.4)
static void expect_engine_equivalence(const Lattice &seed, Rule rule, Neighborhood nh, std::size_t steps, std::size_t partitions, std::size_t min_cells=0)
Rule mechanisms for Aleph::CA.
Dense, contiguous storage for cellular automata cells (1D/2D/3D).