@graphty/webgpu-graph-algorithms 0.5.0 → 0.6.0

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 (264) hide show
  1. package/README.md +104 -52
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-CRbw2Wyo.js → context-BXqgCifx.js} +225 -33
  4. package/dist/chunks/context-BXqgCifx.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +12 -10
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +32 -10
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/components.d.ts.map +1 -1
  11. package/dist/src/algorithms/components.js +12 -13
  12. package/dist/src/algorithms/components.js.map +1 -1
  13. package/dist/src/algorithms/degree.d.ts +6 -8
  14. package/dist/src/algorithms/degree.d.ts.map +1 -1
  15. package/dist/src/algorithms/degree.js +58 -35
  16. package/dist/src/algorithms/degree.js.map +1 -1
  17. package/dist/src/algorithms/pagerank.d.ts.map +1 -1
  18. package/dist/src/algorithms/pagerank.js +16 -14
  19. package/dist/src/algorithms/pagerank.js.map +1 -1
  20. package/dist/src/algorithms/power-iteration.d.ts +2 -2
  21. package/dist/src/algorithms/power-iteration.d.ts.map +1 -1
  22. package/dist/src/algorithms/power-iteration.js +17 -14
  23. package/dist/src/algorithms/power-iteration.js.map +1 -1
  24. package/dist/src/constants.d.ts +85 -8
  25. package/dist/src/constants.d.ts.map +1 -1
  26. package/dist/src/constants.js +85 -8
  27. package/dist/src/constants.js.map +1 -1
  28. package/dist/src/errors.d.ts +3 -2
  29. package/dist/src/errors.d.ts.map +1 -1
  30. package/dist/src/errors.js +2 -1
  31. package/dist/src/errors.js.map +1 -1
  32. package/dist/src/index.d.ts +10 -5
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +14 -5
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/kernel/dispatch.d.ts +8 -3
  37. package/dist/src/kernel/dispatch.d.ts.map +1 -1
  38. package/dist/src/kernel/dispatch.js +18 -7
  39. package/dist/src/kernel/dispatch.js.map +1 -1
  40. package/dist/src/kernel/kernel.d.ts +30 -1
  41. package/dist/src/kernel/kernel.d.ts.map +1 -1
  42. package/dist/src/kernel/kernel.js +49 -5
  43. package/dist/src/kernel/kernel.js.map +1 -1
  44. package/dist/src/kernel/prelude.d.ts.map +1 -1
  45. package/dist/src/kernel/prelude.js +9 -1
  46. package/dist/src/kernel/prelude.js.map +1 -1
  47. package/dist/src/kernel/profiler.d.ts +15 -3
  48. package/dist/src/kernel/profiler.d.ts.map +1 -1
  49. package/dist/src/kernel/profiler.js +27 -4
  50. package/dist/src/kernel/profiler.js.map +1 -1
  51. package/dist/src/kernels.d.ts +18 -8
  52. package/dist/src/kernels.d.ts.map +1 -1
  53. package/dist/src/kernels.js +345 -22
  54. package/dist/src/kernels.js.map +1 -1
  55. package/dist/src/layouts/calibrate.d.ts +51 -0
  56. package/dist/src/layouts/calibrate.d.ts.map +1 -0
  57. package/dist/src/layouts/calibrate.js +172 -0
  58. package/dist/src/layouts/calibrate.js.map +1 -0
  59. package/dist/src/layouts/force-simulation.d.ts +42 -5
  60. package/dist/src/layouts/force-simulation.d.ts.map +1 -1
  61. package/dist/src/layouts/force-simulation.js +84 -22
  62. package/dist/src/layouts/force-simulation.js.map +1 -1
  63. package/dist/src/layouts/forceatlas2.d.ts +107 -38
  64. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  65. package/dist/src/layouts/forceatlas2.js +297 -290
  66. package/dist/src/layouts/forceatlas2.js.map +1 -1
  67. package/dist/src/layouts/fruchterman-reingold.d.ts +241 -0
  68. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
  69. package/dist/src/layouts/fruchterman-reingold.js +739 -0
  70. package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
  71. package/dist/src/layouts/model-common.d.ts +140 -0
  72. package/dist/src/layouts/model-common.d.ts.map +1 -0
  73. package/dist/src/layouts/model-common.js +269 -0
  74. package/dist/src/layouts/model-common.js.map +1 -0
  75. package/dist/src/layouts/repulsion-grid.d.ts +152 -0
  76. package/dist/src/layouts/repulsion-grid.d.ts.map +1 -0
  77. package/dist/src/layouts/repulsion-grid.js +318 -0
  78. package/dist/src/layouts/repulsion-grid.js.map +1 -0
  79. package/dist/src/layouts/spring-electrical.d.ts +224 -0
  80. package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
  81. package/dist/src/layouts/spring-electrical.js +665 -0
  82. package/dist/src/layouts/spring-electrical.js.map +1 -0
  83. package/dist/src/memory/residency.d.ts +6 -2
  84. package/dist/src/memory/residency.d.ts.map +1 -1
  85. package/dist/src/memory/residency.js +84 -14
  86. package/dist/src/memory/residency.js.map +1 -1
  87. package/dist/src/primitives/core-shape.d.ts +38 -2
  88. package/dist/src/primitives/core-shape.d.ts.map +1 -1
  89. package/dist/src/primitives/core-shape.js +71 -3
  90. package/dist/src/primitives/core-shape.js.map +1 -1
  91. package/dist/src/primitives/grid-pyramid.d.ts +71 -0
  92. package/dist/src/primitives/grid-pyramid.d.ts.map +1 -0
  93. package/dist/src/primitives/grid-pyramid.js +143 -0
  94. package/dist/src/primitives/grid-pyramid.js.map +1 -0
  95. package/dist/src/primitives/grid.d.ts +118 -0
  96. package/dist/src/primitives/grid.d.ts.map +1 -0
  97. package/dist/src/primitives/grid.js +225 -0
  98. package/dist/src/primitives/grid.js.map +1 -0
  99. package/dist/src/primitives/histogram.d.ts +67 -0
  100. package/dist/src/primitives/histogram.d.ts.map +1 -0
  101. package/dist/src/primitives/histogram.js +190 -0
  102. package/dist/src/primitives/histogram.js.map +1 -0
  103. package/dist/src/primitives/radix-sort.d.ts +75 -0
  104. package/dist/src/primitives/radix-sort.d.ts.map +1 -0
  105. package/dist/src/primitives/radix-sort.js +168 -0
  106. package/dist/src/primitives/radix-sort.js.map +1 -0
  107. package/dist/src/primitives/scan.d.ts +44 -0
  108. package/dist/src/primitives/scan.d.ts.map +1 -0
  109. package/dist/src/primitives/scan.js +151 -0
  110. package/dist/src/primitives/scan.js.map +1 -0
  111. package/dist/src/primitives/segmented-reduce.d.ts +25 -17
  112. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
  113. package/dist/src/primitives/segmented-reduce.js +166 -47
  114. package/dist/src/primitives/segmented-reduce.js.map +1 -1
  115. package/dist/src/primitives/spmv.d.ts +18 -14
  116. package/dist/src/primitives/spmv.d.ts.map +1 -1
  117. package/dist/src/primitives/spmv.js +94 -58
  118. package/dist/src/primitives/spmv.js.map +1 -1
  119. package/dist/src/primitives/verify.d.ts +49 -0
  120. package/dist/src/primitives/verify.d.ts.map +1 -0
  121. package/dist/src/primitives/verify.js +229 -0
  122. package/dist/src/primitives/verify.js.map +1 -0
  123. package/dist/src/types/accelerator.d.ts +7 -3
  124. package/dist/src/types/accelerator.d.ts.map +1 -1
  125. package/dist/src/types/context.d.ts +53 -0
  126. package/dist/src/types/context.d.ts.map +1 -1
  127. package/dist/src/types/layout.d.ts +52 -0
  128. package/dist/src/types/layout.d.ts.map +1 -1
  129. package/dist/src/types/options.d.ts +43 -1
  130. package/dist/src/types/options.d.ts.map +1 -1
  131. package/dist/src/wgsl/counting-scatter.wgsl.d.ts +8 -0
  132. package/dist/src/wgsl/counting-scatter.wgsl.d.ts.map +1 -0
  133. package/dist/src/wgsl/counting-scatter.wgsl.js +17 -0
  134. package/dist/src/wgsl/counting-scatter.wgsl.js.map +1 -0
  135. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +23 -8
  136. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
  137. package/dist/src/wgsl/fa2-attraction.wgsl.js +100 -17
  138. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
  139. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
  140. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
  141. package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
  142. package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
  143. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
  144. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
  145. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
  146. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
  147. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +12 -1
  148. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
  149. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +54 -0
  150. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
  151. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +8 -0
  152. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -0
  153. package/dist/src/wgsl/grid-cell-key.wgsl.js +30 -0
  154. package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -0
  155. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +8 -0
  156. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts.map +1 -0
  157. package/dist/src/wgsl/grid-centroid-hub.wgsl.js +29 -0
  158. package/dist/src/wgsl/grid-centroid-hub.wgsl.js.map +1 -0
  159. package/dist/src/wgsl/grid-centroid.wgsl.d.ts +8 -0
  160. package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -0
  161. package/dist/src/wgsl/grid-centroid.wgsl.js +29 -0
  162. package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -0
  163. package/dist/src/wgsl/grid-downsample.wgsl.d.ts +7 -0
  164. package/dist/src/wgsl/grid-downsample.wgsl.d.ts.map +1 -0
  165. package/dist/src/wgsl/grid-downsample.wgsl.js +28 -0
  166. package/dist/src/wgsl/grid-downsample.wgsl.js.map +1 -0
  167. package/dist/src/wgsl/grid-far-field.wgsl.d.ts +13 -0
  168. package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -0
  169. package/dist/src/wgsl/grid-far-field.wgsl.js +98 -0
  170. package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -0
  171. package/dist/src/wgsl/grid-near-field.wgsl.d.ts +19 -0
  172. package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -0
  173. package/dist/src/wgsl/grid-near-field.wgsl.js +129 -0
  174. package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -0
  175. package/dist/src/wgsl/histogram.wgsl.d.ts +7 -0
  176. package/dist/src/wgsl/histogram.wgsl.d.ts.map +1 -0
  177. package/dist/src/wgsl/histogram.wgsl.js +15 -0
  178. package/dist/src/wgsl/histogram.wgsl.js.map +1 -0
  179. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts +8 -0
  180. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts.map +1 -0
  181. package/dist/src/wgsl/indirect-finalize.wgsl.js +26 -0
  182. package/dist/src/wgsl/indirect-finalize.wgsl.js.map +1 -0
  183. package/dist/src/wgsl/radix-hist.wgsl.d.ts +9 -0
  184. package/dist/src/wgsl/radix-hist.wgsl.d.ts.map +1 -0
  185. package/dist/src/wgsl/radix-hist.wgsl.js +31 -0
  186. package/dist/src/wgsl/radix-hist.wgsl.js.map +1 -0
  187. package/dist/src/wgsl/radix-scatter.wgsl.d.ts +9 -0
  188. package/dist/src/wgsl/radix-scatter.wgsl.d.ts.map +1 -0
  189. package/dist/src/wgsl/radix-scatter.wgsl.js +40 -0
  190. package/dist/src/wgsl/radix-scatter.wgsl.js.map +1 -0
  191. package/dist/src/wgsl/scan-add.wgsl.d.ts +6 -0
  192. package/dist/src/wgsl/scan-add.wgsl.d.ts.map +1 -0
  193. package/dist/src/wgsl/scan-add.wgsl.js +14 -0
  194. package/dist/src/wgsl/scan-add.wgsl.js.map +1 -0
  195. package/dist/src/wgsl/scan-block.wgsl.d.ts +8 -0
  196. package/dist/src/wgsl/scan-block.wgsl.d.ts.map +1 -0
  197. package/dist/src/wgsl/scan-block.wgsl.js +30 -0
  198. package/dist/src/wgsl/scan-block.wgsl.js.map +1 -0
  199. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +22 -8
  200. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -1
  201. package/dist/src/wgsl/segmented-reduce.wgsl.js +84 -15
  202. package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -1
  203. package/dist/src/wgsl/spmv-pull.wgsl.d.ts +22 -11
  204. package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -1
  205. package/dist/src/wgsl/spmv-pull.wgsl.js +110 -36
  206. package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -1
  207. package/dist/tsconfig.build.tsbuildinfo +1 -1
  208. package/dist/webgpu-graph-algorithms.js +5016 -1130
  209. package/dist/webgpu-graph-algorithms.js.map +1 -1
  210. package/package.json +10 -7
  211. package/src/accelerator.ts +46 -12
  212. package/src/algorithms/components.ts +12 -16
  213. package/src/algorithms/degree.ts +58 -43
  214. package/src/algorithms/pagerank.ts +20 -18
  215. package/src/algorithms/power-iteration.ts +19 -18
  216. package/src/constants.ts +108 -8
  217. package/src/errors.ts +3 -1
  218. package/src/index.ts +25 -5
  219. package/src/kernel/dispatch.ts +18 -7
  220. package/src/kernel/kernel.ts +59 -5
  221. package/src/kernel/prelude.ts +15 -0
  222. package/src/kernel/profiler.ts +28 -4
  223. package/src/kernels.ts +378 -24
  224. package/src/layouts/calibrate.ts +187 -0
  225. package/src/layouts/force-simulation.ts +111 -26
  226. package/src/layouts/forceatlas2.ts +346 -324
  227. package/src/layouts/fruchterman-reingold.ts +918 -0
  228. package/src/layouts/model-common.ts +323 -0
  229. package/src/layouts/repulsion-grid.ts +451 -0
  230. package/src/layouts/spring-electrical.ts +845 -0
  231. package/src/memory/residency.ts +126 -20
  232. package/src/primitives/core-shape.ts +91 -4
  233. package/src/primitives/grid-pyramid.ts +221 -0
  234. package/src/primitives/grid.ts +349 -0
  235. package/src/primitives/histogram.ts +273 -0
  236. package/src/primitives/radix-sort.ts +246 -0
  237. package/src/primitives/scan.ts +197 -0
  238. package/src/primitives/segmented-reduce.ts +214 -56
  239. package/src/primitives/spmv.ts +125 -65
  240. package/src/primitives/verify.ts +249 -0
  241. package/src/types/accelerator.ts +15 -3
  242. package/src/types/context.ts +56 -0
  243. package/src/types/layout.ts +58 -0
  244. package/src/types/options.ts +45 -1
  245. package/src/wgsl/counting-scatter.wgsl.ts +16 -0
  246. package/src/wgsl/fa2-attraction.wgsl.ts +100 -17
  247. package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
  248. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
  249. package/src/wgsl/fa2-stats-finalize.wgsl.ts +54 -0
  250. package/src/wgsl/grid-cell-key.wgsl.ts +29 -0
  251. package/src/wgsl/grid-centroid-hub.wgsl.ts +28 -0
  252. package/src/wgsl/grid-centroid.wgsl.ts +28 -0
  253. package/src/wgsl/grid-downsample.wgsl.ts +27 -0
  254. package/src/wgsl/grid-far-field.wgsl.ts +97 -0
  255. package/src/wgsl/grid-near-field.wgsl.ts +128 -0
  256. package/src/wgsl/histogram.wgsl.ts +14 -0
  257. package/src/wgsl/indirect-finalize.wgsl.ts +25 -0
  258. package/src/wgsl/radix-hist.wgsl.ts +30 -0
  259. package/src/wgsl/radix-scatter.wgsl.ts +39 -0
  260. package/src/wgsl/scan-add.wgsl.ts +13 -0
  261. package/src/wgsl/scan-block.wgsl.ts +29 -0
  262. package/src/wgsl/segmented-reduce.wgsl.ts +84 -15
  263. package/src/wgsl/spmv-pull.wgsl.ts +110 -36
  264. package/dist/chunks/context-CRbw2Wyo.js.map +0 -1
