@graphty/layout 1.2.0 → 1.2.2

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.
Files changed (92) hide show
  1. package/.github/workflows/ci.yml +3 -7
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +1 -1
  4. package/dist/vitest.config.js +2 -2
  5. package/dist/vitest.config.js.map +1 -1
  6. package/examples/3d-kamada-kawai.html +4 -19
  7. package/examples/bfs-layout.html +26 -32
  8. package/examples/bipartite-layout.html +3 -3
  9. package/examples/forceatlas2-layout.html +106 -151
  10. package/{dist → examples}/layout-helpers.js +58 -30
  11. package/examples/multipartite-layout.html +32 -14
  12. package/examples/shell-layout.html +4 -2
  13. package/examples/spring-layout.html +1 -11
  14. package/package.json +3 -3
  15. package/src/algorithms/index.ts +6 -0
  16. package/src/algorithms/optimization/index.ts +12 -0
  17. package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
  18. package/src/algorithms/optimization/lbfgs.ts +68 -0
  19. package/src/algorithms/optimization/line-search.ts +50 -0
  20. package/src/algorithms/optimization/types.ts +8 -0
  21. package/src/algorithms/planarity/check.ts +38 -0
  22. package/src/algorithms/planarity/embedding.ts +216 -0
  23. package/src/algorithms/planarity/index.ts +12 -0
  24. package/src/algorithms/planarity/lr-test.ts +70 -0
  25. package/src/algorithms/planarity/special-graphs.ts +126 -0
  26. package/src/generators/basic.ts +93 -0
  27. package/src/generators/bipartite.ts +45 -0
  28. package/src/generators/grid.ts +42 -0
  29. package/src/generators/index.ts +15 -0
  30. package/src/generators/random.ts +39 -0
  31. package/src/generators/scale-free.ts +72 -0
  32. package/src/index.ts +18 -0
  33. package/src/layouts/basic/index.ts +5 -0
  34. package/src/layouts/basic/random.ts +32 -0
  35. package/src/layouts/force-directed/arf.ts +130 -0
  36. package/src/layouts/force-directed/forceatlas2.ts +407 -0
  37. package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
  38. package/src/layouts/force-directed/index.ts +9 -0
  39. package/src/layouts/force-directed/kamada-kawai.ts +112 -0
  40. package/src/layouts/force-directed/spring.ts +35 -0
  41. package/src/layouts/geometric/circular.ts +77 -0
  42. package/src/layouts/geometric/index.ts +7 -0
  43. package/src/layouts/geometric/shell.ts +80 -0
  44. package/src/layouts/geometric/spiral.ts +93 -0
  45. package/src/layouts/hierarchical/bfs.ts +81 -0
  46. package/src/layouts/hierarchical/bipartite.ts +94 -0
  47. package/src/layouts/hierarchical/index.ts +7 -0
  48. package/src/layouts/hierarchical/multipartite.ts +88 -0
  49. package/src/layouts/index.ts +9 -0
  50. package/src/layouts/specialized/index.ts +6 -0
  51. package/src/layouts/specialized/planar.ts +65 -0
  52. package/src/layouts/specialized/spectral.ts +128 -0
  53. package/src/types/embedding.ts +11 -0
  54. package/src/types/graph.ts +13 -0
  55. package/src/types/index.ts +7 -0
  56. package/src/types/layout.ts +9 -0
  57. package/src/utils/graph.ts +77 -0
  58. package/src/utils/index.ts +9 -0
  59. package/src/utils/numpy.ts +111 -0
  60. package/src/utils/params.ts +26 -0
  61. package/src/utils/random.ts +53 -0
  62. package/src/utils/rescale.ts +137 -0
  63. package/test/arf-layout.test.ts +1 -1
  64. package/test/bfs-layout.test.ts +1 -1
  65. package/test/bipartite-layout.test.ts +1 -1
  66. package/test/circular-layout.test.ts +1 -1
  67. package/test/forceatlas2-layout.test.ts +1 -1
  68. package/test/fruchterman-reingold-layout.test.ts +1 -1
  69. package/test/graph-generators.test.ts +1 -1
  70. package/test/kamada-kawai-layout.test.ts +1 -1
  71. package/test/multipartite-layout.test.ts +1 -1
  72. package/test/planar-layout.test.ts +1 -1
  73. package/test/random-layout.test.ts +1 -1
  74. package/test/rescale-layout.test.ts +1 -1
  75. package/test/shell-layout.test.ts +1 -1
  76. package/test/spectral-layout.test.ts +1 -1
  77. package/test/spiral-layout.test.ts +1 -1
  78. package/test/spring-layout.test.ts +1 -1
  79. package/test/test-utils.ts +34 -0
  80. package/test/utils-graph.test.ts +155 -0
  81. package/test/utils-index.test.ts +77 -0
  82. package/test/utils-numpy.test.ts +272 -0
  83. package/test/utils-params.test.ts +99 -0
  84. package/test/utils-random.test.ts +229 -0
  85. package/vitest.config.ts +2 -2
  86. package/dist/layout-helpers.d.ts +0 -123
  87. package/dist/layout-helpers.js.map +0 -1
  88. package/dist/layout.d.ts +0 -275
  89. package/dist/layout.js +0 -2304
  90. package/dist/layout.js.map +0 -1
  91. package/layout-helpers.ts +0 -560
  92. package/layout.ts +0 -2893
