@graphty/webgpu-graph-algorithms 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +27 -21
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-CRbw2Wyo.js → context-BR7fx3vR.js} +47 -5
  4. package/dist/chunks/context-BR7fx3vR.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +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/constants.d.ts +47 -0
  11. package/dist/src/constants.d.ts.map +1 -1
  12. package/dist/src/constants.js +47 -0
  13. package/dist/src/constants.js.map +1 -1
  14. package/dist/src/index.d.ts +7 -4
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +9 -5
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/kernel/prelude.d.ts.map +1 -1
  19. package/dist/src/kernel/prelude.js +4 -1
  20. package/dist/src/kernel/prelude.js.map +1 -1
  21. package/dist/src/kernels.d.ts +3 -3
  22. package/dist/src/kernels.d.ts.map +1 -1
  23. package/dist/src/kernels.js +27 -11
  24. package/dist/src/kernels.js.map +1 -1
  25. package/dist/src/layouts/force-simulation.d.ts +3 -1
  26. package/dist/src/layouts/force-simulation.d.ts.map +1 -1
  27. package/dist/src/layouts/force-simulation.js +13 -3
  28. package/dist/src/layouts/force-simulation.js.map +1 -1
  29. package/dist/src/layouts/forceatlas2.d.ts +1 -3
  30. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  31. package/dist/src/layouts/forceatlas2.js +2 -191
  32. package/dist/src/layouts/forceatlas2.js.map +1 -1
  33. package/dist/src/layouts/fruchterman-reingold.d.ts +195 -0
  34. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
  35. package/dist/src/layouts/fruchterman-reingold.js +579 -0
  36. package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
  37. package/dist/src/layouts/model-common.d.ts +102 -0
  38. package/dist/src/layouts/model-common.d.ts.map +1 -0
  39. package/dist/src/layouts/model-common.js +198 -0
  40. package/dist/src/layouts/model-common.js.map +1 -0
  41. package/dist/src/layouts/spring-electrical.d.ts +179 -0
  42. package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
  43. package/dist/src/layouts/spring-electrical.js +508 -0
  44. package/dist/src/layouts/spring-electrical.js.map +1 -0
  45. package/dist/src/types/accelerator.d.ts +7 -3
  46. package/dist/src/types/accelerator.d.ts.map +1 -1
  47. package/dist/src/types/layout.d.ts +32 -0
  48. package/dist/src/types/layout.d.ts.map +1 -1
  49. package/dist/src/types/options.d.ts +43 -1
  50. package/dist/src/types/options.d.ts.map +1 -1
  51. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +5 -2
  52. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
  53. package/dist/src/wgsl/fa2-attraction.wgsl.js +6 -1
  54. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
  55. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
  56. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
  57. package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
  58. package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
  59. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
  60. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
  61. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
  62. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
  63. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +8 -1
  64. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
  65. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +33 -0
  66. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
  67. package/dist/tsconfig.build.tsbuildinfo +1 -1
  68. package/dist/webgpu-graph-algorithms.js +1358 -284
  69. package/dist/webgpu-graph-algorithms.js.map +1 -1
  70. package/package.json +6 -4
  71. package/src/accelerator.ts +46 -12
  72. package/src/constants.ts +70 -0
  73. package/src/index.ts +15 -5
  74. package/src/kernel/prelude.ts +6 -0
  75. package/src/kernels.ts +27 -11
  76. package/src/layouts/force-simulation.ts +20 -3
  77. package/src/layouts/forceatlas2.ts +17 -220
  78. package/src/layouts/fruchterman-reingold.ts +737 -0
  79. package/src/layouts/model-common.ts +228 -0
  80. package/src/layouts/spring-electrical.ts +666 -0
  81. package/src/types/accelerator.ts +15 -3
  82. package/src/types/layout.ts +36 -0
  83. package/src/types/options.ts +45 -1
  84. package/src/wgsl/fa2-attraction.wgsl.ts +6 -1
  85. package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
  86. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
  87. package/src/wgsl/fa2-stats-finalize.wgsl.ts +33 -0
  88. package/dist/chunks/context-CRbw2Wyo.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { W as WebGpuGraphError, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, B as BufferUsage, i as isWebGpuGraphError, d as deviceLostError, U as UNIFORM_SLOT_BYTES, b as U32_MAX, F as FA2_DEFAULTS, c as MAX_ITERATIONS_PER_STEP, e as MAX_1D_ITEMS, h as hasErrorCode, f as FA2_FLAG_FIRST, P as PARTIAL_BYTES, L as LAYOUT_TUNING_DEFAULTS, T as TRACE_RECORD_BYTES } from "./chunks/context-CRbw2Wyo.js";
2
- import { A, E, G, g, j, k, l } from "./chunks/context-CRbw2Wyo.js";
1
+ import { W as WebGpuGraphError, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, B as BufferUsage, i as isWebGpuGraphError, d as deviceLostError, U as UNIFORM_SLOT_BYTES, b as U32_MAX, F as FA2_DEFAULTS, c as MAX_ITERATIONS_PER_STEP, e as MAX_1D_ITEMS, h as hasErrorCode, f as FA2_FLAG_FIRST, P as PARTIAL_BYTES, L as LAYOUT_TUNING_DEFAULTS, T as TRACE_RECORD_BYTES, g as FR_ADAPTIVE_MAX_ITERATIONS, j as FR_START_TEMPERATURE, k as FA2_FLAG_ADAPTIVE, l as FR_REHEAT_FRACTION, m as FR_DEFAULTS, n as SE_DEFAULTS, o as SE_SCALE_REFERENCE_NODES } from "./chunks/context-BR7fx3vR.js";
2
+ import { A, E, G, p, q, r, s } from "./chunks/context-BR7fx3vR.js";
3
3
  import { renumberPartition, makeMask, maskTest, expandEdges } from "@graphty/graph-format";
4
4
  function perDimension(caps) {
5
5
  return Math.min(MAX_WORKGROUPS_PER_DIM, caps.limits.maxComputeWorkgroupsPerDimension);
@@ -436,6 +436,8 @@ fn attraction(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_i
436
436
  if (HAS_WEIGHTS) { w = weights[a]; }
437
437
  let d = pos[j].xyz - pi.xyz; // toward j
438
438
  let len = max(length(d), FA2_DIST_FLOOR);
439
+ if (LAW == 1u) { w = length(d) / P.frK; } // LAW 1 (FR, 7.20): |F| = d^2 / k along d / d, unfloored; the linear select below applies w as is
440
+ if (LAW == 2u) { w = P.springCoefficient * (len - P.springLength) / len; } // LAW 2 (spring, ngraph generateCreateSpringForce.js:33-36): Hooke k_s (d - L) toward j
439
441
  let mag = select(w, w * log(1.0 + len) / len, LINLOG); // linear: |F| = w len; linlog: |F| = w log(1 + len)
440
442
  f = f + d * mag;
441
443
  }
@@ -460,6 +462,7 @@ fn integrate(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id
460
462
  var p = vec4f(0.0);
461
463
  var free = false;
462
464
  var valid = false;
465
+ var ke = 0.0;
463
466
  if (i < P.n) {
464
467
  valid = true;
465
468
  let f = load_force(i);
@@ -468,7 +471,25 @@ fn integrate(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id
468
471
  if (SWING_MODE == 0u) { swing_i = p.w * length(f - load_old(i)); } // paper: m |F(t) - F(t-1)|, recomputed inline (7.2)
469
472
  let factor = S.speed / (1.0 + sqrt(S.speed * swing_i));
470
473
  let fixed = mask_bit(fixedMask[i >> 5u], i);
471
- dp = select(f * factor, vec3f(0.0), fixed); // no clamp on dp (D25)
474
+ dp = select(f * factor, vec3f(0.0), fixed); // APPLY 0 (FA2): no clamp on dp (D25)
475
+ if (APPLY == 1u) { // APPLY 1 (FR, 7.20): move along F by min(|F|, t); a fixed node stays
476
+ let mag = length(f);
477
+ let t = select(P.temperature, S.temperature, (P.flags & FA2_FLAG_ADAPTIVE) != 0u); // adaptive cooling: K1's state temperature
478
+ dp = vec3f(0.0);
479
+ if (mag > 0.0 && !fixed) { dp = f * (min(mag, t) / mag); }
480
+ ke = select(0.0, dot(f, f), !fixed); // the force energy of Hu's step control, folded like the spring preset's kinetic energy
481
+ }
482
+ if (APPLY == 2u) { // APPLY 2 (spring-electrical): ngraph's Euler step over the velocity in the oldForce slot (PD-2)
483
+ var v = load_old(i);
484
+ let fd = f - P.dragCoefficient * v; // drag (generateCreateDragForce.js:18)
485
+ v = v + (P.timeStep / p.w) * fd; // v += (dt / m) F (generateIntegrator.js:27-29)
486
+ let sp = length(v);
487
+ if (sp > 1.0) { v = v / sp; } // the unit speed clamp (generateIntegrator.js:33-37)
488
+ if (P.dim == 2u) { v.z = 0.0; }
489
+ dp = select(P.timeStep * v, vec3f(0.0), fixed); // dp = dt v; a pinned body is skipped (generateIntegrator.js:21, 39-41)
490
+ if (!fixed) { store_old(i, v); }
491
+ ke = select(0.0, 0.5 * p.w * dot(v, v), !fixed); // partials B under APPLY 2 (PD-4)
492
+ }
472
493
  if (P.dim == 2u) { dp.z = 0.0; } // 2D never integrates z (7.13)
473
494
  p = vec4f(p.xyz + dp, p.w);
474
495
  pos[i] = p;
@@ -494,12 +515,14 @@ fn integrate(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id
494
515
  let tHi = wg_reduce_vec4(hi, lid.x, 2u);
495
516
  let tDl = wg_reduce_f32(dl, lid.x, 0u);
496
517
  let tFr = wg_reduce_u32(fr, lid.x, 0u);
518
+ let tKe = wg_reduce_f32(ke, lid.x, 0u);
497
519
  if (lid.x == 0u) {
498
520
  let g = group_id(wid);
499
521
  partials[g].sum = tSum;
500
522
  partials[g].min = tLo;
501
523
  partials[g].max = tHi;
502
524
  partials[g].dispFree = vec2f(tDl, f32(tFr));
525
+ if (APPLY != 0u) { partials[g].swingTraction = vec2f(tKe, 0.0); } // overwrites K3's epilogue: K4 never runs under FR or the preset (PD-4)
503
526
  }
504
527
  }`
505
528
  );
@@ -515,6 +538,11 @@ fn store_force(i: u32, f: vec3f) {
515
538
  force[3u * i + 2u] = f.z;
516
539
  }
517
540
  fn load_old(i: u32) -> vec3f { return vec3f(oldForce[3u * i], oldForce[3u * i + 1u], oldForce[3u * i + 2u]); }
541
+ fn kick_magnitude(mi: f32, mj: f32) -> f32 { // the law's magnitude at d = FA2_DIST_FLOOR (PD-10)
542
+ if (LAW == 1u) { return P.frK * P.frK / FA2_DIST_FLOOR; }
543
+ if (LAW == 2u) { return -P.coulomb * mi * mj / FA2_DIST_FLOOR_SQ; }
544
+ return P.scalingRatio * mi * mj / FA2_DIST_FLOOR;
545
+ }
518
546
  fn gravity_force(pi: vec4f) -> vec3f { // spec 7.9: centroid (GRAVITY_CENTER 0) or origin (1); regular or strong
519
547
  var q = pi.xyz;
520
548
  if (GRAVITY_CENTER == 0u) { q = pi.xyz - S.centroid.xyz; }
@@ -542,13 +570,15 @@ fn repulsion(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id
542
570
  if (o.w > 0.0 && jj != i) { // mass > 0 for every real node, 0 for the pad
543
571
  let d = pi.xyz - o.xyz;
544
572
  var d2 = dot(d, d);
545
- if (d2 < FA2_COINCIDENT_SQ) { // coincident: antisymmetric unit kick of magnitude k m_i m_j / 0.01 (7.2)
546
- f = f + kick_dir(i, jj, P.dim) * (P.scalingRatio * pi.w * o.w / FA2_DIST_FLOOR);
573
+ if (d2 < FA2_COINCIDENT_SQ) { // coincident: antisymmetric unit kick of the law's magnitude at d = 0.01 (7.2; PD-10)
574
+ f = f + kick_dir(i, jj, P.dim) * kick_magnitude(pi.w, o.w);
547
575
  continue;
548
576
  }
549
- d2 = max(d2, FA2_DIST_FLOOR_SQ); // d >= 0.01
577
+ if (LAW == 0u) { d2 = max(d2, FA2_DIST_FLOOR_SQ); } // FA2 alone floors d >= 0.01 (7.2); FR and coulomb are unfloored (7.20)
550
578
  let k = P.scalingRatio * pi.w * o.w;
551
- f = f + d * (k / d2); // |F| = k m_i m_j / d along d / d
579
+ if (LAW == 0u) { f = f + d * (k / d2); } // LAW 0 (FA2): |F| = k m_i m_j / d along d / d
580
+ if (LAW == 1u) { f = f + d * (P.frK * P.frK / d2); } // LAW 1 (FR, 7.20): |F| = k^2 / d, mass ignored
581
+ if (LAW == 2u) { f = f + d * (-P.coulomb * pi.w * o.w / (d2 * sqrt(d2))); } // LAW 2 (coulomb, ngraph generateQuadTree.js:131-132): |F| = -g m_i m_j / d^2
552
582
  }
553
583
  }
554
584
  workgroupBarrier();
@@ -629,6 +659,7 @@ fn stats_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
629
659
  var hi = vec4f(-F32_MAX);
630
660
  var disp = 0.0;
631
661
  var free = 0u;
662
+ var ke = 0.0;
632
663
  if (fold) {
633
664
  for (var g = lid.x; g < groups; g = g + WG) { // sequential per lane in index order: deterministic
634
665
  let q = partials[g];
@@ -637,6 +668,7 @@ fn stats_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
637
668
  hi = max(hi, q.max);
638
669
  disp = disp + q.dispFree.x;
639
670
  free = free + u32(q.dispFree.y);
671
+ ke = ke + q.swingTraction.x;
640
672
  }
641
673
  }
642
674
  let tSum = wg_reduce_vec4(sum, lid.x, 0u);
@@ -644,6 +676,7 @@ fn stats_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
644
676
  let tHi = wg_reduce_vec4(hi, lid.x, 2u);
645
677
  let tDisp = wg_reduce_f32(disp, lid.x, 0u);
646
678
  let tFree = wg_reduce_u32(free, lid.x, 0u);
679
+ let tKe = wg_reduce_f32(ke, lid.x, 0u);
647
680
  if (lid.x == 0u) {
648
681
  if (fold) {
649
682
  let n = f32(P.n);
@@ -661,6 +694,29 @@ fn stats_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
661
694
  T[P.iterationIndex].meanDisplacement = S.meanDisplacement;
662
695
  T[P.iterationIndex].settledCount = S.settledCount;
663
696
  T[P.iterationIndex].iteration = S.iteration;
697
+ if (STATS_MODE == 1u) { // FR: this iteration's temperature (7.20) into the state and the trace
698
+ if ((P.flags & FA2_FLAG_ADAPTIVE) != 0u) { // adaptive cooling (Yifan Hu 2005 3.2): tKe is the previous iteration's sum |F|^2 over free nodes
699
+ if (fold) {
700
+ var t = S.temperature;
701
+ if (tKe < S.frEnergy) {
702
+ S.frProgress = S.frProgress + 1u;
703
+ if (S.frProgress >= FR_COOLING_PATIENCE) { S.frProgress = 0u; t = t / FR_COOLING_STEP; }
704
+ } else {
705
+ S.frProgress = 0u;
706
+ t = t * FR_COOLING_STEP;
707
+ }
708
+ S.frEnergy = tKe;
709
+ S.temperature = t;
710
+ }
711
+ } else {
712
+ S.temperature = P.temperature;
713
+ }
714
+ T[P.iterationIndex].modelScalar = S.temperature;
715
+ }
716
+ if (STATS_MODE == 2u) { // spring-electrical: the kinetic energy K5 folded into partials B (PD-4); 0 on the first iteration after load()
717
+ S.kineticEnergy = tKe;
718
+ T[P.iterationIndex].modelScalar = tKe;
719
+ }
664
720
  }
665
721
  }`
666
722
  );
@@ -1021,7 +1077,15 @@ const FA2_PARAMS = UniformBlock.define("Fa2Params", [
1021
1077
  ["extentFactor", "f32"],
1022
1078
  ["gridMax", "u32"],
1023
1079
  ["levels", "u32"],
1024
- ["pad", "vec4f"]
1080
+ ["pad", "vec4f"],
1081
+ ["frK", "f32"],
1082
+ ["temperature", "f32"],
1083
+ ["springLength", "f32"],
1084
+ ["springCoefficient", "f32"],
1085
+ ["coulomb", "f32"],
1086
+ ["dragCoefficient", "f32"],
1087
+ ["timeStep", "f32"],
1088
+ ["pad1", "f32"]
1025
1089
  ]);
1026
1090
  const FA2_STATE = UniformBlock.define(
1027
1091
  "Fa2State",
@@ -1042,7 +1106,10 @@ const FA2_STATE = UniformBlock.define(
1042
1106
  ["settledCount", "u32"],
1043
1107
  ["outsideGrid", "u32"],
1044
1108
  ["maxCellOccupancy", "u32"],
1045
- ["reserved0", "vec4f"],
1109
+ ["temperature", "f32"],
1110
+ ["kineticEnergy", "f32"],
1111
+ ["frEnergy", "f32"],
1112
+ ["frProgress", "u32"],
1046
1113
  ["reserved1", "vec4f"],
1047
1114
  ["reserved2", "vec4f"],
1048
1115
  ["reserved3", "vec4f"],
@@ -1064,7 +1131,7 @@ const FA2_TRACE = UniformBlock.define(
1064
1131
  ["meanDisplacement", "f32"],
1065
1132
  ["settledCount", "u32"],
1066
1133
  ["iteration", "u32"],
1067
- ["pad0", "u32"]
1134
+ ["modelScalar", "f32"]
1068
1135
  ],
1069
1136
  { layout: "storage" }
1070
1137
  );
@@ -1197,7 +1264,7 @@ const FA2_STATS_FINALIZE = {
1197
1264
  decl(1, 2, "T", "storage", "array<Fa2Trace>"),
1198
1265
  decl(2, 0, "P", "uniform", "Fa2Params")
1199
1266
  ],
1200
- overrideDecls: [],
1267
+ overrideDecls: [{ name: "STATS_MODE", type: "u32", default: 0 }],
1201
1268
  uniforms: [FA2_PARAMS, FA2_STATE, FA2_TRACE, FA2_PARTIAL],
1202
1269
  needs: ["subgroups"],
1203
1270
  snippetSlots: [],
@@ -1215,7 +1282,8 @@ const FA2_ATTRACTION = {
1215
1282
  overrideDecls: [
1216
1283
  { name: "LINLOG", type: "bool", default: false },
1217
1284
  { name: "DISTRIBUTED", type: "bool", default: false },
1218
- { name: "TIER", type: "u32", default: 0 }
1285
+ { name: "TIER", type: "u32", default: 0 },
1286
+ { name: "LAW", type: "u32", default: 0 }
1219
1287
  ],
1220
1288
  uniforms: [FA2_PARAMS],
1221
1289
  needs: [],
@@ -1238,7 +1306,8 @@ const FA2_REPULSION_EXACT = {
1238
1306
  overrideDecls: [
1239
1307
  { name: "SWING_MODE", type: "u32", default: 0 },
1240
1308
  { name: "STRONG_GRAVITY", type: "bool", default: false },
1241
- { name: "GRAVITY_CENTER", type: "u32", default: 0 }
1309
+ { name: "GRAVITY_CENTER", type: "u32", default: 0 },
1310
+ { name: "LAW", type: "u32", default: 0 }
1242
1311
  ],
1243
1312
  uniforms: [FA2_PARAMS, FA2_STATE, FA2_PARTIAL],
1244
1313
  needs: ["subgroups"],
@@ -1274,7 +1343,10 @@ const FA2_INTEGRATE = {
1274
1343
  decl(1, 5, "partials", "storage", "array<Fa2Partial>"),
1275
1344
  decl(2, 0, "P", "uniform", "Fa2Params")
1276
1345
  ],
1277
- overrideDecls: [{ name: "SWING_MODE", type: "u32", default: 0 }],
1346
+ overrideDecls: [
1347
+ { name: "SWING_MODE", type: "u32", default: 0 },
1348
+ { name: "APPLY", type: "u32", default: 0 }
1349
+ ],
1278
1350
  uniforms: [FA2_PARAMS, FA2_STATE, FA2_PARTIAL],
1279
1351
  needs: ["subgroups"],
1280
1352
  snippetSlots: [],
@@ -1475,9 +1547,9 @@ function checkDest$3(dest, n) {
1475
1547
  }
1476
1548
  );
1477
1549
  }
1478
- function coreOf$3(ctx, s) {
1550
+ function coreOf$3(ctx, s2) {
1479
1551
  try {
1480
- return ctx.residency.core(s);
1552
+ return ctx.residency.core(s2);
1481
1553
  } catch (error) {
1482
1554
  if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
1483
1555
  throw new WebGpuGraphError(
@@ -1489,9 +1561,9 @@ function coreOf$3(ctx, s) {
1489
1561
  throw error;
1490
1562
  }
1491
1563
  }
1492
- async function degree(ctx, s, options) {
1564
+ async function degree(ctx, s2, options) {
1493
1565
  ctx.assertReady();
1494
- const n = s.nodeCount;
1566
+ const n = s2.nodeCount;
1495
1567
  const dest = checkDest$3(options?.dest, n);
1496
1568
  if (options?.signal?.aborted) {
1497
1569
  throw new WebGpuGraphError("E_ABORTED", "degree: the signal was aborted before any work started", {});
@@ -1500,8 +1572,8 @@ async function degree(ctx, s, options) {
1500
1572
  options?.onProgress?.(1, 1);
1501
1573
  return dest ?? new Uint32Array(0);
1502
1574
  }
1503
- const core = coreOf$3(ctx, s);
1504
- if (s.arcCount === 0) {
1575
+ const core = coreOf$3(ctx, s2);
1576
+ if (s2.arcCount === 0) {
1505
1577
  const zeros = dest ?? new Uint32Array(n);
1506
1578
  zeros.fill(0);
1507
1579
  options?.onProgress?.(1, 1);
@@ -1522,7 +1594,7 @@ async function degree(ctx, s, options) {
1522
1594
  await ctx.allocator.check();
1523
1595
  const kernel = await ctx.pipelines.kernel(kernelSpec("degree", graphOverrides(core, null)));
1524
1596
  const bytes = new ArrayBuffer(RANGE_PARAMS.byteLength);
1525
- RANGE_PARAMS.write(new DataView(bytes), { start: 0, end: n, arcBase: 0, arcEnd: s.arcCount, accumulate: 0, n });
1597
+ RANGE_PARAMS.write(new DataView(bytes), { start: 0, end: n, arcBase: 0, arcEnd: s2.arcCount, accumulate: 0, n });
1526
1598
  ctx.device.queue.writeBuffer(params, 0, bytes);
1527
1599
  const outBinding = { buffer: out, offset: 0, size: byteLength, window: null };
1528
1600
  const paramsBinding = { buffer: params, offset: 0, size: RANGE_PARAMS.byteLength, window: null };
@@ -2065,9 +2137,9 @@ function checkDest$2(dest, n) {
2065
2137
  }
2066
2138
  );
2067
2139
  }
2068
- function coreOf$2(ctx, s) {
2140
+ function coreOf$2(ctx, s2) {
2069
2141
  try {
2070
- return ctx.residency.core(s);
2142
+ return ctx.residency.core(s2);
2071
2143
  } catch (error) {
2072
2144
  if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
2073
2145
  throw new WebGpuGraphError(
@@ -2096,18 +2168,18 @@ function labelResult(labels, count) {
2096
2168
  const sizes = new Uint32Array(count);
2097
2169
  let next = 0;
2098
2170
  for (let v = 0; v < n; v++) {
2099
- let k2 = dense[labels[v]];
2100
- if (k2 === U32_MAX) {
2101
- k2 = next++;
2102
- dense[labels[v]] = k2;
2171
+ let k = dense[labels[v]];
2172
+ if (k === U32_MAX) {
2173
+ k = next++;
2174
+ dense[labels[v]] = k;
2103
2175
  }
2104
- sizes[k2]++;
2176
+ sizes[k]++;
2105
2177
  }
2106
2178
  const built = Array.from(sizes, (size) => new Uint32Array(size));
2107
2179
  const filled = new Uint32Array(count);
2108
2180
  for (let v = 0; v < n; v++) {
2109
- const k2 = dense[labels[v]];
2110
- built[k2][filled[k2]++] = v;
2181
+ const k = dense[labels[v]];
2182
+ built[k][filled[k]++] = v;
2111
2183
  }
2112
2184
  groups = built;
2113
2185
  return built;
@@ -2147,9 +2219,9 @@ function checkLabels(raw) {
2147
2219
  }
2148
2220
  return count;
2149
2221
  }
2150
- async function connectedComponents(ctx, s, options) {
2222
+ async function connectedComponents(ctx, s2, options) {
2151
2223
  ctx.assertReady();
2152
- const n = s.nodeCount;
2224
+ const n = s2.nodeCount;
2153
2225
  const renumber = options?.renumber !== false;
2154
2226
  const dest = checkDest$2(options?.dest, n);
2155
2227
  if (options?.signal?.aborted) {
@@ -2159,8 +2231,8 @@ async function connectedComponents(ctx, s, options) {
2159
2231
  options?.onProgress?.(1, 1);
2160
2232
  return labelResult(dest ?? new Uint32Array(0), 0);
2161
2233
  }
2162
- const core = coreOf$2(ctx, s);
2163
- if (s.arcCount === 0) {
2234
+ const core = coreOf$2(ctx, s2);
2235
+ if (s2.arcCount === 0) {
2164
2236
  const labels = dest ?? new Uint32Array(n);
2165
2237
  for (let v = 0; v < n; v++) {
2166
2238
  labels[v] = v;
@@ -2168,7 +2240,7 @@ async function connectedComponents(ctx, s, options) {
2168
2240
  options?.onProgress?.(1, 1);
2169
2241
  return labelResult(labels, n);
2170
2242
  }
2171
- const edges = ctx.residency.view(s, "edgeList");
2243
+ const edges = ctx.residency.view(s2, "edgeList");
2172
2244
  const edgeCount = edges.scalars.edgeCount[0];
2173
2245
  const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS$2);
2174
2246
  try {
@@ -2209,8 +2281,8 @@ async function connectedComponents(ctx, s, options) {
2209
2281
  [fillParams.offset]
2210
2282
  );
2211
2283
  const graph = graphBindings(core, null);
2212
- for (let r = 0; r < 2; r++) {
2213
- const params = wccParams({ items: n, stride: rowPlan.stride ?? n, r, giant: U32_MAX });
2284
+ for (let r2 = 0; r2 < 2; r2++) {
2285
+ const params = wccParams({ items: n, stride: rowPlan.stride ?? n, r: r2, giant: U32_MAX });
2214
2286
  const bound = linkSample.bind({ ...graph, comp: compBinding, P: params.binding });
2215
2287
  linkSample.dispatch(pass, bound, rowPlan, [params.offset]);
2216
2288
  }
@@ -2580,9 +2652,9 @@ function checkDest$1(dest, n, algorithm) {
2580
2652
  }
2581
2653
  );
2582
2654
  }
2583
- function coreOf$1(ctx, s, algorithm) {
2655
+ function coreOf$1(ctx, s2, algorithm) {
2584
2656
  try {
2585
- return ctx.residency.core(s);
2657
+ return ctx.residency.core(s2);
2586
2658
  } catch (error) {
2587
2659
  if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
2588
2660
  throw new WebGpuGraphError(
@@ -2604,9 +2676,9 @@ function aborted$1(algorithm, batchId) {
2604
2676
  batchId === void 0 ? {} : { batchId }
2605
2677
  );
2606
2678
  }
2607
- async function run(ctx, s, personalization, options, algorithm) {
2679
+ async function run(ctx, s2, personalization, options, algorithm) {
2608
2680
  ctx.assertReady();
2609
- const n = s.nodeCount;
2681
+ const n = s2.nodeCount;
2610
2682
  const alpha = options?.dampingFactor ?? 0.85;
2611
2683
  const maxIterations = options?.maxIterations ?? 100;
2612
2684
  const tolerance = options?.tolerance ?? 1e-6;
@@ -2626,8 +2698,8 @@ async function run(ctx, s, personalization, options, algorithm) {
2626
2698
  options?.onProgress?.(maxIterations, maxIterations);
2627
2699
  return { scores: dest ?? new Float32Array(0), iterations: 0, converged: true, danglingMass: 0, precision: "f32" };
2628
2700
  }
2629
- const core = coreOf$1(ctx, s, algorithm);
2630
- if (s.arcCount === 0) {
2701
+ const core = coreOf$1(ctx, s2, algorithm);
2702
+ if (s2.arcCount === 0) {
2631
2703
  const scores = dest ?? new Float32Array(n);
2632
2704
  if (personalization === null) {
2633
2705
  scores.fill(1 / n);
@@ -2637,7 +2709,7 @@ async function run(ctx, s, personalization, options, algorithm) {
2637
2709
  options?.onProgress?.(maxIterations, maxIterations);
2638
2710
  return { scores, iterations: 0, converged: true, danglingMass: 1, precision: "f32" };
2639
2711
  }
2640
- const view = ctx.residency.view(s, "reverse");
2712
+ const view = ctx.residency.view(s2, "reverse");
2641
2713
  const rev = coreOfView(view, view.scalars.arcCount[0]);
2642
2714
  const weights = useWeights ? void 0 : null;
2643
2715
  const weightedCore = useWeights ? core : { ...core, weights: null, hasWeights: false };
@@ -2686,13 +2758,13 @@ async function run(ctx, s, personalization, options, algorithm) {
2686
2758
  let cur = 0;
2687
2759
  let iterationsRun = 0;
2688
2760
  for (; ; ) {
2689
- const k2 = Math.min(PR_BATCH, maxIterations - iterationsRun);
2761
+ const k = Math.min(PR_BATCH, maxIterations - iterationsRun);
2690
2762
  const batch = new CommandBatch(ctx, algorithm);
2691
2763
  const pass = batch.pass("iterations");
2692
2764
  if (iterationsRun === 0) {
2693
2765
  normaliser.record(pass, weightedCore, outWeightSumBinding);
2694
2766
  }
2695
- for (let i = 0; i < k2; i++) {
2767
+ for (let i = 0; i < k; i++) {
2696
2768
  const params = scope.params(PR_PARAMS, {
2697
2769
  n,
2698
2770
  groups,
@@ -2731,7 +2803,7 @@ async function run(ctx, s, personalization, options, algorithm) {
2731
2803
  scope.flush();
2732
2804
  const submitted = batch.submit();
2733
2805
  const back = await submitted.readback;
2734
- iterationsRun += k2;
2806
+ iterationsRun += k;
2735
2807
  ctx.assertReady();
2736
2808
  if (options?.signal?.aborted) {
2737
2809
  throw aborted$1(algorithm, submitted.id);
@@ -2760,11 +2832,11 @@ async function run(ctx, s, personalization, options, algorithm) {
2760
2832
  scope.dispose();
2761
2833
  }
2762
2834
  }
2763
- function pageRank(ctx, s, options) {
2764
- return run(ctx, s, null, options, "pageRank");
2835
+ function pageRank(ctx, s2, options) {
2836
+ return run(ctx, s2, null, options, "pageRank");
2765
2837
  }
2766
- async function personalizedPageRank(ctx, s, personalization, options) {
2767
- const n = s.nodeCount;
2838
+ async function personalizedPageRank(ctx, s2, personalization, options) {
2839
+ const n = s2.nodeCount;
2768
2840
  const invalid2 = (value, expected) => new WebGpuGraphError("E_INVALID_ARGUMENT", `personalizedPageRank: personalization must be ${expected}`, {
2769
2841
  argument: "personalization",
2770
2842
  value,
@@ -2788,7 +2860,7 @@ async function personalizedPageRank(ctx, s, personalization, options) {
2788
2860
  for (let v = 0; v < n; v++) {
2789
2861
  normalised2[v] = personalization[v] / total;
2790
2862
  }
2791
- return run(ctx, s, normalised2, options, "personalizedPageRank");
2863
+ return run(ctx, s2, normalised2, options, "personalizedPageRank");
2792
2864
  }
2793
2865
  const BATCH = 8;
2794
2866
  const RING_SLOTS = 4 * BATCH + 8;
@@ -2809,9 +2881,9 @@ function checkDest(dest, n, algorithm) {
2809
2881
  }
2810
2882
  );
2811
2883
  }
2812
- function coreOf(ctx, s, algorithm) {
2884
+ function coreOf(ctx, s2, algorithm) {
2813
2885
  try {
2814
- return ctx.residency.core(s);
2886
+ return ctx.residency.core(s2);
2815
2887
  } catch (error) {
2816
2888
  if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
2817
2889
  throw new WebGpuGraphError(
@@ -2823,8 +2895,8 @@ function coreOf(ctx, s, algorithm) {
2823
2895
  throw error;
2824
2896
  }
2825
2897
  }
2826
- function reverseOf(ctx, s) {
2827
- const view = ctx.residency.view(s, "reverse");
2898
+ function reverseOf(ctx, s2) {
2899
+ const view = ctx.residency.view(s2, "reverse");
2828
2900
  return coreOfView(view, view.scalars.arcCount[0]);
2829
2901
  }
2830
2902
  function aborted(algorithm, batchId) {
@@ -2871,10 +2943,10 @@ async function runPowerIteration(ctx, n, config) {
2871
2943
  const coefficients = { alpha: config.alpha, beta: config.beta, uniformP: config.uniformP };
2872
2944
  let iterationsRun = 0;
2873
2945
  for (; ; ) {
2874
- const k2 = Math.min(BATCH, config.maxIterations - iterationsRun);
2946
+ const k = Math.min(BATCH, config.maxIterations - iterationsRun);
2875
2947
  const batch = new CommandBatch(ctx, config.label);
2876
2948
  const pass = batch.pass("iterations");
2877
- for (let i = 0; i < k2; i++) {
2949
+ for (let i = 0; i < k; i++) {
2878
2950
  const iteration = iterationsRun + i + 1;
2879
2951
  const params = scope.params(PR_PARAMS, {
2880
2952
  n,
@@ -2896,12 +2968,12 @@ async function runPowerIteration(ctx, n, config) {
2896
2968
  }
2897
2969
  batch.endPass();
2898
2970
  const headerRequest = batch.readback(partialsBuffer, 0, PR_PARTIAL.byteLength);
2899
- const scoresRequest = batch.readback(ring[(iterationsRun + k2) % ring.length].buffer, 0, bytes);
2900
- const previousRequest = config.alternate === null ? null : batch.readback(ring[(iterationsRun + k2 - 1) % ring.length].buffer, 0, bytes);
2971
+ const scoresRequest = batch.readback(ring[(iterationsRun + k) % ring.length].buffer, 0, bytes);
2972
+ const previousRequest = config.alternate === null ? null : batch.readback(ring[(iterationsRun + k - 1) % ring.length].buffer, 0, bytes);
2901
2973
  scope.flush();
2902
2974
  const submitted = batch.submit();
2903
2975
  const back = await submitted.readback;
2904
- iterationsRun += k2;
2976
+ iterationsRun += k;
2905
2977
  ctx.assertReady();
2906
2978
  if (config.signal?.aborted === true) {
2907
2979
  throw aborted(config.label, submitted.id);
@@ -2927,9 +2999,9 @@ async function runPowerIteration(ctx, n, config) {
2927
2999
  scope.dispose();
2928
3000
  }
2929
3001
  }
2930
- function resolve(ctx, s, options, algorithm) {
3002
+ function resolve(ctx, s2, options, algorithm) {
2931
3003
  ctx.assertReady();
2932
- const n = s.nodeCount;
3004
+ const n = s2.nodeCount;
2933
3005
  const maxIterations = options?.maxIterations ?? 100;
2934
3006
  const tolerance = options?.tolerance ?? 1e-6;
2935
3007
  if (!Number.isInteger(maxIterations) || maxIterations < 1) {
@@ -2987,58 +3059,58 @@ function latestOfKind(sameSeed, otherSeed) {
2987
3059
  const index = Math.max(fromSame, fromOther);
2988
3060
  return (index === chain.iterationsRun ? chain.scores : chain.previous) ?? chain.scores;
2989
3061
  }
2990
- async function eigenvectorCentrality(ctx, s, options) {
3062
+ async function eigenvectorCentrality(ctx, s2, options) {
2991
3063
  const algorithm = "eigenvectorCentrality";
2992
- const r = resolve(ctx, s, options, algorithm);
2993
- if (r.n === 0) {
2994
- return empty(r.dest, r.maxIterations, options?.onProgress);
3064
+ const r2 = resolve(ctx, s2, options, algorithm);
3065
+ if (r2.n === 0) {
3066
+ return empty(r2.dest, r2.maxIterations, options?.onProgress);
2995
3067
  }
2996
- const run2 = await runPowerIteration(ctx, r.n, {
3068
+ const run2 = await runPowerIteration(ctx, r2.n, {
2997
3069
  normMode: 2,
2998
- adjacency: coreOf(ctx, s, algorithm),
3070
+ adjacency: coreOf(ctx, s2, algorithm),
2999
3071
  alternate: null,
3000
3072
  alpha: 1,
3001
3073
  beta: 0,
3002
3074
  uniformP: 0,
3003
- maxIterations: r.maxIterations,
3004
- tolerance: r.tolerance,
3005
- weights: r.weights,
3075
+ maxIterations: r2.maxIterations,
3076
+ tolerance: r2.tolerance,
3077
+ weights: r2.weights,
3006
3078
  label: algorithm,
3007
3079
  signal: options?.signal,
3008
3080
  onProgress: options?.onProgress
3009
3081
  });
3010
- return scoresOf(run2, r.dest, "l2");
3082
+ return scoresOf(run2, r2.dest, "l2");
3011
3083
  }
3012
- async function katzCentrality(ctx, s, options) {
3084
+ async function katzCentrality(ctx, s2, options) {
3013
3085
  const algorithm = "katzCentrality";
3014
3086
  const alpha = finite(options?.alpha, 0.1, "alpha");
3015
3087
  const beta = finite(options?.beta, 1, "beta");
3016
- const r = resolve(ctx, s, options, algorithm);
3017
- if (r.n === 0) {
3018
- return empty(r.dest, r.maxIterations, options?.onProgress);
3088
+ const r2 = resolve(ctx, s2, options, algorithm);
3089
+ if (r2.n === 0) {
3090
+ return empty(r2.dest, r2.maxIterations, options?.onProgress);
3019
3091
  }
3020
- const run2 = await runPowerIteration(ctx, r.n, {
3092
+ const run2 = await runPowerIteration(ctx, r2.n, {
3021
3093
  normMode: 4,
3022
- adjacency: reverseOf(ctx, s),
3094
+ adjacency: reverseOf(ctx, s2),
3023
3095
  alternate: null,
3024
3096
  alpha,
3025
3097
  beta,
3026
3098
  uniformP: 1,
3027
- maxIterations: r.maxIterations,
3028
- tolerance: r.tolerance,
3029
- weights: r.weights,
3099
+ maxIterations: r2.maxIterations,
3100
+ tolerance: r2.tolerance,
3101
+ weights: r2.weights,
3030
3102
  label: algorithm,
3031
3103
  signal: options?.signal,
3032
3104
  onProgress: options?.onProgress
3033
3105
  });
3034
- return scoresOf(run2, r.dest, "l2");
3106
+ return scoresOf(run2, r2.dest, "l2");
3035
3107
  }
3036
- async function hits(ctx, s, options) {
3108
+ async function hits(ctx, s2, options) {
3037
3109
  const algorithm = "hits";
3038
- const r = resolve(ctx, s, options, algorithm);
3039
- const total = 2 * r.maxIterations;
3040
- if (r.n === 0) {
3041
- const { scores, iterations, converged, precision } = empty(r.dest, total, options?.onProgress);
3110
+ const r2 = resolve(ctx, s2, options, algorithm);
3111
+ const total = 2 * r2.maxIterations;
3112
+ if (r2.n === 0) {
3113
+ const { scores, iterations, converged, precision } = empty(r2.dest, total, options?.onProgress);
3042
3114
  return { hubs: scores, authorities: new Float32Array(0), iterations, converged, precision };
3043
3115
  }
3044
3116
  const onProgress = options?.onProgress;
@@ -3047,14 +3119,14 @@ async function hits(ctx, s, options) {
3047
3119
  alpha: 1,
3048
3120
  beta: 0,
3049
3121
  uniformP: 0,
3050
- maxIterations: r.maxIterations,
3051
- tolerance: r.tolerance,
3052
- weights: r.weights,
3122
+ maxIterations: r2.maxIterations,
3123
+ tolerance: r2.tolerance,
3124
+ weights: r2.weights,
3053
3125
  signal: options?.signal
3054
3126
  };
3055
- const forward = coreOf(ctx, s, algorithm);
3056
- const reverse = reverseOf(ctx, s);
3057
- const hubSeeded = await runPowerIteration(ctx, r.n, {
3127
+ const forward = coreOf(ctx, s2, algorithm);
3128
+ const reverse = reverseOf(ctx, s2);
3129
+ const hubSeeded = await runPowerIteration(ctx, r2.n, {
3058
3130
  ...shared,
3059
3131
  adjacency: reverse,
3060
3132
  alternate: forward,
@@ -3063,17 +3135,17 @@ async function hits(ctx, s, options) {
3063
3135
  onProgress(done, total);
3064
3136
  }
3065
3137
  });
3066
- const authoritySeeded = await runPowerIteration(ctx, r.n, {
3138
+ const authoritySeeded = await runPowerIteration(ctx, r2.n, {
3067
3139
  ...shared,
3068
3140
  adjacency: forward,
3069
3141
  alternate: reverse,
3070
3142
  label: `${algorithm}/authority-seeded`,
3071
3143
  onProgress: onProgress === void 0 ? void 0 : (done) => {
3072
- onProgress(r.maxIterations + done, total);
3144
+ onProgress(r2.maxIterations + done, total);
3073
3145
  }
3074
3146
  });
3075
3147
  return {
3076
- hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r.dest, "sum"),
3148
+ hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r2.dest, "sum"),
3077
3149
  authorities: normalised(latestOfKind(authoritySeeded, hubSeeded), null, "sum"),
3078
3150
  iterations: Math.max(hubSeeded.iterations, authoritySeeded.iterations),
3079
3151
  converged: hubSeeded.converged && authoritySeeded.converged,
@@ -3119,8 +3191,8 @@ function resolveCenter(center) {
3119
3191
  }
3120
3192
  return out;
3121
3193
  }
3122
- function seedPositions(s, positions, seed, dim, scale, center, range) {
3123
- const n = s.nodeCount;
3194
+ function seedPositions(s2, positions, seed, dim, scale, center, range) {
3195
+ const n = s2.nodeCount;
3124
3196
  if (dim !== 2 && dim !== 3) {
3125
3197
  throw new WebGpuGraphError("E_INVALID_ARGUMENT", `dim must be 2 or 3, got ${String(dim)}`, {
3126
3198
  argument: "dim",
@@ -3292,7 +3364,7 @@ function requireFields(block, required, what) {
3292
3364
  throw invalidArgument(
3293
3365
  what,
3294
3366
  field,
3295
- required.map((r) => r[0]),
3367
+ required.map((r2) => r2[0]),
3296
3368
  `${what} block ${block.name} must declare ${field}`
3297
3369
  );
3298
3370
  }
@@ -3308,7 +3380,7 @@ function requireFields(block, required, what) {
3308
3380
  }
3309
3381
  function stableKey(record) {
3310
3382
  return JSON.stringify(
3311
- Object.keys(record).sort().map((k2) => [k2, record[k2]])
3383
+ Object.keys(record).sort().map((k) => [k, record[k]])
3312
3384
  );
3313
3385
  }
3314
3386
  function withMs(stats, msPerIteration) {
@@ -3441,7 +3513,7 @@ class HeaderWriter {
3441
3513
  this.queued.clear();
3442
3514
  }
3443
3515
  }
3444
- function createPending(k2, generation) {
3516
+ function createPending(k, generation) {
3445
3517
  let resolveFn = () => void 0;
3446
3518
  let rejectFn = () => void 0;
3447
3519
  const promise = new Promise((resolve2, reject) => {
@@ -3449,7 +3521,7 @@ function createPending(k2, generation) {
3449
3521
  rejectFn = reject;
3450
3522
  });
3451
3523
  return {
3452
- k: k2,
3524
+ k,
3453
3525
  generation,
3454
3526
  promise,
3455
3527
  resolve: () => {
@@ -3778,6 +3850,15 @@ class ForceSimulation {
3778
3850
  `the model resolved ${inputs.mass.length} masses for ${n} nodes`
3779
3851
  );
3780
3852
  }
3853
+ const fixedWords = Math.ceil(n / 32);
3854
+ if (inputs.fixed !== void 0 && inputs.fixed !== null && inputs.fixed.length < fixedWords) {
3855
+ throw invalidArgument(
3856
+ "fixed",
3857
+ inputs.fixed.length,
3858
+ fixedWords,
3859
+ `the model resolved a fixed mask of ${inputs.fixed.length} words, ${fixedWords} needed for ${n} nodes`
3860
+ );
3861
+ }
3781
3862
  }
3782
3863
  this.generationValue++;
3783
3864
  this.discardPending();
@@ -3802,6 +3883,10 @@ class ForceSimulation {
3802
3883
  this.fixedWords = makeMask(n);
3803
3884
  this.fixedDirty = false;
3804
3885
  }
3886
+ if (inputs !== null && inputs.fixed !== void 0 && inputs.fixed !== null) {
3887
+ this.fixedWords.set(inputs.fixed.subarray(0, Math.ceil(n / 32)));
3888
+ this.fixedDirty = true;
3889
+ }
3805
3890
  if (n === 0 || core === null || inputs === null) {
3806
3891
  this.weightsUpload = null;
3807
3892
  this.resources = null;
@@ -3863,14 +3948,14 @@ class ForceSimulation {
3863
3948
  )
3864
3949
  );
3865
3950
  }
3866
- const k2 = iterations ?? this.optionsValue.iterationsPerStep ?? FA2_DEFAULTS.iterationsPerStep;
3867
- if (!Number.isInteger(k2) || k2 < 1 || k2 > MAX_ITERATIONS_PER_STEP) {
3951
+ const k = iterations ?? this.optionsValue.iterationsPerStep ?? FA2_DEFAULTS.iterationsPerStep;
3952
+ if (!Number.isInteger(k) || k < 1 || k > MAX_ITERATIONS_PER_STEP) {
3868
3953
  return Promise.reject(
3869
3954
  invalidArgument(
3870
3955
  "iterations",
3871
- k2,
3956
+ k,
3872
3957
  `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`,
3873
- `step(${k2}): iterations must be an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
3958
+ `step(${k}): iterations must be an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
3874
3959
  )
3875
3960
  );
3876
3961
  }
@@ -3881,7 +3966,7 @@ class ForceSimulation {
3881
3966
  this.coalescedValue++;
3882
3967
  return this.pending[0].promise;
3883
3968
  }
3884
- const record = createPending(k2, this.generationValue);
3969
+ const record = createPending(k, this.generationValue);
3885
3970
  this.pending.push(record);
3886
3971
  this.submitChain = this.submitChain.then(() => this.submitBatch(record));
3887
3972
  return record.promise;
@@ -3892,7 +3977,7 @@ class ForceSimulation {
3892
3977
  */
3893
3978
  async flush() {
3894
3979
  while (this.pending.length > 0) {
3895
- await Promise.allSettled(this.pending.map((r) => r.promise));
3980
+ await Promise.allSettled(this.pending.map((r2) => r2.promise));
3896
3981
  }
3897
3982
  }
3898
3983
  /**
@@ -3931,8 +4016,8 @@ class ForceSimulation {
3931
4016
  if (signal?.aborted === true) {
3932
4017
  throw this.abortedError();
3933
4018
  }
3934
- const k2 = Math.min(batch, budget - this.iterationsDoneValue);
3935
- await this.step(k2);
4019
+ const k = Math.min(batch, budget - this.iterationsDoneValue);
4020
+ await this.step(k);
3936
4021
  }
3937
4022
  if (signal?.aborted === true) {
3938
4023
  throw this.abortedError();
@@ -4159,27 +4244,27 @@ class ForceSimulation {
4159
4244
  */
4160
4245
  recordAndSubmit(record) {
4161
4246
  const buffers = this.requireBuffers();
4162
- const { k: k2 } = record;
4247
+ const { k } = record;
4163
4248
  const { device } = this.ctx;
4164
4249
  if (this.fixedDirty) {
4165
4250
  device.queue.writeBuffer(buffers.fixed, 0, this.fixedWords);
4166
4251
  this.fixedDirty = false;
4167
4252
  }
4168
4253
  this.flushStateWrites(buffers.state);
4169
- const first = this.ring.reserve(k2);
4170
- for (let i = 0; i < k2; i++) {
4254
+ const first = this.ring.reserve(k);
4255
+ for (let i = 0; i < k; i++) {
4171
4256
  const flags = i === 0 && this.firstPending ? FA2_FLAG_FIRST : 0;
4172
4257
  this.ring.write(first + i, this.model.params, this.paramsForSlot(this.iterationsSubmitted + i, i, flags));
4173
4258
  }
4174
4259
  this.ring.flush();
4175
4260
  const batch = new CommandBatch(this.ctx, `${this.label}/batch`, this.generationValue);
4176
4261
  const beforeEpilogue = this.lastIterationStage();
4177
- for (let i = 0; i < k2; i++) {
4178
- this.model.recordIteration(batch, first + i, this.tierValue, i === k2 - 1 ? void 0 : beforeEpilogue);
4262
+ for (let i = 0; i < k; i++) {
4263
+ this.model.recordIteration(batch, first + i, this.tierValue, i === k - 1 ? void 0 : beforeEpilogue);
4179
4264
  }
4180
4265
  batch.endPass();
4181
4266
  const scene = batch.readback(buffers.scene, 0, 12 * this.n);
4182
- const state = batch.readback(buffers.state, 0, STATE_HEADER_BYTES + k2 * this.traceBytes);
4267
+ const state = batch.readback(buffers.state, 0, STATE_HEADER_BYTES + k * this.traceBytes);
4183
4268
  const { profiler } = this.ctx;
4184
4269
  const profile = profiler === null ? null : profiler.resolveInto(batch);
4185
4270
  const submitted = batch.submit();
@@ -4190,7 +4275,7 @@ class ForceSimulation {
4190
4275
  record.profile = profile;
4191
4276
  record.startedAt = performance.now();
4192
4277
  this.lastSubmittedBatchIdValue = submitted.id;
4193
- this.iterationsSubmitted += k2;
4278
+ this.iterationsSubmitted += k;
4194
4279
  this.firstPending = false;
4195
4280
  return submitted;
4196
4281
  }
@@ -4211,11 +4296,11 @@ class ForceSimulation {
4211
4296
  return;
4212
4297
  }
4213
4298
  try {
4214
- const { k: k2 } = record;
4299
+ const { k } = record;
4215
4300
  const { n } = this;
4216
4301
  const needed = Math.max(
4217
4302
  record.sceneOffset + 12 * n,
4218
- record.stateOffset + STATE_HEADER_BYTES + k2 * this.traceBytes
4303
+ record.stateOffset + STATE_HEADER_BYTES + k * this.traceBytes
4219
4304
  );
4220
4305
  if (bytes.byteLength < needed) {
4221
4306
  record.resolve();
@@ -4223,15 +4308,15 @@ class ForceSimulation {
4223
4308
  }
4224
4309
  this.copyScene(owner, bytes, record);
4225
4310
  const header = new DataView(bytes, record.stateOffset, STATE_HEADER_BYTES);
4226
- const trace = new DataView(bytes, record.stateOffset + STATE_HEADER_BYTES, k2 * this.traceBytes);
4311
+ const trace = new DataView(bytes, record.stateOffset + STATE_HEADER_BYTES, k * this.traceBytes);
4227
4312
  this.writer.absorb(header);
4228
4313
  const settledCount = this.model.state.readField(header, "settledCount");
4229
4314
  if (typeof settledCount !== "number") {
4230
4315
  throw invalidArgument("settledCount", settledCount, "a scalar", "settledCount must be a scalar field");
4231
4316
  }
4232
4317
  const ms = this.batchMilliseconds(record, bytes);
4233
- this.statsValue = withMs(this.model.readStats(header, trace), ms / k2);
4234
- this.iterationsDoneValue += k2;
4318
+ this.statsValue = withMs(this.model.readStats(header, trace), ms / k);
4319
+ this.iterationsDoneValue += k;
4235
4320
  this.settledCountValue = record.id <= this.reheatedAfterBatchId ? 0 : settledCount;
4236
4321
  this.settledValue = this.computeSettled();
4237
4322
  record.resolve();
@@ -4357,22 +4442,24 @@ class ForceSimulation {
4357
4442
  * The uniform values of one iteration slot: the model's values with the shared fields on top (PLAN DECISION 6).
4358
4443
  * @param global - the global iteration index (iterationsSubmitted + i)
4359
4444
  * @param index - the slot index within the batch (the trace slot)
4360
- * @param flags - FA2_FLAG_FIRST for the first iteration after load(), else 0
4445
+ * @param flags - FA2_FLAG_FIRST for the first iteration after load(), else 0; the model's own flag bits are OR-ed in
4361
4446
  * @returns the values
4362
4447
  */
4363
4448
  paramsForSlot(global, index, flags) {
4364
4449
  const [cx, cy, cz] = this.center;
4450
+ const model = this.model.paramsFor(global, this.optionsValue);
4451
+ const modelFlags = typeof model.flags === "number" ? model.flags : 0;
4365
4452
  const shared = {
4366
4453
  n: this.n,
4367
4454
  dim: this.dimValue,
4368
- flags,
4455
+ flags: flags | modelFlags,
4369
4456
  iterationIndex: index,
4370
4457
  seed: this.seedU32(),
4371
4458
  scale: this.scale,
4372
4459
  center: [cx, cy, cz, 0],
4373
4460
  settleThreshold: this.settleThreshold()
4374
4461
  };
4375
- return { ...this.model.paramsFor(global, this.optionsValue), ...shared };
4462
+ return { ...model, ...shared };
4376
4463
  }
4377
4464
  /**
4378
4465
  * The option seed as a u32 (0 when unseeded), the near-field hash seed of P4.
@@ -4652,9 +4739,9 @@ class ForceSimulation {
4652
4739
  const dx = packed[4 * i] - mx;
4653
4740
  const dy = packed[4 * i + 1] - my;
4654
4741
  const dz = packed[4 * i + 2] - mz;
4655
- const q = dx * dx + dy * dy + dz * dz;
4656
- sumSq += q;
4657
- maxSq = Math.max(maxSq, q);
4742
+ const q2 = dx * dx + dy * dy + dz * dz;
4743
+ sumSq += q2;
4744
+ maxSq = Math.max(maxSq, q2);
4658
4745
  }
4659
4746
  const { writer } = this;
4660
4747
  writer.set("iteration", 0);
@@ -4902,16 +4989,16 @@ function checkMass(mass, source) {
4902
4989
  }
4903
4990
  }
4904
4991
  }
4905
- function degreePlusOne(s) {
4906
- const n = s.nodeCount;
4907
- const degree2 = s.outDegree();
4992
+ function degreePlusOne(s2) {
4993
+ const n = s2.nodeCount;
4994
+ const degree2 = s2.outDegree();
4908
4995
  const out = new Float32Array(n);
4909
4996
  for (let i = 0; i < n; i++) {
4910
4997
  out[i] = degree2[i] + 1;
4911
4998
  }
4912
4999
  return out;
4913
5000
  }
4914
- function massFromColumn(s, column) {
5001
+ function massFromColumn(s2, column) {
4915
5002
  const { name } = column.meta;
4916
5003
  if (column.meta.components !== 1) {
4917
5004
  throw invalid$1(
@@ -4925,14 +5012,14 @@ function massFromColumn(s, column) {
4925
5012
  switch (column.dtype) {
4926
5013
  case "f32":
4927
5014
  case "f64": {
4928
- const view = s.nodes.gpuView(name);
5015
+ const view = s2.nodes.gpuView(name);
4929
5016
  const out2 = view instanceof Float32Array ? view : new Float32Array(view);
4930
5017
  checkMass(out2, `node column "${name}"`);
4931
5018
  return out2;
4932
5019
  }
4933
5020
  case "u32":
4934
5021
  case "i32":
4935
- values = s.nodes.gpuView(name);
5022
+ values = s2.nodes.gpuView(name);
4936
5023
  break;
4937
5024
  case "u8":
4938
5025
  values = column.data;
@@ -4945,21 +5032,21 @@ function massFromColumn(s, column) {
4945
5032
  `node column "${name}" is ${column.dtype}, not numeric`
4946
5033
  );
4947
5034
  }
4948
- const out = new Float32Array(s.nodeCount);
5035
+ const out = new Float32Array(s2.nodeCount);
4949
5036
  for (let i = 0; i < out.length; i++) {
4950
5037
  out[i] = values[i];
4951
5038
  }
4952
5039
  checkMass(out, `node column "${name}"`);
4953
5040
  return out;
4954
5041
  }
4955
- function resolveNodeMass(s, spec) {
4956
- const n = s.nodeCount;
5042
+ function resolveNodeMass(s2, spec) {
5043
+ const n = s2.nodeCount;
4957
5044
  if (spec === null || spec === void 0) {
4958
- const column = s.nodes.byRole("mass");
5045
+ const column = s2.nodes.byRole("mass");
4959
5046
  if (column === null) {
4960
- return degreePlusOne(s);
5047
+ return degreePlusOne(s2);
4961
5048
  }
4962
- return massFromColumn(s, column);
5049
+ return massFromColumn(s2, column);
4963
5050
  }
4964
5051
  if (spec instanceof Float32Array) {
4965
5052
  if (spec.length !== n) {
@@ -4969,7 +5056,7 @@ function resolveNodeMass(s, spec) {
4969
5056
  return spec;
4970
5057
  }
4971
5058
  if (typeof spec === "string") {
4972
- const column = s.nodes.get(spec);
5059
+ const column = s2.nodes.get(spec);
4973
5060
  if (column === null) {
4974
5061
  throw invalid$1(
4975
5062
  "nodeMass",
@@ -4978,7 +5065,7 @@ function resolveNodeMass(s, spec) {
4978
5065
  `nodeMass names node column "${spec}", which the snapshot does not hold`
4979
5066
  );
4980
5067
  }
4981
- return massFromColumn(s, column);
5068
+ return massFromColumn(s2, column);
4982
5069
  }
4983
5070
  if (ArrayBuffer.isView(spec)) {
4984
5071
  throw invalid$1(
@@ -4997,9 +5084,9 @@ function resolveNodeMass(s, spec) {
4997
5084
  }
4998
5085
  );
4999
5086
  }
5000
- function resolveWeights(s, spec) {
5087
+ function resolveWeights(s2, spec) {
5001
5088
  if (spec === true) {
5002
- return { data: s.weights, source: "arcs", column: null };
5089
+ return { data: s2.weights, source: "arcs", column: null };
5003
5090
  }
5004
5091
  if (spec === false || spec === null || spec === void 0) {
5005
5092
  return { data: null, source: "none", column: null };
@@ -5012,7 +5099,7 @@ function resolveWeights(s, spec) {
5012
5099
  "weight must be true, false, null or an edge column name"
5013
5100
  );
5014
5101
  }
5015
- const column = s.edges.get(spec);
5102
+ const column = s2.edges.get(spec);
5016
5103
  if (column === null) {
5017
5104
  throw invalid$1(
5018
5105
  "weight",
@@ -5034,15 +5121,15 @@ function resolveWeights(s, spec) {
5034
5121
  case "f64":
5035
5122
  case "u32":
5036
5123
  case "i32": {
5037
- const view = s.edges.gpuView(spec);
5038
- const expanded = expandEdges(s, view);
5124
+ const view = s2.edges.gpuView(spec);
5125
+ const expanded = expandEdges(s2, view);
5039
5126
  const data = expanded instanceof Float32Array ? expanded : new Float32Array(expanded);
5040
5127
  return { data, source: "column", column };
5041
5128
  }
5042
5129
  case "string":
5043
5130
  case "list":
5044
5131
  case "json":
5045
- s.edges.gpuView(spec);
5132
+ s2.edges.gpuView(spec);
5046
5133
  throw invalid$1("weight", spec, "a numeric edge column", `edge column "${spec}" is ${column.dtype}`);
5047
5134
  default:
5048
5135
  throw invalid$1(
@@ -5053,6 +5140,113 @@ function resolveWeights(s, spec) {
5053
5140
  );
5054
5141
  }
5055
5142
  }
5143
+ const FORCE_BYTES_PER_NODE = 12;
5144
+ const FILL_PARAMS_BUFFER = "fillParams";
5145
+ const U32_MODULUS = 4294967296;
5146
+ function describeValue(value) {
5147
+ if (value === null) {
5148
+ return "null";
5149
+ }
5150
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
5151
+ return String(value);
5152
+ }
5153
+ if (typeof value === "undefined") {
5154
+ return "undefined";
5155
+ }
5156
+ if (typeof value === "object" && "length" in value && typeof value.length === "number") {
5157
+ return `[${value.length} values]`;
5158
+ }
5159
+ return typeof value;
5160
+ }
5161
+ function invalid(argument, value, expected) {
5162
+ return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be ${expected}; got ${describeValue(value)}`, {
5163
+ argument,
5164
+ value,
5165
+ expected
5166
+ });
5167
+ }
5168
+ function pickNumber(name, given, fallback, check, expected) {
5169
+ const value = given === void 0 ? fallback : given;
5170
+ if (typeof value !== "number" || !Number.isFinite(value) || !check(value)) {
5171
+ throw invalid(name, value, expected);
5172
+ }
5173
+ return value;
5174
+ }
5175
+ function pickBoolean(name, given, fallback) {
5176
+ const value = given === void 0 ? fallback : given;
5177
+ if (typeof value !== "boolean") {
5178
+ throw invalid(name, value, "a boolean");
5179
+ }
5180
+ return value;
5181
+ }
5182
+ function pickDim(given, fallback) {
5183
+ const value = given === void 0 ? fallback : given;
5184
+ if (value !== 2 && value !== 3) {
5185
+ throw invalid("dim", value, "2 or 3");
5186
+ }
5187
+ return value;
5188
+ }
5189
+ function pickCenter(given, fallback) {
5190
+ if (given === void 0) {
5191
+ return fallback;
5192
+ }
5193
+ const expected = "an array of 2 or 3 finite numbers";
5194
+ const value = given;
5195
+ if (typeof value !== "object" || value === null || !("length" in value)) {
5196
+ throw invalid("center", given, expected);
5197
+ }
5198
+ const { length } = value;
5199
+ if (length !== 2 && length !== 3) {
5200
+ throw invalid("center", given, expected);
5201
+ }
5202
+ const x = given[0];
5203
+ const y = given[1];
5204
+ const z = length === 3 ? given[2] : 0;
5205
+ if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
5206
+ throw invalid("center", given, expected);
5207
+ }
5208
+ return [x, y, z];
5209
+ }
5210
+ function pickSeed(given, fallback) {
5211
+ if (given === void 0) {
5212
+ return fallback;
5213
+ }
5214
+ const value = given;
5215
+ if (value !== null && (typeof value !== "number" || !Number.isFinite(value))) {
5216
+ throw invalid("seed", given, "a finite number or null");
5217
+ }
5218
+ return value;
5219
+ }
5220
+ function isPositiveInteger(value) {
5221
+ return Number.isInteger(value) && value >= 1;
5222
+ }
5223
+ function seedWord(seed) {
5224
+ if (seed === null) {
5225
+ return 0;
5226
+ }
5227
+ return Math.floor(Math.abs(seed)) % U32_MODULUS;
5228
+ }
5229
+ function scalar(values, name) {
5230
+ const value = values[name];
5231
+ if (typeof value !== "number") {
5232
+ throw invalid(name, value, "a scalar field");
5233
+ }
5234
+ return value;
5235
+ }
5236
+ function vector(values, name) {
5237
+ const value = values[name];
5238
+ if (typeof value === "number") {
5239
+ throw invalid(name, value, "a vector field");
5240
+ }
5241
+ return value;
5242
+ }
5243
+ function subset(merged, defaults) {
5244
+ const out = {};
5245
+ for (const name of Object.keys(defaults)) {
5246
+ out[name] = name in merged ? merged[name] : defaults[name];
5247
+ }
5248
+ return out;
5249
+ }
5056
5250
  class RepulsionExact {
5057
5251
  /**
5058
5252
  * Holds the two compiled kernels; create() is the only caller.
@@ -5177,13 +5371,10 @@ class RepulsionExact {
5177
5371
  }
5178
5372
  }
5179
5373
  const FA2_STAGES = ["K1", "K2", "K3", "K4", "K5", "toScene"];
5180
- const FORCE_BYTES_PER_NODE = 12;
5181
- const FILL_PARAMS_BUFFER = "fillParams";
5182
- const ONE_WORKGROUP = { x: 1, y: 1, z: 1, items: 1, stride: null };
5183
- const K2_DEFAULTS = { LINLOG: false, DISTRIBUTED: false, TIER: 0, USE_PERM: false, HAS_WEIGHTS: false };
5184
- const K5_DEFAULTS = { SWING_MODE: 0 };
5185
- const U32_MODULUS = 4294967296;
5186
- const DEFAULT_RESOLVED = Object.freeze({
5374
+ const ONE_WORKGROUP$2 = { x: 1, y: 1, z: 1, items: 1, stride: null };
5375
+ const K2_DEFAULTS$2 = { LINLOG: false, DISTRIBUTED: false, TIER: 0, USE_PERM: false, HAS_WEIGHTS: false };
5376
+ const K5_DEFAULTS$2 = { SWING_MODE: 0 };
5377
+ const DEFAULT_RESOLVED$2 = Object.freeze({
5187
5378
  ...FA2_DEFAULTS,
5188
5379
  nodeMass: null,
5189
5380
  nodeSize: null,
@@ -5191,110 +5382,6 @@ const DEFAULT_RESOLVED = Object.freeze({
5191
5382
  center: [0, 0, 0],
5192
5383
  seed: null
5193
5384
  });
5194
- function describeValue(value) {
5195
- if (value === null) {
5196
- return "null";
5197
- }
5198
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
5199
- return String(value);
5200
- }
5201
- if (typeof value === "undefined") {
5202
- return "undefined";
5203
- }
5204
- if (typeof value === "object" && "length" in value && typeof value.length === "number") {
5205
- return `[${value.length} values]`;
5206
- }
5207
- return typeof value;
5208
- }
5209
- function invalid(argument, value, expected) {
5210
- return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be ${expected}; got ${describeValue(value)}`, {
5211
- argument,
5212
- value,
5213
- expected
5214
- });
5215
- }
5216
- function pickNumber(name, given, fallback, check, expected) {
5217
- const value = given === void 0 ? fallback : given;
5218
- if (typeof value !== "number" || !Number.isFinite(value) || !check(value)) {
5219
- throw invalid(name, value, expected);
5220
- }
5221
- return value;
5222
- }
5223
- function pickBoolean(name, given, fallback) {
5224
- const value = given === void 0 ? fallback : given;
5225
- if (typeof value !== "boolean") {
5226
- throw invalid(name, value, "a boolean");
5227
- }
5228
- return value;
5229
- }
5230
- function pickDim(given, fallback) {
5231
- const value = given === void 0 ? fallback : given;
5232
- if (value !== 2 && value !== 3) {
5233
- throw invalid("dim", value, "2 or 3");
5234
- }
5235
- return value;
5236
- }
5237
- function pickCenter(given, fallback) {
5238
- if (given === void 0) {
5239
- return fallback;
5240
- }
5241
- const expected = "an array of 2 or 3 finite numbers";
5242
- const value = given;
5243
- if (typeof value !== "object" || value === null || !("length" in value)) {
5244
- throw invalid("center", given, expected);
5245
- }
5246
- const { length } = value;
5247
- if (length !== 2 && length !== 3) {
5248
- throw invalid("center", given, expected);
5249
- }
5250
- const x = given[0];
5251
- const y = given[1];
5252
- const z = length === 3 ? given[2] : 0;
5253
- if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
5254
- throw invalid("center", given, expected);
5255
- }
5256
- return [x, y, z];
5257
- }
5258
- function pickSeed(given, fallback) {
5259
- if (given === void 0) {
5260
- return fallback;
5261
- }
5262
- const value = given;
5263
- if (value !== null && (typeof value !== "number" || !Number.isFinite(value))) {
5264
- throw invalid("seed", given, "a finite number or null");
5265
- }
5266
- return value;
5267
- }
5268
- function isPositiveInteger(value) {
5269
- return Number.isInteger(value) && value >= 1;
5270
- }
5271
- function seedWord(seed) {
5272
- if (seed === null) {
5273
- return 0;
5274
- }
5275
- return Math.floor(Math.abs(seed)) % U32_MODULUS;
5276
- }
5277
- function scalar(values, name) {
5278
- const value = values[name];
5279
- if (typeof value !== "number") {
5280
- throw invalid(name, value, "a scalar field");
5281
- }
5282
- return value;
5283
- }
5284
- function vector(values, name) {
5285
- const value = values[name];
5286
- if (typeof value === "number") {
5287
- throw invalid(name, value, "a vector field");
5288
- }
5289
- return value;
5290
- }
5291
- function subset(merged, defaults) {
5292
- const out = {};
5293
- for (const name of Object.keys(defaults)) {
5294
- out[name] = name in merged ? merged[name] : defaults[name];
5295
- }
5296
- return out;
5297
- }
5298
5385
  function repulsionOverrides(merged) {
5299
5386
  return {
5300
5387
  SWING_MODE: merged.SWING_MODE === 1 ? 1 : 0,
@@ -5304,7 +5391,7 @@ function repulsionOverrides(merged) {
5304
5391
  }
5305
5392
  function resolveForceAtlas2Options(options, previous) {
5306
5393
  const o = options ?? {};
5307
- const base = previous ?? DEFAULT_RESOLVED;
5394
+ const base = previous ?? DEFAULT_RESOLVED$2;
5308
5395
  if (previous !== void 0 && o.maxInFlight !== void 0 && o.maxInFlight !== previous.maxInFlight) {
5309
5396
  throw new WebGpuGraphError(
5310
5397
  "E_INVALID_ARGUMENT",
@@ -5458,10 +5545,10 @@ class ForceAtlas2Model {
5458
5545
  * @param options - the simulation's current option record
5459
5546
  * @returns the per-load inputs
5460
5547
  */
5461
- inputs(s, options) {
5548
+ inputs(s2, options) {
5462
5549
  const resolved = resolveForceAtlas2Options(options, this.current);
5463
5550
  const { repulsion, exactMaxNodes } = this.tuning;
5464
- const n = s.nodeCount;
5551
+ const n = s2.nodeCount;
5465
5552
  if (repulsion === "grid" || repulsion === "auto" && n > exactMaxNodes) {
5466
5553
  throw new WebGpuGraphError(
5467
5554
  "E_UNSUPPORTED",
@@ -5472,7 +5559,7 @@ class ForceAtlas2Model {
5472
5559
  }
5473
5560
  );
5474
5561
  }
5475
- return { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) };
5562
+ return { mass: resolveNodeMass(s2, resolved.nodeMass), weights: resolveWeights(s2, resolved.weight) };
5476
5563
  }
5477
5564
  /**
5478
5565
  * { LINLOG, DISTRIBUTED, TIER: 0, SWING_MODE: compat === "networkx" ? 1 : 0, STRONG_GRAVITY, GRAVITY_CENTER:
@@ -5506,10 +5593,10 @@ class ForceAtlas2Model {
5506
5593
  const [repulsionSpec, speedSpec] = RepulsionExact.specs(repulsionOverrides(overrides));
5507
5594
  return [
5508
5595
  kernelSpec("fa2-stats-finalize"),
5509
- kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS)),
5596
+ kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS$2)),
5510
5597
  repulsionSpec,
5511
5598
  speedSpec,
5512
- kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS)),
5599
+ kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS$2)),
5513
5600
  kernelSpec("fa2-to-scene"),
5514
5601
  kernelSpec("fill")
5515
5602
  ];
@@ -5530,8 +5617,8 @@ class ForceAtlas2Model {
5530
5617
  }
5531
5618
  const [k1, k2, k5, toScene, fill] = await Promise.all([
5532
5619
  pipelines.kernel(kernelSpec("fa2-stats-finalize")),
5533
- pipelines.kernel(kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS))),
5534
- pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS))),
5620
+ pipelines.kernel(kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS$2))),
5621
+ pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS$2))),
5535
5622
  pipelines.kernel(kernelSpec("fa2-to-scene")),
5536
5623
  pipelines.kernel(kernelSpec("fill"))
5537
5624
  ]);
@@ -5655,7 +5742,7 @@ class ForceAtlas2Model {
5655
5742
  bound.fill.dispatch(pass, bound.fillOldBound, bound.fillPlan, [0]);
5656
5743
  }
5657
5744
  }
5658
- bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
5745
+ bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP$2, [offset]);
5659
5746
  if (stop < 1) {
5660
5747
  return;
5661
5748
  }
@@ -5805,7 +5892,7 @@ class ForceAtlas2Model {
5805
5892
  this.bound = null;
5806
5893
  }
5807
5894
  }
5808
- function resolvePatch(patch, current) {
5895
+ function resolvePatch$2(patch, current) {
5809
5896
  return resolveForceAtlas2Options(patch, resolveForceAtlas2Options(current));
5810
5897
  }
5811
5898
  function createForceAtlas2(ctx, options) {
@@ -5813,16 +5900,981 @@ function createForceAtlas2(ctx, options) {
5813
5900
  const resolved = resolveForceAtlas2Options(options);
5814
5901
  const tuning = resolveLayoutTuning(options);
5815
5902
  const model = new ForceAtlas2Model(tuning, resolved);
5816
- return new ForceSimulation(ctx, model, resolved, tuning, resolvePatch);
5817
- }
5818
- function copyBetweenness(defaults) {
5819
- const copy = { ...defaults };
5820
- if (defaults.sources !== void 0) {
5821
- copy.sources = Object.freeze([...defaults.sources]);
5903
+ return new ForceSimulation(ctx, model, resolved, tuning, resolvePatch$2);
5904
+ }
5905
+ const FR_STAGES = ["K1", "K2", "K3", "K5", "toScene"];
5906
+ const ONE_WORKGROUP$1 = { x: 1, y: 1, z: 1, items: 1, stride: null };
5907
+ const FR_OVERRIDES = Object.freeze({
5908
+ LINLOG: false,
5909
+ DISTRIBUTED: false,
5910
+ TIER: 0,
5911
+ SWING_MODE: 1,
5912
+ STRONG_GRAVITY: false,
5913
+ GRAVITY_CENTER: 0,
5914
+ LAW: 1,
5915
+ APPLY: 1,
5916
+ STATS_MODE: 1
5917
+ });
5918
+ const K1_DEFAULTS$1 = { STATS_MODE: 0 };
5919
+ const K2_DEFAULTS$1 = {
5920
+ LINLOG: false,
5921
+ DISTRIBUTED: false,
5922
+ TIER: 0,
5923
+ USE_PERM: false,
5924
+ HAS_WEIGHTS: false,
5925
+ LAW: 0
5926
+ };
5927
+ const K3_DEFAULTS$1 = { SWING_MODE: 0, STRONG_GRAVITY: false, GRAVITY_CENTER: 0, LAW: 0 };
5928
+ const K5_DEFAULTS$1 = { SWING_MODE: 0, APPLY: 0 };
5929
+ const DEFAULT_RESOLVED$1 = Object.freeze({
5930
+ ...FR_DEFAULTS,
5931
+ center: [0, 0, 0],
5932
+ seed: null
5933
+ });
5934
+ function resolveK(given, fallback) {
5935
+ if (given === void 0) {
5936
+ return fallback;
5822
5937
  }
5823
- return Object.freeze(copy);
5938
+ if (given === null || given === 0 || Number.isNaN(given)) {
5939
+ return null;
5940
+ }
5941
+ return pickNumber("k", given, 1, (v) => v > 0, "> 0 or null");
5824
5942
  }
5825
- function copyAlgorithms(algorithms) {
5943
+ function pickCooling(given, fallback) {
5944
+ if (given === void 0) {
5945
+ return fallback;
5946
+ }
5947
+ if (given === "linear" || given === "adaptive") {
5948
+ return given;
5949
+ }
5950
+ throw invalid("cooling", given, '"linear" | "adaptive"');
5951
+ }
5952
+ function resolveFruchtermanReingoldOptions(options, previous) {
5953
+ const o = options ?? {};
5954
+ const base = previous ?? DEFAULT_RESOLVED$1;
5955
+ if (previous !== void 0 && o.maxInFlight !== void 0 && o.maxInFlight !== previous.maxInFlight) {
5956
+ throw new WebGpuGraphError(
5957
+ "E_INVALID_ARGUMENT",
5958
+ `maxInFlight cannot change after creation (the uniform ring is sized by it): got ${describeValue(o.maxInFlight)}, current ${previous.maxInFlight}`,
5959
+ { argument: "maxInFlight", value: o.maxInFlight, expected: previous.maxInFlight }
5960
+ );
5961
+ }
5962
+ const fixed = o.fixed === void 0 ? base.fixed : o.fixed;
5963
+ if (fixed !== null && typeof fixed !== "string" && !(fixed instanceof Uint32Array)) {
5964
+ throw invalid("fixed", fixed, "a NodeMask (Uint32Array), the name of a bool node column, or null");
5965
+ }
5966
+ const cooling = pickCooling(o.cooling, base.cooling);
5967
+ const iterationsFallback = previous === void 0 && cooling === "adaptive" && o.iterations === void 0 ? FR_ADAPTIVE_MAX_ITERATIONS : base.iterations;
5968
+ const resolved = {
5969
+ k: resolveK(o.k, base.k),
5970
+ iterations: pickNumber(
5971
+ "iterations",
5972
+ o.iterations,
5973
+ iterationsFallback,
5974
+ (v) => Number.isInteger(v) && v >= 0,
5975
+ "an integer >= 0"
5976
+ ),
5977
+ cooling,
5978
+ fixed,
5979
+ dim: pickDim(o.dim, base.dim),
5980
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
5981
+ center: pickCenter(o.center, base.center),
5982
+ seed: pickSeed(o.seed, base.seed),
5983
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
5984
+ settleWindow: pickNumber(
5985
+ "settleWindow",
5986
+ o.settleWindow,
5987
+ base.settleWindow,
5988
+ isPositiveInteger,
5989
+ "an integer >= 1"
5990
+ ),
5991
+ iterationsPerStep: pickNumber(
5992
+ "iterationsPerStep",
5993
+ o.iterationsPerStep,
5994
+ base.iterationsPerStep,
5995
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
5996
+ `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
5997
+ ),
5998
+ maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
5999
+ };
6000
+ return Object.freeze(resolved);
6001
+ }
6002
+ function resolveFixed(s2, spec) {
6003
+ const words = Math.ceil(s2.nodeCount / 32);
6004
+ if (typeof spec === "string") {
6005
+ const column = s2.nodes.get(spec);
6006
+ if (column === null) {
6007
+ throw invalid(
6008
+ "fixed",
6009
+ spec,
6010
+ `the name of a bool node column (the snapshot holds no node column "${spec}")`
6011
+ );
6012
+ }
6013
+ if (column.dtype !== "bool") {
6014
+ throw invalid("fixed", spec, `the name of a bool node column ("${spec}" is ${column.dtype})`);
6015
+ }
6016
+ return new Uint32Array(column.data.subarray(0, words));
6017
+ }
6018
+ if (spec !== null) {
6019
+ if (spec.length < words) {
6020
+ throw invalid("fixed", spec.length, `a mask of at least ${words} words for ${s2.nodeCount} nodes`);
6021
+ }
6022
+ return new Uint32Array(spec.subarray(0, words));
6023
+ }
6024
+ const byRole = s2.nodes.byRole("fixed");
6025
+ if (byRole !== null && byRole.dtype === "bool") {
6026
+ return new Uint32Array(byRole.data.subarray(0, words));
6027
+ }
6028
+ return null;
6029
+ }
6030
+ class FruchtermanReingoldModel {
6031
+ /**
6032
+ * Creates the model for one simulation.
6033
+ * @param tuning - the resolved GPU-only tuning (the tier rule; compat has no effect on this model)
6034
+ * @param resolved - the resolved option record at creation
6035
+ */
6036
+ constructor(tuning, resolved) {
6037
+ this.kind = "fruchtermanReingold";
6038
+ this.stages = FR_STAGES;
6039
+ this.params = FA2_PARAMS;
6040
+ this.state = FA2_STATE;
6041
+ this.trace = FA2_TRACE;
6042
+ this.tempOrigin = 0;
6043
+ this.pendingReheat = false;
6044
+ this.resources = null;
6045
+ this.bound = null;
6046
+ this.openPass = null;
6047
+ this.tuning = tuning;
6048
+ this.current = resolved;
6049
+ }
6050
+ /**
6051
+ * force 12n and oldForce 12n (zeroed; bound and never read, PD-2) plus the 256-byte FillParams uniform buffer the
6052
+ * fill dispatch reads. n = 0 reports one node's worth of bytes so no zero-length buffer is ever created.
6053
+ * @param n - the node count
6054
+ * @param _dim - the layout dimension (the force arrays are stride 3 in both)
6055
+ * @returns the three model-owned buffer specs
6056
+ */
6057
+ buffers(n, _dim) {
6058
+ const bytes = Math.max(1, n) * FORCE_BYTES_PER_NODE;
6059
+ const usage = BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST;
6060
+ return [
6061
+ { name: "force", byteLength: bytes, usage, zero: true },
6062
+ { name: "oldForce", byteLength: bytes, usage, zero: true },
6063
+ {
6064
+ name: FILL_PARAMS_BUFFER,
6065
+ byteLength: UNIFORM_SLOT_BYTES,
6066
+ usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
6067
+ zero: false
6068
+ }
6069
+ ];
6070
+ }
6071
+ /**
6072
+ * Mass 1 for every node and no weights (PD-11: FR has no mass and ignores weights), the fixed mask of the
6073
+ * `fixed` option applied at load (PD-6). The tier rule is the simulation's (load() throws for the grid tier
6074
+ * before calling this).
6075
+ * @param s - the snapshot being loaded
6076
+ * @param options - the simulation's current option record
6077
+ * @returns the per-load inputs
6078
+ */
6079
+ inputs(s2, options) {
6080
+ const resolved = resolveFruchtermanReingoldOptions(options, this.current);
6081
+ return {
6082
+ mass: new Float32Array(s2.nodeCount).fill(1),
6083
+ weights: { data: null, source: "none", column: null },
6084
+ fixed: resolveFixed(s2, resolved.fixed)
6085
+ };
6086
+ }
6087
+ /**
6088
+ * The constant FR override set (PD-1): no option changes a law, so setParams never recompiles.
6089
+ * @param _options - an option record (unused: the set is constant)
6090
+ * @returns the model's own override set
6091
+ */
6092
+ overrides(_options) {
6093
+ return FR_OVERRIDES;
6094
+ }
6095
+ /**
6096
+ * The six module specs of an override set in dispatch order -- K1, K2, K3, K5, toScene, fill -- each with only the
6097
+ * override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix.
6098
+ * @param overrides - the merged override set (the model's plus USE_PERM / HAS_WEIGHTS)
6099
+ * @param _subgroups - accepted for the ForceModel interface and unused (the composer picks the twin from caps)
6100
+ * @returns the specs
6101
+ */
6102
+ specs(overrides, _subgroups) {
6103
+ return [
6104
+ kernelSpec("fa2-stats-finalize", subset(overrides, K1_DEFAULTS$1)),
6105
+ kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS$1)),
6106
+ kernelSpec("fa2-repulsion-exact", subset(overrides, K3_DEFAULTS$1)),
6107
+ kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS$1)),
6108
+ kernelSpec("fa2-to-scene"),
6109
+ kernelSpec("fill")
6110
+ ];
6111
+ }
6112
+ /**
6113
+ * Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2 (or the fill of
6114
+ * force when arcCount === 0), K3, K5, toScene; writes the FillParams { count: 3n, value: 0, mode: 0 } into the
6115
+ * model's uniform buffer. With n === 0 nothing is bound.
6116
+ * @param resources - the graph, the shared and model buffers, the ring and the cache
6117
+ * @param overrides - the merged override set
6118
+ */
6119
+ async bind(resources, overrides) {
6120
+ this.dropBound();
6121
+ this.resources = resources;
6122
+ const { n, pipelines, caps, core, perm, ring, device } = resources;
6123
+ if (n === 0) {
6124
+ return;
6125
+ }
6126
+ const k1 = await pipelines.kernel(kernelSpec("fa2-stats-finalize", subset(overrides, K1_DEFAULTS$1)));
6127
+ const k2 = await pipelines.kernel(kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS$1)));
6128
+ const k3 = await pipelines.kernel(kernelSpec("fa2-repulsion-exact", subset(overrides, K3_DEFAULTS$1)));
6129
+ const k5 = await pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS$1)));
6130
+ const toScene = await pipelines.kernel(kernelSpec("fa2-to-scene"));
6131
+ const fill = await pipelines.kernel(kernelSpec("fill"));
6132
+ if (this.resources !== resources) {
6133
+ return;
6134
+ }
6135
+ const pos = resources.buffer("positions");
6136
+ const scene = resources.buffer("scenePositions");
6137
+ const fixed = resources.buffer("fixed");
6138
+ const partials = resources.buffer("partials");
6139
+ const state = resources.buffer("state");
6140
+ const trace = resources.buffer("trace");
6141
+ const force = resources.buffer("force");
6142
+ const oldForce = resources.buffer("oldForce");
6143
+ const fillParamsBuffer = resources.buffer(FILL_PARAMS_BUFFER);
6144
+ const params = ring.binding(FA2_PARAMS);
6145
+ const fillParams = {
6146
+ buffer: fillParamsBuffer.buffer,
6147
+ offset: fillParamsBuffer.offset,
6148
+ size: FILL_PARAMS.byteLength,
6149
+ window: null
6150
+ };
6151
+ const fillBytes = new ArrayBuffer(FILL_PARAMS.byteLength);
6152
+ FILL_PARAMS.write(new DataView(fillBytes), { count: 3 * n, value: 0, mode: 0 });
6153
+ device.queue.writeBuffer(fillParamsBuffer.buffer, fillParamsBuffer.offset, fillBytes);
6154
+ const hasArcs = core.colIdx !== null;
6155
+ const wg = k1.workgroupSize;
6156
+ this.bound = {
6157
+ n,
6158
+ plan: plan1d(n, wg, caps),
6159
+ fillPlan: plan1d(3 * n, wg, caps),
6160
+ k1,
6161
+ k1Bound: k1.bind({ partials, S: state, T: trace, P: params }),
6162
+ k2,
6163
+ k2Bound: hasArcs ? k2.bind({ ...graphBindings(core, perm, resources.weights), pos, force, P: params }) : null,
6164
+ k3,
6165
+ k3Bound: k3.bind({ pos, S: state, force, oldForce, fixedMask: fixed, partials, P: params }),
6166
+ k5,
6167
+ k5Bound: k5.bind({ force, oldForce, fixedMask: fixed, S: state, pos, partials, P: params }),
6168
+ toScene,
6169
+ toSceneBound: toScene.bind({ pos, scene, P: params }),
6170
+ fill,
6171
+ fillForceBound: hasArcs ? null : fill.bind({ dst: force, P: fillParams })
6172
+ };
6173
+ }
6174
+ /**
6175
+ * The Fa2Params values of one iteration: the FA2 fields at their neutral values (scalingRatio, gravity and
6176
+ * jitterTolerance 0), `frK` = k or 1 / sqrt(n), and this iteration's temperature (PD-5 anchors here). Under
6177
+ * `cooling: "adaptive"` the flags carry FA2_FLAG_ADAPTIVE and the kernels take the temperature from the state
6178
+ * block instead (K1 updates it, K5 reads it); the uniform's value is then the start temperature and unused. The
6179
+ * simulation overwrites the shared fields with the same values and OR-s in its own flags.
6180
+ * @param iteration - the GLOBAL iteration index
6181
+ * @param options - the simulation's current option record
6182
+ * @returns the uniform values
6183
+ */
6184
+ paramsFor(iteration, options) {
6185
+ const { n } = this.requireResources();
6186
+ const resolved = resolveFruchtermanReingoldOptions(options, this.current);
6187
+ const { nearMax, extentFactor } = this.tuning;
6188
+ const adaptive = resolved.cooling === "adaptive";
6189
+ return {
6190
+ n,
6191
+ dim: resolved.dim,
6192
+ flags: adaptive ? FA2_FLAG_ADAPTIVE : 0,
6193
+ tierStart: 0,
6194
+ tierEnd: n,
6195
+ iterationIndex: iteration,
6196
+ seed: seedWord(resolved.seed),
6197
+ nearMax,
6198
+ scalingRatio: 0,
6199
+ gravity: 0,
6200
+ jitterTolerance: 0,
6201
+ scale: resolved.scale,
6202
+ center: [resolved.center[0], resolved.center[1], resolved.center[2], 0],
6203
+ settleThreshold: resolved.settleThreshold,
6204
+ extentFactor,
6205
+ gridMax: 0,
6206
+ levels: 0,
6207
+ pad: [0, 0, 0, 0],
6208
+ frK: resolved.k ?? 1 / Math.sqrt(n),
6209
+ temperature: adaptive ? FR_START_TEMPERATURE : this.temperatureAt(iteration, resolved)
6210
+ };
6211
+ }
6212
+ /**
6213
+ * Records one iteration into the batch: K1, K2 (or the fill of force when arcCount === 0), K3, K5 in the batch's
6214
+ * ONE compute pass (opened by the first call of a batch and reused by every later call with the same batch.id),
6215
+ * then toScene in a second pass that ends it, stopping after stage `upTo` when given. With n === 0 nothing is
6216
+ * recorded; a call before bind() completed is E_NOT_LOADED (never a silent no-op).
6217
+ * @param batch - the batch being recorded
6218
+ * @param slot - the UniformRing slot holding this iteration's Fa2Params
6219
+ * @param tier - "exact" (the grid tier is E_UNSUPPORTED until P4)
6220
+ * @param upTo - a stage name to stop after; undefined records every stage including toScene
6221
+ */
6222
+ recordIteration(batch, slot, tier, upTo) {
6223
+ if (tier === "grid") {
6224
+ throw new WebGpuGraphError("E_UNSUPPORTED", "the grid repulsion tier lands in P4", {
6225
+ feature: "repulsion.grid",
6226
+ hint: 'pass repulsion: "exact"'
6227
+ });
6228
+ }
6229
+ const resources = this.requireResources();
6230
+ const stop = upTo === void 0 ? FR_STAGES.length - 1 : this.stageIndex(upTo);
6231
+ const { bound } = this;
6232
+ if (bound === null) {
6233
+ if (resources.n === 0) {
6234
+ return;
6235
+ }
6236
+ throw new WebGpuGraphError(
6237
+ "E_NOT_LOADED",
6238
+ "the Fruchterman-Reingold model is not bound (bind() has not completed)",
6239
+ { state: "loaded" }
6240
+ );
6241
+ }
6242
+ const offset = resources.ring.offsetOf(slot);
6243
+ const pass = this.openPass !== null && this.openPass.id === batch.id ? this.openPass.pass : batch.pass("fr");
6244
+ this.openPass = { id: batch.id, pass };
6245
+ bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP$1, [offset]);
6246
+ if (stop < 1) {
6247
+ return;
6248
+ }
6249
+ if (bound.k2Bound !== null) {
6250
+ bound.k2.dispatch(pass, bound.k2Bound, bound.plan, [offset]);
6251
+ } else if (bound.fillForceBound !== null) {
6252
+ bound.fill.dispatch(pass, bound.fillForceBound, bound.fillPlan, [0]);
6253
+ }
6254
+ if (stop < 2) {
6255
+ return;
6256
+ }
6257
+ bound.k3.dispatch(pass, bound.k3Bound, bound.plan, [offset]);
6258
+ if (stop < 3) {
6259
+ return;
6260
+ }
6261
+ bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
6262
+ if (stop < 4) {
6263
+ return;
6264
+ }
6265
+ this.openPass = null;
6266
+ const scenePass = batch.pass("fa2-to-scene");
6267
+ bound.toScene.dispatch(scenePass, bound.toSceneBound, bound.plan, [offset]);
6268
+ }
6269
+ /**
6270
+ * temperature = 0.1 (what stats reads before the first batch lands), kineticEnergy = 0; the temperature index
6271
+ * restarts at 0 (PD-5).
6272
+ * @param state - the state writer of the simulation
6273
+ */
6274
+ onLoad(state) {
6275
+ this.resetAdaptive(state);
6276
+ state.set("kineticEnergy", 0);
6277
+ this.tempOrigin = 0;
6278
+ this.pendingReheat = false;
6279
+ }
6280
+ /**
6281
+ * temperature = 0.1, the adaptive controller's energy = +infinity (the first fold always counts as a fall) and
6282
+ * its progress counter = 0: the state K1 reads under `cooling: "adaptive"`, and what stats reads before the first
6283
+ * batch lands under either schedule.
6284
+ * @param state - the state writer of the simulation
6285
+ */
6286
+ resetAdaptive(state) {
6287
+ state.set("temperature", FR_START_TEMPERATURE);
6288
+ state.set("frEnergy", Number.POSITIVE_INFINITY);
6289
+ state.set("frProgress", 0);
6290
+ }
6291
+ /**
6292
+ * Arms the re-anchoring of the temperature index at floor(0.7 * iterations), placed by the next paramsFor (PD-5);
6293
+ * under `cooling: "adaptive"` restarts the controller at the start temperature instead.
6294
+ * @param state - the state writer of the simulation
6295
+ */
6296
+ onReheat(state) {
6297
+ this.pendingReheat = true;
6298
+ if (this.current.cooling === "adaptive") {
6299
+ this.resetAdaptive(state);
6300
+ }
6301
+ }
6302
+ /**
6303
+ * Replaces the record (a new k or budget takes effect at the next paramsFor: a new dt, a new frK; no recompile).
6304
+ * The PD-16 `fixed` check already ran in the simulation's resolve callback.
6305
+ * @param patch - the setParams patch
6306
+ * @param _state - the state writer of the simulation (nothing to write)
6307
+ */
6308
+ onSetParams(patch, _state) {
6309
+ this.current = resolveFruchtermanReingoldOptions(patch, this.current);
6310
+ }
6311
+ /**
6312
+ * Decodes the state header and the k trace records of a completed batch into FruchtermanReingoldStats: the exact
6313
+ * tier with null grid fields, msPerIteration null (the simulation owns the clock), the temperature K1 wrote.
6314
+ * @param state - a DataView over the 256-byte state header
6315
+ * @param trace - a DataView over the k Fa2Trace records of the batch
6316
+ * @returns the stats
6317
+ */
6318
+ readStats(state, trace) {
6319
+ const header = FA2_STATE.read(state);
6320
+ const centroid = vector(header, "centroid");
6321
+ const records = [];
6322
+ const count = Math.floor(trace.byteLength / TRACE_RECORD_BYTES);
6323
+ for (let i = 0; i < count; i++) {
6324
+ const record = FA2_TRACE.read(trace, i * TRACE_RECORD_BYTES);
6325
+ records.push({
6326
+ temperature: scalar(record, "modelScalar"),
6327
+ meanDisplacement: scalar(record, "meanDisplacement"),
6328
+ settledCount: scalar(record, "settledCount")
6329
+ });
6330
+ }
6331
+ return {
6332
+ iteration: scalar(header, "iteration"),
6333
+ meanDisplacement: scalar(header, "meanDisplacement"),
6334
+ rmsRadius: scalar(header, "rmsRadius"),
6335
+ layoutRadius: scalar(header, "radius"),
6336
+ centroid: [centroid[0], centroid[1], centroid[2]],
6337
+ repulsionTier: "exact",
6338
+ maxCellOccupancy: null,
6339
+ outsideGrid: null,
6340
+ msPerIteration: null,
6341
+ temperature: scalar(header, "temperature"),
6342
+ trace: records
6343
+ };
6344
+ }
6345
+ /**
6346
+ * The temperature of a global iteration (PD-5): a pending reheat re-anchors tempOrigin so this iteration's
6347
+ * temperature index is floor(0.7 * iterations); then max(0, 0.1 - dt * index), dt = 0.1 / (iterations + 1).
6348
+ * @param global - the global iteration index
6349
+ * @param resolved - the current record
6350
+ * @returns the temperature (f64; the uniform slot rounds it to f32)
6351
+ */
6352
+ temperatureAt(global, resolved) {
6353
+ if (this.pendingReheat) {
6354
+ this.tempOrigin = global - Math.floor(FR_REHEAT_FRACTION * resolved.iterations);
6355
+ this.pendingReheat = false;
6356
+ }
6357
+ const dt = FR_START_TEMPERATURE / (resolved.iterations + 1);
6358
+ return Math.max(0, FR_START_TEMPERATURE - dt * (global - this.tempOrigin));
6359
+ }
6360
+ /**
6361
+ * The resources of the last bind(), or E_NOT_LOADED before it.
6362
+ * @returns the resources
6363
+ */
6364
+ requireResources() {
6365
+ if (this.resources === null) {
6366
+ throw new WebGpuGraphError(
6367
+ "E_NOT_LOADED",
6368
+ "the Fruchterman-Reingold model has not been bound (load() first)",
6369
+ { state: "created" }
6370
+ );
6371
+ }
6372
+ return this.resources;
6373
+ }
6374
+ /**
6375
+ * The index of a stage name in FR_STAGES, or E_INVALID_ARGUMENT.
6376
+ * @param upTo - the stage name
6377
+ * @returns its index
6378
+ */
6379
+ stageIndex(upTo) {
6380
+ for (let i = 0; i < FR_STAGES.length; i++) {
6381
+ if (FR_STAGES[i] === upTo) {
6382
+ return i;
6383
+ }
6384
+ }
6385
+ throw invalid("upTo", upTo, FR_STAGES.join(" | "));
6386
+ }
6387
+ /** Drops the bind groups of the previous bind() (the buffers changed) and forgets the pass of a batch recorded before the rebind. */
6388
+ dropBound() {
6389
+ this.openPass = null;
6390
+ const { bound } = this;
6391
+ if (bound === null) {
6392
+ return;
6393
+ }
6394
+ for (const kernel of [bound.k1, bound.k2, bound.k3, bound.k5, bound.toScene, bound.fill]) {
6395
+ kernel.invalidate();
6396
+ }
6397
+ this.bound = null;
6398
+ }
6399
+ }
6400
+ function resolvePatch$1(patch, current) {
6401
+ if ("fixed" in patch) {
6402
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "setParams({ fixed }) is not a live option (spec 7.12)", {
6403
+ argument: "fixed",
6404
+ value: patch.fixed,
6405
+ expected: "absent",
6406
+ hint: "use setFixed(mask); fixed is applied at load()"
6407
+ });
6408
+ }
6409
+ return resolveFruchtermanReingoldOptions(patch, resolveFruchtermanReingoldOptions(current));
6410
+ }
6411
+ function createFruchtermanReingold(ctx, options) {
6412
+ ctx.assertReady();
6413
+ const resolved = resolveFruchtermanReingoldOptions(options);
6414
+ const tuning = resolveLayoutTuning(options);
6415
+ const model = new FruchtermanReingoldModel(tuning, resolved);
6416
+ return new ForceSimulation(
6417
+ ctx,
6418
+ model,
6419
+ resolved,
6420
+ tuning,
6421
+ resolvePatch$1
6422
+ );
6423
+ }
6424
+ const SE_STAGES = ["K1", "K2", "K3", "K5", "toScene"];
6425
+ const ONE_WORKGROUP = { x: 1, y: 1, z: 1, items: 1, stride: null };
6426
+ const SE_OVERRIDES = Object.freeze({
6427
+ LINLOG: false,
6428
+ DISTRIBUTED: false,
6429
+ TIER: 0,
6430
+ SWING_MODE: 1,
6431
+ STRONG_GRAVITY: false,
6432
+ GRAVITY_CENTER: 0,
6433
+ LAW: 2,
6434
+ APPLY: 2,
6435
+ STATS_MODE: 2
6436
+ });
6437
+ const K1_DEFAULTS = { STATS_MODE: 0 };
6438
+ const K2_DEFAULTS = {
6439
+ LINLOG: false,
6440
+ DISTRIBUTED: false,
6441
+ TIER: 0,
6442
+ USE_PERM: false,
6443
+ HAS_WEIGHTS: false,
6444
+ LAW: 0
6445
+ };
6446
+ const K3_DEFAULTS = { SWING_MODE: 0, STRONG_GRAVITY: false, GRAVITY_CENTER: 0, LAW: 0 };
6447
+ const K5_DEFAULTS = { SWING_MODE: 0, APPLY: 0 };
6448
+ const VELOCITY_BUFFER = "velocity";
6449
+ const DEFAULT_RESOLVED = Object.freeze({
6450
+ ...SE_DEFAULTS,
6451
+ gravity: null,
6452
+ springCoefficient: null,
6453
+ center: [0, 0, 0],
6454
+ seed: null
6455
+ });
6456
+ function springSizeFactor(n) {
6457
+ return Math.min(1, SE_SCALE_REFERENCE_NODES / Math.max(1, n));
6458
+ }
6459
+ function pickNullable(name, given, fallback, ok, range) {
6460
+ if (given === void 0) {
6461
+ return fallback;
6462
+ }
6463
+ if (given === null) {
6464
+ return null;
6465
+ }
6466
+ return pickNumber(name, given, 1, ok, `${range} or null`);
6467
+ }
6468
+ function resolveSpringElectricalOptions(options, previous) {
6469
+ const o = options ?? {};
6470
+ const base = previous ?? DEFAULT_RESOLVED;
6471
+ if (previous !== void 0 && o.maxInFlight !== void 0 && o.maxInFlight !== previous.maxInFlight) {
6472
+ throw new WebGpuGraphError(
6473
+ "E_INVALID_ARGUMENT",
6474
+ `maxInFlight cannot change after creation (the uniform ring is sized by it): got ${describeValue(o.maxInFlight)}, current ${previous.maxInFlight}`,
6475
+ { argument: "maxInFlight", value: o.maxInFlight, expected: previous.maxInFlight }
6476
+ );
6477
+ }
6478
+ const resolved = {
6479
+ springLength: pickNumber("springLength", o.springLength, base.springLength, (v) => v > 0, "> 0"),
6480
+ springCoefficient: pickNullable(
6481
+ "springCoefficient",
6482
+ o.springCoefficient,
6483
+ base.springCoefficient,
6484
+ (v) => v > 0,
6485
+ "> 0"
6486
+ ),
6487
+ gravity: pickNullable("gravity", o.gravity, base.gravity, () => true, "a finite number (negative repels)"),
6488
+ dragCoefficient: pickNumber("dragCoefficient", o.dragCoefficient, base.dragCoefficient, (v) => v >= 0, ">= 0"),
6489
+ timeStep: pickNumber("timeStep", o.timeStep, base.timeStep, (v) => v > 0, "> 0"),
6490
+ dim: pickDim(o.dim, base.dim),
6491
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
6492
+ center: pickCenter(o.center, base.center),
6493
+ seed: pickSeed(o.seed, base.seed),
6494
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
6495
+ settleWindow: pickNumber(
6496
+ "settleWindow",
6497
+ o.settleWindow,
6498
+ base.settleWindow,
6499
+ isPositiveInteger,
6500
+ "an integer >= 1"
6501
+ ),
6502
+ iterationsPerStep: pickNumber(
6503
+ "iterationsPerStep",
6504
+ o.iterationsPerStep,
6505
+ base.iterationsPerStep,
6506
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
6507
+ `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
6508
+ ),
6509
+ maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
6510
+ };
6511
+ return Object.freeze(resolved);
6512
+ }
6513
+ function massOf(s2) {
6514
+ const degree2 = s2.outDegree();
6515
+ const out = new Float32Array(s2.nodeCount);
6516
+ for (let i = 0; i < s2.nodeCount; i++) {
6517
+ out[i] = 1 + degree2[i] / 3;
6518
+ }
6519
+ return out;
6520
+ }
6521
+ class SpringElectricalModel {
6522
+ /**
6523
+ * Creates the model for one simulation.
6524
+ * @param tuning - the resolved GPU-only tuning
6525
+ * @param resolved - the resolved option record at creation
6526
+ */
6527
+ constructor(tuning, resolved) {
6528
+ this.kind = "springElectrical";
6529
+ this.stages = SE_STAGES;
6530
+ this.params = FA2_PARAMS;
6531
+ this.state = FA2_STATE;
6532
+ this.trace = FA2_TRACE;
6533
+ this.resources = null;
6534
+ this.bound = null;
6535
+ this.openPass = null;
6536
+ this.tuning = tuning;
6537
+ this.current = resolved;
6538
+ }
6539
+ /**
6540
+ * force 12n (zeroed), velocity 12n (zeroed: every load() starts at rest, `allocate()` / `clearKept()` honour
6541
+ * `zero`) and the 256-byte FillParams uniform buffer. n = 0 reports one node's worth of bytes so no zero-length
6542
+ * buffer is ever created.
6543
+ * @param n - the node count
6544
+ * @param _dim - the layout dimension (the arrays are stride 3 in both)
6545
+ * @returns the three model-owned buffer specs
6546
+ */
6547
+ buffers(n, _dim) {
6548
+ const bytes = Math.max(1, n) * FORCE_BYTES_PER_NODE;
6549
+ const usage = BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST;
6550
+ return [
6551
+ { name: "force", byteLength: bytes, usage, zero: true },
6552
+ { name: VELOCITY_BUFFER, byteLength: bytes, usage, zero: true },
6553
+ {
6554
+ name: FILL_PARAMS_BUFFER,
6555
+ byteLength: UNIFORM_SLOT_BYTES,
6556
+ usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
6557
+ zero: false
6558
+ }
6559
+ ];
6560
+ }
6561
+ /**
6562
+ * { mass: 1 + degree / 3, weights: none } (PD-11): the preset has no mass or weight option, and K2 compiles
6563
+ * HAS_WEIGHTS = false. No `fixed` (setFixed is the live API). The tier rule is the simulation's (load() throws
6564
+ * E_UNSUPPORTED for the grid tier before calling this).
6565
+ * @param s - the snapshot being loaded
6566
+ * @param _options - the simulation's current option record (unused: nothing in it affects the inputs)
6567
+ * @returns the per-load inputs
6568
+ */
6569
+ inputs(s2, _options) {
6570
+ return { mass: massOf(s2), weights: { data: null, source: "none", column: null } };
6571
+ }
6572
+ /**
6573
+ * The constant SE_OVERRIDES (PD-1): no option changes a law, so setParams never recompiles.
6574
+ * @param _options - an option record (unused)
6575
+ * @returns the model's own override set
6576
+ */
6577
+ overrides(_options) {
6578
+ return SE_OVERRIDES;
6579
+ }
6580
+ /**
6581
+ * The six module specs of an override set in dispatch order -- K1, K2, K3, K5, toScene, fill -- each with only the
6582
+ * override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix.
6583
+ * @param overrides - the merged override set (the model's plus USE_PERM / HAS_WEIGHTS)
6584
+ * @param _subgroups - accepted for the ForceModel interface and unused (the composer picks the twin from caps)
6585
+ * @returns the specs
6586
+ */
6587
+ specs(overrides, _subgroups) {
6588
+ return [
6589
+ kernelSpec("fa2-stats-finalize", subset(overrides, K1_DEFAULTS)),
6590
+ kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS)),
6591
+ kernelSpec("fa2-repulsion-exact", subset(overrides, K3_DEFAULTS)),
6592
+ kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS)),
6593
+ kernelSpec("fa2-to-scene"),
6594
+ kernelSpec("fill")
6595
+ ];
6596
+ }
6597
+ /**
6598
+ * Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2 (or the fill of
6599
+ * force when arcCount === 0), K3 and K5 with the velocity in their `oldForce` slot (PD-2), toScene; writes the
6600
+ * FillParams { count: 3n, value: 0, mode: 0 } into the model's uniform buffer. With n === 0 nothing is bound.
6601
+ * @param resources - the graph, the shared and model buffers, the ring and the cache
6602
+ * @param overrides - the merged override set
6603
+ */
6604
+ async bind(resources, overrides) {
6605
+ this.dropBound();
6606
+ this.resources = resources;
6607
+ const { n, pipelines, caps, core, perm, ring, device } = resources;
6608
+ if (n === 0) {
6609
+ return;
6610
+ }
6611
+ const [k1, k2, k3, k5, toScene, fill] = await Promise.all([
6612
+ pipelines.kernel(kernelSpec("fa2-stats-finalize", subset(overrides, K1_DEFAULTS))),
6613
+ pipelines.kernel(kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS))),
6614
+ pipelines.kernel(kernelSpec("fa2-repulsion-exact", subset(overrides, K3_DEFAULTS))),
6615
+ pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS))),
6616
+ pipelines.kernel(kernelSpec("fa2-to-scene")),
6617
+ pipelines.kernel(kernelSpec("fill"))
6618
+ ]);
6619
+ if (this.resources !== resources) {
6620
+ return;
6621
+ }
6622
+ const pos = resources.buffer("positions");
6623
+ const scene = resources.buffer("scenePositions");
6624
+ const fixed = resources.buffer("fixed");
6625
+ const partials = resources.buffer("partials");
6626
+ const state = resources.buffer("state");
6627
+ const trace = resources.buffer("trace");
6628
+ const force = resources.buffer("force");
6629
+ const velocity = resources.buffer(VELOCITY_BUFFER);
6630
+ const fillParamsBuffer = resources.buffer(FILL_PARAMS_BUFFER);
6631
+ const params = ring.binding(FA2_PARAMS);
6632
+ const fillParams = {
6633
+ buffer: fillParamsBuffer.buffer,
6634
+ offset: fillParamsBuffer.offset,
6635
+ size: FILL_PARAMS.byteLength,
6636
+ window: null
6637
+ };
6638
+ const fillBytes = new ArrayBuffer(FILL_PARAMS.byteLength);
6639
+ FILL_PARAMS.write(new DataView(fillBytes), { count: 3 * n, value: 0, mode: 0 });
6640
+ device.queue.writeBuffer(fillParamsBuffer.buffer, fillParamsBuffer.offset, fillBytes);
6641
+ const hasArcs = core.colIdx !== null;
6642
+ const wg = k1.workgroupSize;
6643
+ this.bound = {
6644
+ n,
6645
+ plan: plan1d(n, wg, caps),
6646
+ fillPlan: plan1d(3 * n, wg, caps),
6647
+ k1,
6648
+ k1Bound: k1.bind({ partials, S: state, T: trace, P: params }),
6649
+ k2,
6650
+ k2Bound: hasArcs ? k2.bind({ ...graphBindings(core, perm, resources.weights), pos, force, P: params }) : null,
6651
+ k3,
6652
+ k3Bound: k3.bind({ pos, S: state, force, oldForce: velocity, fixedMask: fixed, partials, P: params }),
6653
+ k5,
6654
+ k5Bound: k5.bind({ force, oldForce: velocity, fixedMask: fixed, S: state, pos, partials, P: params }),
6655
+ toScene,
6656
+ toSceneBound: toScene.bind({ pos, scene, P: params }),
6657
+ fill,
6658
+ fillForceBound: hasArcs ? null : fill.bind({ dst: force, P: fillParams })
6659
+ };
6660
+ }
6661
+ /**
6662
+ * The Fa2Params values of one iteration: the shared fields, the FA2 fields at 0 (no centre gravity, PD-12; no
6663
+ * FR temperature) and ngraph's five constants with `gravity` in `coulomb`.
6664
+ * @param iteration - the global iteration index
6665
+ * @param options - the simulation's current option record
6666
+ * @returns the uniform values
6667
+ */
6668
+ paramsFor(iteration, options) {
6669
+ const { n } = this.requireResources();
6670
+ const resolved = resolveSpringElectricalOptions(options, this.current);
6671
+ const { nearMax, extentFactor } = this.tuning;
6672
+ return {
6673
+ n,
6674
+ dim: resolved.dim,
6675
+ flags: 0,
6676
+ tierStart: 0,
6677
+ tierEnd: n,
6678
+ iterationIndex: iteration,
6679
+ seed: seedWord(resolved.seed),
6680
+ nearMax,
6681
+ scalingRatio: 0,
6682
+ gravity: 0,
6683
+ jitterTolerance: 0,
6684
+ scale: resolved.scale,
6685
+ center: [resolved.center[0], resolved.center[1], resolved.center[2], 0],
6686
+ settleThreshold: resolved.settleThreshold,
6687
+ extentFactor,
6688
+ gridMax: 0,
6689
+ levels: 0,
6690
+ pad: [0, 0, 0, 0],
6691
+ frK: 0,
6692
+ temperature: 0,
6693
+ springLength: resolved.springLength,
6694
+ springCoefficient: resolved.springCoefficient ?? SE_DEFAULTS.springCoefficient * springSizeFactor(n),
6695
+ coulomb: resolved.gravity ?? SE_DEFAULTS.gravity * springSizeFactor(n),
6696
+ dragCoefficient: resolved.dragCoefficient,
6697
+ timeStep: resolved.timeStep
6698
+ };
6699
+ }
6700
+ /**
6701
+ * Records one iteration into the batch: K1, K2 (or the fill of force when arcCount === 0), K3, K5 in the batch's
6702
+ * ONE K1-K5 compute pass, then toScene in a second pass that ends it, stopping after stage `upTo` when given.
6703
+ * With n === 0 nothing is recorded; a call before bind() completed is E_NOT_LOADED.
6704
+ * @param batch - the batch being recorded
6705
+ * @param slot - the UniformRing slot holding this iteration's Fa2Params
6706
+ * @param tier - "exact" (the grid tier is E_UNSUPPORTED; the simulation never passes "grid")
6707
+ * @param upTo - a stage name to stop after; undefined records every stage including toScene
6708
+ */
6709
+ recordIteration(batch, slot, tier, upTo) {
6710
+ if (tier === "grid") {
6711
+ throw new WebGpuGraphError("E_UNSUPPORTED", "the grid repulsion tier lands in P4", {
6712
+ feature: "repulsion.grid",
6713
+ hint: 'pass repulsion: "exact"'
6714
+ });
6715
+ }
6716
+ const resources = this.requireResources();
6717
+ const stop = upTo === void 0 ? SE_STAGES.length - 1 : this.stageIndex(upTo);
6718
+ const { bound } = this;
6719
+ if (bound === null) {
6720
+ if (resources.n === 0) {
6721
+ return;
6722
+ }
6723
+ throw new WebGpuGraphError(
6724
+ "E_NOT_LOADED",
6725
+ "the spring-electrical model is not bound (bind() has not completed)",
6726
+ { state: "loaded" }
6727
+ );
6728
+ }
6729
+ const offset = resources.ring.offsetOf(slot);
6730
+ const pass = this.openPass !== null && this.openPass.id === batch.id ? this.openPass.pass : batch.pass("se");
6731
+ this.openPass = { id: batch.id, pass };
6732
+ bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
6733
+ if (stop < 1) {
6734
+ return;
6735
+ }
6736
+ if (bound.k2Bound !== null) {
6737
+ bound.k2.dispatch(pass, bound.k2Bound, bound.plan, [offset]);
6738
+ } else if (bound.fillForceBound !== null) {
6739
+ bound.fill.dispatch(pass, bound.fillForceBound, bound.fillPlan, [0]);
6740
+ }
6741
+ if (stop < 2) {
6742
+ return;
6743
+ }
6744
+ bound.k3.dispatch(pass, bound.k3Bound, bound.plan, [offset]);
6745
+ if (stop < 3) {
6746
+ return;
6747
+ }
6748
+ bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
6749
+ if (stop < 4) {
6750
+ return;
6751
+ }
6752
+ this.openPass = null;
6753
+ const scenePass = batch.pass("se-to-scene");
6754
+ bound.toScene.dispatch(scenePass, bound.toSceneBound, bound.plan, [offset]);
6755
+ }
6756
+ /**
6757
+ * kineticEnergy = 0 and temperature = 0 in the header (the velocities start at 0 through the buffer's `zero`).
6758
+ * @param state - the state writer of the simulation
6759
+ */
6760
+ onLoad(state) {
6761
+ state.set("kineticEnergy", 0);
6762
+ state.set("temperature", 0);
6763
+ }
6764
+ /**
6765
+ * Nothing: the velocities carry on (ngraph has no reheat; a drag lands in the next batch through the override
6766
+ * list, spec 7.12).
6767
+ * @param _state - the state writer of the simulation (unused)
6768
+ */
6769
+ onReheat(_state) {
6770
+ }
6771
+ /**
6772
+ * Replaces the record with the patch applied (every option is a numeric tweak; nothing recompiles, no reset).
6773
+ * @param patch - the setParams patch
6774
+ * @param _state - the state writer of the simulation (unused)
6775
+ */
6776
+ onSetParams(patch, _state) {
6777
+ this.current = resolveSpringElectricalOptions(patch, this.current);
6778
+ }
6779
+ /**
6780
+ * Decodes the state header and the k trace records of a completed batch into SpringElectricalStats: the exact
6781
+ * tier with null grid fields; msPerIteration null (the simulation owns the clock); `kineticEnergy` is the last
6782
+ * folded value (one iteration behind the last integrate, PD-4).
6783
+ * @param state - a DataView over the 256-byte state header
6784
+ * @param trace - a DataView over the k Fa2Trace records of the batch
6785
+ * @returns the stats
6786
+ */
6787
+ readStats(state, trace) {
6788
+ const header = FA2_STATE.read(state);
6789
+ const centroid = vector(header, "centroid");
6790
+ const records = [];
6791
+ const count = Math.floor(trace.byteLength / TRACE_RECORD_BYTES);
6792
+ for (let i = 0; i < count; i++) {
6793
+ const record = FA2_TRACE.read(trace, i * TRACE_RECORD_BYTES);
6794
+ records.push({
6795
+ kineticEnergy: scalar(record, "modelScalar"),
6796
+ meanDisplacement: scalar(record, "meanDisplacement"),
6797
+ settledCount: scalar(record, "settledCount")
6798
+ });
6799
+ }
6800
+ return {
6801
+ iteration: scalar(header, "iteration"),
6802
+ meanDisplacement: scalar(header, "meanDisplacement"),
6803
+ rmsRadius: scalar(header, "rmsRadius"),
6804
+ layoutRadius: scalar(header, "radius"),
6805
+ centroid: [centroid[0], centroid[1], centroid[2]],
6806
+ repulsionTier: "exact",
6807
+ maxCellOccupancy: null,
6808
+ outsideGrid: null,
6809
+ msPerIteration: null,
6810
+ kineticEnergy: scalar(header, "kineticEnergy"),
6811
+ trace: records
6812
+ };
6813
+ }
6814
+ /**
6815
+ * The resources of the last bind(), or E_NOT_LOADED before it.
6816
+ * @returns the resources
6817
+ */
6818
+ requireResources() {
6819
+ if (this.resources === null) {
6820
+ throw new WebGpuGraphError(
6821
+ "E_NOT_LOADED",
6822
+ "the spring-electrical model has not been bound (load() first)",
6823
+ { state: "created" }
6824
+ );
6825
+ }
6826
+ return this.resources;
6827
+ }
6828
+ /**
6829
+ * The index of a stage name in SE_STAGES, or E_INVALID_ARGUMENT.
6830
+ * @param upTo - the stage name
6831
+ * @returns its index
6832
+ */
6833
+ stageIndex(upTo) {
6834
+ for (let i = 0; i < SE_STAGES.length; i++) {
6835
+ if (SE_STAGES[i] === upTo) {
6836
+ return i;
6837
+ }
6838
+ }
6839
+ throw invalid("upTo", upTo, SE_STAGES.join(" | "));
6840
+ }
6841
+ /** Drops the bind groups of the previous bind() (the buffers changed) and forgets the pass of a batch recorded before the rebind. */
6842
+ dropBound() {
6843
+ this.openPass = null;
6844
+ const { bound } = this;
6845
+ if (bound === null) {
6846
+ return;
6847
+ }
6848
+ for (const kernel of [bound.k1, bound.k2, bound.k3, bound.k5, bound.toScene, bound.fill]) {
6849
+ kernel.invalidate();
6850
+ }
6851
+ this.bound = null;
6852
+ }
6853
+ }
6854
+ function resolvePatch(patch, current) {
6855
+ return resolveSpringElectricalOptions(patch, resolveSpringElectricalOptions(current));
6856
+ }
6857
+ function createSpringElectrical(ctx, options) {
6858
+ ctx.assertReady();
6859
+ const resolved = resolveSpringElectricalOptions(options);
6860
+ const tuning = resolveLayoutTuning(options);
6861
+ const model = new SpringElectricalModel(tuning, resolved);
6862
+ return new ForceSimulation(
6863
+ ctx,
6864
+ model,
6865
+ resolved,
6866
+ tuning,
6867
+ resolvePatch
6868
+ );
6869
+ }
6870
+ function copyBetweenness(defaults) {
6871
+ const copy = { ...defaults };
6872
+ if (defaults.sources !== void 0) {
6873
+ copy.sources = Object.freeze([...defaults.sources]);
6874
+ }
6875
+ return Object.freeze(copy);
6876
+ }
6877
+ function copyAlgorithms(algorithms) {
5826
6878
  const copy = { ...algorithms };
5827
6879
  if (algorithms.betweenness !== void 0) {
5828
6880
  copy.betweenness = copyBetweenness(algorithms.betweenness);
@@ -5858,6 +6910,24 @@ function createAccelerator(ctx, options) {
5858
6910
  ctx.assertReady();
5859
6911
  return createForceAtlas2(ctx, { ...o, ...frozen.layout });
5860
6912
  },
6913
+ /**
6914
+ * The Fruchterman-Reingold simulation with this accelerator's layout tuning (spec 3.3, 7.20; P5).
6915
+ * @param o - the CPU option type (spec 9.3 FruchtermanReingoldOptions); GPU tuning keys come from `options.layout`
6916
+ * @returns a fresh simulation in state "created"
6917
+ */
6918
+ fruchtermanReingold(o) {
6919
+ ctx.assertReady();
6920
+ return createFruchtermanReingold(ctx, { ...o, ...frozen.layout });
6921
+ },
6922
+ /**
6923
+ * The spring-electrical preset with this accelerator's layout tuning (spec 3.3, 7.20; P5).
6924
+ * @param o - the CPU option type (spec 9.3 SpringElectricalOptions, ngraph's names)
6925
+ * @returns a fresh simulation in state "created"
6926
+ */
6927
+ springElectrical(o) {
6928
+ ctx.assertReady();
6929
+ return createSpringElectrical(ctx, { ...o, ...frozen.layout });
6930
+ },
5861
6931
  /**
5862
6932
  * PageRank on the device (spec 8.2; contract 3.14).
5863
6933
  * @param gs - the snapshot
@@ -5935,8 +7005,8 @@ function createAccelerator(ctx, options) {
5935
7005
  * Destroys every device buffer recorded for the snapshot (spec 4.5); delegates to ctx.release.
5936
7006
  * @param s - the snapshot the app is done with
5937
7007
  */
5938
- release(s) {
5939
- ctx.release(s);
7008
+ release(s2) {
7009
+ ctx.release(s2);
5940
7010
  },
5941
7011
  /**
5942
7012
  * Disposes the context (spec 2.8); delegates to ctx.dispose, idempotent.
@@ -5950,22 +7020,26 @@ export {
5950
7020
  A as ARC_WINDOW_ALIGN,
5951
7021
  E as EXACT_MAX_NODES,
5952
7022
  FA2_DEFAULTS,
7023
+ FR_DEFAULTS,
5953
7024
  G as GpuContext,
5954
7025
  LAYOUT_TUNING_DEFAULTS,
5955
7026
  MAX_1D_ITEMS,
5956
7027
  MAX_WORKGROUPS_PER_DIM,
5957
- g as PASSTHROUGH_FORMAT_CODES,
5958
- j as STORAGE_ALIGN,
5959
- k as WORKGROUP_SIZE,
7028
+ p as PASSTHROUGH_FORMAT_CODES,
7029
+ SE_DEFAULTS,
7030
+ q as STORAGE_ALIGN,
7031
+ r as WORKGROUP_SIZE,
5960
7032
  WebGpuGraphError,
5961
7033
  connectedComponents,
5962
7034
  createAccelerator,
5963
7035
  createForceAtlas2,
7036
+ createFruchtermanReingold,
7037
+ createSpringElectrical,
5964
7038
  degree,
5965
7039
  eigenvectorCentrality,
5966
7040
  hasErrorCode,
5967
7041
  hits,
5968
- l as isSoftwareAdapter,
7042
+ s as isSoftwareAdapter,
5969
7043
  isWebGpuGraphError,
5970
7044
  katzCentrality,
5971
7045
  pageRank,