Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
tpl_ca_hashlife_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
47# include <cstdint>
48# include <set>
49# include <sstream>
50# include <string>
51# include <utility>
52
53# include <gtest/gtest.h>
54
55# include <tpl_ca_hashlife.H>
56
57using namespace Aleph::CA;
58
59namespace
60{
61 using Cell = std::pair<std::int64_t, std::int64_t>;
62 using Cell_Set = std::set<Cell>;
63
64 // ------------------------------------------------------------------
65 // Helpers
66 // ------------------------------------------------------------------
67
69 {
70 Cell_Set s;
71 e.for_each_alive([&s](std::int64_t x, std::int64_t y)
72 { s.emplace(x, y); });
73 return s;
74 }
75
77 {
78 for (const auto & c : cells)
79 e.set_alive(c.first, c.second, true);
80 return engine_cells(e);
81 }
82
83 // Brute-force one Game-of-Life-style step on an arbitrary cell set.
84 // Operates on the union of alive cells and their 8-neighbour shells, so
85 // unbounded growth is correctly tracked.
88 {
90 for (const auto & c : cur)
91 for (int dy = -1; dy <= 1; ++dy)
92 for (int dx = -1; dx <= 1; ++dx)
93 candidates.emplace(c.first + dx, c.second + dy);
94
96 for (const auto & c : candidates)
97 {
98 std::uint8_t n = 0;
99 for (int dy = -1; dy <= 1; ++dy)
100 for (int dx = -1; dx <= 1; ++dx)
101 {
102 if (dx == 0 and dy == 0)
103 continue;
104 if (cur.count({ c.first + dx, c.second + dy }))
105 ++n;
106 }
107 const bool alive = cur.count(c) != 0;
108 if (rule.apply(alive, n))
109 nxt.insert(c);
110 }
111 return nxt;
112 }
113
115 const Outer_Totalistic_Binary_Rule & rule,
116 std::uint64_t steps)
117 {
118 for (std::uint64_t i = 0; i < steps; ++i)
119 cur = brute_force_step(cur, rule);
120 return cur;
121 }
122
123 // Canonical patterns expressed as cell sets.
124
126 {
127 return Cell_Set{ { 0, -1 }, { 0, 0 }, { 0, 1 } };
128 }
129
131 {
132 return Cell_Set{ { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } };
133 }
134
135 // Standard SE-bound glider:
136 // .O.
137 // ..O
138 // OOO
140 {
141 return Cell_Set{ { 1, 0 },
142 { 2, 1 },
143 { 0, 2 }, { 1, 2 }, { 2, 2 } };
144 }
145
146 Cell_Set translate(const Cell_Set & cs, std::int64_t dx, std::int64_t dy)
147 {
149 for (const auto & c : cs)
150 out.emplace(c.first + dx, c.second + dy);
151 return out;
152 }
153
154 // Gosper glider gun (LWSS / classic configuration).
155 // Coordinates from the canonical RLE; placed near origin.
157 {
158 static const int xy[][2] = {
159 { 1, 5 }, { 1, 6 }, { 2, 5 }, { 2, 6 },
160 { 11, 5 }, { 11, 6 }, { 11, 7 }, { 12, 4 }, { 12, 8 },
161 { 13, 3 }, { 13, 9 }, { 14, 3 }, { 14, 9 },
162 { 15, 6 }, { 16, 4 }, { 16, 8 },
163 { 17, 5 }, { 17, 6 }, { 17, 7 }, { 18, 6 },
164 { 21, 3 }, { 21, 4 }, { 21, 5 },
165 { 22, 3 }, { 22, 4 }, { 22, 5 },
166 { 23, 2 }, { 23, 6 },
167 { 25, 1 }, { 25, 2 }, { 25, 6 }, { 25, 7 },
168 { 35, 3 }, { 35, 4 }, { 36, 3 }, { 36, 4 },
169 };
170 Cell_Set s;
171 for (const auto & p : xy)
172 s.emplace(p[0], p[1]);
173 return s;
174 }
175} // namespace
176
177
178// ----------------------------------------------------------------------
179// 1. Cell-level API
180// ----------------------------------------------------------------------
181
189
191{
193 e.set_alive(3, -7, true);
194 EXPECT_TRUE(e.alive_at(3, -7));
195 EXPECT_FALSE(e.alive_at(0, 0));
196 EXPECT_EQ(e.population(), 1u);
197
198 auto bb = e.bbox();
199 EXPECT_EQ(bb.x_min, 3);
200 EXPECT_EQ(bb.y_min, -7);
201 EXPECT_EQ(bb.x_max, 3);
202 EXPECT_EQ(bb.y_max, -7);
203 EXPECT_EQ(bb.width(), 1);
204 EXPECT_EQ(bb.height(), 1);
205
206 e.set_alive(3, -7, false);
207 EXPECT_FALSE(e.alive_at(3, -7));
208 EXPECT_EQ(e.population(), 0u);
209}
210
212{
214 Cell_Set src{ { -100, -100 }, { 0, 0 }, { 7, 13 }, { -3, 5 } };
215 for (const auto & c : src)
216 e.set_alive(c.first, c.second);
217 EXPECT_EQ(engine_cells(e), src);
218 EXPECT_EQ(e.population(), src.size());
219
220 auto bb = e.bbox();
221 EXPECT_EQ(bb.x_min, -100);
222 EXPECT_EQ(bb.x_max, 7);
223 EXPECT_EQ(bb.y_min, -100);
224 EXPECT_EQ(bb.y_max, 13);
225}
226
228{
230 load_cells(e, blinker());
231 e.advance(5);
232 EXPECT_GT(e.generation(), 0);
233 EXPECT_GT(e.population(), 0u);
234
235 e.clear();
236 EXPECT_EQ(e.population(), 0u);
237 EXPECT_EQ(e.generation(), 0);
239}
240
242{
244 e.set_alive(0, 0);
245 EXPECT_FALSE(e.alive_at(1'000'000, 1'000'000));
246 EXPECT_FALSE(e.alive_at(-1'000'000, -1'000'000));
247}
248
249
250// ----------------------------------------------------------------------
251// 2. Canonical Game of Life patterns
252// ----------------------------------------------------------------------
253
255{
257 load_cells(e, block());
258 const auto initial = engine_cells(e);
259 for (unsigned k : { 0u, 1u, 2u, 3u, 4u, 5u })
260 {
261 e.advance(k);
263 << "block changed after advance(" << k << ")";
264 }
265}
266
268{
270 load_cells(e, blinker());
271 // The blinker stored by blinker() is horizontal (3 cells along
272 // axis 1); after an even number of generations it must return to
273 // this same configuration.
274 const auto initial = engine_cells(e);
275 const std::uint64_t steps0 = e.advance(0);
276 const std::uint64_t steps10 = e.advance(10);
277 // The blinker has period 2; only an even total of advanced
278 // generations is guaranteed to land back on the initial pattern.
279 ASSERT_EQ((steps0 + steps10) % 2u, 0u)
280 << "advance returned an odd total of generations: "
281 << steps0 << " + " << steps10;
283 << "after " << (steps0 + steps10) << " generations";
284 EXPECT_EQ(e.population(), 3u);
285}
286
288{
290 const auto initial = glider();
292
293 // After 4 steps the glider has shifted by (+1, +1) and is otherwise
294 // identical (same cell offsets).
295 const std::uint64_t advanced = e.run(4);
296 ASSERT_GE(advanced, 4u);
298 static_cast<std::int64_t>(advanced) / 4,
299 static_cast<std::int64_t>(advanced) / 4));
300 EXPECT_EQ(e.population(), 5u);
301}
302
304{
306 load_cells(e, glider());
307 Cell_Set ref = glider();
308
309 // Iterate by power-of-two chunks, comparing at every checkpoint.
310 for (unsigned k = 0; k <= 9; ++k) // up to 1024 generations
311 {
312 const std::uint64_t advanced = e.advance(k);
314 EXPECT_EQ(engine_cells(e), ref)
315 << "after advance(" << k << ") = " << advanced << " steps";
316 }
317}
318
319
320// ----------------------------------------------------------------------
321// 3. Cross-validation across several rules
322// ----------------------------------------------------------------------
323
325{
326 const char * name;
329 std::uint64_t steps;
330};
331
332class CAHashlifeBruteForceMatch : public ::testing::TestWithParam<Rule_Sample>
333{};
334
336{
337 const auto & sample = GetParam();
338 Hashlife_Engine e(sample.rule);
339 load_cells(e, sample.seed);
340 e.run(sample.steps);
341
342 const Cell_Set ref =
343 brute_force_run(sample.seed, sample.rule, sample.steps);
344 EXPECT_EQ(engine_cells(e), ref) << sample.name;
345}
346
349 ::testing::Values(
350 Rule_Sample{ "Conway_blinker_128", Conway_Life, blinker(), 128u },
351 Rule_Sample{ "Conway_block_128", Conway_Life, block(), 128u },
352 Rule_Sample{ "Conway_glider_256", Conway_Life, glider(), 256u },
353 Rule_Sample{ "HighLife_glider_128", HighLife, glider(), 128u },
354 Rule_Sample{ "Seeds_blinker_32", Seeds, blinker(), 32u },
355 Rule_Sample{ "Day_Night_block_64", Day_And_Night, block(), 64u }
356 ),
357 [](const ::testing::TestParamInfo<Rule_Sample> & i)
358 { return std::string(i.param.name); });
359
360
361// ----------------------------------------------------------------------
362// 4. Advance / run semantics
363// ----------------------------------------------------------------------
364
366{
368 load_cells(e, glider());
369 for (unsigned k = 0; k <= 8; ++k)
370 {
371 const std::uint64_t before = static_cast<std::uint64_t>(e.generation());
372 const std::uint64_t advanced = e.advance(k);
373 EXPECT_GE(advanced, std::uint64_t{1} << k);
374 EXPECT_EQ(static_cast<std::uint64_t>(e.generation()),
375 before + advanced);
376 }
377}
378
380{
382 load_cells(e, glider());
383 const std::int64_t target = e.generation() + 1023;
384 e.run(1023);
385 EXPECT_GE(e.generation(), target);
386}
387
389{
391 EXPECT_THROW(e.advance(63), std::domain_error);
392 EXPECT_THROW(e.advance(100), std::domain_error);
393}
394
399
400
401// ----------------------------------------------------------------------
402// 5. RLE round-trip
403// ----------------------------------------------------------------------
404
406{
407 Hashlife_Engine src;
408 load_cells(src, glider());
409 const std::string rle = src.save_rle_string("test");
410
413
414 EXPECT_EQ(dst.population(), src.population());
415 // Both engines may centre the pattern differently, but the *shape*
416 // (set of cells minus its bbox origin) must coincide.
417 auto normalise = [](const Cell_Set & cs)
418 {
419 if (cs.empty())
420 return cs;
421 std::int64_t mx = cs.begin()->first;
422 std::int64_t my = cs.begin()->second;
423 for (const auto & c : cs)
424 {
425 if (c.first < mx) mx = c.first;
426 if (c.second < my) my = c.second;
427 }
429 for (const auto & c : cs)
430 out.emplace(c.first - mx, c.second - my);
431 return out;
432 };
434}
435
437{
438 Hashlife_Engine src;
439 load_cells(src, gosper_gun());
440 const std::string rle = src.save_rle_string();
441
444
445 EXPECT_EQ(dst.population(), src.population());
446 EXPECT_EQ(dst.population(), gosper_gun().size());
447}
448
450{
452 load_cells(e, blinker());
453 const std::string rle = e.save_rle_string();
454 EXPECT_NE(rle.find("rule = B36/S23"), std::string::npos)
455 << "rule field missing from: " << rle;
456}
457
459{
460 // Glider in canonical RLE.
461 const std::string glider_rle =
462 "#N glider\n"
463 "x = 3, y = 3, rule = B3/S23\n"
464 "bob$2bo$3o!\n";
467 EXPECT_EQ(e.population(), 5u);
468 // After 4 steps it should still have 5 cells (gliders don't lose cells).
469 e.run(4);
470 EXPECT_EQ(e.population(), 5u);
471}
472
474{
475 for (const auto & r : { Conway_Life, HighLife, Day_And_Night, Seeds })
476 {
477 const std::string s = format_rule(r);
478 auto parsed = parse_rule(s);
479 ASSERT_TRUE(parsed.has_value());
480 EXPECT_EQ(*parsed, r) << "round-trip failed for: " << s;
481 }
482}
483
484
485// ----------------------------------------------------------------------
486// 6. Cache and rule changes
487// ----------------------------------------------------------------------
488
490{
492 load_cells(e, glider());
493 e.advance(5);
494 const auto stats = e.stats();
495 EXPECT_GT(stats.canonical_nodes, 0u);
496 EXPECT_GT(stats.result_misses, 0u);
497}
498
500{
502 load_cells(e, glider());
503 e.advance(4);
504 const auto before = e.stats();
505 EXPECT_GT(before.result_misses, 0u);
506
508 EXPECT_EQ(e.stats().result_cache_clears, before.result_cache_clears + 1);
509}
510
512{
513 Hashlife_Engine e(Conway_Life, /*cache_capacity*/ 2048);
515 e.run(2048);
516 // The result cache must have been cleared at least once before we hit
517 // 2 Ki canonical nodes; otherwise the bound would be violated.
519}
520
521
522// ----------------------------------------------------------------------
523// 7. Comparison against a synchronous brute-force reference at scale
524// ----------------------------------------------------------------------
525
535
537{
540 const std::uint64_t pop_initial = e.population();
541 e.run(120); // 4 gliders emitted (one per 30 generations)
542 const std::uint64_t pop_after = e.population();
544 // Gosper gun emits one glider every 30 generations and oscillates
545 // through configurations of varying internal density. Within the first
546 // few hundred generations, total population stays in a band that easily
547 // accommodates the gun (36-50 cells across its cycle) plus roughly
548 // four newly-emitted gliders (5 cells each).
549 EXPECT_GE(pop_after, 40u);
550 EXPECT_LE(pop_after, 100u);
551}
552
553
554// ----------------------------------------------------------------------
555// 8. Edge cases
556// ----------------------------------------------------------------------
557
559{
561 load_cells(e, blinker());
562 const auto before = engine_cells(e);
563 EXPECT_EQ(e.run(0), 0u);
565 EXPECT_EQ(e.generation(), 0);
566}
567
569{
571 e.set_alive(1'000'000, -1'000'000);
572 EXPECT_TRUE(e.alive_at(1'000'000, -1'000'000));
573 EXPECT_EQ(e.population(), 1u);
574 // A lone cell dies on the next Conway step (no neighbours).
575 e.run(1);
576 EXPECT_EQ(e.population(), 0u);
577}
578
580{
582 for (int i = 0; i < 10; ++i)
583 e.set_alive(5, 5, true);
584 EXPECT_EQ(e.population(), 1u);
585 for (int i = 0; i < 10; ++i)
586 e.set_alive(5, 5, false);
587 EXPECT_EQ(e.population(), 0u);
588}
589
591{
592 // A short stress test: a known random-looking pattern on Conway, run for
593 // 512 steps, must match the brute-force exactly. Any drift exposes a
594 // bug in the macrocell evolution.
596 // R-pentomino: notoriously chaotic until step ~1100; perfect for
597 // catching subtle bugs.
598 for (auto p : { Cell{1, 0}, Cell{2, 0}, Cell{0, 1}, Cell{1, 1}, Cell{1, 2} })
599 seed.insert(p);
600
602 load_cells(e, seed);
603 e.run(512);
604 const Cell_Set ref = brute_force_run(seed, Conway_Life, 512);
605 EXPECT_EQ(engine_cells(e), ref);
606 EXPECT_EQ(e.population(), ref.size());
607}
size_t steps
Definition ca-c-api.h:126
size_t size_t int32_t * out
Definition ca-c-api.h:120
Hashlife engine for outer-totalistic binary cellular automata.
void load_rle_string(const std::string &s)
Convenience overload that reads the pattern from a string.
BBox bbox() const
Tight bounding box of the alive cells (empty if population() == 0).
void set_rule(const Rule &r)
Replace the rule. Invalidates the result cache.
std::uint64_t run(std::uint64_t generations)
Advance by exactly generations steps.
void for_each_alive(F &&f) const
Iterate over all alive cells, calling f(x, y) for each.
Stats stats() const noexcept
Diagnostic counters and current root level.
void set_alive(const std::int64_t x, const std::int64_t y, const bool alive=true)
Set or clear the cell at world coordinates (x, y).
std::uint64_t advance(const unsigned k)
Advance the universe by 2^k generations.
std::string save_rle_string(const std::string &comment={}) const
Convenience overload returning the RLE serialisation as a string.
void clear()
Reset the universe to empty (preserves rule, capacity and node cache).
std::int64_t generation() const noexcept
Number of generations elapsed since construction (or the last clear).
bool alive_at(const std::int64_t x, const std::int64_t y) const
Read the cell at world coordinates (x, y).
std::uint64_t population() const noexcept
Number of alive cells.
#define TEST(name)
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
static mpfr_t y
Definition mpfr_mul_d.c:3
std::optional< Outer_Totalistic_Binary_Rule > parse_rule(const std::string &s)
Parse a B.../S... (or Wolfram S/B) rule string.
std::string format_rule(const Outer_Totalistic_Binary_Rule &r)
Format a rule as a Conway-style Bxxx/Sxxx string.
constexpr Outer_Totalistic_Binary_Rule Conway_Life
Conway's Game of Life: B3/S23.
constexpr Outer_Totalistic_Binary_Rule HighLife
Nathan Thompson's HighLife: B36/S23 (replicators).
constexpr Outer_Totalistic_Binary_Rule Day_And_Night
Bays' Day & Night: B3678/S34678 (self-complementary).
constexpr Outer_Totalistic_Binary_Rule Seeds
Seeds: B2/S (every live cell dies, two neighbours produce a birth).
Compiler_SSA_Block & block(Compiler_SSA_Function &function, const Compiler_SSA_Block_Id id)
Definition SSA.H:636
size_t size(Node *root) noexcept
and
Check uniqueness with explicit hash + equality functors.
bool is_empty() const noexcept
True iff there are no alive cells.
std::size_t result_cache_clears
times the result cache was invalidated
Outer-totalistic binary rule encoded as two 9-bit bitmasks.
constexpr bool apply(const bool current, const std::uint8_t n) const noexcept
True iff a cell with current state and n live neighbours is alive next.
std::uint64_t steps
Outer_Totalistic_Binary_Rule rule
ValueArg< size_t > seed
Definition testHash.C:53
static int * k
gsl_rng * r
Hashlife engine for outer-totalistic binary cellular automata.
INSTANTIATE_TEST_SUITE_P(CAHashlifeBruteForce, CAHashlifeBruteForceMatch, ::testing::Values(Rule_Sample{ "Conway_blinker_128", Conway_Life, blinker(), 128u }, Rule_Sample{ "Conway_block_128", Conway_Life, block(), 128u }, Rule_Sample{ "Conway_glider_256", Conway_Life, glider(), 256u }, Rule_Sample{ "HighLife_glider_128", HighLife, glider(), 128u }, Rule_Sample{ "Seeds_blinker_32", Seeds, blinker(), 32u }, Rule_Sample{ "Day_Night_block_64", Day_And_Night, block(), 64u }), [](const ::testing::TestParamInfo< Rule_Sample > &i) { return std::string(i.param.name);})
TEST_P(CAHashlifeBruteForceMatch, MatchesUnboundedReference)