@@ -171,10 +171,13 @@
171
171
  multipartiteLayout,
172
172
  scaleFreeGraph,
173
173
  starGraph,
174
- randomGraph,
174
+ randomGraph
175
+ } from "./layout.js";
176
+
177
+ import {
175
178
  groupNodes,
176
179
  findBestRoot
177
- } from "./layout.js";
180
+ } from "./layout-helpers.js";
178
181
 
179
182
  let currentGraph = null;
180
183
  let currentLayers = null;
@@ -307,11 +310,13 @@
307
310
 
308
311
  // Determine color based on layer
309
312
  let nodeColor = '#666';
310
- Object.entries(graph.layers).forEach(([layerIdx, layerNodes]) => {
311
- if (layerNodes.includes(node)) {
312
- nodeColor = layerColors[layerIdx % layerColors.length];
313
- }
314
- });
313
+ if (currentLayers) {
314
+ currentLayers.forEach((layerNodes, layerIdx) => {
315
+ if (layerNodes.includes(node)) {
316
+ nodeColor = layerColors[layerIdx % layerColors.length];
317
+ }
318
+ });
319
+ }
315
320
 
316
321
  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
317
322
  circle.setAttribute('cx', x);
@@ -328,7 +333,7 @@
328
333
  text.setAttribute('text-anchor', 'middle');
329
334
  text.setAttribute('font-size', '9');
330
335
  text.setAttribute('fill', 'white');
331
- text.textContent = node.replace(/L\d+N/, '');
336
+ text.textContent = String(node).replace(/L\d+N/, '') || String(node);
332
337
  svg.appendChild(text);
333
338
  });
334
339
  }
@@ -336,9 +341,9 @@
336
341
  window.newGraph = function() {
337
342
  const numLayers = parseInt(document.getElementById('num-layers').value);
338
343
  const nodesPerLayer = parseInt(document.getElementById('nodes-per-layer').value);
339
- const edgeType = document.getElementById('edge-type').value;
344
+ const graphType = document.getElementById('graph-type').value;
340
345
 
341
- currentGraph = generateMultipartiteGraph(numLayers, nodesPerLayer, edgeType);
346
+ currentGraph = generateGraph(graphType, numLayers, nodesPerLayer);
342
347
 
343
348
  // Just visualize with random positions initially
344
349
  const positions = {};
@@ -349,13 +354,26 @@
349
354
  ];
350
355
  });
351
356
 
357
+ // Generate layers for multipartite layout
358
+ const layerMethod = document.getElementById('layer-method').value;
359
+
360
+ if (layerMethod === 'bfs') {
361
+ const root = findBestRoot(currentGraph);
362
+ currentLayers = groupNodes(currentGraph, 'bfs', 0, { root });
363
+ } else if (layerMethod === 'degree') {
364
+ currentLayers = groupNodes(currentGraph, 'degree', numLayers);
365
+ } else {
366
+ // community-based
367
+ currentLayers = groupNodes(currentGraph, 'community', numLayers);
368
+ }
369
+
352
370
  visualizeGraph(currentGraph, positions);
353
371
 
354
372
  // Update layer info
355
373
  const infoDiv = document.getElementById('layers-info');
356
- const layerInfo = Object.entries(currentGraph.layers).map(([idx, nodes]) =>
374
+ const layerInfo = currentLayers ? currentLayers.map((nodes, idx) =>
357
375
  `Layer ${idx}: ${nodes.length} nodes`
358
- ).join('<br>');
376
+ ).join('<br>') : 'No layers generated';
359
377
 
