120° symmetric Belousov-Zhabotinsky patterns

A forum where anything goes. Introduce yourselves to other members of the forums, discuss how your name evolves when written out in the Game of Life, or just tell us how you found it. This is the forum for "non-academic" content.
Post Reply
User avatar
pcallahan
Posts: 854
Joined: April 26th, 2013, 1:04 pm

120° symmetric Belousov-Zhabotinsky patterns

Post by pcallahan » December 23rd, 2022, 7:50 pm

This is sort of CA-related, but I'm using a floating point calculation so I'll put it in sandbox. I'm actually exploring the decorative potential of these spirals after converting to vector graphic and cutting on die cutter. But I thought the code with symmetry might be of interest. I haven't seen this done before.

I used Python along with some common libraries to run a Belousov-Zhabotinsky simulation I found online. After playing around with it, I decided I wanted a symmetric pattern (actually to make a drink coaster) and my favorite symmetry is the kind you get packing rhombuses with 120° rotation. I already worked on embedding these in a 2D array for hex CAs, so it wasn't hard to apply to this case.

Code: Select all

import math
import numpy as np
from scipy.ndimage import convolve
from sys import argv
from PIL import Image

# Simulation method modified from https://scipython.com/blog/simulating-the-belousov-zhabotinsky-reaction/

# Side length of base rhombus (stored as an nXn grid)
n = 200 if len(argv) < 2 else int(argv[1])
# Value for all Belousov-Zhabotinsky reaction coefficients. 
param = 1 if len(argv) < 3 else float(argv[2])
# Number of iterations to run.
niter = 500 if len(argv) < 4 else int(argv[3])
# File to write image to.
imgfile = "./bzrhombus.png" if len(argv) < 5 else argv[4]

# Reaction parameters.
alpha, beta, gamma = param, param, param

# Shape of hexagonal neighborhood out to radius 3.
FILTER_STR = '''
1111000
1111100
1111110
1111111
0111111
0011111
0001111
'''
FILTER_INT = [[int(c) for c in row] for row in FILTER_STR.split()]
FILTER = np.array([FILTER_INT])/sum(sum(row) for row in FILTER_INT)

def update(p,arr):
    """Update arr[p] to arr[q] by evolving in time."""

    # Count the average amount of each species in the neighborhood around each cell
    # by convolution with FILTER
    s = convolve(arr[p], FILTER, mode='wrap')

    # Apply the reaction equations
    q = (p+1) % 2
    arr[q,0] = s[0] + s[0]*(alpha*s[1] - gamma*s[2])
    arr[q,1] = s[1] + s[1]*(beta*s[2] - alpha*s[0])
    arr[q,2] = s[2] + s[2]*(gamma*s[0] - beta*s[1])

    # Ensure the species concentrations are kept within [0,1].
    np.clip(arr[q], 0, 1, arr[q])
    return arr

