@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
124 lines (97 loc) • 3.56 kB
JavaScript
//
import { randomIntegerBetween } from "../math/random/randomIntegerBetween.js";
import { seededRandom } from "../math/random/seededRandom.js";
import { graph_compute_distance_matrix } from "./graph_compute_distance_matrix.js";
/**
* @param {MultiNode<T>[]} node_array
* @param {number} k
* @param {number} random_seed
* @param {number[]|Uint8Array|Uint16Array|Uint32Array} node_cluster_assignments
* @param {Map<T, number>} node_index_map
* @param {Graph<MultiNode<T>>} graph
* @returns {number[][]}
*/
export function graph_k_means_cluster_detailed(
node_array,
k,
random_seed,
node_cluster_assignments,
node_index_map,
graph
) {
const total_node_count = node_array.length;
if (k > total_node_count) {
throw new Error(`Not enough nodes in the graph, K(=${k}) > |V| (=${total_node_count})`);
}
const random = seededRandom(random_seed);
//seeds
const clusters_seeds = [];
/**
*
* @type {number[][]}
*/
const clusters = [];
const UNASSIGNED_CLUSTER = 255;
// seed clusters
for (let i = 0; i < k; i++) {
//
const node_index = randomIntegerBetween(random, 0, total_node_count);
if (node_cluster_assignments[node_index] !== UNASSIGNED_CLUSTER) {
// node taken, retry
i--;
continue;
}
node_cluster_assignments[node_index] = i;
clusters_seeds[i] = node_index;
clusters[i] = [node_index];
}
const m_distances = graph_compute_distance_matrix(graph, node_array, clusters_seeds, node_index_map);
for (let node_index = 0; node_index < total_node_count; node_index++) {
if (node_cluster_assignments[node_index] !== UNASSIGNED_CLUSTER) {
// already assigned
continue;
}
let closest_cluster = 0;
let closest_distance = Number.POSITIVE_INFINITY;
for (let cluster_index = 0; cluster_index < k; cluster_index++) {
const cluster_seed = clusters_seeds[cluster_index];
const distance = m_distances.getCellValue(cluster_seed, node_index);
if (distance < closest_distance) {
closest_distance = distance;
closest_cluster = cluster_index;
}
}
// assign node to closest cluster
node_cluster_assignments[node_index] = closest_cluster;
clusters[closest_cluster].push(node_index);
}
return clusters;
}
/**
* Partition graph into K parts using K-means algorithm
* @template T
* @param {Graph<T>} graph
* @param {number} k number of desired parts
* @param {number} random_seed seed for random number generator, useful for restarting partitioning
* @returns {number[][]}
*/
export function graph_k_means_cluster(graph, k, random_seed) {
const node_array = Array.from(graph.getNodes());
const node_count = node_array.length;
/**
*
* @type {Uint8Array}
*/
const node_cluster_assignments = new Uint8Array(node_count);
// fill with "UNASSIGNED" value
node_cluster_assignments.fill(255);
/**
* build node index
* @type {Map<T, number>}
*/
const node_index_map = new Map();
for (let i = 0; i < node_count; i++) {
node_index_map.set(node_array[i], i);
}
return graph_k_means_cluster_detailed(node_array, k, random_seed, node_cluster_assignments, node_index_map, graph);
}