@graphty/webgpu-graph-algorithms 0.4.1 → 0.5.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.
Files changed (93) hide show
  1. package/README.md +27 -21
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-CRbw2Wyo.js → context-BR7fx3vR.js} +47 -5
  4. package/dist/chunks/context-BR7fx3vR.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +15 -12
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +35 -12
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/constants.d.ts +47 -0
  11. package/dist/src/constants.d.ts.map +1 -1
  12. package/dist/src/constants.js +47 -0
  13. package/dist/src/constants.js.map +1 -1
  14. package/dist/src/index.d.ts +8 -5
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +9 -5
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/kernel/prelude.d.ts.map +1 -1
  19. package/dist/src/kernel/prelude.js +4 -1
  20. package/dist/src/kernel/prelude.js.map +1 -1
  21. package/dist/src/kernels.d.ts +3 -3
  22. package/dist/src/kernels.d.ts.map +1 -1
  23. package/dist/src/kernels.js +27 -11
  24. package/dist/src/kernels.js.map +1 -1
  25. package/dist/src/layouts/force-simulation.d.ts +3 -1
  26. package/dist/src/layouts/force-simulation.d.ts.map +1 -1
  27. package/dist/src/layouts/force-simulation.js +13 -3
  28. package/dist/src/layouts/force-simulation.js.map +1 -1
  29. package/dist/src/layouts/forceatlas2.d.ts +1 -3
  30. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  31. package/dist/src/layouts/forceatlas2.js +2 -191
  32. package/dist/src/layouts/forceatlas2.js.map +1 -1
  33. package/dist/src/layouts/fruchterman-reingold.d.ts +195 -0
  34. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
  35. package/dist/src/layouts/fruchterman-reingold.js +579 -0
  36. package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
  37. package/dist/src/layouts/model-common.d.ts +102 -0
  38. package/dist/src/layouts/model-common.d.ts.map +1 -0
  39. package/dist/src/layouts/model-common.js +198 -0
  40. package/dist/src/layouts/model-common.js.map +1 -0
  41. package/dist/src/layouts/spring-electrical.d.ts +179 -0
  42. package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
  43. package/dist/src/layouts/spring-electrical.js +508 -0
  44. package/dist/src/layouts/spring-electrical.js.map +1 -0
  45. package/dist/src/types/accelerator.d.ts +14 -157
  46. package/dist/src/types/accelerator.d.ts.map +1 -1
  47. package/dist/src/types/accelerator.js +4 -5
  48. package/dist/src/types/accelerator.js.map +1 -1
  49. package/dist/src/types/algorithms.d.ts +1 -1
  50. package/dist/src/types/algorithms.js +1 -1
  51. package/dist/src/types/layout.d.ts +32 -0
  52. package/dist/src/types/layout.d.ts.map +1 -1
  53. package/dist/src/types/options.d.ts +43 -1
  54. package/dist/src/types/options.d.ts.map +1 -1
  55. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +5 -2
  56. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
  57. package/dist/src/wgsl/fa2-attraction.wgsl.js +6 -1
  58. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
  59. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
  60. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
  61. package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
  62. package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
  63. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
  64. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
  65. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
  66. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
  67. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +8 -1
  68. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
  69. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +33 -0
  70. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
  71. package/dist/tsconfig.build.tsbuildinfo +1 -1
  72. package/dist/webgpu-graph-algorithms.js +1358 -284
  73. package/dist/webgpu-graph-algorithms.js.map +1 -1
  74. package/package.json +6 -3
  75. package/src/accelerator.ts +49 -14
  76. package/src/constants.ts +70 -0
  77. package/src/index.ts +19 -8
  78. package/src/kernel/prelude.ts +6 -0
  79. package/src/kernels.ts +27 -11
  80. package/src/layouts/force-simulation.ts +20 -3
  81. package/src/layouts/forceatlas2.ts +17 -220
  82. package/src/layouts/fruchterman-reingold.ts +737 -0
  83. package/src/layouts/model-common.ts +228 -0
  84. package/src/layouts/spring-electrical.ts +666 -0
  85. package/src/types/accelerator.ts +58 -174
  86. package/src/types/algorithms.ts +1 -1
  87. package/src/types/layout.ts +36 -0
  88. package/src/types/options.ts +45 -1
  89. package/src/wgsl/fa2-attraction.wgsl.ts +6 -1
  90. package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
  91. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
  92. package/src/wgsl/fa2-stats-finalize.wgsl.ts +33 -0
  93. package/dist/chunks/context-CRbw2Wyo.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphty/webgpu-graph-algorithms",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "WebGPU-accelerated graph algorithms and layouts over the @graphty/graph-format snapshot, for Node (Dawn) and browsers",
5
5
  "author": "Adam Powers <apowers@ato.ms>",
