Revamp neighbor sampling, shredding unused features and correct calculations

This commit is contained in:
Pitchaya Boonsarngsuk
2018-01-26 13:33:07 +00:00
parent 44a5c8561a
commit 97d8b71963
4 changed files with 121 additions and 231 deletions

View File

@@ -157,12 +157,6 @@
<input type="range" min="1" max="100" value="3" step="1" oninput="d3.select('#hlsampleSizeSliderOutput').text(value); SAMPLE_SIZE=value;">
</label>
<br/>
<label title="DistanceRange">
Subset: Distance Range
<output id="hldistanceRangeSliderOutput">10</output><br/>
<input type="range" min="1" max="100" value="10" step="1" oninput="d3.select('#hldistanceRangeSliderOutput').text(value); SELECTED_DISTANCE=value;">
</label>
<br/>
<label title="Number of iterations done at the end">
Full: Iterations
<output id="fullIterationsSliderOutput">20</output><br/>
@@ -180,12 +174,6 @@
<output id="hlFullsampleSizeSliderOutput">3</output><br/>
<input type="range" min="1" max="100" value="3" step="1" oninput="d3.select('#hlFullsampleSizeSliderOutput').text(value); FULL_SAMPLE_SIZE=value;">
</label>
<br/>
<label title="DistanceRange">
Full: Distance Range
<output id="hlFulldistanceRangeSliderOutput">10</output><br/>
<input type="range" min="1" max="100" value="10" step="1" oninput="d3.select('#hlFulldistanceRangeSliderOutput').text(value); FULL_SELECTED_DISTANCE=value;">
</label>
</div>
<input id="NSButton" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startNeighbourSamplingSimulation)">Neighbour
@@ -202,12 +190,6 @@
<output id="sampleSizeSliderOutput">3</output><br/>
<input type="range" min="1" max="100" value="3" step="1" oninput="d3.select('#sampleSizeSliderOutput').text(value); SAMPLE_SIZE=value;">
</label>
<br/>
<label title="DistanceRange">
Distance Range
<output id="distanceRangeSliderOutput">10</output><br/>
<input type="range" min="1" max="100" value="10" step="1" oninput="d3.select('#distanceRangeSliderOutput').text(value); SELECTED_DISTANCE=value;">
</label>
</div>
<input class="noParameters" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startLinkSimulation); tweakedVerOfLink=false;">Link force in D3<br>
<input class="noParameters" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startLinkSimulation); tweakedVerOfLink=true; ">Link force (tweaked)<br>

View File

@@ -57,10 +57,8 @@ var MULTIPLIER = 50,
FULL_ITERATIONS = 20,
NODE_SIZE = 10,
COLOR_ATTRIBUTE = "",
SELECTED_DISTANCE = 10,
FULL_NEIGHBOUR_SIZE = 6,
FULL_SAMPLE_SIZE = 3,
FULL_SELECTED_DISTANCE = 10;
FULL_SAMPLE_SIZE = 3;
// Create a color scheme for a range of numbers.
var color = d3.scaleOrdinal(d3.schemeCategory10);
@@ -205,12 +203,12 @@ function ticked() {
intercom.emit("passedData", simulation.force(forceName).distributionData());
}
if(alreadyRanIterations == ITERATIONS) {
simulation.stop();
ended();
}
}
function ended() {
simulation.stop();
console.log("ended");
if (rendering !== true) { // Never drawn anything before? Now it's time.
node

View File

@@ -4,25 +4,24 @@
function startNeighbourSamplingSimulation() {
console.log("startNeighbourSamplingSimulation");
//springForce = true;
alreadyRanIterations = 0;
simulation.stop();
p1 = performance.now();
let force = d3.forceNeighbourSamplingDistance()
.neighbourSize(NEIGHBOUR_SIZE)
.sampleSize(SAMPLE_SIZE)
.distance(function (s, t) {
return distanceFunction(s, t, props, norm);
})
.stableVelocity(0.000001)
.stableVeloHandler(ended);
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName, d3.forceNeighbourSamplingDistance()
// Set the parameters for the algorithm (optional).
.neighbourSize(NEIGHBOUR_SIZE)
.sampleSize(SAMPLE_SIZE)
// .freeness(0.5)
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (s, t) {
return distanceFunction(s, t, props, norm);
})
.stableVelocity(0.004)
.stableVeloHandler( function(){simulation.stop(); ended();} )
);
.alphaDecay(0)
.alpha(1)
.force(forceName, force);
// Restart the simulation.
console.log(simulation.force(forceName).neighbourSize(), simulation.force(forceName).sampleSize());
simulation.alpha(1).restart();
simulation.restart();
}

