From 7ff8c1b0068e299e891ed995570be224837b0b87 Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Wed, 13 Dec 2017 00:19:08 +0100 Subject: [PATCH] Implement hebbian didabots --- LemmingSim.html | 2 +- lemming_sim_student.js | 170 +++++++++++++++++++++++++++++++++++------ sensors.js | 13 ++++ utils.js | 4 + 4 files changed, 163 insertions(+), 26 deletions(-) diff --git a/LemmingSim.html b/LemmingSim.html index 578c4e8..b054427 100644 --- a/LemmingSim.html +++ b/LemmingSim.html @@ -53,7 +53,7 @@ along with this program. If not, see . -x +x diff --git a/lemming_sim_student.js b/lemming_sim_student.js index d5071ae..97ab518 100644 --- a/lemming_sim_student.js +++ b/lemming_sim_student.js @@ -30,11 +30,11 @@ Feel free to use any of this code. "use strict" // Simulation settings; please change anything that you think makes sense. var simInfo = { - maxSteps: 50000, // maximal number of simulation steps to run + maxSteps: 20000, // maximal number of simulation steps to run airDrag: 0.1, // "air" friction of enviroment; 0 is vacuum, 0.9 is molasses - boxFric: 0.001, // friction between boxes during collisions + boxFric: 0.005, // friction between boxes during collisions boxMass: 0.01, // mass of boxes - boxSize: 10, // size of the boxes, in pixels + boxSize: 15, // size of the boxes, in pixels robotSize: 13, // approximate robot radius, in pixels (note the SVG gets scaled down) robotMass: 0.4, // robot mass (a.u) gravity: 0, // constant acceleration in Y-direction @@ -43,8 +43,97 @@ var simInfo = { debugMouse: true, // allow dragging any object with the mouse wanderRate: 0.0002, }; +class SVGRobot extends Robot { + makeBody(robotInfo) { + // load robot's body shape from SVG file + const bodySVGpoints = loadFromSVG(); + return Matter.Bodies.fromVertices(robotInfo.init.x, + robotInfo.init.y, + bodySVGpoints, { + frictionAir: simInfo.airDrag, + mass: simInfo.robotMass, + color: [255, 255, 255], + role: 'robot' + }, true); + } +} +class HebbDidaBot extends Robot { + constructor(props) { + const angle = Math.PI/3.5 + super(Object.assign({ + learningRate: 0.001, + forgettingRate: 0.001, + sensors: [ + new CollisionSensor('collL', { + attachAngle: -angle + }), + new CollisionSensor('collR', { + attachAngle: angle + }), + new DistanceSensor('distL', { + attachAngle: -angle + }), + new DistanceSensor('distR', { + attachAngle: angle + }), + ].concat(props.sensors || []) + }, props)) + this.display = document.createElement('canvas') + this.display.width = this.display.height = 100 + this.display.style.border = '1px solid black' + this.weights = [[Math.random()/10,Math.random()/10],[Math.random()/10,Math.random()/10]] + } + move() { + const {distL, distR, collL, collR} = this.getSensors() + // feed forward: perception + // collision + function feedforward(weights, activations, intrinsic) { + return intrinsic.map((x, i) => { + return x + sum(zip(weights[i], activations).map(([a,b]) => a*b)) + }) + } + function hebb(weight, lrate, frate, act_src, act_dst) { + const avg = sum(act_src) / act_src.length + return weight.map((row, i) => row.map((wij, j) => + wij + (lrate * (act_src[j] * act_dst[i]) - frate * avg * wij) / act_src.length + )) + } + // input: + const motorweights = [[0,1],[1,0]] + const collisionAct = [+collL, +collR] + const threshold = theta => x => x > theta + const percact = [distL, distR].map(x => x == Infinity ? 0 : (50 - x)/50) + const collact = [+collL, +collR] + const collisionlayer = feedforward(this.weights, percact, collact) + const motorLayer = feedforward(motorweights, collisionlayer, [0,0]) + this.weights = hebb(this.weights, this.info.learningRate, this.info.forgettingRate, percact, collisionlayer) + const [left, right] = motorLayer.map(threshold(0.5)) + // output: 2 neurons. if left is activated, turn left. if right is activated, turn right. + // if both, go backwards. otherwise: forward + if (left && !right) this.rotate(-0.003) + else if (!left && right) this.rotate(+0.003) + else if (left && right) this.drive(-2e-4) + else this.drive(2e-4) + this.layers = [percact, collisionlayer, motorLayer] + } + getDisplay() { + return this.layers + } + updateDisplay() { + const ctx = this.display.getContext('2d') + if (!this.weights) return + ctx.clearRect(0,0,100,100) + this.weights.forEach((a, i) => { + a.forEach((b, j) => { + ctx.fillStyle = convrgb([b*128, b*128, b*128].map(x => 255-x)) + const [x,y] = [j*50,i*50] + ctx.fillRect(x, y, x+50, y+50) + }) + }) + } +} // our own lemming implementation -class Lemming extends Robot { +class Lemming extends SVGRobot { constructor(props) { super(Object.assign({ sensors: [ @@ -321,7 +410,7 @@ function initSimulation() { sim.init() let noRobots = +document.getElementById('robotAmnt').value while(noRobots--) { - sim.addRobot(new Lemming({ + sim.addRobot(new HebbDidaBot({ // random color color: [Math.random()*255, Math.random()*255, Math.random()*255], // color of the robot shape init: {x: 50*noRobots + 50, y: 50, angle: 0}, // initial position and orientation @@ -359,10 +448,33 @@ function redblocksatwall(sim) { }) return result } +function countclusters(sim) { + const boxes = sim.world.composites[0].bodies + function dist(box1, box2) { + return Vec2.dist(box1.position, box2.position) + } + const clusters = [] + const maxdist = 2 * Math.sqrt(2) * simInfo.boxSize + const unclaimed = new Set(boxes) + while(unclaimed.size) { + const box = unclaimed.entries().next().value[0] + const queue = new Set([box]) + let no_boxes = 0 + while(queue.size) { + const box2 = queue.entries().next().value[0] + unclaimed.delete(box2) + queue.delete(box2) + no_boxes++ + Array.from(unclaimed).filter(box3 => (dist(box2, box3) < maxdist)).forEach(x => queue.add(x)) + } + clusters.push(no_boxes) + } + return clusters.sort().reverse() +} // automatically run the experiments based on the values from the html -async function experimentRunner(stopCondition=blocksSorted) { +async function experimentRunner(stopCondition) { initSimulation() - while(!stopCondition(sim) && sim.curSteps < simInfo.maxSteps) { + while((stopCondition ? !stopCondition(sim) : true) && sim.curSteps < simInfo.maxSteps) { sim.runSteps(500) // take a break for the gui to stay responsive await promiseTimeout(50) @@ -371,8 +483,9 @@ async function experimentRunner(stopCondition=blocksSorted) { // save data const rv = { steps: sim.curSteps, - done: stopCondition(sim), - redblocks: redblocksatwall(sim), + //done: stopCondition(sim), + //redblocks: redblocksatwall(sim), + clusters: countclusters(sim), robots: sim.robots.length } sim.destroy() @@ -394,15 +507,15 @@ class ResultsTable { clear() { this.elem.innerHTML = "Experiment Results" const tr = document.createElement('tr') - const headers = ["robots", "steps", "sorted", "red blocks@wall"] + const headers = ["robots", "steps", "clusters"] headers.forEach(txt => { tr.appendChild(document.createElement('th')).appendChild(document.createTextNode(txt)) }) this.elem.appendChild(tr) } - add({steps, done, redblocks, robots}) { + add({steps, clusters, robots}) { const tr = document.createElement('tr') - ;[robots, steps, done, redblocks].forEach(txt => { + ;[robots, steps, clusters.toString()].forEach(txt => { tr.appendChild(document.createElement('td')).appendChild(document.createTextNode(txt)) }) this.elem.appendChild(tr) @@ -427,6 +540,9 @@ class Bay { robot.sensors.forEach(sensor => { makeInteractiveElement(sensor, this.elem) }) + const bayDisplay = document.getElementById('bayDisplay') + bayDisplay.innerHTML = "" + bayDisplay.appendChild(robot.display) this.repaint() } repaint() { @@ -447,7 +563,7 @@ class Bay { }).join('') document.getElementById('SensorLabel').innerHTML = sensorString; - document.getElementById('bayDisplay').innerHTML = robot.getDisplay() + robot.updateDisplay() } } transformMouse({x,y}) { @@ -470,19 +586,8 @@ function loadFromSVG() { return vertexSets; }; - function Robot(robotInfo) { - // load robot's body shape from SVG file - const bodySVGpoints = loadFromSVG(); - this.body = Matter.Bodies.fromVertices(robotInfo.init.x, - robotInfo.init.y, - bodySVGpoints, { - frictionAir: simInfo.airDrag, - mass: simInfo.robotMass, - color: [255, 255, 255], - role: 'robot' - }, true); - + this.body = this.makeBody(robotInfo) Matter.World.add(sim.world, this.body); Matter.Body.setAngle(this.body, robotInfo.init.angle); @@ -492,8 +597,20 @@ function Robot(robotInfo) { // attach its helper functions this.info = robotInfo; + this.display = document.createElement('span') } Object.assign(Robot.prototype, { + makeBody(robotInfo) { + const nSides = 20, + circle = Matter.Bodies.circle; + return circle(robotInfo.init.x, robotInfo.init.y, simInfo.robotSize, + { + frictionAir: simInfo.airDrag, + mass: simInfo.robotMass, + color: [255, 255, 255], + role: 'robot' + }, nSides) + }, rotate(torque=0) { /* Apply a torque to the robot to rotate it. * @@ -506,6 +623,9 @@ Object.assign(Robot.prototype, { getDisplay() { return "" }, + updateDisplay() { + this.display.innerHTML = this.getDisplay() + }, drive(force=0) { /* Apply a force to the robot to move it. diff --git a/sensors.js b/sensors.js index 445849d..9c4ad79 100644 --- a/sensors.js +++ b/sensors.js @@ -230,3 +230,16 @@ class Gyroscope extends Sensor { this.valueStr = format(this.value) + '°' } } + +class CollisionSensor extends DistanceSensor { + constructor(id, props) { + super(id, Object.assign({ + maxVal: 8 + }, props)) + } + sense() { + super.sense() + this.value = this.value != Infinity + this.valueStr = this.value ? "yes" : "no" + } +} diff --git a/utils.js b/utils.js index 01f6355..be8315b 100644 --- a/utils.js +++ b/utils.js @@ -48,3 +48,7 @@ function gaussNoise(sigma=1) { const x1 = 1.0 - Math.random(); return sigma * Math.sqrt(-2 * Math.log(x0)) * Math.cos(2 * Math.PI * x1); }; +const zip = (arr, ...arrs) => { + return arr.map((val, i) => arrs.reduce((a, arr) => [...a, arr[i]], [val])); +} +const sum = (arr) => arr.reduce((a,b) => a+b)