6
6
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  "homepage": "https://github.com/graphty-org/graphty-monorepo/tree/master/webgpu-graph-algorithms#readme",
61
61
  "dependencies": {
62
62
  "@webgpu/types": "^0.1.72",
63
- "@graphty/graph-format": "^1.0.1"
63
+ "@graphty/graph-format": "^1.0.2"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@graphty/algorithms": "^1.0.0",
@@ -84,13 +84,16 @@
84
84
  "@vitest/coverage-v8": "^3.2.4",
85
85
  "@vitest/ui": "^3.2.4",
86
86
  "fast-check": "^4.2.0",
87
+ "ngraph.forcelayout": "^3.3.1",
88
+ "ngraph.graph": "^20.0.1",
87
89
  "playwright": "^1.54.1",
88
90
  "tsx": "^4.20.3",
89
91
  "typescript": "^5.9.3",
90
92
  "vite": "^7.0.5",
91
93
  "vitest": "^3.2.4",
92
94
  "webgpu": "0.4.0",
93
- "@graphty/layout": "^1.7.0"
95
+ "@graphty/layout": "^1.8.0",
96
+ "@graphty/algorithms": "^1.8.1"
94
97
  },
95
98
  "scripts": {
96
99
  "build": "tsc -p tsconfig.build.json",
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * createAccelerator (spec 3.3, 9; contract 3.14): the injectable object that satisfies the CPU packages'
3
- * AlgorithmAccelerator and LayoutAccelerator interfaces (spec 9.2, 9.3; D27). Since W1b `LayoutAccelerator` is
4
- * the REAL `@graphty/layout` declaration, `import type`d by src/types/accelerator.ts; the AlgorithmAccelerator half
5
- * is still satisfied STRUCTURALLY against that file's mirror until M8a gives it something real. It carries P3's
6
- * `forceAtlas2`, `release` and `dispose` and P7's seven
7
- * algorithm members (spec 8.2, 8.3; M8b-T8, PD-14) and nothing else: the CPU-side dispatchers (`accelerated()`,
8
- * `createSimulation()`) test `acc.betweennessCentrality !== undefined` and route to the CPU when the member is
9
- * absent (spec 2.4 row "method missing"), so a method the GPU does not implement must not exist here -- never a
10
- * throwing stub. The remaining algorithm members arrive one per shipped algorithm from P8; `fruchtermanReingold` /
11
- * `springElectrical` with P5.
3
+ * AlgorithmAccelerator and LayoutAccelerator interfaces (spec 9.2, 9.3). Both are the REAL declarations,
4
+ * `LayoutAccelerator` from `@graphty/layout` (W1b, layout half) and `AlgorithmAccelerator` from
5
+ * `@graphty/algorithms` (W1b, algorithms half), `import type`d by src/types/accelerator.ts, so the object built
6
+ * here is checked against the CPU packages' own contracts. It carries P3's `forceAtlas2`, `release` and `dispose`,
7
+ * P5's `fruchtermanReingold` and `springElectrical` (the two other layout members of spec 9.3, landed together once
8
+ * both models were green, P5 PD-19) and P7's seven algorithm members (spec 8.2, 8.3; M8b-T8, PD-14) and nothing
9
+ * else: the CPU-side dispatchers (`accelerated()`, `createSimulation()`) test `acc.betweennessCentrality !== undefined`
10
+ * / `acc.fruchtermanReingold !== undefined` and route to the CPU when the member is absent (spec 2.4 row "method
11
+ * missing"), so a method the GPU does not implement must not exist here -- never a throwing stub. The remaining
12
+ * algorithm members arrive one per shipped algorithm from P8.
12
13
  */
13
14
 
14
15
  import { type F32, type F64, type GraphSnapshot } from "@graphty/graph-format";
@@ -18,6 +19,8 @@ import { pageRank, personalizedPageRank } from "./algorithms/pagerank.js";
18
19
  import { eigenvectorCentrality, hits, katzCentrality } from "./algorithms/spectral.js";
19
20
  import { type GpuContext } from "./context.js";
20
21
  import { createForceAtlas2 } from "./layouts/forceatlas2.js";
22
+ import { createFruchtermanReingold } from "./layouts/fruchterman-reingold.js";
23
+ import { createSpringElectrical } from "./layouts/spring-electrical.js";
21
24
  import { type AcceleratorOptions, type GpuAccelerator } from "./types/accelerator.js";
22
25
  import {
23
26
  type ComponentsOptions,
@@ -30,8 +33,18 @@ import {
30
33
  type KatzOptions,
31
34
  type PageRankOptions,
32
35
  } from "./types/algorithms.js";
33
- import { type ForceAtlas2Stats, type GpuLayoutSimulation, type GpuLayoutTuning } from "./types/layout.js";
34
- import { type ForceAtlas2Options } from "./types/options.js";
36
+ import {
37
+ type ForceAtlas2Stats,
38
+ type FruchtermanReingoldStats,
39
+ type GpuLayoutSimulation,
40
+ type GpuLayoutTuning,
41
+ type SpringElectricalStats,
42
+ } from "./types/layout.js";
43
+ import {
44
+ type ForceAtlas2Options,
45
+ type FruchtermanReingoldOptions,
46
+ type SpringElectricalOptions,
47
+ } from "./types/options.js";
35
48
 
36
49
  /** The `algorithms` record of AcceleratorOptions (spec 3.3), named for the copy helpers. */
37
50
  type AlgorithmDefaults = NonNullable<AcceleratorOptions["algorithms"]>;
@@ -90,7 +103,8 @@ function freezeOptions(options: AcceleratorOptions | undefined): Readonly<Accele
90
103
 
91
104
  /**
92
105
  * Spec 3.3 createAccelerator, verbatim: the object implementing AlgorithmAccelerator & LayoutAccelerator
93
- * structurally; P3's forceAtlas2, release and dispose plus P7's seven algorithm members, each a delegation to
106
+ * structurally; P3's forceAtlas2, release and dispose, P5's fruchtermanReingold and springElectrical (the same
107
+ * `{ ...o, ...options.layout }` shape as forceAtlas2) plus P7's seven algorithm members, each a delegation to
94
108
  * its algorithm with `ctx.assertReady()` first. The accelerator's algorithm defaults are not consulted by any of
95
109
  * them: only `betweenness` has any, and it belongs to P9. One per call (the app creates one and injects
96
110
  * it, spec 2.4); `kind` is "webgpu"; `options` is a frozen deep copy; `forceAtlas2(o)` is
@@ -100,8 +114,9 @@ function freezeOptions(options: AcceleratorOptions | undefined): Readonly<Accele
100
114
  * @param ctx - the context every simulation the accelerator creates runs on
101
115
  * @param options - GPU-only defaults inherited by every simulation (`layout`) and, from P9, the algorithm defaults
102
116
  * @returns the injectable accelerator
103
- * @throws E_DISPOSED / E_DEVICE_LOST from `ctx.assertReady()` (here and inside `forceAtlas2()`); `forceAtlas2()`
104
- * also throws what createForceAtlas2 throws (E_UNSUPPORTED for `nodeSize`, E_INVALID_ARGUMENT for a bad range)
117
+ * @throws E_DISPOSED / E_DEVICE_LOST from `ctx.assertReady()` (here and inside every creating member); `forceAtlas2()`
118
+ * also throws what createForceAtlas2 throws (E_UNSUPPORTED for `nodeSize`, E_INVALID_ARGUMENT for a bad range), and
119
+ * `fruchtermanReingold()` / `springElectrical()` what their factories throw (E_INVALID_ARGUMENT for a bad range)
105
120
  */
106
121
  export function createAccelerator(ctx: GpuContext, options?: AcceleratorOptions): GpuAccelerator {
107
122
  ctx.assertReady();
@@ -119,6 +134,26 @@ export function createAccelerator(ctx: GpuContext, options?: AcceleratorOptions)
119
134
  ctx.assertReady();
120
135
  return createForceAtlas2(ctx, { ...o, ...frozen.layout });
121
136
  },
137
+ /**
138
+ * The Fruchterman-Reingold simulation with this accelerator's layout tuning (spec 3.3, 7.20; P5).
139
+ * @param o - the CPU option type (spec 9.3 FruchtermanReingoldOptions); GPU tuning keys come from `options.layout`
140
+ * @returns a fresh simulation in state "created"
141
+ */
142
+ fruchtermanReingold(
143
+ o?: FruchtermanReingoldOptions,
144
+ ): GpuLayoutSimulation<FruchtermanReingoldOptions, FruchtermanReingoldStats> {
145
+ ctx.assertReady();
146
+ return createFruchtermanReingold(ctx, { ...o, ...frozen.layout });
147
+ },
148
+ /**
149
+ * The spring-electrical preset with this accelerator's layout tuning (spec 3.3, 7.20; P5).
150
+ * @param o - the CPU option type (spec 9.3 SpringElectricalOptions, ngraph's names)
151
+ * @returns a fresh simulation in state "created"
152
+ */
153
+ springElectrical(o?: SpringElectricalOptions): GpuLayoutSimulation<SpringElectricalOptions, SpringElectricalStats> {
154
+ ctx.assertReady();
155
+ return createSpringElectrical(ctx, { ...o, ...frozen.layout });
156
+ },
122
157
  /**
123
158
  * PageRank on the device (spec 8.2; contract 3.14).
124
159
  * @param gs - the snapshot
package/src/constants.ts CHANGED
@@ -114,3 +114,73 @@ export const FA2_DISTANCE_FLOOR_SQ = 0.0001;
114
114
  export const FA2_COINCIDENT_SQ = 1e-8;
115
115
  /** Bits of Fa2Params.flags (contract 4.4). */
116
116
  export const FA2_FLAG_FIRST = 1;
117
+ /** Fa2Params.flags bit: the Fruchterman-Reingold temperature is the adaptive one in the state block, not the uniform's (the `cooling: "adaptive"` option). */
118
+ export const FA2_FLAG_ADAPTIVE = 2;
119
+ /** The Fruchterman-Reingold loop's starting temperature (spec 7.20; `layout/src/simulation/fruchterman-reingold.ts:30`). */
120
+ export const FR_START_TEMPERATURE = 0.1;
121
+ /** The reheat point of spec 7.20: the temperature index a reheat restarts at, as a fraction of `iterations`. */
122
+ export const FR_REHEAT_FRACTION = 0.7;
123
+ /** Adaptive cooling (Yifan Hu 2005, section 3.2): the temperature is multiplied by this when the force energy rose, divided by it after FR_COOLING_PATIENCE consecutive falls. */
124
+ export const FR_COOLING_STEP = 0.9;
125
+ /** Adaptive cooling: consecutive iterations of falling energy before the temperature grows. */
126
+ export const FR_COOLING_PATIENCE = 5;
127
+ /** Adaptive cooling: the iteration budget when `iterations` is not given (the schedule no longer needs one; this is the cap on a run that never settles). */
128
+ export const FR_ADAPTIVE_MAX_ITERATIONS = 10_000;
129
+ /** Fruchterman-Reingold defaults (spec 7.20, 9.3; the CPU simulation's, `layout/src/simulation/fruchterman-reingold.ts:32`): `k` null = `1 / sqrt(n)`. */
130
+ export const FR_DEFAULTS: Readonly<{
131
+ k: null;
132
+ iterations: 50;
133
+ fixed: null;
134
+ dim: 2;
135
+ scale: 1;
136
+ settleThreshold: 0.001;
137
+ settleWindow: 10;
138
+ iterationsPerStep: 1;
139
+ maxInFlight: 2;
140
+ cooling: "linear";
141
+ }> = Object.freeze({
142
+ k: null,
143
+ iterations: 50,
144
+ fixed: null,
145
+ dim: 2,
146
+ scale: 1,
147
+ settleThreshold: 0.001,
148
+ settleWindow: 10,
149
+ iterationsPerStep: 1,
150
+ maxInFlight: 2,
151
+ cooling: "linear",
152
+ });
153
+ /**
154
+ * Spring-electrical size scaling: when `gravity` or `springCoefficient` is left to its default, ngraph's constant is
155
+ * multiplied by min(1, SE_SCALE_REFERENCE_NODES / n). ngraph's values were tuned for graphs of a few hundred nodes;
156
+ * on tens of thousands the per-node forces are so large that every node moves at the unit speed clamp and the layout
157
+ * never comes to rest (measured on the 58k-node Brightkite graph: 3,350 iterations to the settle rule with the
158
+ * kinetic energy still at the clamp; a uniform 1/1000 factor settled in 1,000 with the energy decayed 25x).
159
+ */
160
+ export const SE_SCALE_REFERENCE_NODES = 300;
161
+ /** Spring-electrical defaults: ngraph.forcelayout 3.3.1's (`lib/createPhysicsSimulator.js:29,34,40,54,59`; spec 7.20), plus the shared simulation defaults. `gravity` is ngraph's Coulomb constant: negative repels. */
162
+ export const SE_DEFAULTS: Readonly<{
163
+ springLength: 10;
164
+ springCoefficient: 0.8;
165
+ gravity: -12;
166
+ dragCoefficient: 0.9;
167
+ timeStep: 0.5;
168
+ dim: 2;
169
+ scale: 1;
170
+ settleThreshold: 0.001;
171
+ settleWindow: 10;
172
+ iterationsPerStep: 1;
173
+ maxInFlight: 2;
174
+ }> = Object.freeze({
175
+ springLength: 10,
176
+ springCoefficient: 0.8,
177
+ gravity: -12,
178
+ dragCoefficient: 0.9,
179
+ timeStep: 0.5,
180
+ dim: 2,
181
+ scale: 1,
182
+ settleThreshold: 0.001,
183
+ settleWindow: 10,
184
+ iterationsPerStep: 1,
185
+ maxInFlight: 2,
186
+ });
package/src/index.ts CHANGED
@@ -7,20 +7,24 @@
7
7
  * ForceSimulation, ForceAtlas2Model: tests import them from their files). P0: the error class, the constants and
8
8
  * isSoftwareAdapter. P1 adds GpuContext and degree (values) and the context / run / profiler types. P2 adds nothing
9
9
  * (Lease, CommandBatch, UniformRing are internal). P3 adds the layout factory, the accelerator, the two default
10
- * tables, the seeder and the layout / accelerator types. test/index.test.ts pins the value list and
11
- * test/types/public-api.test-d.ts the type list; P4+ extends both. This comment must never spell the internal
10
+ * tables, the seeder and the layout / accelerator types. P5 adds the two factories, the two default tables and the
11
+ * two stats records. test/index.test.ts pins the value list and test/types/public-api.test-d.ts the type list; P4+
12
+ * extends both. This comment must never spell the internal
12
13
  * JSDoc tag: it is the leading comment of the first export statement, and stripInternal would drop that statement
13
14
  * from the emitted declarations.
14
15
  */
15
16
 
16
- // ==================== constants and errors (P0; FA2_DEFAULTS / LAYOUT_TUNING_DEFAULTS public from P3)
17
+ // ==================== constants and errors (P0; FA2_DEFAULTS / LAYOUT_TUNING_DEFAULTS public from P3, FR_DEFAULTS /
18
+ // SE_DEFAULTS from P5)
17
19
  export {
18
20
  ARC_WINDOW_ALIGN,
19
21
  EXACT_MAX_NODES,
20
22
  FA2_DEFAULTS,
23
+ FR_DEFAULTS,
21
24
  LAYOUT_TUNING_DEFAULTS,
22
25
  MAX_1D_ITEMS,
23
26
  MAX_WORKGROUPS_PER_DIM,
27
+ SE_DEFAULTS,
24
28
  STORAGE_ALIGN,
25
29
  WORKGROUP_SIZE,
26
30
  } from "./constants.js";
@@ -45,24 +49,27 @@ export { connectedComponents } from "./algorithms/components.js";
45
49
  export { pageRank, personalizedPageRank } from "./algorithms/pagerank.js";
46
50
  export { eigenvectorCentrality, hits, katzCentrality } from "./algorithms/spectral.js";
47
51
 
48
- // ==================== layouts and the accelerator (P3)
52
+ // ==================== layouts and the accelerator (P3; the two P5 factories)
49
53
  export { createAccelerator } from "./accelerator.js";
50
54
  export { createForceAtlas2 } from "./layouts/forceatlas2.js";
55
+ export { createFruchtermanReingold } from "./layouts/fruchterman-reingold.js";
51
56
  export { seedPositions } from "./layouts/seed.js";
57
+ export { createSpringElectrical } from "./layouts/spring-electrical.js";
52
58
 
53
- // ==================== types: the accelerator surface, layout's re-exported declarations (spec 9.3; W1b) and the
54
- // @graphty/algorithms mirrors (spec 9.2; D27, until M8a)
59
+ // ==================== types: the accelerator surface and the re-exported declarations of @graphty/layout (spec 9.3)
60
+ // and @graphty/algorithms (spec 9.2), both `import type` since W1b (D27)
55
61
  export type {
56
62
  AcceleratorOptions,
57
63
  AlgorithmAccelerator,
58
64
  ApspResultLike,
59
65
  BellmanFordResultLike,
66
+ BetweennessAcceleratorOptions,
60
67
  BfsResultLike,
61
68
  CommunityResultLike,
62
69
  CorenessResultLike,
63
- CpuAlgorithmOptions,
64
70
  EdgeScoresResultLike,
65
71
  GpuAccelerator,
72
+ HitsOptionsLike,
66
73
  HitsResultLike,
67
74
  LabelResultLike,
68
75
  LayoutAccelerator,
@@ -100,14 +107,18 @@ export type {
100
107
  RaisableLimit,
101
108
  } from "./types/context.js";
102
109
 
103
- // ==================== types: layouts (P3)
110
+ // ==================== types: layouts (P3; the P5 stats and trace records)
104
111
  export type {
105
112
  ForceAtlas2Stats,
106
113
  ForceAtlas2TraceRecord,
114
+ FruchtermanReingoldStats,
115
+ FruchtermanReingoldTraceRecord,
107
116
  GpuLayoutSimulation,
108
117
  GpuLayoutTuning,
109
118
  LayoutStatsBase,
110
119
  RunOptions,
120
+ SpringElectricalStats,
121
+ SpringElectricalTraceRecord,
111
122
  } from "./types/layout.js";
112
123
  export type {
113
124
  CommonLayoutOptions,
@@ -15,7 +15,10 @@ import {
15
15
  FA2_COINCIDENT_SQ,
16
16
  FA2_DISTANCE_FLOOR,
17
17
  FA2_DISTANCE_FLOOR_SQ,
18
+ FA2_FLAG_ADAPTIVE,
18
19
  FA2_FLAG_FIRST,
20
+ FR_COOLING_PATIENCE,
21
+ FR_COOLING_STEP,
19
22
  MAX_WORKGROUPS_PER_DIM,
20
23
  U32_MAX,
21
24
  WORKGROUP_SIZE,
@@ -49,6 +52,9 @@ const FA2_DIST_FLOOR: f32 = ${wgslF32Literal(FA2_DISTANCE_FLOOR)};
49
52
  const FA2_DIST_FLOOR_SQ: f32 = ${wgslF32Literal(FA2_DISTANCE_FLOOR_SQ)};
50
53
  const FA2_COINCIDENT_SQ: f32 = ${wgslF32Literal(FA2_COINCIDENT_SQ)};
51
54
  const FA2_FLAG_FIRST: u32 = ${FA2_FLAG_FIRST}u;
55
+ const FA2_FLAG_ADAPTIVE: u32 = ${FA2_FLAG_ADAPTIVE}u;
56
+ const FR_COOLING_STEP: f32 = ${wgslF32Literal(FR_COOLING_STEP)};
57
+ const FR_COOLING_PATIENCE: u32 = ${FR_COOLING_PATIENCE}u;
52
58
  const F32_MAX: f32 = 0x1.fffffep+127;
53
59
  override WG: u32 = ${WORKGROUP_SIZE}u;
54
60
  override USE_PERM: bool = false;
package/src/kernels.ts CHANGED
@@ -101,7 +101,7 @@ export const FILL_PARAMS: UniformBlock = UniformBlock.define("FillParams", [
101
101
  ["pad0", "u32"],
102
102
  ]);
103
103
 
104
- /** `Fa2Params` (uniform, 96 B; spec 7.3): the per-iteration ForceAtlas2 parameters -- `n` @0, `dim` @4, `flags` @8 (bit 0 = FA2_FLAG_FIRST), `tierStart` @12, `tierEnd` @16, `iterationIndex` @20, `seed` @24, `nearMax` @28, `scalingRatio` @32, `gravity` @36, `jitterTolerance` @40, `scale` @44, `center` @48 (xyz, w 0), `settleThreshold` @64, `extentFactor` @68, `gridMax` @72, `levels` @76, `pad` @80 (reserved for the P4 GridSpec). */
104
+ /** `Fa2Params` (uniform, 128 B; spec 7.3): the per-iteration ForceAtlas2 parameters -- `n` @0, `dim` @4, `flags` @8 (bit 0 = FA2_FLAG_FIRST), `tierStart` @12, `tierEnd` @16, `iterationIndex` @20, `seed` @24, `nearMax` @28, `scalingRatio` @32, `gravity` @36, `jitterTolerance` @40, `scale` @44, `center` @48 (xyz, w 0), `settleThreshold` @64, `extentFactor` @68, `gridMax` @72, `levels` @76, `pad` @80 (reserved for the P4 GridSpec); the P5 model fields (PD-3): `frK` @96 (the FR optimal distance), `temperature` @100 (the FR temperature of this iteration), `springLength` @104, `springCoefficient` @108, `coulomb` @112 (ngraph's `gravity`, negative repels), `dragCoefficient` @116, `timeStep` @120, `pad1` @124; 128 B. */
105
105
  export const FA2_PARAMS: UniformBlock = UniformBlock.define("Fa2Params", [
106
106
  ["n", "u32"],
107
107
  ["dim", "u32"],
@@ -121,9 +121,17 @@ export const FA2_PARAMS: UniformBlock = UniformBlock.define("Fa2Params", [
121
121
  ["gridMax", "u32"],
122
122
  ["levels", "u32"],
123
123
  ["pad", "vec4f"],
124
+ ["frK", "f32"],
125
+ ["temperature", "f32"],
126
+ ["springLength", "f32"],
127
+ ["springCoefficient", "f32"],
128
+ ["coulomb", "f32"],
129
+ ["dragCoefficient", "f32"],
130
+ ["timeStep", "f32"],
131
+ ["pad1", "f32"],
124
132
  ]);
125
133
 
126
- /** `Fa2State` (storage, padded to STATE_HEADER_BYTES = 256; spec 7.3): the device-resident controller state the finalize kernels write and the host reads back for stats -- `speed` @0, `speedEfficiency` @4, `swing` @8, `traction` @12, `centroid` @16, `rmsRadius` @32, `radius` @36, `meanDisplacement` @40, `iteration` @44, `min` @48, `max` @64, `gridMin` @80 (P4), `eps` @96 (P4), `settledCount` @100, `outsideGrid` @104 (P4), `maxCellOccupancy` @108 (P4), `reserved0` .. `reserved8` @112 .. @240. */
134
+ /** `Fa2State` (storage, padded to STATE_HEADER_BYTES = 256; spec 7.3): the device-resident controller state the finalize kernels write and the host reads back for stats -- `speed` @0, `speedEfficiency` @4, `swing` @8, `traction` @12, `centroid` @16, `rmsRadius` @32, `radius` @36, `meanDisplacement` @40, `iteration` @44, `min` @48, `max` @64, `gridMin` @80 (P4), `eps` @96 (P4), `settledCount` @100, `outsideGrid` @104 (P4), `maxCellOccupancy` @108 (P4), `temperature` @112 (FR, written by K1 under STATS_MODE 1), `kineticEnergy` @116 (the preset, K1 under STATS_MODE 2), `reserved0` @120 (vec2f), `reserved1` .. `reserved8` @128 .. @240. */
127
135
  export const FA2_STATE: UniformBlock = UniformBlock.define(
128
136
  "Fa2State",
129
137
  [
@@ -143,7 +151,10 @@ export const FA2_STATE: UniformBlock = UniformBlock.define(
143
151
  ["settledCount", "u32"],
144
152
  ["outsideGrid", "u32"],
145
153
  ["maxCellOccupancy", "u32"],
146
- ["reserved0", "vec4f"],
154
+ ["temperature", "f32"],
155
+ ["kineticEnergy", "f32"],
156
+ ["frEnergy", "f32"],
157
+ ["frProgress", "u32"],
147
158
  ["reserved1", "vec4f"],
148
159
  ["reserved2", "vec4f"],
149
160
  ["reserved3", "vec4f"],
@@ -156,7 +167,7 @@ export const FA2_STATE: UniformBlock = UniformBlock.define(
156
167
  { layout: "storage", padTo: STATE_HEADER_BYTES },
157
168
  );
158
169
 
159
- /** `Fa2Trace` (storage record, 32 B; spec 7.3): one per-iteration trace record -- `swing` @0, `traction` @4, `speed` @8, `speedEfficiency` @12 (written by K4), `meanDisplacement` @16, `settledCount` @20, `iteration` @24 (written by K1), `pad0` @28; the trace region is `array<Fa2Trace>` at byte offset STATE_HEADER_BYTES of the state buffer. */
170
+ /** `Fa2Trace` (storage record, 32 B; spec 7.3): one per-iteration trace record -- `swing` @0, `traction` @4, `speed` @8, `speedEfficiency` @12 (written by K4), `meanDisplacement` @16, `settledCount` @20, `iteration` @24 (written by K1), `modelScalar` @28 (K1: the temperature under STATS_MODE 1, the kinetic energy under 2, 0 under 0); the trace region is `array<Fa2Trace>` at byte offset STATE_HEADER_BYTES of the state buffer. */
160
171
  export const FA2_TRACE: UniformBlock = UniformBlock.define(
161
172
  "Fa2Trace",
162
173
  [
@@ -167,7 +178,7 @@ export const FA2_TRACE: UniformBlock = UniformBlock.define(
167
178
  ["meanDisplacement", "f32"],
168
179
  ["settledCount", "u32"],
169
180
  ["iteration", "u32"],
170
- ["pad0", "u32"],
181
+ ["modelScalar", "f32"],
171
182
  ],
172
183
  { layout: "storage" },
173
184
  );
@@ -335,7 +346,7 @@ const SEGMENTED_REDUCE: KernelEntry = {
335
346
  phase: "P2",
336
347
  };
337
348
 
338
- /** `fa2-stats-finalize` (K1, 3.10.1): the one-workgroup fold of the previous integrate's partials into the state block and the K1 half of the trace record; 3 storage bindings; calls the reduction helpers. */
349
+ /** `fa2-stats-finalize` (K1, 3.10.1): the one-workgroup fold of the previous integrate's partials into the state block and the K1 half of the trace record; STATS_MODE 0 FA2 / 1 FR temperature / 2 kinetic energy (P5); 3 storage bindings; calls the reduction helpers. */
339
350
  const FA2_STATS_FINALIZE: KernelEntry = {
340
351
  id: "fa2-stats-finalize",
341
352
  body: fa2StatsFinalizeWgsl,
@@ -346,14 +357,14 @@ const FA2_STATS_FINALIZE: KernelEntry = {
346
357
  decl(1, 2, "T", "storage", "array<Fa2Trace>"),
347
358
  decl(2, 0, "P", "uniform", "Fa2Params"),
348
359
  ],
349
- overrideDecls: [],
360
+ overrideDecls: [{ name: "STATS_MODE", type: "u32", default: 0 }],
350
361
  uniforms: [FA2_PARAMS, FA2_STATE, FA2_TRACE, FA2_PARTIAL],
351
362
  needs: ["subgroups"],
352
363
  snippetSlots: [],
353
364
  phase: "P3",
354
365
  };
355
366
 
356
- /** `fa2-attraction` (K2, 3.10.1): the thread-per-row attraction gather over the CSR rows (the first writer of `force` each iteration); LINLOG / DISTRIBUTED / TIER 0 plus the standard USE_PERM / HAS_WEIGHTS; 6 storage bindings. */
367
+ /** `fa2-attraction` (K2, 3.10.1): the thread-per-row attraction gather over the CSR rows (the first writer of `force` each iteration); LINLOG / DISTRIBUTED / TIER 0 plus the standard USE_PERM / HAS_WEIGHTS; LAW 0 FA2 / 1 FR / 2 spring (P5); 6 storage bindings. */
357
368
  const FA2_ATTRACTION: KernelEntry = {
358
369
  id: "fa2-attraction",
359
370
  body: fa2AttractionWgsl,
@@ -367,6 +378,7 @@ const FA2_ATTRACTION: KernelEntry = {
367
378
  { name: "LINLOG", type: "bool", default: false },
368
379
  { name: "DISTRIBUTED", type: "bool", default: false },
369
380
  { name: "TIER", type: "u32", default: 0 },
381
+ { name: "LAW", type: "u32", default: 0 },
370
382
  ],
371
383
  uniforms: [FA2_PARAMS],
372
384
  needs: [],
@@ -374,7 +386,7 @@ const FA2_ATTRACTION: KernelEntry = {
374
386
  phase: "P3",
375
387
  };
376
388
 
377
- /** `fa2-repulsion-exact` (K3, 3.10.1): the tiled all-pairs repulsion with the gravity and swing / traction epilogue; 6 storage bindings (`oldForce` read-only: it only calls load_old); calls the reduction helpers. */
389
+ /** `fa2-repulsion-exact` (K3, 3.10.1): the tiled all-pairs repulsion with the gravity and swing / traction epilogue; LAW 0 FA2 / 1 FR / 2 coulomb (P5); 6 storage bindings (`oldForce` read-only: it only calls load_old); calls the reduction helpers. */
378
390
  const FA2_REPULSION_EXACT: KernelEntry = {
379
391
  id: "fa2-repulsion-exact",
380
392
  body: fa2RepulsionExactWgsl,
@@ -392,6 +404,7 @@ const FA2_REPULSION_EXACT: KernelEntry = {
392
404
  { name: "SWING_MODE", type: "u32", default: 0 },
393
405
  { name: "STRONG_GRAVITY", type: "bool", default: false },
394
406
  { name: "GRAVITY_CENTER", type: "u32", default: 0 },
407
+ { name: "LAW", type: "u32", default: 0 },
395
408
  ],
396
409
  uniforms: [FA2_PARAMS, FA2_STATE, FA2_PARTIAL],
397
410
  needs: ["subgroups"],
@@ -417,7 +430,7 @@ const FA2_SPEED_FINALIZE: KernelEntry = {
417
430
  phase: "P1",
418
431
  };
419
432
 
420
- /** `fa2-integrate` (K5, 3.10.1): the per-node speed factor and position update (no clamp, D25), `oldForce` stored in SWING_MODE 0, and the partials A / C of the next K1; 6 storage bindings; calls the reduction helpers. */
433
+ /** `fa2-integrate` (K5, 3.10.1): the per-node speed factor and position update (no clamp, D25), `oldForce` stored in SWING_MODE 0, and the partials A / C of the next K1; APPLY 0 FA2 / 1 FR temperature cap / 2 ngraph Euler (P5); 6 storage bindings; calls the reduction helpers. */
421
434
  const FA2_INTEGRATE: KernelEntry = {
422
435
  id: "fa2-integrate",
423
436
  body: fa2IntegrateWgsl,
@@ -431,7 +444,10 @@ const FA2_INTEGRATE: KernelEntry = {
431
444
  decl(1, 5, "partials", "storage", "array<Fa2Partial>"),
432
445
  decl(2, 0, "P", "uniform", "Fa2Params"),
433
446
  ],
434
- overrideDecls: [{ name: "SWING_MODE", type: "u32", default: 0 }],
447
+ overrideDecls: [
448
+ { name: "SWING_MODE", type: "u32", default: 0 },
449
+ { name: "APPLY", type: "u32", default: 0 },
450
+ ],
435
451
  uniforms: [FA2_PARAMS, FA2_STATE, FA2_PARTIAL],
436
452
  needs: ["subgroups"],
437
453
  snippetSlots: [],
@@ -84,6 +84,8 @@ export interface ModelResources {
84
84
  export interface ModelInputs {
85
85
  readonly mass: F32;
86
86
  readonly weights: ResolvedWeights;
87
+ /** A mask to apply at load (the FR `fixed` option, PD-6); validated against `ceil(n / 32)` words; absent / null leaves the words as they are (kept on a same-size reload, cleared on a resize, spec 7.12). */
88
+ readonly fixed?: NodeMask | null | undefined;
87
89
  }
88
90
 
89
91
  /**
@@ -930,6 +932,15 @@ export class ForceSimulation<
930
932
  `the model resolved ${inputs.mass.length} masses for ${n} nodes`,
931
933
  );
932
934
  }
935
+ const fixedWords = Math.ceil(n / 32);
936
+ if (inputs.fixed !== undefined && inputs.fixed !== null && inputs.fixed.length < fixedWords) {
937
+ throw invalidArgument(
938
+ "fixed",
939
+ inputs.fixed.length,
940
+ fixedWords,
941
+ `the model resolved a fixed mask of ${inputs.fixed.length} words, ${fixedWords} needed for ${n} nodes`,
942
+ );
943
+ }
933
944
  }
934
945
 
935
946
  // ---- every check passed: mutate
@@ -956,6 +967,10 @@ export class ForceSimulation<
956
967
  this.fixedWords = makeMask(n);
957
968
  this.fixedDirty = false;
958
969
  }
970
+ if (inputs !== null && inputs.fixed !== undefined && inputs.fixed !== null) {
971
+ this.fixedWords.set(inputs.fixed.subarray(0, Math.ceil(n / 32)));
972
+ this.fixedDirty = true;
973
+ }
959
974
  if (n === 0 || core === null || inputs === null) {
960
975
  // PLAN DECISION 10: an empty graph loads with no GPU work
961
976
  this.weightsUpload = null;
@@ -1542,22 +1557,24 @@ export class ForceSimulation<
1542
1557
  * The uniform values of one iteration slot: the model's values with the shared fields on top (PLAN DECISION 6).
1543
1558
  * @param global - the global iteration index (iterationsSubmitted + i)
1544
1559
  * @param index - the slot index within the batch (the trace slot)
1545
- * @param flags - FA2_FLAG_FIRST for the first iteration after load(), else 0
1560
+ * @param flags - FA2_FLAG_FIRST for the first iteration after load(), else 0; the model's own flag bits are OR-ed in
1546
1561
  * @returns the values
1547
1562
  */
1548
1563
  private paramsForSlot(global: number, index: number, flags: number): UniformValues {
1549
1564
  const [cx, cy, cz] = this.center;
1565
+ const model = this.model.paramsFor(global, this.optionsValue);
1566
+ const modelFlags = typeof model.flags === "number" ? model.flags : 0;
1550
1567
  const shared: UniformValues = {
1551
1568
  n: this.n,
1552
1569
  dim: this.dimValue,
1553
- flags,
1570
+ flags: flags | modelFlags,
1554
1571
  iterationIndex: index,
1555
1572
  seed: this.seedU32(),
1556
1573
  scale: this.scale,
1557
1574
  center: [cx, cy, cz, 0],
1558
1575
  settleThreshold: this.settleThreshold(),
1559
1576
  };
1560
- return { ...this.model.paramsFor(global, this.optionsValue), ...shared };
1577
+ return { ...model, ...shared };
1561
1578
  }
1562
1579
 
1563
1580
  /**