View File

@@ -2,240 +2,171 @@ import constant from "./constant";
import jiggle from "./jiggle";
import {getStress} from "./stress";
/**
* Set the node id accessor to the specified i.
* @param {node} d - node.
* @param {accessor} i - id accessor.
* @return {accessor} - node id accessor.
*/
function index(d, i) {
return i;
}
/**
* The implementation of Chalmers' 1996 Neighbour and Sampling algorithm.
* It uses random sampling to find the most suited neighbours from the
* data set.
* @return {force} calculated forces.
*/
function sortDistances(a, b) {
return b[1] - a[1];
}
export default function () {
var id = index,
neighbours = [],
samples = new Array(),
var neighbours = [],
distance = constant(300),
nodes,
neighbourSize = 6,
sampleSize = 3,
//freeness = 0.85,
//springForce = 0.7,
//dampingFactor = 0.3,
velocity,
stableVelocity = 0,
stableVeloHandler = null;
stableVeloHandler = null,
dataSizeFactor;
/**
* Calculates the forces at each iteration between the node and the
* objects in neighbour and sample sets.
* @param {number} alpha - controls the stopping of the
* particle simulations.
* Apply spring forces at each iteration.
* @param {number} alpha - multiplier for amount of force applied
*/
function force(alpha) {
let velocityDiff = 0;
for (let node of nodes) {
node.oldvx = node.vx;
node.oldvy = node.vy;
}
for (let i = 0, n = nodes.length; i < n; ++i) {
// Randomize the samples for every node.
samples[i] = randomizeSample(i);
// Calculate the forces between node and its neighbours.
for (let [keyN, valueN] of neighbours[i]) {
setVelocity(i, keyN, valueN, alpha);
let n = nodes.length;
// Cache old velocity for comparison later
if (stableVeloHandler!==null && stableVelocity!=0) {
for (let i = n-1, node; i>=0; i--) {
node = nodes[i];
node.oldvx = node.vx;
node.oldvy = node.vy;
}
// Calculate the forces between node and its sample set.
for (let [keyS, valueS] of samples[i]) {
setVelocity(i, keyS, valueS, alpha);
}
for (let i = n-1, node, samples; i>=0; i--) {
node = nodes[i];
samples = createRandomSamples(i);
for (let [neighbourID, highDDist] of neighbours[i]) {
setVelocity(node, nodes[neighbourID], highDDist, alpha);
}
// Check if there are a better neighbours in a sample array
// for each node.
findNewNeighbours(i);
for (let [sampleID, highDDist] of samples) {
setVelocity(node, nodes[sampleID], highDDist, alpha);
}
// Replace neighbours with better ones found in the samples
neighbours[i] = findNewNeighbours(neighbours[i], samples);
}
for (let node of nodes) {
velocityDiff += Math.abs(node.oldvx - node.vx) + Math.abs(node.oldvy - node.vy);
delete node.oldvx;
delete node.oldvy;
// Calculate velocity changes, aka force applied.
if (stableVeloHandler!==null && stableVelocity!=0) {
let velocityDiff = 0;
for (let i = n-1, node; i>=0; i--) {
node = nodes[i];
velocityDiff += Math.abs(Math.hypot(node.vx-node.oldvx, node.vy-node.oldvy));
}
velocityDiff /= n*(neighbourSize+sampleSize);
if(stableVeloHandler!==null && velocityDiff<stableVelocity){
console.log("Neighbour sampling is now", velocityDiff, ", calling stableVeloHandler().")
stableVeloHandler();
}
else console.log(velocityDiff);
}
velocityDiff /= nodes.length*(neighbourSize+sampleSize);
// Now total velocity changes per link, alpha considered
// TODO per property too
if(stableVeloHandler!==null && velocityDiff<stableVelocity){
console.log("Neighbour sampling is now", velocityDiff, ", calling stableVeloHandler().")
stableVeloHandler();
}
else console.log(velocityDiff);
}
/**
* Set the velocities of the source and target nodes.
* @param {number} sourceId - source node id.
* @param {number} targetId - target node id.
* @param {number} dist - high dimensional distance between
* the two nodes.
* @param {number} alpha - controls the speed of simulation.
* Apply force to both source and target nodes.
* @param {number} source - source node object
* @param {number} target - target node object
* @param {number} dist - high dimensional distance between the two nodes
* @param {number} alpha - multiplier for the amount of force applied
*/
function setVelocity(sourceId, targetId, dist, alpha) {
let source, target, x, y, l;
source = nodes[sourceId], target = nodes[targetId];
// If x or y coordinates not defined, add some randomness.
function setVelocity(source, target, dist, alpha) {
let x, y, l;
// jiggle so it wont divide / multiply by zero after this
x = target.x + target.vx - source.x - source.vx || jiggle();
y = target.y + target.vy - source.y - source.vy || jiggle();
l = Math.sqrt(x * x + y * y);
l = (l - dist) / l * alpha;
l = (l - dist) / l * dataSizeFactor * alpha;
x *= l, y *= l;
// Set the calculated velocites for both nodes.
target.vx -= x*0.5;
target.vy -= y*0.5;
source.vx += x*0.5;
source.vy += y*0.5;
target.vx -= x;
target.vy -= y;
source.vx += x;
source.vy += y;
}
/**
* Initialize the neighbour and sample set at the start.
*/
function initialize() {
if (!nodes) return;
// Initialize for each node a neighbour and sample arrays
// with random values.
for (let i = 0, n = nodes.length; i < n; ++i) {
let exclude = []; // Array that keeps the indices of nodes to ignore.
exclude.push(i);
let neighbs = createRandomNeighbours(i, exclude, n, neighbourSize);
// Initialize for each node some random neighbours.
for (let i = nodes.length-1; i>=0; i--) {
let neighbs = pickRandomNodesFor(i, [i], neighbourSize);
// Sort the neighbour set by the distances.
neighbs = new Map([...neighbs.entries()].sort(sortDistances));
neighbours[i] = neighbs;
//exclude.concat(neighbs);
//samples[i] = createRandomSample(i, exclude, n, sampleSize);
//Samples will be created at the start of each it anyway.
neighbours[i] = new Map(neighbs.sort(sortDistances));
}
initDataSizeFactor();
}
function initDataSizeFactor(){
dataSizeFactor = 0.5/(neighbourSize+sampleSize);
}
/**
* Function that compares to map elements by its values.
* @param {object} a
* @param {object} b
* @return {number} - 0, if values are equal, positive number if b > a,
* negative otherwise.
*/
function sortDistances(a, b) {
return b[1] - a[1];
}
/**
* Create an array of random integers, all different, with maximum
* value max and with size elements. No elements from exlucde should
* be included.
* @param {number} index - index of current node.
* Generates an array of array[index, high-d distance to the node of index]
* where all indices are different and the size is as specified unless
* impossible (may be due to too large size requested)
* @param {number} index - index of a node to calculate distance against
* @param {array} exclude - indices of the nodes to ignore.
* @param {number} max - maximum value.
* @param {number} size - the number of elements in map to return.
* @return {map} - a created map that contains random elements from
* data set.
* @param {number} size - max number of elements in the map to return.
* @return {array}
*/
function createRandomNeighbours(index, exclude, max, size) {
let randElements = new Map();
let triedElements = 0;
function pickRandomNodesFor(index, exclude, size) {
let randElements = [];
let max = nodes.length;
while ((randElements.size < size) && (randElements.size + exclude.length + triedElements < nodes.length)) {
let rand = Math.floor((Math.random() * max));
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.has(rand) || exclude.includes(rand)) {
rand = Math.floor((Math.random() * max));
}
let dist = +distance(nodes[index], nodes[rand]);
randElements.set(rand, dist);
}
return randElements;
}
function createRandomSample(index, exclude, max, size) {
let randElements = new Map();
for (let i = 0; i < size; ++i) {
for (let i = 0; i < size; i++) {
// Stop when no new elements can be found.
if (randElements.size + exclude.length >= nodes.length) {
if (randElements.length + exclude.length >= nodes.length) {
break;
}
let rand = Math.floor((Math.random() * max));
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.has(rand) || exclude.includes(rand)) {
// Re-random until suitable value is found.
while (randElements.includes(rand) || exclude.includes(rand)) {
rand = Math.floor((Math.random() * max));
}
randElements.set(rand, +distance(nodes[index], nodes[rand]));
randElements.push(rand);
}
for(let i=randElements.length-1, rand; i>=0; i--){
rand = randElements[i];
randElements[i] = [rand, distance(nodes[index], nodes[rand])];
}
return randElements;
}
/**
* Creates a new map of random numbers to be used by the samples list.
* @param {number} index - index of current node.
* @return {map} - map that contains random elements from data set.
* Generates a map {index: high-dimensional distance to the node of index}
* to be used as samples set for the node of the specified index.
* @param {number} index - index of the node to generate sample for
* @return {map}
*/
function randomizeSample(index) {
function createRandomSamples(index) {
// Ignore the current neighbours of the node and itself.
let exclude = [index];
exclude = exclude.concat(Array.from(neighbours[index].keys()));
return createRandomSample(index, exclude, nodes.length, sampleSize);
return new Map(pickRandomNodesFor(index, exclude, sampleSize));
}
/**
* Compares the elements from sample set to the neighbour set and
* replaces the elements from neighbour set if better neighbours are
* found in sample set.
* @param {number} index - index of current node.
* Compares the elements from sample set to the neighbour set and replaces the
* elements in the neighbour set if any better neighbours are found.
* @param {map} neighbours - map of neighbours
* @param {map} samples - map of samples
* @return {map} - new map of neighbours
*/
function findNewNeighbours(index) {
let sample = samples[index];
if (neighbours[index].size > 0) {
for (let [key, value] of sample) {
let neighbMax = neighbours[index].entries().next().value;
// Check if a value from sample could be a better neighbour
// if so, replace it.
if (value < neighbMax[1]) {
neighbours[index].delete(neighbMax[0]);
neighbours[index].set(key, value)
neighbours[index] = new Map([...neighbours[index].entries()].sort(sortDistances));
}
}
}
function findNewNeighbours(neighbours, samples) {
let combined = [...neighbours.entries()].concat([...samples.entries()]);
combined = combined.sort(sortDistances);
return new Map(combined.slice(0, neighbourSize));
}
/**
* Calculates the average velocity of the force calculation at the
* current iteration.
* @return {number} - average velocity.
*/
function getAvgVelocity() {
return velocity / ((neighbourSize + sampleSize) * nodes.length);
}
function getDistributionData() {
let d = [];
for (let i = 0; i < nodes.length; i++) {
d.push({ "index": i, "size": neighbours[i].size });
}
return { "maxSize": neighbourSize, "l": nodes.length, "distribution": d };
}
// API for initializing the algorithm, setting parameters and querying
// metrics.
@@ -244,42 +175,22 @@ export default function () {
initialize();
};
force.id = function (_) {
return arguments.length ? (id = _, force) : id;
};
force.neighbourSize = function (_) {
return arguments.length ? (neighbourSize = +_, force) : neighbourSize;
return arguments.length ? (neighbourSize = +_, initDataSizeFactor(), force) : neighbourSize;
};
force.sampleSize = function (_) {
return arguments.length ? (sampleSize = +_, force) : sampleSize;
return arguments.length ? (sampleSize = +_, initDataSizeFactor(), force) : sampleSize;
};
force.distance = function (_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
};
force.stress = function () {
return getStress(nodes, distance);
};
force.velocity = function () {
return getAvgVelocity();
};
/*force.freeness = function (_) {
return arguments.length ? (freeness = +_, force) : freeness;
};*/
force.nodeNeighbours = function (_) {
return arguments.length ? neighbours[+_] : neighbours;
};
force.distributionData = function () {
return getDistributionData();
};
force.stableVeloHandler = function (_) {
return arguments.length ? (stableVeloHandler = _, force) : stableVeloHandler;
};