diff --git a/content/07_ca.html b/content/07_ca.html index 8b7c4739..8ab4a382 100644 --- a/content/07_ca.html +++ b/content/07_ca.html @@ -39,89 +39,86 @@
I have a line of cells, each with an initial state, and each with two neighbors. The exciting thing is, even with this simplest CA imaginable, the properties of complex systems can emerge. But I haven’t yet discussed perhaps the most important detail of how cellular automata work: change over time.
+I’m not really talking about real-world time here, but rather about the CA developing across a series of discrete time steps, which could also be called a generations. In the case of a CA in p5.js, time will likely be tied to the frame count of the animation. The question, as depicted in Figure 7.5, is this: given the states of the cells at time equals 0 (or generation 0), how do I compute the states for all cells at generation 1? And then how do I get from generation 1 to generation 2? And so on and so forth.
-I have a line of cells, each with an initial state, and each with two neighbors. The exciting thing is, even with this simplest CA imaginable, the properties of complex systems can emerge. But I haven’t yet discussed perhaps the most important detail of how cellular automata work: time.
-I’m not really talking about real-world time here, but rather about the CA developing across a series of discrete time steps, which could also be called a generations. In the case of a CA in p5.js, time will likely be tied to the frame count of the animation. The question, as depicted in Figure 7.6, is this: given the states of the cells at time equals 0 (or generation 0), how do I compute the states for all cells at generation 1? And then how do I get from generation 1 to generation 2? And so on and so forth.
-Let’s say there’s an individual cell in the CA called \text{cell}. The formula for calculating the cell’s state at any given time t (\text{cell}_t) is as follows:
In other words, a cell’s new state is a function of all the states in the cell’s neighborhood at the previous generation (time t-1). A new state value is calculated by looking at the previous generation’s neighbor states (Figure 7.7).
+In other words, a cell’s new state is a function of all the states in the cell’s neighborhood at the previous generation (time t-1). A new state value is calculated by looking at the previous generation’s neighbor states (Figure 7.6).
There are many ways to compute a cell’s state from its neighbors’ states. Consider blurring an image. (Guess what? Image processing works with CA-like rules!) A pixel’s new state (its color) is the average of its neighbors’ colors. Similarly, a cell’s new state could be the sum of all of its neighbors’ states. However, in Wolfram’s elementary CA, the process takes a different approach: instead of mathematical operations, new states are determined by predefined rules that account for every possible configuration of a cell and its neighbors. These rules are known collectively as a ruleset.
-This approach might seem ridiculous at first—wouldn’t there be way too many possibilities for it to be practical? Well, let’s give it a try. A neighborhood consists of three cells, each with a state of 0 or 1. How many possible ways can the states in a neighborhood be configured? A quick way to figure this out is to think of each neighborhood configuration as a binary number. Binary numbers use “base 2,” meaning they’re represented with only two possible digits (0 and 1). In this case, each neighborhood configuration corresponds to a 3-bit number, and how many values can you represent with 3 bits? Eight, from 0 (000) up to 7 (111). Figure 7.8 shows how.
+This approach might seem ridiculous at first—wouldn’t there be way too many possibilities for it to be practical? Well, let’s give it a try. A neighborhood consists of three cells, each with a state of 0 or 1. How many possible ways can the states in a neighborhood be configured? A quick way to figure this out is to think of each neighborhood configuration as a binary number. Binary numbers use “base 2,” meaning they’re represented with only two possible digits (0 and 1). In this case, each neighborhood configuration corresponds to a 3-bit number, and how many values can you represent with 3 bits? Eight, from 0 (000) up to 7 (111). Figure 7.7 shows how.
-Once all the possible neighborhood configurations are defined, an outcome (new state value: 0 or 1) is specified for each configuration, as in Figure 7.9.
+Once all the possible neighborhood configurations are defined, an outcome (new state value: 0 or 1) is specified for each configuration. In Wolfram's original notation and other common references, these configurations are written in descending order, Figure 7.8 follows this convention, starting with 111 and counting down to 000.
Keep in mind that unlike the sum or averaging methods, the rulesets in elementary CA don’t follow any arithmetic logic—they’re just arbitrary mappings of inputs to outputs. The input is the current configuration of the neighborhood (one of eight possibilities), and the output is the next state of the middle cell in the neighborhood (0 or 1—it’s up to you to define the rule).
Once you have a ruleset, you can set the cellular automaton in motion. The standard Wolfram model is to start generation 0 with all cells having a state of 0 except for the middle cell, which should have a state of 1. You can do this with any size (length) grid, but for clarity, I’ll use a one-dimensional CA of nine cells so that the middle is easy to pick out.
-Based on the ruleset in Figure 7.9, how do the cells change from generation 0 to generation 1? In Figure 7.11, I’ve shown how the center cell, with a neighborhood of 010, switches from a 1 to a 0. Try applying the ruleset to the remaining cells to fill in the rest of the generation 1 states.
+Based on the ruleset in Figure 7.8, how do the cells change from generation 0 to generation 1? Figure 7.10 shows how the center cell, with a neighborhood of 010, switches from a 1 to a 0. Try applying the ruleset to the remaining cells to fill in the rest of the generation 1 states.
-Now for a slight change: instead of representing the cells’ states with 0s and 1s, I’ll indicate them with visual cues—white for 0 and black for 1 (see FIgure 7.12). Although this might seem counterintuitive, as 0 usually signifies black in computer graphics, I’m using this convention because the examples in this book have a white background, so “turning on” a cell corresponds to switching its color from white to black.
+Now for a slight change: instead of representing the cells’ states with 0s and 1s, I’ll indicate them with visual cues—white for 0 and black for 1 (see Figure 7.11). Although this might seem counterintuitive, as 0 usually signifies black in computer graphics, I’m using this convention because the examples in this book have a white background, so “turning on” a cell corresponds to switching its color from white to black.
-With this switch from numerical representations into visual forms, the fascinating dynamics and patterns of cellular automata will come into view! To see them even more clearly, instead of drawing one generation at a time, I’ll also start stacking the generations, with each new generation appearing below the previous one, as shown in Figure 7.13.
+With this switch from numerical representations into visual forms, the fascinating dynamics and patterns of cellular automata will come into view! To see them even more clearly, instead of drawing one generation at a time, I’ll also start stacking the generations, with each new generation appearing below the previous one, as shown in Figure 7.12.
-The low-resolution shape that emerges in Figure 7.13 is the Sierpiński triangle. Named after the Polish mathematician Wacław Sierpiński, it’s a famous example of a fractal. I’ll examine fractals more closely in the next chapter, but briefly, they’re patterns where the same shapes repeat themselves at different scales. To give you a better sense of this, Figure 7.14 shows the CA over several more generations, and with a wider grid size.
+The low-resolution shape that emerges in Figure 7.12 is the Sierpiński triangle. Named after the Polish mathematician Wacław Sierpiński, it’s a famous example of a fractal. I’ll examine fractals more closely in the next chapter, but briefly, they’re patterns where the same shapes repeat themselves at different scales. To give you a better sense of this, Figure 7.13 shows the CA over several more generations, and with a wider grid size.
-And Figure 7.15 shows the CA again, this time with cells that are just a single pixel wide so the resolution is much higher.
+And Figure 7.14 shows the CA again, this time with cells that are just a single pixel wide so the resolution is much higher.
Take a moment to let the enormity of what you’ve just seen sink in. Using an incredibly simple system of 0s and 1s, with little neighborhoods of three cells, I was able to generate a shape as sophisticated and detailed as the Sierpiński triangle. This is the beauty of complex systems.
-Of course, this particular result didn’t happen by accident. I picked the set of rules in Figure 7.9 because I knew the pattern it would generate. The mere act of defining a ruleset doesn’t guarantee visually exciting results. In fact, for a one-dimensional CA where each cell can have two possible states, there are exactly 256 possible rulesets to choose from, and only a handful of them are on par with the Sierpiński triangle. How do I know there are 256 possible rulesets? It comes down to a little more binary math.
+Of course, this particular result didn’t happen by accident. I picked the set of rules in Figure 7.8 because I knew the pattern it would generate. The mere act of defining a ruleset doesn’t guarantee visually exciting results. In fact, for a one-dimensional CA where each cell can have two possible states, there are exactly 256 possible rulesets to choose from, and only a handful of them are on par with the Sierpiński triangle. How do I know there are 256 possible rulesets? It comes down to a little more binary math.
Take a look back at Figure 7.8 and notice again how there are eight possible neighborhood configurations, from 000 to 111. These are a ruleset’s inputs, and they remain constant from ruleset to ruleset. It’s only the outputs that vary from one ruleset to another—the individual 0 or 1 paired with each neighborhood configuration. Figure 7.9 represented a ruleset entirely with 0s and 1s. Now Figure 7.16 shows the same ruleset visualized with white and black squares.
+Take a look back at Figure 7.7 and notice again how there are eight possible neighborhood configurations, from 000 to 111. These are a ruleset’s inputs, and they remain constant from ruleset to ruleset. It’s only the outputs that vary from one ruleset to another—the individual 0 or 1 paired with each neighborhood configuration. Figure 7.9 represented a ruleset entirely with 0s and 1s. Now Figure 7.16 shows the same ruleset visualized with white and black squares.
-Since the eight possible inputs are the same no matter what, a potential shorthand for indicating a ruleset is to specify just the outputs, writing them as a sequence of eight 0s or 1s—in other words, an 8-bit binary number. For example, the ruleset in Figure 7.16 could be written as 01011010. The 0 on the right corresponds to input configuration 000, the 1 next to it corresponds to input 001, and so on. On Wolfram’s website, CA rules are illustrated using a combination of this binary shorthand an Figure 7.16’s black-and-white square representation, yielding depictions like Figure 7.17.
+Since the eight possible inputs are the same no matter what, a potential shorthand for indicating a ruleset is to specify just the outputs, writing them as a sequence of eight 0s or 1s—in other words, an 8-bit binary number. For example, the ruleset in Figure 7.15 could be written as 01011010. The 0 on the right corresponds to input configuration 000, the 1 next to it corresponds to input 001, and so on. On Wolfram’s website, CA rules are illustrated using a combination of this binary shorthand and the black-and-white square representation, yielding depictions like Figure 7.16.
-A ruleset can be reduced to an 8-bit number. And how many combinations of eight 0s and 1s are there? Exactly 2^8, or 256. You might remember this from when you first learned about RGB color in p5.js. When you write background(r, g, b)
, each color component (red, green, and blue) is represented by an 8-bit number ranging from 0 to 255 in decimal, or 00000000 to 11111111 in binary. And speaking of decimal, an even shorter shorthand for a ruleset is to convert its 8-bit binary number to decimal (or “base 10”). “Rule 01011010” is therefore more commonly known as “rule 90.”
Figure 7.16 is identified by Wolfram as “Rule 90.” Where does 90 come from? Each ruleset is essentially an 8-bit number. How many combinations of eight 0s and 1s are there? Exactly 2^8, or 256. You might remember this from when you first learned about RGB color in p5.js. When you write background(r, g, b)
, each color component (red, green, and blue) is represented by an 8-bit number ranging from 0 to 255 in decimal, or 00000000 to 11111111 in binary.
To make ruleset naming even more concise, Wolfram uses decimal (or “base 10”) representations. To name a rule, you convert its 8-bit binary number to its decimal counterpart. The binary number “01011010” translates to the decimal number 90, and therefore it’s named “Rule 90.”
Since there are 256 possible combinations of eight 0s and 1s, there are also 256 unique rulesets. Let’s check out another one. How about rule 11011110, or more commonly, rule 222. Figure 7.18 shows how it looks.
-The result is a recognizable shape, though it certainly isn’t as exciting as the Sierpiński triangle. As I saidd earlier, most of the 256 elementary rulesets dodn’t produce compelling outcomes. However, it’s still quite incredible that even just a few of these rulesets—simple systems of cells with only two possible states—can produce fascinating patterns seen every day in nature (see Figure 7.19, a snail shell resembling Wolfram’s rule 30). This demonstrates how valuable CAs can be in simulation and pattern generation.
+The result is a recognizable shape, though it certainly isn’t as exciting as the Sierpiński triangle. As I saidd earlier, most of the 256 elementary rulesets dodn’t produce compelling outcomes. However, it’s still quite incredible that even just a few of these rulesets—simple systems of cells with only two possible states—can produce fascinating patterns seen every day in nature (see Figure 7.18, a snail shell resembling Wolfram’s rule 30). This demonstrates how valuable CAs can be in simulation and pattern generation.
Before I go too far down the road of characterizing the results of different rulesets, though, let’s look at how to build a p5.js sketch that generates and visualizes a Wolfram elementary CA.
You may be thinking: “OK, I’ve got this cell thing. And the cell thing has some properties, like a state, what generation it’s on, who its neighbors are, and where it lives pixel-wise on the screen. And maybe it has some functions, like to display itself and determine its new state.” This line of thinking is an excellent one and would likely lead you to write some code like this:
@@ -130,10 +127,10 @@However, this isn’t the road I want to travel down right now. Later in this chapter, I’ll discuss why an object-oriented approach could prove valuable in developing a CA simulation, but to begin, it’s easier to work with a more elementary data structure. After all, what is an elementary CA but a list of 0s and 1s? Why not describe a generation of a one-dimensional CA using an array?
let cells = [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0];-
This array corresponds to the row of cells shown in Figure 7.20.
+This array corresponds to the row of cells shown in Figure 7.19.
To show that array, I check if each element is a 0 or a 1, choose a fill color accordingly, and draw a rectangle.
//{!1} Loop through every cell. @@ -217,10 +214,10 @@Programming an Elementary CA
I’m almost done, but I still need to define
rules()
, the function that computes the new state value based on the neighborhood (left, middle, and right cells). I know the function needs to return an integer (0 or 1), as well as receive three arguments (for the three neighbors).//{!1} Function signature: receives 3 ints and returns 1. function rules (a, b, c) { return _______ }-There are many ways to write this function, but I’d like to start with a long-winded one that will hopefully provide a clear illustration of what’s happening. First, I need to establish how I’ll store the ruleset. Recall that a ruleset is a series of 8 bits (0 or 1) defining the outcome for every possible neighborhood configuration. If you need to refresh your memory, Figure 7.21 shows a visual representation of the Sierpiński triangle ruleset.
+There are many ways to write this function, but I’d like to start with a long-winded one that will hopefully provide a clear illustration of what's happening. How shall I store the ruleset? Remember that a ruleset is a series of 8 bits (0 or 1) that define the outcome for every possible neighborhood configuration. If you need a refresher, Figure 7.20 shows the Wolfram notation for the Sierpiński triangle ruleset, along with the corresponding 0s and 1s listed in order. This should give you a hint as to the data structure I have in mind!
I can store this ruleset in an array.
let ruleset = [0, 1, 0, 1, 1, 0, 1, 0];@@ -292,10 +289,10 @@Programming an Elementary CA
}
This is great, but there’s still one more missing piece: what good is a cellular automaton if you can’t see it?
The standard technique for drawing an elementary CA is to stack the generations one on top of the other, and to draw each cell as a square that’s black (for state 1) or white (for state 0), as in Figure 7.22. Before implementing this particular visualization, however, I’d like to point out two things.
+The standard technique for drawing an elementary CA is to stack the generations one on top of the other, and to draw each cell as a square that’s black (for state 1) or white (for state 0), as in Figure 7.21. Before implementing this particular visualization, however, I’d like to point out two things.
First, this visual interpretation of the data is completely literal. It’s useful for demonstrating the algorithms and results of Wolfram’s elementary CA, but it shouldn’t necessarily drive your own personal work. It’s rather unlikely that you’re building a project that needs precisely this algorithm with this visual style. So while learning to draw a CA in this way will help you understand and implement CA systems, this skill should exist only as a foundation.
Second, the fact that a one-dimensional CA is visualized with a two-dimensional image can be misleading. It’s very important to remember that this is not a 2D CA. I’m simply choosing to show a history of all the generations stacked vertically. This technique creates a two-dimensional image out of many instances of one-dimensional data, but the system itself is one-dimensional. Later, I’ll show you an actual 2D CA (the Game of Life), and I’ll cover how to visualize such a system.
@@ -304,7 +301,7 @@Assuming the canvas is 640 pixels wide, that means the CA will have 64 cells. Of course, I can calculate this value dynamically when I initialize the cells
array in setup()
.
//{!1} How many cells fit across given a certain width let cells = new Array(floor(width / w));-
Drawing the cells now involves iterating over the array and drawing a white square when the state equals 1 or a black when the state equals 0.
+Drawing the cells now involves iterating over the array and drawing a square based on the state of each cell.
for (let i = 0; i < cells.length; i++) { //{!2} By multiplying the cell state the result is 0 or 255 fill(cells[i] * 255); @@ -312,8 +309,8 @@-Drawing an Elementary CA
// 0, 10, 20, 30, all the way to 640. square(i * w, 0, w); }
In truth, I could simplify the code by having a black background and only drawing when there is a white cell (saving the work of drawing many squares), but in most cases this solution is good enough (and necessary for other more sophisticated designs with varying colors.) I’ll also note that if the size of each cell were 1 pixel I would not want to use p5.js’s square()
function, but rather access the pixel array directly.
Right now the y-position for each square is hardcoded to 0. If I want the generations to be stacked on top of each other, with each row of cells marking a new generation, I’ll also need to calculate a y-position based on the generation number. I could accomplish this by adding a generation
variable and incrementing it each time through draw()
. With these additions, I can now look at the entire sketch.
There are two things off about this code. First, when multiplying the state by 255, cells with a state of 1 will be black and those with 0 will be white, which is the opposite of what I originally intended! While this is of course “ok” since the color representation is arbitrary, I’ll correct this in the full example.
+The more pressing issue is that the y-position for each square is hardcoded to 0. If I want the generations to be stacked on top of each other, with each row of cells marking a new generation, I’ll also need to calculate a y-position based on the generation number. I can accomplish this by adding a generation
variable and incrementing it each time through draw()
. With these additions, I can now look at the entire sketch.