genetic algorithm implementation

This commit is contained in:
Robert Bendun 2021-06-19 08:45:42 +02:00
parent 82c3de94ef
commit 8e7b565dd7
11 changed files with 402 additions and 118 deletions

View File

@ -18,8 +18,10 @@
<script src="configuration.js"></script> <script src="configuration.js"></script>
<script src="utilities.js"></script> <script src="utilities.js"></script>
<script src="logic/product.js"></script> <script src="logic/product.js"></script>
<script src="logic/knowledge.js"></script>
<script src="logic/time.js"></script> <script src="logic/time.js"></script>
<script src="logic/shelf.js"></script> <script src="logic/shelf.js"></script>
<script src="logic/evolution.js"></script>
<script src="view/floor.js"></script> <script src="view/floor.js"></script>
<script src="view/grid.js"></script> <script src="view/grid.js"></script>
@ -28,9 +30,9 @@
<script src="view/agentController.js"></script> <script src="view/agentController.js"></script>
<script src="logic/agent.js"></script> <script src="logic/agent.js"></script>
<script src="view/products.js"></script> <script src="view/products.js"></script>
<script src="logic/knowledge.js"></script>
<script src="logic/shop.js"></script> <script src="logic/shop.js"></script>
<script src="logic/costMap.js"></script> <script src="logic/costMap.js"></script>
<script src="view/visualisation.js"></script>
<!-- Main script file --> <!-- Main script file -->
<script src="main.js"></script> <script src="main.js"></script>
@ -77,7 +79,7 @@
</div> </div>
<div class="logs-label"> <div class="logs-label">
<span>LOGS</span> <span>INFO</span>
</div> </div>
<div class="content"> <div class="content">
@ -279,7 +281,20 @@
<div class="logs-wrapper"> <div class="logs-wrapper">
<div class="logs-content"> <div class="logs-content">
<!-- All logs should go here --> <div>
Nr <b id="populationIndex"></b> of <b id="populationSize"></b>
</div>
<div>
Similarity:
<b id="similarity"></b>
</div>
<div>
Best: <b id="best"></b>
</div>
<div>Worst: <b id="worst"></b></div>
<ul id="history">
</ul>
</div> </div>
</div> </div>

View File