@@ -0,0 +1,323 @@
1
+ /**
2
+ * The option and value helpers every force model's resolver and stats decoder share (P5 PD-7): moved verbatim
3
+ * from src/layouts/forceatlas2.ts (P3-T2) so the Fruchterman-Reingold and spring-electrical models of P5 neither
4
+ * copy them nor import a sibling model; and, since P4-T6, the K2 tier binding and dispatch the three models share
5
+ * (`bindAttraction` / `recordAttraction`, P4 PD-7), so every model's bind() / recordIteration() calls one function
6
+ * instead of holding its own copy. Layout zone.
7
+ */
8
+
9
+ import { WebGpuGraphError } from "../errors.js";
10
+ import { type DispatchPlan, plan1d } from "../kernel/dispatch.js";
11
+ import { type BoundKernel, type Kernel } from "../kernel/kernel.js";
12
+ import { type UniformValues } from "../kernel/struct-block.js";
13
+ import { graphBindings, kernelSpec } from "../kernels.js";
14
+ import { MID_TIER_LANES } from "../primitives/core-shape.js";
15
+ import { type Binding } from "../types/memory.js";
16
+ import { type ModelResources } from "./force-simulation.js";
17
+
18
+ /** An override record as the kernel layer takes it. */
19
+ export type Overrides = Readonly<Record<string, number | boolean>>;
20
+
21
+ /** The K2 (`fa2-attraction`) dispatches of one iteration: `[kernel, bind group, plan]` in dispatch order TIER 2, TIER 1, TIER 0 (only the tiers whose row range is non-empty). */
22
+ export interface AttractionBound {
23
+ readonly kernels: readonly (readonly [Kernel, BoundKernel, DispatchPlan])[];
24
+ }
25
+
26
+ /** The group-1 / group-2 bindings of K2 every tier dispatch shares: `pos` (vec4f, mass in w), `force` (stride-3 f32) and the Fa2Params slot of the UniformRing. @public the bindings parameter of bindAttraction */
27
+ export interface AttractionBindings {
28
+ readonly pos: Binding;
29
+ readonly force: Binding;
30
+ readonly params: Binding;
31
+ }
32
+
33
+ /**
34
+ * Compiles (through the cache) and binds the `fa2-attraction` pipelines a load needs against the graph group and
35
+ * { pos, force, P } (P4 PD-7): TIER 0 always; TIER 1 when a row of degree 32..1023 exists (`[hiEnd, midEnd)` is
36
+ * non-empty); TIER 2 when a row of degree >= 1024 exists (`hiEnd > 0`). The dispatch plans are one workgroup per
37
+ * row for TIER 2, `WG / 32` rows per workgroup for TIER 1 and one row per thread for TIER 0, over each tier's row
38
+ * count (the K2 body reads its range from `Fa2Params.hiEnd` / `midEnd` / `tierStart` / `tierEnd`). The TIER 1 / 2
39
+ * pipelines compile on the first load that needs them (a one-time cost at that load); a model's `specs()` lists
40
+ * the TIER 0 spec only, because it has no `n` to know which tiers a load needs. A device whose workgroup size is
41
+ * below 32 cannot fold the mid tier: E_UNSUPPORTED { feature: "fa2-attraction.tiers" } when a permutation is bound.
42
+ * @param resources - the load's resources (core, tiers, weights, pipelines, caps)
43
+ * @param k2 - the K2 override record of the model (LINLOG / DISTRIBUTED / LAW plus USE_PERM / HAS_WEIGHTS; TIER is overwritten per dispatch)
44
+ * @param bindings - the group-1 / group-2 bindings shared by every tier
45
+ * @returns the bound dispatches in order TIER 2, 1, 0
46
+ */
47
+ export async function bindAttraction(
48
+ resources: ModelResources,
49
+ k2: Overrides,
50
+ bindings: AttractionBindings,
51
+ ): Promise<AttractionBound> {
52
+ const { n, core, perm, tiers, weights, pipelines, caps } = resources;
53
+ const so = tiers?.segmentOffsets;
54
+ const hiEnd = so?.[1] ?? 0;
55
+ const midEnd = so?.[2] ?? 0;
56
+ const ranges: readonly { readonly tier: 0 | 1 | 2; readonly rows: number }[] = [
57
+ { tier: 2, rows: hiEnd },
58
+ { tier: 1, rows: midEnd - hiEnd },
59
+ { tier: 0, rows: n - midEnd },
60
+ ];
61
+ const group = {
62
+ ...graphBindings(core, perm, weights),
63
+ pos: bindings.pos,
64
+ force: bindings.force,
65
+ P: bindings.params,
66
+ };
67
+ const kernels: (readonly [Kernel, BoundKernel, DispatchPlan])[] = [];
68
+ for (const { tier, rows } of ranges) {
69
+ if (tier !== 0 && rows <= 0) {
70
+ continue;
71
+ }
72
+ // sequential on purpose: PipelineCache.get compiles inside a validation scope, one stack per device
73
+ const kernel = await pipelines.kernel(kernelSpec("fa2-attraction", { ...k2, TIER: tier }));
74
+ const wg = kernel.workgroupSize;
75
+ if (tier !== 0 && wg < MID_TIER_LANES) {
76
+ throw new WebGpuGraphError(
77
+ "E_UNSUPPORTED",
78
+ `fa2-attraction: the mid tier folds ${MID_TIER_LANES} lanes per row, more than the workgroup size ${wg}`,
79
+ { feature: "fa2-attraction.tiers" },
80
+ );
81
+ }
82
+ if (rows <= 0) {
83
+ continue;
84
+ }
85
+ let rowsPerGroup = wg;
86
+ if (tier === 2) {
87
+ rowsPerGroup = 1;
88
+ } else if (tier === 1) {
89
+ rowsPerGroup = wg / MID_TIER_LANES;
90
+ }
91
+ kernels.push([kernel, kernel.bind(group), plan1d(rows, rowsPerGroup, caps)]);
92
+ }
93
+ return { kernels };
94
+ }
95
+
96
+ /**
97
+ * Records the K2 dispatches of one iteration in order TIER 2, TIER 1, TIER 0 with the iteration's params offset.
98
+ * @param pass - the open compute pass
99
+ * @param bound - what bindAttraction produced
100
+ * @param paramsOffset - the UniformRing byte offset of this iteration's Fa2Params
101
+ */
102
+ export function recordAttraction(pass: GPUComputePassEncoder, bound: AttractionBound, paramsOffset: number): void {
103
+ for (const [kernel, group, plan] of bound.kernels) {
104
+ kernel.dispatch(pass, group, plan, [paramsOffset]);
105
+ }
106
+ }
107
+
108
+ /** Bytes of the stride-3 f32 force arrays per node. */
109
+ export const FORCE_BYTES_PER_NODE = 12;
110
+
111
+ /** The name of the model-owned FillParams buffer (a BufferSpec, reached through ModelResources.buffer). */
112
+ export const FILL_PARAMS_BUFFER = "fillParams";
113
+
114
+ /** 2^32, the modulus of the u32 seed word (computed with `%`, never a bitwise operator). */
115
+ const U32_MODULUS = 4294967296;
116
+
117
+ /**
118
+ * A short, safe rendering of an argument value for error messages (never String() on an object).
119
+ * @param value - the value
120
+ * @returns the rendering
121
+ */
122
+ export function describeValue(value: unknown): string {
123
+ if (value === null) {
124
+ return "null";
125
+ }
126
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
127
+ return String(value);
128
+ }
129
+ if (typeof value === "undefined") {
130
+ return "undefined";
131
+ }
132
+ if (typeof value === "object" && "length" in value && typeof value.length === "number") {
133
+ return `[${value.length} values]`;
134
+ }
135
+ return typeof value;
136
+ }
137
+
138
+ /**
139
+ * The E_INVALID_ARGUMENT error of an option check (contract 3.1: { argument, value, expected }).
140
+ * @param argument - the option name
141
+ * @param value - the value given
142
+ * @param expected - what was expected
143
+ * @returns the error (not thrown here)
144
+ */
145
+ export function invalid(argument: string, value: unknown, expected: string): WebGpuGraphError {
146
+ return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be ${expected}; got ${describeValue(value)}`, {
147
+ argument,
148
+ value,
149
+ expected,
150
+ });
151
+ }
152
+
153
+ /**
154
+ * A numeric option: the given value when defined, else the fallback; validated by `check` (the value is checked as
155
+ * `unknown` so a JS caller's string or object is E_INVALID_ARGUMENT too).
156
+ * @param name - the option name
157
+ * @param given - the value given (undefined = absent)
158
+ * @param fallback - the previous record's value or the default
159
+ * @param check - the range predicate over a finite number
160
+ * @param expected - the range in words (the error message)
161
+ * @returns the value
162
+ */
163
+ export function pickNumber(
164
+ name: string,
165
+ given: number | undefined,
166
+ fallback: number,
167
+ check: (value: number) => boolean,
168
+ expected: string,
169
+ ): number {
170
+ const value: unknown = given === undefined ? fallback : given;
171
+ if (typeof value !== "number" || !Number.isFinite(value) || !check(value)) {
172
+ throw invalid(name, value, expected);
173
+ }
174
+ return value;
175
+ }
176
+
177
+ /**
178
+ * A boolean option: the given value when defined, else the fallback; a non-boolean is E_INVALID_ARGUMENT.
179
+ * @param name - the option name
180
+ * @param given - the value given (undefined = absent)
181
+ * @param fallback - the previous record's value or the default
182
+ * @returns the value
183
+ */
184
+ export function pickBoolean(name: string, given: boolean | undefined, fallback: boolean): boolean {
185
+ const value: unknown = given === undefined ? fallback : given;
186
+ if (typeof value !== "boolean") {
187
+ throw invalid(name, value, "a boolean");
188
+ }
189
+ return value;
190
+ }
191
+
192
+ /**
193
+ * The layout dimension: 2 or 3.
194
+ * @param given - the value given (undefined = absent)
195
+ * @param fallback - the previous record's value or the default
196
+ * @returns 2 or 3
197
+ */
198
+ export function pickDim(given: 2 | 3 | undefined, fallback: 2 | 3): 2 | 3 {
199
+ const value: unknown = given === undefined ? fallback : given;
200
+ if (value !== 2 && value !== 3) {
201
+ throw invalid("dim", value, "2 or 3");
202
+ }
203
+ return value;
204
+ }
205
+
206
+ /**
207
+ * The scene-unit center: an array-like of 2 (z = 0) or 3 finite numbers.
208
+ * @param given - the value given (undefined = absent)
209
+ * @param fallback - the previous record's value or the default
210
+ * @returns the three components
211
+ */
212
+ export function pickCenter(
213
+ given: ArrayLike<number> | undefined,
214
+ fallback: readonly [number, number, number],
215
+ ): readonly [number, number, number] {
216
+ if (given === undefined) {
217
+ return fallback;
218
+ }
219
+ const expected = "an array of 2 or 3 finite numbers";
220
+ const value: unknown = given;
221
+ if (typeof value !== "object" || value === null || !("length" in value)) {
222
+ throw invalid("center", given, expected);
223
+ }
224
+ const { length } = value;
225
+ if (length !== 2 && length !== 3) {
226
+ throw invalid("center", given, expected);
227
+ }
228
+ const x: unknown = given[0];
229
+ const y: unknown = given[1];
230
+ const z: unknown = length === 3 ? given[2] : 0;
231
+ if (
232
+ typeof x !== "number" ||
233
+ typeof y !== "number" ||
234
+ typeof z !== "number" ||
235
+ !Number.isFinite(x) ||
236
+ !Number.isFinite(y) ||
237
+ !Number.isFinite(z)
238
+ ) {
239
+ throw invalid("center", given, expected);
240
+ }
241
+ return [x, y, z];
242
+ }
243
+
244
+ /**
245
+ * The seed: a finite number, or null (unseeded; 0 keeps the port's "0 = unseeded" quirk through the Lcg).
246
+ * @param given - the value given (undefined = absent)
247
+ * @param fallback - the previous record's value or the default
248
+ * @returns the seed or null
249
+ */
250
+ export function pickSeed(given: number | null | undefined, fallback: number | null): number | null {
251
+ if (given === undefined) {
252
+ return fallback;
253
+ }
254
+ const value: unknown = given;
255
+ if (value !== null && (typeof value !== "number" || !Number.isFinite(value))) {
256
+ throw invalid("seed", given, "a finite number or null");
257
+ }
258
+ return value;
259
+ }
260
+
261
+ /**
262
+ * Integer >= 1.
263
+ * @param value - a finite number
264
+ * @returns whether it is a positive integer
265
+ */
266
+ export function isPositiveInteger(value: number): boolean {
267
+ return Number.isInteger(value) && value >= 1;
268
+ }
269
+
270
+ /**
271
+ * The u32 word written into Fa2Params.seed: 0 for null, else floor(|seed|) mod 2^32.
272
+ * @param seed - the resolved seed
273
+ * @returns the u32 value
274
+ */
275
+ export function seedWord(seed: number | null): number {
276
+ if (seed === null) {
277
+ return 0;
278
+ }
279
+ return Math.floor(Math.abs(seed)) % U32_MODULUS;
280
+ }
281
+
282
+ /**
283
+ * A scalar field of a block's read() result.
284
+ * @param values - the values read
285
+ * @param name - the field name
286
+ * @returns the number
287
+ */
288
+ export function scalar(values: UniformValues, name: string): number {
289
+ const value = values[name];
290
+ if (typeof value !== "number") {
291
+ throw invalid(name, value, "a scalar field");
292
+ }
293
+ return value;
294
+ }
295
+
296
+ /**
297
+ * A vector field of a block's read() result.
298
+ * @param values - the values read
299
+ * @param name - the field name
300
+ * @returns the components
301
+ */
302
+ export function vector(values: UniformValues, name: string): readonly number[] {
303
+ const value = values[name];
304
+ if (typeof value === "number") {
305
+ throw invalid(name, value, "a vector field");
306
+ }
307
+ return value;
308
+ }
309
+
310
+ /**
311
+ * The override record a kernel gets: its defaults overlaid with the values present in the merged set (contract 3.9:
312
+ * a name a spec does not declare is rejected at compose time, so nothing else is passed through).
313
+ * @param merged - the merged override set of the model (plus USE_PERM / HAS_WEIGHTS from the simulation)
314
+ * @param defaults - the kernel's accepted names with their defaults
315
+ * @returns the kernel's override record, every accepted name explicit
316
+ */
317
+ export function subset(merged: Overrides, defaults: Overrides): Overrides {
318
+ const out: Record<string, number | boolean> = {};
319
+ for (const name of Object.keys(defaults)) {
320
+ out[name] = name in merged ? merged[name] : defaults[name];
321
+ }
322
+ return out;
323
+ }