Created
July 15, 2022 11:38
-
-
Save LiorB-D/37b8e894745ffd76dbd21b954ece288a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<head> | |
<!-- Load in Tensorflow, P5, and our Car Class--> | |
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]/dist/tf.min.js"></script> | |
<script | |
src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js" | |
integrity="sha512-WIklPM6qPCIp6d3fSSr90j+1unQHUOoWDS4sdTiR8gxUTnyZ8S2Mr8e10sKKJ/bhJgpAa/qG068RDkg6fIlNFA==" | |
crossorigin="anonymous" | |
></script> | |
<script src="car.js"></script> | |
<script> | |
// Screen dimensions | |
const screenHt = 650; | |
const screenWd = 800; | |
const innerDi = 300; // Diameter of Inner Circle | |
const outerDi = 750; // Diameter of Outer Circle | |
const carDi = 20; // Diameter of each car | |
let frameCount = 0; | |
let currGen = 1; | |
let topFitnessScore = 0; // Highest fitness score of any generation | |
//Obstacles can be added/removed without any additional implementation | |
let obsts = [ | |
{ x: screenWd / 2, y: screenHt / 4, di: 40 }, | |
{ x: screenWd / 2, y: (3 * screenHt) / 4, di: 40 }, | |
]; | |
// UI elements | |
let nextGenBtn, mrateInput, mrateLbl; | |
let infoLbl; | |
let cars = []; | |
function setup() { | |
createCanvas(screenWd + 20 + 200, screenHt + 20); | |
frameRate(60); // Edit Frame Rate to change speed of simulation | |
// Setup UI elements with P5 | |
nextGenBtn = createButton("Next Generation"); | |
nextGenBtn.position(screenWd + 30, 110); | |
mrateInput = createInput("0.15"); | |
mrateInput.position(screenWd + 30, 80); | |
mrateLbl = createElement("p", "Mutation Rate:"); | |
mrateLbl.position(screenWd + 30, 45); | |
infoLbl = createElement( | |
"p", | |
"Gen: " + currGen + ", Top Score: " + topFitnessScore | |
); | |
infoLbl.position(screenWd + 30, 150); | |
// Add 20 new cars | |
for (let i = 0; i < 20; i++) { | |
cars.push(new Car(outerDi, innerDi, screenHt, screenWd, carDi, obsts)); | |
} | |
} | |
function draw() { | |
// Looped based on framerate | |
//Draw Driving Map | |
strokeWeight(1); | |
fill(50); | |
stroke("black"); | |
rect(0, 0, screenWd, screenHt); | |
//Outer Circle | |
fill(210); | |
ellipse(screenWd / 2, screenHt / 2, outerDi, outerDi / 1.5); | |
fill("green"); | |
strokeWeight(3); | |
//Obstacles | |
obsts.forEach((obs) => { | |
ellipse(obs.x, obs.y, obs.di); | |
}); | |
//Inner circle | |
ellipse(screenWd / 2, screenHt / 2, innerDi, innerDi / 2); | |
//Draw Finish Line | |
stroke("red"); | |
strokeWeight(4); | |
line( | |
screenWd / 2 - outerDi / 2, | |
screenHt / 2, | |
screenWd / 2 - innerDi / 2, | |
screenHt / 2 | |
); | |
cars.forEach((car) => { | |
//Draw car | |
//Draw Wheels | |
fill("black"); | |
strokeWeight(0); | |
ellipse( | |
car.x + (carDi / 2) * cos(car.angle + PI / 4), | |
car.y + (carDi / 2) * sin(car.angle + PI / 4), | |
carDi / 2.5 | |
); | |
ellipse( | |
car.x + (carDi / 2) * cos(car.angle - PI / 4), | |
car.y + (carDi / 2) * sin(car.angle - PI / 4), | |
carDi / 2.5 | |
); | |
ellipse( | |
car.x + (carDi / 2) * cos(car.angle + 0.75 * PI), | |
car.y + (carDi / 2) * sin(car.angle + 0.75 * PI), | |
carDi / 2.5 | |
); | |
ellipse( | |
car.x + (carDi / 2) * cos(car.angle - 0.75 * PI), | |
car.y + (carDi / 2) * sin(car.angle - 0.75 * PI), | |
carDi / 2.5 | |
); | |
//Draw Car Base | |
fill("blue"); | |
stroke("black"); | |
strokeWeight(1); | |
ellipse(car.x, car.y, carDi); | |
//Draw Eye | |
fill("yellow"); | |
ellipse(car.eyeX, car.eyeY, carDi / 3); | |
// Car Behavior | |
if (car.dead == false && car.ptInObst(car.x, car.y) == false) { | |
// Make sure car is alive | |
car.updatePos(); | |
//Make Decision | |
//Get obstacle detection distances | |
let inputs = tf.tensor2d([car.getCollisDist()]); | |
// Predict the value | |
let decArr = car.model.predict(inputs).dataSync(); | |
//Adjust angle and velocity based on outputs | |
car.v += 0.1 * decArr[0]; // Accelerate | |
car.v -= 0.2 * decArr[1]; // Brake | |
car.angle -= 0.1 * decArr[2]; // Turn Left | |
car.angle += 0.1 * decArr[3]; // Turn Right | |
} else { | |
car.dead = true; // Kill the car if it is in obstacle | |
} | |
}); | |
frameCount += 1; | |
// If 200 frames has passed plus a scaled of the highestScore, then start next generation | |
if (frameCount > 200 + topFitnessScore * 3) { | |
startNewGen(); | |
frameCount = 0; | |
} else if (frameCount > 80) { // Check for inactivity after 80 frames | |
let stillActive = false; | |
// Make sure atleast one car has made progress without dying | |
cars.forEach((car) => { | |
if (car.fitness > 3 && car.dead == false) { | |
stillActive = true; | |
} | |
}); | |
if (stillActive == false) { | |
startNewGen(); | |
frameCount = 0; | |
} | |
} | |
} | |
function endGen() { | |
let totalFitness = 0; | |
bestOfGen = 0; | |
// Calculate total fitness and find highest performing car | |
cars.forEach((car, ind) => { | |
car.dead = true; | |
totalFitness += car.fitness; | |
if (car.fitness > bestOfGen) { | |
bestOfGen = car.fitness; | |
} | |
}); | |
//Update highest score if applicable | |
if (bestOfGen > topFitnessScore) { | |
topFitnessScore = bestOfGen; | |
} | |
return totalFitness; | |
} | |
function selectRandParent(tFit) { | |
let randVal = tFit * random(); // Random value between (0, tFit) | |
let count = 0; // Cumulitive Fitness | |
for (let i = 0; i < cars.length; i++) { | |
count += cars[i].fitness; | |
if (randVal <= count) { | |
return i; | |
} | |
} | |
} | |
function startNewGen() { | |
totalFitness = endGen(); | |
let newCarArr = []; // The next generation | |
mutationRate = mrateInput.value() // Pull mutation rate | |
//Get parent weights | |
let parentWeights = []; | |
cars.forEach((c) => { | |
parentWeights.push(c.copyWeights()); | |
}); | |
//Create next generation | |
for (let i = 0; i < 10; i++) { | |
// Initialize Child | |
const child = new Car( | |
outerDi, | |
innerDi, | |
screenHt, | |
screenWd, | |
carDi, | |
obsts | |
); | |
let currWeights = child.model.getWeights(); | |
let newWeights = []; | |
for (let i = 0; i < currWeights.length; i++) { | |
// Iterate through each layer | |
//Get Current Weight information | |
let currVals = currWeights[i].dataSync(); // Array format | |
let shape = currWeights[i].shape; | |
for (let j = 0; j < currVals.length; j++) { | |
// Iterate through each weight | |
// Select trait from random parent | |
currVals[j] = parentWeights[selectRandParent(totalFitness)][i][j]; | |
// Assign random value to weight from Gaussian Distribution(Function from P5) | |
if(random() < mutationRate) { | |
currVals[j] += randomGaussian() | |
} | |
} | |
let newTens = tf.tensor(currVals, shape); | |
newWeights[i] = newTens; | |
} | |
child.model.setWeights(newWeights); | |
newCarArr.push(child); | |
} | |
cars = newCarArr; | |
// Increment Generation and Update Label | |
currGen += 1; | |
infoLbl.elt.innerText = | |
"Gen: " + currGen + ", Top Score: " + topFitnessScore; | |
} | |
</script> | |
<title>Learning to Drive w/ Genetic Algorithm</title> | |
</head> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment