@graphty/webgpu-graph-algorithms 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -21
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-CRbw2Wyo.js → context-BR7fx3vR.js} +47 -5
- package/dist/chunks/context-BR7fx3vR.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +15 -12
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +35 -12
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/constants.d.ts +47 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +47 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +8 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/prelude.d.ts.map +1 -1
- package/dist/src/kernel/prelude.js +4 -1
- package/dist/src/kernel/prelude.js.map +1 -1
- package/dist/src/kernels.d.ts +3 -3
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +27 -11
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/layouts/force-simulation.d.ts +3 -1
- package/dist/src/layouts/force-simulation.d.ts.map +1 -1
- package/dist/src/layouts/force-simulation.js +13 -3
- package/dist/src/layouts/force-simulation.js.map +1 -1
- package/dist/src/layouts/forceatlas2.d.ts +1 -3
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
- package/dist/src/layouts/forceatlas2.js +2 -191
- package/dist/src/layouts/forceatlas2.js.map +1 -1
- package/dist/src/layouts/fruchterman-reingold.d.ts +195 -0
- package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
- package/dist/src/layouts/fruchterman-reingold.js +579 -0
- package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
- package/dist/src/layouts/model-common.d.ts +102 -0
- package/dist/src/layouts/model-common.d.ts.map +1 -0
- package/dist/src/layouts/model-common.js +198 -0
- package/dist/src/layouts/model-common.js.map +1 -0
- package/dist/src/layouts/spring-electrical.d.ts +179 -0
- package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
- package/dist/src/layouts/spring-electrical.js +508 -0
- package/dist/src/layouts/spring-electrical.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +14 -157
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/accelerator.js +4 -5
- package/dist/src/types/accelerator.js.map +1 -1
- package/dist/src/types/algorithms.d.ts +1 -1
- package/dist/src/types/algorithms.js +1 -1
- package/dist/src/types/layout.d.ts +32 -0
- package/dist/src/types/layout.d.ts.map +1 -1
- package/dist/src/types/options.d.ts +43 -1
- package/dist/src/types/options.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +5 -2
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-attraction.wgsl.js +6 -1
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
- package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +8 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +33 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/webgpu-graph-algorithms.js +1358 -284
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +6 -3
- package/src/accelerator.ts +49 -14
- package/src/constants.ts +70 -0
- package/src/index.ts +19 -8
- package/src/kernel/prelude.ts +6 -0
- package/src/kernels.ts +27 -11
- package/src/layouts/force-simulation.ts +20 -3
- package/src/layouts/forceatlas2.ts +17 -220
- package/src/layouts/fruchterman-reingold.ts +737 -0
- package/src/layouts/model-common.ts +228 -0
- package/src/layouts/spring-electrical.ts +666 -0
- package/src/types/accelerator.ts +58 -174
- package/src/types/algorithms.ts +1 -1
- package/src/types/layout.ts +36 -0
- package/src/types/options.ts +45 -1
- package/src/wgsl/fa2-attraction.wgsl.ts +6 -1
- package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
- package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +33 -0
- 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-
|
|
2
|
-
import { A, E, G,
|
|
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
|
|
546
|
-
f = f + kick_dir(i, jj, P.dim) * (
|
|
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);
|
|
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);
|
|
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
|
-
["
|
|
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
|
-
["
|
|
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: [
|
|
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,
|
|
1550
|
+
function coreOf$3(ctx, s2) {
|
|
1479
1551
|
try {
|
|
1480
|
-
return ctx.residency.core(
|
|
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,
|
|
1564
|
+
async function degree(ctx, s2, options) {
|
|
1493
1565
|
ctx.assertReady();
|
|
1494
|
-
const n =
|
|
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,
|
|
1504
|
-
if (
|
|
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:
|
|
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,
|
|
2140
|
+
function coreOf$2(ctx, s2) {
|
|
2069
2141
|
try {
|
|
2070
|
-
return ctx.residency.core(
|
|
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
|
|
2100
|
-
if (
|
|
2101
|
-
|
|
2102
|
-
dense[labels[v]] =
|
|
2171
|
+
let k = dense[labels[v]];
|
|
2172
|
+
if (k === U32_MAX) {
|
|
2173
|
+
k = next++;
|
|
2174
|
+
dense[labels[v]] = k;
|
|
2103
2175
|
}
|
|
2104
|
-
sizes[
|
|
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
|
|
2110
|
-
built[
|
|
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,
|
|
2222
|
+
async function connectedComponents(ctx, s2, options) {
|
|
2151
2223
|
ctx.assertReady();
|
|
2152
|
-
const n =
|
|
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,
|
|
2163
|
-
if (
|
|
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(
|
|
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
|
|
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,
|
|
2655
|
+
function coreOf$1(ctx, s2, algorithm) {
|
|
2584
2656
|
try {
|
|
2585
|
-
return ctx.residency.core(
|
|
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,
|
|
2679
|
+
async function run(ctx, s2, personalization, options, algorithm) {
|
|
2608
2680
|
ctx.assertReady();
|
|
2609
|
-
const n =
|
|
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,
|
|
2630
|
-
if (
|
|
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(
|
|
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
|
|
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 <
|
|
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 +=
|
|
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,
|
|
2764
|
-
return run(ctx,
|
|
2835
|
+
function pageRank(ctx, s2, options) {
|
|
2836
|
+
return run(ctx, s2, null, options, "pageRank");
|
|
2765
2837
|
}
|
|
2766
|
-
async function personalizedPageRank(ctx,
|
|
2767
|
-
const n =
|
|
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,
|
|
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,
|
|
2884
|
+
function coreOf(ctx, s2, algorithm) {
|
|
2813
2885
|
try {
|
|
2814
|
-
return ctx.residency.core(
|
|
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,
|
|
2827
|
-
const view = ctx.residency.view(
|
|
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
|
|
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 <
|
|
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 +
|
|
2900
|
-
const previousRequest = config.alternate === null ? null : batch.readback(ring[(iterationsRun +
|
|
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 +=
|
|
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,
|
|
3002
|
+
function resolve(ctx, s2, options, algorithm) {
|
|
2931
3003
|
ctx.assertReady();
|
|
2932
|
-
const n =
|
|
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,
|
|
3062
|
+
async function eigenvectorCentrality(ctx, s2, options) {
|
|
2991
3063
|
const algorithm = "eigenvectorCentrality";
|
|
2992
|
-
const
|
|
2993
|
-
if (
|
|
2994
|
-
return empty(
|
|
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,
|
|
3068
|
+
const run2 = await runPowerIteration(ctx, r2.n, {
|
|
2997
3069
|
normMode: 2,
|
|
2998
|
-
adjacency: coreOf(ctx,
|
|
3070
|
+
adjacency: coreOf(ctx, s2, algorithm),
|
|
2999
3071
|
alternate: null,
|
|
3000
3072
|
alpha: 1,
|
|
3001
3073
|
beta: 0,
|
|
3002
3074
|
uniformP: 0,
|
|
3003
|
-
maxIterations:
|
|
3004
|
-
tolerance:
|
|
3005
|
-
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,
|
|
3082
|
+
return scoresOf(run2, r2.dest, "l2");
|
|
3011
3083
|
}
|
|
3012
|
-
async function katzCentrality(ctx,
|
|
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
|
|
3017
|
-
if (
|
|
3018
|
-
return empty(
|
|
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,
|
|
3092
|
+
const run2 = await runPowerIteration(ctx, r2.n, {
|
|
3021
3093
|
normMode: 4,
|
|
3022
|
-
adjacency: reverseOf(ctx,
|
|
3094
|
+
adjacency: reverseOf(ctx, s2),
|
|
3023
3095
|
alternate: null,
|
|
3024
3096
|
alpha,
|
|
3025
3097
|
beta,
|
|
3026
3098
|
uniformP: 1,
|
|
3027
|
-
maxIterations:
|
|
3028
|
-
tolerance:
|
|
3029
|
-
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,
|
|
3106
|
+
return scoresOf(run2, r2.dest, "l2");
|
|
3035
3107
|
}
|
|
3036
|
-
async function hits(ctx,
|
|
3108
|
+
async function hits(ctx, s2, options) {
|
|
3037
3109
|
const algorithm = "hits";
|
|
3038
|
-
const
|
|
3039
|
-
const total = 2 *
|
|
3040
|
-
if (
|
|
3041
|
-
const { scores, iterations, converged, precision } = empty(
|
|
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:
|
|
3051
|
-
tolerance:
|
|
3052
|
-
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,
|
|
3056
|
-
const reverse = reverseOf(ctx,
|
|
3057
|
-
const hubSeeded = await runPowerIteration(ctx,
|
|
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,
|
|
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(
|
|
3144
|
+
onProgress(r2.maxIterations + done, total);
|
|
3073
3145
|
}
|
|
3074
3146
|
});
|
|
3075
3147
|
return {
|
|
3076
|
-
hubs: normalised(latestOfKind(hubSeeded, authoritySeeded),
|
|
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(
|
|
3123
|
-
const n =
|
|
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((
|
|
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((
|
|
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(
|
|
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
|
|
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
|
|
3867
|
-
if (!Number.isInteger(
|
|
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
|
-
|
|
3956
|
+
k,
|
|
3872
3957
|
`an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`,
|
|
3873
|
-
`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(
|
|
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((
|
|
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
|
|
3935
|
-
await this.step(
|
|
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
|
|
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(
|
|
4170
|
-
for (let i = 0; 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 <
|
|
4178
|
-
this.model.recordIteration(batch, first + i, this.tierValue, i ===
|
|
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 +
|
|
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 +=
|
|
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
|
|
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 +
|
|
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,
|
|
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 /
|
|
4234
|
-
this.iterationsDoneValue +=
|
|
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 { ...
|
|
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
|
|
4656
|
-
sumSq +=
|
|
4657
|
-
maxSq = Math.max(maxSq,
|
|
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(
|
|
4906
|
-
const n =
|
|
4907
|
-
const degree2 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
4956
|
-
const n =
|
|
5042
|
+
function resolveNodeMass(s2, spec) {
|
|
5043
|
+
const n = s2.nodeCount;
|
|
4957
5044
|
if (spec === null || spec === void 0) {
|
|
4958
|
-
const column =
|
|
5045
|
+
const column = s2.nodes.byRole("mass");
|
|
4959
5046
|
if (column === null) {
|
|
4960
|
-
return degreePlusOne(
|
|
5047
|
+
return degreePlusOne(s2);
|
|
4961
5048
|
}
|
|
4962
|
-
return massFromColumn(
|
|
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 =
|
|
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(
|
|
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(
|
|
5087
|
+
function resolveWeights(s2, spec) {
|
|
5001
5088
|
if (spec === true) {
|
|
5002
|
-
return { data:
|
|
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 =
|
|
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 =
|
|
5038
|
-
const expanded = expandEdges(
|
|
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
|
-
|
|
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
|
|
5181
|
-
const
|
|
5182
|
-
const
|
|
5183
|
-
const
|
|
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(
|
|
5548
|
+
inputs(s2, options) {
|
|
5462
5549
|
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
5463
5550
|
const { repulsion, exactMaxNodes } = this.tuning;
|
|
5464
|
-
const n =
|
|
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(
|
|
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
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
5939
|
-
ctx.release(
|
|
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
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
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
|
-
|
|
7042
|
+
s as isSoftwareAdapter,
|
|
5969
7043
|
isWebGpuGraphError,
|
|
5970
7044
|
katzCentrality,
|
|
5971
7045
|
pageRank,
|