Notion - Update docs

This commit is contained in:
shiffman 2023-10-11 02:09:08 +00:00 committed by GitHub
parent ef1c41dafd
commit 4862469d91
14 changed files with 72 additions and 77 deletions

View file

@ -2,10 +2,8 @@
<h1 id="chapter-0-randomness">Chapter 0. Randomness</h1>
<div class="chapter-opening-quote">
<blockquote data-type="epigraph">
<p>
“The generation of random numbers is
too important to be left to chance.”
</p>
<p>“The generation of random numbers is</p>
<p>too important to be left to chance.”</p>
<p>— Robert R. Coveyou</p>
</blockquote>
</div>
@ -92,10 +90,10 @@
<pre class="codesplit" data-code-language="javascript">let choice = floor(random(4));</pre>
<p>Here I declare a variable <code>choice</code> and assign it a random integer (whole number) with a value of 0, 1, 2, or 3 by removing the decimal places from the random floating point number using <code>floor()</code>. Technically speaking, the number picked by calling <code>random(4)</code> can never be 4.0, since the top end of the range isnt inclusive. Rather, the highest possibility is 3.999999999 (with as many 9s as JavaScript will allow), which <code>floor()</code> will round down to 3.</p>
<div data-type="note">
<h3 id="declaring-variables">Declaring Variables</h3>
<h3 id="coding-conventions">Coding Conventions</h3>
<p>
In JavaScript, variables can be declared using either <code>let</code> or <code>const</code>. A typical approach would be to declare all variables with <code>const</code> and change to <code>let</code> when needed. In this first example, <code>const</code> would be appropriate for declaring <code>choice</code> as its never reassigned a new value over the course of its life inside each call to <code>step()</code>. While this differentiation is important, Im choosing to follow the p5.js example convention and declare all variables with <code>let</code>. I recognize there are important reasons for having <code>const</code> and <code>let</code>. However, the distinction can be a distraction and confusing for beginners. I encourage you, the reader, to explore the topic further and make your own decisions about how to best declare variables in your own sketches. For more, you can read <a href="https://github.com/processing/p5.js/issues/3877">the discussion surrounding issue #3877 in the p5.js GitHub repository</a>.
While I'm at it, I'm also choosing to use the "strict" equality boolean operator in JavaScript: <code>===</code>. This operator tests both value and type equality. For example, <code>3 === '3'</code> will evaluate to <code>false</code> because the types are different (number vs. string), even though they look similar. On the other hand, using <code>==</code> in <code>3 == '3'</code> will result in <code>true</code> because the two different types are converted to be comparable, which can sometimes lead to unexpected results. Although the loose comparison <code>==</code> would work fine here, <code>===</code> is probably a safer option.
Ill also note that Im choosing to use JavaScripts ”strict” <code>===</code> equality operator (and its inequality counterpart, <code>!==</code>). This Boolean operator tests both value and type equality. For example, <code>3 === '3'</code> will evaluate to <code>false</code> because the types are different (number vs. string), even though they look similar. On the other hand, using the loose <code>==</code> operator in <code>3 == '3'</code> would result in <code>true</code> because the two different types are converted to be comparable. Although the loose comparison often works fine, it can sometimes lead to unexpected results, so <code>===</code> is probably the safer choice.
</p>
</div>
<p>Next, the walker takes the appropriate step (left, right, up, or down), depending on which random number was picked. Heres the full <code>step()</code> method closing out the <code>Walker</code> class.</p>
@ -243,7 +241,7 @@ if (r &#x3C; probability) {
<p>You can use the same method to apply unequal weights to multiple outcomes. Lets say you want singing to have a 60 percent chance of happening, dancing, a 10 percent chance, and sleeping, a 30 percent chance. Again, you can pick a random number between 0 and 1 and see where it falls:</p>
<ul>
<li>Between 0.0 and 0.6 (60 percent) → Singing</li>
<li>Between 0.6 and 0.7 (30 percent) → Dancing</li>
<li>Between 0.6 and 0.7 (10 percent) → Dancing</li>
<li>Between 0.7 and 1.0 (30 percent) → Sleeping</li>
</ul>
<pre class="codesplit" data-code-language="javascript">let num = random(1);
@ -251,10 +249,10 @@ if (r &#x3C; probability) {
// If random number is less than 0.6
if (num &#x3C; 0.6) {
print("Sing!");
// Between 0.6 and 0.7
// Greater than or equal to 0.6 and less than 0.7
} else if (num &#x3C; 0.7) {
print("Dance!");
// Greater than 0.7
// All other cases (greater or equal to 0.7)
} else {
print("Sleep!");
}</pre>
@ -670,12 +668,14 @@ for (let x = 0; x &#x3C; width; x++) {
</div>
<figcaption>Figure 0.9 A tree with Perlin noise on the left, and a flow field with Perlin noise on the right</figcaption>
</figure>
<p>Just as you can overuse randomness, however, its easy to fall into the trap of overusing Perlin noise. How should an object move? Perlin noise! What color should it be? Perlin noise! How fast should it grow? Perlin noise! If that becomes your answer to ever question, you may not be thinking hard enough.</p>
<p>The point of all of this is not to say that you should or shouldnt use randomness. Or that you should or shouldnt use Perlin noise. The point is that the rules of your system are yours to define. The larger your programming toolbox, the more choices youll have as you implement those rules. If all you know is randomness, then your design thinking is limited. Sure, Perlin noise helps, but youll need more. A lot more. The goal of this book is to fill your toolbox, so you can make more informed choices and design more thoughtful systems.</p>
<div data-type="project">
<h3 id="the-ecosystem-project">The Ecosystem Project</h3>
<p><em>As mentioned in the Introduction, one way to use this book is to build a single project over the course of reading it, incorporating elements from each chapter as you go. One idea for this is a simulation of an ecosystem. Imagine a population of computational creatures swimming around a digital pond, interacting with each other according to various rules.</em></p>
<p>Step 0 Exercise:</p>
<p>Develop a set of rules for simulating the real-world behavior of a creature, building on top of principles from the “random walk” or other noise-driven motions. Can you simulate a jittery bug that flies in unpredictable ways, or perhaps a floating leaf carried by an inconsistent breeze? Start by exploring the boundaries of how much you can express a creatures personality through its behavior before focusing on its visual characteristics.</p>
<p>Here's an illustration to help you generate ideas for building an ecosystem based on the topics covered in this book. Watch how the illustration evolves as new concepts and techniques are introduced with each subsequent chapter. The goal of this book is to demonstrate algorithms and behaviors, so my examples will almost always only include a single primitive shape, such as a circle. However, I fully expect that there are creative sparks within you, and encourage you to challenge yourself with the designs of the elements you draw on the canvas. If drawing with code is new to you, the book's illustrator, Zannah Marsh, has written a helpful guide that you can find in the books Appendix.</p>
<p>Develop a set of rules for simulating the real-world behavior of a creature, building on top of principles from the “random walk” or other noise-driven motions. Can you simulate a jittery bug that flies in unpredictable ways, or perhaps a floating leaf carried by an inconsistent breeze? Start by exploring the boundaries of how much you can express a creatures personality purely through its behavior. Then you can think about its visual characteristics.</p>
<p>Heres an illustration to help you generate ideas for building an ecosystem based on the topics covered in this book. Watch how the illustration evolves as new concepts and techniques are introduced with each subsequent chapter. The goal of this book is to demonstrate algorithms and behaviors, so my examples will almost always only include a single primitive shape, such as a circle. However, I fully expect that there are creative sparks within you, and encourage you to challenge yourself with the designs of the elements you draw on the canvas. If drawing with code is new to you, the book's illustrator, Zannah Marsh, has written a helpful guide that you can find in the books Appendix.</p>
<figure>
<img src="images/00_randomness/00_randomness_9.png" alt="">
<figcaption></figcaption>

View file

@ -8,8 +8,8 @@
</div>
<div class="chapter-opening-figure">
<figure>
<img src="images/04_particles/04_particles_1.png" alt="">
<figcaption></figcaption>
<img src="images/04_particles/04_particles_1.png" alt="Cloud chamber photograph by Carl D. Anderson, Public Domain.">
<figcaption>Cloud chamber photograph by Carl D. Anderson, Public Domain.</figcaption>
</figure>
<p><strong>Positron</strong></p>
<p>This early 20th century photograph from a cloud chamber offers a glimpse into the world of subatomic particles, capturing the first ever observed positron. Cloud chambers are devices that make visible the paths of charged particles as they move through a supersaturated vapor.</p>
@ -368,7 +368,8 @@ function draw() {
</strong>
<strong>for (let i = particles.length - 1; i >= 0; i--) {
<strong>let length = particles.length - 1;</strong>
<strong>for (let i = length; i >= 0; i--) {
let particles = particles[i];
particle.run();
if (particle.isDead()) {
@ -391,7 +392,8 @@ function draw() {
}
run() {
<strong>for (let i = this.particles.length - 1; i >= 0; i--) {
<strong> let length = this.particles.length - 1;
</strong> <strong>for (let i = length; i >= 0; i--) {</strong><strong>
let particle = this.particles[i];
particle.run();
if (particle.isDead()) {

View file

@ -205,7 +205,7 @@ desired.setMag(this.maxspeed);</pre>
<h3 id="exercise-52">Exercise 5.2</h3>
<p>Implement a seeking behavior with a moving target, often referred to as “pursuit.” In this case, your desired vector wont point toward the objects current position, but rather its future position as extrapolated from its current velocity. Youll see this ability for a vehicle to “predict the future” in later examples. The solution is covered in the <a href="https://thecodingtrain.com/tracks/the-nature-of-code-2/noc/5-autonomous-agents/3-pursue-and-evade">“Pursue and Evade” video</a> from my Nature of Code series at thecodingtrain.com.</p>
<figure>
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/RkDbZ0drr" data-example-path="examples/05_steering/exercise_5_2"></div>
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/RkDbZ0drr" data-example-path="examples/05_steering/exercise_5_2"><img src="examples/05_steering/exercise_5_2/screenshot.png"></div>
<figcaption></figcaption>
</figure>
</div>

View file

@ -1615,7 +1615,7 @@ function draw() {
constructor(n, length) {
this.particles = [];
for (let i = 0; i &#x3C; n; i++) {
//[offset-down] Heres a funny little detail. The physics will misbehave
// Heres a funny little detail. The physics will misbehave
// if all the particles are created in exactly the same position.
let x = width / 2 + random(-1, 1);
let y = height / 2 + random(-1, 1);
@ -1757,7 +1757,7 @@ let behavior = new AttractionBehavior(particle, distance, strength);</pre>
constructor(x, y, r) {
super(x, y);
this.r = r;
//[offset-down] Every time a Particle is made, an AttractionBehavior is
// Every time a Particle is made, an AttractionBehavior is
// generated and added to the physics world.
// Note that when the strength
// is negative, its a repulsive force!

View file

@ -43,8 +43,8 @@
</figure>
<p>2) <strong>States</strong>. The simplest set of states (beyond having only one state) would be two states: 0 or 1 (Figure 7.3). Perhaps the initial states are set randomly.</p>
<figure>
<img src="images/07_ca/07_ca_4.png" alt="Figure 7.3: A one-dimensional line of cells marked with states 0 or 1. What familiar programming data structure that could represent this sequence?">
<figcaption>Figure 7.3: A one-dimensional line of cells marked with states 0 or 1. What familiar programming data structure that could represent this sequence?</figcaption>
<img src="images/07_ca/07_ca_4.png" alt="Figure 7.3: A one-dimensional line of cells marked with states 0 or 1. What familiar programming data structure could represent this sequence?">
<figcaption>Figure 7.3: A one-dimensional line of cells marked with states 0 or 1. What familiar programming data structure could represent this sequence?</figcaption>
</figure>
<p>3) <strong>Neighborhood</strong>. The simplest neighborhood in one dimension for any given cell would be the cell itself and its two adjacent neighbors: one to the left and one to the right (Figure 7.4). Ill have to decide what I want to do with the cells on the left and right edges, since those only have one neighbor each, but this is something I can sort out later.</p>
<figure>
@ -67,8 +67,8 @@
<p>There are many ways to compute a cells state from its neighbors states. Consider blurring an image. (Guess what? Image processing works with CA-like rules!) A pixels new state (its color) is the average of its neighbors colors. Similarly, a cells new state could be the sum of all of its neighbors states. However, in Wolframs 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 <strong>ruleset</strong>.</p>
<p>This approach might seem ridiculous at first—wouldnt there be way too many possibilities for it to be practical? Well, lets 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 theyre 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.</p>
<figure>
<img src="images/07_ca/07_ca_8.png" alt="Figure 7.7: Counting with 3 bits in binary, or the eight possible configurations of a three-cell neighborh">
<figcaption>Figure 7.7: Counting with 3 bits in binary, or the eight possible configurations of a three-cell neighborh</figcaption>
<img src="images/07_ca/07_ca_8.png" alt="Figure 7.7: Counting with 3 bits in binary, or the eight possible configurations of a three-cell neighborhood">
<figcaption>Figure 7.7: Counting with 3 bits in binary, or the eight possible configurations of a three-cell neighborhood</figcaption>
</figure>
<p>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.</p>
<figure>
@ -279,7 +279,7 @@ function setup() {
for (let i = 0; i &#x3C; width; i++) {
cells[i] = 0;
}
// Except the center cell is set to state 1.
//{!1} Except the center cell is set to state 1.
cells[floor(cells.length / 2)] = 1;
}
@ -520,13 +520,11 @@ for (let i = 0; i &#x3C; columns; i++) {
next[x][y] = _______________?;
}
}</pre>
<div class="half-width-right">
<figure>
<img src="images/07_ca/07_ca_32.png" alt="Figure 7.31: The index values for the neighborhood of cells.">
<figcaption>Figure 7.31: The index values for the neighborhood of cells.</figcaption>
</figure>
</div>
<p>Next, I need to sort out how to actually calculate each cells new state. For that, I need to determine how to reference the cells neighbors. In the case of a 1D CA, this was simple: if a cell index was <code>i</code>, its neighbors were <code>i-1</code> and <code>i+1</code>. Here, each cell doesnt have a single index, but rather a column and row index: <code>i,j</code>. As shown in Figure 7.31, the neighbors are <code>i-1,j-1</code> , <code>i,j-1</code>, <code>i+1,j-1</code>, <code>i-1,j</code>, <code>i+1,j</code>, <code>i-1,j+1</code>, <code>i,j+1</code>, and <code>i+1,j+1</code>.</p>
<figure>
<img src="images/07_ca/07_ca_32.png" alt="Figure 7.31: The index values for the neighborhood of cells.">
<figcaption>Figure 7.31: The index values for the neighborhood of cells.</figcaption>
</figure>
<p>The Game of Life rules operate by knowing how many neighbors are alive. If I create a counter variable and increment it for each neighbor with a state of 1, Ill have the total of live neighbors.</p>
<pre class="codesplit" data-code-language="javascript">let sum = 0;
@ -543,7 +541,7 @@ if (board[i + 1][j ] === 1) sum++;
if (board[i - 1][j + 1] === 1) sum++;
if (board[i ][j + 1] === 1) sum++;
if (board[i + 1][j + 1] === 1) sum++;</pre>
<p>Much like with the Wolfram CA, I find myself writing out a bunch of <code>if</code> statements. This is another situation where, for teaching purposes, its useful and clear way to write the code this way, explicitly stating every step (each time a neighbor has a state of 1, the counter increases). Nevertheless, its a bit silly to say, “If the cell state equals 1, add 1e to a counter” when I could instead just say, “Add every cell state to a counter.” After all, if the state can only be 0 or 1, the sum of all the neighbors states will yield the total number of live cells. Since the neighbors are arranged in a mini 3 by 3 grid, I can introduce another nested loop to computer the sum more efficiently.</p>
<p>Much like with the Wolfram CA, I find myself writing out a bunch of <code>if</code> statements. This is another situation where, for teaching purposes, its useful and clear way to write the code this way, explicitly stating every step (each time a neighbor has a state of 1, the counter increases). Nevertheless, its a bit silly to say, “If the cell state equals 1, add 1 to a counter” when I could instead just say, “Add every cell state to a counter.” After all, if the state can only be 0 or 1, the sum of all the neighbors states will yield the total number of live cells. Since the neighbors are arranged in a mini 3 by 3 grid, I can introduce another nested loop to computer the sum more efficiently.</p>
<pre class="codesplit" data-code-language="javascript">let sum = 0;
//{!2} Using k and l as the counters since i and j are already used!
@ -645,7 +643,7 @@ board = next;</pre>
this.y = y;
this.w = w;
...</pre>
<p>In the non-OOP version, I used a separate 2D arrays to keep track of the states for the current and next generation. By making a cell an object, however, each cell could keep track of both states by introducing a variable for the “previous” state.</p>
<p>In the non-OOP version, I used separate 2D arrays to keep track of the states for the current and next generation. By making a cell an object, however, each cell could keep track of both states by introducing a variable for the “previous” state.</p>
<pre class="codesplit" data-code-language="javascript">...
// What was its previous state?
this.previous = this.state;

View file

@ -348,7 +348,7 @@ function drawCircles(x, y, radius) {
<pre class="codesplit" data-code-language="javascript">function generate() {
let next = [];
for (let segment of segments) {
//{!5} A KochLine needs a methods that returns all five point computed according to the Koch rules.
//{!5} A KochLine needs a method that returns all five point computed according to the Koch rules.
let [a, b, c, d, e] = segment.kochPoints();
next.push(new KochLine(a, b));
@ -472,7 +472,7 @@ function setup() {
<img src="images/08_fractals/08_fractals_21.png" alt="Figure 8.18: The process of drawing a line, translating to the end of the line, and rotating by an angle.">
<figcaption>Figure 8.18: The process of drawing a line, translating to the end of the line, and rotating by an angle.</figcaption>
</figure>
<p>Heres the code for the process illustrated in Figure 8.19. Im using an angle of 30 degrees, or <span data-type="equation">\pi/6</span> radians.</p>
<p>Heres the code for the process illustrated in Figure 8.18. Im using an angle of 30 degrees, or <span data-type="equation">\pi/6</span> radians.</p>
<pre class="codesplit" data-code-language="javascript">translate(0, -100);
//{$1} PI divided by 6 is equivalent to 30°
rotate(PI / 6);
@ -536,6 +536,7 @@ function branch(len) {
//{!1} Each branchs length shrinks by one-third.
len *= 0.67;
//{!1} Exit condition for the recursion!
if (len > 2) {
push();
rotate(angle);
@ -876,9 +877,9 @@ translate(0, length);</code></pre>
let c = sentence.charAt(i);
//{!14} Performing the correct task for each character.
// This could also be written with a “case” statement,
// This could also be written with a “switch” statement,
// which might be nicer to look at, but leaving it as an
// if/else if structure helps readers not familiar with case statements.
// if/else if structure helps readers not unfamiliar with that syntax.
if (c === 'F') {
line(0, 0, length, 0);
translate(length, 0);

View file

@ -53,7 +53,7 @@ console.log(s);</pre>
<ol>
<li><strong>Heredity.</strong> There must be a mechanism that allows “parent” creatures in one generation to pass their traits down to “child” creatures in the next generation.</li>
<li><strong>Variation. </strong>There must be a variety of traits present in the population of creatures or a means with which to introduce variation for evolution to take place. Imagine if there were a population of beetles in which all the beetles were exactly the same: same color, same size, same wingspan, same everything. Without any variety in the population, the children would always be identical to the parents and to each other. New combinations of traits can never occur, and nothing can evolve.</li>
<li><strong>Selection. </strong>There must be a mechanism by which some creatures have the opportunity to be parents and pass on their genetic information, while others dont. This is commonly referred to as “survival of the fittest.” Take, for example, a population of gazelles that are chased by lions. The faster gazelles have a better chance of escaping the lions, increasing their chances of living longer, reproducing, and passing on their genetic information to offspring. The term <em>fittest</em> can be misleading, however. Its often thought to mean biggest, fastest, or strongest, but while it can sometimes encompass physical attributes like size, speed, or strength, it doesnt have to. The core of natural selection lies in whatever traits best suit an organism's environment and increasing the likelihood of survival and ultimately reproduction. Instead of asserting superiority, “fittest” can be better understood as “able to reproduce.” Take the “Dolania americana” (also known as the American sand-burrowing mayfly), which is believed to have the shortest lifespan of any insect. An adult female lives for only 5 minutes, but as long as it has managed to deposit its egg in the water, it will pass its genetic information to the next generation. For the typing cats, a more “fit” cat, one that I will assign as more likely to reproduce, is one that has typed more words present in a given phrase of Shakespeare.</li>
<li><strong>Selection. </strong>There must be a mechanism by which some creatures have the opportunity to be parents and pass on their genetic information, while others dont. This is commonly referred to as “survival of the fittest.” Take, for example, a population of gazelles that are chased by lions. The faster gazelles have a better chance of escaping the lions, increasing their chances of living longer, reproducing, and passing on their genetic information to offspring. The term <em>fittest</em> can be misleading, however. Its often thought to mean biggest, fastest, or strongest, but while it can sometimes encompass physical attributes like size, speed, or strength, it doesnt have to. The core of natural selection lies in whatever traits best suit an organisms environment and increase its likelihood of survival and ultimately reproduction. Instead of asserting superiority, “fittest” can be better understood as “able to reproduce.” Take the <em>Dolania americana</em> (also known as the American sand-burrowing mayfly), which is believed to have the shortest lifespan of any insect. An adult female lives for only five minutes, but as long as it has managed to deposit its egg in the water, it will pass its genetic information to the next generation. For the typing cats, a more “fit” cat, one that I will assign as more likely to reproduce, is one that has typed more characters present in a given phrase of Shakespeare.</li>
</ol>
<p>I want to emphasize the context in which Im applying these Darwinian concepts: a simulated, artificial environment where specific goals can be quantified, all for the sake of creative exploration. Throughout history, the principles of genetics have been used to harm those who have been marginalized and oppressed by dominant societal structures. I believe it is essential to approach projects involving genetic algorithms with careful consideration of the language used, and to ensure that the documentation and descriptions of the work are framed inclusively.</p>
<p>With these concepts established, Ill begin walking through the narrative of the genetic algorithm. I'll do this in the context of typing cats. The algorithm itself will be divided into several steps that unfold over two parts: a set of conditions for initialization, and the steps that are repeated over and over again until the correct phrase is found.</p>
@ -83,13 +83,13 @@ console.log(s);</pre>
<table>
<thead>
<tr>
<th>Genotype <code>100px</code></th>
<th>Phenotype <code>140px</code></th>
<th style="width:200px">Genotype</th>
<th>Phenotype</th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="width:140px;">0</span></td>
<td>0</td>
<td>
<div style="display: inline-block; vertical-align: sub; height: 18px; width: 18px; background-color: #000000; border: 1px solid black;"></div>
</td>
@ -112,7 +112,7 @@ console.log(s);</pre>
<table>
<thead>
<tr>
<th>Same Genotype</th>
<th style="width:200px">Same Genotype</th>
<th>Different Phenotype (line length)</th>
</tr>
</thead>
@ -434,12 +434,9 @@ function setup() {
}
}</pre>
<p>With the mating pool ready to go, its time to select two parents! Its somewhat of an arbitrary decision to pick two parents for each child. It certainly mirrors human reproduction and is the standard means in the textbook genetic algorithm, but in terms of creative applications, there really arent restrictions here. You could choose only one parent for “cloning,” or devise a reproduction methodology for picking three or four parents from which to generate child DNA. For the demonstration here, Ill stick to two parents and call them <code>parentA</code> and <code>parentB</code>.</p>
<p>First I need two random indices into the mating pool—random numbers between 0 and the size of the array.</p>
<pre class="codesplit" data-code-language="javascript"> let aIndex = floor(random(matingPool.length));
let bIndex = floor(random(matingPool.length));</pre>
<p>I can then use the indices to retrieve a <code>DNA</code> instance from the <code>matingPool</code> array.</p>
<pre class="codesplit" data-code-language="javascript"> let parentA = matingPool[aIndex];
let parentB = matingPool[bIndex];</pre>
<p>I can select two random instances of DNA from the mating pool using the p5.js <code>random()</code> function. When an array is passed as an argument to <code>random()</code>, the function returns a single random element from the array!</p>
<pre class="codesplit" data-code-language="javascript"> let parentA = random(matingPool);
let parentB = random(matingPool);</pre>
<p>This method of building a mating pool and choosing parents from it works, but it isnt the only way to perform selection. There are other, more memory-efficient techniques that dont require an additional array full of multiple references to each element. For example, think back to the discussion of non-uniform distributions of random numbers in Chapter 0. There, I implemented the “accept-reject” method. If applied here, the approach would be to randomly pick an element from the original <code>population</code> array, and then pick a second, “qualifying” random number to check against the elements fitness value. If the fitness is less than the qualifying number, start again and pick a new element. Keep going until two parents are deemed fit enough.</p>
<p>Theres also yet another excellent alternative worth exploring that similarly capitalizes on the principle of fitness-proportionate selection. To understand how it works, imagine a relay race where each member of the population runs a given distance tied to its fitness. The higher the fitness, the farther they run. Lets also assume that the fitness values have been normalized to all add up to 1 (just like with the “wheel of fortune”). The first step is to pick a “starting line”<em></em>a random distance from the finish. This distance is a random number between 0 and 1. (Youll see in a moment that the “finish line”<em> </em>is assumed to be at 0.)</p>
<pre class="codesplit" data-code-language="javascript">let start = random(1);</pre>
@ -471,7 +468,7 @@ while (start > 0) {
index--;
return population[index];
}</pre>
<p>This works well for selection because each and every member has a shot at crossing the finish line (the elements fitness scores all add up to 1), but those who run longer distances (that is, those with higher fitness scores) have a better chance of making it there. However, while this method is more memory efficienct, it can be more <em>computationally </em>demanding, especially for large populations, as it requires iterating through the population for each selection. By contrast, the original <code>matingPool</code> array method only needs a single random lookup into the array per parent.</p>
<p>This works well for selection because each and every member has a shot at crossing the finish line (the elements fitness scores all add up to 1), but those who run longer distances (that is, those with higher fitness scores) have a better chance of making it there. However, while this method is more memory efficient, it can be more <em>computationally </em>demanding, especially for large populations, as it requires iterating through the population for each selection. By contrast, the original <code>matingPool</code> array method only needs a single random lookup into the array per parent.</p>
<p>Depending on the specific requirements and constraints of your application of genetic algorithms, one approach might prove more suitable than the other. Ill alternate between them in the examples outlined in this chapter.</p>
<div data-type="exercise">
<h3 id="exercise-92">Exercise 9.2</h3>
@ -529,7 +526,7 @@ while (start > 0) {
</tr>
</tbody>
</table>
<p>How can you implement this approach? Hint: you dont need to modify the selection algorithm itself. Instead, your task is to calculate the probabilities from the rank rather than the raw fitness score.</p>
<p>How can you implement an approach like this? Hint: you dont need to modify the selection algorithm itself. Instead, your task is to calculate the probabilities from the rank rather than the raw fitness score.</p>
</div>
<p>For any of these algorithms, its possible that the same parent could be picked twice for a given child. If I wanted, I could enhance the algorithm to ensure that this isnt possible. This would likely have very little impact on the end result, but it may be worth exploring as an exercise.</p>
<div data-type="exercise">
@ -572,7 +569,7 @@ child.mutate();</pre>
<pre class="codesplit" data-code-language="javascript">let mutationRate = 0.01;
if (random(1) &#x3C; mutationRate) {
// Any code here would be executed 1% of the time.
/* Any code here would be executed 1% of the time. */
}</pre>
<p>The entire method therefore reads:</p>
<pre class="codesplit" data-code-language="javascript">mutate(mutationRate) {
@ -636,12 +633,10 @@ function draw() {
// <strong>Step 3: Reproduction</strong>
for (let i = 0; i &#x3C; population.length; i++) {
let aIndex = floor(random(matingPool.length));
let bIndex = floor(random(matingPool.length));
let partnerA = matingPool[aIndex];
let partnerB = matingPool[bIndex];
let parentA = random(matingPool);
let parentB = random(matingPool);
// Step 3a: Crossover
let child = partnerA.crossover(partnerB);
let child = parentA.crossover(parentB);
// Step 3b: Mutation
child.mutate(mutationRate);
@ -952,7 +947,7 @@ let populationSize = 150;</pre>
// The genotype is an array of vectors.
this.genes = [];
for (let i = 0; i &#x3C; length; i++) {
//{!1} A PVector pointing in a random direction
//{!1} A vector pointing in a random direction
this.genes[i] = p5.Vector.random2D();
//{!1} And scaled randomly
this.genes[i].mult(random(10));
@ -1060,7 +1055,7 @@ let populationSize = 150;</pre>
</div>
<figcaption>Figure 9.11: On the left, vectors created with random <span data-type="equation">x</span> and <span data-type="equation">y</span> values. On the right, using <code>p5.Vector.random2D()</code>.</figcaption>
</figure>
<p>What would be better here is to pick a random angle and make a vector of length 1 from that angle, such that the results form a circle (see right of Figure 9.11). This could be done with a quick polar to Cartesian conversion, but an even quicker path to the result is just to use <code>p5.Vector.random2D()</code>.</p>
<p>As you may recall from Chapter 3, a better choice is to pick a random angle and create a vector of length 1 from that angle. This produces results that form a circle (see right of Figure 9.11) and can be achieved with polar to Cartesian conversion or the trusty <code>p5.Vector.random2D()</code> method.</p>
<pre class="codesplit" data-code-language="javascript">for (let i = 0; i &#x3C; length; i++) {
//{!1} A random unit vector
this.genes[i] = p5.Vector.random2D();

View file

@ -657,7 +657,7 @@ function draw() {
</figure>
<p>Rather than picking from a discrete set of output options, the goal of the neural network is now to guess a number—any number. Will the house use 30.5 kilowatt-hours of electricity that day? Or 48.7 kWh? Or 100.2 kWh? The output prediction could be any value from a continuous range.</p>
<h3 id="network-design">Network Design</h3>
<p>Knowing what problem youre trying to solve (step 0) also has a significant bearing on the design of the neural network itself, in particular on its input and output layers. Ill demonstrate with another classic “Hello, world!” classification example from the field of data science and machine learning: the iris dataset. This dataset can be found in the University of California Irvine Machine Learning Repository and originated from the work of American botanist Edgar Anderson. Anderson collected flower data over many years across multiple regions of the United States and Canada. After carefully analyzing the data, he built a table to classify iris flowers into three distinct species: <em>Iris setosa</em>, <em>Iris virginica</em>, and <em>Iris versicolor </em>(see Figure 10.17).</p>
<p>Knowing what problem youre trying to solve (step 0) also has a significant bearing on the design of the neural network itself, in particular on its input and output layers. Ill demonstrate with another classic “Hello, world!” classification example from the field of data science and machine learning: the iris dataset. This dataset can be found in the University of California Irvine Machine Learning Repository and originated from the work of American botanist Edgar Anderson. Anderson collected flower data over many years across multiple regions of the United States and Canada. (For more on the origins of this famous dataset, see Unwin and Kleinman, <a href="https://academic.oup.com/jrssig/article/18/6/26/7038520?login=false">“The Iris Data Set: In Search of the Source of </a><a href="https://academic.oup.com/jrssig/article/18/6/26/7038520?login=false"><em>Virginica</em></a><a href="https://academic.oup.com/jrssig/article/18/6/26/7038520?login=false">.”</a>) After carefully analyzing the data, he built a table to classify iris flowers into three distinct species: <em>Iris setosa</em>, <em>Iris virginica</em>, and <em>Iris versicolor </em>(see Figure 10.17).</p>
<figure>
<img src="images/10_nn/10_nn_18.png" alt="Figure 10.17: PLACEHOLDER from https://rss.onlinelibrary.wiley.com/doi/abs/10.1111/1740-9713.01589 (considering an illustration / drawing of the flowers as a visual aid)">
<figcaption>Figure 10.17: PLACEHOLDER from <a href="https://rss.onlinelibrary.wiley.com/doi/abs/10.1111/1740-9713.01589">https://rss.onlinelibrary.wiley.com/doi/abs/10.1111/1740-9713.01589</a> (considering an illustration / drawing of the flowers as a visual aid)</figcaption>
@ -886,9 +886,9 @@ classifier.train(options, finishedTraining);</pre>
<p>The second argument to <code>train()</code> is optional, but its good to include one. It specifies a callback function that runs when the training process is complete—in this case,<code> finshedTraining()</code>. (See the “Callbacks” box for more on callback functions.) This is useful for knowing when you can proceed to the next steps in your code. Theres even another optional callback, which I usually name <code>whileTraining()</code>, thats triggered after each epoch. However, for my purposes, knowing when the training is done is plenty!</p>
<div data-type="note">
<h3 id="callbacks">Callbacks</h3>
<p>If youve worked with p5.js, youre already familiar with the concept of a <strong>callback function</strong> even if you dont know it by that name. Think of the <code>mousePressed()</code> function. You define what should happen inside it, and p5.js takes care of actually <em>calling </em>it at the right moment, when the mouse is pressed.</p>
<p>A callback function in JavaScript operates on a similar principle. Its a function that you provide as an argument to another function, intending for it to be “called back” at a later time. Callbacks are needed for <strong>asynchronous</strong> operations, where you want your code to continue along with animating or doing other things while waiting for another task (like training a machine learning model) to finish. A classic example of this in p5.js is loading data into a sketch with <code>loadJSON()</code>.</p>
<p>In JavaScript, theres also a more recent approach for handling asynchronous operations known as <strong>promises</strong>. With promises, you can use keywords like <code>async</code> and <code>await</code> to make your asynchronous code look more like traditional synchronous code. While ml5.js also supports this style, Ill stick to using callbacks to stay aligned with p5.js style.</p>
<p>A <strong>callback function</strong> in JavaScript is a function you dont actually call yourself. Instead, you provide it as an argument to another function, intending for it to be “called back” automatically at a later time (typically associated with an event, like a mouse press). Youve seen this before when working with Matter.js in Chapter 6, where you specified a function to call whenever a collision is detected.</p>
<p>Callbacks are needed for <strong>asynchronous</strong> operations, where you want your code to continue along with animating or doing other things while waiting for another task (like training a machine learning model) to finish. A classic example of this in p5.js is loading data into a sketch with <code>loadJSON()</code>.</p>
<p>In JavaScript, theres also a more recent approach for handling <strong>asynchronous</strong> operations known as <strong>promises</strong>. With promises, you can use keywords like <code>async</code> and <code>await</code> to make your asynchronous code look more like traditional synchronous code. While ml5.js also supports this style, Ill stick to using callbacks to stay aligned with p5.js style.</p>
</div>
<h3 id="evaluating-the-model">Evaluating the Model</h3>
<p>If <code>debug</code> is set to <code>true</code> in the initial call to <code>ml5.neuralNetwork()</code>, a visual interface should appear once <code>train()</code> is called, covering most of the p5.js page and canvas (see Figure 10.21). This interface, called the Visor, represents the evaluation step.</p>
@ -898,7 +898,7 @@ classifier.train(options, finishedTraining);</pre>
</figure>
<p>The Visor comes from TensorFlow.js (which underlies ml5.js) and includes a graph that provides real-time feedback on the progress of the training. This graph plots the <strong>loss</strong> of the model on the y-axis against the number of epochs along the x-axis. Loss is a measure of how far off the models predictions are from the correct outputs provided by the training data. It quantifies the models total error. When training begins, its common for the loss to be high because the model has yet to learn anything. Ideally, as the model trains through more epochs, it should get better at its predictions, and the loss should decrease. If the graph goes down as the epochs increase, this is a good sign!</p>
<p>Running the training for the 200 epochs depicted in Figure 10.21 might strike you as a bit excessive. In a real-world scenario with more extensive data, I would probably use fewer epochs, like the 25 I specified in the original code snippet. However, because the dataset here is so tiny, the higher number of epochs helps the model get enough “practice” with the data. Remember, this is a “toy” example, aiming to make the concepts clear rather than to produce a sophisticated machine learning model.</p>
<p>Below the graph, the Visor shows a Model Summary table with details on the lower-level TensorFlow.js model architecture created behind the scenes. The summary includes layer names, neuron counts per layer, and a “parameters” count, which is the total number of weights, one for each connection between two neurons. In this case, dense_Dense1 is the hidden layer and dense_Dense2 is the output layer. (TensorFlow.js doesnt think of the inputs as a distinct layer; rather, theyre merely the starting point of the data flow.) The “Output Shape” is listed as “[batch, count]”, where "batch" refers to the amount of training data processed for a single iteration of model training, and "count" indicates the number of neurons. You can see there are 16 for the hidden (chosen by ml5.js) and 4 for the output (defined by the numbers of classification categories).</p>
<p>Below the graph, the Visor shows a Model Summary table with details on the lower-level TensorFlow.js model architecture created behind the scenes. The summary includes layer names, neuron counts per layer (in the Output Shape column), and a “parameters” count, which is the total number of weights, one for each connection between two neurons. In this case, dense_Dense1 is the hidden layer with 16 neurons (an amount chosen by ml5.js), and dense_Dense2 is the output layer with 4 neurons, one for each classification category. (TensorFlow.js doesnt think of the inputs as a distinct layer; rather, theyre merely the starting point of the data flow.) The “batch” in the Output Shape column does not refer to a specific number, but indicates that the model can process a variable amount of training data (a “batch”) for any single cycle of model training.</p>
<p>Before moving on from the evaluation stage, theres a loose end to tie up. When I first outlined the steps of the machine learning lifecycle, I mentioned that preparing the data typically involves splitting the dataset into three parts to help with the evaluation process:</p>
<ol>
<li><strong>Training.</strong> The primary dataset used to train the model.</li>

View file

@ -32,7 +32,7 @@
</figure>
<p>Suppose you wanted to automate the gameplay, and instead of a human tapping, a neural network will make the decision as to whether to flap or not. Could machine learning work here? Skipping over the initial “data” steps in the machine learning lifecycle for a moment, lets think about how to choose a model. What are the inputs and outputs of the neural network?</p>
<p>This is quite the intriguing question because, at least in the case of the inputs, there isnt a definitive answer. In a scenario where you dont know much about the game or dont want to put your thumb on the scale in terms of identifying what aspects of the game are important, it might make the most sense to have the inputs be all the pixels of the game screen. This approach attempts to feed <em>everything</em> about the game into the model and let the model figure out for itself what matters.</p>
<p>Ive played <em>Flappy Bird</em> enough that I feel I understand it quite well, however. I can therefore bypass feeding all the pixels to the model and boil the essence of the game down to just a few input data points necessary for making predictions. In machine learning, these data points are often called <strong>features</strong>. The term underscores the idea of distinct characteristics of the data that are most salient for the prediction—like how your facial features help others identify you. In the case of <em>Flappy Bird</em>, the most crucial features are:</p>
<p>Ive played <em>Flappy Bird</em> enough that I feel I understand it quite well, however. I can therefore bypass feeding all the pixels to the model and boil the essence of the game down to just a few input data points necessary for making predictions. These data points, often referred to as <strong>features</strong> in machine learning, represent the distinctive characteristics of the data that are most salient for the prediction. Imagine biting into a mysteriously juicy fruit—its features like its taste (sweet!), texture (crisp!), and color (a vibrant red!) help you identify it as an apple. In the case of <em>Flappy Bird</em>, the most crucial features are:</p>
<ol>
<li>The <span data-type="equation">y</span> position of the bird.</li>
<li>The <span data-type="equation">y</span> velocity of the bird.</li>
@ -256,12 +256,12 @@ function draw() {
...</pre>
<p>With the inputs in hand, Im ready to pass them to the neural networks <code>classify()</code> method. Theres another small problem, however: <code>classify()</code> is asynchronous, meaning Id have to implement a callback inside the <code>Bird</code> class to process the models decision. This would add a significant level of complexity to the code, but luckily, its entirely unnecessary in this case. Asynchronous callbacks with ml5.jss machine learning functions are typically needed due to 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, only has four floating point inputs and two output labels! Its tiny and can run fast enough that theres no reason to use asynchronous code.</p>
<p>For completeness, Ill include a version of the example on the books website that implements neuroevolution with asynchronous callbacks. For the discussion here, however, Im going to use a feature of ml5.js that allows me to take a shortcut. The method <code>classifySync()</code> is identical to <code>classify()</code>, 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. Heres the end of the <code>think()</code> method with <code>classifySync()</code>.</p>
<pre class="codesplit" data-code-language="javascript"> ...
let results = this.brain.classifySync(inputs);
if (results[0].label === "flap") {
this.flap();
}
}</pre>
<pre class="codesplit" data-code-language="javascript">...
let results = this.brain.classifySync(inputs);
if (results[0].label === "flap") {
this.flap();
}
}</pre>
<p>The neural networks prediction is in the same format as the gesture classifier from the previous chapter, and the decision can be made by checking the first element of the <code>results</code> array. If the output label is <code>"flap"</code>, then call <code>flap()</code>.</p>
<p>Now that Ive finished the <code>think()</code> method, the real challenge can begin: teaching the bird to win the game by consistently flapping its wings at the right moment. This is where the genetic algorithm comes back into the picture. Recalling the discussion of from Chapter 9, there are three key principles that underpin Darwinian evolution: variation, selection, and heredity. Ill revisit each of these principles in turn as I implement the steps of the genetic algorithm in this new context of neural networks.</p>
<h3 id="variation-a-flock-of-flappy-birds">Variation: A Flock of Flappy Birds</h3>
@ -440,7 +440,7 @@ function resetPipes() {
// Remove all the pipes but the very latest one
pipes.splice(0, pipes.length - 1);
}</pre>
<p>Note the addition of a new <code>resetPipes()</code> function. If I don't remove pipes before starting a new generation, the birds may immediately restart at a position colliding with a pipe. Even the best bird will not have a chance to fly! The full online code for Example 11.2 also adjusts the behavior of birds so that they die when they leave the canvas, either by crashing into the ground or soaring too high above the top.</p>
<p>Note the addition of a new <code>resetPipes()</code> function. If I dont remove the pipes before starting a new generation, the birds may immediately restart at a position colliding with a pipe, in which case even the best bird wont have a chance to fly! The full online code for Example 11.2 also adjusts the behavior of the birds so that they die when they leave the canvas, either by crashing into the ground or soaring too high above the top.</p>
<div data-type="note">
<h3 id="exercise-112">Exercise 11.2</h3>
<p>It takes a very long time for Example 11.2 to produce any results. How might you “speed up time” by skipping the drawing every single frame of the game to reach an optimal bird faster? (A solution will be presented later in the chapter.) Additionally, could you add an overlay that displays information about the simulations status, such as the number of birds still in play, the current generation, and the lifespan of the best bird?</p>
@ -654,7 +654,7 @@ function draw() {
<p>There are a few elements in this chapters examples that dont quite fit with my dream of simulating a natural ecosystem. The first goes back to an issue I raised in Chapter 9 with the introduction of the “bloops.” A system of creatures that all live and dies together, starting completely over with each subsequent generation—that isnt how the biological world works! Id like to revisit this dilemma in this chapters context of neuroevolution.</p>
<p>Second, and perhaps more important, theres a major flaw in the way Im extracting features from a scene to train a model. The creatures in Example 11.4 are all knowing. Sure, its reasonable to assume that a creature is aware of its own current velocity, but Ive also allowed each creature to know exactly where the glow is, regardless of how far away it is or what might be blocking its vision or senses. This is a bridge too far. It flies in the face of one of the main tenets of autonomous agents I introduced in Chapter 5: an agent should have a <em>limited</em> ability to perceive its environment.</p>
<h3 id="sensing-the-environment">Sensing the Environment</h3>
<p>A common approach to simulating how a creature (or robot) in the real world would have a limited awareness of its surroundings involves attaching <strong>sensors</strong> to an agent. Think back to that mouse in the maze from the beginning of the chapter (hopefully its been thriving on the cheese its been getting as a reward), and now imagine it has to navigate the maze in the dark. Its whiskers might act as proximity sensors to detect walls and turns. The mouse whiskers cant “see” the entire maze, only the immediate surroundings. Another example of sensors is a bat using echolocation to navigate, or a car on a winding road that can only see whats projected in front of its headlights.</p>
<p>A common approach to simulating how a real-world creature (or robot) would have a limited awareness of its surroundings is to attach <strong>sensors</strong> to an agent. Think back to that mouse in the maze from the beginning of the chapter (hopefully its been thriving on the cheese its been getting as a reward), and now imagine it has to navigate the maze in the dark. Its whiskers might act as proximity sensors to detect walls and turns. The mouse whiskers cant “see” the entire maze, only the immediate surroundings. Another example of sensors is a bat using echolocation to navigate, or a car on a winding road that can only see whats projected in front of its headlights.</p>
<p>Id like to build on this idea of the whiskers (or more formally the <em>vibrissae</em>) found in mice, cats, and other mammals. In the real world, animals use their vibrissae to navigate and detect nearby objects, especially in dark or obscured environments (see Figure 11.4). How can I attach whisker-like sensors to my neuroevolutionary seeking creatures?</p>
<figure>
<img src="images/11_nn_ga/11_nn_ga_5.jpg" alt="Figure 11.4: ILLUSTRATION OF A MOUSE OR CAT OR FICTIONAL CREATURE SENSING ITS ENVIRONMENT WITH ITS WHISKERS (image temporarily from https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Cat_whiskers_closeup.jpg/629px-Cat_whiskers_closeup.jpg?20120309014158)">

View file

@ -1 +1 @@
[{"title":"0. Randomness","src":"./00_randomness.html","slug":"random"},{"title":"1. Vectors","src":"./01_vectors.html","slug":"vectors"},{"title":"2. Forces","src":"./02_forces.html","slug":"force"},{"title":"3. Oscillation","src":"./03_oscillation.html","slug":"oscillation"},{"title":"4. Particle Systems","src":"./04_particles.html","slug":"particles"},{"title":"5. Autonomous Agents","src":"./05_steering.html","slug":"autonomous-agents"},{"title":"6. Physics Libraries","src":"./06_libraries.html","slug":"physics-libraries"},{"title":"7. Cellular Automata","src":"./07_ca.html","slug":"cellular-automata"},{"title":"8. Fractals","src":"./08_fractals.html","slug":"fractals"},{"title":"9. Evolutionary Computing","src":"./09_ga.html","slug":"genetic-algorithms"},{"title":"10. Neural Networks","src":"./10_nn.html","slug":"neural-networks"},{"title":"11. Neuroevolution","src":"./11_nn_ga.html","slug":"neuro-evolution"}]
[{"title":"0. Randomness","src":"./00_randomness.html","slug":"random"},{"title":"1. Vectors","src":"./01_vectors.html","slug":"vectors"},{"title":"2. Forces","src":"./02_forces.html","slug":"force"},{"title":"3. Oscillation","src":"./03_oscillation.html","slug":"oscillation"},{"title":"4. Particle Systems","src":"./04_particles.html","slug":"particles"},{"title":"5. Autonomous Agents","src":"./05_steering.html","slug":"autonomous-agents"},{"title":"6. Physics Libraries","src":"./06_libraries.html","slug":"physics-libraries"},{"title":"7. Cellular Automata","src":"./07_ca.html","slug":"cellular-automata"},{"title":"8. Fractals","src":"./08_fractals.html","slug":"fractals"},{"title":"9. Evolutionary Computing","src":"./09_ga.html","slug":"genetic-algorithms"},{"title":"10. Neural Networks","src":"./10_nn.html","slug":"neural-networks"},{"title":"11. Neuroevolution","src":"./11_nn_ga.html","slug":"neuroevolution"}]

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -17,7 +17,8 @@ let target;
function setup() {
createCanvas(640, 240);
pursuer = new Vehicle(100, 100);
target = new Target(200, 100);
target = new Target(500, 200);
background(255);
}
function draw() {

View file

@ -33,7 +33,7 @@ class Vehicle {
prediction.mult(10);
target.add(prediction);
fill(175);
noStroke();
stroke(0);
circle(target.x, target.y, 16);
return this.seek(target);
}

View file

@ -67,10 +67,8 @@ function draw() {
// Step 3: Reproduction
for (let i = 0; i < population.length; i++) {
let aIndex = floor(random(matingPool.length));
let bIndex = floor(random(matingPool.length));
let partnerA = matingPool[aIndex];
let partnerB = matingPool[bIndex];
let partnerA = random(matingPool);
let partnerB = random(matingPool);
// Step 3a: Crossover
let child = partnerA.crossover(partnerB);
// Step 3b: Mutation