360
378
  infoDiv.innerHTML = `
361
379
  <strong>Structure:</strong><br>
@@ -365,11 +383,11 @@
365
383
  };
366
384
 
367
385
  window.applyLayout = function() {
368
- if(!currentGraph) return;
386
+ if(!currentGraph || !currentLayers) return;
369
387
 
370
388
  const alignment = document.getElementById('alignment').value;
371
389
 
372
- const positions = multipartiteLayout(currentGraph, currentGraph.layers, alignment, 1, [0, 0]);
390
+ const positions = multipartiteLayout(currentGraph, currentLayers, alignment, 1, [0, 0]);
373
391
 
374
392
  visualizeGraph(currentGraph, positions);
375
393
  };
@@ -146,10 +146,12 @@
146
146
  import {
147
147
  shellLayout,
148
148
  scaleFreeGraph,
149
- randomGraph,
149
+ randomGraph
150
+ } from "./layout.js";
151
+ import {
150
152
  groupNodes,
151
153
  findBestRoot
152
- } from "./layout.js";
154
+ } from "./layout-helpers.js";
153
155
 
154
156
  let currentGraph = null;
155
157
 
@@ -286,17 +286,7 @@
286
286
  if(!currentGraph) return;
287
287
 
288
288
  const iterations = parseInt(document.getElementById('iterations').value);
289
- const autoConfig = document.getElementById('auto-config').checked;
290
-
291
- let k;
292
- if (autoConfig && currentConfig) {
293
- k = currentConfig.k;
294
- // Update slider to show auto-configured value
295
- document.getElementById('k-value').value = k;
296
- document.getElementById('k-value-value').textContent = k.toFixed(2);
297
- } else {
298
- k = parseFloat(document.getElementById('k-value').value);
299
- }
289
+ const k = parseFloat(document.getElementById('k-value').value);
300
290
 
301
291
  const positions = springLayout(currentGraph, k, null, null, iterations, 1, [0, 0], 2);
302
292
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@graphty/layout",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "graph layout algorithms based on networkx",
5
- "main": "dist/layout.js",
5
+ "main": "dist/src/index.js",
6
6
  "type": "module",
7
7
  "directories": {
8
8
  "example": "examples"
@@ -14,7 +14,7 @@
14
14
  "test:coverage": "vitest run --coverage",
15
15
  "prepare": "husky",
16
16
  "build": "tsc",
17
- "build:examples": "npm run build && cp dist/layout.js examples/layout.js",
17
+ "build:examples": "npm run build && node build-examples-script.js",
18
18
  "watch": "tsc --watch",
19
19
  "dev": "tsc --watch",
20
20
  "commit": "cz",
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Re-export all algorithm utilities
3
+ */
4
+
5
+ export * from './planarity';
6
+ export * from './optimization';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Re-export all optimization algorithms
3
+ */
4
+
5
+ export type { DistanceMap } from './types';
6
+ export {
7
+ _computeShortestPathDistances,
8
+ _kamadaKawaiSolve,
9
+ _kamadaKawaiCostfn
10
+ } from './kamada-kawai-solver';
11
+ export { _lbfgsDirection } from './lbfgs';
12
+ export { _backtrackingLineSearch } from './line-search';
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Kamada-Kawai layout algorithm optimization functions
3
+ */
4
+
5
+ import { Graph, Node, Edge } from '../../types';
6
+ import { DistanceMap } from './types';
7
+ import { _lbfgsDirection } from './lbfgs';
8
+ import { _backtrackingLineSearch } from './line-search';
9
+ import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph';
10
+
11
+ /**
12
+ * Compute all-pairs shortest path distances for the graph
13
+ *
14
+ * @param G - NetworkX graph
15
+ * @param weight - Edge attribute for weight
16
+ * @returns Dictionary of dictionaries of shortest path distances
17
+ */
18
+ export function _computeShortestPathDistances(
19
+ G: Graph,
20
+ weight: string
21
+ ): DistanceMap {
22
+ const distances: DistanceMap = {};
23
+ const nodes = getNodesFromGraph(G);
24
+ const edges = getEdgesFromGraph(G);
25
+
26
+ // Initialize distances with direct edges
27
+ for (const node of nodes) {
28
+ distances[node] = {};
29
+ distances[node][node] = 0;
30
+
31
+ for (const other of nodes) {
32
+ if (node !== other) {
33
+ distances[node][other] = Infinity;
34
+ }
35
+ }
36
+ }
37
+
38
+ // Add direct edges
39
+ for (const [source, target] of edges) {
40
+ // In a real implementation, we would get the weight from the graph
41
+ // For now, assume weight = 1 or use weight attribute if available
42
+ let edgeWeight = 1;
43
+ if (G.getEdgeData) {
44
+ edgeWeight = G.getEdgeData(source, target, weight) || 1;
45
+ }
46
+
47
+ distances[source][target] = edgeWeight;
48
+ distances[target][source] = edgeWeight; // Assuming undirected graph
49
+ }
50
+
51
+ // Floyd-Warshall algorithm for all-pairs shortest paths
52
+ for (const k of nodes) {
53
+ for (const i of nodes) {
54
+ for (const j of nodes) {
55
+ if (distances[i][k] + distances[k][j] < distances[i][j]) {
56
+ distances[i][j] = distances[i][k] + distances[k][j];
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ return distances;
63
+ }
64
+
65
+ /**
66
+ * Solve the Kamada-Kawai layout optimization problem
67
+ *
68
+ * @param distMatrix - Matrix of desired distances between nodes
69
+ * @param positions - Initial node positions
70
+ * @param dim - Dimension of layout
71
+ * @returns Optimized node positions
72
+ */
73
+ export function _kamadaKawaiSolve(
74
+ distMatrix: number[][],
75
+ positions: number[][],
76
+ dim: number
77
+ ): number[][] {
78
+ // Implementation of L-BFGS optimization for Kamada-Kawai
79
+ const nNodes = positions.length;
80
+ const meanWeight = 1e-3;
81
+
82
+ // Convert distances to inverse distances (with protection against division by zero)
83
+ const invDistMatrix = distMatrix.map(row =>
84
+ row.map(d => d === 0 ? 0 : 1 / (d + 1e-3))
85
+ );
86
+
87
+ // Flatten positions for optimization
88
+ let posVec = positions.flat();
89
+
90
+ // Optimization parameters
91
+ const maxIter = 500;
92
+ const gtol = 1e-5;
93
+ const m = 10; // L-BFGS memory size
94
+
95
+ // Implement a simplified L-BFGS-B algorithm
96
+ let alpha = 1.0;
97
+ const oldValues: number[][] = [];
98
+ const oldGrads: number[][] = [];
99
+
100
+ for (let iter = 0; iter < maxIter; iter++) {
101
+ // Calculate cost and gradient
102
+ const [cost, grad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
103
+
104
+ // Compute search direction using L-BFGS approximation
105
+ const direction = _lbfgsDirection(grad, oldValues, oldGrads, m);
106
+
107
+ // Simple line search for step size
108
+ alpha = _backtrackingLineSearch(
109
+ posVec, direction, cost, grad,
110
+ (x: number[]) => _kamadaKawaiCostfn(x, invDistMatrix, meanWeight, dim)[0],
111
+ alpha
112
+ );
113
+
114
+ // Save current position and gradient for next iteration
115
+ const oldPos = [...posVec];
116
+
117
+ // Update position
118
+ for (let i = 0; i < posVec.length; i++) {
119
+ posVec[i] += alpha * direction[i];
120
+ }
121
+
122
+ // Calculate new gradient
123
+ const [, newGrad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
124
+
125
+ // Update L-BFGS memory
126
+ oldValues.push(posVec.map((val, i) => val - oldPos[i]));
127
+ oldGrads.push(newGrad.map((val, i) => val - grad[i]));
128
+
129
+ // Keep only m most recent updates
130
+ if (oldValues.length > m) {
131
+ oldValues.shift();
132
+ oldGrads.shift();
133
+ }
134
+
135
+ // Check convergence
136
+ const gradNorm = Math.sqrt(newGrad.reduce((sum, g) => sum + g * g, 0));
137
+ if (gradNorm < gtol) {
138
+ break;
139
+ }
140
+ }
141
+
142
+ // Reshape result back into positions array
143
+ const result: number[][] = [];
144
+ for (let i = 0; i < nNodes; i++) {
145
+ result.push(posVec.slice(i * dim, (i + 1) * dim));
146
+ }
147
+
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Cost function and gradient for Kamada-Kawai layout algorithm
153
+ *
154
+ * @param posVec - Flattened position array
155
+ * @param invDist - Inverse distance matrix
156
+ * @param meanWeight - Weight for centering positions
157
+ * @param dim - Dimension of layout
158
+ * @returns Array with [cost, gradient]
159
+ */
160
+ export function _kamadaKawaiCostfn(
161
+ posVec: number[],
162
+ invDist: number[][],
163
+ meanWeight: number,
164
+ dim: number
165
+ ): [number, number[]] {
166
+ const nNodes = invDist.length;
167
+ const positions: number[][] = [];
168
+
169
+ // Reshape flat vector into positions array
170
+ for (let i = 0; i < nNodes; i++) {
171
+ positions.push(posVec.slice(i * dim, (i + 1) * dim));
172
+ }
173
+
174
+ // Calculate cost
175
+ let cost = 0;
176
+
177
+ // Add mean position penalty term
178
+ const sumPos = Array(dim).fill(0);
179
+ for (let i = 0; i < nNodes; i++) {
180
+ for (let d = 0; d < dim; d++) {
181
+ sumPos[d] += positions[i][d];
182
+ }
183
+ }
184
+ cost += 0.5 * meanWeight * sumPos.reduce((sum, val) => sum + val * val, 0);
185
+
186
+ // Add distance penalty terms
187
+ for (let i = 0; i < nNodes; i++) {
188
+ for (let j = i + 1; j < nNodes; j++) {
189
+ // Calculate actual distance
190
+ const diff = positions[i].map((val, d) => val - positions[j][d]);
191
+ const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0));
192
+
193
+ // Add penalty for difference between actual and ideal distance
194
+ const idealInvDist = invDist[i][j];
195
+ const offset = distance * idealInvDist - 1.0;
196
+ cost += 0.5 * offset * offset;
197
+ }
198
+ }
199
+
200
+ // Calculate gradient
201
+ const grad = new Array(posVec.length).fill(0);
202
+
203
+ // Add gradient of mean position penalty
204
+ for (let i = 0; i < nNodes; i++) {
205
+ for (let d = 0; d < dim; d++) {
206
+ grad[i * dim + d] += meanWeight * sumPos[d];
207
+ }
208
+ }
209
+
210
+ // Add gradient of distance penalties
211
+ for (let i = 0; i < nNodes; i++) {
212
+ for (let j = i + 1; j < nNodes; j++) {
213
+ // Calculate actual distance and direction
214
+ const diff = positions[i].map((val, d) => val - positions[j][d]);
215
+ const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 1e-10;
216
+ const direction = diff.map(d => d / distance);
217
+
218
+ // Calculate contribution to gradient
219
+ const idealInvDist = invDist[i][j];
220
+ const offset = distance * idealInvDist - 1.0;
221
+
222
+ for (let d = 0; d < dim; d++) {
223
+ const force = idealInvDist * offset * direction[d];
224
+ grad[i * dim + d] += force;
225
+ grad[j * dim + d] -= force;
226
+ }
227
+ }
228
+ }
229
+
230
+ return [cost, grad];
231
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * L-BFGS optimization utilities
3
+ */
4
+
5
+ /**
6
+ * Compute the search direction using L-BFGS approximation
7
+ *
8
+ * @param grad - Current gradient
9
+ * @param sList - List of position differences (s_k)
10
+ * @param yList - List of gradient differences (y_k)
11
+ * @param m - Memory size
12
+ * @returns Direction vector
13
+ */
14
+ export function _lbfgsDirection(
15
+ grad: number[],
16
+ sList: number[][],
17
+ yList: number[][],
18
+ m: number
19
+ ): number[] {
20
+ if (sList.length === 0) {
21
+ // First iteration - use negative gradient
22
+ return grad.map(g => -g);
23
+ }
24
+
25
+ const q = grad.slice();
26
+ const alpha = Array(sList.length).fill(0);
27
+ const rho: number[] = [];
28
+
29
+ // Compute rho values
30
+ for (let i = 0; i < sList.length; i++) {
31
+ const s = sList[i];
32
+ const y = yList[i];
33
+ rho.push(1 / y.reduce((sum, val, j) => sum + val * s[j], 0));
34
+ }
35
+
36
+ // Forward pass
37
+ for (let i = sList.length - 1; i >= 0; i--) {
38
+ const s = sList[i];
39
+ alpha[i] = rho[i] * s.reduce((sum, val, j) => sum + val * q[j], 0);
40
+ for (let j = 0; j < q.length; j++) {
41
+ q[j] -= alpha[i] * yList[i][j];
42
+ }
43
+ }
44
+
45
+ // Scale initial Hessian approximation
46
+ let gamma = 1;
47
+ if (sList.length > 0 && yList.length > 0) {
48
+ const y = yList[yList.length - 1];
49
+ const s = sList[sList.length - 1];
50
+ gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) /
51
+ y.reduce((sum, val) => sum + val * val, 0);
52
+ }
53
+
54
+ // Initialize direction with scaled negative gradient
55
+ const direction = q.map(val => -gamma * val);
56
+
57
+ // Backward pass
58
+ for (let i = 0; i < sList.length; i++) {
59
+ const s = sList[i];
60
+ const y = yList[i];
61
+ const beta = rho[i] * y.reduce((sum, val, j) => sum + val * direction[j], 0);
62
+ for (let j = 0; j < direction.length; j++) {
63
+ direction[j] += s[j] * (alpha[i] - beta);
64
+ }
65
+ }
66
+
67
+ return direction;
68
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Line search optimization utilities
3
+ */
4
+
5
+ /**
6
+ * Backtracking line search to find step size
7
+ *
8
+ * @param x - Current position
9
+ * @param direction - Search direction
10
+ * @param f - Function value at current position
11
+ * @param grad - Gradient at current position
12
+ * @param func - Function to evaluate cost
13
+ * @param alpha0 - Initial step size
14
+ * @returns Optimal step size
15
+ */
16
+ export function _backtrackingLineSearch(
17
+ x: number[],
18
+ direction: number[],
19
+ f: number,
20
+ grad: number[],
21
+ func: (x: number[]) => number,
22
+ alpha0: number
23
+ ): number {
24
+ const c1 = 1e-4;
25
+ const c2 = 0.9;
26
+ const initialSlope = grad.reduce((sum, g, i) => sum + g * direction[i], 0);
27
+
28
+ if (initialSlope >= 0) {
29
+ return 1e-8; // Direction is not a descent direction
30
+ }
31
+
32
+ let alpha = alpha0;
33
+ const maxIter = 20;
34
+
35
+ for (let i = 0; i < maxIter; i++) {
36
+ // Try step
37
+ const newX = x.map((val, i) => val + alpha * direction[i]);
38
+ const newF = func(newX);
39
+
40
+ // Check sufficient decrease condition (Armijo condition)
41
+ if (newF <= f + c1 * alpha * initialSlope) {
42
+ return alpha;
43
+ }
44
+
45
+ // Reduce step size
46
+ alpha *= c2;
47
+ }
48
+
49
+ return alpha; // Return last alpha even if not optimal
50
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Type definitions for optimization algorithms
3
+ */
4
+
5
+ import { Node } from '../../types';
6
+
7
+ // Type definitions for distance structure used in Kamada-Kawai
8
+ export type DistanceMap = Record<Node, Record<Node, number>>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Main planarity checking function
3
+ */
4
+
5
+ import { Graph, Node, Edge, Embedding } from '../../types';
6
+ import { isK5, isK33 } from './special-graphs';
7
+ import { lrPlanarityTest } from './lr-test';
8
+ import { createTriangulationEmbedding } from './embedding';
9
+
10
+ /**
11
+ * Check if graph is planar using a simplified version of Boyer-Myrvold algorithm.
12
+ * Returns planarity and embedding information.
13
+ *
14
+ * @param G - Graph
15
+ * @param nodes - List of nodes
16
+ * @param edges - List of edges
17
+ * @returns Object containing isPlanar flag and embedding
18
+ */
19
+ export function checkPlanarity(
20
+ G: Graph,
21
+ nodes: Node[],
22
+ edges: Edge[]
23
+ ): { isPlanar: boolean; embedding: Embedding | null } {
24
+ // For small graphs (n <= 4), all are planar
25
+ if (nodes.length <= 4) {
26
+ return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
27
+ }
28
+
29
+ // For K5 (complete graph with 5 nodes) and K3,3 (complete bipartite with 3,3 nodes)
30
+ // these are not planar by Kuratowski's theorem
31
+ if (isK5(nodes, edges) || isK33(nodes, edges)) {
32
+ return { isPlanar: false, embedding: null };
33
+ }
34
+
35
+ // For other graphs, use LR algorithm (Left-Right Planarity Test)
36
+ const result = lrPlanarityTest(nodes, edges);
37
+ return result;
38
+ }