@ -10,8 +10,7 @@ class Agent extends AgentController {
const cycle = async () => { const cycle = async () => {
const jobRequest = Products.instance.getJobRequest(); const jobRequest = Products.instance.getJobRequest();
if (jobRequest) { if (jobRequest) {
const requiredAmount = 50 - jobRequest.amount; await this.takeFromStore(jobRequest.product, jobRequest.amount);
await this.takeFromStore(jobRequest.product, requiredAmount);
await this.deliver(jobRequest.x, jobRequest.y); await this.deliver(jobRequest.x, jobRequest.y);
} else { } else {
await waitFor(1000); await waitFor(1000);
@ -44,7 +43,7 @@ class Agent extends AgentController {
Products.instance.exchange(x, y, this.ownedProduct, 50); Products.instance.exchange(x, y, this.ownedProduct, 50);
this.ownedProductAmount = 0; this.ownedProductAmount = 0;
this.ownedProduct = Product.REGISTRY.empty; this.ownedProduct = null;
} }
/** /**

View File

@ -36,43 +36,3 @@ class CostMap {
} }
} }
} }
class CostMapVisualisation {
static instance
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d');
canvas.width = 2000
canvas.height = 900
CostMapVisualisation.instance = this
document.addEventListener('keydown', ({ key }) => {
if (key !== 'c')
return
if (this.hasBeenUpdated) {
this.ctx.clearRect(0, 0, 2000, 900);
} else {
this.update();
}
this.hasBeenUpdated = !this.hasBeenUpdated
})
}
update() {
CostMap.instance.values.forEach((cost, i) => {
const y = Math.floor(i / 20) * 100;
const x = (i % 20) * 100;
let color = Math.floor((cost * 255)).toString(16)
if (color.length == 1)
color = "0" + color
this.ctx.fillStyle = '#' + color + color + color;
this.ctx.fillRect(x, y, 100, 100)
})
}
}

251
src/logic/evolution.js Normal file
View File

@ -0,0 +1,251 @@
const ShelfCount = 17 * 6;
const InRow = 3 * 6;
// index to abstract coordinates
function i2ac(index) {
const y = Math.floor(index / InRow);
const x = (index % InRow);
if (y === 5 && x >= 6)
return [x + 6, y];
return [x, y];
}
function ac2i(x, y) {
return y * InRow + x - (y === 5 && x >= 6) * 6;
}
// index to grid coordinates
function i2gc(index) {
const [x, y] = i2ac(index);
return [x + Number(x > 11) + Number(x > 5), y + Math.floor((y + 1) / 2)];
}
function gc2i(x, y) {
x -= Number(x >= 13) + Number(x >= 6);
switch (y) {
case 0: y = 0; break;
case 2: y = 1; break;
case 3: y = 2; break;
case 5: y = 3; break;
case 6: y = 4; break;
case 8: y = 5; break;
}
return ac2i(x, y);
}
class Arrangement {
generateRandom() {
this.products = [...Array(ShelfCount).keys()].map(() => Knowledge.semanticNetwork.getRandom().name);
this.productsAmount = null;
this.similarities = null;
}
constructor(products = []) {
this.products = products;
this.productsAmount = null;
this.similarities = null;
}
clone() {
return JSON.parse(JSON.stringify(this.products));
}
simulate() {
this.similarities ||= [...Array(ShelfCount)].map(() => 0);
this.productsAmount ||= [...Array(ShelfCount)].map(() => 50);
this.similarity = 0;
this.products.forEach((product, i) => {
let similarityWithNeighbours = 0;
const neighbours = neighboursByIndex(i);
for (const neighbour of neighbours) {
similarityWithNeighbours += similarity(product, this.products[neighbour]);
}
similarityWithNeighbours /= neighbours.length;
this.similarities[i] = 1 - similarityWithNeighbours;
this.similarity += this.similarities[i];
});
this.similarity /= ShelfCount;
}
}
const PopulationSize = 31 * 2;
const PopulationToKeep = 0.3;
const MutationRate = 0.2;
class Evolution extends Arrangement {
constructor() {
super();
this.generate();
this.setup();
this.productIndex = 0;
this.productsAmount = [...Array(ShelfCount)].map(() => 0);
}
generate() {
this.population = [...Array(PopulationSize)].map(() => {
const arr = new Arrangement();
arr.generateRandom();
return arr;
});
}
setup() {
this.populationIndex = 0;
this.population.forEach(x => x.simulate());
this.min_similarity = this.population.reduce((p, c) => Math.min(p, c.similarity || 1), 1);
this.max_similarity = this.population.reduce((p, c) => Math.max(p, c.similarity || 0), 0);
}
next(days) {
for (let i = 0; i < Number(days) - 1; ++i) {
this.step(false);
}
this.step(true);
}
step(render = true) {
// Complete previous day
this.products = this.population[this.populationIndex].products;
this.simulate();
this.productsAmount = [...Array(ShelfCount)].map((_, i) => Math.round(50 * this.similarities[i]));
const productsSold = ShelfCount * 50 - this.productsAmount.reduce((p, c) => p + c, 0);
// Setup new day
this.populationIndex++;
this.productIndex = 0;
// Generate new population if needed
if (this.populationIndex >= PopulationSize) {
/**
* @type{Arrangement[]}
*/
let newPopulation = [];
this.population.sort((a, b) => a.similarity - b.similarity);
let fitness = this.population.map(({ similarity }) => similarity);
let sum = fitness.reduce((p, c) => p + c, 0);
fitness = fitness.map(f => Math.pow(f / sum, 2));
const D = PopulationToKeep * PopulationSize;
for (let i = 0; i < this.population.length; ++i) {
const candidateA = this.population[pick(fitness)];
const candidateB = this.population[pick(fitness)];
const candidate = crossover(candidateA, candidateB);
mutate(candidate);
newPopulation.push(candidate);
}
this.population = this.population.slice(0, D).concat(newPopulation).slice(0, PopulationSize);
this.setup();
}
if (render) {
this.update();
console.log(productsSold)
}
}
getJobRequest() {
if (this.productIndex > ShelfCount)
return null;
for (; this.productIndex < ShelfCount; ++this.productIndex) {
const inPopulation = this.population[this.populationIndex];
let amount = 50;
if (this.products[this.productIndex] === inPopulation.products[this.productIndex]) {
if (this.productsAmount[this.productIndex] == 50)
continue;
amount -= this.productsAmount[this.productIndex];
}
const [x, y] = i2gc(this.productIndex++);
return {
product: Knowledge.semanticNetwork.findProductByName(this.population[this.populationIndex].products[this.productIndex]),
amount,
x, y
}
}
}
getSimilarity() {
try {
this.population[this.populationIndex].simulate();
return this.population[this.populationIndex].similarity;
} catch (e) {
return this.similarity;
}
}
}
/**
*
* @param {Arrangement} arr
*/
function mutate(arr) {
const { floor, random } = Math;
for (let i = 0; i < ShelfCount / 6; ++i) {
while (random() <= MutationRate) {
const a = floor(random() * arr.products.length);
const b = floor(random() * arr.products.length);
if (a !== b) {
let t = arr.products[a];
arr.products[a] = arr.products[b];
arr.products[b] = t;
}
}
}
}
/**
* @param {Arrangement} ca
* @param {Arrangement} cb
*/
function crossover(ca, cb) {
const child = [];
for (let i = 0; i < ca.products.length; ++i)
child.push(ca.similarities[i] > cb.similarities[i] ? ca.products[i] : cb.products[i]);
return new Arrangement(child);
}
function neighboursByIndex(index) {
const [bx, by] = i2ac(index);
const at = (dx, dy) => {
const x = bx + dx;
const y = by + dy;
if (by != y) {
if ((by === 1 || by === 3) && (by - y < 0)) return [];
if ((by === 2 || by === 4) && (by - y > 0)) return [];
}
if (x >= 0 && y >= 0) {
const index = ac2i(x, y);
if (index < ShelfCount)
return [index];
}
return [];
};
return [at(0, -1), at(-1, 0), at(1, 0), at(0, 1)].flat();
}
// given two products, find how similar they are based on categories
// similiary is defined as euclidian distance
function similarity(leftProductName, rightProductName) {
const lhs = Knowledge.semanticNetwork.findByName(leftProductName);
const rhs = Knowledge.semanticNetwork.findByName(rightProductName);
console.assert(lhs && rhs);
const categories = new Set([...lhs.categories, ...rhs.categories]);
const d = 1 / categories.size;
let similarity = 0;
for (const category of categories) {
similarity += Math.pow(
(lhs.categories.indexOf(category) >= 0 ? d : 0) -
(rhs.categories.indexOf(category) >= 0 ? d : 0), 2);
}
return Math.sqrt(similarity);
}

View File

@ -1,6 +1,6 @@
class Time { class Time {
isPaused = false; isPaused = true;
timeMultiplier = 1; timeMultiplier = 1;
day = 0; day = 0;
@ -58,7 +58,13 @@ class Time {
* Add days * Add days
*/ */
addDays(days) { addDays(days) {
Products.instance.next(days);
Products.instance.update();
const ul = document.querySelector('ul#history');
this.setDay(this.day += Number(days)); this.setDay(this.day += Number(days));
const li = document.createElement('li');
li.innerHTML = `${this.day}: ${nice(Products.instance.max_similarity)}`;
ul.appendChild(li);
} }
} }

View File

@ -10,12 +10,11 @@ window.addEventListener('DOMContentLoaded', async () => {
const costMap = new CostMap(); const costMap = new CostMap();
const time = new Time(); const time = new Time();
const timeController = new TimeController(); const timeController = new TimeController();
const costMapVisualisation = new CostMapVisualisation(costCanvas);
const floor = new Floor(floorCanvas); const floor = new Floor(floorCanvas);
const grid = new Grid(gridCanvas); const grid = new Grid(gridCanvas);
const agent = new Agent(agentCanvas); const agent = new Agent(agentCanvas);
await Knowladge.loadFromFile('/data/products.tsv') await Knowledge.loadFromFile('/data/products.tsv')
const products = new Products(productsCanvas); const products = new Products(productsCanvas);
const fpsElement = document.getElementById('fps'); const fpsElement = document.getElementById('fps');

View File

@ -107,6 +107,8 @@ main {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: auto; overflow: auto;
padding: 10px;
box-sizing: border-box;
} }
.logs-content::-webkit-scrollbar { .logs-content::-webkit-scrollbar {

View File

@ -41,3 +41,13 @@ function Enum(...labels) {
} }
return e return e
} }
function pick(array) {
let index = 0;
for (let r = Math.random(); r >= 0; r -= array[index]) {
index++;
}
return index - 1;
}
function nice(v) { return `${(v * 100).toFixed(1)}%` }

View File

@ -4,7 +4,7 @@ class AgentController {
this.ctx = canvas.getContext('2d'); this.ctx = canvas.getContext('2d');
this.setCanvasSize(canvas); this.setCanvasSize(canvas);
this.ownedProduct = Product.REGISTRY.empty; this.ownedProduct = null;
this.ownedProductAmount = 0; this.ownedProductAmount = 0;
// Akcje // Akcje
@ -242,6 +242,7 @@ class AgentController {
this.ctx.rotate(-rotation * Math.PI / 180); this.ctx.rotate(-rotation * Math.PI / 180);
// Product // Product
if (this.ownedProduct) {
this.ctx.font = '900 40px "Font Awesome 5 Free"'; this.ctx.font = '900 40px "Font Awesome 5 Free"';
this.ctx.textAlign = 'center'; this.ctx.textAlign = 'center';
this.ctx.fillStyle = 'white'; this.ctx.fillStyle = 'white';
@ -254,6 +255,7 @@ class AgentController {
this.ctx.fillStyle = 'white'; this.ctx.fillStyle = 'white';
this.ctx.fillText(this.ownedProductAmount, 10, 35); this.ctx.fillText(this.ownedProductAmount, 10, 35);
} }
}
this.ctx.translate(-(newX + (w / 2)), -(newY + (h / 2))); this.ctx.translate(-(newX + (w / 2)), -(newY + (h / 2)));
} }

View File

@ -9,38 +9,20 @@ function filterReduce2D(array, reducer, filter, init) {
return result; return result;
} }
class Products { class Products extends Evolution {
/**
* @type{Products}
*/
static instance static instance
constructor(canvas) { constructor(canvas) {
const { random, floor } = Math super();
// this.generateRandom();
this.gridProducts = []
this.gridProductsAmount = []
this.min = 50;
for (let i = 0; i < 20; ++i) {
const gridProductsColumn = []
const gridProductsAmountColumn = []
for (let j = 0; j < 9; ++j) {
if (j % 3 !== 1 && (i+1) % 7 !== 0) {
gridProductsColumn.push(Product.RANDOM_FROM_REGISTRY());
const amount = floor(random() * 51);
gridProductsAmountColumn.push(amount);
this.min = this.min > amount ? amount : this.min;
} else {
gridProductsColumn.push('');
gridProductsAmountColumn.push('');
}
}
this.gridProducts.push(gridProductsColumn);
this.gridProductsAmount.push(gridProductsAmountColumn);
}
this.ctx = canvas.getContext('2d'); this.ctx = canvas.getContext('2d');
this.setCanvasSize(canvas); this.setCanvasSize(canvas);
this.update(); this.update();
Products.instance = this Products.instance = this
@ -51,33 +33,47 @@ class Products {
canvas.height = 900; canvas.height = 900;
} }
update(){ update() {
function write(tag, text) {
const qr = document.querySelector(tag);
if (qr) {
qr.innerHTML = text;
}
}
write('#similarity', nice(this.getSimilarity()));
write('#populationIndex', this.populationIndex + 1);
write('#populationSize', PopulationSize);
write('#best', nice(this.max_similarity));
write('#worst', nice(this.min_similarity));
this.clearCanvas(); this.clearCanvas();
this.drawProducts(); this.drawProducts();
} }
drawProducts () { drawProducts() {
for (let [x, line] of Grid.instance.grid.entries()) { this.min = this.productsAmount?.reduce((p, c) => Math.min(p, c), 50) || 50;
for (let [y, type] of line.entries()) { this.products.forEach((product, index) => {
if (type === GRID_FIELD_TYPE.SHELF) { let [x, y] = i2gc(index);
let v = this.drawValue(x, y); x *= 100; y *= 100;
this.product(x * 100, y * 100, v, x, y); const amount = this.productsAmount === null ? 50 : this.productsAmount[index];
} this.drawAmount(x, y, amount);
} const { icon } = Knowledge.semanticNetwork.findProductByName(product);
} this.product(x, y, icon, amount);
})
} }
drawValue(x, y) { drawAmount(x, y, amount) {
let fontSize = 20; let fontSize = 20;
this.ctx.font = `${fontSize}px Montserrat`; this.ctx.font = `${fontSize}px Montserrat`;
this.ctx.textAlign = "left"; this.ctx.textAlign = "left";
this.ctx.fillStyle = 'white'; this.ctx.fillStyle = 'white';
let number = this.gridProductsAmount[x][y]; this.ctx.fillText(amount, x + 10, y + 90);
this.ctx.fillText(number, (x * 100) + 10, (y * 100) + 90);
return number;
} }
product(x, y, v, productsKey, productsValue) { product(x, y, icon, amount) {
let fontSize = 40; let fontSize = 40;
this.ctx.font = `900 ${fontSize}px "Font Awesome 5 Free"`; this.ctx.font = `900 ${fontSize}px "Font Awesome 5 Free"`;
this.ctx.textAlign = "center"; this.ctx.textAlign = "center";
@ -86,17 +82,15 @@ class Products {
// white - full shelf // white - full shelf
// red - empty shelf // red - empty shelf
const t = Math.cbrt const t = Math.cbrt
v = (v - this.min) / (50 - this.min); const v = (amount - this.min) / (50 - this.min);
let color = Math.floor(t(v) * 255).toString(16) let color = Math.floor(t(v) * 255).toString(16)
if (color.length == 1) if (color.length == 1)
color = "0" + color color = "0" + color
this.ctx.fillStyle = `#ff${color}${color}`; this.ctx.fillStyle = `#ff${color}${color}`;
let productsColumn = this.gridProducts[productsKey];
let prdct = productsColumn[productsValue];
this.ctx.font = '900 40px "Font Awesome 5 Free"'; this.ctx.font = '900 40px "Font Awesome 5 Free"';
this.ctx.textAlign = 'center'; this.ctx.textAlign = 'center';
this.ctx.fillText(prdct.icon, x+50, y+60); this.ctx.fillText(icon, x + 50, y + 60);
} }
clearCanvas() { clearCanvas() {
@ -142,13 +136,13 @@ class Products {
* @returns {{product: Product; amount: number} | null } Old shelf content * @returns {{product: Product; amount: number} | null } Old shelf content
*/ */
exchange(x, y, incomingProduct, incomingAmount) { exchange(x, y, incomingProduct, incomingAmount) {
const product = this.gridProducts[x][y]; const index = gc2i(x, y);
const amount = this.gridProductsAmount[x][y]; const product = this.products[index];
this.gridProducts[x][y] = incomingProduct; const amount = this.productsAmount[index];
this.gridProductsAmount[x][y] = incomingAmount; this.products[index] = incomingProduct.name;
this.productsAmount[index] = incomingAmount;
this.min = filterReduce2D(this.gridProductsAmount, (p, c) => Math.min(p, c), this.min = this.productsAmount?.reduce((p, c) => Math.min(p, c), 50) || 50;
x => typeof x === 'number', 50);
this.clearCanvas(); this.clearCanvas();
this.drawProducts(); this.drawProducts();
@ -170,13 +164,6 @@ class Products {
return this.exchange(x, y, incomingProduct, incomingAmount) return this.exchange(x, y, incomingProduct, incomingAmount)
} }
getJobRequest() {
const maybeProduct = this.filter((_product, amount, _i, _j, end) => amount < 50).sort((a, b) => {
return a.amount - b.amount
})
return maybeProduct.length === 0 ? null : maybeProduct[0]
}
findAvailableLocationsFor(product) { findAvailableLocationsFor(product) {
return this.filter((p, amount) => product.equals(p) && amount < 50) return this.filter((p, amount) => product.equals(p) && amount < 50)
} }

53
src/view/visualisation.js Normal file
View File

@ -0,0 +1,53 @@
class Visualisation {
static instance
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d');
canvas.width = 2000
canvas.height = 900
Visualisation.instance = this
document.addEventListener('keydown', ({ key }) => {
const keys = {
c: () => this.drawCostMap(),
p: () => this.drawSimilarities(),
};
this.ctx.clearRect(0, 0, 2000, 900);
if (this.lastkey != key) {
for (const k in keys)
if (k == key) {
keys[k]();
this.lastkey = key;
}
} else
this.lastkey = null;
})
}
drawSimilarities() {
Products.instance.similarities.map((similarity, index) => {
const [x, y] = i2gc(index);
let color = Math.floor(similarity * 120);
this.ctx.fillStyle = `hsl(${color}, 100%, 50%)`;
this.ctx.fillRect(x * 100, y * 100, 100, 100)
})
}
drawCostMap() {
CostMap.instance.values.forEach((cost, i) => {
const y = Math.floor(i / 20) * 100;
const x = (i % 20) * 100;
let color = Math.floor((cost * 255)).toString(16)
if (color.length == 1)
color = "0" + color
this.ctx.fillStyle = '#' + color + color + color;
this.ctx.fillRect(x, y, 100, 100)
})
}
}