@woosh/meep-engine 2.134.0 → 2.134.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/graph/coloring/colorizeGraphMCS.js +1 -1
- package/src/core/graph/convert_graph_to_dot_string.d.ts.map +1 -1
- package/src/core/graph/convert_graph_to_dot_string.js +2 -1
- package/src/core/graph/graph_compute_distance_matrix.d.ts +18 -1
- package/src/core/graph/graph_compute_distance_matrix.d.ts.map +1 -1
- package/src/core/graph/graph_compute_distance_matrix.js +21 -2
- package/src/core/graph/graph_compute_laplacian_matrix.d.ts.map +1 -1
- package/src/core/graph/graph_compute_laplacian_matrix.js +2 -1
- package/src/core/graph/graph_k_means_cluster.d.ts +10 -3
- package/src/core/graph/graph_k_means_cluster.d.ts.map +1 -1
- package/src/core/graph/graph_k_means_cluster.js +250 -123
- package/src/core/graph/layout/CircleLayout.js +6 -6
- package/src/core/graph/layout/Connection.js +1 -1
- package/src/core/graph/layout/box/aabb2_force_into_container.d.ts.map +1 -1
- package/src/core/graph/layout/box/aabb2_force_into_container.js +4 -2
- package/src/core/math/statistics/computeStatisticalMedian.js +1 -1
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"description": "Pure JavaScript game engine. Fully featured and production ready.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Alexander Goldring",
|
|
8
|
-
"version": "2.134.
|
|
8
|
+
"version": "2.134.1",
|
|
9
9
|
"main": "build/meep.module.js",
|
|
10
10
|
"module": "build/meep.module.js",
|
|
11
11
|
"exports": {
|
|
@@ -75,7 +75,7 @@ export function colorizeGraphMCS(graph) {
|
|
|
75
75
|
|
|
76
76
|
// Remove the maximum weight node from the graph so that it won't
|
|
77
77
|
// be accidentally added again
|
|
78
|
-
closed.set(
|
|
78
|
+
closed.set(max_vertex, true);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
return colorizeGraphGreedy(graph, ordering);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert_graph_to_dot_string.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/convert_graph_to_dot_string.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"convert_graph_to_dot_string.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/convert_graph_to_dot_string.js"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,0FAFa,MAAM,CAgElB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import LineBuilder from "../codegen/LineBuilder.js";
|
|
2
2
|
import { EdgeDirectionType } from "./Edge.js";
|
|
3
|
+
import { graphviz_escape_string } from "./format/graphviz/graphviz_escape_string.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @template T
|
|
@@ -7,7 +8,7 @@ import { EdgeDirectionType } from "./Edge.js";
|
|
|
7
8
|
* @return {string}
|
|
8
9
|
*/
|
|
9
10
|
function defaultNodeToDot(node) {
|
|
10
|
-
return `[label="${node.toString()}"]`;
|
|
11
|
+
return `[label="${graphviz_escape_string(node.toString())}"]`;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Produce a distance matrix from an input graph, tracing distances from each
|
|
2
|
+
* Produce a distance matrix from an input graph, tracing distances from each
|
|
3
|
+
* node to every node specified in the target vector.
|
|
4
|
+
*
|
|
5
|
+
* Traversal is BFS and each neighbour is only visited once. As a consequence
|
|
6
|
+
* this implementation gives correct shortest-path distances only when every
|
|
7
|
+
* edge has the same weight (typically `edge.weight === 1`). On graphs with
|
|
8
|
+
* heterogeneous weights the returned distances are the weight sum along the
|
|
9
|
+
* BFS tree, which is not the shortest-path distance in general — use
|
|
10
|
+
* Dijkstra or Floyd–Warshall for those cases instead.
|
|
11
|
+
*
|
|
12
|
+
* Edge costs are read from `edge.weight`, so uniform non-1 weights still
|
|
13
|
+
* produce distances scaled by the common weight, but this remains correct
|
|
14
|
+
* only because every path of the same hop count has the same total weight.
|
|
15
|
+
*
|
|
16
|
+
* Output layout: `m[row, col]` is the distance from the node at index `row`
|
|
17
|
+
* to the target node at index `col`. Columns whose index is not in `targets`
|
|
18
|
+
* are left at `POSITIVE_INFINITY` (except the diagonal, which is 0).
|
|
19
|
+
*
|
|
3
20
|
* @see "A Fast Algorithm to Find All-Pairs Shortest Paths in Complex Networks" by Wei Peng et Al. 2012
|
|
4
21
|
* @template T
|
|
5
22
|
* @param {Graph<T>} graph
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph_compute_distance_matrix.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/graph_compute_distance_matrix.js"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"graph_compute_distance_matrix.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/graph_compute_distance_matrix.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,4FAJW,MAAM,EAAE,mCAEN,YAAY,CAkExB;6BA7F4B,gCAAgC"}
|
|
@@ -3,7 +3,24 @@ import { Deque } from "../collection/queue/Deque.js";
|
|
|
3
3
|
import { SquareMatrix } from "../math/matrix/SquareMatrix.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Produce a distance matrix from an input graph, tracing distances from each
|
|
6
|
+
* Produce a distance matrix from an input graph, tracing distances from each
|
|
7
|
+
* node to every node specified in the target vector.
|
|
8
|
+
*
|
|
9
|
+
* Traversal is BFS and each neighbour is only visited once. As a consequence
|
|
10
|
+
* this implementation gives correct shortest-path distances only when every
|
|
11
|
+
* edge has the same weight (typically `edge.weight === 1`). On graphs with
|
|
12
|
+
* heterogeneous weights the returned distances are the weight sum along the
|
|
13
|
+
* BFS tree, which is not the shortest-path distance in general — use
|
|
14
|
+
* Dijkstra or Floyd–Warshall for those cases instead.
|
|
15
|
+
*
|
|
16
|
+
* Edge costs are read from `edge.weight`, so uniform non-1 weights still
|
|
17
|
+
* produce distances scaled by the common weight, but this remains correct
|
|
18
|
+
* only because every path of the same hop count has the same total weight.
|
|
19
|
+
*
|
|
20
|
+
* Output layout: `m[row, col]` is the distance from the node at index `row`
|
|
21
|
+
* to the target node at index `col`. Columns whose index is not in `targets`
|
|
22
|
+
* are left at `POSITIVE_INFINITY` (except the diagonal, which is 0).
|
|
23
|
+
*
|
|
7
24
|
* @see "A Fast Algorithm to Find All-Pairs Shortest Paths in Complex Networks" by Wei Peng et Al. 2012
|
|
8
25
|
* @template T
|
|
9
26
|
* @param {Graph<T>} graph
|
|
@@ -60,7 +77,9 @@ export function graph_compute_distance_matrix(graph, node_array, targets, node_i
|
|
|
60
77
|
// If we haven't visited this neighbor yet, its distance is the current distance + 1
|
|
61
78
|
if (m_distances.getCellValue(index_0, target_index) === Number.POSITIVE_INFINITY) {
|
|
62
79
|
|
|
63
|
-
|
|
80
|
+
// Fall back to unit weight when the edge has no explicit weight, so BFS
|
|
81
|
+
// produces meaningful hop-count distances on plain (non-weighted) edges.
|
|
82
|
+
const transition_cost = edge.weight !== undefined ? edge.weight : 1;
|
|
64
83
|
|
|
65
84
|
const new_distance = m_distances.getCellValue(index_1, target_index) + transition_cost;
|
|
66
85
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph_compute_laplacian_matrix.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/graph_compute_laplacian_matrix.js"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,2EAHW,YAAY,
|
|
1
|
+
{"version":3,"file":"graph_compute_laplacian_matrix.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/graph_compute_laplacian_matrix.js"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,2EAHW,YAAY,sCAYtB;6BArB4B,gCAAgC"}
|
|
@@ -12,7 +12,8 @@ import { graph_compute_degree_matrix } from "./graph_compute_degree_matrix.js";
|
|
|
12
12
|
* @param {Map<T,number>} node_indices Map from graph nodes to matrix indices.
|
|
13
13
|
*/
|
|
14
14
|
export function graph_compute_laplacian_matrix(graph, result, node_indices) {
|
|
15
|
-
|
|
15
|
+
// Uint32 prevents silent overflow for high-degree or multi-edge graphs
|
|
16
|
+
const degree = new SquareMatrix(result.size, BinaryDataType.Uint32);
|
|
16
17
|
const adjacency = new SquareMatrix(result.size, BinaryDataType.Uint8);
|
|
17
18
|
|
|
18
19
|
graph_compute_degree_matrix(graph, degree, node_indices);
|
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
* @param {MultiNode<T>[]} node_array
|
|
3
3
|
* @param {number} k
|
|
4
4
|
* @param {number} random_seed
|
|
5
|
-
* @param {number[]|
|
|
5
|
+
* @param {number[]|Uint32Array} node_cluster_assignments pre-filled with UNASSIGNED_CLUSTER
|
|
6
6
|
* @param {Map<T, number>} node_index_map
|
|
7
7
|
* @param {Graph<MultiNode<T>>} graph
|
|
8
8
|
* @returns {number[][]}
|
|
9
9
|
*/
|
|
10
|
-
export function graph_k_means_cluster_detailed(node_array: MultiNode<T>[], k: number, random_seed: number, node_cluster_assignments: number[] |
|
|
10
|
+
export function graph_k_means_cluster_detailed(node_array: MultiNode<T>[], k: number, random_seed: number, node_cluster_assignments: number[] | Uint32Array, node_index_map: Map<T, number>, graph: Graph<MultiNode<T>>): number[][];
|
|
11
11
|
/**
|
|
12
|
-
* Partition graph into K parts using K-means
|
|
12
|
+
* Partition graph into K parts using K-medoids (graph K-means).
|
|
13
|
+
* Iteratively reassigns nodes to the nearest medoid, then recomputes medoids
|
|
14
|
+
* as the within-cluster node minimising the sum of distances to other members.
|
|
15
|
+
* Stops when medoids stabilise or MAX_ITERATIONS is reached.
|
|
16
|
+
*
|
|
17
|
+
* Distances are BFS-based and so treat all edges as unit-weight; see
|
|
18
|
+
* {@link graph_compute_distance_matrix}.
|
|
19
|
+
*
|
|
13
20
|
* @template T
|
|
14
21
|
* @param {Graph<T>} graph
|
|
15
22
|
* @param {number} k number of desired parts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph_k_means_cluster.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/graph_k_means_cluster.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"graph_k_means_cluster.d.ts","sourceRoot":"","sources":["../../../../src/core/graph/graph_k_means_cluster.js"],"names":[],"mappings":"AA0DA;;;;;;;;GAQG;AACH,2DARW,cAAc,KACd,MAAM,eACN,MAAM,4BACN,MAAM,EAAE,GAAC,WAAW,kBACpB,OAAO,MAAM,CAAC,+BAEZ,MAAM,EAAE,EAAE,CA+ItB;AAED;;;;;;;;;;;;;;GAcG;AACH,6DAJW,MAAM,eACN,MAAM,GACJ,MAAM,EAAE,EAAE,CA0BtB"}
|
|
@@ -1,123 +1,250 @@
|
|
|
1
|
-
|
|
2
|
-
import { randomIntegerBetween } from "../math/random/randomIntegerBetween.js";
|
|
3
|
-
import { seededRandom } from "../math/random/seededRandom.js";
|
|
4
|
-
import { graph_compute_distance_matrix } from "./graph_compute_distance_matrix.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
1
|
+
import { assert } from "../assert.js";
|
|
2
|
+
import { randomIntegerBetween } from "../math/random/randomIntegerBetween.js";
|
|
3
|
+
import { seededRandom } from "../math/random/seededRandom.js";
|
|
4
|
+
import { graph_compute_distance_matrix } from "./graph_compute_distance_matrix.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sentinel value marking a node as not yet assigned to any cluster.
|
|
9
|
+
* Must be distinguishable from any valid cluster index 0..k-1, so k must
|
|
10
|
+
* stay strictly below this.
|
|
11
|
+
* @type {number}
|
|
12
|
+
*/
|
|
13
|
+
const UNASSIGNED_CLUSTER = 0xFFFFFFFF;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of refinement iterations before the algorithm stops even
|
|
17
|
+
* if medoids haven't stabilised. In practice convergence is reached within
|
|
18
|
+
* a handful of iterations on well-connected graphs.
|
|
19
|
+
* @type {number}
|
|
20
|
+
*/
|
|
21
|
+
const MAX_ITERATIONS = 32;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find the medoid of a set of nodes: the member with minimum sum of
|
|
26
|
+
* distances to every other member.
|
|
27
|
+
*
|
|
28
|
+
* @param {number[]} members cluster member node indices
|
|
29
|
+
* @param {SquareMatrix} m_distances matrix whose columns for each member index are filled
|
|
30
|
+
* @returns {number} node index of the medoid
|
|
31
|
+
*/
|
|
32
|
+
function find_cluster_medoid(members, m_distances) {
|
|
33
|
+
const n = members.length;
|
|
34
|
+
|
|
35
|
+
let best_member = members[0];
|
|
36
|
+
let best_sum = Number.POSITIVE_INFINITY;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const candidate = members[i];
|
|
40
|
+
|
|
41
|
+
let sum = 0;
|
|
42
|
+
for (let j = 0; j < n; j++) {
|
|
43
|
+
if (i === j) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
sum += m_distances.getCellValue(candidate, members[j]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (sum < best_sum) {
|
|
50
|
+
best_sum = sum;
|
|
51
|
+
best_member = candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return best_member;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {MultiNode<T>[]} node_array
|
|
61
|
+
* @param {number} k
|
|
62
|
+
* @param {number} random_seed
|
|
63
|
+
* @param {number[]|Uint32Array} node_cluster_assignments pre-filled with UNASSIGNED_CLUSTER
|
|
64
|
+
* @param {Map<T, number>} node_index_map
|
|
65
|
+
* @param {Graph<MultiNode<T>>} graph
|
|
66
|
+
* @returns {number[][]}
|
|
67
|
+
*/
|
|
68
|
+
export function graph_k_means_cluster_detailed(
|
|
69
|
+
node_array,
|
|
70
|
+
k,
|
|
71
|
+
random_seed,
|
|
72
|
+
node_cluster_assignments,
|
|
73
|
+
node_index_map,
|
|
74
|
+
graph
|
|
75
|
+
) {
|
|
76
|
+
const total_node_count = node_array.length;
|
|
77
|
+
|
|
78
|
+
if (k > total_node_count) {
|
|
79
|
+
throw new Error(`Not enough nodes in the graph, K(=${k}) > |V| (=${total_node_count})`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Cluster indices are stored alongside UNASSIGNED_CLUSTER in the same array, so
|
|
83
|
+
// k must fit strictly below the sentinel to avoid collisions.
|
|
84
|
+
assert.lessThan(k, UNASSIGNED_CLUSTER, `k must be less than UNASSIGNED_CLUSTER(=${UNASSIGNED_CLUSTER})`);
|
|
85
|
+
|
|
86
|
+
if (k === 0) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const random = seededRandom(random_seed);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Current medoid (centroid) node index for each cluster
|
|
94
|
+
* @type {number[]}
|
|
95
|
+
*/
|
|
96
|
+
const seeds = [];
|
|
97
|
+
|
|
98
|
+
// Farthest-first seeding: pick the first seed uniformly at random, then each
|
|
99
|
+
// subsequent seed is the node whose minimum distance to any existing seed is
|
|
100
|
+
// largest. This reliably spreads seeds across disconnected components and
|
|
101
|
+
// avoids the common K-means failure mode where random initialisation puts
|
|
102
|
+
// every seed into the same component.
|
|
103
|
+
{
|
|
104
|
+
// randomIntegerBetween is inclusive on both ends, so the max index is count-1
|
|
105
|
+
const first_seed = randomIntegerBetween(random, 0, total_node_count - 1);
|
|
106
|
+
seeds[0] = first_seed;
|
|
107
|
+
node_cluster_assignments[first_seed] = 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (let i = 1; i < k; i++) {
|
|
111
|
+
const m_partial = graph_compute_distance_matrix(graph, node_array, seeds, node_index_map);
|
|
112
|
+
|
|
113
|
+
let best_node = -1;
|
|
114
|
+
let best_min_dist = -1;
|
|
115
|
+
|
|
116
|
+
for (let node_index = 0; node_index < total_node_count; node_index++) {
|
|
117
|
+
if (node_cluster_assignments[node_index] !== UNASSIGNED_CLUSTER) {
|
|
118
|
+
// already a seed
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let min_dist = Number.POSITIVE_INFINITY;
|
|
123
|
+
for (let j = 0; j < i; j++) {
|
|
124
|
+
const d = m_partial.getCellValue(node_index, seeds[j]);
|
|
125
|
+
if (d < min_dist) {
|
|
126
|
+
min_dist = d;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (min_dist > best_min_dist) {
|
|
131
|
+
best_min_dist = min_dist;
|
|
132
|
+
best_node = node_index;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
seeds[i] = best_node;
|
|
137
|
+
node_cluster_assignments[best_node] = i;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Node indices belonging to each cluster. Rebuilt every iteration.
|
|
142
|
+
* @type {number[][]}
|
|
143
|
+
*/
|
|
144
|
+
let cluster_members = [];
|
|
145
|
+
|
|
146
|
+
for (let iter = 0; iter < MAX_ITERATIONS; iter++) {
|
|
147
|
+
// 1. Compute distances from every current seed to every node (BFS from each seed)
|
|
148
|
+
const m_distances_from_seeds = graph_compute_distance_matrix(graph, node_array, seeds, node_index_map);
|
|
149
|
+
|
|
150
|
+
// 2. Assign every node to its nearest seed
|
|
151
|
+
for (let node_index = 0; node_index < total_node_count; node_index++) {
|
|
152
|
+
|
|
153
|
+
let closest_cluster = 0;
|
|
154
|
+
let closest_distance = Number.POSITIVE_INFINITY;
|
|
155
|
+
|
|
156
|
+
for (let cluster_index = 0; cluster_index < k; cluster_index++) {
|
|
157
|
+
const cluster_seed = seeds[cluster_index];
|
|
158
|
+
|
|
159
|
+
// Matrix columns are the seed (target) indices, rows are source nodes.
|
|
160
|
+
// We need the distance from node_index to the seed, so read m[node_index, seed].
|
|
161
|
+
const distance = m_distances_from_seeds.getCellValue(node_index, cluster_seed);
|
|
162
|
+
|
|
163
|
+
if (distance < closest_distance) {
|
|
164
|
+
closest_distance = distance;
|
|
165
|
+
closest_cluster = cluster_index;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
node_cluster_assignments[node_index] = closest_cluster;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 3. Rebuild cluster member lists
|
|
173
|
+
cluster_members = [];
|
|
174
|
+
for (let i = 0; i < k; i++) {
|
|
175
|
+
cluster_members[i] = [];
|
|
176
|
+
}
|
|
177
|
+
for (let node_index = 0; node_index < total_node_count; node_index++) {
|
|
178
|
+
cluster_members[node_cluster_assignments[node_index]].push(node_index);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4. Recompute each seed as the medoid of its cluster
|
|
182
|
+
let seeds_changed = false;
|
|
183
|
+
|
|
184
|
+
for (let cluster_index = 0; cluster_index < k; cluster_index++) {
|
|
185
|
+
const members = cluster_members[cluster_index];
|
|
186
|
+
|
|
187
|
+
if (members.length <= 1) {
|
|
188
|
+
// 0 or 1 member: medoid is trivially the current seed
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// BFS from every member to compute within-cluster pairwise distances
|
|
193
|
+
const m_within = graph_compute_distance_matrix(graph, node_array, members, node_index_map);
|
|
194
|
+
|
|
195
|
+
const new_seed = find_cluster_medoid(members, m_within);
|
|
196
|
+
|
|
197
|
+
if (new_seed !== seeds[cluster_index]) {
|
|
198
|
+
seeds[cluster_index] = new_seed;
|
|
199
|
+
seeds_changed = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!seeds_changed) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return cluster_members;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Partition graph into K parts using K-medoids (graph K-means).
|
|
213
|
+
* Iteratively reassigns nodes to the nearest medoid, then recomputes medoids
|
|
214
|
+
* as the within-cluster node minimising the sum of distances to other members.
|
|
215
|
+
* Stops when medoids stabilise or MAX_ITERATIONS is reached.
|
|
216
|
+
*
|
|
217
|
+
* Distances are BFS-based and so treat all edges as unit-weight; see
|
|
218
|
+
* {@link graph_compute_distance_matrix}.
|
|
219
|
+
*
|
|
220
|
+
* @template T
|
|
221
|
+
* @param {Graph<T>} graph
|
|
222
|
+
* @param {number} k number of desired parts
|
|
223
|
+
* @param {number} random_seed seed for random number generator, useful for restarting partitioning
|
|
224
|
+
* @returns {number[][]}
|
|
225
|
+
*/
|
|
226
|
+
export function graph_k_means_cluster(graph, k, random_seed) {
|
|
227
|
+
|
|
228
|
+
const node_array = Array.from(graph.getNodes());
|
|
229
|
+
const node_count = node_array.length;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Uint32 so cluster indices and the UNASSIGNED_CLUSTER sentinel (0xFFFFFFFF) don't collide
|
|
233
|
+
* for any practical k
|
|
234
|
+
* @type {Uint32Array}
|
|
235
|
+
*/
|
|
236
|
+
const node_cluster_assignments = new Uint32Array(node_count);
|
|
237
|
+
node_cluster_assignments.fill(UNASSIGNED_CLUSTER);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* build node index
|
|
241
|
+
* @type {Map<T, number>}
|
|
242
|
+
*/
|
|
243
|
+
const node_index_map = new Map();
|
|
244
|
+
|
|
245
|
+
for (let i = 0; i < node_count; i++) {
|
|
246
|
+
node_index_map.set(node_array[i], i);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return graph_k_means_cluster_detailed(node_array, k, random_seed, node_cluster_assignments, node_index_map, graph);
|
|
250
|
+
}
|
|
@@ -626,9 +626,9 @@ function circleOverlapsConnection(circle, edge) {
|
|
|
626
626
|
const dX = x1 - x0;
|
|
627
627
|
const dY = y1 - y0;
|
|
628
628
|
|
|
629
|
-
//Vector from
|
|
630
|
-
const fX =
|
|
631
|
-
const fY =
|
|
629
|
+
//Vector from ray start to circle center
|
|
630
|
+
const fX = x0 - cX;
|
|
631
|
+
const fY = y0 - cY;
|
|
632
632
|
|
|
633
633
|
const a = dX * dX + dY * dY;
|
|
634
634
|
const b = 2 * (fX * dX + fY * dY);
|
|
@@ -695,9 +695,9 @@ function resolveCircleLineOverlap(circle, c0, c1) {
|
|
|
695
695
|
const dX = x1 - x0;
|
|
696
696
|
const dY = y1 - y0;
|
|
697
697
|
|
|
698
|
-
//Vector from
|
|
699
|
-
const fX =
|
|
700
|
-
const fY =
|
|
698
|
+
//Vector from ray start to circle center
|
|
699
|
+
const fX = x0 - cX;
|
|
700
|
+
const fY = y0 - cY;
|
|
701
701
|
|
|
702
702
|
const a = dX * dX + dY * dY;
|
|
703
703
|
const b = 2 * (fX * dX + fY * dY);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aabb2_force_into_container.d.ts","sourceRoot":"","sources":["../../../../../../src/core/graph/layout/box/aabb2_force_into_container.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aabb2_force_into_container.d.ts","sourceRoot":"","sources":["../../../../../../src/core/graph/layout/box/aabb2_force_into_container.js"],"names":[],"mappings":"AA8CA;;;;GAIG;AACH,+EAMC"}
|
|
@@ -27,14 +27,16 @@ function interval_force_into_bounds(
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
}else{
|
|
30
|
-
// too big to fit,
|
|
30
|
+
// too big to fit, shift so subject covers the entire bounds
|
|
31
31
|
|
|
32
32
|
const a = subject_v0 - bounds_v0;
|
|
33
33
|
const b = bounds_v1 - subject_v1;
|
|
34
34
|
|
|
35
|
-
if(a > 0
|
|
35
|
+
if(a > 0){
|
|
36
|
+
// subject left edge is inside bounds, shift left so it covers
|
|
36
37
|
displacement = -a;
|
|
37
38
|
}else if(b > 0){
|
|
39
|
+
// subject right edge is inside bounds, shift right so it covers
|
|
38
40
|
displacement = b;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
@@ -6,5 +6,5 @@ import { computeStatisticalPartialMedian } from "./computeStatisticalPartialMedi
|
|
|
6
6
|
* @returns {number}
|
|
7
7
|
*/
|
|
8
8
|
export function computeStatisticalMedian(values) {
|
|
9
|
-
return computeStatisticalPartialMedian(values, 0, values.length);
|
|
9
|
+
return computeStatisticalPartialMedian(values, 0, values.length - 1);
|
|
10
10
|
}
|