@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.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 (269) hide show
  1. package/README.md +378 -23
  2. package/dist/browser.d.ts +1 -0
  3. package/dist/browser.js +32 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/chunks/context-E6iKaeuJ.js +3136 -0
  6. package/dist/chunks/context-E6iKaeuJ.js.map +1 -0
  7. package/dist/node.d.ts +1 -0
  8. package/dist/node.js +131 -0
  9. package/dist/node.js.map +1 -0
  10. package/dist/src/accelerator.d.ts +26 -0
  11. package/dist/src/accelerator.d.ts.map +1 -0
  12. package/dist/src/accelerator.js +101 -0
  13. package/dist/src/accelerator.js.map +1 -0
  14. package/dist/src/algorithms/degree.d.ts +35 -0
  15. package/dist/src/algorithms/degree.d.ts.map +1 -0
  16. package/dist/src/algorithms/degree.js +119 -0
  17. package/dist/src/algorithms/degree.js.map +1 -0
  18. package/dist/src/browser/index.d.ts +23 -0
  19. package/dist/src/browser/index.d.ts.map +1 -0
  20. package/dist/src/browser/index.js +48 -0
  21. package/dist/src/browser/index.js.map +1 -0
  22. package/dist/src/constants.d.ts +92 -0
  23. package/dist/src/constants.d.ts.map +1 -0
  24. package/dist/src/constants.js +92 -0
  25. package/dist/src/constants.js.map +1 -0
  26. package/dist/src/context.d.ts +84 -0
  27. package/dist/src/context.d.ts.map +1 -0
  28. package/dist/src/context.js +304 -0
  29. package/dist/src/context.js.map +1 -0
  30. package/dist/src/device/acquire.d.ts +57 -0
  31. package/dist/src/device/acquire.d.ts.map +1 -0
  32. package/dist/src/device/acquire.js +232 -0
  33. package/dist/src/device/acquire.js.map +1 -0
  34. package/dist/src/device/caps.d.ts +43 -0
  35. package/dist/src/device/caps.d.ts.map +1 -0
  36. package/dist/src/device/caps.js +104 -0
  37. package/dist/src/device/caps.js.map +1 -0
  38. package/dist/src/device/error-scope.d.ts +75 -0
  39. package/dist/src/device/error-scope.d.ts.map +1 -0
  40. package/dist/src/device/error-scope.js +152 -0
  41. package/dist/src/device/error-scope.js.map +1 -0
  42. package/dist/src/device/lost.d.ts +51 -0
  43. package/dist/src/device/lost.d.ts.map +1 -0
  44. package/dist/src/device/lost.js +130 -0
  45. package/dist/src/device/lost.js.map +1 -0
  46. package/dist/src/device/webgpu-constants.d.ts +31 -0
  47. package/dist/src/device/webgpu-constants.d.ts.map +1 -0
  48. package/dist/src/device/webgpu-constants.js +31 -0
  49. package/dist/src/device/webgpu-constants.js.map +1 -0
  50. package/dist/src/errors.d.ts +56 -0
  51. package/dist/src/errors.d.ts.map +1 -0
  52. package/dist/src/errors.js +57 -0
  53. package/dist/src/errors.js.map +1 -0
  54. package/dist/src/index.d.ts +29 -0
  55. package/dist/src/index.d.ts.map +1 -0
  56. package/dist/src/index.js +27 -0
  57. package/dist/src/index.js.map +1 -0
  58. package/dist/src/kernel/batch.d.ts +116 -0
  59. package/dist/src/kernel/batch.d.ts.map +1 -0
  60. package/dist/src/kernel/batch.js +335 -0
  61. package/dist/src/kernel/batch.js.map +1 -0
  62. package/dist/src/kernel/dispatch.d.ts +59 -0
  63. package/dist/src/kernel/dispatch.d.ts.map +1 -0
  64. package/dist/src/kernel/dispatch.js +139 -0
  65. package/dist/src/kernel/dispatch.js.map +1 -0
  66. package/dist/src/kernel/kernel.d.ts +84 -0
  67. package/dist/src/kernel/kernel.d.ts.map +1 -0
  68. package/dist/src/kernel/kernel.js +239 -0
  69. package/dist/src/kernel/kernel.js.map +1 -0
  70. package/dist/src/kernel/pipeline-cache.d.ts +90 -0
  71. package/dist/src/kernel/pipeline-cache.d.ts.map +1 -0
  72. package/dist/src/kernel/pipeline-cache.js +251 -0
  73. package/dist/src/kernel/pipeline-cache.js.map +1 -0
  74. package/dist/src/kernel/prelude.d.ts +35 -0
  75. package/dist/src/kernel/prelude.d.ts.map +1 -0
  76. package/dist/src/kernel/prelude.js +211 -0
  77. package/dist/src/kernel/prelude.js.map +1 -0
  78. package/dist/src/kernel/profiler.d.ts +64 -0
  79. package/dist/src/kernel/profiler.d.ts.map +1 -0
  80. package/dist/src/kernel/profiler.js +120 -0
  81. package/dist/src/kernel/profiler.js.map +1 -0
  82. package/dist/src/kernel/struct-block.d.ts +122 -0
  83. package/dist/src/kernel/struct-block.d.ts.map +1 -0
  84. package/dist/src/kernel/struct-block.js +353 -0
  85. package/dist/src/kernel/struct-block.js.map +1 -0
  86. package/dist/src/kernel/uniform-ring.d.ts +70 -0
  87. package/dist/src/kernel/uniform-ring.d.ts.map +1 -0
  88. package/dist/src/kernel/uniform-ring.js +146 -0
  89. package/dist/src/kernel/uniform-ring.js.map +1 -0
  90. package/dist/src/kernel/wgsl.d.ts +88 -0
  91. package/dist/src/kernel/wgsl.d.ts.map +1 -0
  92. package/dist/src/kernel/wgsl.js +390 -0
  93. package/dist/src/kernel/wgsl.js.map +1 -0
  94. package/dist/src/kernels.d.ts +81 -0
  95. package/dist/src/kernels.d.ts.map +1 -0
  96. package/dist/src/kernels.js +417 -0
  97. package/dist/src/kernels.js.map +1 -0
  98. package/dist/src/layouts/force-simulation.d.ts +498 -0
  99. package/dist/src/layouts/force-simulation.d.ts.map +1 -0
  100. package/dist/src/layouts/force-simulation.js +1650 -0
  101. package/dist/src/layouts/force-simulation.js.map +1 -0
  102. package/dist/src/layouts/forceatlas2.d.ts +210 -0
  103. package/dist/src/layouts/forceatlas2.d.ts.map +1 -0
  104. package/dist/src/layouts/forceatlas2.js +759 -0
  105. package/dist/src/layouts/forceatlas2.js.map +1 -0
  106. package/dist/src/layouts/inputs.d.ts +40 -0
  107. package/dist/src/layouts/inputs.d.ts.map +1 -0
  108. package/dist/src/layouts/inputs.js +185 -0
  109. package/dist/src/layouts/inputs.js.map +1 -0
  110. package/dist/src/layouts/repulsion-exact.d.ts +85 -0
  111. package/dist/src/layouts/repulsion-exact.d.ts.map +1 -0
  112. package/dist/src/layouts/repulsion-exact.js +134 -0
  113. package/dist/src/layouts/repulsion-exact.js.map +1 -0
  114. package/dist/src/layouts/seed.d.ts +56 -0
  115. package/dist/src/layouts/seed.d.ts.map +1 -0
  116. package/dist/src/layouts/seed.js +173 -0
  117. package/dist/src/layouts/seed.js.map +1 -0
  118. package/dist/src/memory/buffer-pool.d.ts +73 -0
  119. package/dist/src/memory/buffer-pool.d.ts.map +1 -0
  120. package/dist/src/memory/buffer-pool.js +170 -0
  121. package/dist/src/memory/buffer-pool.js.map +1 -0
  122. package/dist/src/memory/lease.d.ts +53 -0
  123. package/dist/src/memory/lease.d.ts.map +1 -0
  124. package/dist/src/memory/lease.js +85 -0
  125. package/dist/src/memory/lease.js.map +1 -0
  126. package/dist/src/memory/readback.d.ts +143 -0
  127. package/dist/src/memory/readback.d.ts.map +1 -0
  128. package/dist/src/memory/readback.js +375 -0
  129. package/dist/src/memory/readback.js.map +1 -0
  130. package/dist/src/memory/residency.d.ts +83 -0
  131. package/dist/src/memory/residency.d.ts.map +1 -0
  132. package/dist/src/memory/residency.js +573 -0
  133. package/dist/src/memory/residency.js.map +1 -0
  134. package/dist/src/memory/upload-plan.d.ts +101 -0
  135. package/dist/src/memory/upload-plan.d.ts.map +1 -0
  136. package/dist/src/memory/upload-plan.js +265 -0
  137. package/dist/src/memory/upload-plan.js.map +1 -0
  138. package/dist/src/node/index.d.ts +64 -0
  139. package/dist/src/node/index.d.ts.map +1 -0
  140. package/dist/src/node/index.js +183 -0
  141. package/dist/src/node/index.js.map +1 -0
  142. package/dist/src/primitives/reduce.d.ts +57 -0
  143. package/dist/src/primitives/reduce.d.ts.map +1 -0
  144. package/dist/src/primitives/reduce.js +161 -0
  145. package/dist/src/primitives/reduce.js.map +1 -0
  146. package/dist/src/primitives/segmented-reduce.d.ts +38 -0
  147. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -0
  148. package/dist/src/primitives/segmented-reduce.js +211 -0
  149. package/dist/src/primitives/segmented-reduce.js.map +1 -0
  150. package/dist/src/types/accelerator.d.ts +209 -0
  151. package/dist/src/types/accelerator.d.ts.map +1 -0
  152. package/dist/src/types/accelerator.js +8 -0
  153. package/dist/src/types/accelerator.js.map +1 -0
  154. package/dist/src/types/context.d.ts +114 -0
  155. package/dist/src/types/context.d.ts.map +1 -0
  156. package/dist/src/types/context.js +7 -0
  157. package/dist/src/types/context.js.map +1 -0
  158. package/dist/src/types/layout.d.ts +95 -0
  159. package/dist/src/types/layout.d.ts.map +1 -0
  160. package/dist/src/types/layout.js +6 -0
  161. package/dist/src/types/layout.js.map +1 -0
  162. package/dist/src/types/memory.d.ts +22 -0
  163. package/dist/src/types/memory.d.ts.map +1 -0
  164. package/dist/src/types/memory.js +7 -0
  165. package/dist/src/types/memory.js.map +1 -0
  166. package/dist/src/types/options.d.ts +78 -0
  167. package/dist/src/types/options.d.ts.map +1 -0
  168. package/dist/src/types/options.js +7 -0
  169. package/dist/src/types/options.js.map +1 -0
  170. package/dist/src/types/run.d.ts +13 -0
  171. package/dist/src/types/run.d.ts.map +1 -0
  172. package/dist/src/types/run.js +6 -0
  173. package/dist/src/types/run.js.map +1 -0
  174. package/dist/src/wgsl/degree.wgsl.d.ts +10 -0
  175. package/dist/src/wgsl/degree.wgsl.d.ts.map +1 -0
  176. package/dist/src/wgsl/degree.wgsl.js +25 -0
  177. package/dist/src/wgsl/degree.wgsl.js.map +1 -0
  178. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +12 -0
  179. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -0
  180. package/dist/src/wgsl/fa2-attraction.wgsl.js +37 -0
  181. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -0
  182. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +13 -0
  183. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -0
  184. package/dist/src/wgsl/fa2-integrate.wgsl.js +69 -0
  185. package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -0
  186. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +12 -0
  187. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -0
  188. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +79 -0
  189. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -0
  190. package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts +15 -0
  191. package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts.map +1 -0
  192. package/dist/src/wgsl/fa2-speed-finalize.wgsl.js +54 -0
  193. package/dist/src/wgsl/fa2-speed-finalize.wgsl.js.map +1 -0
  194. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +14 -0
  195. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -0
  196. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +57 -0
  197. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -0
  198. package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts +11 -0
  199. package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts.map +1 -0
  200. package/dist/src/wgsl/fa2-to-scene.wgsl.js +19 -0
  201. package/dist/src/wgsl/fa2-to-scene.wgsl.js.map +1 -0
  202. package/dist/src/wgsl/fill.wgsl.d.ts +7 -0
  203. package/dist/src/wgsl/fill.wgsl.d.ts.map +1 -0
  204. package/dist/src/wgsl/fill.wgsl.js +14 -0
  205. package/dist/src/wgsl/fill.wgsl.js.map +1 -0
  206. package/dist/src/wgsl/reduce.wgsl.d.ts +10 -0
  207. package/dist/src/wgsl/reduce.wgsl.d.ts.map +1 -0
  208. package/dist/src/wgsl/reduce.wgsl.js +63 -0
  209. package/dist/src/wgsl/reduce.wgsl.js.map +1 -0
  210. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +13 -0
  211. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -0
  212. package/dist/src/wgsl/segmented-reduce.wgsl.js +35 -0
  213. package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -0
  214. package/dist/tsconfig.build.tsbuildinfo +1 -0
  215. package/dist/webgpu-graph-algorithms.d.ts +1 -0
  216. package/dist/webgpu-graph-algorithms.js +4454 -0
  217. package/dist/webgpu-graph-algorithms.js.map +1 -0
  218. package/package.json +108 -17
  219. package/src/accelerator.ts +117 -0
  220. package/src/algorithms/degree.ts +142 -0
  221. package/src/browser/index.ts +57 -0
  222. package/src/constants.ts +116 -0
  223. package/src/context.ts +399 -0
  224. package/src/device/acquire.ts +256 -0
  225. package/src/device/caps.ts +122 -0
  226. package/src/device/error-scope.ts +171 -0
  227. package/src/device/lost.ts +142 -0
  228. package/src/device/webgpu-constants.ts +44 -0
  229. package/src/errors.ts +94 -0
  230. package/src/index.ts +102 -0
  231. package/src/kernel/batch.ts +427 -0
  232. package/src/kernel/dispatch.ts +162 -0
  233. package/src/kernel/kernel.ts +311 -0
  234. package/src/kernel/pipeline-cache.ts +288 -0
  235. package/src/kernel/prelude.ts +229 -0
  236. package/src/kernel/profiler.ts +148 -0
  237. package/src/kernel/struct-block.ts +439 -0
  238. package/src/kernel/uniform-ring.ts +184 -0
  239. package/src/kernel/wgsl.ts +490 -0
  240. package/src/kernels.ts +511 -0
  241. package/src/layouts/force-simulation.ts +2111 -0
  242. package/src/layouts/forceatlas2.ts +942 -0
  243. package/src/layouts/inputs.ts +252 -0
  244. package/src/layouts/repulsion-exact.ts +183 -0
  245. package/src/layouts/seed.ts +198 -0
  246. package/src/memory/buffer-pool.ts +204 -0
  247. package/src/memory/lease.ts +93 -0
  248. package/src/memory/readback.ts +429 -0
  249. package/src/memory/residency.ts +753 -0
  250. package/src/memory/upload-plan.ts +350 -0
  251. package/src/node/index.ts +230 -0
  252. package/src/primitives/reduce.ts +233 -0
  253. package/src/primitives/segmented-reduce.ts +270 -0
  254. package/src/types/accelerator.ts +236 -0
  255. package/src/types/context.ts +135 -0
  256. package/src/types/layout.ts +103 -0
  257. package/src/types/memory.ts +23 -0
  258. package/src/types/options.ts +84 -0
  259. package/src/types/run.ts +13 -0
  260. package/src/wgsl/degree.wgsl.ts +24 -0
  261. package/src/wgsl/fa2-attraction.wgsl.ts +37 -0
  262. package/src/wgsl/fa2-integrate.wgsl.ts +69 -0
  263. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +78 -0
  264. package/src/wgsl/fa2-speed-finalize.wgsl.ts +53 -0
  265. package/src/wgsl/fa2-stats-finalize.wgsl.ts +57 -0
  266. package/src/wgsl/fa2-to-scene.wgsl.ts +19 -0
  267. package/src/wgsl/fill.wgsl.ts +13 -0
  268. package/src/wgsl/reduce.wgsl.ts +62 -0
  269. package/src/wgsl/segmented-reduce.wgsl.ts +35 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * nodeMass and weight resolution from graph-format primitives only (spec 7.14, 7.5; D28, Q-30): the role-"mass"
3
+ * node column first, then outDegree() + 1; weights from the arc array, from a named edge column expanded to arcs
4
+ * with expandEdges, or none. The Record form of nodeMass is rejected with the hint of spec 7.14 (graphty-element
5
+ * writes its nodeMass config as the role column once at engine creation, so both simulations find it the same way
6
+ * and this package parses no ids).
7
+ */
8
+
9
+ import { type Column, expandEdges, type F32, type GraphSnapshot, type NodeId } from "@graphty/graph-format";
10
+
11
+ import { WebGpuGraphError } from "../errors.js";
12
+
13
+ /** The message of spec 7.14 for the Record form of nodeMass. */
14
+ const NODE_MASS_HINT =
15
+ "write a role 'mass' node column (nodes.set(name, vec, { role: 'mass', replaceRole: true })) or pass a " +
16
+ "Float32Array; @graphty/layout's resolveNodeVector does this on the CPU path";
17
+
18
+ /**
19
+ * An E_INVALID_ARGUMENT with the documented details shape.
20
+ * @param argument - the option name
21
+ * @param value - the offending value
22
+ * @param expected - what was expected
23
+ * @param message - the message
24
+ * @returns the error
25
+ */
26
+ function invalid(argument: string, value: unknown, expected: unknown, message: string): WebGpuGraphError {
27
+ return new WebGpuGraphError("E_INVALID_ARGUMENT", message, { argument, value, expected });
28
+ }
29
+
30
+ /**
31
+ * Asserts every mass is finite and > 0 (spec 7.14: a mass of 0 would divide the distributed action by zero).
32
+ * @param mass - the masses
33
+ * @param source - what the masses came from, for the message
34
+ */
35
+ function checkMass(mass: ArrayLike<number>, source: string): void {
36
+ for (let i = 0; i < mass.length; i++) {
37
+ const v = mass[i];
38
+ if (!Number.isFinite(v) || v <= 0) {
39
+ throw new WebGpuGraphError(
40
+ "E_INVALID_ARGUMENT",
41
+ `${source}: mass[${i}] = ${v} is not a finite number > 0`,
42
+ {
43
+ argument: "nodeMass",
44
+ value: v,
45
+ index: i,
46
+ expected: "finite masses > 0",
47
+ },
48
+ );
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * outDegree()[i] + 1 for every node (spec 7.2 "Mass" row; one O(n) loop over the cached view).
55
+ * @param s - the snapshot
56
+ * @returns a fresh F32 of nodeCount masses
57
+ */
58
+ function degreePlusOne(s: GraphSnapshot): F32 {
59
+ const n = s.nodeCount;
60
+ const degree = s.outDegree();
61
+ const out = new Float32Array(n);
62
+ for (let i = 0; i < n; i++) {
63
+ out[i] = degree[i] + 1;
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /**
69
+ * The masses of a node column: f32 as is, f64 / u32 / i32 through gpuView (f64 = the cached f32 copy; u32 / i32
70
+ * converted to a fresh F32), u8 through column.data; every other dtype is E_INVALID_ARGUMENT (PLAN DECISION 14).
71
+ * @param s - the snapshot (the column belongs to s.nodes)
72
+ * @param column - the node column
73
+ * @returns nodeCount masses
74
+ */
75
+ function massFromColumn(s: GraphSnapshot, column: Column): F32 {
76
+ const { name } = column.meta;
77
+ if (column.meta.components !== 1) {
78
+ throw invalid(
79
+ "nodeMass",
80
+ name,
81
+ "a one-component numeric node column",
82
+ `node column "${name}" has ${column.meta.components} components; a mass column has one`,
83
+ );
84
+ }
85
+ let values: ArrayLike<number>;
86
+ switch (column.dtype) {
87
+ case "f32":
88
+ case "f64": {
89
+ const view = s.nodes.gpuView(name);
90
+ const out = view instanceof Float32Array ? view : new Float32Array(view);
91
+ checkMass(out, `node column "${name}"`);
92
+ return out;
93
+ }
94
+ case "u32":
95
+ case "i32":
96
+ values = s.nodes.gpuView(name);
97
+ break;
98
+ case "u8":
99
+ values = column.data;
100
+ break;
101
+ default:
102
+ throw invalid(
103
+ "nodeMass",
104
+ name,
105
+ "a numeric node column (f32, f64, u32, i32, u8)",
106
+ `node column "${name}" is ${column.dtype}, not numeric`,
107
+ );
108
+ }
109
+ const out = new Float32Array(s.nodeCount);
110
+ for (let i = 0; i < out.length; i++) {
111
+ out[i] = values[i];
112
+ }
113
+ checkMass(out, `node column "${name}"`);
114
+ return out;
115
+ }
116
+
117
+ /**
118
+ * nodeMass resolution by ROLE (spec 7.14): null -> the role-"mass" node column when present (any numeric dtype
119
+ * through gpuView, converted to a fresh F32 when not f32), else outDegree()[i] + 1; an F32 of length n -> as is;
120
+ * a column NAME -> nodes.get(name) (numeric); a Record -> E_UNSUPPORTED with the spec's message. Always returns n
121
+ * values, every one finite and > 0.
122
+ * @param s - the snapshot
123
+ * @param spec - the ForceAtlas2Options.nodeMass value
124
+ * @returns nodeCount masses (the caller's array when an F32 was given)
125
+ */
126
+ export function resolveNodeMass(
127
+ s: GraphSnapshot,
128
+ spec: F32 | string | Readonly<Record<NodeId, number>> | null | undefined,
129
+ ): F32 {
130
+ const n = s.nodeCount;
131
+ if (spec === null || spec === undefined) {
132
+ const column = s.nodes.byRole("mass");
133
+ if (column === null) {
134
+ return degreePlusOne(s);
135
+ }
136
+ return massFromColumn(s, column);
137
+ }
138
+ if (spec instanceof Float32Array) {
139
+ if (spec.length !== n) {
140
+ throw invalid("nodeMass", spec.length, `${n} values`, `nodeMass has ${spec.length} values, expected ${n}`);
141
+ }
142
+ checkMass(spec, "nodeMass");
143
+ return spec;
144
+ }
145
+ if (typeof spec === "string") {
146
+ const column = s.nodes.get(spec);
147
+ if (column === null) {
148
+ throw invalid(
149
+ "nodeMass",
150
+ spec,
151
+ "an existing numeric node column",
152
+ `nodeMass names node column "${spec}", which the snapshot does not hold`,
153
+ );
154
+ }
155
+ return massFromColumn(s, column);
156
+ }
157
+ if (ArrayBuffer.isView(spec)) {
158
+ throw invalid(
159
+ "nodeMass",
160
+ "typed array",
161
+ "a Float32Array",
162
+ "nodeMass must be a Float32Array, a column name or null",
163
+ );
164
+ }
165
+ throw new WebGpuGraphError(
166
+ "E_UNSUPPORTED",
167
+ `nodeMass as a Record is not supported on the GPU path: ${NODE_MASS_HINT}`,
168
+ {
169
+ option: "nodeMass",
170
+ hint: NODE_MASS_HINT,
171
+ },
172
+ );
173
+ }
174
+
175
+ /** What resolveWeights found. */
176
+ export interface ResolvedWeights {
177
+ readonly data: F32 | null;
178
+ readonly source: "arcs" | "column" | "none";
179
+ readonly column: Column | null;
180
+ }
181
+
182
+ /**
183
+ * Weight resolution (spec 7.5, 7.14): true -> { data: s.weights, source: "arcs" } (data null when unweighted ->
184
+ * ones); a string -> a name `s.edges` does not hold is E_INVALID_ARGUMENT (spec 7.14's nodeMass rule applied to
185
+ * weight too), else `column = s.edges.get(name)`, then by `column.dtype`: f32 / f64 / u32 / i32 ->
186
+ * expandEdges(s, s.edges.gpuView(name)) (f64 arrives as gpuView's cached f32 copy; u32 / i32 are converted to a
187
+ * fresh F32; `Column.data` is NOT used because it is U8 for u8 / bool and absent for string / list / json), string /
188
+ * list / json -> the gpuView E_GPU_INELIGIBLE pass-through, u8 / bool / dict -> E_INVALID_ARGUMENT (gpuView returns
189
+ * packed words, not per-edge values); source "column", `column` kept for the version check; false / null /
190
+ * undefined -> none. A named column must have one component (PLAN DECISION 15); the weight values are never
191
+ * inspected.
192
+ * @param s - the snapshot
193
+ * @param spec - the ForceAtlas2Options.weight value
194
+ * @returns the resolved weights
195
+ */
196
+ export function resolveWeights(s: GraphSnapshot, spec: boolean | string | null | undefined): ResolvedWeights {
197
+ if (spec === true) {
198
+ return { data: s.weights, source: "arcs", column: null };
199
+ }
200
+ if (spec === false || spec === null || spec === undefined) {
201
+ return { data: null, source: "none", column: null };
202
+ }
203
+ if (typeof spec !== "string") {
204
+ throw invalid(
205
+ "weight",
206
+ spec,
207
+ "true, false, null or an edge column name",
208
+ "weight must be true, false, null or an edge column name",
209
+ );
210
+ }
211
+ const column = s.edges.get(spec);
212
+ if (column === null) {
213
+ throw invalid(
214
+ "weight",
215
+ spec,
216
+ "an existing numeric edge column",
217
+ `weight names edge column "${spec}", which the snapshot does not hold`,
218
+ );
219
+ }
220
+ if (column.meta.components !== 1) {
221
+ throw invalid(
222
+ "weight",
223
+ spec,
224
+ "a one-component numeric edge column",
225
+ `edge column "${spec}" has ${column.meta.components} components; a weight column has one`,
226
+ );
227
+ }
228
+ switch (column.dtype) {
229
+ case "f32":
230
+ case "f64":
231
+ case "u32":
232
+ case "i32": {
233
+ const view = s.edges.gpuView(spec);
234
+ const expanded = expandEdges(s, view);
235
+ const data = expanded instanceof Float32Array ? expanded : new Float32Array(expanded);
236
+ return { data, source: "column", column };
237
+ }
238
+ case "string":
239
+ case "list":
240
+ case "json":
241
+ // gpuView throws the GraphFormatError E_GPU_INELIGIBLE, which passes through unchanged (D12)
242
+ s.edges.gpuView(spec);
243
+ throw invalid("weight", spec, "a numeric edge column", `edge column "${spec}" is ${column.dtype}`);
244
+ default:
245
+ throw invalid(
246
+ "weight",
247
+ spec,
248
+ "an f32 / f64 / u32 / i32 edge column",
249
+ `edge column "${spec}" is ${column.dtype}: its gpuView is packed words, not per-edge values`,
250
+ );
251
+ }
252
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The exact-tier repulsion stage of ForceAtlas2 (spec 7.6, 7.9, 7.10; contract 3.13): K3, the tiled all-pairs
3
+ * repulsion with the gravity term and the swing / traction epilogue, followed by K4, the one-workgroup speed finalize
4
+ * that folds the per-workgroup partials and runs estimateFactor on the device (D15). P1 ships the stage driven over a
5
+ * pass the caller opens (the tests); P3-T2 records it from ForceAtlas2Model.recordIteration without changing these
6
+ * declarations. Both kernels come from the registry (src/kernels.ts) through the pipeline cache, so a distinct
7
+ * override set is a distinct pipeline and the subgroup twin is selected by the device's features (spec 5.1, D16).
8
+ */
9
+
10
+ import { WebGpuGraphError } from "../errors.js";
11
+ import { type DispatchPlan, plan1d } from "../kernel/dispatch.js";
12
+ import { type BoundKernel, type Kernel } from "../kernel/kernel.js";
13
+ import { type PipelineCache } from "../kernel/pipeline-cache.js";
14
+ import { type WgslModuleSpec } from "../kernel/wgsl.js";
15
+ import { kernelSpec } from "../kernels.js";
16
+ import { type PlanCaps } from "../types/context.js";
17
+ import { type Binding } from "../types/memory.js";
18
+
19
+ /** The buffers the exact-tier repulsion stage binds (all in layout units; spec 7.3). */
20
+ export interface RepulsionExactResources {
21
+ readonly pos: Binding;
22
+ readonly state: Binding;
23
+ readonly trace: Binding;
24
+ readonly force: Binding;
25
+ readonly oldForce: Binding;
26
+ readonly fixedMask: Binding;
27
+ readonly partials: Binding;
28
+ readonly params: Binding;
29
+ }
30
+
31
+ /** The overrides K3 / K4 compile with. */
32
+ export interface RepulsionExactOverrides {
33
+ readonly SWING_MODE: 0 | 1;
34
+ readonly STRONG_GRAVITY: boolean;
35
+ readonly GRAVITY_CENTER: 0 | 1;
36
+ }
37
+
38
+ /** K3 (tiled all-pairs repulsion + gravity + the swing / traction epilogue) followed by K4 (the one-workgroup speed finalize) (spec 7.6, 7.10). */
39
+ export class RepulsionExact {
40
+ /** The overrides both kernels were compiled with (a frozen copy of the argument of create()). */
41
+ readonly overrides: RepulsionExactOverrides;
42
+
43
+ private readonly caps: PlanCaps;
44
+ private readonly repulsion: Kernel;
45
+ private readonly speedFinalize: Kernel;
46
+ /** Exactly one workgroup of K4 (spec 7.4: "1 workgroup"). */
47
+ private readonly finalizePlan: DispatchPlan;
48
+ private boundRepulsion: BoundKernel | null = null;
49
+ private boundSpeedFinalize: BoundKernel | null = null;
50
+
51
+ /**
52
+ * Holds the two compiled kernels; create() is the only caller.
53
+ * @param repulsion - the K3 kernel
54
+ * @param speedFinalize - the K4 kernel
55
+ * @param caps - the device caps the dispatch planner reads
56
+ * @param overrides - the override set both kernels were compiled with
57
+ */
58
+ private constructor(repulsion: Kernel, speedFinalize: Kernel, caps: PlanCaps, overrides: RepulsionExactOverrides) {
59
+ this.repulsion = repulsion;
60
+ this.speedFinalize = speedFinalize;
61
+ this.caps = caps;
62
+ this.overrides = Object.freeze({
63
+ SWING_MODE: overrides.SWING_MODE,
64
+ STRONG_GRAVITY: overrides.STRONG_GRAVITY,
65
+ GRAVITY_CENTER: overrides.GRAVITY_CENTER,
66
+ });
67
+ this.finalizePlan = plan1d(speedFinalize.workgroupSize, speedFinalize.workgroupSize, caps);
68
+ }
69
+
70
+ /**
71
+ * Compiles both kernels through the cache (the twin is selected by caps.features).
72
+ * @param pipelines - the context's pipeline cache
73
+ * @param caps - the device caps the dispatch planner reads
74
+ * @param overrides - the SWING_MODE / STRONG_GRAVITY / GRAVITY_CENTER set of this stage
75
+ * @returns the stage, ready for bind()
76
+ */
77
+ static async create(
78
+ pipelines: PipelineCache,
79
+ caps: PlanCaps,
80
+ overrides: RepulsionExactOverrides,
81
+ ): Promise<RepulsionExact> {
82
+ const [repulsionSpec, finalizeSpec] = RepulsionExact.specs(overrides);
83
+ // Sequential on purpose: PipelineCache.get runs createComputePipelineAsync inside a validation scope, and error
84
+ // scopes form one stack per device, so two interleaved compilations would pop each other's scope.
85
+ const repulsion = await pipelines.kernel(repulsionSpec);
86
+ const speedFinalize = await pipelines.kernel(finalizeSpec);
87
+ return new RepulsionExact(repulsion, speedFinalize, caps, overrides);
88
+ }
89
+
90
+ /**
91
+ * The two specs (for warm() and the compile matrix).
92
+ * @param overrides - the override set of the stage
93
+ * @returns the K3 spec then the K4 spec
94
+ */
95
+ static specs(overrides: RepulsionExactOverrides): readonly [WgslModuleSpec, WgslModuleSpec] {
96
+ return [
97
+ kernelSpec("fa2-repulsion-exact", {
98
+ SWING_MODE: overrides.SWING_MODE,
99
+ STRONG_GRAVITY: overrides.STRONG_GRAVITY,
100
+ GRAVITY_CENTER: overrides.GRAVITY_CENTER,
101
+ }),
102
+ kernelSpec("fa2-speed-finalize", { SWING_MODE: overrides.SWING_MODE }),
103
+ ];
104
+ }
105
+
106
+ /**
107
+ * Creates the bind groups once per load(): K3 takes pos, state (S), force, oldForce, fixedMask, partials and the
108
+ * params slot; K4 takes partials, state (S), trace (T) and the params slot (the 3.10.1 binding names). The state
109
+ * header and the trace region are two disjoint ranges of one buffer, which Kernel.bind() accepts (no writable alias).
110
+ * @param resources - the buffers of spec 7.3
111
+ */
112
+ bind(resources: RepulsionExactResources): void {
113
+ this.boundRepulsion = this.repulsion.bind({
114
+ pos: resources.pos,
115
+ S: resources.state,
116
+ force: resources.force,
117
+ oldForce: resources.oldForce,
118
+ fixedMask: resources.fixedMask,
119
+ partials: resources.partials,
120
+ P: resources.params,
121
+ });
122
+ this.boundSpeedFinalize = this.speedFinalize.bind({
123
+ partials: resources.partials,
124
+ S: resources.state,
125
+ T: resources.trace,
126
+ P: resources.params,
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Records K3 (plan1d(n)) then K4 (1 workgroup) with the params slot's dynamic offset.
132
+ * @param pass - the open compute pass of the batch
133
+ * @param n - the node count (rows [0, n) of pos / force)
134
+ * @param paramsOffset - the dynamic offset of this iteration's Fa2Params slot in the uniform ring
135
+ */
136
+ record(pass: GPUComputePassEncoder, n: number, paramsOffset: number): void {
137
+ this.recordRepulsion(pass, n, paramsOffset);
138
+ this.recordSpeedFinalize(pass, paramsOffset);
139
+ }
140
+
141
+ /**
142
+ * Records K3 only (the inspect() stage split, spec 11.9 item 2).
143
+ * @param pass - the open compute pass
144
+ * @param n - the node count
145
+ * @param paramsOffset - the dynamic offset of the Fa2Params slot
146
+ * @internal
147
+ */
148
+ recordRepulsion(pass: GPUComputePassEncoder, n: number, paramsOffset: number): void {
149
+ const bound = this.bound(this.boundRepulsion, "recordRepulsion");
150
+ const plan = plan1d(n, this.repulsion.workgroupSize, this.caps);
151
+ this.repulsion.dispatch(pass, bound, plan, [paramsOffset]);
152
+ }
153
+
154
+ /**
155
+ * Records K4 only.
156
+ * @param pass - the open compute pass
157
+ * @param paramsOffset - the dynamic offset of the Fa2Params slot
158
+ * @internal
159
+ */
160
+ recordSpeedFinalize(pass: GPUComputePassEncoder, paramsOffset: number): void {
161
+ const bound = this.bound(this.boundSpeedFinalize, "recordSpeedFinalize");
162
+ this.speedFinalize.dispatch(pass, bound, this.finalizePlan, [paramsOffset]);
163
+ }
164
+
165
+ /**
166
+ * The bound kernel of a method, or E_NOT_LOADED when bind() has not run.
167
+ * @param bound - the cached BoundKernel, null before bind()
168
+ * @param method - the caller's name for the message
169
+ * @returns the bound kernel
170
+ */
171
+ private bound(bound: BoundKernel | null, method: string): BoundKernel {
172
+ if (bound === null) {
173
+ throw new WebGpuGraphError(
174
+ "E_NOT_LOADED",
175
+ `RepulsionExact.${method}(): bind() has not been called for this stage`,
176
+ {
177
+ state: "unbound",
178
+ },
179
+ );
180
+ }
181
+ return bound;
182
+ }
183
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The CPU port's random number generator, bit for bit, and the NaN-row seeding both paths share (spec 7.2 "Initial
3
+ * positions", 9.3 seedPositions, 7.14 `pos`, 7.19 topology change): a seed gives the same start on the CPU and the
4
+ * GPU because both write the same f32 values in index order. The package carries its own copy of the LCG for its
5
+ * whole life (D27: it cannot import @graphty/layout); W1 cross-tests it against the real RandomNumberGenerator.
6
+ */
7
+
8
+ import type { F32, GraphSnapshot } from "@graphty/graph-format";
9
+
10
+ import { WebGpuGraphError } from "../errors.js";
11
+
12
+ /** The CPU port's LCG constants (layout/src/utils/random.ts): m = 2^35 - 31, a = 185852, c = 1. */
13
+ export const LCG_M = 34359738337;
14
+ /** The multiplier a of the port's LCG. */
15
+ export const LCG_A = 185852;
16
+ /** The increment c of the port's LCG. */
17
+ export const LCG_C = 1;
18
+
19
+ /**
20
+ * The port's RandomNumberGenerator, bit for bit: `seed || Math.floor(Math.random() * 1000000)` (seed 0 / null =
21
+ * unseeded, the quirk preserved), state = seed % m, next = (a * state + c) % m, value = state / m. The product
22
+ * a * state stays below 2^53 (a < 2^18, state < 2^35), so every step is exact in f64, as it is in the port.
23
+ */
24
+ export class Lcg {
25
+ /** The seed actually used (a random one when unseeded). */
26
+ readonly seed: number;
27
+
28
+ private state: number;
29
+
30
+ /**
31
+ * Creates the generator; a seed of 0, -0, NaN or null means unseeded (the port's `seed || random` quirk).
32
+ * @param seed - the seed, or null for a random seed
33
+ */
34
+ constructor(seed: number | null) {
35
+ this.seed = seed === null || seed === 0 || Number.isNaN(seed) ? Math.floor(Math.random() * 1000000) : seed;
36
+ this.state = this.seed % LCG_M;
37
+ }
38
+
39
+ /**
40
+ * Next value in [0, 1).
41
+ * @returns state / m after one LCG step
42
+ */
43
+ next(): number {
44
+ this.state = (LCG_A * this.state + LCG_C) % LCG_M;
45
+ return this.state / LCG_M;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * The three center components of a CommonLayoutOptions.center (missing components are 0).
51
+ * @param center - the caller's center, or null
52
+ * @returns [x, y, z]; E_INVALID_ARGUMENT when a component is not finite
53
+ */
54
+ function resolveCenter(center: ArrayLike<number> | null): [number, number, number] {
55
+ const out: [number, number, number] = [0, 0, 0];
56
+ if (center === null) {
57
+ return out;
58
+ }
59
+ for (let axis = 0; axis < 3 && axis < center.length; axis++) {
60
+ const v = center[axis];
61
+ if (!Number.isFinite(v)) {
62
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `center[${axis}] is not finite`, {
63
+ argument: "center",
64
+ value: v,
65
+ expected: "finite components",
66
+ });
67
+ }
68
+ out[axis] = v;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * Seeds the unseeded rows of the owner's stride-3 SCENE array in index order (spec 9.3 seedPositions, 7.14 `pos`,
75
+ * 7.19 topology change): a row is unseeded when any of its `dim` components is not finite; every such component
76
+ * draws one LCG value; when NO row is fully finite the draw is uniform in [-1, 1) layout units per axis (`range:
77
+ * "fa2"`; `"fr"` is [0, 1)), otherwise uniform inside the [min, max] box of the finite components per axis,
78
+ * converted to layout units (an axis with no finite value falls back to the range box); the value is written as
79
+ * `v * scale + center[axis]`, which for scale 1 / center 0 is bit-identical to the port's `u * 2 - 1` (resp. `u`);
80
+ * in 2D the third component of a seeded row is written as center[2]. Finite components are never changed and a
81
+ * fully finite row is never touched. No random number is drawn when nothing needs seeding.
82
+ * PLAN DECISION 12 (P3-T1): the box is taken over every finite COMPONENT (the port's rule), not over the fully
83
+ * finite ROWS only; the two differ only when a partially finite row's finite axis lies outside the fully finite
84
+ * rows' box (pinned by test/layouts/seed.test.ts). PLAN DECISION 13: a 2D row whose x and y are finite is seeded
85
+ * whatever its z holds.
86
+ * @param s - the snapshot (nodeCount rows)
87
+ * @param positions - the owner's stride-3 scene-unit array, length 3 * nodeCount, modified in place
88
+ * @param seed - the LCG seed (0 / null = unseeded, the port's quirk)
89
+ * @param dim - 2 or 3
90
+ * @param scale - the scene scale (> 0)
91
+ * @param center - the scene center, or null for the origin
92
+ * @param range - "fa2" for [-1, 1), "fr" for [0, 1)
93
+ */
94
+ export function seedPositions(
95
+ s: GraphSnapshot,
96
+ positions: F32,
97
+ seed: number | null,
98
+ dim: 2 | 3,
99
+ scale: number,
100
+ center: ArrayLike<number> | null,
101
+ range: "fa2" | "fr",
102
+ ): void {
103
+ const n = s.nodeCount;
104
+ if (dim !== 2 && dim !== 3) {
105
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `dim must be 2 or 3, got ${String(dim)}`, {
106
+ argument: "dim",
107
+ value: dim,
108
+ expected: "2 or 3",
109
+ });
110
+ }
111
+ if (positions.length !== 3 * n) {
112
+ throw new WebGpuGraphError(
113
+ "E_INVALID_ARGUMENT",
114
+ `positions has ${positions.length} entries, expected ${3 * n}`,
115
+ {
116
+ argument: "positions",
117
+ value: positions.length,
118
+ expected: 3 * n,
119
+ },
120
+ );
121
+ }
122
+ if (!Number.isFinite(scale) || scale <= 0) {
123
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `scale must be a finite number > 0, got ${scale}`, {
124
+ argument: "scale",
125
+ value: scale,
126
+ expected: "a finite number > 0",
127
+ });
128
+ }
129
+ const c = resolveCenter(center);
130
+
131
+ // classify the rows and collect the per-axis box of the finite components (scene units)
132
+ const lo = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
133
+ const hi = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
134
+ let finiteRows = 0;
135
+ let unseededRows = 0;
136
+ for (let i = 0; i < n; i++) {
137
+ let finite = true;
138
+ for (let axis = 0; axis < dim; axis++) {
139
+ const v = positions[3 * i + axis];
140
+ if (Number.isFinite(v)) {
141
+ if (v < lo[axis]) {
142
+ lo[axis] = v;
143
+ }
144
+ if (v > hi[axis]) {
145
+ hi[axis] = v;
146
+ }
147
+ } else {
148
+ finite = false;
149
+ }
150
+ }
151
+ if (finite) {
152
+ finiteRows++;
153
+ } else {
154
+ unseededRows++;
155
+ }
156
+ }
157
+ if (unseededRows === 0) {
158
+ return;
159
+ }
160
+
161
+ // the per-axis draw box in layout units
162
+ const rangeLo = range === "fr" ? 0 : -1;
163
+ const rangeHi = 1;
164
+ const boxLo = [rangeLo, rangeLo, rangeLo];
165
+ const boxHi = [rangeHi, rangeHi, rangeHi];
166
+ if (finiteRows > 0) {
167
+ for (let axis = 0; axis < dim; axis++) {
168
+ if (Number.isFinite(lo[axis]) && Number.isFinite(hi[axis])) {
169
+ boxLo[axis] = (lo[axis] - c[axis]) / scale;
170
+ boxHi[axis] = (hi[axis] - c[axis]) / scale;
171
+ }
172
+ }
173
+ }
174
+
175
+ const rng = new Lcg(seed);
176
+ for (let i = 0; i < n; i++) {
177
+ let unseeded = false;
178
+ for (let axis = 0; axis < dim; axis++) {
179
+ if (!Number.isFinite(positions[3 * i + axis])) {
180
+ unseeded = true;
181
+ }
182
+ }
183
+ if (!unseeded) {
184
+ continue;
185
+ }
186
+ for (let axis = 0; axis < dim; axis++) {
187
+ const at = 3 * i + axis;
188
+ if (!Number.isFinite(positions[at])) {
189
+ const u = rng.next();
190
+ const v = boxLo[axis] + u * (boxHi[axis] - boxLo[axis]);
191
+ positions[at] = v * scale + c[axis];
192
+ }
193
+ }
194
+ if (dim === 2) {
195
+ positions[3 * i + 2] = c[2];
196
+ }
197
+ }
198
+ }