Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
ca_multi_field_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
49#include <array>
50#include <cmath>
51#include <cstdint>
52#include <tuple>
53#include <vector>
54
55#include <gtest/gtest.h>
56
57#include <ca-traits.H>
59#include <tpl_ca_engine.H>
60#include <tpl_ca_lattice.H>
64#include <tpl_ca_neighborhood.H>
65#include <tpl_ca_rule.H>
66#include <tpl_ca_storage.H>
67
68using namespace Aleph;
69using namespace Aleph::CA;
70
71namespace
72{
73
74// ---------- Discrete predator-prey rule (integer fields) -------------
75
76struct Discrete_Predator_Prey_Rule
77{
78 // Fields: (prey, predator), both std::int32_t. Toy update used as a
79 // golden discrete oracle to compare AoS and SoA frame-by-frame.
80 using fields = std::tuple<std::int32_t, std::int32_t>;
81 using nb_views
82 = std::tuple<Neighbor_View<std::int32_t>, Neighbor_View<std::int32_t>>;
83
84 std::int32_t operator()(const fields &cur, const nb_views &nb) const noexcept = delete;
85
86 fields operator_impl(const fields &cur, const nb_views &nb) const noexcept
87 {
88 const auto prey = std::get<0>(cur);
89 const auto pred = std::get<1>(cur);
90 std::int32_t prey_n = 0;
91 std::int32_t pred_n = 0;
92 for (const auto &p : std::get<0>(nb)) prey_n += p;
93 for (const auto &p : std::get<1>(nb)) pred_n += p;
94 // Discrete coupling: prey grows from neighbour reservoir minus
95 // local predator pressure; predators average neighbour density and
96 // gain from prey but lose by self-density.
97 const std::int32_t next_prey = (prey + (prey_n / 8) - pred);
98 const std::int32_t next_pred = ((pred_n / 8) + (prey / 4) - (pred / 2));
99 return {next_prey, next_pred};
100 }
101};
102
103// Functor-callable variant matching Multi_Field_Rule contract.
104struct Discrete_PP
105{
106 using fields = std::tuple<std::int32_t, std::int32_t>;
107 using nb_views
108 = std::tuple<Neighbor_View<std::int32_t>, Neighbor_View<std::int32_t>>;
109 fields operator()(const fields &cur, const nb_views &nb) const noexcept
110 {
111 const auto prey = std::get<0>(cur);
112 const auto pred = std::get<1>(cur);
113 std::int32_t prey_n = 0;
114 std::int32_t pred_n = 0;
115 for (const auto &p : std::get<0>(nb)) prey_n += p;
116 for (const auto &p : std::get<1>(nb)) pred_n += p;
117 const std::int32_t next_prey = (prey + (prey_n / 8) - pred);
118 const std::int32_t next_pred = ((pred_n / 8) + (prey / 4) - (pred / 2));
119 return {next_prey, next_pred};
120 }
121};
122
123template <typename Layout>
125 std::int32_t, std::int32_t>;
126
127template <typename Layout>
128PP_Lattice<Layout> make_seeded_pp(const ca_size_t side, const std::uint32_t seed)
129{
131 // Deterministic seeding (no RNG dependency).
132 for (ca_size_t i = 0; i < side; ++i)
133 for (ca_size_t j = 0; j < side; ++j)
134 {
135 const std::int32_t prey = static_cast<std::int32_t>((i + j + seed) % 7);
136 const std::int32_t pred = static_cast<std::int32_t>((i * 2 + j + seed) % 5);
137 lat.template set<0>({static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)},
138 prey);
139 lat.template set<1>({static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)},
140 pred);
141 }
142 return lat;
143}
144
145template <typename A, typename B>
146bool frames_equal_pp(const A &a, const B &b)
147{
148 if (a.extents() != b.extents())
149 return false;
150 for (ca_size_t i = 0; i < a.size(0); ++i)
151 for (ca_size_t j = 0; j < a.size(1); ++j)
152 {
153 const Coord_Vec<2> c{static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)};
154 if (a.template at<0>(c) != b.template at<0>(c))
155 return false;
156 if (a.template at<1>(c) != b.template at<1>(c))
157 return false;
158 }
159 return true;
160}
161
162} // namespace
163
164// =====================================================================
165// AoS vs SoA equivalence
166// =====================================================================
167
169{
170 constexpr ca_size_t side = 16;
173
174 ASSERT_TRUE(frames_equal_pp(aos, soa)) << "initial frames differ";
175
178
179 Aos_Eng aeng(std::move(aos), Discrete_PP{}, Moore<2, 1>{});
180 Soa_Eng seng(std::move(soa), Discrete_PP{}, Moore<2, 1>{});
181
182 for (std::size_t t = 0; t < 25; ++t)
183 {
184 ASSERT_TRUE(frames_equal_pp(aeng.frame(), seng.frame()))
185 << "diverged at step " << t;
186 aeng.step();
187 seng.step();
188 }
189 EXPECT_TRUE(frames_equal_pp(aeng.frame(), seng.frame()))
190 << "diverged after 25 steps";
191}
192
193// =====================================================================
194// SoA per-field buffer plays nicely with mono-field engine
195// =====================================================================
196
198{
200 L lat({4, 4});
201 // Seed only field<0> via the legacy Lattice<> reference.
202 auto &f0 = lat.field<0>();
203 for (ca_size_t i = 0; i < 4; ++i)
204 for (ca_size_t j = 0; j < 4; ++j)
205 f0.set({static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)},
206 static_cast<int>((i + j) & 1));
207
208 // Run a mono-field Game-of-Life engine on field<0> only.
212 eng.step();
213
214 // The SoA lattice's field<1> must remain at its default value (0.0).
215 for (ca_size_t i = 0; i < 4; ++i)
216 for (ca_size_t j = 0; j < 4; ++j)
218 lat.at<1>({static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)}),
219 0.0);
220}
221
222// =====================================================================
223// Multi-field Gray-Scott migration: identical to monolithic rule
224// =====================================================================
225
226namespace
227{
228
229// A multi-field Gray-Scott rule that consumes (u, v) views directly
230// instead of the monolithic Reaction_Diffusion_Cell. The arithmetic
231// is reproduced verbatim from Gray_Scott_Rule so frames agree with
232// the monolithic version to within a tight numerical tolerance
233// (FP reorderings prevent exact bit-identity — see the test below).
234class Multi_Field_Gray_Scott
235{
236 double feed_, kill_, du_, dv_, dt_;
238
239public:
240 Multi_Field_Gray_Scott(double feed, double kill, double du, double dv, double dt = 1.0)
241 : feed_(feed), kill_(kill), du_(du), dv_(dv), dt_(dt)
242 {}
243
244 std::tuple<double, double>
245 operator()(const std::tuple<double, double> &cur,
246 const std::tuple<Neighbor_View<double>, Neighbor_View<double>> &nb) const
247 {
248 const double u = std::get<0>(cur);
249 const double v = std::get<1>(cur);
250 const auto &uv = std::get<0>(nb);
251 const auto &vv = std::get<1>(nb);
252 double lu = u * lap_.center();
253 double lv = v * lap_.center();
254 for (std::size_t k = 0; k < uv.size(); ++k)
255 lu += uv[k] * lap_.neighbour_weight(k);
256 for (std::size_t k = 0; k < vv.size(); ++k)
257 lv += vv[k] * lap_.neighbour_weight(k);
258 const double uvv = u * v * v;
259 double next_u = u + dt_ * (du_ * lu - uvv + feed_ * (1.0 - u));
260 double next_v = v + dt_ * (dv_ * lv + uvv - (feed_ + kill_) * v);
261 // Mirror the clamp-after-check logic from Gray_Scott_Rule.
262 if (next_u < 0.0) next_u = 0.0;
263 else if (next_u > 1.0) next_u = 1.0;
264 if (next_v < 0.0) next_v = 0.0;
265 else if (next_v > 1.0) next_v = 1.0;
266 return {next_u, next_v};
267 }
268};
269
270} // namespace
271
273{
275 constexpr ca_size_t side = 24;
277 Mono_Lat mono({side, side}, Cell{1.0, 0.0});
278
279 using MF_Lat
281 MF_Lat mf({side, side}, std::tuple{1.0, 0.0});
282
283 // Seed a small v-rich square at the centre — same on both lattices.
284 for (ca_size_t i = side / 2 - 2; i < side / 2 + 2; ++i)
285 for (ca_size_t j = side / 2 - 2; j < side / 2 + 2; ++j)
286 {
287 const Coord_Vec<2> c{static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)};
288 mono.set(c, Cell{0.5, 0.25});
289 mf.set<0>(c, 0.5);
290 mf.set<1>(c, 0.25);
291 }
292
294 std::move(mono), Gray_Scott_Rule<>{0.0367, 0.0649, 0.16, 0.08, 1.0},
295 Moore<2, 1>{});
297 std::move(mf), Multi_Field_Gray_Scott{0.0367, 0.0649, 0.16, 0.08, 1.0},
298 Moore<2, 1>{});
299
300 for (std::size_t t = 0; t < 50; ++t)
301 {
302 meng.step();
303 feng.step();
304 }
305
306 // The two engines are intended to apply the same arithmetic in the
307 // same neighbour order, but they live in two distinct rule classes
308 // (struct-based `Gray_Scott_Rule<>` vs tuple-based
309 // `Multi_Field_Gray_Scott`). Compilers are free to apply different
310 // FP reorderings to each — FMA contraction on AArch64 GCC Release
311 // is enough to make `ASSERT_DOUBLE_EQ` fail in the last bits, even
312 // though both implementations remain individually correct. Use a
313 // tight numeric tolerance instead of bit-identity to keep the
314 // intent ("the two paths agree to within machine noise") while
315 // surviving optimiser-driven reorderings.
316 constexpr double kGSTolerance = 1e-12;
317 for (ca_size_t i = 0; i < side; ++i)
318 for (ca_size_t j = 0; j < side; ++j)
319 {
320 const Coord_Vec<2> c{static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)};
321 const Cell mc = meng.frame().at(c);
322 ASSERT_NEAR(feng.frame().at<0>(c), mc.u, kGSTolerance)
323 << "u differs at (" << i << "," << j << ")";
324 ASSERT_NEAR(feng.frame().at<1>(c), mc.v, kGSTolerance)
325 << "v differs at (" << i << "," << j << ")";
326 }
327}
328
329// =====================================================================
330// Lotka-Volterra continuous predator-prey
331// =====================================================================
332
333namespace
334{
335
336class Lotka_Volterra_Rule
337{
338 double alpha_, beta_, delta_, gamma_, dt_;
339 double diff_;
340
341public:
342 Lotka_Volterra_Rule(double alpha, double beta, double delta, double gamma,
343 double dt, double diffusion)
344 : alpha_(alpha), beta_(beta), delta_(delta), gamma_(gamma), dt_(dt),
345 diff_(diffusion) {}
346
347 std::tuple<double, double>
348 operator()(const std::tuple<double, double> &cur,
349 const std::tuple<Neighbor_View<double>, Neighbor_View<double>> &nb) const
350 {
351 const double prey = std::get<0>(cur);
352 const double pred = std::get<1>(cur);
353 // 4-neighbour average (we use Von_Neumann<2,1>).
354 double prey_lap = -static_cast<double>(std::get<0>(nb).size()) * prey;
355 double pred_lap = -static_cast<double>(std::get<1>(nb).size()) * pred;
356 for (const auto &p : std::get<0>(nb)) prey_lap += p;
357 for (const auto &p : std::get<1>(nb)) pred_lap += p;
358 const double next_prey = prey + dt_ * (alpha_ * prey - beta_ * prey * pred
359 + diff_ * prey_lap);
360 const double next_pred = pred + dt_ * (delta_ * prey * pred - gamma_ * pred
361 + diff_ * pred_lap);
362 return {next_prey < 0.0 ? 0.0 : next_prey,
363 next_pred < 0.0 ? 0.0 : next_pred};
364 }
365};
366
367} // namespace
368
370{
372 constexpr ca_size_t side = 32;
373 L lat({side, side}, std::tuple{1.0, 0.5}); // homogeneous start
374
375 // Perturb the centre to break symmetry.
376 for (ca_size_t i = side / 2 - 1; i <= side / 2 + 1; ++i)
377 for (ca_size_t j = side / 2 - 1; j <= side / 2 + 1; ++j)
378 {
379 const Coord_Vec<2> c{static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)};
380 lat.set<0>(c, 1.5);
381 lat.set<1>(c, 0.8);
382 }
383
385 std::move(lat), Lotka_Volterra_Rule{1.0, 0.5, 0.4, 0.5, 0.05, 0.05},
387
388 // Track total prey/predator population every step. With these
389 // parameters the system should oscillate (no monotonic blow-up).
390 std::vector<double> prey_history;
391 std::vector<double> pred_history;
392 prey_history.reserve(200);
393 pred_history.reserve(200);
394 for (std::size_t t = 0; t < 200; ++t)
395 {
396 eng.step();
397 double prey_total = 0;
398 double pred_total = 0;
399 for (ca_size_t i = 0; i < side; ++i)
400 for (ca_size_t j = 0; j < side; ++j)
401 {
402 const Coord_Vec<2> c{static_cast<ca_index_t>(i), static_cast<ca_index_t>(j)};
403 prey_total += eng.frame().at<0>(c);
404 pred_total += eng.frame().at<1>(c);
405 ASSERT_GE(eng.frame().at<0>(c), 0.0) << "prey<0 at step " << t;
406 ASSERT_GE(eng.frame().at<1>(c), 0.0) << "pred<0 at step " << t;
407 ASSERT_LT(eng.frame().at<0>(c), 1e6) << "prey blew up at step " << t;
408 ASSERT_LT(eng.frame().at<1>(c), 1e6) << "pred blew up at step " << t;
409 }
410 prey_history.push_back(prey_total);
411 pred_history.push_back(pred_total);
412 }
413
414 // Detect at least one prey local extremum (a turning point) — a
415 // direct sanity check that the system actually oscillates rather
416 // than monotonically driving to a fixed point.
417 std::size_t turning_points = 0;
418 for (std::size_t k = 1; k + 1 < prey_history.size(); ++k)
419 {
420 const double a = prey_history[k - 1];
421 const double b = prey_history[k];
422 const double c = prey_history[k + 1];
423 if ((b > a and b > c) or (b < a and b < c))
425 }
427 << "Lotka-Volterra prey trajectory shows no oscillation";
428}
429
430// =====================================================================
431// Field_Slice_Rule: untouched fields propagate verbatim
432// =====================================================================
433
435{
437 L lat({3, 3}, std::tuple{0, 7}); // field 0 = 0, field 1 = 7
438 // Place a single live cell in field<0>; field<1> stays at 7 everywhere.
439 lat.set<0>({1, 1}, 1);
440
442 Multi_Field_Engine<L, decltype(slice), Moore<2, 1>> eng(
443 std::move(lat), std::move(slice), Moore<2, 1>{});
444 eng.run(5);
445
446 // Field<1> must be 7 everywhere (untouched by the GoL slice on
447 // field<0>).
448 for (ca_size_t i = 0; i < 3; ++i)
449 for (ca_size_t j = 0; j < 3; ++j)
450 EXPECT_EQ(eng.frame().at<1>({static_cast<ca_index_t>(i),
451 static_cast<ca_index_t>(j)}),
452 7);
453
454 // Field<0> must actually evolve under Game of Life: a lone live cell
455 // with no live neighbours dies on the very first step (B3/S23) and
456 // stays dead forever, so after 5 steps the seeded centre must be 0.
457 // Without this check the test could pass with a slice rule that did
458 // nothing on field<0>.
459 EXPECT_EQ(eng.frame().at<0>({static_cast<ca_index_t>(1),
460 static_cast<ca_index_t>(1)}),
461 0);
462 // For good measure, the whole field<0> must be empty (no cell has
463 // ever had three live neighbours so no birth can have occurred).
464 for (ca_size_t i = 0; i < 3; ++i)
465 for (ca_size_t j = 0; j < 3; ++j)
466 EXPECT_EQ(eng.frame().at<0>({static_cast<ca_index_t>(i),
467 static_cast<ca_index_t>(j)}),
468 0);
469}
Common typedefs and tag types for the Cellular Automata module.
Adaptor: apply a mono-field rule to field I, leave the other fields unchanged.
Gray-Scott reaction-diffusion rule.
Dense odd-sized 2-D convolution kernel.
Definition ca-kernels.H:83
T neighbour_weight(const std::size_t k) const
Return a centre-skipping neighbour weight.
Definition ca-kernels.H:205
constexpr T center() const noexcept
Return the centre weight.
Definition ca-kernels.H:228
Lattice that adds boundary-aware access on top of a storage.
Moore (Chebyshev) neighborhood of radius R in N dimensions.
Synchronous double-buffered engine for multi-field lattices.
void step()
Apply the multi-field rule to every cell once and swap buffers.
Multi-field lattice with selectable storage layout.
auto & field()
Mutable reference to the underlying single-field lattice for field I.
void set(const coord_type &c, const V &v)
Strict write access to field I.
Synchronous double-buffered engine.
Von Neumann (L1) neighborhood of radius R in N dimensions.
#define TEST(name)
__gmp_expr< T, __gmp_unary_expr< __gmp_expr< T, U >, __gmp_gamma_function > > gamma(const __gmp_expr< T, U > &expr)
Definition gmpfrxx.h:4103
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
Freq_Node * pred
Predecessor node in level-order traversal.
constexpr std::uint32_t delta
File contains only the cells that changed relative to a baseline.
constexpr Game_Of_Life_Rule make_game_of_life_rule() noexcept
Build the canonical Game of Life rule.
std::span< const T > Neighbor_View
Read-only view over a contiguous range of neighbour values.
Definition ca-traits.H:90
std::ptrdiff_t ca_index_t
Signed coordinate component used by lattices and neighborhoods.
Definition ca-traits.H:60
std::array< ca_index_t, N > Coord_Vec
Default coordinate vector.
Definition ca-traits.H:69
std::size_t ca_size_t
Unsigned size component used for extents and counts.
Definition ca-traits.H:63
Outer_Totalistic_Rule< Game_Of_Life_Functor > Game_Of_Life_Rule
Outer-totalistic rule type implementing Conway's Game of Life.
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
and
Check uniqueness with explicit hash + equality functors.
STL namespace.
Out-of-range neighbours behave as if the lattice ended.
Definition ca-traits.H:119
Two-field state for reaction-diffusion cellular automata.
The lattice wraps around on every axis.
Definition ca-traits.H:124
ValueArg< size_t > seed
Definition testHash.C:53
static int * k
Continuous and memory-bearing CA rules (Phase 9).
Synchronous double-buffered engine for cellular automata.
Cellular automata lattice with pluggable boundary policies.
Phase 14 synchronous double-buffered engine for multi-field cellular automata.
Phase 14 multi-field cellular automata lattice (AoS / SoA).
Phase 14 multi-field local rules for Aleph::CA.
Neighborhoods catalogue for Aleph::CA.
Rule mechanisms for Aleph::CA.
Dense, contiguous storage for cellular automata cells (1D/2D/3D).