Message Boards Message Boards

Mysterious beating heart patterns in totalistic cellular automata

Posted 2 years ago

static Fig. 1. Music full heart, water tiger mountain mind.

Chinese New Year will continue until the Lantern Festival tomorrow, while in the western world, Feb. 14 is a special holiday for celebrating movements of the heart. The purpose of this memo is to show how a mysterious totalistic cellular automata, even if it isn't very well-defined, can still be used for purposes other than complicated math proofs. The goal is to find forms, such as above, and to put them into beating motion.

What is a totalistic Cellular Automaton? -------

A totalistic cellular automaton is a restricted type of cellular automaton, which calculates update values based on a total taken across a set of cells. First let's calculate codes of all totalistic elementary cellular automata:

TotalisticQ[code_, col_ : 2, rad_ : 1] := With[
 {totals = Total /@ Tuples[Range[0, col - 1], 2 rad + 1]},
  And @@ (SameQ @@ IntegerDigits[code, col, col^(2 rad + 1)][[
         Position[totals, #][[All, 1]]]] & /@ Union[totals])]

Flatten[Position[TotalisticQ[#] & /@ Range[0, 255], True] - 1]
Out[]={0, 1, 22, 23, 104, 105, 126, 127, 128, 129, 150, 151, 232, 233, 254, 255}

As an example, let's see what Rule 126 looks like starting from random initial conditions:

SeedRandom["PlotFun!"];
Image[CellularAutomaton[126,
  RandomInteger[1, 100], 100],
 ImageSize -> 400]

R126

Notice that some of the triangles come to singular points, while others do not. This is an important, subtle detail if you are interested in studying Brownian motion--one of the topics from Einstein's miracle year. For now we're just having fun, not doing any serious science, but we may return to Brownian motion at a later time. It is also possible to plot the above using a slightly different call:

SeedRandom["PlotFun!"];
Image[CellularAutomaton[{6, {2, 1}},
  RandomInteger[1, 100], 100],
 ImageSize -> 400]
Out[]= (*Same as Above*)

When we compare alternative rules:

Column[MapThread[RulePlot[#1, ImageSize -> #2] &, {
   {CellularAutomaton[126], CellularAutomaton[{6, {2, 1}}]},
   {400, 200}}], Center]

rule compare

We can see that the totalistic rule on the second line is four bits shorter, but still backwards compatible with the "elementary" definition on the first line. This is a nice feature of totalistic cellular automata, because small rule spaces are easier to search for interesting behavior.

Enigmatic Searching

Presently, we're concerned with finding a growth pattern that has a heart shape (♡) boundary. To get a heart shape to grow from a simple initial condition, we need a two dimensional cellular automaton, and guess a neighborhood shape function:

HeartNeighborhood = {{-1, 1}, {1, 1}, {-1, 0}, {1, 0}, {0, 0}, {0, -1}};
Graphics[Rectangle /@ HeartNeighborhood, ImageSize -> 50]

ox horns

Okay, that looks like Ox Horns more than a heart, but let's just see what happens if we use this primitive to grow cellular automata. Next we introduce an admittedly confusing black-box code, something like this:

CAFirstArg[code_, transform_ : Identity] := {code, {4, 1}, 
 transform /@ HeartNeighborhood}

Don't ask me what this means or how it works exactly. This particular format for the first argument of the (too?) general CellularAutomaton function was shown to me in some other context, where rigor was not a high priority. Unfortunately, it's not well documented on the manual page, and RulePlot doesn't do anything either:

RulePlot[CellularAutomaton@CAFirstArg[1324123]]
Out[]:= (* nothing changes *)

[[Aside: In fact, now that I think about it, this might not even be a totalistic rule. We're not really trying to prove anything, so it doesn't exactly matter, for now. However, I am curious if anyone can explain what this code means in terms of an output equivalent, first principles implementation? If yes, perhaps we should file another bug report to add a missing value to RulePlot?]]

One thing we do know is that the number 4 indicates count of colors. The thinking behind this design choice is that four colors will give us enough degrees of freedom to create visually appealing patterns. If the rule is even totalistic (?), the number of valid codes is 4^19, where 4 is the number of colors, and 19 is the number of unique totals. Just as a quick, hackerish double check, we can try and see what happens if we call the C.A. evolution with too large a code:

CellularAutomaton[CAFirstArg[4^19], {{{1}}, 0}, 1]
Print:>CellularAutomaton::rsize: The specified rule number 274877906944 
              is greater than the largest possible rule number (274877906943).

Thanks to nice error handling, we now know that codes must be integers between 0 and 4^19-1. This makes us a little more confident that it is a totalistic code (as we have been told by relevant authorities). Still a nagging doubt persists, as if "don't know" mind is never okay.

With so many possible codes, we can't do a simple brute force search. Fortunately for us, we don't immediately need Machine Learning or Genetic Algorithms. It is relatively easy to find heart shapes just searching randomly. Here is a basic, entry-level search function:

SearchCA[ntime_] := Quiet[With[{RandDat = DeleteCases[
      Rule[Show[#, ImageSize -> 200] &@Image[CellularAutomaton[
             CAFirstArg[#, Reverse], {{{1}}, 0}, {{{ntime}}}]/3],
         #] & /@ RandomInteger[{0, 4^19 - 1}, 100],  Rule[Image[{}], _]]},
   Select[RandDat, 10 < ImageDimensions[#[[1]]][[1]] < 2 ntime &]]]

The purpose of the selector at the end is to limit the search to smaller outputs, which seem to do a better job of exploring all possible boundary shapes. If the cutoff is changed to 5 or larger, many more results will be returned, mostly with a pentagonal shape reminiscent of baseball's home plate. Pentagonal cellular automata could be interesting in their own right, but for now lets see what we can find that look like hearts:

ResourceFunction["InteractiveListSelector"][SearchCA[25]]
Out[]=(* clickable search panel *)

It may take numerous searches to find exactly what you're looking for, but it's not too difficult to find heart-shaped candidates. When you do, just click the add button beneath interesting examples, then click copy and the codes go to the clipboard. Codes can then be pasted into curly brackets with ctrl-v. Here's the results of taking all from an example search:

search results

The case 134500106171 looks pretty well heart-shaped, as does the last element in the table 60598807586. Let's look at the evolution over time to see how the heart shape comes into existence as a time series:

Grid[Partition[  Image[# /. {0 -> {1, 1, 0}, 1 -> {1, 0, 0}, 2 -> {0, 1, 0}, 
       3 -> {0, 0, 1}},
     ImageSize -> 3 Reverse[Dimensions[#]]] & /@ CellularAutomaton[
    CAFirstArg[134500106171, Reverse], {{{1}}, 0}, {{0, 30}}], 5],
 Frame -> All, FrameStyle -> Directive[Lighter[Gray, 0.5], Thick]]

evolution

This is very nice already, and the tenth image, though small, also has quite a nice form. Even though we don't exactly know how this rule works, let's move on and possibly circle back to answer that question later. Instead of having an empty heart and a mind full of incomplete proofs, what we really need is a mind empty of doubt and a heart full of color (not to mention caring, compassion, etc.).

Building the Animation

After a little more searching I found another code, which produces really nice results: 113724623751. Let's see what this looks like for a few frames around t=25, and this time we are using a "sunset palette" with warm heart colors:

Grid[Partition[With[{glist = Image[# /. MapThread[Rule, {
           SortBy[Tally[Flatten[#]], -#[[2]] &][[All, 1]],
           {White, Blend[{Orange, Red}, .6], 
            Blend[{Magenta, Red}, .25], Purple}
           }], ImageSize -> 4 Reverse[Dimensions[#]]] & /@ 
      CellularAutomaton[CAFirstArg[113724623751, Reverse],
 {{{1}}, 0}, {{22, 30 }}]}, glist], 3], Frame -> All, 
 FrameStyle -> Directive[LightGray, Thick]]

Heart grid

If you look more closely, the colors actually oscillate with period 3. We have hacked around this issue by sorting C.A. values according to their frequency statistics, thus attaining consistency between frames. With sorted colors, these outputs look pretty well like hearts. We can make them even more convincing with extra styling layers.

The basic idea is to separate colors, treat each vertex as a graph node, and draw in extra edges using the Nearest function. This is a design choice, which ultimately gives the images more texture, depth, and room to breathe. These next few functions are a bit of messy magic, but as they say, ends justify means.

GraphPattern[CA_, sort_, ColSets_, thick_ : 1/2, cut_ : Infinity] :=  Function[
   {NNEdges, NNNEdges}, Graphics[{
     Thickness[.005 thick],
     Transpose[{Reverse@{Purple, Blend[{Magenta, Red}, .25], 
         Blend[{Orange, Red}, .6]},
       Reverse@Map[Line[{#[[1]], #[[2]]}] &, NNNEdges, {2}]}],
     Thickness[.01 thick],
     Transpose[{{Purple, Blend[{Magenta, Red}, .25], 
        Blend[{Orange, Red}, .6]},
       Map[Line[{#[[1]], #[[2]]}] &, NNEdges, {2}]}],
     EdgeForm[None],
     Transpose[{{Purple, Blend[{Magenta, Red}, .25], 
        Blend[{Orange, Red}, .6]},
       Map[Disk[#, 1/3 thick] &, ColSets, {2}]}]}]][
  Function[{oneCol}, Union[Flatten[
      Select[
        Edge[#, Nearest[Complement[oneCol, {#}], #]] & /@ oneCol,
        EuclideanDistance[#[[1]], #[[2, 1]]] == 1 &
        ] /. Edge[or_, ylist_] :> (Edge[or, #] & /@ ylist), 1]]] /@ 
   ColSets,
  Function[{oneCol}, Union[Flatten[
      Select[
        Function[{pt}, 
          Edge[pt, Nearest[Complement[oneCol, pt + # & /@ {
               {1, 0}, {0, 0}, {-1, 0}, {0, 1}, {0, -1}}], pt]]
          ] /@ oneCol, EuclideanDistance[#[[1]], #[[2, 1]]] <= cut &
        ] /. Edge[or_, ylist_] :> (Edge[or, #] & /@ ylist), 1]]] /@ 
   ColSets]

LayeredShieldGraph[CA_, sort_] := With[{BigColSets = Function[{oneCol},
        # + {1/2, 1/2} & /@ 
         Select[oneCol, Length[Intersection[oneCol,
              Function[{off}, # + off] /@ Tuples[{0, 1}, 2] ]] == 
            4 &]
        ][Position[CA, #]] & /@ sort},
  Show[GraphPattern[CA, sort, Complement[Position[CA, #],
       Flatten[Function[{or}, or + # & /@ Tuples[{1/2, -1/2}, 2]
          ] /@ Flatten[BigColSets, 1], 1]] & /@ sort],
   GraphPattern[CA, sort, BigColSets, 1/2, 3]]]

LayeredShieldGraph[code_, time_, sort_] := 
 With[{CA = CellularAutomaton[
     {code, {4, 
       1}, {{0, 0}, {0, -1}, {-1, 0}, {1, 0}, {0, 1}, {0, 2}}
      }, {{{1}}, 0}, {{{time }}}]}, LayeredShieldGraph[CA, sort]]

Now the same data as above looks as follows:

Grid[data = Partition[With[{glist = LayeredShieldGraph[#,
         SortBy[Tally[Flatten[#]], #[[2]] &][[1 ;; 3, 1]]] & /@ 
       CellularAutomaton[
        CAFirstArg[113724623751, Times[#, {1, -1}] &],
        {{{1}}, 0}, {{22, 30 }}]}, glist], 3], Frame -> All,
 FrameStyle -> Directive[LightGray, Thick]]

heart graph grid

The view function does a little bit of extra work to introduce emptiness where numeric primitives have solid color. This works to our advantage, allowing us to enrich the pallette by stacking successive frames while fading to white:

LayeredHearts[code_, init_, final_, part_, fade_] := Show[
     Reverse[MapIndexed[#1 /. RGBColor[x_, y_, z_] :> Lighter[
          RGBColor[x, y, z], fade*( #2[[1]] - 1)] &, Reverse[#]]]] & /@
   Partition[With[
    {glist = LayeredShieldGraph[#, SortBy[Tally[Flatten[#]], #[[2]] &
           ][[1 ;; 3, 1]]] & /@ 
       CellularAutomaton[CAFirstArg[code, Times[#, {1, -1}] &],
        {{{1}}, 0}, {{init, final }}]}, glist], part, 1]

Grid[Partition[LayeredHearts @@ {113724623751, 22, 32, 3, 0.35}, 3], 
 Frame -> All, FrameStyle -> Directive[LightGray, Thick], 
 Background -> {None, None, {{3, 1} -> LightBlue}}]

Layered Graph Grid

In the context of a time dynamic animation, the highlighted frame bottom left will be the beating hearts resting shape. The animation will cycle through surrounding frames according to a pulse shape that expands quickly, contracts for a slightly longer time interval, and ultimately returns to rest before repeating again:

PulseShape = {7, 7, 7, 8, 7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7};

AnimationData = Map[ Show[#, PlotRange -> {{-5, 63}, {-1, 51}}, ImageSize -> 620] &,
   Part[LayeredHearts @@ {113724623751, 22, 32, 3, 0.35}, 
    PulseShape]];

ListAnimate[AnimationData];

Beating Heart  1

The feature I like about this particular plot is the purple Celtic cross occurring in the still frame, but I don't expect everyone to agree with me on preferences. If some other reader dislikes the purple cross, or more likely, desires some other boundary shape, that person can hopefully understand how to produce similar animations by changing only a few values: code, time window, stacking depth, and fade rate. Here is one more layout for example, or you can try to find your own:

Grid[Partition[LayeredHearts @@ {154655395227, 23, 35, 4, 0.25}, 3], 
 Frame -> All,
 FrameStyle -> Directive[LightGray, Thick], 
 Background -> {None, None, {{3, 1} -> LightBlue}}]

Layered graph 2

And the corresponding animation is:

AnimationData2 = Map[
   Show[#, PlotRange -> {{-5, 41}, {-3, 32}}, ImageSize -> 620] &,
   Part[LayeredHearts @@ {154655395227, 23, 35, 4, 0.25}, 
    PulseShape]];

ListAnimate[AnimationData2]

beating heart 2

Happy Valentine's day... Don't forget the heart has it's own language entirely separate from what we think to say or to program in the computer!

Acknowledgements

The special form for C.A. first argument was known of by Stephen Wolfram, who told it to me at a meeting called by Danielle, thanks again!

POSTED BY: Brad Klee
4 Replies

enter image description here -- you have earned Featured Contributor Badge enter image description here Your exceptional post has been selected for our editorial column Staff Picks http://wolfr.am/StaffPicks and Your Profile is now distinguished by a Featured Contributor Badge and is displayed on the Featured Contributor Board. Thank you!

POSTED BY: EDITORIAL BOARD

What else would we expect on Valentines Day. Thanks Brad!

POSTED BY: Keith Stendall

This is an incredible project!

POSTED BY: Joel Dietz

Brad, nice exploration.

I am not sure if you are being playful when you say you don't understand the arguments to the CellularAutomaton function.

I wouldn't use the offsets in the radius, but instead use weights, e.g., CellularAutomaton[{113724623751, {4, {{1,0,1},{1,1,1},{0,1,0}}},{1,1}}] or

ArrayPlot /@ 
 CellularAutomaton[{113724623751, {4, {{1, 0, 1}, {1, 1, 1}, {0, 1, 
      0}}}, {1, 1}}, {{{1}}, 0}, {{25, 30}}]

and it seems that you have the neighborhood upside down.

POSTED BY: Todd Rowland
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract