diff --git a/content/04_particles.html b/content/04_particles.html index 66ae2c6b..73b9a272 100644 --- a/content/04_particles.html +++ b/content/04_particles.html @@ -67,7 +67,7 @@

A Single Particle

} } -

This is about as simple as a particle can get. From here, I could take the particle in several directions. I could add the applyForce() method to affect the particle’s behavior (I’ll do precisely this in a future example). I could also add variables to describe color and shape, or load a p5.Image to draw the particle in a more interesting way. For now, however, I’ll focus on adding just one additional detail: life span.

+

This is about as simple as a particle can get. From here, I could take the particle in several directions. I could add the applyForce() method to affect the particle’s behavior (I’ll do precisely this in a future example). I could also add variables to describe color and shape, or load a p5.Image to draw the particle in a more interesting way. For now, however, I’ll focus on adding just one additional detail:
life span.

Some particle systems involve an emitter that serves as the source of the particles. The emitter controls the initial settings for the particles: position, velocity, and more. It might emit a single burst of particles, a continuous stream of particles, or some variation thereof. The new feature here is that particles born at the emitter can’t live forever. If they did, the p5.js sketch would eventually grind to a halt as the particles add up to an unwieldy number over time. As new particles are born, old particles need to be removed, creating the illusion of an infinite stream of particles without hurting the performance of the sketch.

There are many ways to decide when a particle is ready to be removed. For example, it could “die” when it comes into contact with another object or when it leaves the frame of the canvas. For now, I’ll choose to give particles a lifespan variable that acts like a timer. It will start at 255 and count down to 0 as the sketch progresses, at which point the particle will be considered dead. Here’s the added code in the Particle class:

class Particle {
@@ -678,7 +678,7 @@ 

Inheritance Basics

eat() { // Call eat() from Animal. A child can execute a function from the parent. super.eat(); - // Additional code for a dog’s specific eating characteristics. + // Additional code for a Dog object’s specific eating characteristics. print("Woof!!!"); } diff --git a/content/05_steering.html b/content/05_steering.html index 616d0aa6..d22c30af 100644 --- a/content/05_steering.html +++ b/content/05_steering.html @@ -68,7 +68,7 @@

The Steering Force

Figure 5.1: A vehicle with a velocity and a target
Figure 5.1: A vehicle with a velocity and a target
-

The vehicle’s goal and subsequent action is to seek the target. Thinking back to Chapter 2, you might begin by making the target an attractor and applying a gravitational force that pulls the vehicle to the target. This would be a perfectly reasonable solution, but conceptually it’s not what I’m looking for here.

+

The vehicle’s goal and subsequent action is to seek the target. Thinking back to Chapter 2, you might begin by making the target an attractor and applying a gravitational force that pulls
the vehicle to the target. This would be a perfectly reasonable solution, but conceptually it’s not what I’m looking
for here.

I don’t want to simply calculate a force that pushes the vehicle toward its target; rather, I want to ask the vehicle to make an intelligent decision to steer toward the target based on its perception of its own state (its speed and the direction in which it’s currently moving) and its environment (the location of the target). The vehicle should consider how it desires to move (a vector pointing to the target), compare that goal with how it’s currently moving (its velocity), and apply a force accordingly. That’s exactly what Reynolds’s steering force formula says:

\text{steering force} = \text{desired velocity} - \text{current velocity}

Or, as you might write in p5.js:

diff --git a/content/07_ca.html b/content/07_ca.html index f3734065..cf7f7061 100644 --- a/content/07_ca.html +++ b/content/07_ca.html @@ -206,9 +206,7 @@

Programming an Elementary CA

I need to fix one more problem before this is done, and identifying it is absolutely fundamental to the techniques behind programming CA simulations. The bug is subtle and won’t trigger an error; the CA just won’t perform correctly. It all lies in this line of code:

  cells[i] = newstate;

This may seem perfectly innocent. After all, once I’ve computed a new state value, I want to assign the cell its new state. But think about the next iteration of the for loop. Let’s say the new state for cell 5 was just computed, and the loop is moving on to cell 6. What happens next?

- +

Cell 6, generation 1 = a function of states for cell 5, cell 6, and cell 7 at generation 0

A cell’s new state is a function of the previous neighbor states, so in this case, the value of cell 5 at generation 0 is needed in order to calculate cell 6’s new state at generation 1. Have I saved cell 5’s value at generation 0? No! Remember, this line of code was just executed for i equals 5:

  cells[i] = newstate;

Once this happens, cell 5’s state at generation 0 is gone; cells[5] is now storing the value for generation 1. I can’t overwrite the values in the array while I’m processing the array, because I need those values to calculate the new values!

@@ -267,7 +265,8 @@

Programming an Elementary CA

  // Invert the index so 0 becomes 7, 1 becomes 6, and so on.
   return ruleset[7 - index];

I now have everything needed to compute the generations for a Wolfram elementary CA. Here’s how the code looks all together:

-
//{!1} Array for the cells
+
+
//{!1} Array for the cells
 let cells = [];
 //{!1} Arbitrarily start with rule 90.
 let ruleset = [0, 1, 0, 1, 1, 0, 1, 0];
@@ -299,6 +298,7 @@ 

Programming an Elementary CA

let index = parseInt(s, 2); return ruleset[7 - index]; }
+

This is great, but one more piece is still missing: What good is a CA if you can’t see it?

Drawing an Elementary CA

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.

@@ -582,6 +582,7 @@

The Implementation

} // Correct by subtracting the cell state. neighborSum -= board[i][j]; + //{!1} The rules of life! if (board[i][j] === 1 && neighborSum < 2) next[i][j] = 0; //{.continue} diff --git a/content/08_fractals.html b/content/08_fractals.html index f4cb5d82..dbb05bea 100644 --- a/content/08_fractals.html +++ b/content/08_fractals.html @@ -222,8 +222,10 @@

Drawing the Cantor Set with Recur
Figure 8.10: Two generations of lines drawn with the Cantor set rules

This works over two generations, but continuing to manually call line() will quickly become unwieldy. For the succeeding generations, I’d need 4, then 8, then 16 calls to line(). A for loop is the usual way around such a problem, but give that a try and you’ll see that working out the math for each iteration quickly proves inordinately complicated. Don’t despair, however: here’s where recursion comes to the rescue!

-

Look at where I draw the first line of the second generation, from the start to the one-third mark:

-
  line(x, y + 20, x + length / 3, y + 20);
+
+

Look at where I draw the first line of the second generation, from the start to the one-third mark:

+
  line(x, y + 20, x + length / 3, y + 20);
+

Instead of calling the line() function directly, why not call the cantor() function? After all, what does the cantor() function do? It draws a line at an (x, y) position with a given length. The x value stays the same, y increments by 20, and the length is length / 3:

  cantor(x, y + 20, length / 3);

This call to cantor() is precisely equivalent to the earlier call to line(). And for the next line in the second generation, I can call cantor() again:

@@ -296,8 +298,9 @@

The Monster Curve

}

Now that I have the KochLine class, I can get started on setup() and draw(). I’ll need a data structure to keep track of what will eventually become many KochLine objects, and a JavaScript array will do just fine (see Chapter 4 for a review of arrays):

let segments = [];
-

In setup(), I’ll want to add the first line segment to the array, a line that stretches from 0 to the width of the canvas:

-
function setup() {
+
+

In setup(), I’ll want to add the first line segment to the array, a line that stretches from 0 to the width of the canvas:

+
function setup() {
   createCanvas(640, 240);
   // Left side of the canvas
   let start = createVector(0, 200);
@@ -306,6 +309,7 @@ 

The Monster Curve

//{!1} The first KochLine object segments.push(new KochLine(start, end)); }
+

Then in draw(), all KochLine objects (just one for now) can be rendered with a for...of loop:

function draw() {
   background(255);
@@ -465,8 +469,10 @@ 

The Deterministic Version

I touched on transformations in Chapter 3. They’re a set of functions, such as translate(), rotate(), scale(), push(), and pop(), that allow you to change the position, orientation, and scale of shapes in your sketch. The translate() function moves the coordinate system, rotate() rotates it, and push() and pop() help save and restore the current transformation state. If you aren’t familiar with these functions, I have a set of videos on transformations in p5.js available at the Coding Train website.

I’ll begin by drawing a single branch, the trunk of the tree. Since I’m going to be using the rotate() function, I need to make sure I’m continuously translating along the branches while drawing. Remember, when you rotate in p5.js, you’re always rotating around the origin, or point (0, 0), so here the origin must always be translated to the start of the next branch being drawn (equivalent to the end of the previous branch). Since the trunk starts at the bottom of the window, I first have to translate to that spot:

translate(width / 2, height);
-

Then I can draw the trunk as a line upward:

-
line(0, 0, 0, -100);
+
+

Then I can draw the trunk as a line upward:

+
line(0, 0, 0, -100);
+

Once I’ve drawn the line, I must translate to the end of that line and rotate in order to draw the next branch, as demonstrated in Figure 8.18. (Eventually, I’m going to need to package up what I’m doing right now into a recursive function, but I’ll sort out the individual steps first.)

Figure 8.18: The process of drawing a line, translating to the end of the line, and rotating by an angle @@ -482,8 +488,9 @@

The Deterministic Version

Figure 8.19: After “popping” back, a new branch is rotated to the left.
Figure 8.19: After “popping” back, a new branch is rotated to the left.
-

Here’s all the code together:

-
translate(width / 2, height);
+
+

Here’s all the code together:

+
translate(width / 2, height);
 //{!1} The root
 line(0, 0, 0, -100);
 translate(0, -100);
@@ -495,6 +502,7 @@ 

The Deterministic Version

// Branch to the left rotate(-PI / 6); line(0, 0, 0, -100);
+

Think of each call to the line() function as a branch, and you can begin to see how this code has implemented a definition of branching as a line that has two lines connected to its end. I could keep adding more and more calls to line() for more and more branches, but just as with the Cantor set and Koch curve, my code would soon become incredibly complicated and unwieldy. Instead, I’ll use the code I’ve already written as a foundation for a branch() function, replacing the second and third calls to line() with recursive calls to branch() itself:

function branch() {
   // Draw the branch.
@@ -747,27 +755,29 @@ 

Example 8.8: Simple L-sy -

Here’s how this L-system plays out over a few generations:

- - - - - - - - - - - - - - - - - - - -
Generation 0A
Generation 1ABA
Generation 2ABABBBABA
Generation 3ABABBBABABBBBBBBBBABABBBABA
+
+

Here’s how this L-system plays out over a few generations:

+ + + + + + + + + + + + + + + + + + + +
Generation 0A
Generation 1ABA
Generation 2ABABBBABA
Generation 3ABABBBABABBBBBBBBBABABBBABA
+

To turn this into a drawing, I’ll translate the system’s alphabet in the following way:

diff --git a/content/09_ga.html b/content/09_ga.html index de85f2f3..03ca5cbd 100644 --- a/content/09_ga.html +++ b/content/09_ga.html @@ -37,7 +37,7 @@

Why Use Genetic Algorithms?

Figure 9.1: Infinite cats typing at infinite keyboards

This is my meow-velous twist on the infinite monkey theorem, which is stated as follows: a monkey hitting keys randomly on a typewriter will eventually type the complete works of Shakespeare, given an infinite amount of time. It’s only a theory because in practice the number of possible combinations of letters and words makes the likelihood of the monkey actually typing Shakespeare minuscule. To put it in perspective, even if the monkey had started typing at the beginning of the universe, the probability that by now it would have produced just Hamlet, to say nothing of the entire works of Shakespeare, is still absurdly unlikely.

-

Consider a cat named Clawdius. Clawdius types on a reduced typewriter containing only 27 characters: the 26 English letters plus the spacebar. The probability of Clawdius hitting any given key is 1 in 27.

+

Consider a cat named Clawdius. Clawdius types on a reduced typewriter containing only 27
characters: the 26 English letters plus the spacebar. The probability of Clawdius hitting any given key is 1 in 27.

Next, consider the phrase “to be or not to be that is the question” (for simplicity, I’m ignoring capitalization and punctuation). The phrase is 39 characters long, including spaces. If Clawdius starts typing, the chance he’ll get the first character right is 1 in 27. Since the probability he’ll get the second character right is also 1 in 27, he has a 1 in 729 (27 \times 27) chance of landing the first two characters in correct order. (This follows directly from our discussion of probability in Chapter 0.) Therefore, the probability that Clawdius will type the full phrase is 1 in 27 multiplied by itself 39 times, or (1/27)^{39}. That equals a probability of . . .

1 \text{ in } \text{66,555,937,033,867,822,607,895,549,241,096,482,953,017,615,834,735,226,163}

Needless to say, even hitting just this one phrase, let alone an entire play, let alone all 38 Shakespeare plays (yes, even The Two Noble Kinsmen) is highly unlikely. Even if Clawdius were a computer simulation and could type a million random phrases per second, for Clawdius to have a 99 percent probability of eventually getting just the one phrase right, he would have to type for 9,719,096,182,010,563,073,125,591,133,903,305,625,605,017 years. (For comparison, the universe is estimated to be a mere 13,750,000,000 years old.)

@@ -1581,9 +1581,10 @@

Genotype and Phenotype

} } -

The phenotype is the bloop itself, whose size and speed are assigned by adding an instance of a DNA object to the Bloop class:

-
-
class Bloop {
+
+

The phenotype is the bloop itself, whose size and speed are assigned by adding an instance of a DNA object to the Bloop class:

+
+
class Bloop {
   constructor(x, y, dna) {
     this.dna = dna;
     // DNA will determine size and max speed.
@@ -1591,6 +1592,7 @@ 

Genotype and Phenotype

this.maxSpeed = map(this.dna.genes[0], 0, 1, 15, 0); this.r = map(this.dna.genes[0], 0, 1, 0, 25); /* All the rest of the bloop initialization */
+

Note that the maxSpeed property is mapped to a range from 15 to 0. A bloop with a gene value of 0 will move at a speed of 15, while a bloop with a gene value of 1 won’t move at all (speed of 0).

Selection and Reproduction

@@ -1605,8 +1607,9 @@

Selection and Reproduction

/* A Bloop baby! */ } }
-

How does a bloop reproduce? In previous examples, the reproduction process involved calling the crossover() method in the DNA class and creating a new object from the resulting array of genes. However, in this case, since I’m making a child from a single parent, I’ll call a method called copy() instead:

-
  reproduce() {
+
+

How does a bloop reproduce? In previous examples, the reproduction process involved calling the crossover() method in the DNA class and creating a new object from the resulting array of genes. However, in this case, since I’m making a child from a single parent, I’ll call a method called copy() instead:

+
  reproduce() {
     if (random(1) < 0.005) {
       // A child is an exact copy of a single parent.      
       let childDNA = this.dna.copy();
@@ -1616,6 +1619,7 @@ 

Selection and Reproduction

return new Bloop(this.position.copy(), childDNA); } }
+

Note that I’ve lowered the probability of reproduction from 1 percent to 0.05 percent. This change makes a significant difference; with a high reproduction probability, the system will rapidly become overpopulated. Too low a probability, and everything will likely die out quickly.

Writing the copy() method into the DNA class is easy with the JavaScript array method slice(), a standard JavaScript method that makes a new array by copying elements from an existing array:

class DNA {
diff --git a/content/10_nn.html b/content/10_nn.html
index 04e24a6a..36c97e35 100644
--- a/content/10_nn.html
+++ b/content/10_nn.html
@@ -188,7 +188,7 @@ 

Simple Pattern Recognitio Figure 10.5: A perceptron with two inputs (x_0 and x_1), a weight for each input (w_0 and w_1), and a processing neuron that generates the output
Figure 10.5: A perceptron with two inputs (x_0 and x_1), a weight for each input (w_0 and w_1), and a processing neuron that generates the output
-

This scheme has a pretty significant problem, however. What if my data point is (0, 0), and I send this point into the perceptron as inputs x_0 = 0 and x_1=0? No matter what the weights are, multiplication by 0 is 0. The weighted inputs are therefore still 0, and their sum will be 0 too. And the sign of 0 is . . . hmmm, there’s that deep philosophical quandary again. Regardless of how I feel about it, the point (0, 0) could certainly be above or below various lines in a 2D world. How is the perceptron supposed to interpret it accurately?

+

This scheme has a pretty significant problem, however. What if my data point is (0, 0), and I send
this point into the perceptron as inputs x_0 = 0 and x_1=0? No matter what the weights are, multiplication by 0 is 0. The weighted inputs are therefore still 0, and their sum will be 0 too. And the sign of 0 is . . . hmmm, there’s that deep philosophical quandary again. Regardless of how I feel about it, the point (0, 0) could certainly be above or below various lines in a 2D world. How is the perceptron supposed to interpret it accurately?

To avoid this dilemma, the perceptron requires a third input, typically referred to as a bias input. This extra input always has the value of 1 and is also weighted. Figure 10.6 shows the perceptron with the addition of the bias.

Figure 10.6: Adding a bias input, along with its weight, to the perceptron @@ -523,7 +523,9 @@

Putting the “Network” in Neur Figure 10.11: Truth tables for the AND and OR logical operators. The true and false outputs can be separated by a line.
Figure 10.11: Truth tables for the AND and OR logical operators. The true and false outputs can be separated by a line.

-

The XOR operator is the equivalent of (OR) AND (NOT AND). In other words, A XOR B evaluates to true only if one of the inputs is true. If both inputs are false or both are true, the output is false. To illustrate, let’s say you’re having pizza for dinner. You love pineapple on pizza, and you love mushrooms on pizza, but put them together—yech! And plain pizza, that’s no good either!

+
+

The XOR operator is the equivalent of (OR) AND (NOT AND). In other words, A XOR B evaluates to true only if one of the inputs is true. If both inputs are false or both are true, the output is false. To illustrate, let’s say you’re having pizza for dinner. You love pineapple on pizza, and you love mushrooms on pizza, but put them together—yech! And plain pizza, that’s no good either!

+

The XOR truth table in Figure 10.12 isn’t linearly separable. Try to draw a straight line to separate the true outputs from the false ones—you can’t!

Figure 10.12: The truth tables for whether you want to eat the pizza (left) and XOR (right). Note how the true and false outputs can’t be separated by a single line. diff --git a/content/11_nn_ga.html b/content/11_nn_ga.html index 64524930..7220ac74 100644 --- a/content/11_nn_ga.html +++ b/content/11_nn_ga.html @@ -268,7 +268,9 @@

The Bird Brain

];

With the inputs in hand, I’m ready to pass them to the neural network’s classify() method. I have another small problem, however: classify() is asynchronous, meaning I’d have to implement a callback inside the Bird class to process the model’s decision. This would add a significant level of complexity to the code, but luckily, it’s entirely unnecessary in this case. Asynchronous callbacks with ml5.js’s machine learning functions are typically needed because of the time required to process the large amount of data in the model. Without a callback, the code might have to wait a long time to get a result, and if the model is running as part of a p5.js sketch, that delay could severely impact the smoothness of the animation. The neural network here, however, has only four floating-point inputs and two output labels! It’s tiny and can run fast enough that there’s no reason to use asynchronous code.

-

For completeness, I include a version of the example on the book’s website that implements neuroevolution with asynchronous callbacks. For this discussion, however, I’m going to use a feature of ml5.js that allows me to take a shortcut. The method classifySync() is identical to classify(), but it runs synchronously, meaning the code stops and waits for the results before moving on. You should be very careful when using this version of the method as it can cause problems in other contexts, but it will work well for this simple scenario. Here’s the end of the think() method with classifySync():

+
+

For completeness, I include a version of the example on the book’s website that implements neuroevolution with asynchronous callbacks. For this discussion, however, I’m going to use a feature of ml5.js that allows me to take a shortcut. The method classifySync() is identical to classify(), but it runs synchronously, meaning the code stops and waits for the results before moving on. You should be very careful when using this version of the method as it can cause problems in other contexts, but it will work well for this simple scenario. Here’s the end of the think() method with classifySync():

+
    let results = this.brain.classifySync(inputs);
     if (results[0].label === "flap") {
@@ -389,7 +391,7 @@ 

Selection: Flappy Bird Fitness

}

Once normalized, each bird’s fitness is equal to its probability of being selected.

Heredity: Baby Birds

-

Only one step is left in the GA—reproduction. In Chapter 9, I explored in great detail the two-step process for generating a child element: crossover and mutation. Crossover is where the third key principle of heredity arrives: the DNA from the two selected parents is combined to form the child’s DNA.

+

Only one step is left in the GA—reproduction. In Chapter 9, I explored in great detail the two-step process for generating a child element: crossover and mutation. Crossover is where the third key principle of heredity arrives: the DNA from the two selected parents is combined to form the
child’s DNA.

At first glance, the idea of inventing a crossover algorithm for two neural networks might seem daunting, and yet it’s quite straightforward. Think of the individual “genes” of a bird’s brain as the weights within the neural network. Mixing two such brains boils down to creating a new neural network with each weight chosen by a virtual coin flip—the weight comes from either the first or the second parent:

// Pick two parents and create a child with crossover.
 let parentA = weightedSelection();
diff --git a/content/xx_1_creature_design.html b/content/xx_1_creature_design.html
index cf169c97..61dc4a0d 100644
--- a/content/xx_1_creature_design.html
+++ b/content/xx_1_creature_design.html
@@ -26,7 +26,7 @@ 

Appendix: Creature Design

Figure A.2: Drawing a simple creature

You might want to keep your design simple and stop right there! But before you start re-creating your drawing in code, consider the perspective, or view, you’ll have on your ecosystem. Are you looking at the scene from above, as if you’re gazing into a pond? Or are you looking from the side, across a meadow, or into a forest? (Think of a top-down video game versus a side-scroller.)

-

The orientation of your creature is also important, especially since you’ll be moving it around a scene. In Figure A.2, do the two curved lines represent legs or antennae? Most creatures move in a headfirst direction. But in this example, where’s the head? Reuse the basic shapes and extras to add features—such as a mouth, eyes, nose, ears, tail, antennae, and horns—to clarify your creature’s orientation.

+

The orientation of your creature is also important, especially since you’ll be moving it around a scene. In Figure A.2, do the two curved lines represent legs or antennae? Most creatures move in a headfirst direction. But in this example, where’s the head? Reuse the basic shapes and extras to add features—such as a mouth, eyes, nose, ears, tail, antennae, and horns—to clarify your creature’s orientation, as in Figure A.3.

Figure A.3: Adding details to indicate orientation