def equivalence(i, j, n):
  """Find the equivalent coordinates in the base rhombus by rotating through symmetries."""
  rot = 0
  while True:
    if ((i // n) + (j //n)) % 3 == 0:
      return i % n, j % n 
    i, j = -j - 1, i - j
    rot += 1

# Initialize the base rhombus with random amounts of A, B and C.
data = np.random.random(size=(2, 3, n, n))

# Build a 3nX3n patch based on 120-degree rhombic packing.
# This can be tiled with simple toroidal wrapping at the boundaries.
arr = np.zeros((2, 3, n * 3, n * 3))
for i in range(arr.shape[2]):
  for j in range(arr.shape[3]):
    ti, tj = equivalence(i, j, n)
    arr[:, :, i, j] = data[:, :, ti, tj]

# run the iteration
for i in range(0, niter):
  arr = update(i % 2, arr)

# Create an image from just the top nXn square, equivalent to the base rhombus
disp = np.concatenate((arr[(i + 1) % 2].transpose(2, 1, 0)[:n, :n, :], np.full((n, n, 1), 1, dtype=np.uint8)), axis=2)
img = Image.fromarray((disp * 255).astype(np.uint8), mode='RGBA')
w, h = img.size
img = img.transform((int(w * 3), int(h * math.sqrt(3))),
                    Image.Transform.AFFINE, (0.5, 0.5 / math.sqrt(3), -0.5 * w, 0, 1 / math.sqrt(3), 0),
                    resample=Image.Resampling.BICUBIC) 
img.save(imgfile)
A couple of notes: I extended the neighborhood convolution out a little to a radius-3 hex neighborhood. I guessed (and confirmed empirically) that I could get thicker spirals that way. I don't really know how the math works out.

Since the built-in convolution does not have the rotational symmetry I need, I duplicated the initial nXn data into a 3nX3n array, at which point, the patch can be tiled without any rotation. This means doing about 9x as much computation as needed. I prefer this to adding code to pad around boundaries each pass, though that would also work.

Here's a rhombus I produced with the command "python bz_rhombus.py 200 1 500 ./bzrhombus.png"
bzrhombus.png
bzrhombus.png (212.89 KiB) Viewed 654 times
It's a transparent png, so manually piecing it together in GIMP with rotations, I turned it into this hexagon, which will tile the plane without rotation.
bzhex.png
bzhex.png (581.94 KiB) Viewed 654 times
For fun I added an emboss effect on top of it. My only caveat is that this probably won't tile as well because of boundary effects in embossing. This is fixable but I would need to add some context around the image first and I didn't bother this time.
bzhexemboss.png
bzhexemboss.png (795.29 KiB) Viewed 654 times

User avatar
pcallahan
Posts: 854
Joined: April 26th, 2013, 1:04 pm

Re: 120° symmetric Belousov-Zhabotinsky patterns

Post by pcallahan » December 25th, 2022, 1:04 pm

This wasn't what I was going for but when I set the base grid to 50x50, I the symmetry produced a tessellation of the plane using three symmetrical tiles. I used GIMP to flatten out the colors. It's not very significant but could be used to produce decorative designs. The green tiles are a little fragile to cut out. They can be widened at the bridges.

I wonder if I could produce aperiodic tilings using Belousov-Zhabotinsky boundaries. (I have a half-baked idea here of mixing initial conditions and symmetry to get tiles that only match on one of three sides.)
tesselation.jpg
tesselation.jpg (177.87 KiB) Viewed 600 times
Note that the result usually doesn't split as neatly into tiles. It still makes nice wallpaper (after adjusting color, etc.) E.g.:
bzrhombus.png
bzrhombus.png (262.49 KiB) Viewed 598 times
Update: Here's one with a smaller base rhombus that makes roughly identical pieces. Again with colors flattened.
bzrhombus.png
bzrhombus.png (88.63 KiB) Viewed 590 times
I think if I want to make an aperiodic tile set like I wrote above, it's a little silly to try to generate them directly. I could just mix and match boundaries. There might be a slight issue of continuity where they join up. (M.C. Escher I'm not, but with computers I get to pretend.)

User avatar
pcallahan
Posts: 854
Joined: April 26th, 2013, 1:04 pm

Re: 120° symmetric Belousov-Zhabotinsky patterns

Post by pcallahan » December 28th, 2022, 5:27 pm

Something funny I noticed when thresholding a brightness map of a generated Belousov-Zhabotinsky pattern.
outline.jpg
outline.jpg (802.75 KiB) Viewed 535 times
if you ignore the islands and consider boundaries as graph edges, you get a degree-3 planar graph. I wonder if there's anything interesting to say about it. I know the dual map is three-colorable because I literally started out with the coloring and derived the graph from it (assume boundaries that merely "kiss" are not connected). Other than that, I don't have any great insights, but it's kind of cool looking.

Update: Purely as a solution in search of a problem, those edges are the paths you can take between spiral centers without climbing over the peaks in this embossed picture. So if you wanted to calculate shortest distance to some destination without climbing, you could run Dijkstra's on the graph, labeled with corresponding lengths.
bzmetal.jpg
bzmetal.jpg (1017.27 KiB) Viewed 527 times
When the original spirals come too close, there's a little bit of a mountain pass to climb. Hmm... maybe you get a better graph following centers, not boundaries.

User avatar
pcallahan
Posts: 854
Joined: April 26th, 2013, 1:04 pm

Re: 120° symmetric Belousov-Zhabotinsky patterns

Post by pcallahan » January 20th, 2023, 12:49 pm

Here's one cut out on two colors of card stock and coated in resin.
20230120_083021.jpg
20230120_083021.jpg (301.06 KiB) Viewed 435 times

Post Reply