@graphty/webgpu-graph-algorithms 0.6.4 → 0.6.6

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 (147) hide show
  1. package/README.md +38 -17
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-Dvq-Cc6v.js → context-DiSr6eiz.js} +45 -33
  4. package/dist/chunks/context-DiSr6eiz.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/algorithms/bfs.d.ts +11 -7
  7. package/dist/src/algorithms/bfs.d.ts.map +1 -1
  8. package/dist/src/algorithms/bfs.js +33 -10
  9. package/dist/src/algorithms/bfs.js.map +1 -1
  10. package/dist/src/algorithms/scope.d.ts +3 -3
  11. package/dist/src/algorithms/scope.d.ts.map +1 -1
  12. package/dist/src/algorithms/scope.js +0 -2
  13. package/dist/src/algorithms/scope.js.map +1 -1
  14. package/dist/src/algorithms/sssp.d.ts +4 -3
  15. package/dist/src/algorithms/sssp.d.ts.map +1 -1
  16. package/dist/src/algorithms/sssp.js +4 -3
  17. package/dist/src/algorithms/sssp.js.map +1 -1
  18. package/dist/src/constants.d.ts +33 -2
  19. package/dist/src/constants.d.ts.map +1 -1
  20. package/dist/src/constants.js +33 -2
  21. package/dist/src/constants.js.map +1 -1
  22. package/dist/src/kernel/dispatch.d.ts +2 -2
  23. package/dist/src/kernel/dispatch.d.ts.map +1 -1
  24. package/dist/src/kernel/kernel.d.ts +1 -1
  25. package/dist/src/kernel/kernel.js +2 -2
  26. package/dist/src/kernel/kernel.js.map +1 -1
  27. package/dist/src/kernel/prelude.d.ts.map +1 -1
  28. package/dist/src/kernel/prelude.js +2 -1
  29. package/dist/src/kernel/prelude.js.map +1 -1
  30. package/dist/src/kernels.d.ts +15 -11
  31. package/dist/src/kernels.d.ts.map +1 -1
  32. package/dist/src/kernels.js +42 -21
  33. package/dist/src/kernels.js.map +1 -1
  34. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  35. package/dist/src/layouts/forceatlas2.js +2 -1
  36. package/dist/src/layouts/forceatlas2.js.map +1 -1
  37. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -1
  38. package/dist/src/layouts/fruchterman-reingold.js +4 -2
  39. package/dist/src/layouts/fruchterman-reingold.js.map +1 -1
  40. package/dist/src/layouts/repulsion-exact.d.ts +16 -0
  41. package/dist/src/layouts/repulsion-exact.d.ts.map +1 -1
  42. package/dist/src/layouts/repulsion-exact.js +21 -1
  43. package/dist/src/layouts/repulsion-exact.js.map +1 -1
  44. package/dist/src/layouts/repulsion-grid.d.ts +1 -1
  45. package/dist/src/layouts/repulsion-grid.js +1 -1
  46. package/dist/src/layouts/spring-electrical.d.ts.map +1 -1
  47. package/dist/src/layouts/spring-electrical.js +6 -2
  48. package/dist/src/layouts/spring-electrical.js.map +1 -1
  49. package/dist/src/primitives/advance.d.ts +3 -2
  50. package/dist/src/primitives/advance.d.ts.map +1 -1
  51. package/dist/src/primitives/advance.js.map +1 -1
  52. package/dist/src/primitives/frontier.d.ts +34 -38
  53. package/dist/src/primitives/frontier.d.ts.map +1 -1
  54. package/dist/src/primitives/frontier.js +24 -32
  55. package/dist/src/primitives/frontier.js.map +1 -1
  56. package/dist/src/primitives/grid-pyramid.d.ts +4 -4
  57. package/dist/src/primitives/grid-pyramid.d.ts.map +1 -1
  58. package/dist/src/primitives/grid-pyramid.js +4 -3
  59. package/dist/src/primitives/grid-pyramid.js.map +1 -1
  60. package/dist/src/primitives/grid.d.ts +13 -10
  61. package/dist/src/primitives/grid.d.ts.map +1 -1
  62. package/dist/src/primitives/grid.js +10 -7
  63. package/dist/src/primitives/grid.js.map +1 -1
  64. package/dist/src/wgsl/advance-expand.wgsl.d.ts +4 -3
  65. package/dist/src/wgsl/advance-expand.wgsl.d.ts.map +1 -1
  66. package/dist/src/wgsl/advance-expand.wgsl.js +4 -3
  67. package/dist/src/wgsl/advance-expand.wgsl.js.map +1 -1
  68. package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts +4 -3
  69. package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts.map +1 -1
  70. package/dist/src/wgsl/bfs-bottom-up.wgsl.js +4 -3
  71. package/dist/src/wgsl/bfs-bottom-up.wgsl.js.map +1 -1
  72. package/dist/src/wgsl/bfs-fused.wgsl.d.ts +6 -6
  73. package/dist/src/wgsl/bfs-fused.wgsl.d.ts.map +1 -1
  74. package/dist/src/wgsl/bfs-fused.wgsl.js +6 -6
  75. package/dist/src/wgsl/bfs-next-degree.wgsl.d.ts +23 -0
  76. package/dist/src/wgsl/bfs-next-degree.wgsl.d.ts.map +1 -0
  77. package/dist/src/wgsl/bfs-next-degree.wgsl.js +34 -0
  78. package/dist/src/wgsl/bfs-next-degree.wgsl.js.map +1 -0
  79. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -1
  80. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
  81. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +18 -2
  82. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
  83. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +1 -1
  84. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
  85. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +4 -2
  86. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
  87. package/dist/src/wgsl/frontier-finalize.wgsl.d.ts +44 -49
  88. package/dist/src/wgsl/frontier-finalize.wgsl.d.ts.map +1 -1
  89. package/dist/src/wgsl/frontier-finalize.wgsl.js +62 -107
  90. package/dist/src/wgsl/frontier-finalize.wgsl.js.map +1 -1
  91. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +3 -2
  92. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -1
  93. package/dist/src/wgsl/grid-cell-key.wgsl.js +4 -2
  94. package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -1
  95. package/dist/src/wgsl/grid-centroid.wgsl.d.ts +3 -2
  96. package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -1
  97. package/dist/src/wgsl/grid-centroid.wgsl.js +3 -2
  98. package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -1
  99. package/dist/src/wgsl/grid-downsample.wgsl.d.ts +1 -1
  100. package/dist/src/wgsl/grid-downsample.wgsl.js +1 -1
  101. package/dist/src/wgsl/grid-far-field.wgsl.d.ts +6 -4
  102. package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -1
  103. package/dist/src/wgsl/grid-far-field.wgsl.js +15 -8
  104. package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -1
  105. package/dist/src/wgsl/grid-near-field.wgsl.d.ts +2 -2
  106. package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -1
  107. package/dist/src/wgsl/grid-near-field.wgsl.js +6 -2
  108. package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -1
  109. package/dist/src/wgsl/histogram.wgsl.d.ts +1 -1
  110. package/dist/src/wgsl/histogram.wgsl.js +1 -1
  111. package/dist/webgpu-graph-algorithms.js +144 -119
  112. package/dist/webgpu-graph-algorithms.js.map +1 -1
  113. package/package.json +3 -3
  114. package/src/algorithms/bfs.ts +34 -10
  115. package/src/algorithms/pagerank.ts +19 -5
  116. package/src/algorithms/power-iteration.ts +8 -2
  117. package/src/algorithms/scope.ts +3 -10
  118. package/src/algorithms/sssp.ts +4 -3
  119. package/src/constants.ts +35 -2
  120. package/src/kernel/dispatch.ts +2 -2
  121. package/src/kernel/kernel.ts +2 -2
  122. package/src/kernel/prelude.ts +2 -0
  123. package/src/kernels.ts +44 -21
  124. package/src/layouts/forceatlas2.ts +2 -0
  125. package/src/layouts/fruchterman-reingold.ts +4 -1
  126. package/src/layouts/repulsion-exact.ts +29 -1
  127. package/src/layouts/repulsion-grid.ts +1 -1
  128. package/src/layouts/spring-electrical.ts +8 -1
  129. package/src/memory/residency.ts +14 -4
  130. package/src/primitives/advance.ts +5 -4
  131. package/src/primitives/frontier.ts +42 -56
  132. package/src/primitives/grid-pyramid.ts +6 -5
  133. package/src/primitives/grid.ts +17 -12
  134. package/src/wgsl/advance-expand.wgsl.ts +4 -3
  135. package/src/wgsl/bfs-bottom-up.wgsl.ts +4 -3
  136. package/src/wgsl/bfs-fused.wgsl.ts +6 -6
  137. package/src/wgsl/bfs-next-degree.wgsl.ts +33 -0
  138. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +18 -2
  139. package/src/wgsl/fa2-stats-finalize.wgsl.ts +4 -2
  140. package/src/wgsl/frontier-finalize.wgsl.ts +62 -107
  141. package/src/wgsl/grid-cell-key.wgsl.ts +4 -2
  142. package/src/wgsl/grid-centroid.wgsl.ts +3 -2
  143. package/src/wgsl/grid-downsample.wgsl.ts +1 -1
  144. package/src/wgsl/grid-far-field.wgsl.ts +15 -8
  145. package/src/wgsl/grid-near-field.wgsl.ts +6 -2
  146. package/src/wgsl/histogram.wgsl.ts +1 -1
  147. package/dist/chunks/context-Dvq-Cc6v.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * G5, the `grid-downsample` kernel body (spec 7.7; P4-T9): one dispatch per coarser level; every parent cell is the
3
3
  * sum of its 4 (2D) or 8 (3D) children at the level below, read at P.childBase and written at P.parentBase (the
4
- * pseudo-cell, index cells of level 0, is never a child). No atomics. Body only; normative text.
4
+ * pseudo-cells, indices cells .. of level 0, are never children). No atomics. Body only; normative text.
5
5
  */
6
6
  export const gridDownsampleWgsl = /* wgsl */ `
7
7
  @compute @workgroup_size(WG)
@@ -2,12 +2,14 @@
2
2
  * G6, the `grid-far-field` kernel body (spec 7.7; P4-T10; D24): per node `i = sortedIdx[t]`, its finest cell
3
3
  * recomputed from `pos[i]` and the state (PD-10); for an inside node the coarsest level minus the 3x3 (3x3x3)
4
4
  * around its coarsest cell, then at every finer level the 6x6 (6x6x6) block that is the parent's 3x3 minus this
5
- * level's own 3x3 -- space tiled exactly once, no theta -- plus the outside pseudo-cell's centroid; for an outside
6
- * node the coarsest level in full and no pseudo-cell. Every cell term is the per-cell law on the mass-weighted
7
- * centroid (Gephi Region semantics), softened by `eps^2`: `LAW` 0 (FA2) `d * (k m_i M / d2)`, `LAW` 1 (FR, 7.20)
5
+ * level's own 3x3 -- space tiled exactly once, no theta -- plus the centroid of each of the 2^dim outside
6
+ * pseudo-cells, one per orthant about the grid centre (issue #90); for an outside node the coarsest level in full and
7
+ * no pseudo-cell. Every cell term is the per-cell law on the mass-weighted
8
+ * centroid (Gephi Region semantics), softened by `eps^2`: `LAW` 0 (FA2) `d * (k m_i M / d2)` with `|d|^2` first
9
+ * floored at 0.01^2 like K3's pair law (issue #89), `LAW` 1 (FR, 7.20)
8
10
  * `d * (k^2 M / d2)` (mass 1 per node, so `M` is the cell's count), `LAW` 2 (coulomb) `d * (-g m_i M / d2^1.5)`
9
11
  * (P4-T13, PD-22). `force += f` (K2 wrote it). The loop bounds are `P.levels` and `P.gridMax` from the uniform,
10
12
  * not a `LEVELS` override (PD-16, DEP-P4-G). Body only; normative text.
11
13
  */
12
- export declare const gridFarFieldWgsl = "\nfn load_force(i: u32) -> vec3f { return vec3f(force[3u * i], force[3u * i + 1u], force[3u * i + 2u]); }\nfn store_force(i: u32, f: vec3f) {\n force[3u * i] = f.x;\n force[3u * i + 1u] = f.y;\n force[3u * i + 2u] = f.z;\n}\nfn grid_cells() -> u32 { return P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u); }\nfn grid_side(level: u32) -> u32 { return P.gridMax >> level; }\nfn level_base(level: u32) -> u32 { // the pyramid index of level L's cell 0 (level 0 carries the pseudo-cell at index cells)\n var base = 0u;\n for (var l = 0u; l < level; l = l + 1u) {\n let s = grid_side(l);\n base = base + s * s * select(1u, s, P.dim == 3u) + select(0u, 1u, l == 0u);\n }\n return base;\n}\nfn cell_at(level: u32, cx: i32, cy: i32, cz: i32) -> u32 {\n let s = grid_side(level);\n return level_base(level) + u32(cx) + s * (u32(cy) + select(0u, s * u32(cz), P.dim == 3u));\n}\nfn cell_force(pi: vec4f, q: vec4f) -> vec3f { // one far-field term, softened by state.eps (7.7)\n if (q.w <= 0.0) { return vec3f(0.0); } // an empty cell\n let d = pi.xyz - q.xyz / q.w; // to the mass-weighted centroid\n let d2 = dot(d, d) + S.eps * S.eps;\n if (LAW == 1u) { return d * (P.frK * P.frK * q.w / d2); } // LAW 1 (FR, 7.20): k^2 / d per node, q.w nodes at the centroid\n if (LAW == 2u) { return d * (-P.coulomb * pi.w * q.w / (d2 * sqrt(d2))); } // LAW 2 (coulomb): -g m_i M_cell / d^2\n return d * (P.scalingRatio * pi.w * q.w / d2); // LAW 0 (FA2): |F| = k m_i M_cell / d\n}\n\n@compute @workgroup_size(WG)\nfn grid_far_field(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {\n let t = linear_id(wid, lid.x);\n if (t >= P.n) { return; } // no barrier follows\n let i = sortedIdx[t]; // sorted order (D24)\n let pi = pos[i];\n let gf = f32(P.gridMax);\n let q = (pi.xyz - S.gridMin.xyz) * S.invCellSize; // PD-10\n var c0 = vec3<i32>(floor(clamp(q, vec3f(-1.0), vec3f(gf + 1.0))));\n if (P.dim == 2u) { c0.z = 0; } // 2D: one z plane; the loops below visit cz = 0 only, so the 3x3 test must see cz - 0\n let g = i32(P.gridMax);\n var inside = c0.x >= 0 && c0.x < g && c0.y >= 0 && c0.y < g;\n if (P.dim == 3u) { inside = inside && c0.z >= 0 && c0.z < g; }\n let top = P.levels - 1u;\n let ts = i32(grid_side(top)); // the coarsest side (4)\n let zTop = select(0, ts - 1, P.dim == 3u); // z ranges: one plane in 2D\n var f = vec3f(0.0);\n if (inside) {\n let ct = c0 / i32(1u << top); // the node's coarsest cell\n for (var cz = 0; cz <= zTop; cz = cz + 1) {\n for (var cy = 0; cy < ts; cy = cy + 1) {\n for (var cx = 0; cx < ts; cx = cx + 1) {\n if (abs(cx - ct.x) <= 1 && abs(cy - ct.y) <= 1 && abs(cz - ct.z) <= 1) { continue; } // the 3x3(x3) is finer levels' work\n f = f + cell_force(pi, pyramid[cell_at(top, cx, cy, cz)]);\n }\n }\n }\n for (var l = top; l > 0u; l = l - 1u) { // level l - 1: the parent's 3x3 at level l, refined, minus this level's own 3x3\n let level = l - 1u;\n let cl = c0 / i32(1u << level);\n let cp = cl / 2;\n let side = i32(grid_side(level));\n let zLo = select(0, max(0, 2 * (cp.z - 1)), P.dim == 3u);\n let zHi = select(0, min(side - 1, 2 * (cp.z + 1) + 1), P.dim == 3u);\n for (var cz = zLo; cz <= zHi; cz = cz + 1) {\n for (var cy = max(0, 2 * (cp.y - 1)); cy <= min(side - 1, 2 * (cp.y + 1) + 1); cy = cy + 1) {\n for (var cx = max(0, 2 * (cp.x - 1)); cx <= min(side - 1, 2 * (cp.x + 1) + 1); cx = cx + 1) {\n if (abs(cx - cl.x) <= 1 && abs(cy - cl.y) <= 1 && abs(cz - cl.z) <= 1) { continue; }\n f = f + cell_force(pi, pyramid[cell_at(level, cx, cy, cz)]);\n }\n }\n }\n }\n f = f + cell_force(pi, pyramid[grid_cells()]); // the outside pseudo-cell as one far-field term\n } else {\n for (var cz = 0; cz <= zTop; cz = cz + 1) { // an outside node: the coarsest level in full, no pseudo-cell (it would include itself)\n for (var cy = 0; cy < ts; cy = cy + 1) {\n for (var cx = 0; cx < ts; cx = cx + 1) {\n f = f + cell_force(pi, pyramid[cell_at(top, cx, cy, cz)]);\n }\n }\n }\n }\n store_force(i, load_force(i) + f);\n}\n";
14
+ export declare const gridFarFieldWgsl = "\nfn load_force(i: u32) -> vec3f { return vec3f(force[3u * i], force[3u * i + 1u], force[3u * i + 2u]); }\nfn store_force(i: u32, f: vec3f) {\n force[3u * i] = f.x;\n force[3u * i + 1u] = f.y;\n force[3u * i + 2u] = f.z;\n}\nfn grid_cells() -> u32 { return P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u); }\nfn grid_side(level: u32) -> u32 { return P.gridMax >> level; }\nfn outside_cells() -> u32 { return select(4u, 8u, P.dim == 3u); } // one pseudo-cell per orthant (issue #90)\nfn level_base(level: u32) -> u32 { // the pyramid index of level L's cell 0 (level 0 carries the pseudo-cells at cells ..)\n var base = 0u;\n for (var l = 0u; l < level; l = l + 1u) {\n let s = grid_side(l);\n base = base + s * s * select(1u, s, P.dim == 3u) + select(0u, outside_cells(), l == 0u);\n }\n return base;\n}\nfn cell_at(level: u32, cx: i32, cy: i32, cz: i32) -> u32 {\n let s = grid_side(level);\n return level_base(level) + u32(cx) + s * (u32(cy) + select(0u, s * u32(cz), P.dim == 3u));\n}\nfn cell_force(pi: vec4f, q: vec4f) -> vec3f { // one far-field term, softened by state.eps (7.7)\n if (q.w <= 0.0) { return vec3f(0.0); } // an empty cell\n let d = pi.xyz - q.xyz / q.w; // to the mass-weighted centroid\n var d2 = dot(d, d);\n if (LAW == 0u) { d2 = max(d2, FA2_DIST_FLOOR_SQ); } // FA2 alone floors d >= 0.01, as K3 and G7 do (issue #89); FR and coulomb are unfloored (7.20)\n d2 = d2 + S.eps * S.eps;\n if (LAW == 1u) { return d * (P.frK * P.frK * q.w / d2); } // LAW 1 (FR, 7.20): k^2 / d per node, q.w nodes at the centroid\n if (LAW == 2u) { return d * (-P.coulomb * pi.w * q.w / (d2 * sqrt(d2))); } // LAW 2 (coulomb): -g m_i M_cell / d^2\n return d * (P.scalingRatio * pi.w * q.w / d2); // LAW 0 (FA2): |F| = k m_i M_cell / d\n}\n\n@compute @workgroup_size(WG)\nfn grid_far_field(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {\n let t = linear_id(wid, lid.x);\n if (t >= P.n) { return; } // no barrier follows\n let i = sortedIdx[t]; // sorted order (D24)\n let pi = pos[i];\n let gf = f32(P.gridMax);\n let q = (pi.xyz - S.gridMin.xyz) * S.invCellSize; // PD-10\n var c0 = vec3<i32>(floor(clamp(q, vec3f(-1.0), vec3f(gf + 1.0))));\n if (P.dim == 2u) { c0.z = 0; } // 2D: one z plane; the loops below visit cz = 0 only, so the 3x3 test must see cz - 0\n let g = i32(P.gridMax);\n var inside = c0.x >= 0 && c0.x < g && c0.y >= 0 && c0.y < g;\n if (P.dim == 3u) { inside = inside && c0.z >= 0 && c0.z < g; }\n let top = P.levels - 1u;\n let ts = i32(grid_side(top)); // the coarsest side (4)\n let zTop = select(0, ts - 1, P.dim == 3u); // z ranges: one plane in 2D\n var f = vec3f(0.0);\n if (inside) {\n let ct = c0 / i32(1u << top); // the node's coarsest cell\n for (var cz = 0; cz <= zTop; cz = cz + 1) {\n for (var cy = 0; cy < ts; cy = cy + 1) {\n for (var cx = 0; cx < ts; cx = cx + 1) {\n if (abs(cx - ct.x) <= 1 && abs(cy - ct.y) <= 1 && abs(cz - ct.z) <= 1) { continue; } // the 3x3(x3) is finer levels' work\n f = f + cell_force(pi, pyramid[cell_at(top, cx, cy, cz)]);\n }\n }\n }\n for (var l = top; l > 0u; l = l - 1u) { // level l - 1: the parent's 3x3 at level l, refined, minus this level's own 3x3\n let level = l - 1u;\n let cl = c0 / i32(1u << level);\n let cp = cl / 2;\n let side = i32(grid_side(level));\n let zLo = select(0, max(0, 2 * (cp.z - 1)), P.dim == 3u);\n let zHi = select(0, min(side - 1, 2 * (cp.z + 1) + 1), P.dim == 3u);\n for (var cz = zLo; cz <= zHi; cz = cz + 1) {\n for (var cy = max(0, 2 * (cp.y - 1)); cy <= min(side - 1, 2 * (cp.y + 1) + 1); cy = cy + 1) {\n for (var cx = max(0, 2 * (cp.x - 1)); cx <= min(side - 1, 2 * (cp.x + 1) + 1); cx = cx + 1) {\n if (abs(cx - cl.x) <= 1 && abs(cy - cl.y) <= 1 && abs(cz - cl.z) <= 1) { continue; }\n f = f + cell_force(pi, pyramid[cell_at(level, cx, cy, cz)]);\n }\n }\n }\n }\n for (var o = 0u; o < outside_cells(); o = o + 1u) { // every outside pseudo-cell: one far-field term per orthant\n f = f + cell_force(pi, pyramid[grid_cells() + o]);\n }\n } else {\n for (var cz = 0; cz <= zTop; cz = cz + 1) { // an outside node: the coarsest level in full, no pseudo-cell (G7 sums them pair by pair)\n for (var cy = 0; cy < ts; cy = cy + 1) {\n for (var cx = 0; cx < ts; cx = cx + 1) {\n f = f + cell_force(pi, pyramid[cell_at(top, cx, cy, cz)]);\n }\n }\n }\n }\n store_force(i, load_force(i) + f);\n}\n";
13
15
  //# sourceMappingURL=grid-far-field.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grid-far-field.wgsl.d.ts","sourceRoot":"","sources":["../../../src/wgsl/grid-far-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,mzJAqF5B,CAAC"}
1
+ {"version":3,"file":"grid-far-field.wgsl.d.ts","sourceRoot":"","sources":["../../../src/wgsl/grid-far-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,qrKA0F5B,CAAC"}
@@ -2,9 +2,11 @@
2
2
  * G6, the `grid-far-field` kernel body (spec 7.7; P4-T10; D24): per node `i = sortedIdx[t]`, its finest cell
3
3
  * recomputed from `pos[i]` and the state (PD-10); for an inside node the coarsest level minus the 3x3 (3x3x3)
4
4
  * around its coarsest cell, then at every finer level the 6x6 (6x6x6) block that is the parent's 3x3 minus this
5
- * level's own 3x3 -- space tiled exactly once, no theta -- plus the outside pseudo-cell's centroid; for an outside
6
- * node the coarsest level in full and no pseudo-cell. Every cell term is the per-cell law on the mass-weighted
7
- * centroid (Gephi Region semantics), softened by `eps^2`: `LAW` 0 (FA2) `d * (k m_i M / d2)`, `LAW` 1 (FR, 7.20)
5
+ * level's own 3x3 -- space tiled exactly once, no theta -- plus the centroid of each of the 2^dim outside
6
+ * pseudo-cells, one per orthant about the grid centre (issue #90); for an outside node the coarsest level in full and
7
+ * no pseudo-cell. Every cell term is the per-cell law on the mass-weighted
8
+ * centroid (Gephi Region semantics), softened by `eps^2`: `LAW` 0 (FA2) `d * (k m_i M / d2)` with `|d|^2` first
9
+ * floored at 0.01^2 like K3's pair law (issue #89), `LAW` 1 (FR, 7.20)
8
10
  * `d * (k^2 M / d2)` (mass 1 per node, so `M` is the cell's count), `LAW` 2 (coulomb) `d * (-g m_i M / d2^1.5)`
9
11
  * (P4-T13, PD-22). `force += f` (K2 wrote it). The loop bounds are `P.levels` and `P.gridMax` from the uniform,
10
12
  * not a `LEVELS` override (PD-16, DEP-P4-G). Body only; normative text.
@@ -18,11 +20,12 @@ fn store_force(i: u32, f: vec3f) {
18
20
  }
19
21
  fn grid_cells() -> u32 { return P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u); }
20
22
  fn grid_side(level: u32) -> u32 { return P.gridMax >> level; }
21
- fn level_base(level: u32) -> u32 { // the pyramid index of level L's cell 0 (level 0 carries the pseudo-cell at index cells)
23
+ fn outside_cells() -> u32 { return select(4u, 8u, P.dim == 3u); } // one pseudo-cell per orthant (issue #90)
24
+ fn level_base(level: u32) -> u32 { // the pyramid index of level L's cell 0 (level 0 carries the pseudo-cells at cells ..)
22
25
  var base = 0u;
23
26
  for (var l = 0u; l < level; l = l + 1u) {
24
27
  let s = grid_side(l);
25
- base = base + s * s * select(1u, s, P.dim == 3u) + select(0u, 1u, l == 0u);
28
+ base = base + s * s * select(1u, s, P.dim == 3u) + select(0u, outside_cells(), l == 0u);
26
29
  }
27
30
  return base;
28
31
  }
@@ -33,7 +36,9 @@ fn cell_at(level: u32, cx: i32, cy: i32, cz: i32) -> u32 {
33
36
  fn cell_force(pi: vec4f, q: vec4f) -> vec3f { // one far-field term, softened by state.eps (7.7)
34
37
  if (q.w <= 0.0) { return vec3f(0.0); } // an empty cell
35
38
  let d = pi.xyz - q.xyz / q.w; // to the mass-weighted centroid
36
- let d2 = dot(d, d) + S.eps * S.eps;
39
+ var d2 = dot(d, d);
40
+ if (LAW == 0u) { d2 = max(d2, FA2_DIST_FLOOR_SQ); } // FA2 alone floors d >= 0.01, as K3 and G7 do (issue #89); FR and coulomb are unfloored (7.20)
41
+ d2 = d2 + S.eps * S.eps;
37
42
  if (LAW == 1u) { return d * (P.frK * P.frK * q.w / d2); } // LAW 1 (FR, 7.20): k^2 / d per node, q.w nodes at the centroid
38
43
  if (LAW == 2u) { return d * (-P.coulomb * pi.w * q.w / (d2 * sqrt(d2))); } // LAW 2 (coulomb): -g m_i M_cell / d^2
39
44
  return d * (P.scalingRatio * pi.w * q.w / d2); // LAW 0 (FA2): |F| = k m_i M_cell / d
@@ -82,9 +87,11 @@ fn grid_far_field(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocati
82
87
  }
83
88
  }
84
89
  }
85
- f = f + cell_force(pi, pyramid[grid_cells()]); // the outside pseudo-cell as one far-field term
90
+ for (var o = 0u; o < outside_cells(); o = o + 1u) { // every outside pseudo-cell: one far-field term per orthant
91
+ f = f + cell_force(pi, pyramid[grid_cells() + o]);
92
+ }
86
93
  } else {
87
- for (var cz = 0; cz <= zTop; cz = cz + 1) { // an outside node: the coarsest level in full, no pseudo-cell (it would include itself)
94
+ for (var cz = 0; cz <= zTop; cz = cz + 1) { // an outside node: the coarsest level in full, no pseudo-cell (G7 sums them pair by pair)
88
95
  for (var cy = 0; cy < ts; cy = cy + 1) {
89
96
  for (var cx = 0; cx < ts; cx = cx + 1) {
90
97
  f = f + cell_force(pi, pyramid[cell_at(top, cx, cy, cz)]);
@@ -1 +1 @@
1
- {"version":3,"file":"grid-far-field.wgsl.js","sourceRoot":"","sources":["../../../src/wgsl/grid-far-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqF1C,CAAC"}
1
+ {"version":3,"file":"grid-far-field.wgsl.js","sourceRoot":"","sources":["../../../src/wgsl/grid-far-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0F1C,CAAC"}
@@ -3,7 +3,7 @@
3
3
  * exact pair law of K3 (`LAW` 0: `|F| = k m_i m_j / d` with the 0.01 floor; `LAW` 1: FR's unfloored `k^2 / d`;
4
4
  * `LAW` 2: the unfloored coulomb `-g m_i m_j / d^2`; the antisymmetric coincident kick at the law's magnitude at
5
5
  * d = 0.01, PD-22) over the 9 (27) finest
6
- * cells around its own, or over the outside pseudo-cell alone for an outside node; a cell above `nearMax` entries
6
+ * cells around its own, or over the 2^dim outside pseudo-cells (one per orthant, issue #90) for an outside node; a cell above `nearMax` entries
7
7
  * is sampled by `nearMax` INDEPENDENT draws with replacement, draw `k` reading the slot
8
8
  * `lowbias32(((c ^ (iteration * 0x9E3779B9)) ^ seed) ^ (k * 0x85EBCA6B)) % count` (every slot's inclusion
9
9
  * probability is `nearMax / count` whatever its position in the sorted order, so a duplicated draw is counted twice
@@ -15,5 +15,5 @@
15
15
  * The helpers `load_force`, `store_force`, `load_old`, `kick_magnitude`, `gravity_force` and the epilogue are K3's
16
16
  * text. Body only; normative text.
17
17
  */
18
- export declare const gridNearFieldWgsl = "\nfn load_force(i: u32) -> vec3f { return vec3f(force[3u * i], force[3u * i + 1u], force[3u * i + 2u]); }\nfn store_force(i: u32, f: vec3f) {\n force[3u * i] = f.x;\n force[3u * i + 1u] = f.y;\n force[3u * i + 2u] = f.z;\n}\nfn load_old(i: u32) -> vec3f { return vec3f(oldForce[3u * i], oldForce[3u * i + 1u], oldForce[3u * i + 2u]); }\nfn gravity_force(pi: vec4f) -> vec3f { // spec 7.9: centroid (GRAVITY_CENTER 0) or origin (1); regular or strong\n var q = pi.xyz;\n if (GRAVITY_CENTER == 0u) { q = pi.xyz - S.centroid.xyz; }\n if (STRONG_GRAVITY) { return -P.gravity * pi.w * q; }\n let d = length(q);\n if (d > FA2_DIST_FLOOR) { return -P.gravity * pi.w * q / d; }\n return vec3f(0.0);\n}\nfn grid_cells() -> u32 { return P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u); }\nfn kick_magnitude(mi: f32, mj: f32) -> f32 { // the law's magnitude at d = FA2_DIST_FLOOR (the P5 plan's PD-10)\n if (LAW == 1u) { return P.frK * P.frK / FA2_DIST_FLOOR; }\n if (LAW == 2u) { return -P.coulomb * mi * mj / FA2_DIST_FLOOR_SQ; }\n return P.scalingRatio * mi * mj / FA2_DIST_FLOOR;\n}\nfn pair_force(i: u32, pi: vec4f, jj: u32, o: vec4f) -> vec3f { // the exact pair law of K3 (7.6, 7.20): the floor (FA2 only), the coincident kick\n let d = pi.xyz - o.xyz;\n var d2 = dot(d, d);\n if (d2 < FA2_COINCIDENT_SQ) { return kick_dir(i, jj, P.dim) * kick_magnitude(pi.w, o.w); }\n if (LAW == 0u) { d2 = max(d2, FA2_DIST_FLOOR_SQ); }\n let k = P.scalingRatio * pi.w * o.w;\n if (LAW == 1u) { return d * (P.frK * P.frK / d2); }\n if (LAW == 2u) { return d * (-P.coulomb * pi.w * o.w / (d2 * sqrt(d2))); }\n return d * (k / d2);\n}\nfn cell_sum(i: u32, pi: vec4f, c: u32, own: bool) -> vec3f { // one finest cell: exact below nearMax entries, Horvitz-Thompson above (PD-15)\n let start = cellStart[c];\n let count = cellStart[c + 1u] - start;\n var f = vec3f(0.0);\n if (count <= P.nearMax) {\n for (var k = start; k < start + count; k = k + 1u) {\n let jj = sortedIdx[k];\n if (jj != i) { f = f + pair_force(i, pi, jj, pos[jj]); }\n }\n return f;\n }\n let base = (c ^ (P.iterationIndex * 0x9E3779B9u)) ^ P.seed; // the per-iteration draw seed (7.16)\n var sampled = 0u;\n for (var k = 0u; k < P.nearMax; k = k + 1u) {\n let jj = sortedIdx[start + (lowbias32(base ^ (k * 0x85EBCA6Bu)) % count)]; // draw k: independent inclusion, with replacement (PD-15)\n if (jj == i) { continue; }\n f = f + pair_force(i, pi, jj, pos[jj]);\n sampled = sampled + 1u;\n }\n if (sampled == 0u) { return vec3f(0.0); }\n let others = select(count, count - 1u, own);\n return f * (f32(others) / f32(sampled)); // others / sampled over the realised sample (DEP-P4-K)\n}\n\n@compute @workgroup_size(WG)\nfn grid_near_field(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {\n let t = linear_id(wid, lid.x);\n let valid = t < P.n;\n var i = 0u;\n var pi = vec4f(0.0);\n var f = vec3f(0.0);\n if (valid) {\n i = sortedIdx[t]; // sorted order (D24)\n pi = pos[i];\n let gf = f32(P.gridMax);\n let q = (pi.xyz - S.gridMin.xyz) * S.invCellSize;\n var c0 = vec3<i32>(floor(clamp(q, vec3f(-1.0), vec3f(gf + 1.0))));\n if (P.dim == 2u) { c0.z = 0; } // 2D: the one z plane\n let g = i32(P.gridMax);\n var inside = c0.x >= 0 && c0.x < g && c0.y >= 0 && c0.y < g;\n if (P.dim == 3u) { inside = inside && c0.z >= 0 && c0.z < g; }\n if (inside) {\n let zr = select(0, 1, P.dim == 3u);\n for (var dz = -zr; dz <= zr; dz = dz + 1) {\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let cx = c0.x + dx;\n let cy = c0.y + dy;\n let cz = c0.z + dz;\n if (cx < 0 || cx >= g || cy < 0 || cy >= g || cz < 0 || cz >= g) { continue; }\n let c = u32(cx) + P.gridMax * (u32(cy) + select(0u, P.gridMax * u32(cz), P.dim == 3u));\n f = f + cell_sum(i, pi, c, dx == 0 && dy == 0 && dz == 0);\n }\n }\n }\n } else {\n f = cell_sum(i, pi, grid_cells(), true); // an outside node: the pseudo-cell alone\n }\n }\n // epilogue (7.9, 7.10): gravity and force += under the guard, the swing / traction reduction outside it (K3's text)\n var sw = 0.0;\n var tr = 0.0;\n if (valid) {\n f = f + gravity_force(pi);\n let fnew = load_force(i) + f;\n store_force(i, fnew);\n if (SWING_MODE == 1u) { // NetworkX: positions and forces mixed, every node (7.2)\n sw = pi.w * length(pi.xyz - fnew);\n tr = 0.5 * pi.w * length(pi.xyz + fnew);\n } else if (!mask_bit(fixedMask[i >> 5u], i)) { // paper: free nodes only\n let fold = load_old(i);\n sw = pi.w * length(fnew - fold);\n tr = 0.5 * pi.w * length(fnew + fold);\n }\n }\n let tt = wg_reduce_vec4(vec4f(sw, tr, 0.0, 0.0), lid.x, 0u); // uniform control flow: 256 -> 1\n if (lid.x == 0u) { partials[group_id(wid)].swingTraction = tt.xy; }\n}\n";
18
+ export declare const gridNearFieldWgsl = "\nfn load_force(i: u32) -> vec3f { return vec3f(force[3u * i], force[3u * i + 1u], force[3u * i + 2u]); }\nfn store_force(i: u32, f: vec3f) {\n force[3u * i] = f.x;\n force[3u * i + 1u] = f.y;\n force[3u * i + 2u] = f.z;\n}\nfn load_old(i: u32) -> vec3f { return vec3f(oldForce[3u * i], oldForce[3u * i + 1u], oldForce[3u * i + 2u]); }\nfn gravity_force(pi: vec4f) -> vec3f { // spec 7.9: centroid (GRAVITY_CENTER 0) or origin (1); regular or strong\n var q = pi.xyz;\n if (GRAVITY_CENTER == 0u) { q = pi.xyz - S.centroid.xyz; }\n if (STRONG_GRAVITY) { return -P.gravity * pi.w * q; }\n let d = length(q);\n if (d > FA2_DIST_FLOOR) { return -P.gravity * pi.w * q / d; }\n return vec3f(0.0);\n}\nfn grid_cells() -> u32 { return P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u); }\nfn kick_magnitude(mi: f32, mj: f32) -> f32 { // the law's magnitude at d = FA2_DIST_FLOOR (the P5 plan's PD-10)\n if (LAW == 1u) { return P.frK * P.frK / FA2_DIST_FLOOR; }\n if (LAW == 2u) { return -P.coulomb * mi * mj / FA2_DIST_FLOOR_SQ; }\n return P.scalingRatio * mi * mj / FA2_DIST_FLOOR;\n}\nfn pair_force(i: u32, pi: vec4f, jj: u32, o: vec4f) -> vec3f { // the exact pair law of K3 (7.6, 7.20): the floor (FA2 only), the coincident kick\n let d = pi.xyz - o.xyz;\n var d2 = dot(d, d);\n if (d2 < FA2_COINCIDENT_SQ) { return kick_dir(i, jj, P.dim) * kick_magnitude(pi.w, o.w); }\n if (LAW == 0u) { d2 = max(d2, FA2_DIST_FLOOR_SQ); }\n let k = P.scalingRatio * pi.w * o.w;\n if (LAW == 1u) { return d * (P.frK * P.frK / d2); }\n if (LAW == 2u) { return d * (-P.coulomb * pi.w * o.w / (d2 * sqrt(d2))); }\n return d * (k / d2);\n}\nfn cell_sum(i: u32, pi: vec4f, c: u32, own: bool) -> vec3f { // one finest cell: exact below nearMax entries, Horvitz-Thompson above (PD-15)\n let start = cellStart[c];\n let count = cellStart[c + 1u] - start;\n var f = vec3f(0.0);\n if (count <= P.nearMax) {\n for (var k = start; k < start + count; k = k + 1u) {\n let jj = sortedIdx[k];\n if (jj != i) { f = f + pair_force(i, pi, jj, pos[jj]); }\n }\n return f;\n }\n let base = (c ^ (P.iterationIndex * 0x9E3779B9u)) ^ P.seed; // the per-iteration draw seed (7.16)\n var sampled = 0u;\n for (var k = 0u; k < P.nearMax; k = k + 1u) {\n let jj = sortedIdx[start + (lowbias32(base ^ (k * 0x85EBCA6Bu)) % count)]; // draw k: independent inclusion, with replacement (PD-15)\n if (jj == i) { continue; }\n f = f + pair_force(i, pi, jj, pos[jj]);\n sampled = sampled + 1u;\n }\n if (sampled == 0u) { return vec3f(0.0); }\n let others = select(count, count - 1u, own);\n return f * (f32(others) / f32(sampled)); // others / sampled over the realised sample (DEP-P4-K)\n}\n\n@compute @workgroup_size(WG)\nfn grid_near_field(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {\n let t = linear_id(wid, lid.x);\n let valid = t < P.n;\n var i = 0u;\n var pi = vec4f(0.0);\n var f = vec3f(0.0);\n if (valid) {\n i = sortedIdx[t]; // sorted order (D24)\n pi = pos[i];\n let gf = f32(P.gridMax);\n let q = (pi.xyz - S.gridMin.xyz) * S.invCellSize;\n var c0 = vec3<i32>(floor(clamp(q, vec3f(-1.0), vec3f(gf + 1.0))));\n if (P.dim == 2u) { c0.z = 0; } // 2D: the one z plane\n let g = i32(P.gridMax);\n var inside = c0.x >= 0 && c0.x < g && c0.y >= 0 && c0.y < g;\n if (P.dim == 3u) { inside = inside && c0.z >= 0 && c0.z < g; }\n if (inside) {\n let zr = select(0, 1, P.dim == 3u);\n for (var dz = -zr; dz <= zr; dz = dz + 1) {\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let cx = c0.x + dx;\n let cy = c0.y + dy;\n let cz = c0.z + dz;\n if (cx < 0 || cx >= g || cy < 0 || cy >= g || cz < 0 || cz >= g) { continue; }\n let c = u32(cx) + P.gridMax * (u32(cy) + select(0u, P.gridMax * u32(cz), P.dim == 3u));\n f = f + cell_sum(i, pi, c, dx == 0 && dy == 0 && dz == 0);\n }\n }\n }\n } else {\n var own = grid_cells() + select(0u, 1u, c0.x >= g / 2) + select(0u, 2u, c0.y >= g / 2); // G1's orthant key\n if (P.dim == 3u) { own = own + select(0u, 4u, c0.z >= g / 2); }\n for (var o = grid_cells(); o < grid_cells() + select(4u, 8u, P.dim == 3u); o = o + 1u) { // an outside node: every outside pseudo-cell\n f = f + cell_sum(i, pi, o, o == own);\n }\n }\n }\n // epilogue (7.9, 7.10): gravity and force += under the guard, the swing / traction reduction outside it (K3's text)\n var sw = 0.0;\n var tr = 0.0;\n if (valid) {\n f = f + gravity_force(pi);\n let fnew = load_force(i) + f;\n store_force(i, fnew);\n if (SWING_MODE == 1u) { // NetworkX: positions and forces mixed, every node (7.2)\n sw = pi.w * length(pi.xyz - fnew);\n tr = 0.5 * pi.w * length(pi.xyz + fnew);\n } else if (!mask_bit(fixedMask[i >> 5u], i)) { // paper: free nodes only\n let fold = load_old(i);\n sw = pi.w * length(fnew - fold);\n tr = 0.5 * pi.w * length(fnew + fold);\n }\n }\n let tt = wg_reduce_vec4(vec4f(sw, tr, 0.0, 0.0), lid.x, 0u); // uniform control flow: 256 -> 1\n if (lid.x == 0u) { partials[group_id(wid)].swingTraction = tt.xy; }\n}\n";
19
19
  //# sourceMappingURL=grid-near-field.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grid-near-field.wgsl.d.ts","sourceRoot":"","sources":["../../../src/wgsl/grid-near-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,27KA8G7B,CAAC"}
1
+ {"version":3,"file":"grid-near-field.wgsl.d.ts","sourceRoot":"","sources":["../../../src/wgsl/grid-near-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,ivLAkH7B,CAAC"}
@@ -3,7 +3,7 @@
3
3
  * exact pair law of K3 (`LAW` 0: `|F| = k m_i m_j / d` with the 0.01 floor; `LAW` 1: FR's unfloored `k^2 / d`;
4
4
  * `LAW` 2: the unfloored coulomb `-g m_i m_j / d^2`; the antisymmetric coincident kick at the law's magnitude at
5
5
  * d = 0.01, PD-22) over the 9 (27) finest
6
- * cells around its own, or over the outside pseudo-cell alone for an outside node; a cell above `nearMax` entries
6
+ * cells around its own, or over the 2^dim outside pseudo-cells (one per orthant, issue #90) for an outside node; a cell above `nearMax` entries
7
7
  * is sampled by `nearMax` INDEPENDENT draws with replacement, draw `k` reading the slot
8
8
  * `lowbias32(((c ^ (iteration * 0x9E3779B9)) ^ seed) ^ (k * 0x85EBCA6B)) % count` (every slot's inclusion
9
9
  * probability is `nearMax / count` whatever its position in the sorted order, so a duplicated draw is counted twice
@@ -103,7 +103,11 @@ fn grid_near_field(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocat
103
103
  }
104
104
  }
105
105
  } else {
106
- f = cell_sum(i, pi, grid_cells(), true); // an outside node: the pseudo-cell alone
106
+ var own = grid_cells() + select(0u, 1u, c0.x >= g / 2) + select(0u, 2u, c0.y >= g / 2); // G1's orthant key
107
+ if (P.dim == 3u) { own = own + select(0u, 4u, c0.z >= g / 2); }
108
+ for (var o = grid_cells(); o < grid_cells() + select(4u, 8u, P.dim == 3u); o = o + 1u) { // an outside node: every outside pseudo-cell
109
+ f = f + cell_sum(i, pi, o, o == own);
110
+ }
107
111
  }
108
112
  }
109
113
  // epilogue (7.9, 7.10): gravity and force += under the guard, the swing / traction reduction outside it (K3's text)
@@ -1 +1 @@
1
- {"version":3,"file":"grid-near-field.wgsl.js","sourceRoot":"","sources":["../../../src/wgsl/grid-near-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8G3C,CAAC"}
1
+ {"version":3,"file":"grid-near-field.wgsl.js","sourceRoot":"","sources":["../../../src/wgsl/grid-near-field.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkH3C,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The `histogram` kernel body (spec 6 row 5; P4-T3): one atomicAdd per key into the global `hist` (zeroed by a fill
3
3
  * dispatch earlier in the pass, PD-4). Order-independent, hence deterministic. A key >= P.bins is not counted (the
4
- * caller's contract; the grid's keys are always < cells + 1). Body only; normative text.
4
+ * caller's contract; the grid's keys are always < cells + 2^dim). Body only; normative text.
5
5
  */
6
6
  export declare const histogramWgsl = "\n@compute @workgroup_size(WG)\nfn histogram(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {\n let i = linear_id(wid, lid.x);\n if (i >= P.count) { return; } // no barrier follows\n let k = keys[i];\n if (k < P.bins) { atomicAdd(&hist[k], 1u); } // order-independent: the count is the same whatever the schedule\n}\n";
7
7
  //# sourceMappingURL=histogram.wgsl.d.ts.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The `histogram` kernel body (spec 6 row 5; P4-T3): one atomicAdd per key into the global `hist` (zeroed by a fill
3
3
  * dispatch earlier in the pass, PD-4). Order-independent, hence deterministic. A key >= P.bins is not counted (the
4
- * caller's contract; the grid's keys are always < cells + 1). Body only; normative text.
4
+ * caller's contract; the grid's keys are always < cells + 2^dim). Body only; normative text.
5
5
  */
6
6
  export const histogramWgsl = /* wgsl */ `
7
7
  @compute @workgroup_size(WG)