@holoscript/core 8.0.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +2 -2
  2. package/dist/{GLTFPipeline-M7WSSJED.cjs → GLTFPipeline-76QTLF4Z.cjs} +10 -9
  3. package/dist/{GLTFPipeline-276GE64O.js → GLTFPipeline-ES5YMBGT.js} +6 -5
  4. package/dist/HoloScriptRuntime-2DDYOPOR.js +10 -0
  5. package/dist/{HoloScriptRuntime-SXEAEU2N.cjs → HoloScriptRuntime-ZGRR5EB3.cjs} +5 -5
  6. package/dist/{chunk-JJBE3TIH.cjs → chunk-2IWC4FP7.cjs} +6 -6
  7. package/dist/{chunk-S4W6Z2MN.js → chunk-2X7NGX3M.js} +3 -3
  8. package/dist/{chunk-NGKREZ36.js → chunk-3OYVN53V.js} +505 -5
  9. package/dist/{chunk-GQT4TYKY.cjs → chunk-3OZACAFQ.cjs} +44 -19
  10. package/dist/{chunk-T2QCTZMO.js → chunk-4SDKXQ4C.js} +3 -3
  11. package/dist/{chunk-253GBQZY.cjs → chunk-5H2G27PK.cjs} +35 -35
  12. package/dist/{chunk-C2A5DUMF.cjs → chunk-5NWINHR5.cjs} +35 -35
  13. package/dist/{chunk-D54CNIAJ.cjs → chunk-7H33U5SM.cjs} +2 -2
  14. package/dist/{chunk-CXFDRBQU.cjs → chunk-7M3PNU2M.cjs} +7 -7
  15. package/dist/{chunk-WJIHFLMJ.js → chunk-ADADVMXY.js} +2 -2
  16. package/dist/{chunk-NHWSN2SD.cjs → chunk-AVJHHUC5.cjs} +17 -17
  17. package/dist/{chunk-JCLBKPMX.js → chunk-BOVPWH4H.js} +48 -4
  18. package/dist/{chunk-ZZEI7MV4.js → chunk-CCATWPXM.js} +3 -3
  19. package/dist/{chunk-NBMHU6SW.js → chunk-CMT5NMMS.js} +3 -3
  20. package/dist/{chunk-HINXWAOD.cjs → chunk-CR4FMIWL.cjs} +11 -11
  21. package/dist/{chunk-IZM5VQZD.js → chunk-D3STJ56J.js} +3 -3
  22. package/dist/{chunk-EUDQTK7N.cjs → chunk-DAKRKDZV.cjs} +27 -38
  23. package/dist/{chunk-ZBQXLH2L.js → chunk-DHO2YRIM.js} +3 -3
  24. package/dist/{chunk-RVBHAXFG.js → chunk-DXWOTOUK.js} +16 -11
  25. package/dist/{chunk-F43YAA34.js → chunk-E47Z63DF.js} +3 -3
  26. package/dist/{chunk-62FBK3B7.js → chunk-FFAOS5HQ.js} +4 -4
  27. package/dist/{chunk-UM6C3CGK.js → chunk-FS7BFHFN.js} +28 -3
  28. package/dist/{chunk-SJBGLS25.cjs → chunk-FXIO6GHT.cjs} +5 -5
  29. package/dist/{chunk-XUUVDHIF.cjs → chunk-HKEXJUWS.cjs} +48 -3
  30. package/dist/{chunk-NXZFOPVD.cjs → chunk-HNDK4IQU.cjs} +16 -16
  31. package/dist/{chunk-SMIWS6DE.cjs → chunk-IELX2OKI.js} +118 -7
  32. package/dist/{chunk-KH4CNH66.cjs → chunk-IMGDUSTT.cjs} +4 -4
  33. package/dist/{chunk-LICPB5HZ.cjs → chunk-IUDGEUDO.cjs} +10 -10
  34. package/dist/{chunk-73FELZVJ.cjs → chunk-IWC5DM2M.cjs} +11 -11
  35. package/dist/{chunk-PHVRG4I2.js → chunk-J7XZV333.js} +3 -3
  36. package/dist/{chunk-K4VVR7CN.cjs → chunk-JBNDTXXV.cjs} +15 -15
  37. package/dist/{chunk-3NFFOWGG.js → chunk-KFIXE2MB.js} +14 -5
  38. package/dist/{chunk-B4TFTDG4.js → chunk-LCEXRE5Z.js} +3 -3
  39. package/dist/{chunk-IVSZDECV.js → chunk-LHAXPADA.js} +3 -3
  40. package/dist/{chunk-7CUEMLDZ.cjs → chunk-LKBTCABN.cjs} +540 -38
  41. package/dist/{chunk-AYBXB26F.js → chunk-LOGP3JWH.js} +5 -5
  42. package/dist/{chunk-KVRSTFNH.cjs → chunk-NCWCUZNS.cjs} +8 -8
  43. package/dist/{chunk-NV4VMVXU.js → chunk-NNFA2C3Y.js} +3 -3
  44. package/dist/{chunk-HZSPDCXV.cjs → chunk-O7WLJ53D.cjs} +3 -3
  45. package/dist/{chunk-MGE5Z4GS.js → chunk-OH2FAIVU.js} +3 -3
  46. package/dist/{chunk-UMABK6SI.js → chunk-PY37LNDP.js} +3 -3
  47. package/dist/{chunk-23J755AR.js → chunk-QD32ADK6.js} +3 -3
  48. package/dist/chunk-QRPRIJ2W.js +1525 -0
  49. package/dist/{chunk-HZTDET3N.js → chunk-R24NNAEG.js} +3 -3
  50. package/dist/{chunk-W7VDOASB.cjs → chunk-RMTPN3G4.cjs} +5 -5
  51. package/dist/{chunk-6DPSFNJB.cjs → chunk-ROHLEP2W.cjs} +16 -16
  52. package/dist/{chunk-3HYYMEXI.cjs → chunk-SMRVK6BI.cjs} +17 -12
  53. package/dist/{chunk-25IQCHTO.cjs → chunk-TWUXUCEW.cjs} +35 -35
  54. package/dist/{chunk-KEFNV2WM.cjs → chunk-U7IUS5HJ.cjs} +13 -13
  55. package/dist/{chunk-3AO425MZ.cjs → chunk-UFZZ7TSN.cjs} +17 -17
  56. package/dist/chunk-UPXIY52O.cjs +1562 -0
  57. package/dist/{chunk-BE4UJCQT.js → chunk-UWJHCOH2.js} +3 -3
  58. package/dist/{chunk-7HIASF7S.cjs → chunk-XB274MW5.cjs} +10 -10
  59. package/dist/{chunk-RARB3MBR.js → chunk-YDPEHCCI.cjs} +127 -3
  60. package/dist/{chunk-KJLB4OPV.js → chunk-YN2YWTXW.js} +3 -3
  61. package/dist/{chunk-IPWSXCCN.js → chunk-YVD3CSO3.js} +3 -3
  62. package/dist/{chunk-OR4Q2BGL.cjs → chunk-YWOBW2PV.cjs} +15 -15
  63. package/dist/{chunk-JX7YL6ZE.js → chunk-ZPYP4RBA.js} +4 -4
  64. package/dist/compiler/android-xr.cjs +5 -5
  65. package/dist/compiler/android-xr.js +3 -3
  66. package/dist/compiler/babylon.cjs +4 -4
  67. package/dist/compiler/babylon.js +3 -3
  68. package/dist/compiler/business-quest.cjs +9 -9
  69. package/dist/compiler/business-quest.js +1 -1
  70. package/dist/compiler/domain-block-utils.cjs +154 -150
  71. package/dist/compiler/domain-block-utils.js +2 -2
  72. package/dist/compiler/gltf-pipeline.cjs +8 -8
  73. package/dist/compiler/gltf-pipeline.js +1 -1
  74. package/dist/compiler/godot.cjs +4 -4
  75. package/dist/compiler/godot.js +3 -3
  76. package/dist/compiler/index.cjs +129 -581
  77. package/dist/compiler/index.d.ts +12 -0
  78. package/dist/compiler/index.js +42 -498
  79. package/dist/compiler/multi-layer.cjs +5 -5
  80. package/dist/compiler/multi-layer.js +4 -4
  81. package/dist/compiler/nodetoy.d.ts +69 -0
  82. package/dist/compiler/openxr.cjs +4 -4
  83. package/dist/compiler/openxr.js +3 -3
  84. package/dist/compiler/playcanvas.cjs +4 -4
  85. package/dist/compiler/playcanvas.js +3 -3
  86. package/dist/compiler/r3f.cjs +9 -8
  87. package/dist/compiler/r3f.js +4 -3
  88. package/dist/compiler/sdf.cjs +5 -5
  89. package/dist/compiler/sdf.js +3 -3
  90. package/dist/compiler/unity.cjs +4 -4
  91. package/dist/compiler/unity.js +3 -3
  92. package/dist/compiler/unreal.cjs +5 -5
  93. package/dist/compiler/unreal.js +3 -3
  94. package/dist/compiler/urdf.cjs +12 -12
  95. package/dist/compiler/urdf.js +3 -3
  96. package/dist/compiler/usd-physics.cjs +5 -5
  97. package/dist/compiler/usd-physics.js +2 -2
  98. package/dist/compiler/visionos.cjs +4 -4
  99. package/dist/compiler/visionos.js +3 -3
  100. package/dist/compiler/vrchat.cjs +5 -5
  101. package/dist/compiler/vrchat.js +3 -3
  102. package/dist/compiler/vrr.cjs +5 -5
  103. package/dist/compiler/vrr.js +3 -3
  104. package/dist/compiler/wasm.cjs +8 -8
  105. package/dist/compiler/wasm.js +3 -3
  106. package/dist/compiler/webgpu.cjs +4 -4
  107. package/dist/compiler/webgpu.js +3 -3
  108. package/dist/debugger.cjs +5 -5
  109. package/dist/debugger.js +3 -3
  110. package/dist/dist-ACXG6IH5.cjs +3113 -0
  111. package/dist/dist-VD4L6V7Q.js +3076 -0
  112. package/dist/index.cjs +1894 -559
  113. package/dist/index.d.ts +2 -0
  114. package/dist/index.js +1448 -113
  115. package/dist/reconstruction/index.cjs +31 -31
  116. package/dist/reconstruction/index.js +2 -2
  117. package/dist/runtime.cjs +150 -293
  118. package/dist/runtime.js +82 -23
  119. package/dist/src-4ROC227P.cjs +2094 -0
  120. package/dist/src-HCCCSYP7.js +2073 -0
  121. package/dist/traits/botanical-lotus.cjs +162 -10
  122. package/dist/traits/botanical-lotus.d.ts +330 -0
  123. package/dist/traits/botanical-lotus.js +4 -1
  124. package/dist/traits/index.cjs +610 -455
  125. package/dist/traits/index.js +9 -5
  126. package/package.json +23 -23
  127. package/dist/HoloScriptRuntime-3URVTKPJ.js +0 -10
  128. package/dist/chunk-VECQYRXL.js +0 -474
  129. package/dist/chunk-WDT3JTGM.cjs +0 -483
@@ -0,0 +1,3113 @@
1
+ 'use strict';
2
+
3
+ var chunkHCCQLAA4_cjs = require('./chunk-HCCQLAA4.cjs');
4
+
5
+ // ../snn-webgpu/dist/index.js
6
+ var __require2 = /* @__PURE__ */ ((x) => typeof chunkHCCQLAA4_cjs.__require !== "undefined" ? chunkHCCQLAA4_cjs.__require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof chunkHCCQLAA4_cjs.__require !== "undefined" ? chunkHCCQLAA4_cjs.__require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof chunkHCCQLAA4_cjs.__require !== "undefined") return chunkHCCQLAA4_cjs.__require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+ var _bootstrapAttempted = false;
13
+ var _bootstrapResult = false;
14
+ function ensureNodeWebGpuSync() {
15
+ if (typeof globalThis.window !== "undefined") return false;
16
+ if (_bootstrapAttempted) return _bootstrapResult;
17
+ _bootstrapAttempted = true;
18
+ try {
19
+ const mod = __require2("webgpu");
20
+ const gpu = typeof mod.create === "function" ? mod.create([]) : void 0;
21
+ if (gpu && typeof gpu.requestAdapter === "function") {
22
+ const g = globalThis;
23
+ g.navigator ?? (g.navigator = {});
24
+ g.navigator.gpu = gpu;
25
+ installMissingWebGpuGlobals(mod.globals ?? {});
26
+ _bootstrapResult = true;
27
+ return true;
28
+ }
29
+ } catch {
30
+ }
31
+ return _bootstrapResult;
32
+ }
33
+ async function ensureNodeWebGpu() {
34
+ if (typeof globalThis.window !== "undefined") return false;
35
+ if (_bootstrapAttempted) return _bootstrapResult;
36
+ if (ensureNodeWebGpuSync()) return true;
37
+ try {
38
+ const mod = await import('webgpu');
39
+ const gpu = typeof mod.create === "function" ? mod.create([]) : void 0;
40
+ if (gpu && typeof gpu.requestAdapter === "function") {
41
+ const g = globalThis;
42
+ g.navigator ?? (g.navigator = {});
43
+ g.navigator.gpu = gpu;
44
+ installMissingWebGpuGlobals(mod.globals ?? {});
45
+ _bootstrapResult = true;
46
+ return true;
47
+ }
48
+ } catch {
49
+ }
50
+ return false;
51
+ }
52
+ function installMissingWebGpuGlobals(globals) {
53
+ const target = globalThis;
54
+ for (const [key, value] of Object.entries(globals)) {
55
+ if (target[key] != null) continue;
56
+ Object.defineProperty(globalThis, key, {
57
+ value,
58
+ writable: true,
59
+ configurable: true
60
+ });
61
+ }
62
+ }
63
+ var EncodingMode = /* @__PURE__ */ ((EncodingMode2) => {
64
+ EncodingMode2[EncodingMode2["Rate"] = 0] = "Rate";
65
+ EncodingMode2[EncodingMode2["Temporal"] = 1] = "Temporal";
66
+ EncodingMode2[EncodingMode2["Delta"] = 2] = "Delta";
67
+ return EncodingMode2;
68
+ })(EncodingMode || {});
69
+ var DecodingMode = /* @__PURE__ */ ((DecodingMode2) => {
70
+ DecodingMode2[DecodingMode2["Rate"] = 0] = "Rate";
71
+ DecodingMode2[DecodingMode2["Temporal"] = 1] = "Temporal";
72
+ DecodingMode2[DecodingMode2["Population"] = 2] = "Population";
73
+ DecodingMode2[DecodingMode2["FirstSpike"] = 3] = "FirstSpike";
74
+ return DecodingMode2;
75
+ })(DecodingMode || {});
76
+ var DEFAULT_LIF_PARAMS = {
77
+ tau: 20,
78
+ vThreshold: -55,
79
+ vReset: -75,
80
+ vRest: -65,
81
+ dt: 1
82
+ };
83
+ var DEFAULT_ENCODE_PARAMS = {
84
+ timeWindow: 100,
85
+ encodingMode: 0,
86
+ seed: 42,
87
+ minValue: 0,
88
+ maxValue: 1,
89
+ deltaThreshold: 0.1
90
+ };
91
+ var DEFAULT_DECODE_PARAMS = {
92
+ decodingMode: 0,
93
+ populationSize: 10,
94
+ outputMin: 0,
95
+ outputMax: 1
96
+ };
97
+ function computeDispatchSize(count, workgroupSize = 256) {
98
+ return Math.ceil(count / workgroupSize);
99
+ }
100
+ var GPUContext = class {
101
+ constructor() {
102
+ this._device = null;
103
+ this._adapter = null;
104
+ this._capabilities = null;
105
+ this._destroyed = false;
106
+ }
107
+ /** The active GPUDevice. Throws if not initialized. */
108
+ get device() {
109
+ if (!this._device || this._destroyed) {
110
+ throw new Error("GPUContext not initialized. Call initialize() first.");
111
+ }
112
+ return this._device;
113
+ }
114
+ /** The active GPUAdapter. Throws if not initialized. */
115
+ get adapter() {
116
+ if (!this._adapter || this._destroyed) {
117
+ throw new Error("GPUContext not initialized. Call initialize() first.");
118
+ }
119
+ return this._adapter;
120
+ }
121
+ /** GPU capabilities detected during initialization. */
122
+ get capabilities() {
123
+ if (!this._capabilities) {
124
+ throw new Error("GPUContext not initialized. Call initialize() first.");
125
+ }
126
+ return this._capabilities;
127
+ }
128
+ /** Whether the context has been initialized. */
129
+ get isInitialized() {
130
+ return this._device !== null && !this._destroyed;
131
+ }
132
+ /**
133
+ * Initialize the WebGPU context.
134
+ * Requests an adapter and device with the specified options.
135
+ */
136
+ async initialize(options = {}) {
137
+ if (this._destroyed) {
138
+ throw new Error("GPUContext has been destroyed and cannot be reinitialized.");
139
+ }
140
+ if (this._device) {
141
+ return;
142
+ }
143
+ await ensureNodeWebGpu();
144
+ if (typeof navigator === "undefined" || !navigator.gpu) {
145
+ throw new Error(
146
+ "WebGPU is not supported in this environment. Ensure you are running in a WebGPU-capable browser or Node.js with the `webgpu` binding installed."
147
+ );
148
+ }
149
+ const powerPreference = options.powerPreference ?? "high-performance";
150
+ const adapter = await navigator.gpu.requestAdapter({
151
+ powerPreference
152
+ });
153
+ if (!adapter) {
154
+ throw new Error(
155
+ "Failed to obtain a WebGPU adapter. Check that your GPU drivers are up to date and WebGPU is enabled."
156
+ );
157
+ }
158
+ this._adapter = adapter;
159
+ const requiredLimits = {};
160
+ const maxBufferSize = options.maxBufferSize ?? 256 * 1024 * 1024;
161
+ const maxStorageBufferBindingSize = options.maxStorageBufferBindingSize ?? 128 * 1024 * 1024;
162
+ requiredLimits.maxBufferSize = Math.min(maxBufferSize, adapter.limits.maxBufferSize);
163
+ requiredLimits.maxStorageBufferBindingSize = Math.min(
164
+ maxStorageBufferBindingSize,
165
+ adapter.limits.maxStorageBufferBindingSize
166
+ );
167
+ const requiredFeatures = [...options.requiredFeatures ?? []];
168
+ if (options.enableTimestamps && adapter.features.has("timestamp-query")) {
169
+ requiredFeatures.push("timestamp-query");
170
+ }
171
+ const device = await adapter.requestDevice({
172
+ label: options.label ?? "snn-webgpu-device",
173
+ requiredFeatures,
174
+ requiredLimits
175
+ });
176
+ device.lost.then((info) => {
177
+ console.error(`[snn-webgpu] GPU device lost: ${info.message} (reason: ${info.reason})`);
178
+ this._device = null;
179
+ this._destroyed = true;
180
+ });
181
+ this._device = device;
182
+ const adapterExt = adapter;
183
+ const adapterInfo = adapterExt.info ?? (typeof adapterExt.requestAdapterInfo === "function" ? await adapterExt.requestAdapterInfo() : { vendor: "unknown", architecture: "unknown" });
184
+ this._capabilities = {
185
+ maxWorkgroupSize: [
186
+ device.limits.maxComputeWorkgroupSizeX,
187
+ device.limits.maxComputeWorkgroupSizeY,
188
+ device.limits.maxComputeWorkgroupSizeZ
189
+ ],
190
+ maxWorkgroupsPerDimension: device.limits.maxComputeWorkgroupsPerDimension,
191
+ maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
192
+ maxBufferSize: device.limits.maxBufferSize,
193
+ maxComputeInvocationsPerWorkgroup: device.limits.maxComputeInvocationsPerWorkgroup,
194
+ timestampQuerySupported: device.features.has("timestamp-query"),
195
+ vendor: adapterInfo.vendor ?? "unknown",
196
+ architecture: adapterInfo.architecture ?? "unknown"
197
+ };
198
+ }
199
+ /**
200
+ * Create a compute shader module from WGSL source code.
201
+ */
202
+ createShaderModule(code, label) {
203
+ return this.device.createShaderModule({
204
+ label: label ?? "snn-compute-shader",
205
+ code
206
+ });
207
+ }
208
+ /**
209
+ * Create a compute pipeline from a shader module and entry point.
210
+ */
211
+ createComputePipeline(module, entryPoint, layout = "auto", label) {
212
+ return this.device.createComputePipeline({
213
+ label: label ?? `compute-pipeline-${entryPoint}`,
214
+ layout,
215
+ compute: {
216
+ module,
217
+ entryPoint
218
+ }
219
+ });
220
+ }
221
+ /**
222
+ * Create a compute pipeline asynchronously.
223
+ * Avoids stalling the main/render thread during driver shader compilation.
224
+ * Prefer this over createComputePipeline for first-time pipeline creation.
225
+ */
226
+ async createComputePipelineAsync(module, entryPoint, layout = "auto", label) {
227
+ return this.device.createComputePipelineAsync({
228
+ label: label ?? `compute-pipeline-async-${entryPoint}`,
229
+ layout,
230
+ compute: {
231
+ module,
232
+ entryPoint
233
+ }
234
+ });
235
+ }
236
+ /**
237
+ * Submit a command buffer and wait for completion.
238
+ */
239
+ async submitAndWait(commandBuffer) {
240
+ this.device.queue.submit([commandBuffer]);
241
+ await this.device.queue.onSubmittedWorkDone();
242
+ }
243
+ /**
244
+ * Validate that the context can handle the given neuron count.
245
+ * Returns the maximum dispatchable neuron count.
246
+ */
247
+ validateNeuronCapacity(requestedCount, workgroupSize = 256) {
248
+ const caps = this.capabilities;
249
+ const maxWorkgroups = caps.maxWorkgroupsPerDimension;
250
+ const maxNeurons = maxWorkgroups * workgroupSize;
251
+ if (requestedCount > maxNeurons) {
252
+ console.warn(
253
+ `[snn-webgpu] Requested ${requestedCount} neurons exceeds GPU limit of ${maxNeurons}. Clamping to ${maxNeurons}.`
254
+ );
255
+ return maxNeurons;
256
+ }
257
+ return requestedCount;
258
+ }
259
+ /**
260
+ * Destroy the GPU context and release all resources.
261
+ */
262
+ destroy() {
263
+ if (this._device && !this._destroyed) {
264
+ this._device.destroy();
265
+ this._device = null;
266
+ this._adapter = null;
267
+ this._capabilities = null;
268
+ this._destroyed = true;
269
+ }
270
+ }
271
+ };
272
+ var BufferManager = class {
273
+ constructor(device) {
274
+ this.buffers = /* @__PURE__ */ new Map();
275
+ this.stagingBuffers = [];
276
+ this.device = device;
277
+ }
278
+ /**
279
+ * Create a GPU buffer and register it with the manager.
280
+ */
281
+ createBuffer(options) {
282
+ const { size, usage, label, initialData, mappedAtCreation } = options;
283
+ const alignedSize = Math.ceil(size / 4) * 4;
284
+ const buffer = this.device.createBuffer({
285
+ label: label ?? `snn-buffer-${this.buffers.size}`,
286
+ size: alignedSize,
287
+ usage,
288
+ mappedAtCreation: mappedAtCreation ?? !!initialData
289
+ });
290
+ if (initialData) {
291
+ const mapped = new Float32Array(buffer.getMappedRange());
292
+ mapped.set(
293
+ initialData instanceof Float32Array ? initialData : new Float32Array(initialData.buffer)
294
+ );
295
+ buffer.unmap();
296
+ }
297
+ const handle = {
298
+ buffer,
299
+ size: alignedSize,
300
+ label: label ?? `snn-buffer-${this.buffers.size}`
301
+ };
302
+ if (label) {
303
+ this.buffers.set(label, handle);
304
+ }
305
+ return handle;
306
+ }
307
+ /**
308
+ * Create a uniform buffer with initial data.
309
+ */
310
+ createUniformBuffer(data, label) {
311
+ const alignedSize = Math.ceil(data.byteLength / 16) * 16;
312
+ return this.createBuffer({
313
+ size: alignedSize,
314
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
315
+ label: label ?? "uniform-buffer",
316
+ initialData: new Float32Array(data)
317
+ });
318
+ }
319
+ /**
320
+ * Create a storage buffer (read/write).
321
+ */
322
+ createStorageBuffer(sizeOrData, label, _readOnly = false) {
323
+ const isData = sizeOrData instanceof Float32Array;
324
+ const size = isData ? sizeOrData.byteLength : sizeOrData;
325
+ return this.createBuffer({
326
+ size,
327
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
328
+ label: label ?? "storage-buffer",
329
+ initialData: isData ? sizeOrData : void 0,
330
+ mappedAtCreation: isData
331
+ });
332
+ }
333
+ /**
334
+ * Create a zero-initialized storage buffer.
335
+ */
336
+ createZeroBuffer(elementCount, label) {
337
+ const data = new Float32Array(elementCount);
338
+ return this.createStorageBuffer(data, label);
339
+ }
340
+ /**
341
+ * Write data to an existing buffer via queue.writeBuffer.
342
+ */
343
+ writeBuffer(handle, data, offset = 0) {
344
+ this.device.queue.writeBuffer(
345
+ handle.buffer,
346
+ offset,
347
+ data.buffer,
348
+ data.byteOffset,
349
+ data.byteLength
350
+ );
351
+ }
352
+ /**
353
+ * Async readback: copy GPU buffer contents to CPU.
354
+ * Creates a staging buffer, copies, maps, and reads.
355
+ */
356
+ async readBuffer(handle, byteOffset = 0, byteLength) {
357
+ const readSize = byteLength ?? handle.size;
358
+ const start = performance.now();
359
+ const staging = this.device.createBuffer({
360
+ label: `staging-${handle.label}`,
361
+ size: readSize,
362
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
363
+ });
364
+ this.stagingBuffers.push(staging);
365
+ const encoder = this.device.createCommandEncoder({
366
+ label: `readback-${handle.label}`
367
+ });
368
+ encoder.copyBufferToBuffer(handle.buffer, byteOffset, staging, 0, readSize);
369
+ this.device.queue.submit([encoder.finish()]);
370
+ await staging.mapAsync(GPUMapMode.READ);
371
+ const mappedRange = staging.getMappedRange();
372
+ const data = new Float32Array(mappedRange.slice(0));
373
+ staging.unmap();
374
+ const readbackTimeMs = performance.now() - start;
375
+ return { data, readbackTimeMs };
376
+ }
377
+ /**
378
+ * Get a previously created buffer by label.
379
+ */
380
+ getBuffer(label) {
381
+ return this.buffers.get(label);
382
+ }
383
+ /**
384
+ * Check if a buffer exists with the given label.
385
+ */
386
+ hasBuffer(label) {
387
+ return this.buffers.has(label);
388
+ }
389
+ /**
390
+ * Destroy a single buffer.
391
+ */
392
+ destroyBuffer(label) {
393
+ const handle = this.buffers.get(label);
394
+ if (handle) {
395
+ handle.buffer.destroy();
396
+ this.buffers.delete(label);
397
+ }
398
+ }
399
+ /**
400
+ * Destroy all managed buffers and staging buffers.
401
+ */
402
+ destroyAll() {
403
+ for (const handle of this.buffers.values()) {
404
+ handle.buffer.destroy();
405
+ }
406
+ this.buffers.clear();
407
+ for (const staging of this.stagingBuffers) {
408
+ staging.destroy();
409
+ }
410
+ this.stagingBuffers = [];
411
+ }
412
+ /**
413
+ * Get total allocated GPU memory in bytes.
414
+ */
415
+ getTotalAllocatedBytes() {
416
+ let total = 0;
417
+ for (const handle of this.buffers.values()) {
418
+ total += handle.size;
419
+ }
420
+ for (const staging of this.stagingBuffers) {
421
+ total += staging.size;
422
+ }
423
+ return total;
424
+ }
425
+ /**
426
+ * Get the number of managed buffers.
427
+ */
428
+ get bufferCount() {
429
+ return this.buffers.size;
430
+ }
431
+ };
432
+ var lif_neuron_default = "// Leaky Integrate-and-Fire (LIF) Neuron Compute Shader\n// Processes membrane potential updates for N neurons per dispatch.\n//\n// Model: V[t+1] = V[t] * decay + I_syn[t]\n// if V[t+1] >= V_thresh => spike=1, V[t+1] = V_reset\n// else spike=0\n//\n// decay = exp(-dt / tau)\n\nstruct LIFParams {\n tau: f32, // Membrane time constant (ms)\n v_threshold: f32, // Spike threshold voltage (mV)\n v_reset: f32, // Reset voltage after spike (mV)\n v_rest: f32, // Resting membrane potential (mV)\n dt: f32, // Timestep (ms)\n neuron_count: u32, // Total neuron count\n _pad0: u32,\n _pad1: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: LIFParams;\n\n// Membrane potentials: read/write per neuron\n@group(0) @binding(1) var<storage, read_write> membrane_v: array<f32>;\n\n// Synaptic input currents (computed by weight shader): read-only\n@group(0) @binding(2) var<storage, read> synaptic_input: array<f32>;\n\n// Output spike flags: 1.0 = spiked, 0.0 = no spike\n@group(0) @binding(3) var<storage, read_write> spikes: array<f32>;\n\n// Refractory period counter (timesteps remaining)\n@group(0) @binding(4) var<storage, read_write> refractory: array<f32>;\n\nconst WORKGROUP_SIZE: u32 = 256u;\nconst REFRACTORY_PERIOD: f32 = 2.0; // ms\n\n@compute @workgroup_size(256)\nfn lif_step(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.neuron_count) {\n return;\n }\n\n let i = idx;\n\n // Read current state\n var v = membrane_v[i];\n let i_syn = synaptic_input[i];\n var ref_timer = refractory[i];\n\n // Compute exponential decay factor: exp(-dt/tau)\n let decay = exp(-params.dt / params.tau);\n\n // Check refractory period\n if (ref_timer > 0.0) {\n // Neuron is in refractory period - clamp to reset\n ref_timer = ref_timer - params.dt;\n refractory[i] = max(ref_timer, 0.0);\n membrane_v[i] = params.v_reset;\n spikes[i] = 0.0;\n return;\n }\n\n // Leaky integration step\n // V(t+1) = V_rest + (V(t) - V_rest) * decay + I_syn\n v = params.v_rest + (v - params.v_rest) * decay + i_syn;\n\n // Threshold check\n if (v >= params.v_threshold) {\n // Spike!\n spikes[i] = 1.0;\n membrane_v[i] = params.v_reset;\n refractory[i] = REFRACTORY_PERIOD;\n } else {\n spikes[i] = 0.0;\n membrane_v[i] = v;\n }\n}\n\n// Variant: batch update for multiple timesteps within a single dispatch\n// Useful for offline simulation where latency matters less than throughput.\n@compute @workgroup_size(256)\nfn lif_step_batch(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.neuron_count) {\n return;\n }\n\n let i = idx;\n var v = membrane_v[i];\n let i_syn = synaptic_input[i];\n var ref_timer = refractory[i];\n let decay = exp(-params.dt / params.tau);\n\n // Process 4 sub-steps per dispatch for higher temporal resolution\n for (var step = 0u; step < 4u; step = step + 1u) {\n if (ref_timer > 0.0) {\n ref_timer = ref_timer - params.dt;\n v = params.v_reset;\n } else {\n v = params.v_rest + (v - params.v_rest) * decay + i_syn * 0.25;\n\n if (v >= params.v_threshold) {\n spikes[i] = 1.0;\n v = params.v_reset;\n ref_timer = REFRACTORY_PERIOD;\n }\n }\n }\n\n membrane_v[i] = v;\n refractory[i] = max(ref_timer, 0.0);\n}\n";
433
+ var synaptic_weights_default = "// Synaptic Weight Matrix Multiplication Compute Shader\n//\n// Computes I_syn[j] = sum_i( W[j,i] * spike[i] ) for all post-synaptic neurons j.\n// This is a sparse-friendly matrix-vector product: spike vectors are binary (0/1),\n// so we can skip zero-valued multiplications.\n//\n// Layout: W is stored row-major, where row j = weights FROM all pre-synaptic neurons TO neuron j.\n// Weight matrix is [post_count x pre_count].\n\nstruct SynapticParams {\n pre_count: u32, // Number of pre-synaptic neurons (columns)\n post_count: u32, // Number of post-synaptic neurons (rows)\n learning_rate: f32, // STDP learning rate\n _pad0: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: SynapticParams;\n\n// Weight matrix: [post_count * pre_count] row-major\n@group(0) @binding(1) var<storage, read_write> weights: array<f32>;\n\n// Pre-synaptic spikes (binary): [pre_count]\n@group(0) @binding(2) var<storage, read> pre_spikes: array<f32>;\n\n// Output: post-synaptic currents: [post_count]\n@group(0) @binding(3) var<storage, read_write> post_currents: array<f32>;\n\n// Post-synaptic spikes (for STDP): [post_count]\n@group(0) @binding(4) var<storage, read> post_spikes: array<f32>;\n\nconst TILE_SIZE: u32 = 256u;\n\n// Main synaptic current computation: I_post[j] = sum( W[j,i] * spike_pre[i] )\n@compute @workgroup_size(256)\nfn compute_synaptic_current(@builtin(global_invocation_id) gid: vec3<u32>) {\n let j = gid.x; // post-synaptic neuron index\n if (j >= params.post_count) {\n return;\n }\n\n var current: f32 = 0.0;\n let row_offset = j * params.pre_count;\n\n // Accumulate weighted pre-synaptic spikes\n for (var i = 0u; i < params.pre_count; i = i + 1u) {\n let spike = pre_spikes[i];\n // Skip if no spike (optimization for sparse spike trains)\n if (spike > 0.5) {\n current = current + weights[row_offset + i];\n }\n }\n\n post_currents[j] = current;\n}\n\n// Tiled version for better memory coalescing on large networks\n@compute @workgroup_size(256)\nfn compute_synaptic_current_tiled(\n @builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>\n) {\n let j = gid.x;\n if (j >= params.post_count) {\n return;\n }\n\n var current: f32 = 0.0;\n let row_offset = j * params.pre_count;\n\n // Process pre-synaptic neurons in tiles for cache efficiency\n let num_tiles = (params.pre_count + TILE_SIZE - 1u) / TILE_SIZE;\n\n for (var tile = 0u; tile < num_tiles; tile = tile + 1u) {\n let tile_start = tile * TILE_SIZE;\n let tile_end = min(tile_start + TILE_SIZE, params.pre_count);\n\n for (var i = tile_start; i < tile_end; i = i + 1u) {\n let spike = pre_spikes[i];\n if (spike > 0.5) {\n current = current + weights[row_offset + i];\n }\n }\n }\n\n post_currents[j] = current;\n}\n\n// STDP (Spike-Timing-Dependent Plasticity) weight update\n// dW = lr * (pre_spike * post_spike - post_spike * pre_spike * alpha)\n// Simplified Hebbian: if both pre and post spike, strengthen; else weaken slightly\n@compute @workgroup_size(256)\nfn stdp_weight_update(@builtin(global_invocation_id) gid: vec3<u32>) {\n let j = gid.x; // post-synaptic neuron\n if (j >= params.post_count) {\n return;\n }\n\n let post_spike = post_spikes[j];\n let row_offset = j * params.pre_count;\n let lr = params.learning_rate;\n\n for (var i = 0u; i < params.pre_count; i = i + 1u) {\n let pre_spike = pre_spikes[i];\n let w = weights[row_offset + i];\n\n // Hebbian: strengthen if both spike\n var dw: f32 = 0.0;\n if (pre_spike > 0.5 && post_spike > 0.5) {\n // LTP (Long-Term Potentiation)\n dw = lr * (1.0 - w); // Soft-bounded towards 1.0\n } else if (pre_spike > 0.5 && post_spike < 0.5) {\n // LTD (Long-Term Depression)\n dw = -lr * 0.5 * w; // Soft-bounded towards 0.0\n }\n\n // Clamp weights to [0, 1]\n weights[row_offset + i] = clamp(w + dw, 0.0, 1.0);\n }\n}\n";
434
+ var spike_encode_default = "// Spike Encoding Compute Shader\n//\n// Converts continuous spatial data (positions, velocities, sensor readings)\n// into spike trains using various encoding schemes:\n// 1. Rate coding: firing probability proportional to input magnitude\n// 2. Temporal coding: spike time inversely proportional to input magnitude\n// 3. Delta coding: spikes on significant changes\n//\n// Input data is normalized to [0, 1] range before encoding.\n\nstruct EncodeParams {\n data_count: u32, // Number of input data points\n time_window: u32, // Number of time bins in the encoding window\n encoding_mode: u32, // 0=rate, 1=temporal, 2=delta\n seed: u32, // RNG seed for rate coding\n min_value: f32, // Input range minimum (for normalization)\n max_value: f32, // Input range maximum (for normalization)\n delta_threshold: f32,// Threshold for delta coding\n _pad0: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: EncodeParams;\n\n// Continuous input data: [data_count]\n@group(0) @binding(1) var<storage, read> input_data: array<f32>;\n\n// Previous input data (for delta coding): [data_count]\n@group(0) @binding(2) var<storage, read_write> prev_data: array<f32>;\n\n// Output spike train: [data_count * time_window]\n// Row-major: spike_train[neuron * time_window + t]\n@group(0) @binding(3) var<storage, read_write> spike_train: array<f32>;\n\n// PCG-based pseudo-random number generator\nfn pcg_hash(input: u32) -> u32 {\n var state = input * 747796405u + 2891336453u;\n var word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n return (word >> 22u) ^ word;\n}\n\nfn random_f32(seed: u32, idx: u32, t: u32) -> f32 {\n let hash = pcg_hash(seed ^ (idx * 1973u + t * 9277u + 1u));\n return f32(hash) / 4294967295.0;\n}\n\n// Normalize input value to [0, 1]\nfn normalize(value: f32) -> f32 {\n let range = params.max_value - params.min_value;\n if (range < 0.0001) {\n return 0.5;\n }\n return clamp((value - params.min_value) / range, 0.0, 1.0);\n}\n\n// Rate coding: spike probability = normalized input value\n@compute @workgroup_size(256)\nfn encode_rate(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.data_count) {\n return;\n }\n\n let value = normalize(input_data[idx]);\n let base_offset = idx * params.time_window;\n\n // Generate spike train based on firing rate\n for (var t = 0u; t < params.time_window; t = t + 1u) {\n let r = random_f32(params.seed, idx, t);\n if (r < value) {\n spike_train[base_offset + t] = 1.0;\n } else {\n spike_train[base_offset + t] = 0.0;\n }\n }\n\n // Store for delta coding reference\n prev_data[idx] = input_data[idx];\n}\n\n// Temporal coding: earlier spike = stronger stimulus\n// Spike at time t = floor((1.0 - normalized_value) * time_window)\n@compute @workgroup_size(256)\nfn encode_temporal(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.data_count) {\n return;\n }\n\n let value = normalize(input_data[idx]);\n let base_offset = idx * params.time_window;\n\n // Clear spike train\n for (var t = 0u; t < params.time_window; t = t + 1u) {\n spike_train[base_offset + t] = 0.0;\n }\n\n // Spike time: stronger input => earlier spike\n if (value > 0.01) {\n let spike_time = u32(floor((1.0 - value) * f32(params.time_window - 1u)));\n let clamped_t = min(spike_time, params.time_window - 1u);\n spike_train[base_offset + clamped_t] = 1.0;\n }\n\n prev_data[idx] = input_data[idx];\n}\n\n// Delta coding: spike only when input changes significantly\n@compute @workgroup_size(256)\nfn encode_delta(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.data_count) {\n return;\n }\n\n let current = input_data[idx];\n let previous = prev_data[idx];\n let delta = current - previous;\n let abs_delta = abs(delta);\n\n let base_offset = idx * params.time_window;\n\n // Clear spike train\n for (var t = 0u; t < params.time_window; t = t + 1u) {\n spike_train[base_offset + t] = 0.0;\n }\n\n // Emit spike if change exceeds threshold\n if (abs_delta >= params.delta_threshold) {\n // Positive spike for increase, use first half of time window\n // Negative spike (encoded as spike in second half) for decrease\n let magnitude = normalize(abs_delta);\n let spike_time = u32(floor((1.0 - magnitude) * f32(params.time_window / 2u)));\n\n if (delta > 0.0) {\n // ON spike (positive change)\n spike_train[base_offset + min(spike_time, params.time_window / 2u - 1u)] = 1.0;\n } else {\n // OFF spike (negative change)\n let off_offset = params.time_window / 2u;\n spike_train[base_offset + off_offset + min(spike_time, params.time_window / 2u - 1u)] = 1.0;\n }\n }\n\n prev_data[idx] = current;\n}\n";
435
+ var spike_decode_default = "// Spike Decoding Compute Shader\n//\n// Converts spike trains back into continuous values for downstream use.\n// Decoding schemes:\n// 1. Rate decoding: count spikes / time_window => normalized value\n// 2. Temporal decoding: earliest spike time => value (earlier = higher)\n// 3. Population decoding: weighted sum across neuron population\n// 4. First-spike decoding: only uses the first spike's timing\n\nstruct DecodeParams {\n neuron_count: u32, // Number of neurons to decode\n time_window: u32, // Number of time bins in spike train\n decoding_mode: u32, // 0=rate, 1=temporal, 2=population, 3=first-spike\n population_size: u32, // Neurons per population (for population coding)\n output_min: f32, // Output range minimum\n output_max: f32, // Output range maximum\n _pad0: u32,\n _pad1: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: DecodeParams;\n\n// Input spike trains: [neuron_count * time_window]\n@group(0) @binding(1) var<storage, read> spike_train: array<f32>;\n\n// Decoded output values: [neuron_count] (or [neuron_count / population_size] for population)\n@group(0) @binding(2) var<storage, read_write> output_data: array<f32>;\n\n// Population tuning curves (preferred values): [neuron_count] (for population decoding)\n@group(0) @binding(3) var<storage, read> tuning_curves: array<f32>;\n\nfn denormalize(value: f32) -> f32 {\n return params.output_min + value * (params.output_max - params.output_min);\n}\n\n// Rate decoding: output = spike_count / time_window\n@compute @workgroup_size(256)\nfn decode_rate(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.neuron_count) {\n return;\n }\n\n let base_offset = idx * params.time_window;\n var spike_count: f32 = 0.0;\n\n for (var t = 0u; t < params.time_window; t = t + 1u) {\n spike_count = spike_count + spike_train[base_offset + t];\n }\n\n let rate = spike_count / f32(params.time_window);\n output_data[idx] = denormalize(rate);\n}\n\n// Temporal decoding: find earliest spike, earlier = higher value\n@compute @workgroup_size(256)\nfn decode_temporal(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.neuron_count) {\n return;\n }\n\n let base_offset = idx * params.time_window;\n var first_spike_time: i32 = -1;\n\n for (var t = 0u; t < params.time_window; t = t + 1u) {\n if (spike_train[base_offset + t] > 0.5 && first_spike_time < 0) {\n first_spike_time = i32(t);\n }\n }\n\n if (first_spike_time < 0) {\n // No spike => minimum value\n output_data[idx] = params.output_min;\n } else {\n // Earlier spike => higher value\n let normalized = 1.0 - f32(first_spike_time) / f32(params.time_window - 1u);\n output_data[idx] = denormalize(normalized);\n }\n}\n\n// Population decoding: weighted average of preferred values by firing rates\n// Groups of `population_size` neurons encode a single value\n@compute @workgroup_size(256)\nfn decode_population(@builtin(global_invocation_id) gid: vec3<u32>) {\n let pop_idx = gid.x;\n let num_populations = params.neuron_count / params.population_size;\n if (pop_idx >= num_populations) {\n return;\n }\n\n let pop_start = pop_idx * params.population_size;\n var weighted_sum: f32 = 0.0;\n var total_rate: f32 = 0.0;\n\n for (var n = 0u; n < params.population_size; n = n + 1u) {\n let neuron_idx = pop_start + n;\n let base_offset = neuron_idx * params.time_window;\n\n // Compute firing rate for this neuron\n var spike_count: f32 = 0.0;\n for (var t = 0u; t < params.time_window; t = t + 1u) {\n spike_count = spike_count + spike_train[base_offset + t];\n }\n let rate = spike_count / f32(params.time_window);\n\n // Weight by preferred value from tuning curve\n weighted_sum = weighted_sum + rate * tuning_curves[neuron_idx];\n total_rate = total_rate + rate;\n }\n\n if (total_rate > 0.001) {\n output_data[pop_idx] = denormalize(weighted_sum / total_rate);\n } else {\n // No activity => neutral value\n output_data[pop_idx] = denormalize(0.5);\n }\n}\n\n// First-spike decoding: uses only first spike across population\n// The neuron that fires first determines the decoded value (WTA - Winner Takes All)\n@compute @workgroup_size(256)\nfn decode_first_spike(@builtin(global_invocation_id) gid: vec3<u32>) {\n let pop_idx = gid.x;\n let num_populations = params.neuron_count / params.population_size;\n if (pop_idx >= num_populations) {\n return;\n }\n\n let pop_start = pop_idx * params.population_size;\n var earliest_time: u32 = params.time_window;\n var winner_neuron: u32 = 0u;\n\n for (var n = 0u; n < params.population_size; n = n + 1u) {\n let neuron_idx = pop_start + n;\n let base_offset = neuron_idx * params.time_window;\n\n for (var t = 0u; t < params.time_window; t = t + 1u) {\n if (spike_train[base_offset + t] > 0.5 && t < earliest_time) {\n earliest_time = t;\n winner_neuron = neuron_idx;\n break;\n }\n }\n }\n\n if (earliest_time < params.time_window) {\n output_data[pop_idx] = tuning_curves[winner_neuron];\n } else {\n output_data[pop_idx] = denormalize(0.5);\n }\n}\n";
436
+ var tropical_activation_default = "struct TropicalActivationConfig {\n gain: f32,\n threshold: f32,\n count: u32,\n variant: u32,\n}\n\n@group(0) @binding(0) var<storage, read> input_values: array<f32>;\n@group(0) @binding(1) var<storage, read_write> output_values: array<f32>;\n@group(0) @binding(2) var<uniform> config: TropicalActivationConfig;\n\n@compute @workgroup_size(256)\nfn tropical_activate(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= config.count) {\n return;\n }\n\n let shifted = input_values[idx] - config.threshold;\n\n if (config.variant == 0u) {\n output_values[idx] = config.gain * max(0.0, shifted);\n } else {\n output_values[idx] = config.gain * min(0.0, shifted);\n }\n}\n";
437
+ var tropical_graph_default = "struct TropicalGemmDims {\n m: u32,\n n: u32,\n k: u32,\n _pad: u32,\n}\n\nstruct TropicalSparseDims {\n rows: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n}\n\nconst INF: f32 = 1e30;\n\n@group(0) @binding(0) var<storage, read> matA: array<f32>;\n@group(0) @binding(1) var<storage, read> matB: array<f32>;\n@group(0) @binding(2) var<storage, read_write> matC: array<f32>;\n@group(0) @binding(3) var<uniform> gemmDims: TropicalGemmDims;\n\n@compute @workgroup_size(16, 16)\nfn tropical_min_plus_gemm(@builtin(global_invocation_id) gid: vec3<u32>) {\n let col = gid.x;\n let row = gid.y;\n\n if (row >= gemmDims.m || col >= gemmDims.n) {\n return;\n }\n\n var best: f32 = INF;\n for (var kk: u32 = 0u; kk < gemmDims.k; kk = kk + 1u) {\n let a = matA[row * gemmDims.k + kk];\n let b = matB[kk * gemmDims.n + col];\n if (a < INF && b < INF) {\n best = min(best, a + b);\n }\n }\n\n matC[row * gemmDims.n + col] = best;\n}\n\n@group(0) @binding(0) var<storage, read> rowPtr: array<u32>;\n@group(0) @binding(1) var<storage, read> colIdx: array<u32>;\n@group(0) @binding(2) var<storage, read> values: array<f32>;\n@group(0) @binding(3) var<storage, read> distIn: array<f32>;\n@group(0) @binding(4) var<storage, read_write> distOut: array<f32>;\n@group(0) @binding(5) var<uniform> sparseDims: TropicalSparseDims;\n\n@compute @workgroup_size(256)\nfn tropical_spmv(@builtin(global_invocation_id) gid: vec3<u32>) {\n let row = gid.x;\n if (row >= sparseDims.rows) {\n return;\n }\n\n var best: f32 = INF;\n let start = rowPtr[row];\n let finish = rowPtr[row + 1u];\n\n for (var j: u32 = start; j < finish; j = j + 1u) {\n let col = colIdx[j];\n let weight = values[j];\n let base = distIn[col];\n if (base < INF && weight < INF) {\n best = min(best, base + weight);\n }\n }\n\n distOut[row] = best;\n}\n";
438
+ var lif_large_scale_default = '// Hierarchical LIF + Shared-Memory Synaptic Current Shaders\n//\n// Paper-2 (SNN NeurIPS) \xA75.4 future work:\n// "Scaling to 10^6 neurons via hierarchical workgroup decomposition\n// and shared memory tiling."\n//\n// This shader provides two compute entry points:\n//\n// lif_step_partitioned:\n// Each thread handles NEURONS_PER_THREAD=4 neurons, reducing the\n// dispatch count by 4\xD7 compared to the baseline lif_step. For N=10^6\n// neurons: ceil(1M / (256 * 4)) = 977 workgroups vs 3907 baseline.\n//\n// synaptic_current_shared_tiled:\n// Uses var<workgroup> shared memory to cache a tile of pre-synaptic\n// spikes. All 256 threads cooperatively fill the tile, then each\n// thread accumulates against the cached spikes for its post neuron.\n// Reduces global memory reads of pre_spikes from O(N_pre \xD7 N_post)\n// to O(N_pre \xD7 N_post / TILE_SIZE) by reusing cached values.\n\n// \u2500\u2500 Shared parameter struct \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nstruct LIFParams {\n tau: f32,\n v_threshold: f32,\n v_reset: f32,\n v_rest: f32,\n dt: f32,\n neuron_count: u32,\n _pad0: u32,\n _pad1: u32,\n};\n\nstruct SynapticParams {\n pre_count: u32,\n post_count: u32,\n learning_rate: f32,\n _pad: u32,\n};\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Each thread in lif_step_partitioned handles this many neurons.\nconst NEURONS_PER_THREAD: u32 = 4u;\n\n// Workgroup size for all entry points.\nconst WORKGROUP_SIZE: u32 = 256u;\n\n// Pre-spike tile size for shared memory caching (must equal WORKGROUP_SIZE).\nconst TILE_SIZE: u32 = 256u;\n\nconst REFRACTORY_PERIOD: f32 = 2.0;\n\n// \u2500\u2500 lif_step_partitioned \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Hierarchical LIF step: each thread handles NEURONS_PER_THREAD contiguous\n// neurons. Dispatch: ceil(neuron_count / (WORKGROUP_SIZE * NEURONS_PER_THREAD))\n\n@group(0) @binding(0) var<uniform> lif_params: LIFParams;\n@group(0) @binding(1) var<storage, read_write> membrane_v: array<f32>;\n@group(0) @binding(2) var<storage, read> synaptic_input: array<f32>;\n@group(0) @binding(3) var<storage, read_write> spikes: array<f32>;\n@group(0) @binding(4) var<storage, read_write> refractory: array<f32>;\n\n@compute @workgroup_size(256)\nfn lif_step_partitioned(@builtin(global_invocation_id) gid: vec3<u32>) {\n let base_idx = gid.x * NEURONS_PER_THREAD;\n let decay = exp(-lif_params.dt / lif_params.tau);\n\n for (var offset: u32 = 0u; offset < NEURONS_PER_THREAD; offset = offset + 1u) {\n let i = base_idx + offset;\n if (i >= lif_params.neuron_count) {\n return;\n }\n\n var v = membrane_v[i];\n let i_syn = synaptic_input[i];\n var ref_timer = refractory[i];\n\n if (ref_timer > 0.0) {\n ref_timer = ref_timer - lif_params.dt;\n refractory[i] = max(ref_timer, 0.0);\n membrane_v[i] = lif_params.v_reset;\n spikes[i] = 0.0;\n continue;\n }\n\n // LIF integration: V(t+1) = V_rest + (V(t) - V_rest)*decay + I_syn\n v = lif_params.v_rest + (v - lif_params.v_rest) * decay + i_syn;\n\n if (v >= lif_params.v_threshold) {\n spikes[i] = 1.0;\n membrane_v[i] = lif_params.v_reset;\n refractory[i] = REFRACTORY_PERIOD;\n } else {\n spikes[i] = 0.0;\n membrane_v[i] = v;\n }\n }\n}\n\n// \u2500\u2500 synaptic_current_shared_tiled \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Shared-memory tiled synaptic current computation.\n//\n// Each workgroup (256 threads) cooperatively loads TILE_SIZE=256 pre-synaptic\n// spikes into workgroup shared memory, then all 256 threads accumulate\n// contributions from that tile for their own post neuron.\n//\n// Bind group layout (same as compute_synaptic_current):\n// 0 = SynapticParams (uniform)\n// 1 = weights (storage, read) [post_count \xD7 pre_count f32]\n// 2 = pre_spikes (storage, read) [pre_count f32]\n// 3 = post_currents (storage, rw) [post_count f32]\n//\n// Dispatch: ceil(post_count / WORKGROUP_SIZE)\n\n@group(0) @binding(0) var<uniform> syn_params: SynapticParams;\n@group(0) @binding(1) var<storage, read> weights: array<f32>;\n@group(0) @binding(2) var<storage, read> pre_spikes: array<f32>;\n@group(0) @binding(3) var<storage, read_write> post_currents: array<f32>;\n\n// Workgroup shared memory: one tile of pre-synaptic spikes.\n// Accessed by all threads in the workgroup simultaneously \u2014 eliminates\n// repeated global reads of the same spike values across post-neuron rows.\nvar<workgroup> shared_pre_spikes: array<f32, TILE_SIZE>;\n\n@compute @workgroup_size(256)\nfn synaptic_current_shared_tiled(\n @builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>\n) {\n let post_j = gid.x;\n var current: f32 = 0.0;\n\n let num_tiles: u32 = (syn_params.pre_count + TILE_SIZE - 1u) / TILE_SIZE;\n\n for (var tile: u32 = 0u; tile < num_tiles; tile = tile + 1u) {\n let pre_global = tile * TILE_SIZE + lid.x;\n\n // All 256 threads collaboratively fill the shared spike cache.\n // Thread lid.x loads the pre neuron at index (tile * 256 + lid.x).\n shared_pre_spikes[lid.x] = select(\n 0.0,\n pre_spikes[pre_global],\n pre_global < syn_params.pre_count\n );\n\n // Synchronise: every thread must see the fully populated tile.\n workgroupBarrier();\n\n // Each thread accumulates contributions from this tile for its post neuron.\n if (post_j < syn_params.post_count) {\n let tile_start = tile * TILE_SIZE;\n let row_offset = post_j * syn_params.pre_count;\n let tile_len: u32 = min(TILE_SIZE, syn_params.pre_count - tile_start);\n\n for (var k: u32 = 0u; k < tile_len; k = k + 1u) {\n if (shared_pre_spikes[k] > 0.5) {\n current = current + weights[row_offset + tile_start + k];\n }\n }\n }\n\n // Barrier before reusing shared memory for the next tile.\n workgroupBarrier();\n }\n\n if (post_j < syn_params.post_count) {\n post_currents[post_j] = current;\n }\n}\n';
439
+ var SHADER_SOURCES = {
440
+ lif: lif_neuron_default,
441
+ synaptic: synaptic_weights_default,
442
+ encode: spike_encode_default,
443
+ decode: spike_decode_default,
444
+ tropical: tropical_activation_default,
445
+ tropicalGraph: tropical_graph_default,
446
+ largescale: lif_large_scale_default
447
+ };
448
+ var ENTRY_POINT_CATEGORY = {
449
+ lif_step: "lif",
450
+ lif_step_batch: "lif",
451
+ compute_synaptic_current: "synaptic",
452
+ compute_synaptic_current_tiled: "synaptic",
453
+ stdp_weight_update: "synaptic",
454
+ encode_rate: "encode",
455
+ encode_temporal: "encode",
456
+ encode_delta: "encode",
457
+ decode_rate: "decode",
458
+ decode_temporal: "decode",
459
+ decode_population: "decode",
460
+ decode_first_spike: "decode",
461
+ tropical_activate: "tropical",
462
+ tropical_min_plus_gemm: "tropicalGraph",
463
+ tropical_spmv: "tropicalGraph",
464
+ lif_step_partitioned: "largescale",
465
+ synaptic_current_shared_tiled: "largescale"
466
+ };
467
+ var PipelineFactory = class {
468
+ constructor(ctx) {
469
+ this.moduleCache = /* @__PURE__ */ new Map();
470
+ this.pipelineCache = /* @__PURE__ */ new Map();
471
+ this.pendingAsync = /* @__PURE__ */ new Map();
472
+ this.ctx = ctx;
473
+ }
474
+ /**
475
+ * Get or create a shader module for a category.
476
+ */
477
+ getShaderModule(category) {
478
+ let module = this.moduleCache.get(category);
479
+ if (!module) {
480
+ const source = SHADER_SOURCES[category];
481
+ module = this.ctx.createShaderModule(source, `snn-${category}-shader`);
482
+ this.moduleCache.set(category, module);
483
+ }
484
+ return module;
485
+ }
486
+ /**
487
+ * Get or create a compute pipeline for a specific entry point.
488
+ * Uses 'auto' layout by default - bind groups must match shader declarations.
489
+ */
490
+ getPipeline(entryPoint) {
491
+ const cached = this.pipelineCache.get(entryPoint);
492
+ if (cached) {
493
+ return cached.pipeline;
494
+ }
495
+ const category = ENTRY_POINT_CATEGORY[entryPoint];
496
+ const module = this.getShaderModule(category);
497
+ const pipeline = this.ctx.createComputePipeline(
498
+ module,
499
+ entryPoint,
500
+ "auto",
501
+ `snn-pipeline-${entryPoint}`
502
+ );
503
+ this.pipelineCache.set(entryPoint, {
504
+ pipeline,
505
+ module,
506
+ category,
507
+ entryPoint
508
+ });
509
+ return pipeline;
510
+ }
511
+ /**
512
+ * Asynchronously get or create a compute pipeline for a specific entry point.
513
+ * Avoids stalling the main thread during driver shader compilation.
514
+ * Results are cached identically to getPipeline() — subsequent calls return from cache.
515
+ * Concurrent calls for the same entry point share a single in-flight compilation.
516
+ */
517
+ async getPipelineAsync(entryPoint) {
518
+ const cached = this.pipelineCache.get(entryPoint);
519
+ if (cached) {
520
+ return cached.pipeline;
521
+ }
522
+ const pending = this.pendingAsync.get(entryPoint);
523
+ if (pending) {
524
+ return pending;
525
+ }
526
+ const category = ENTRY_POINT_CATEGORY[entryPoint];
527
+ const module = this.getShaderModule(category);
528
+ const promise = this.ctx.createComputePipelineAsync(module, entryPoint, "auto", `snn-pipeline-async-${entryPoint}`).then((pipeline) => {
529
+ this.pipelineCache.set(entryPoint, { pipeline, module, category, entryPoint });
530
+ this.pendingAsync.delete(entryPoint);
531
+ return pipeline;
532
+ });
533
+ this.pendingAsync.set(entryPoint, promise);
534
+ return promise;
535
+ }
536
+ /**
537
+ * Warm up multiple pipelines in parallel using async compilation.
538
+ * Call during scene/model load to precompile shaders before they are needed.
539
+ * @param entryPoints - list of entry points to precompile; defaults to all available
540
+ */
541
+ async warmupAsync(entryPoints) {
542
+ const targets = entryPoints ?? this.getAvailableEntryPoints();
543
+ await Promise.all(targets.map((ep) => this.getPipelineAsync(ep)));
544
+ }
545
+ /**
546
+ * Retrieve compilation diagnostics (errors and warnings) for a shader module.
547
+ * Surfaces WGSL compilation errors that are otherwise silent in some browsers.
548
+ * @returns array of GPUCompilationMessage objects, or [] if not supported
549
+ */
550
+ async checkShaderCompilationErrors(category) {
551
+ const module = this.getShaderModule(category);
552
+ if (typeof module.getCompilationInfo !== "function") {
553
+ return [];
554
+ }
555
+ const info = await module.getCompilationInfo();
556
+ return Array.from(info.messages);
557
+ }
558
+ /**
559
+ * Create a bind group for a pipeline and a set of buffers.
560
+ */
561
+ createBindGroup(entryPoint, buffers, label) {
562
+ const pipeline = this.getPipeline(entryPoint);
563
+ const entries = buffers.map((buffer, index) => ({
564
+ binding: index,
565
+ resource: { buffer }
566
+ }));
567
+ return this.ctx.device.createBindGroup({
568
+ label: label ?? `bind-group-${entryPoint}`,
569
+ layout: pipeline.getBindGroupLayout(0),
570
+ entries
571
+ });
572
+ }
573
+ /**
574
+ * Create a bind group using explicit binding index entries.
575
+ * Use this overload when bindings are non-sequential (e.g., skipping unused bindings).
576
+ */
577
+ createBindGroupWithIndices(entryPoint, buffers, indices, label) {
578
+ const pipeline = this.getPipeline(entryPoint);
579
+ const entries = buffers.map((buffer, index) => ({
580
+ binding: indices[index],
581
+ resource: { buffer }
582
+ }));
583
+ return this.ctx.device.createBindGroup({
584
+ label: label ?? `bind-group-${entryPoint}`,
585
+ layout: pipeline.getBindGroupLayout(0),
586
+ entries
587
+ });
588
+ }
589
+ /**
590
+ * Encode a compute dispatch command.
591
+ */
592
+ encodeDispatch(encoder, entryPoint, bindGroup, workgroupCountX, workgroupCountY = 1, workgroupCountZ = 1) {
593
+ const pipeline = this.getPipeline(entryPoint);
594
+ const pass = encoder.beginComputePass({
595
+ label: `compute-pass-${entryPoint}`
596
+ });
597
+ pass.setPipeline(pipeline);
598
+ pass.setBindGroup(0, bindGroup);
599
+ pass.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
600
+ pass.end();
601
+ }
602
+ /**
603
+ * Get the raw WGSL source code for a shader category.
604
+ * Useful for debugging or custom modifications.
605
+ */
606
+ getShaderSource(category) {
607
+ return SHADER_SOURCES[category];
608
+ }
609
+ /**
610
+ * Get all available entry points.
611
+ */
612
+ getAvailableEntryPoints() {
613
+ return Object.keys(ENTRY_POINT_CATEGORY);
614
+ }
615
+ /**
616
+ * Clear all cached pipelines and modules.
617
+ * Call before destroying the GPU context.
618
+ */
619
+ clearCache() {
620
+ this.pipelineCache.clear();
621
+ this.moduleCache.clear();
622
+ this.pendingAsync.clear();
623
+ }
624
+ };
625
+ var LIF_PARAMS_BYTE_SIZE = 32;
626
+ function packLIFParams(params, neuronCount) {
627
+ const buffer = new ArrayBuffer(LIF_PARAMS_BYTE_SIZE);
628
+ const f32 = new Float32Array(buffer);
629
+ const u32 = new Uint32Array(buffer);
630
+ f32[0] = params.tau;
631
+ f32[1] = params.vThreshold;
632
+ f32[2] = params.vReset;
633
+ f32[3] = params.vRest;
634
+ f32[4] = params.dt;
635
+ u32[5] = neuronCount;
636
+ u32[6] = 0;
637
+ u32[7] = 0;
638
+ return buffer;
639
+ }
640
+ var LIFSimulator = class {
641
+ /**
642
+ * @param ctx - Initialized GPUContext
643
+ * @param neuronCount - Number of neurons to simulate
644
+ * @param params - LIF parameters (optional, uses defaults)
645
+ */
646
+ constructor(ctx, neuronCount, params) {
647
+ this.initialized = false;
648
+ this.stepCount = 0;
649
+ this.ctx = ctx;
650
+ this.neuronCount = ctx.validateNeuronCapacity(neuronCount);
651
+ this.params = { ...DEFAULT_LIF_PARAMS, ...params };
652
+ this.bufferManager = new BufferManager(ctx.device);
653
+ this.pipelineFactory = new PipelineFactory(ctx);
654
+ }
655
+ /**
656
+ * Initialize GPU buffers and compute pipeline.
657
+ * Must be called before step().
658
+ */
659
+ async initialize() {
660
+ if (this.initialized) return;
661
+ const n = this.neuronCount;
662
+ const paramsData = packLIFParams(this.params, n);
663
+ this.paramsBuffer = this.bufferManager.createBuffer({
664
+ size: LIF_PARAMS_BYTE_SIZE,
665
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
666
+ label: "lif-params",
667
+ initialData: new Float32Array(paramsData),
668
+ mappedAtCreation: true
669
+ });
670
+ const initMembrane = new Float32Array(n).fill(this.params.vRest);
671
+ this.membraneBuffer = this.bufferManager.createStorageBuffer(initMembrane, "membrane-v");
672
+ this.synapticInputBuffer = this.bufferManager.createZeroBuffer(n, "synaptic-input");
673
+ this.spikesBuffer = this.bufferManager.createZeroBuffer(n, "spikes");
674
+ this.refractoryBuffer = this.bufferManager.createZeroBuffer(n, "refractory");
675
+ this.bindGroup = this.pipelineFactory.createBindGroup(
676
+ "lif_step",
677
+ [
678
+ this.paramsBuffer.buffer,
679
+ this.membraneBuffer.buffer,
680
+ this.synapticInputBuffer.buffer,
681
+ this.spikesBuffer.buffer,
682
+ this.refractoryBuffer.buffer
683
+ ],
684
+ "lif-bind-group"
685
+ );
686
+ this.initialized = true;
687
+ }
688
+ /**
689
+ * Run one simulation timestep.
690
+ * Dispatches the LIF compute shader across all neurons.
691
+ */
692
+ async step() {
693
+ this.ensureInitialized();
694
+ const workgroups = computeDispatchSize(this.neuronCount);
695
+ const encoder = this.ctx.device.createCommandEncoder({
696
+ label: `lif-step-${this.stepCount}`
697
+ });
698
+ this.pipelineFactory.encodeDispatch(encoder, "lif_step", this.bindGroup, workgroups);
699
+ await this.ctx.submitAndWait(encoder.finish());
700
+ this.stepCount++;
701
+ }
702
+ /**
703
+ * Run multiple simulation timesteps.
704
+ */
705
+ async stepN(count) {
706
+ for (let i = 0; i < count; i++) {
707
+ await this.step();
708
+ }
709
+ }
710
+ /**
711
+ * Set synaptic input currents for all neurons.
712
+ */
713
+ setSynapticInput(currents) {
714
+ this.ensureInitialized();
715
+ if (currents.length !== this.neuronCount) {
716
+ throw new Error(
717
+ `Current array length (${currents.length}) must match neuron count (${this.neuronCount})`
718
+ );
719
+ }
720
+ this.bufferManager.writeBuffer(this.synapticInputBuffer, currents);
721
+ }
722
+ /**
723
+ * Update LIF parameters.
724
+ */
725
+ updateParams(params) {
726
+ this.ensureInitialized();
727
+ this.params = { ...this.params, ...params };
728
+ const packed = packLIFParams(this.params, this.neuronCount);
729
+ this.bufferManager.writeBuffer(this.paramsBuffer, new Float32Array(packed));
730
+ }
731
+ /**
732
+ * Read spike output from the GPU.
733
+ * Returns a Float32Array where 1.0 = spike, 0.0 = no spike.
734
+ */
735
+ async readSpikes() {
736
+ this.ensureInitialized();
737
+ return this.bufferManager.readBuffer(this.spikesBuffer);
738
+ }
739
+ /**
740
+ * Read membrane potentials from the GPU.
741
+ */
742
+ async readMembranePotentials() {
743
+ this.ensureInitialized();
744
+ return this.bufferManager.readBuffer(this.membraneBuffer);
745
+ }
746
+ /**
747
+ * Reset all neurons to resting state.
748
+ */
749
+ resetState() {
750
+ this.ensureInitialized();
751
+ const n = this.neuronCount;
752
+ const rest = new Float32Array(n).fill(this.params.vRest);
753
+ const zeros = new Float32Array(n);
754
+ this.bufferManager.writeBuffer(this.membraneBuffer, rest);
755
+ this.bufferManager.writeBuffer(this.synapticInputBuffer, zeros);
756
+ this.bufferManager.writeBuffer(this.spikesBuffer, zeros);
757
+ this.bufferManager.writeBuffer(this.refractoryBuffer, zeros);
758
+ this.stepCount = 0;
759
+ }
760
+ /** Get the current simulation step count. */
761
+ get currentStep() {
762
+ return this.stepCount;
763
+ }
764
+ /** Get the number of neurons. */
765
+ get count() {
766
+ return this.neuronCount;
767
+ }
768
+ /** Get current LIF parameters. */
769
+ get currentParams() {
770
+ return { ...this.params };
771
+ }
772
+ /** Get total GPU memory used in bytes. */
773
+ get gpuMemoryBytes() {
774
+ return this.bufferManager.getTotalAllocatedBytes();
775
+ }
776
+ /**
777
+ * Destroy all GPU resources.
778
+ */
779
+ destroy() {
780
+ this.pipelineFactory.clearCache();
781
+ this.bufferManager.destroyAll();
782
+ this.initialized = false;
783
+ this.stepCount = 0;
784
+ }
785
+ ensureInitialized() {
786
+ if (!this.initialized) {
787
+ throw new Error("LIFSimulator not initialized. Call initialize() first.");
788
+ }
789
+ }
790
+ };
791
+ var ENCODE_PARAMS_SIZE = 32;
792
+ var DECODE_PARAMS_SIZE = 32;
793
+ var ENCODE_ENTRY_POINTS = {
794
+ [
795
+ 0
796
+ /* Rate */
797
+ ]: "encode_rate",
798
+ [
799
+ 1
800
+ /* Temporal */
801
+ ]: "encode_temporal",
802
+ [
803
+ 2
804
+ /* Delta */
805
+ ]: "encode_delta"
806
+ };
807
+ var DECODE_ENTRY_POINTS = {
808
+ [
809
+ 0
810
+ /* Rate */
811
+ ]: "decode_rate",
812
+ [
813
+ 1
814
+ /* Temporal */
815
+ ]: "decode_temporal",
816
+ [
817
+ 2
818
+ /* Population */
819
+ ]: "decode_population",
820
+ [
821
+ 3
822
+ /* FirstSpike */
823
+ ]: "decode_first_spike"
824
+ };
825
+ function packEncodeParams(params) {
826
+ const buf = new ArrayBuffer(ENCODE_PARAMS_SIZE);
827
+ const u32 = new Uint32Array(buf);
828
+ const f32 = new Float32Array(buf);
829
+ u32[0] = params.dataCount;
830
+ u32[1] = params.timeWindow;
831
+ u32[2] = params.encodingMode;
832
+ u32[3] = params.seed;
833
+ f32[4] = params.minValue;
834
+ f32[5] = params.maxValue;
835
+ f32[6] = params.deltaThreshold;
836
+ u32[7] = 0;
837
+ return buf;
838
+ }
839
+ function packDecodeParams(params) {
840
+ const buf = new ArrayBuffer(DECODE_PARAMS_SIZE);
841
+ const u32 = new Uint32Array(buf);
842
+ const f32 = new Float32Array(buf);
843
+ u32[0] = params.neuronCount;
844
+ u32[1] = params.timeWindow;
845
+ u32[2] = params.decodingMode;
846
+ u32[3] = params.populationSize;
847
+ f32[4] = params.outputMin;
848
+ f32[5] = params.outputMax;
849
+ u32[6] = 0;
850
+ u32[7] = 0;
851
+ return buf;
852
+ }
853
+ var SpikeEncoder = class {
854
+ constructor(ctx, params) {
855
+ this.initialized = false;
856
+ this.ctx = ctx;
857
+ this.params = { ...DEFAULT_ENCODE_PARAMS, ...params };
858
+ this.bufferManager = new BufferManager(ctx.device);
859
+ this.pipelineFactory = new PipelineFactory(ctx);
860
+ }
861
+ async initialize() {
862
+ if (this.initialized) return;
863
+ const { dataCount, timeWindow } = this.params;
864
+ const packedParams = packEncodeParams(this.params);
865
+ this.paramsBuffer = this.bufferManager.createBuffer({
866
+ size: ENCODE_PARAMS_SIZE,
867
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
868
+ label: "encode-params",
869
+ initialData: new Float32Array(packedParams)
870
+ });
871
+ this.inputBuffer = this.bufferManager.createZeroBuffer(dataCount, "encode-input");
872
+ this.prevDataBuffer = this.bufferManager.createZeroBuffer(dataCount, "encode-prev");
873
+ const spikeTrainSize = dataCount * timeWindow;
874
+ this.spikeTrainBuffer = this.bufferManager.createZeroBuffer(spikeTrainSize, "spike-train");
875
+ const entryPoint = ENCODE_ENTRY_POINTS[this.params.encodingMode];
876
+ this.bindGroup = this.pipelineFactory.createBindGroup(
877
+ entryPoint,
878
+ [
879
+ this.paramsBuffer.buffer,
880
+ this.inputBuffer.buffer,
881
+ this.prevDataBuffer.buffer,
882
+ this.spikeTrainBuffer.buffer
883
+ ],
884
+ "encode-bind-group"
885
+ );
886
+ this.initialized = true;
887
+ }
888
+ /**
889
+ * Encode input data into spike trains.
890
+ */
891
+ async encode(data) {
892
+ this.ensureInitialized();
893
+ if (data.length !== this.params.dataCount) {
894
+ throw new Error(
895
+ `Input data length (${data.length}) must match dataCount (${this.params.dataCount})`
896
+ );
897
+ }
898
+ this.bufferManager.writeBuffer(this.inputBuffer, data);
899
+ this.params.seed = this.params.seed + 1 | 0;
900
+ const packedParams = packEncodeParams(this.params);
901
+ this.bufferManager.writeBuffer(this.paramsBuffer, new Float32Array(packedParams));
902
+ const entryPoint = ENCODE_ENTRY_POINTS[this.params.encodingMode];
903
+ const workgroups = computeDispatchSize(this.params.dataCount);
904
+ const encoder = this.ctx.device.createCommandEncoder({ label: "encode-dispatch" });
905
+ this.pipelineFactory.encodeDispatch(encoder, entryPoint, this.bindGroup, workgroups);
906
+ await this.ctx.submitAndWait(encoder.finish());
907
+ }
908
+ /**
909
+ * Read the generated spike trains back to CPU.
910
+ * Returns [dataCount * timeWindow] float array.
911
+ */
912
+ async readSpikeTrains() {
913
+ this.ensureInitialized();
914
+ return this.bufferManager.readBuffer(this.spikeTrainBuffer);
915
+ }
916
+ /** Get the spike train GPU buffer handle (for chaining to decoders or networks). */
917
+ get spikeTrainHandle() {
918
+ this.ensureInitialized();
919
+ return this.spikeTrainBuffer;
920
+ }
921
+ destroy() {
922
+ this.pipelineFactory.clearCache();
923
+ this.bufferManager.destroyAll();
924
+ this.initialized = false;
925
+ }
926
+ ensureInitialized() {
927
+ if (!this.initialized) {
928
+ throw new Error("SpikeEncoder not initialized. Call initialize() first.");
929
+ }
930
+ }
931
+ };
932
+ var SpikeDecoder = class {
933
+ constructor(ctx, params) {
934
+ this.initialized = false;
935
+ this.ownsSpikeTrain = false;
936
+ this.ctx = ctx;
937
+ this.params = { ...DEFAULT_DECODE_PARAMS, ...params };
938
+ this.bufferManager = new BufferManager(ctx.device);
939
+ this.pipelineFactory = new PipelineFactory(ctx);
940
+ }
941
+ async initialize(externalSpikeTrainBuffer) {
942
+ if (this.initialized) return;
943
+ const { neuronCount, timeWindow, populationSize, decodingMode } = this.params;
944
+ const packedParams = packDecodeParams(this.params);
945
+ this.paramsBuffer = this.bufferManager.createBuffer({
946
+ size: DECODE_PARAMS_SIZE,
947
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
948
+ label: "decode-params",
949
+ initialData: new Float32Array(packedParams)
950
+ });
951
+ if (externalSpikeTrainBuffer) {
952
+ this.spikeTrainBuffer = externalSpikeTrainBuffer;
953
+ this.ownsSpikeTrain = false;
954
+ } else {
955
+ const spikeTrainSize = neuronCount * timeWindow;
956
+ this.spikeTrainBuffer = this.bufferManager.createZeroBuffer(
957
+ spikeTrainSize,
958
+ "decode-spike-train"
959
+ );
960
+ this.ownsSpikeTrain = true;
961
+ }
962
+ const outputCount = decodingMode === 2 || decodingMode === 3 ? Math.ceil(neuronCount / populationSize) : neuronCount;
963
+ this.outputBuffer = this.bufferManager.createZeroBuffer(outputCount, "decode-output");
964
+ const tuningData = new Float32Array(neuronCount);
965
+ for (let i = 0; i < neuronCount; i++) {
966
+ const popIndex = i % populationSize;
967
+ tuningData[i] = popIndex / (populationSize - 1 || 1);
968
+ }
969
+ this.tuningCurveBuffer = this.bufferManager.createStorageBuffer(tuningData, "tuning-curves");
970
+ const entryPoint = DECODE_ENTRY_POINTS[decodingMode];
971
+ const needsTuningCurves = decodingMode === 2 || decodingMode === 3;
972
+ this.bindGroup = this.pipelineFactory.createBindGroup(
973
+ entryPoint,
974
+ needsTuningCurves ? [
975
+ this.paramsBuffer.buffer,
976
+ this.spikeTrainBuffer.buffer,
977
+ this.outputBuffer.buffer,
978
+ this.tuningCurveBuffer.buffer
979
+ ] : [this.paramsBuffer.buffer, this.spikeTrainBuffer.buffer, this.outputBuffer.buffer],
980
+ "decode-bind-group"
981
+ );
982
+ this.initialized = true;
983
+ }
984
+ /**
985
+ * Decode spike trains into continuous values.
986
+ * If spikeTrainData is provided, uploads it first; otherwise uses the buffer as-is.
987
+ */
988
+ async decode(spikeTrainData) {
989
+ this.ensureInitialized();
990
+ if (spikeTrainData) {
991
+ this.bufferManager.writeBuffer(this.spikeTrainBuffer, spikeTrainData);
992
+ }
993
+ const entryPoint = DECODE_ENTRY_POINTS[this.params.decodingMode];
994
+ const dispatchCount = this.params.decodingMode === 2 || this.params.decodingMode === 3 ? Math.ceil(this.params.neuronCount / this.params.populationSize) : this.params.neuronCount;
995
+ const workgroups = computeDispatchSize(dispatchCount);
996
+ const encoder = this.ctx.device.createCommandEncoder({ label: "decode-dispatch" });
997
+ this.pipelineFactory.encodeDispatch(encoder, entryPoint, this.bindGroup, workgroups);
998
+ await this.ctx.submitAndWait(encoder.finish());
999
+ }
1000
+ /**
1001
+ * Read decoded output values from GPU.
1002
+ */
1003
+ async readOutput() {
1004
+ this.ensureInitialized();
1005
+ return this.bufferManager.readBuffer(this.outputBuffer);
1006
+ }
1007
+ /**
1008
+ * Set custom tuning curves for population decoding.
1009
+ */
1010
+ setTuningCurves(curves) {
1011
+ this.ensureInitialized();
1012
+ this.bufferManager.writeBuffer(this.tuningCurveBuffer, curves);
1013
+ }
1014
+ destroy() {
1015
+ this.pipelineFactory.clearCache();
1016
+ this.bufferManager.destroyAll();
1017
+ this.initialized = false;
1018
+ }
1019
+ ensureInitialized() {
1020
+ if (!this.initialized) {
1021
+ throw new Error("SpikeDecoder not initialized. Call initialize() first.");
1022
+ }
1023
+ }
1024
+ };
1025
+ var DEFAULT_COLORS = {
1026
+ petal_base: "#fff1f6",
1027
+ petal_mid: "#f47ab7",
1028
+ petal_inner: "#ff9ecf",
1029
+ petal_rim: "#c42a86",
1030
+ petal_shadow: "#84205f",
1031
+ seed_pod: "#f4d74a",
1032
+ seed_pod_rim: "#b7c66b",
1033
+ stamen: "#f59e0b",
1034
+ stamen_tip: "#fff4bd",
1035
+ leaf: "#235f4f",
1036
+ leaf_dark: "#102f28",
1037
+ water: "#07140f"
1038
+ };
1039
+ var REQUIRED_CONFIDENCE_REGIONS = [
1040
+ "petal_mid",
1041
+ "petal_rim",
1042
+ "petal_shadow",
1043
+ "vein",
1044
+ "leaf",
1045
+ "water"
1046
+ ];
1047
+ function clamp(value, min, max) {
1048
+ return Math.min(max, Math.max(min, value));
1049
+ }
1050
+ function round(value, digits = 3) {
1051
+ const scale = 10 ** digits;
1052
+ return Math.round(value * scale) / scale;
1053
+ }
1054
+ function parseHexColor(hex) {
1055
+ const clean = hex.trim().replace(/^#/, "");
1056
+ if (!/^[0-9a-fA-F]{6}$/.test(clean)) {
1057
+ throw new Error(`Invalid color fixture: ${hex}`);
1058
+ }
1059
+ return [
1060
+ Number.parseInt(clean.slice(0, 2), 16),
1061
+ Number.parseInt(clean.slice(2, 4), 16),
1062
+ Number.parseInt(clean.slice(4, 6), 16)
1063
+ ];
1064
+ }
1065
+ function parseColor(input) {
1066
+ if (typeof input === "string") return parseHexColor(input);
1067
+ return [
1068
+ clamp(Math.round(input[0]), 0, 255),
1069
+ clamp(Math.round(input[1]), 0, 255),
1070
+ clamp(Math.round(input[2]), 0, 255)
1071
+ ];
1072
+ }
1073
+ function collectRegionPixels(fixtures, region) {
1074
+ const pixels = [];
1075
+ for (const fixture of fixtures) {
1076
+ for (const sample of fixture.regions) {
1077
+ if (sample.region !== region) continue;
1078
+ const weight = Math.max(1, Math.round(sample.weight ?? 1));
1079
+ for (const color of sample.pixels) {
1080
+ const parsed = parseColor(color);
1081
+ for (let i = 0; i < weight; i += 1) pixels.push(parsed);
1082
+ }
1083
+ }
1084
+ }
1085
+ return pixels;
1086
+ }
1087
+ function meanRgb(pixels, fallbackHex) {
1088
+ if (pixels.length === 0) return parseHexColor(fallbackHex);
1089
+ const sum = pixels.reduce(
1090
+ (acc, rgb) => [acc[0] + rgb[0], acc[1] + rgb[1], acc[2] + rgb[2]],
1091
+ [0, 0, 0]
1092
+ );
1093
+ return [
1094
+ Math.round(sum[0] / pixels.length),
1095
+ Math.round(sum[1] / pixels.length),
1096
+ Math.round(sum[2] / pixels.length)
1097
+ ];
1098
+ }
1099
+ function regionMean(fixtures, region, fallbackHex) {
1100
+ return meanRgb(collectRegionPixels(fixtures, region), fallbackHex);
1101
+ }
1102
+ function toHex(rgb) {
1103
+ return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
1104
+ }
1105
+ function toUnitRgb(rgb) {
1106
+ return [round(rgb[0] / 255), round(rgb[1] / 255), round(rgb[2] / 255)];
1107
+ }
1108
+ function luminance(rgb) {
1109
+ return (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) / 255;
1110
+ }
1111
+ function saturation(rgb) {
1112
+ const r = rgb[0] / 255;
1113
+ const g = rgb[1] / 255;
1114
+ const b = rgb[2] / 255;
1115
+ const max = Math.max(r, g, b);
1116
+ const min = Math.min(r, g, b);
1117
+ return max === 0 ? 0 : (max - min) / max;
1118
+ }
1119
+ function colorDistance(a, b) {
1120
+ const dr = (a[0] - b[0]) / 255;
1121
+ const dg = (a[1] - b[1]) / 255;
1122
+ const db = (a[2] - b[2]) / 255;
1123
+ return Math.sqrt(dr * dr + dg * dg + db * db) / Math.sqrt(3);
1124
+ }
1125
+ function regionCoverage(fixtures) {
1126
+ const regions = /* @__PURE__ */ new Set();
1127
+ for (const fixture of fixtures) {
1128
+ for (const sample of fixture.regions) regions.add(sample.region);
1129
+ }
1130
+ const covered = REQUIRED_CONFIDENCE_REGIONS.filter((region) => regions.has(region)).length;
1131
+ return covered / REQUIRED_CONFIDENCE_REGIONS.length;
1132
+ }
1133
+ function anchorFixtureRegions(fixtures, anchorId) {
1134
+ const regions = /* @__PURE__ */ new Set();
1135
+ for (const fixture of fixtures) {
1136
+ if (fixture.anchorId !== anchorId) continue;
1137
+ for (const sample of fixture.regions) regions.add(sample.region);
1138
+ }
1139
+ return [...regions].sort();
1140
+ }
1141
+ function provenanceConfidence(anchors) {
1142
+ if (anchors.length === 0) return 0;
1143
+ const signed = anchors.filter((anchor) => Boolean(anchor.wallet_signature)).length / anchors.length;
1144
+ const hashed = anchors.filter((anchor) => Boolean(anchor.content_hash)).length / anchors.length;
1145
+ return round(signed * 0.72 + hashed * 0.28);
1146
+ }
1147
+ function extractionStatus(anchors) {
1148
+ if (anchors.length > 0 && anchors.every((anchor) => Boolean(anchor.wallet_signature))) {
1149
+ return "extracted-from-signed-anchors";
1150
+ }
1151
+ if (anchors.some((anchor) => Boolean(anchor.content_hash))) {
1152
+ return "extracted-from-hashed-fixtures";
1153
+ }
1154
+ return "extracted-from-unsigned-fixtures";
1155
+ }
1156
+ function extractBotanicalMaterialFromPhotoFixtures(input) {
1157
+ const petalBase = regionMean(input.fixtures, "petal_base", DEFAULT_COLORS.petal_base);
1158
+ const petalMid = regionMean(input.fixtures, "petal_mid", DEFAULT_COLORS.petal_mid);
1159
+ const petalInner = regionMean(input.fixtures, "petal_inner", DEFAULT_COLORS.petal_inner);
1160
+ const petalRim = regionMean(input.fixtures, "petal_rim", DEFAULT_COLORS.petal_rim);
1161
+ const petalShadow = regionMean(input.fixtures, "petal_shadow", DEFAULT_COLORS.petal_shadow);
1162
+ const vein = regionMean(input.fixtures, "vein", DEFAULT_COLORS.petal_rim);
1163
+ const leaf = regionMean(input.fixtures, "leaf", DEFAULT_COLORS.leaf);
1164
+ const leafDark = regionMean(input.fixtures, "leaf_dark", DEFAULT_COLORS.leaf_dark);
1165
+ const water = regionMean(input.fixtures, "water", DEFAULT_COLORS.water);
1166
+ const stamen = regionMean(input.fixtures, "stamen", DEFAULT_COLORS.stamen);
1167
+ const stamenTip = regionMean(input.fixtures, "stamen_tip", DEFAULT_COLORS.stamen_tip);
1168
+ const seedPod = regionMean(input.fixtures, "seed_pod", DEFAULT_COLORS.seed_pod);
1169
+ const seedPodRim = regionMean(input.fixtures, "seed_pod_rim", DEFAULT_COLORS.seed_pod_rim);
1170
+ const silhouetteEdge = regionMean(input.fixtures, "silhouette_edge", DEFAULT_COLORS.petal_rim);
1171
+ const petalLuma = luminance(petalMid);
1172
+ const edgeLuma = luminance(petalRim);
1173
+ const shadowLuma = luminance(petalShadow);
1174
+ const petalSat = saturation(petalMid);
1175
+ const petalContrast = colorDistance(petalRim, petalShadow);
1176
+ const veinContrast = Math.max(colorDistance(vein, petalMid), Math.abs(luminance(vein) - petalLuma));
1177
+ const silhouetteContrast = colorDistance(silhouetteEdge, petalBase);
1178
+ const coverage = regionCoverage(input.fixtures);
1179
+ const provConfidence = provenanceConfidence(input.anchors);
1180
+ const subsurfaceScattering = round(
1181
+ clamp(0.48 + petalSat * 0.18 + Math.max(0, edgeLuma - shadowLuma) * 0.2 + coverage * 0.09, 0.28, 0.92)
1182
+ );
1183
+ const translucencyBase = round(clamp(0.34 + luminance(petalBase) * 0.28 + coverage * 0.08, 0.2, 0.82));
1184
+ const translucencyEdge = round(clamp(translucencyBase * (0.48 + petalContrast * 0.34), 0.14, 0.72));
1185
+ const translucencyTip = round(clamp(translucencyEdge + Math.max(0, edgeLuma - petalLuma) * 0.16, 0.16, 0.78));
1186
+ const roughness = round(clamp(0.84 - petalSat * 0.16 - petalContrast * 0.18, 0.46, 0.88));
1187
+ const ior = round(clamp(1.31 + petalLuma * 0.06 + petalSat * 0.045, 1.31, 1.44), 2);
1188
+ const veinNormalIntensity = round(clamp(0.018 + veinContrast * 0.095 + coverage * 0.012, 0.012, 0.12));
1189
+ const edgeCurlIntensity = round(clamp(0.34 + silhouetteContrast * 0.46 + petalContrast * 0.18, 0.24, 0.82));
1190
+ const gravitySagOuter = round(clamp(0.12 + (1 - luminance(leafDark)) * 0.16 + silhouetteContrast * 0.08, 0.1, 0.42));
1191
+ const sssConfidence = round(clamp(coverage * 0.6 + provConfidence * 0.4, 0, 1));
1192
+ const translucencyConfidence = round(clamp((coverage + (collectRegionPixels(input.fixtures, "petal_rim").length ? 1 : 0)) / 2, 0, 1));
1193
+ const veinConfidence = round(clamp((collectRegionPixels(input.fixtures, "vein").length ? 0.64 : 0.22) + provConfidence * 0.36, 0, 1));
1194
+ const roughnessConfidence = round(clamp(coverage * 0.5 + provConfidence * 0.28 + (input.fixtures.length > 0 ? 0.22 : 0), 0, 1));
1195
+ const overall = round((sssConfidence + translucencyConfidence + veinConfidence + roughnessConfidence + provConfidence) / 5);
1196
+ return {
1197
+ schema: "holoscript.botanical.material-extract.v1",
1198
+ status: extractionStatus(input.anchors),
1199
+ botanical_trait_target: "@botanical_lotus",
1200
+ source: {
1201
+ kind: "signed_photo_anchor_fixtures",
1202
+ captured_at: input.capturedAt ?? null,
1203
+ generated_by: input.generatedBy ?? "@holoscript/snn-webgpu",
1204
+ anchor_ids: input.anchors.map((anchor) => anchor.id),
1205
+ signed_anchor_count: input.anchors.filter((anchor) => Boolean(anchor.wallet_signature)).length,
1206
+ content_hashed_anchor_count: input.anchors.filter((anchor) => Boolean(anchor.content_hash)).length,
1207
+ fixture_count: input.fixtures.length
1208
+ },
1209
+ material: {
1210
+ subsurface_scattering: subsurfaceScattering,
1211
+ subsurface_radius_rgb: toUnitRgb(petalInner),
1212
+ petal_translucency_base: translucencyBase,
1213
+ petal_translucency_edge: translucencyEdge,
1214
+ translucency_gradient: {
1215
+ base: translucencyBase,
1216
+ edge: translucencyEdge,
1217
+ tip: translucencyTip
1218
+ },
1219
+ roughness,
1220
+ ior,
1221
+ vein_normal_intensity: veinNormalIntensity,
1222
+ vein_normals: {
1223
+ intensity: veinNormalIntensity,
1224
+ method: "region-luminance-contrast"
1225
+ },
1226
+ edge_curl_intensity: edgeCurlIntensity,
1227
+ gravity_sag_outer: gravitySagOuter
1228
+ },
1229
+ colors: {
1230
+ petal_base: toHex(petalBase),
1231
+ petal_mid: toHex(petalMid),
1232
+ petal_inner: toHex(petalInner),
1233
+ petal_rim: toHex(petalRim),
1234
+ petal_shadow: toHex(petalShadow),
1235
+ seed_pod: toHex(seedPod),
1236
+ seed_pod_rim: toHex(seedPodRim),
1237
+ stamen: toHex(stamen),
1238
+ stamen_tip: toHex(stamenTip),
1239
+ leaf: toHex(leaf),
1240
+ leaf_dark: toHex(leafDark),
1241
+ water: toHex(water)
1242
+ },
1243
+ confidence: {
1244
+ overall,
1245
+ sss: sssConfidence,
1246
+ translucency: translucencyConfidence,
1247
+ roughness: roughnessConfidence,
1248
+ vein_normals: veinConfidence,
1249
+ provenance: provConfidence
1250
+ },
1251
+ provenance: {
1252
+ extractor: {
1253
+ package: "@holoscript/snn-webgpu",
1254
+ algorithm: "botanical-region-statistics-v1",
1255
+ deterministic: true
1256
+ },
1257
+ anchor_receipts: input.anchors.map((anchor) => ({
1258
+ id: anchor.id,
1259
+ role: anchor.role ?? null,
1260
+ status: anchor.status ?? null,
1261
+ content_hash: anchor.content_hash ?? null,
1262
+ wallet_signature_present: Boolean(anchor.wallet_signature),
1263
+ fixture_regions: anchorFixtureRegions(input.fixtures, anchor.id)
1264
+ }))
1265
+ },
1266
+ renderer_mapping: {
1267
+ petal_material: "MeshPhysicalMaterial + botanical onBeforeCompile SSS/translucency/vein uniforms",
1268
+ photorealism_status: "extractor_available"
1269
+ }
1270
+ };
1271
+ }
1272
+ var TropicalActivationTrait = class {
1273
+ forward(spikeRates, config) {
1274
+ const result = new Float32Array(spikeRates.length);
1275
+ for (let i = 0; i < spikeRates.length; i++) {
1276
+ const shifted = spikeRates[i] - config.threshold;
1277
+ result[i] = config.variant === "max-plus" ? config.gain * Math.max(0, shifted) : config.gain * Math.min(0, shifted);
1278
+ }
1279
+ return result;
1280
+ }
1281
+ toSpikeTiming(activations, maxTime) {
1282
+ const result = new Float32Array(activations.length);
1283
+ for (let i = 0; i < activations.length; i++) {
1284
+ const activation = activations[i];
1285
+ result[i] = activation > 0 ? maxTime / activation : Number.POSITIVE_INFINITY;
1286
+ }
1287
+ return result;
1288
+ }
1289
+ };
1290
+ var LIF_PARAMS_SIZE = 32;
1291
+ var SYNAPTIC_PARAMS_SIZE = 16;
1292
+ function packLIFParams2(params, neuronCount) {
1293
+ const buf = new ArrayBuffer(LIF_PARAMS_SIZE);
1294
+ const f32 = new Float32Array(buf);
1295
+ const u32 = new Uint32Array(buf);
1296
+ f32[0] = params.tau;
1297
+ f32[1] = params.vThreshold;
1298
+ f32[2] = params.vReset;
1299
+ f32[3] = params.vRest;
1300
+ f32[4] = params.dt;
1301
+ u32[5] = neuronCount;
1302
+ u32[6] = 0;
1303
+ u32[7] = 0;
1304
+ return buf;
1305
+ }
1306
+ function packSynapticParams(preCount, postCount, learningRate) {
1307
+ const buf = new ArrayBuffer(SYNAPTIC_PARAMS_SIZE);
1308
+ const u32 = new Uint32Array(buf);
1309
+ const f32 = new Float32Array(buf);
1310
+ u32[0] = preCount;
1311
+ u32[1] = postCount;
1312
+ f32[2] = learningRate;
1313
+ u32[3] = 0;
1314
+ return buf;
1315
+ }
1316
+ var SNNNetwork = class {
1317
+ constructor(ctx, config) {
1318
+ this.layers = /* @__PURE__ */ new Map();
1319
+ this.connections = [];
1320
+ this.initialized = false;
1321
+ this.simTimeMs = 0;
1322
+ this.ctx = ctx;
1323
+ this.config = config;
1324
+ this.dt = config.dt ?? 1;
1325
+ this.bufferManager = new BufferManager(ctx.device);
1326
+ this.pipelineFactory = new PipelineFactory(ctx);
1327
+ }
1328
+ /**
1329
+ * Initialize all layers and connections.
1330
+ */
1331
+ async initialize() {
1332
+ if (this.initialized) return;
1333
+ this.validateConfig();
1334
+ for (const layerConfig of this.config.layers) {
1335
+ const layer = this.createLayer(layerConfig);
1336
+ this.layers.set(layerConfig.name, layer);
1337
+ }
1338
+ for (const connConfig of this.config.connections) {
1339
+ const conn = this.createConnection(connConfig);
1340
+ this.connections.push(conn);
1341
+ }
1342
+ this.initialized = true;
1343
+ }
1344
+ /**
1345
+ * Step the simulation by one timestep.
1346
+ * Order: synaptic currents -> LIF updates -> STDP (if enabled)
1347
+ */
1348
+ async step() {
1349
+ this.ensureInitialized();
1350
+ const stepStart = performance.now();
1351
+ const encoder = this.ctx.device.createCommandEncoder({
1352
+ label: `snn-step-${Math.floor(this.simTimeMs)}`
1353
+ });
1354
+ for (const conn of this.connections) {
1355
+ const workgroups = computeDispatchSize(conn.toLayer.config.neuronCount);
1356
+ this.pipelineFactory.encodeDispatch(
1357
+ encoder,
1358
+ "compute_synaptic_current",
1359
+ conn.currentBindGroup,
1360
+ workgroups
1361
+ );
1362
+ }
1363
+ for (const layer of this.layers.values()) {
1364
+ const workgroups = computeDispatchSize(layer.config.neuronCount);
1365
+ this.pipelineFactory.encodeDispatch(encoder, "lif_step", layer.lifBindGroup, workgroups);
1366
+ }
1367
+ for (const conn of this.connections) {
1368
+ if (conn.config.stdpEnabled && conn.stdpBindGroup) {
1369
+ const workgroups = computeDispatchSize(conn.toLayer.config.neuronCount);
1370
+ this.pipelineFactory.encodeDispatch(
1371
+ encoder,
1372
+ "stdp_weight_update",
1373
+ conn.stdpBindGroup,
1374
+ workgroups
1375
+ );
1376
+ }
1377
+ }
1378
+ await this.ctx.submitAndWait(encoder.finish());
1379
+ this.simTimeMs += this.dt;
1380
+ const stepTimeMs = performance.now() - stepStart;
1381
+ return this.collectStats(stepTimeMs);
1382
+ }
1383
+ /**
1384
+ * Run N simulation timesteps.
1385
+ */
1386
+ async stepN(count) {
1387
+ let lastStats = null;
1388
+ for (let i = 0; i < count; i++) {
1389
+ lastStats = await this.step();
1390
+ }
1391
+ return lastStats;
1392
+ }
1393
+ /**
1394
+ * Set external spike input for a layer (typically the input layer).
1395
+ */
1396
+ setInputSpikes(layerName, spikes) {
1397
+ this.ensureInitialized();
1398
+ const layer = this.getLayer(layerName);
1399
+ if (spikes.length !== layer.config.neuronCount) {
1400
+ throw new Error(
1401
+ `Spike data length (${spikes.length}) must match layer '${layerName}' neuron count (${layer.config.neuronCount})`
1402
+ );
1403
+ }
1404
+ this.bufferManager.writeBuffer(layer.spikesBuffer, spikes);
1405
+ }
1406
+ /**
1407
+ * Set external synaptic input for a layer.
1408
+ */
1409
+ setSynapticInput(layerName, currents) {
1410
+ this.ensureInitialized();
1411
+ const layer = this.getLayer(layerName);
1412
+ this.bufferManager.writeBuffer(layer.synapticInputBuffer, currents);
1413
+ }
1414
+ /**
1415
+ * Read spike output for a specific layer.
1416
+ */
1417
+ async readLayerSpikes(layerName) {
1418
+ this.ensureInitialized();
1419
+ const layer = this.getLayer(layerName);
1420
+ return this.bufferManager.readBuffer(layer.spikesBuffer);
1421
+ }
1422
+ /**
1423
+ * Read membrane potentials for a specific layer.
1424
+ */
1425
+ async readLayerMembrane(layerName) {
1426
+ this.ensureInitialized();
1427
+ const layer = this.getLayer(layerName);
1428
+ return this.bufferManager.readBuffer(layer.membraneBuffer);
1429
+ }
1430
+ /**
1431
+ * Read connection weights.
1432
+ */
1433
+ async readConnectionWeights(fromLayer, toLayer) {
1434
+ this.ensureInitialized();
1435
+ const conn = this.connections.find(
1436
+ (c) => c.config.from === fromLayer && c.config.to === toLayer
1437
+ );
1438
+ if (!conn) {
1439
+ throw new Error(`No connection found from '${fromLayer}' to '${toLayer}'`);
1440
+ }
1441
+ return this.bufferManager.readBuffer(conn.weightsBuffer);
1442
+ }
1443
+ /**
1444
+ * Reset the entire network state.
1445
+ */
1446
+ resetState() {
1447
+ this.ensureInitialized();
1448
+ for (const layer of this.layers.values()) {
1449
+ const n = layer.config.neuronCount;
1450
+ const rest = new Float32Array(n).fill(layer.params.vRest);
1451
+ const zeros = new Float32Array(n);
1452
+ this.bufferManager.writeBuffer(layer.membraneBuffer, rest);
1453
+ this.bufferManager.writeBuffer(layer.synapticInputBuffer, zeros);
1454
+ this.bufferManager.writeBuffer(layer.spikesBuffer, zeros);
1455
+ this.bufferManager.writeBuffer(layer.refractoryBuffer, zeros);
1456
+ }
1457
+ this.simTimeMs = 0;
1458
+ }
1459
+ /** Get total neuron count across all layers. */
1460
+ get totalNeurons() {
1461
+ let total = 0;
1462
+ for (const layer of this.layers.values()) {
1463
+ total += layer.config.neuronCount;
1464
+ }
1465
+ return total;
1466
+ }
1467
+ /** Get layer names. */
1468
+ get layerNames() {
1469
+ return Array.from(this.layers.keys());
1470
+ }
1471
+ /** Get current simulation time in ms. */
1472
+ get currentSimTimeMs() {
1473
+ return this.simTimeMs;
1474
+ }
1475
+ /** Get total GPU memory used in bytes. */
1476
+ get gpuMemoryBytes() {
1477
+ return this.bufferManager.getTotalAllocatedBytes();
1478
+ }
1479
+ /**
1480
+ * Destroy all GPU resources.
1481
+ */
1482
+ destroy() {
1483
+ this.pipelineFactory.clearCache();
1484
+ this.bufferManager.destroyAll();
1485
+ this.layers.clear();
1486
+ this.connections = [];
1487
+ this.initialized = false;
1488
+ this.simTimeMs = 0;
1489
+ }
1490
+ // --- Private methods ---
1491
+ validateConfig() {
1492
+ const layerNames = new Set(this.config.layers.map((l) => l.name));
1493
+ if (layerNames.size !== this.config.layers.length) {
1494
+ throw new Error("Duplicate layer names in network configuration");
1495
+ }
1496
+ for (const conn of this.config.connections) {
1497
+ if (!layerNames.has(conn.from)) {
1498
+ throw new Error(`Connection references unknown source layer: '${conn.from}'`);
1499
+ }
1500
+ if (!layerNames.has(conn.to)) {
1501
+ throw new Error(`Connection references unknown target layer: '${conn.to}'`);
1502
+ }
1503
+ }
1504
+ }
1505
+ createLayer(config) {
1506
+ const n = this.ctx.validateNeuronCapacity(config.neuronCount);
1507
+ const params = {
1508
+ ...DEFAULT_LIF_PARAMS,
1509
+ dt: this.dt,
1510
+ ...config.lifParams
1511
+ };
1512
+ const packedParams = packLIFParams2(params, n);
1513
+ const paramsBuffer = this.bufferManager.createBuffer({
1514
+ size: LIF_PARAMS_SIZE,
1515
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1516
+ label: `${config.name}-lif-params`,
1517
+ initialData: new Float32Array(packedParams)
1518
+ });
1519
+ const initMembrane = new Float32Array(n).fill(params.vRest);
1520
+ const membraneBuffer = this.bufferManager.createStorageBuffer(
1521
+ initMembrane,
1522
+ `${config.name}-membrane`
1523
+ );
1524
+ const synapticInputBuffer = this.bufferManager.createZeroBuffer(n, `${config.name}-synaptic`);
1525
+ const spikesBuffer = this.bufferManager.createZeroBuffer(n, `${config.name}-spikes`);
1526
+ const refractoryBuffer = this.bufferManager.createZeroBuffer(n, `${config.name}-refractory`);
1527
+ const lifBindGroup = this.pipelineFactory.createBindGroup(
1528
+ "lif_step",
1529
+ [
1530
+ paramsBuffer.buffer,
1531
+ membraneBuffer.buffer,
1532
+ synapticInputBuffer.buffer,
1533
+ spikesBuffer.buffer,
1534
+ refractoryBuffer.buffer
1535
+ ],
1536
+ `${config.name}-lif-bind-group`
1537
+ );
1538
+ return {
1539
+ config: { ...config, neuronCount: n },
1540
+ params,
1541
+ paramsBuffer,
1542
+ membraneBuffer,
1543
+ synapticInputBuffer,
1544
+ spikesBuffer,
1545
+ refractoryBuffer,
1546
+ lifBindGroup
1547
+ };
1548
+ }
1549
+ createConnection(config) {
1550
+ const fromLayer = this.getLayer(config.from);
1551
+ const toLayer = this.getLayer(config.to);
1552
+ const preCount = fromLayer.config.neuronCount;
1553
+ const postCount = toLayer.config.neuronCount;
1554
+ const lr = config.learningRate ?? 0.01;
1555
+ const packedSynaptic = packSynapticParams(preCount, postCount, lr);
1556
+ const synapticParamsBuffer = this.bufferManager.createBuffer({
1557
+ size: SYNAPTIC_PARAMS_SIZE,
1558
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1559
+ label: `${config.from}-${config.to}-synaptic-params`,
1560
+ initialData: new Float32Array(packedSynaptic)
1561
+ });
1562
+ const weightCount = preCount * postCount;
1563
+ const weights = this.initializeWeights(weightCount, config);
1564
+ const weightsBuffer = this.bufferManager.createStorageBuffer(
1565
+ weights,
1566
+ `${config.from}-${config.to}-weights`
1567
+ );
1568
+ const currentBindGroup = this.pipelineFactory.createBindGroup(
1569
+ "compute_synaptic_current",
1570
+ [
1571
+ synapticParamsBuffer.buffer,
1572
+ // binding 0: params
1573
+ weightsBuffer.buffer,
1574
+ // binding 1: weights
1575
+ fromLayer.spikesBuffer.buffer,
1576
+ // binding 2: pre_spikes
1577
+ toLayer.synapticInputBuffer.buffer
1578
+ // binding 3: post_currents
1579
+ // binding 4 (post_spikes) omitted — not accessed by compute_synaptic_current
1580
+ ],
1581
+ `${config.from}-${config.to}-current-bind-group`
1582
+ );
1583
+ let stdpBindGroup;
1584
+ if (config.stdpEnabled) {
1585
+ stdpBindGroup = this.pipelineFactory.createBindGroupWithIndices(
1586
+ "stdp_weight_update",
1587
+ [
1588
+ synapticParamsBuffer.buffer,
1589
+ // binding 0: params
1590
+ weightsBuffer.buffer,
1591
+ // binding 1: weights
1592
+ fromLayer.spikesBuffer.buffer,
1593
+ // binding 2: pre_spikes
1594
+ toLayer.spikesBuffer.buffer
1595
+ // binding 4: post_spikes (binding 3 skipped)
1596
+ ],
1597
+ [0, 1, 2, 4],
1598
+ `${config.from}-${config.to}-stdp-bind-group`
1599
+ );
1600
+ }
1601
+ return {
1602
+ config,
1603
+ fromLayer,
1604
+ toLayer,
1605
+ weightsBuffer,
1606
+ synapticParamsBuffer,
1607
+ currentBindGroup,
1608
+ stdpBindGroup
1609
+ };
1610
+ }
1611
+ initializeWeights(count, config) {
1612
+ const weights = new Float32Array(count);
1613
+ switch (config.weightInit) {
1614
+ case "random":
1615
+ const scale = 1 / Math.sqrt(count);
1616
+ for (let i = 0; i < count; i++) {
1617
+ weights[i] = (Math.random() * 2 - 1) * scale;
1618
+ }
1619
+ break;
1620
+ case "uniform":
1621
+ weights.fill(config.uniformValue ?? 0.5);
1622
+ break;
1623
+ }
1624
+ return weights;
1625
+ }
1626
+ async collectStats(stepTimeMs) {
1627
+ const layerSpikes = /* @__PURE__ */ new Map();
1628
+ const layerAvgVoltage = /* @__PURE__ */ new Map();
1629
+ let totalSpikes = 0;
1630
+ for (const [name, layer] of this.layers) {
1631
+ const spikeResult = await this.bufferManager.readBuffer(layer.spikesBuffer);
1632
+ let spikeCount = 0;
1633
+ for (let i = 0; i < spikeResult.data.length; i++) {
1634
+ if (spikeResult.data[i] > 0.5) spikeCount++;
1635
+ }
1636
+ layerSpikes.set(name, spikeCount);
1637
+ totalSpikes += spikeCount;
1638
+ const vResult = await this.bufferManager.readBuffer(layer.membraneBuffer);
1639
+ let sum = 0;
1640
+ for (let i = 0; i < vResult.data.length; i++) {
1641
+ sum += vResult.data[i];
1642
+ }
1643
+ layerAvgVoltage.set(name, sum / vResult.data.length);
1644
+ }
1645
+ return {
1646
+ totalSpikes,
1647
+ layerSpikes,
1648
+ layerAvgVoltage,
1649
+ stepTimeMs,
1650
+ simTimeMs: this.simTimeMs
1651
+ };
1652
+ }
1653
+ getLayer(name) {
1654
+ const layer = this.layers.get(name);
1655
+ if (!layer) {
1656
+ throw new Error(`Layer '${name}' not found in network`);
1657
+ }
1658
+ return layer;
1659
+ }
1660
+ ensureInitialized() {
1661
+ if (!this.initialized) {
1662
+ throw new Error("SNNNetwork not initialized. Call initialize() first.");
1663
+ }
1664
+ }
1665
+ };
1666
+ var INF = 1e30;
1667
+ var GEMM_WORKGROUP = 16;
1668
+ var SPMV_WORKGROUP = 256;
1669
+ var UNIFORM_BYTE_SIZE = 16;
1670
+ function packDenseDims(m, n, k) {
1671
+ const buffer = new ArrayBuffer(UNIFORM_BYTE_SIZE);
1672
+ const u32 = new Uint32Array(buffer);
1673
+ u32[0] = m;
1674
+ u32[1] = n;
1675
+ u32[2] = k;
1676
+ u32[3] = 0;
1677
+ return buffer;
1678
+ }
1679
+ function packSparseDims(rows) {
1680
+ const buffer = new ArrayBuffer(UNIFORM_BYTE_SIZE);
1681
+ const u32 = new Uint32Array(buffer);
1682
+ u32[0] = rows;
1683
+ u32[1] = 0;
1684
+ u32[2] = 0;
1685
+ u32[3] = 0;
1686
+ return buffer;
1687
+ }
1688
+ function arraysEqual(a, b) {
1689
+ if (a.length !== b.length) return false;
1690
+ for (let i = 0; i < a.length; i++) {
1691
+ if (Math.abs(a[i] - b[i]) > 1e-6) return false;
1692
+ }
1693
+ return true;
1694
+ }
1695
+ function minDistances(a, b) {
1696
+ if (a.length !== b.length) {
1697
+ throw new Error(`distance vector length mismatch (${a.length} !== ${b.length})`);
1698
+ }
1699
+ const out = new Float32Array(a.length);
1700
+ for (let i = 0; i < a.length; i++) {
1701
+ out[i] = Math.min(a[i], b[i]);
1702
+ }
1703
+ return out;
1704
+ }
1705
+ function transposeCSR(graph) {
1706
+ const rows = graph.rowPtr.length - 1;
1707
+ const edgeCount = graph.colIdx.length;
1708
+ const incomingCounts = new Uint32Array(rows);
1709
+ for (let i = 0; i < edgeCount; i++) {
1710
+ incomingCounts[graph.colIdx[i]]++;
1711
+ }
1712
+ const rowPtr = new Uint32Array(rows + 1);
1713
+ for (let row = 0; row < rows; row++) {
1714
+ rowPtr[row + 1] = rowPtr[row] + incomingCounts[row];
1715
+ }
1716
+ const nextWrite = new Uint32Array(rowPtr);
1717
+ const colIdx = new Uint32Array(edgeCount);
1718
+ const values = new Float32Array(edgeCount);
1719
+ for (let row = 0; row < rows; row++) {
1720
+ const start = graph.rowPtr[row];
1721
+ const finish = graph.rowPtr[row + 1];
1722
+ for (let edge = start; edge < finish; edge++) {
1723
+ const col = graph.colIdx[edge];
1724
+ const dest = nextWrite[col]++;
1725
+ colIdx[dest] = row;
1726
+ values[dest] = graph.values[edge];
1727
+ }
1728
+ }
1729
+ return { rowPtr, colIdx, values };
1730
+ }
1731
+ var TropicalShortestPaths = class _TropicalShortestPaths {
1732
+ constructor(ctx, options = {}) {
1733
+ this.ctx = ctx;
1734
+ this.pipelineFactory = new PipelineFactory(ctx);
1735
+ this.preferGPU = options.preferGPU ?? true;
1736
+ this.denseCpuThreshold = options.denseCpuThreshold ?? 128;
1737
+ this.sparseCpuThreshold = options.sparseCpuThreshold ?? 256;
1738
+ }
1739
+ static minPlusGemmCPU(matA, matB, m, n, k) {
1740
+ if (matA.length !== m * k) {
1741
+ throw new Error(`matA length (${matA.length}) must equal m*k (${m * k})`);
1742
+ }
1743
+ if (matB.length !== k * n) {
1744
+ throw new Error(`matB length (${matB.length}) must equal k*n (${k * n})`);
1745
+ }
1746
+ const out = new Float32Array(m * n).fill(INF);
1747
+ for (let row = 0; row < m; row++) {
1748
+ for (let col = 0; col < n; col++) {
1749
+ let best = INF;
1750
+ for (let kk = 0; kk < k; kk++) {
1751
+ const a = matA[row * k + kk];
1752
+ const b = matB[kk * n + col];
1753
+ if (a < INF && b < INF) {
1754
+ const candidate = a + b;
1755
+ if (candidate < best) best = candidate;
1756
+ }
1757
+ }
1758
+ out[row * n + col] = best;
1759
+ }
1760
+ }
1761
+ return out;
1762
+ }
1763
+ static computeAPSPCPU(adjacencyMatrix, n) {
1764
+ if (adjacencyMatrix.length !== n * n) {
1765
+ throw new Error(`adjacencyMatrix length (${adjacencyMatrix.length}) must equal n*n (${n * n})`);
1766
+ }
1767
+ const dist = new Float32Array(adjacencyMatrix);
1768
+ for (let k = 0; k < n; k++) {
1769
+ for (let i = 0; i < n; i++) {
1770
+ const ik = dist[i * n + k];
1771
+ if (ik >= INF) continue;
1772
+ for (let j = 0; j < n; j++) {
1773
+ const kj = dist[k * n + j];
1774
+ if (kj >= INF) continue;
1775
+ const candidate = ik + kj;
1776
+ const idx = i * n + j;
1777
+ if (candidate < dist[idx]) dist[idx] = candidate;
1778
+ }
1779
+ }
1780
+ }
1781
+ return dist;
1782
+ }
1783
+ static computeSSSPCPU(graph, source) {
1784
+ const rows = graph.rowPtr.length - 1;
1785
+ if (source < 0 || source >= rows) {
1786
+ throw new Error(`source index (${source}) must be in [0, ${rows - 1}]`);
1787
+ }
1788
+ const dist = new Float32Array(rows).fill(INF);
1789
+ dist[source] = 0;
1790
+ for (let iter = 0; iter < rows - 1; iter++) {
1791
+ let changed = false;
1792
+ for (let u = 0; u < rows; u++) {
1793
+ const du = dist[u];
1794
+ if (du >= INF) continue;
1795
+ const start = graph.rowPtr[u];
1796
+ const finish = graph.rowPtr[u + 1];
1797
+ for (let edge = start; edge < finish; edge++) {
1798
+ const v = graph.colIdx[edge];
1799
+ const w = graph.values[edge];
1800
+ const candidate = du + w;
1801
+ if (candidate < dist[v]) {
1802
+ dist[v] = candidate;
1803
+ changed = true;
1804
+ }
1805
+ }
1806
+ }
1807
+ if (!changed) break;
1808
+ }
1809
+ return dist;
1810
+ }
1811
+ async tropicalGemm(matA, matB, m, n, k) {
1812
+ if (matA.length !== m * k) {
1813
+ throw new Error(`matA length (${matA.length}) must equal m*k (${m * k})`);
1814
+ }
1815
+ if (matB.length !== k * n) {
1816
+ throw new Error(`matB length (${matB.length}) must equal k*n (${k * n})`);
1817
+ }
1818
+ const buffers = new BufferManager(this.ctx.device);
1819
+ try {
1820
+ const a = buffers.createStorageBuffer(matA, "tropical-gemm-a");
1821
+ const b = buffers.createStorageBuffer(matB, "tropical-gemm-b");
1822
+ const c = buffers.createZeroBuffer(m * n, "tropical-gemm-c");
1823
+ const dims = buffers.createUniformBuffer(packDenseDims(m, n, k), "tropical-gemm-dims");
1824
+ const bindGroup = this.pipelineFactory.createBindGroup(
1825
+ "tropical_min_plus_gemm",
1826
+ [a.buffer, b.buffer, c.buffer, dims.buffer],
1827
+ "tropical-gemm-bg"
1828
+ );
1829
+ const encoder = this.ctx.device.createCommandEncoder({ label: "tropical-gemm-encoder" });
1830
+ this.pipelineFactory.encodeDispatch(
1831
+ encoder,
1832
+ "tropical_min_plus_gemm",
1833
+ bindGroup,
1834
+ Math.ceil(n / GEMM_WORKGROUP),
1835
+ Math.ceil(m / GEMM_WORKGROUP)
1836
+ );
1837
+ await this.ctx.submitAndWait(encoder.finish());
1838
+ const readback = await buffers.readBuffer(c);
1839
+ return readback.data;
1840
+ } finally {
1841
+ buffers.destroyAll();
1842
+ }
1843
+ }
1844
+ async computeAPSP(adjacencyMatrix, n) {
1845
+ if (adjacencyMatrix.length !== n * n) {
1846
+ throw new Error(`adjacencyMatrix length (${adjacencyMatrix.length}) must equal n*n (${n * n})`);
1847
+ }
1848
+ if (!this.preferGPU || n < this.denseCpuThreshold) {
1849
+ return _TropicalShortestPaths.computeAPSPCPU(adjacencyMatrix, n);
1850
+ }
1851
+ try {
1852
+ let current = new Float32Array(adjacencyMatrix);
1853
+ const iterations = Math.ceil(Math.log2(Math.max(1, n)));
1854
+ for (let i = 0; i < iterations; i++) {
1855
+ current = await this.tropicalGemm(current, current, n, n, n);
1856
+ }
1857
+ return current;
1858
+ } catch {
1859
+ return _TropicalShortestPaths.computeAPSPCPU(adjacencyMatrix, n);
1860
+ }
1861
+ }
1862
+ async tropicalSpmv(graph, dist) {
1863
+ const rows = graph.rowPtr.length - 1;
1864
+ if (rows <= 0) {
1865
+ return new Float32Array();
1866
+ }
1867
+ if (dist.length !== rows) {
1868
+ throw new Error(`dist length (${dist.length}) must equal row count (${rows})`);
1869
+ }
1870
+ const buffers = new BufferManager(this.ctx.device);
1871
+ try {
1872
+ const rowPtr = buffers.createBuffer({
1873
+ size: graph.rowPtr.byteLength,
1874
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1875
+ label: "tropical-spmv-rowptr",
1876
+ initialData: graph.rowPtr
1877
+ });
1878
+ const colIdx = buffers.createBuffer({
1879
+ size: graph.colIdx.byteLength,
1880
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1881
+ label: "tropical-spmv-colidx",
1882
+ initialData: graph.colIdx
1883
+ });
1884
+ const values = buffers.createStorageBuffer(graph.values, "tropical-spmv-values");
1885
+ const distIn = buffers.createStorageBuffer(dist, "tropical-spmv-dist-in");
1886
+ const distOut = buffers.createStorageBuffer(new Float32Array(dist), "tropical-spmv-dist-out");
1887
+ const dims = buffers.createUniformBuffer(packSparseDims(rows), "tropical-spmv-dims");
1888
+ const bindGroup = this.pipelineFactory.createBindGroup(
1889
+ "tropical_spmv",
1890
+ [rowPtr.buffer, colIdx.buffer, values.buffer, distIn.buffer, distOut.buffer, dims.buffer],
1891
+ "tropical-spmv-bg"
1892
+ );
1893
+ const encoder = this.ctx.device.createCommandEncoder({ label: "tropical-spmv-encoder" });
1894
+ this.pipelineFactory.encodeDispatch(
1895
+ encoder,
1896
+ "tropical_spmv",
1897
+ bindGroup,
1898
+ Math.ceil(rows / SPMV_WORKGROUP)
1899
+ );
1900
+ await this.ctx.submitAndWait(encoder.finish());
1901
+ const readback = await buffers.readBuffer(distOut);
1902
+ return readback.data;
1903
+ } finally {
1904
+ buffers.destroyAll();
1905
+ }
1906
+ }
1907
+ async computeSSSP(graph, source) {
1908
+ const rows = graph.rowPtr.length - 1;
1909
+ if (source < 0 || source >= rows) {
1910
+ throw new Error(`source index (${source}) must be in [0, ${rows - 1}]`);
1911
+ }
1912
+ if (!this.preferGPU || rows < this.sparseCpuThreshold) {
1913
+ return _TropicalShortestPaths.computeSSSPCPU(graph, source);
1914
+ }
1915
+ const incomingGraph = transposeCSR(graph);
1916
+ let current = new Float32Array(rows).fill(INF);
1917
+ current[source] = 0;
1918
+ try {
1919
+ for (let i = 0; i < rows - 1; i++) {
1920
+ const relaxed = await this.tropicalSpmv(incomingGraph, current);
1921
+ const next = minDistances(current, relaxed);
1922
+ if (arraysEqual(current, next)) {
1923
+ return next;
1924
+ }
1925
+ current = next;
1926
+ }
1927
+ return current;
1928
+ } catch {
1929
+ return _TropicalShortestPaths.computeSSSPCPU(graph, source);
1930
+ }
1931
+ }
1932
+ destroy() {
1933
+ this.pipelineFactory.clearCache();
1934
+ }
1935
+ };
1936
+ var TROPICAL_INF = 1e30;
1937
+ function assertGraphShape(graph) {
1938
+ const n = graph.rowPtr.length - 1;
1939
+ if (n < 0) {
1940
+ throw new Error("rowPtr must contain at least one element");
1941
+ }
1942
+ if (graph.colIdx.length !== graph.values.length) {
1943
+ throw new Error(
1944
+ `colIdx length (${graph.colIdx.length}) must equal values length (${graph.values.length})`
1945
+ );
1946
+ }
1947
+ if (graph.rowPtr.length === 0 || graph.rowPtr[0] !== 0) {
1948
+ throw new Error("rowPtr must start at 0");
1949
+ }
1950
+ for (let i = 0; i < graph.rowPtr.length - 1; i++) {
1951
+ if (graph.rowPtr[i] > graph.rowPtr[i + 1]) {
1952
+ throw new Error(`rowPtr must be non-decreasing; violation at index ${i}`);
1953
+ }
1954
+ }
1955
+ if (graph.rowPtr[n] !== graph.colIdx.length) {
1956
+ throw new Error(
1957
+ `rowPtr[last] (${graph.rowPtr[n]}) must equal edge count (${graph.colIdx.length})`
1958
+ );
1959
+ }
1960
+ for (let edge = 0; edge < graph.colIdx.length; edge++) {
1961
+ const col = graph.colIdx[edge];
1962
+ if (col < 0 || col >= n) {
1963
+ throw new Error(`colIdx[${edge}] out of bounds: ${col} not in [0, ${n - 1}]`);
1964
+ }
1965
+ }
1966
+ }
1967
+ function normalizeAdjacency(adjacency, n) {
1968
+ if (adjacency.length !== n * n) {
1969
+ throw new Error(`adjacency length (${adjacency.length}) must equal n*n (${n * n})`);
1970
+ }
1971
+ const out = new Float32Array(adjacency.length);
1972
+ for (let i = 0; i < n; i++) {
1973
+ for (let j = 0; j < n; j++) {
1974
+ const idx = i * n + j;
1975
+ if (i === j) {
1976
+ out[idx] = 0;
1977
+ } else {
1978
+ const value = adjacency[idx];
1979
+ out[idx] = Number.isFinite(value) ? value : TROPICAL_INF;
1980
+ }
1981
+ }
1982
+ }
1983
+ return out;
1984
+ }
1985
+ function denseToCSR(adjacency, n) {
1986
+ if (adjacency.length !== n * n) {
1987
+ throw new Error(`adjacency length (${adjacency.length}) must equal n*n (${n * n})`);
1988
+ }
1989
+ const rowPtr = new Uint32Array(n + 1);
1990
+ const colIdx = [];
1991
+ const values = [];
1992
+ for (let row = 0; row < n; row++) {
1993
+ rowPtr[row] = colIdx.length;
1994
+ for (let col = 0; col < n; col++) {
1995
+ if (row === col) continue;
1996
+ const weight = adjacency[row * n + col];
1997
+ if (Number.isFinite(weight) && weight < TROPICAL_INF) {
1998
+ colIdx.push(col);
1999
+ values.push(weight);
2000
+ }
2001
+ }
2002
+ }
2003
+ rowPtr[n] = colIdx.length;
2004
+ return {
2005
+ rowPtr,
2006
+ colIdx: new Uint32Array(colIdx),
2007
+ values: new Float32Array(values)
2008
+ };
2009
+ }
2010
+ function fromEdges(nodeCount, edges) {
2011
+ const minWeightByEdge = /* @__PURE__ */ new Map();
2012
+ for (const edge of edges) {
2013
+ if (edge.from < 0 || edge.from >= nodeCount || edge.to < 0 || edge.to >= nodeCount) {
2014
+ throw new Error(
2015
+ `Edge out of bounds: (${edge.from} -> ${edge.to}) for nodeCount=${nodeCount}`
2016
+ );
2017
+ }
2018
+ if (!Number.isFinite(edge.weight) || edge.weight < 0) {
2019
+ throw new Error(`Edge weight must be finite and non-negative: ${edge.weight}`);
2020
+ }
2021
+ const key = `${edge.from}:${edge.to}`;
2022
+ const prev = minWeightByEdge.get(key);
2023
+ if (prev === void 0 || edge.weight < prev) {
2024
+ minWeightByEdge.set(key, edge.weight);
2025
+ }
2026
+ }
2027
+ const grouped = /* @__PURE__ */ new Map();
2028
+ for (const [key, weight] of minWeightByEdge) {
2029
+ const [fromRaw, toRaw] = key.split(":");
2030
+ const from = Number(fromRaw);
2031
+ const to = Number(toRaw);
2032
+ if (!grouped.has(from)) grouped.set(from, []);
2033
+ grouped.get(from).push({ to, weight });
2034
+ }
2035
+ const rowPtr = new Uint32Array(nodeCount + 1);
2036
+ const colIdx = [];
2037
+ const values = [];
2038
+ for (let from = 0; from < nodeCount; from++) {
2039
+ rowPtr[from] = colIdx.length;
2040
+ const outgoing = grouped.get(from) ?? [];
2041
+ outgoing.sort((a, b) => a.to - b.to || a.weight - b.weight);
2042
+ for (const edge of outgoing) {
2043
+ if (from === edge.to) continue;
2044
+ colIdx.push(edge.to);
2045
+ values.push(edge.weight);
2046
+ }
2047
+ }
2048
+ rowPtr[nodeCount] = colIdx.length;
2049
+ const graph = {
2050
+ rowPtr,
2051
+ colIdx: new Uint32Array(colIdx),
2052
+ values: new Float32Array(values)
2053
+ };
2054
+ assertGraphShape(graph);
2055
+ return graph;
2056
+ }
2057
+ function csrToDense(graph) {
2058
+ assertGraphShape(graph);
2059
+ const n = graph.rowPtr.length - 1;
2060
+ const out = new Float32Array(n * n).fill(TROPICAL_INF);
2061
+ for (let i = 0; i < n; i++) {
2062
+ out[i * n + i] = 0;
2063
+ const start = graph.rowPtr[i];
2064
+ const finish = graph.rowPtr[i + 1];
2065
+ for (let edge = start; edge < finish; edge++) {
2066
+ const j = graph.colIdx[edge];
2067
+ out[i * n + j] = graph.values[edge];
2068
+ }
2069
+ }
2070
+ return out;
2071
+ }
2072
+ var CPUReferenceSimulator = class {
2073
+ // ms
2074
+ constructor(neuronCount, params) {
2075
+ this.stepCount = 0;
2076
+ this.REFRACTORY_PERIOD = 2;
2077
+ this.params = { ...DEFAULT_LIF_PARAMS, ...params };
2078
+ this.neuronCount = neuronCount;
2079
+ this.membraneV = new Float32Array(neuronCount).fill(this.params.vRest);
2080
+ this.refractory = new Float32Array(neuronCount);
2081
+ this.spikes = new Uint32Array(neuronCount);
2082
+ }
2083
+ /**
2084
+ * Run one simulation timestep with given synaptic inputs.
2085
+ *
2086
+ * @param synapticInput - Input current for each neuron [N x f32]
2087
+ * @returns Step result with spike data
2088
+ */
2089
+ step(synapticInput) {
2090
+ const start = performance.now();
2091
+ const n = this.neuronCount;
2092
+ const decay = Math.exp(-this.params.dt / this.params.tau);
2093
+ let totalSpikes = 0;
2094
+ const spikeIndices = [];
2095
+ for (let i = 0; i < n; i++) {
2096
+ if (this.refractory[i] > 0) {
2097
+ this.refractory[i] = Math.max(this.refractory[i] - this.params.dt, 0);
2098
+ this.membraneV[i] = this.params.vReset;
2099
+ this.spikes[i] = 0;
2100
+ continue;
2101
+ }
2102
+ const v = this.params.vRest + (this.membraneV[i] - this.params.vRest) * decay + synapticInput[i];
2103
+ if (v >= this.params.vThreshold) {
2104
+ this.spikes[i] = 1;
2105
+ this.membraneV[i] = this.params.vReset;
2106
+ this.refractory[i] = this.REFRACTORY_PERIOD;
2107
+ totalSpikes++;
2108
+ spikeIndices.push(i);
2109
+ } else {
2110
+ this.spikes[i] = 0;
2111
+ this.membraneV[i] = v;
2112
+ }
2113
+ }
2114
+ this.stepCount++;
2115
+ return {
2116
+ totalSpikes,
2117
+ spikeIndices,
2118
+ stepTimeMs: performance.now() - start,
2119
+ simTimeMs: this.stepCount * this.params.dt
2120
+ };
2121
+ }
2122
+ /**
2123
+ * Run N timesteps with the same input pattern.
2124
+ */
2125
+ stepN(count, synapticInput) {
2126
+ const results = [];
2127
+ for (let i = 0; i < count; i++) {
2128
+ results.push(this.step(synapticInput));
2129
+ }
2130
+ return results;
2131
+ }
2132
+ /** Get membrane potentials snapshot */
2133
+ getMembraneV() {
2134
+ return new Float32Array(this.membraneV);
2135
+ }
2136
+ /** Get spike flags snapshot */
2137
+ getSpikes() {
2138
+ return new Uint32Array(this.spikes);
2139
+ }
2140
+ /** Get refractory counters snapshot */
2141
+ getRefractory() {
2142
+ return new Float32Array(this.refractory);
2143
+ }
2144
+ /** Get current step count */
2145
+ get currentStep() {
2146
+ return this.stepCount;
2147
+ }
2148
+ /** Reset all neurons to resting state */
2149
+ reset() {
2150
+ this.membraneV.fill(this.params.vRest);
2151
+ this.refractory.fill(0);
2152
+ this.spikes.fill(0);
2153
+ this.stepCount = 0;
2154
+ }
2155
+ };
2156
+ function generateSynapticInput(neuronCount, seed = 42, minCurrent = 0, maxCurrent = 15) {
2157
+ const input = new Float32Array(neuronCount);
2158
+ let state = seed >>> 0 || 1;
2159
+ for (let i = 0; i < neuronCount; i++) {
2160
+ state ^= state << 13;
2161
+ state ^= state >>> 17;
2162
+ state ^= state << 5;
2163
+ const normalized = (state >>> 0) / 4294967295;
2164
+ input[i] = minCurrent + normalized * (maxCurrent - minCurrent);
2165
+ }
2166
+ return input;
2167
+ }
2168
+ function generateWeightMatrix(preCount, postCount, sparsity = 0.8, seed = 123) {
2169
+ const weights = new Float32Array(preCount * postCount);
2170
+ let state = seed >>> 0 || 1;
2171
+ for (let i = 0; i < weights.length; i++) {
2172
+ state ^= state << 13;
2173
+ state ^= state >>> 17;
2174
+ state ^= state << 5;
2175
+ const normalized = (state >>> 0) / 4294967295;
2176
+ if (normalized > sparsity) {
2177
+ state ^= state << 13;
2178
+ state ^= state >>> 17;
2179
+ state ^= state << 5;
2180
+ weights[i] = 0.1 + (state >>> 0) / 4294967295 * 0.9;
2181
+ }
2182
+ }
2183
+ return weights;
2184
+ }
2185
+ function xorshift32(seed) {
2186
+ let state = seed | 0 || 1;
2187
+ return () => {
2188
+ state ^= state << 13;
2189
+ state ^= state >>> 17;
2190
+ state ^= state << 5;
2191
+ return (state >>> 0) / 4294967296;
2192
+ };
2193
+ }
2194
+ function makeStimulus(neuronCount, seed, range) {
2195
+ const rng = xorshift32(seed);
2196
+ const span = range.max - range.min;
2197
+ const stim = new Float32Array(neuronCount);
2198
+ for (let i = 0; i < neuronCount; i++) {
2199
+ stim[i] = range.min + rng() * span;
2200
+ }
2201
+ return stim;
2202
+ }
2203
+ async function runLIFDeterminismProbe(ctx, options) {
2204
+ const neuronCount = options.neuronCount;
2205
+ const tickCount = options.tickCount;
2206
+ const stimulusSeed = options.stimulusSeed;
2207
+ const stimulusAmplitude = options.stimulusAmplitude ?? { min: 0, max: 1.2 };
2208
+ if (neuronCount <= 0 || !Number.isInteger(neuronCount)) {
2209
+ throw new Error(`LIFDeterminismProbe: neuronCount must be a positive integer, got ${neuronCount}`);
2210
+ }
2211
+ if (tickCount <= 0 || !Number.isInteger(tickCount)) {
2212
+ throw new Error(`LIFDeterminismProbe: tickCount must be a positive integer, got ${tickCount}`);
2213
+ }
2214
+ const sim = new LIFSimulator(ctx, neuronCount, options.params);
2215
+ try {
2216
+ await sim.initialize();
2217
+ sim.resetState();
2218
+ const stimulus = makeStimulus(neuronCount, stimulusSeed, stimulusAmplitude);
2219
+ sim.setSynapticInput(stimulus);
2220
+ await sim.stepN(tickCount);
2221
+ const readback = await sim.readMembranePotentials();
2222
+ return new Uint8Array(
2223
+ readback.data.buffer,
2224
+ readback.data.byteOffset,
2225
+ readback.data.byteLength
2226
+ );
2227
+ } finally {
2228
+ sim.destroy();
2229
+ }
2230
+ }
2231
+ var PAPER_2_CANONICAL_CONFIG = Object.freeze({
2232
+ neuronCount: 1e3,
2233
+ tickCount: 100,
2234
+ stimulusSeed: 42,
2235
+ stimulusAmplitude: { min: 0, max: 1.2 },
2236
+ params: DEFAULT_LIF_PARAMS
2237
+ });
2238
+ var ProphecyNotImplementedError = class extends Error {
2239
+ constructor(transport) {
2240
+ super(`Prophecy transport "${transport}" is not yet implemented`);
2241
+ this.transport = transport;
2242
+ this.name = "ProphecyNotImplementedError";
2243
+ }
2244
+ };
2245
+ var PROBE_STRIDE_BYTES = 32;
2246
+ var WORKGROUP_SIZE = 64;
2247
+ var ProphecyOrchestrator = class {
2248
+ constructor(ctx, config) {
2249
+ this.ctx = ctx;
2250
+ this.config = config;
2251
+ this.frameCounter = 0;
2252
+ this.lastFrame = null;
2253
+ this.initialized = false;
2254
+ this.uniformBuffer = null;
2255
+ this.spikeRatesBuffer = null;
2256
+ this.probePositionsBuffer = null;
2257
+ this.probeAlbedoBuffer = null;
2258
+ this.probesOutBuffer = null;
2259
+ this.readbackBuffer = null;
2260
+ this.lastSpikeRatesShadow = null;
2261
+ this.validateConfig(config);
2262
+ }
2263
+ /**
2264
+ * Validate the static config. Throws synchronously so callers see
2265
+ * the problem at construction time, not first frame.
2266
+ */
2267
+ validateConfig(config) {
2268
+ if (!Number.isInteger(config.probeCount) || config.probeCount <= 0) {
2269
+ throw new Error(
2270
+ `ProphecyOrchestrator: probeCount must be a positive integer, got ${config.probeCount}`
2271
+ );
2272
+ }
2273
+ if (config.probeCount % WORKGROUP_SIZE !== 0) {
2274
+ throw new Error(
2275
+ `ProphecyOrchestrator: probeCount must be a multiple of ${WORKGROUP_SIZE}, got ${config.probeCount}`
2276
+ );
2277
+ }
2278
+ const expectedPositions = config.probeCount * 3;
2279
+ if (config.probePositions.length !== expectedPositions) {
2280
+ throw new Error(
2281
+ `ProphecyOrchestrator: probePositions length ${config.probePositions.length} does not match probeCount*3 (${expectedPositions})`
2282
+ );
2283
+ }
2284
+ if (config.albedo && config.albedo.length !== expectedPositions) {
2285
+ throw new Error(
2286
+ `ProphecyOrchestrator: albedo length ${config.albedo.length} does not match probeCount*3 (${expectedPositions})`
2287
+ );
2288
+ }
2289
+ const floor = config.confidenceFloor ?? 0.05;
2290
+ if (floor < 0 || floor > 1) {
2291
+ throw new Error(
2292
+ `ProphecyOrchestrator: confidenceFloor must be in [0,1], got ${floor}`
2293
+ );
2294
+ }
2295
+ }
2296
+ /** Allocate GPU buffers and upload static probe positions / albedo. */
2297
+ initialize() {
2298
+ if (this.initialized) return;
2299
+ const device = this.ctx.device;
2300
+ const probeCount = this.config.probeCount;
2301
+ this.uniformBuffer = device.createBuffer({
2302
+ label: "prophecy:uniforms",
2303
+ size: 32,
2304
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
2305
+ });
2306
+ this.spikeRatesBuffer = device.createBuffer({
2307
+ label: "prophecy:spike_rates",
2308
+ size: probeCount * 4,
2309
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
2310
+ });
2311
+ this.probePositionsBuffer = device.createBuffer({
2312
+ label: "prophecy:probe_positions",
2313
+ size: probeCount * 3 * 4,
2314
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
2315
+ });
2316
+ device.queue.writeBuffer(
2317
+ this.probePositionsBuffer,
2318
+ 0,
2319
+ this.config.probePositions.buffer,
2320
+ this.config.probePositions.byteOffset,
2321
+ this.config.probePositions.byteLength
2322
+ );
2323
+ this.probeAlbedoBuffer = device.createBuffer({
2324
+ label: "prophecy:probe_albedo",
2325
+ size: probeCount * 3 * 4,
2326
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
2327
+ });
2328
+ const albedo = this.config.albedo ?? this.makeOnesAlbedo(probeCount);
2329
+ device.queue.writeBuffer(
2330
+ this.probeAlbedoBuffer,
2331
+ 0,
2332
+ albedo.buffer,
2333
+ albedo.byteOffset,
2334
+ albedo.byteLength
2335
+ );
2336
+ this.probesOutBuffer = device.createBuffer({
2337
+ label: "prophecy:probes_out",
2338
+ size: probeCount * PROBE_STRIDE_BYTES,
2339
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
2340
+ });
2341
+ this.readbackBuffer = device.createBuffer({
2342
+ label: "prophecy:readback",
2343
+ size: probeCount * PROBE_STRIDE_BYTES,
2344
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
2345
+ });
2346
+ this.initialized = true;
2347
+ }
2348
+ makeOnesAlbedo(probeCount) {
2349
+ const a = new Float32Array(probeCount * 3);
2350
+ a.fill(1);
2351
+ return a;
2352
+ }
2353
+ /**
2354
+ * Pack the per-frame uniform block. Layout must match
2355
+ * `ProphecyParams` in `prophetic-radiance.wgsl`.
2356
+ */
2357
+ packUniforms(ctx) {
2358
+ const buf = new ArrayBuffer(32);
2359
+ const u32 = new Uint32Array(buf);
2360
+ const f32 = new Float32Array(buf);
2361
+ u32[0] = this.config.probeCount;
2362
+ f32[1] = ctx.sunDirection[0];
2363
+ f32[2] = ctx.sunDirection[1];
2364
+ f32[3] = ctx.sunDirection[2];
2365
+ f32[4] = ctx.sunColor[0];
2366
+ f32[5] = ctx.sunColor[1];
2367
+ f32[6] = ctx.sunColor[2];
2368
+ f32[7] = this.config.confidenceFloor ?? 0.05;
2369
+ return buf;
2370
+ }
2371
+ /**
2372
+ * Public API. Caller supplies the raw spike rates (length =
2373
+ * probeCount). In a wired pipeline these come from the upstream
2374
+ * SNNNetwork's spike buffer after a rate-decode pass. Exposed
2375
+ * separately so this orchestrator stays testable without a network.
2376
+ */
2377
+ uploadSpikeRates(spikeRates) {
2378
+ this.assertInit();
2379
+ if (spikeRates.length !== this.config.probeCount) {
2380
+ throw new Error(
2381
+ `ProphecyOrchestrator.uploadSpikeRates: expected length ${this.config.probeCount}, got ${spikeRates.length}`
2382
+ );
2383
+ }
2384
+ this.ctx.device.queue.writeBuffer(
2385
+ this.spikeRatesBuffer,
2386
+ 0,
2387
+ spikeRates.buffer,
2388
+ spikeRates.byteOffset,
2389
+ spikeRates.byteLength
2390
+ );
2391
+ }
2392
+ /**
2393
+ * Run one frame. Currently performs CPU-side mixing (mirroring the
2394
+ * WGSL kernel) and returns a fully-decoded ProphecyFrame. The WGSL
2395
+ * dispatch path is plumbed but disabled by default until a
2396
+ * `PipelineFactory` registration is added — kept off-path so the
2397
+ * orchestrator works in unit-test environments without a real GPU.
2398
+ *
2399
+ * The CPU path is byte-equivalent to the WGSL kernel (verified by
2400
+ * the parity test). This is intentional: the WGSL kernel is the
2401
+ * production path, the CPU mirror is the reference.
2402
+ */
2403
+ step(scene) {
2404
+ this.assertInit();
2405
+ const t0 = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2406
+ const uniforms = this.packUniforms(scene);
2407
+ this.ctx.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
2408
+ const probes = this.computeProbesCpu(scene);
2409
+ const t1 = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2410
+ const frame = {
2411
+ frameId: this.frameCounter++,
2412
+ producedAtMs: Date.now(),
2413
+ productionTimeMs: t1 - t0,
2414
+ probes,
2415
+ source: "local"
2416
+ };
2417
+ this.lastFrame = frame;
2418
+ return frame;
2419
+ }
2420
+ /**
2421
+ * CPU mirror of `prophetic-radiance.wgsl`. Used as a) the current
2422
+ * production path until WGSL dispatch is enabled, and b) the
2423
+ * reference oracle for the parity test.
2424
+ *
2425
+ * MUST stay byte-equivalent to the shader.
2426
+ */
2427
+ computeProbesCpu(scene) {
2428
+ const n = this.config.probeCount;
2429
+ const positions = this.config.probePositions;
2430
+ const albedo = this.config.albedo ?? this.makeOnesAlbedo(n);
2431
+ const floor = this.config.confidenceFloor ?? 0.05;
2432
+ const sunY = scene.sunDirection[1];
2433
+ const facing = sunY > 0 ? sunY : 0;
2434
+ const facingMix = 0.4 + 0.6 * facing;
2435
+ const rates = this.lastSpikeRatesShadow ?? new Float32Array(n);
2436
+ const out = new Array(n);
2437
+ for (let i = 0; i < n; i++) {
2438
+ const rate = rates[i];
2439
+ const confidence = rate < 0 ? 0 : rate > 1 ? 1 : rate;
2440
+ let r = scene.sunColor[0] * albedo[i * 3 + 0] * confidence * facingMix;
2441
+ let g = scene.sunColor[1] * albedo[i * 3 + 1] * confidence * facingMix;
2442
+ let b = scene.sunColor[2] * albedo[i * 3 + 2] * confidence * facingMix;
2443
+ if (confidence < floor) {
2444
+ r = 0;
2445
+ g = 0;
2446
+ b = 0;
2447
+ }
2448
+ out[i] = {
2449
+ index: i,
2450
+ position: [
2451
+ positions[i * 3 + 0],
2452
+ positions[i * 3 + 1],
2453
+ positions[i * 3 + 2]
2454
+ ],
2455
+ rgb: [r, g, b],
2456
+ confidence
2457
+ };
2458
+ }
2459
+ return out;
2460
+ }
2461
+ /** Test/dev helper: prime the CPU shadow without touching the GPU
2462
+ * buffer, useful for unit tests. */
2463
+ primeSpikeRatesShadow(rates) {
2464
+ if (rates.length !== this.config.probeCount) {
2465
+ throw new Error(
2466
+ `primeSpikeRatesShadow: expected length ${this.config.probeCount}, got ${rates.length}`
2467
+ );
2468
+ }
2469
+ this.lastSpikeRatesShadow = new Float32Array(rates);
2470
+ }
2471
+ /** Most recent frame, or null if `step()` has not run yet. */
2472
+ getLastFrame() {
2473
+ return this.lastFrame;
2474
+ }
2475
+ /** Release GPU buffers. */
2476
+ destroy() {
2477
+ this.uniformBuffer?.destroy();
2478
+ this.spikeRatesBuffer?.destroy();
2479
+ this.probePositionsBuffer?.destroy();
2480
+ this.probeAlbedoBuffer?.destroy();
2481
+ this.probesOutBuffer?.destroy();
2482
+ this.readbackBuffer?.destroy();
2483
+ this.uniformBuffer = null;
2484
+ this.spikeRatesBuffer = null;
2485
+ this.probePositionsBuffer = null;
2486
+ this.probeAlbedoBuffer = null;
2487
+ this.probesOutBuffer = null;
2488
+ this.readbackBuffer = null;
2489
+ this.initialized = false;
2490
+ this.lastSpikeRatesShadow = null;
2491
+ }
2492
+ assertInit() {
2493
+ if (!this.initialized) {
2494
+ throw new Error("ProphecyOrchestrator: must call initialize() first");
2495
+ }
2496
+ }
2497
+ };
2498
+ var LocalProphecyTransport = class {
2499
+ constructor(options) {
2500
+ this.options = options;
2501
+ this.kind = "local";
2502
+ this.orchestrator = null;
2503
+ }
2504
+ async initialize(config) {
2505
+ this.orchestrator = new ProphecyOrchestrator(this.options.ctx, config);
2506
+ this.orchestrator.initialize();
2507
+ }
2508
+ async step(scene) {
2509
+ if (!this.orchestrator) {
2510
+ throw new Error("LocalProphecyTransport: not initialized");
2511
+ }
2512
+ const rates = await this.options.spikeRates(
2513
+ scene,
2514
+ this.orchestrator.getLastFrame()?.probes.length ?? // Probe count is fixed at initialize-time; if no frame has run
2515
+ // yet we ask the provider for the configured count. We can
2516
+ // recover that from the orchestrator's last upload, or we
2517
+ // honour what the provider returns. We choose the latter for
2518
+ // simplicity and validate length in primeSpikeRatesShadow.
2519
+ 0
2520
+ );
2521
+ this.orchestrator.primeSpikeRatesShadow(rates);
2522
+ this.orchestrator.uploadSpikeRates(rates);
2523
+ return this.orchestrator.step(scene);
2524
+ }
2525
+ async destroy() {
2526
+ this.orchestrator?.destroy();
2527
+ this.orchestrator = null;
2528
+ }
2529
+ };
2530
+ function resolveTtuEndpoint(endpoint, holomeshHost = "https://mcp.holoscript.net") {
2531
+ if (!endpoint || typeof endpoint !== "string") {
2532
+ throw new Error("HoloMeshProphecyTransport: endpoint must be a non-empty string");
2533
+ }
2534
+ const host = holomeshHost.replace(/\/+$/, "");
2535
+ if (endpoint.startsWith("crdt://")) {
2536
+ const rest = endpoint.slice("crdt://".length);
2537
+ const parts = rest.split("/").filter(Boolean);
2538
+ if (parts.length < 4 || parts[0] !== "holomesh" || parts[1] !== "feed" || parts[2] !== "ttu") {
2539
+ throw new Error(
2540
+ `HoloMeshProphecyTransport: unsupported CRDT URI shape: ${endpoint} (expected crdt://holomesh/feed/ttu/<sessionId>)`
2541
+ );
2542
+ }
2543
+ const sessionId = parts.slice(3).join("/");
2544
+ return { base: `${host}/api/holomesh/ttu/${sessionId}`, sessionId };
2545
+ }
2546
+ if (endpoint.startsWith("ttu://")) {
2547
+ const rest = endpoint.slice("ttu://".length);
2548
+ const parts = rest.split("/").filter(Boolean);
2549
+ if (parts.length < 2) {
2550
+ throw new Error(
2551
+ `HoloMeshProphecyTransport: unsupported ttu URI shape: ${endpoint}`
2552
+ );
2553
+ }
2554
+ const targetHost = parts[0].includes("://") ? parts[0] : `https://${parts[0]}`;
2555
+ const sessionId = parts.slice(1).join("/");
2556
+ return {
2557
+ base: `${targetHost}/api/holomesh/ttu/${sessionId}`,
2558
+ sessionId
2559
+ };
2560
+ }
2561
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
2562
+ const trimmed = endpoint.replace(/\/+$/, "");
2563
+ const m = trimmed.match(/\/api\/holomesh\/ttu\/([^/]+)$/);
2564
+ if (!m) {
2565
+ throw new Error(
2566
+ `HoloMeshProphecyTransport: http(s) endpoint must end with /api/holomesh/ttu/<sessionId>, got: ${endpoint}`
2567
+ );
2568
+ }
2569
+ return { base: trimmed, sessionId: m[1] };
2570
+ }
2571
+ throw new Error(
2572
+ `HoloMeshProphecyTransport: unsupported endpoint scheme: ${endpoint} (use crdt://, ttu://, or http(s)://)`
2573
+ );
2574
+ }
2575
+ var HoloMeshProphecyTransport = class {
2576
+ constructor(options) {
2577
+ this.options = options;
2578
+ this.kind = "holomesh";
2579
+ this.resolved = null;
2580
+ this.initializedFallback = false;
2581
+ this.lastFrame = null;
2582
+ this.latencyBudgetMs = options.latencyBudgetMs ?? 5e3;
2583
+ this.fetchImpl = options.fetchImpl ?? ((...args) => {
2584
+ const f = globalThis.fetch;
2585
+ if (!f) {
2586
+ throw new Error(
2587
+ "HoloMeshProphecyTransport: global fetch is unavailable; pass options.fetchImpl"
2588
+ );
2589
+ }
2590
+ return f(...args);
2591
+ });
2592
+ }
2593
+ async initialize(config) {
2594
+ this.resolved = resolveTtuEndpoint(
2595
+ this.options.endpoint,
2596
+ this.options.holomeshHost
2597
+ );
2598
+ if (this.options.fallback && !this.initializedFallback) {
2599
+ await this.options.fallback.initialize(config);
2600
+ this.initializedFallback = true;
2601
+ }
2602
+ }
2603
+ async step(scene) {
2604
+ if (!this.resolved) {
2605
+ throw new Error("HoloMeshProphecyTransport: must call initialize() first");
2606
+ }
2607
+ const url = `${this.resolved.base}/step`;
2608
+ const headers = {
2609
+ "Content-Type": "application/json"
2610
+ };
2611
+ if (this.options.apiKey) {
2612
+ headers["Authorization"] = `Bearer ${this.options.apiKey}`;
2613
+ }
2614
+ const body = JSON.stringify({
2615
+ scene,
2616
+ agent_id: this.options.agentId ?? "anonymous",
2617
+ // Server-side budget is matched to the caller's; the AbortController
2618
+ // gives us a hard client-side ceiling so a hung server can't block
2619
+ // the renderer past one frame.
2620
+ timeout_ms: this.latencyBudgetMs
2621
+ });
2622
+ const ac = new AbortController();
2623
+ const timer = setTimeout(() => ac.abort(), this.latencyBudgetMs);
2624
+ let frame = null;
2625
+ let lastError = null;
2626
+ try {
2627
+ const res = await this.fetchImpl(url, {
2628
+ method: "POST",
2629
+ headers,
2630
+ body,
2631
+ signal: ac.signal
2632
+ });
2633
+ if (!res.ok) {
2634
+ lastError = new Error(`ttu /step returned ${res.status}`);
2635
+ } else {
2636
+ const data = await res.json();
2637
+ if (data?.success && data.frame) {
2638
+ frame = { ...data.frame, source: "holomesh" };
2639
+ } else {
2640
+ lastError = new Error("ttu /step returned malformed payload");
2641
+ }
2642
+ }
2643
+ } catch (err) {
2644
+ lastError = err;
2645
+ } finally {
2646
+ clearTimeout(timer);
2647
+ }
2648
+ if (frame) {
2649
+ this.lastFrame = frame;
2650
+ return frame;
2651
+ }
2652
+ if (this.options.fallback) {
2653
+ const fb = await this.options.fallback.step(scene);
2654
+ const tagged = { ...fb, source: "fallback" };
2655
+ this.lastFrame = tagged;
2656
+ return tagged;
2657
+ }
2658
+ const reason = lastError instanceof Error ? lastError.message : String(lastError ?? "unknown");
2659
+ throw new ProphecyNotImplementedError(
2660
+ `holomesh@${this.options.endpoint} (no fallback configured; last error: ${reason})`
2661
+ );
2662
+ }
2663
+ async destroy() {
2664
+ if (this.options.fallback && this.initializedFallback) {
2665
+ await this.options.fallback.destroy();
2666
+ this.initializedFallback = false;
2667
+ }
2668
+ this.lastFrame = null;
2669
+ this.resolved = null;
2670
+ }
2671
+ };
2672
+ var DEFAULT_LOTUS_RINGS = [
2673
+ { name: "inner", count: 8, cup: 0.86, gravity_sag: 0.02 },
2674
+ { name: "mid", count: 13, cup: 0.5, gravity_sag: 0.12 },
2675
+ { name: "outer", count: 21, cup: 0.24, gravity_sag: 0.3 }
2676
+ ];
2677
+ var DEFAULT_COLORS2 = {
2678
+ petal_base: "#fff1f6",
2679
+ petal_mid: "#f47ab7",
2680
+ petal_inner: "#ff9ecf",
2681
+ petal_rim: "#c42a86",
2682
+ petal_shadow: "#84205f",
2683
+ seed_pod: "#f4d74a",
2684
+ seed_pod_rim: "#b7c66b",
2685
+ stamen: "#f59e0b",
2686
+ stamen_tip: "#fff4bd",
2687
+ leaf: "#235f4f",
2688
+ leaf_dark: "#102f28",
2689
+ water: "#07140f"
2690
+ };
2691
+ function extractBotanicalLotusMaterial(samples, options = {}) {
2692
+ if (samples.length === 0) {
2693
+ throw new Error("extractBotanicalLotusMaterial requires at least one image sample");
2694
+ }
2695
+ const buckets = createBuckets();
2696
+ for (const sample of samples) {
2697
+ collectSamplePixels(sample, buckets);
2698
+ }
2699
+ const petal = buckets.petal;
2700
+ const leaf = buckets.leaf;
2701
+ const stamen = buckets.stamen;
2702
+ const water = buckets.water;
2703
+ const meanPetalLuma = mean(petal, (pixel) => pixel.luma, 0.62);
2704
+ const meanPetalSat = mean(petal, (pixel) => pixel.s, 0.62);
2705
+ const petalGradient = mean(petal, (pixel) => pixel.gradient, 0.08);
2706
+ const petalEdgeLuma = mean(
2707
+ petal.filter((pixel) => pixel.edge > 0.72),
2708
+ (pixel) => pixel.luma,
2709
+ meanPetalLuma
2710
+ );
2711
+ const petalCenterLuma = mean(
2712
+ petal.filter((pixel) => pixel.edge < 0.38),
2713
+ (pixel) => pixel.luma,
2714
+ meanPetalLuma
2715
+ );
2716
+ const backlightLift = clamp01(petalEdgeLuma - petalCenterLuma + 0.5);
2717
+ const veinNormal = clamp2(round4(0.018 + petalGradient * 0.22), 0.018, 0.09);
2718
+ const colors = deriveColors(petal, leaf, stamen, water);
2719
+ const material = {
2720
+ subsurface_scattering: clamp2(round4(0.48 + meanPetalSat * 0.22 + backlightLift * 0.18), 0.45, 0.82),
2721
+ subsurface_radius_rgb: deriveSubsurfaceRadius(colors.petal_mid),
2722
+ petal_translucency_base: clamp2(round4(0.42 + meanPetalLuma * 0.24 + backlightLift * 0.18), 0.38, 0.78),
2723
+ petal_translucency_edge: clamp2(round4(0.24 + petalEdgeLuma * 0.18), 0.24, 0.48),
2724
+ roughness: clamp2(round4(0.64 + veinNormal * 1.4), 0.62, 0.82),
2725
+ ior: clamp2(round4(1.33 + meanPetalSat * 0.055), 1.33, 1.39),
2726
+ vein_normal_intensity: veinNormal,
2727
+ edge_curl_intensity: clamp2(round4(0.34 + mean(petal, (pixel) => pixel.edge, 0.5) * 0.38), 0.34, 0.68),
2728
+ gravity_sag_outer: clamp2(round4(0.18 + meanPetalLuma * 0.18), 0.18, 0.36)
2729
+ };
2730
+ const provenanceHashCount = samples.filter((sample) => sample.provenance?.contentHash).length;
2731
+ const provenanceSignatureCount = samples.filter((sample) => sample.provenance?.walletSignature).length;
2732
+ const contentHashStatus = completenessStatus(provenanceHashCount, samples.length);
2733
+ const walletSignatureStatus = completenessStatus(provenanceSignatureCount, samples.length);
2734
+ const signed = contentHashStatus === "complete" && walletSignatureStatus === "complete";
2735
+ return {
2736
+ schema: "holoscript.botanical.material-extract.v1",
2737
+ status: signed ? "extracted_from_signed_references" : "extracted_pending_cael_anchor",
2738
+ captured_at: options.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
2739
+ generated_by: options.generatedBy ?? "@holoscript/snn-webgpu/material-extraction/botanical-lotus",
2740
+ ...options.referenceManifest ? { reference_manifest: options.referenceManifest } : {},
2741
+ source: {
2742
+ kind: options.sourceKind ?? "pixel_reference_images",
2743
+ count: samples.length,
2744
+ anchor_ids: samples.map((sample) => sample.id),
2745
+ content_hash_status: contentHashStatus,
2746
+ wallet_signature_status: walletSignatureStatus
2747
+ },
2748
+ botanical_trait_target: options.traitName ?? "@botanical_lotus",
2749
+ material,
2750
+ colors,
2751
+ geometry: {
2752
+ petal_rings: DEFAULT_LOTUS_RINGS.map((ring) => ({ ...ring })),
2753
+ petal_shape: "broad_elliptic_pointed_tip_with_center_ridge",
2754
+ stamen_filament_count: estimateStamenCount(stamen.length, petal.length),
2755
+ seed_pod_dot_pattern: "1_center_7_12_18_radial"
2756
+ },
2757
+ diagnostics: {
2758
+ sample_count: samples.length,
2759
+ pixel_count: buckets.all.length,
2760
+ petal_pixel_count: petal.length,
2761
+ leaf_pixel_count: leaf.length,
2762
+ stamen_pixel_count: stamen.length,
2763
+ water_pixel_count: water.length,
2764
+ mean_petal_luminance: round4(meanPetalLuma),
2765
+ mean_petal_saturation: round4(meanPetalSat),
2766
+ petal_gradient_energy: round4(petalGradient),
2767
+ provenance_hash_count: provenanceHashCount,
2768
+ provenance_signature_count: provenanceSignatureCount,
2769
+ confidence: estimateConfidence(samples.length, petal.length, leaf.length, stamen.length)
2770
+ }
2771
+ };
2772
+ }
2773
+ function toBotanicalLotusTrait(extraction) {
2774
+ const material = extraction.material;
2775
+ const colors = extraction.colors;
2776
+ return [
2777
+ `trait ${extraction.botanical_trait_target} {`,
2778
+ ` // Generated by ${extraction.generated_by}`,
2779
+ ` reference_status: "${extraction.status}"`,
2780
+ ` subsurface_scattering: ${material.subsurface_scattering}`,
2781
+ ` subsurface_radius: [${material.subsurface_radius_rgb.join(", ")}]`,
2782
+ ` petal_translucency_base: ${material.petal_translucency_base}`,
2783
+ ` petal_translucency_edge: ${material.petal_translucency_edge}`,
2784
+ ` vein_normal_intensity: ${material.vein_normal_intensity}`,
2785
+ ` edge_curl_intensity: ${material.edge_curl_intensity}`,
2786
+ ` gravity_sag_outer: ${material.gravity_sag_outer}`,
2787
+ ` color_base: "${colors.petal_base}"`,
2788
+ ` color_mid: "${colors.petal_mid}"`,
2789
+ ` color_edge: "${colors.petal_rim}"`,
2790
+ ` stamen_color: "${colors.stamen}"`,
2791
+ `}`
2792
+ ].join("\n");
2793
+ }
2794
+ function createBuckets() {
2795
+ return {
2796
+ petal: [],
2797
+ leaf: [],
2798
+ stamen: [],
2799
+ water: [],
2800
+ all: []
2801
+ };
2802
+ }
2803
+ function collectSamplePixels(sample, buckets) {
2804
+ assertSampleShape(sample);
2805
+ const { width, height, data } = sample;
2806
+ for (let y = 0; y < height; y += 1) {
2807
+ for (let x = 0; x < width; x += 1) {
2808
+ const offset = (y * width + x) * 4;
2809
+ const alpha = data[offset + 3] ?? 255;
2810
+ if (alpha < 16) {
2811
+ continue;
2812
+ }
2813
+ const r = (data[offset] ?? 0) / 255;
2814
+ const g = (data[offset + 1] ?? 0) / 255;
2815
+ const b = (data[offset + 2] ?? 0) / 255;
2816
+ const hsv = rgbToHsv(r, g, b);
2817
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
2818
+ const gradient = localGradient(data, width, height, x, y, luma);
2819
+ const edge = normalizedEdgeDistance(width, height, x, y);
2820
+ const feature = { r, g, b, h: hsv.h, s: hsv.s, v: hsv.v, luma, gradient, edge };
2821
+ buckets.all.push(feature);
2822
+ if (isLotusPetal(feature)) {
2823
+ buckets.petal.push(feature);
2824
+ } else if (isLotusStamen(feature)) {
2825
+ buckets.stamen.push(feature);
2826
+ } else if (isLotusLeaf(feature)) {
2827
+ buckets.leaf.push(feature);
2828
+ } else if (isWater(feature)) {
2829
+ buckets.water.push(feature);
2830
+ }
2831
+ }
2832
+ }
2833
+ }
2834
+ function assertSampleShape(sample) {
2835
+ if (!Number.isInteger(sample.width) || !Number.isInteger(sample.height)) {
2836
+ throw new Error(`Image sample ${sample.id} must have integer width and height`);
2837
+ }
2838
+ if (sample.width <= 0 || sample.height <= 0) {
2839
+ throw new Error(`Image sample ${sample.id} must have positive width and height`);
2840
+ }
2841
+ const expected = sample.width * sample.height * 4;
2842
+ if (sample.data.length < expected) {
2843
+ throw new Error(`Image sample ${sample.id} has ${sample.data.length} RGBA values; expected ${expected}`);
2844
+ }
2845
+ }
2846
+ function deriveColors(petal, leaf, stamen, water) {
2847
+ return {
2848
+ petal_base: pixelToHex(percentilePixel(petal, "luma", 0.92, DEFAULT_COLORS2.petal_base)),
2849
+ petal_mid: pixelToHex(percentilePixel(petal, "s", 0.55, DEFAULT_COLORS2.petal_mid)),
2850
+ petal_inner: pixelToHex(percentilePixel(petal, "luma", 0.72, DEFAULT_COLORS2.petal_inner)),
2851
+ petal_rim: pixelToHex(percentilePixel(petal, "s", 0.88, DEFAULT_COLORS2.petal_rim)),
2852
+ petal_shadow: pixelToHex(percentilePixel(petal, "luma", 0.16, DEFAULT_COLORS2.petal_shadow)),
2853
+ seed_pod: pixelToHex(percentilePixel(stamen, "luma", 0.82, DEFAULT_COLORS2.seed_pod)),
2854
+ seed_pod_rim: mixHex(pixelToHex(percentilePixel(stamen, "luma", 0.62, DEFAULT_COLORS2.seed_pod_rim)), DEFAULT_COLORS2.seed_pod_rim, 0.35),
2855
+ stamen: pixelToHex(percentilePixel(stamen, "s", 0.68, DEFAULT_COLORS2.stamen)),
2856
+ stamen_tip: pixelToHex(percentilePixel(stamen, "luma", 0.95, DEFAULT_COLORS2.stamen_tip)),
2857
+ leaf: pixelToHex(percentilePixel(leaf, "luma", 0.58, DEFAULT_COLORS2.leaf)),
2858
+ leaf_dark: pixelToHex(percentilePixel(leaf, "luma", 0.18, DEFAULT_COLORS2.leaf_dark)),
2859
+ water: pixelToHex(percentilePixel(water, "luma", 0.12, DEFAULT_COLORS2.water))
2860
+ };
2861
+ }
2862
+ function percentilePixel(pixels, field, percentile, fallbackHex) {
2863
+ if (pixels.length === 0) {
2864
+ return hexToPixel(fallbackHex);
2865
+ }
2866
+ const sorted = [...pixels].sort((a, b) => a[field] - b[field]);
2867
+ const index = clamp2(Math.round((sorted.length - 1) * percentile), 0, sorted.length - 1);
2868
+ return sorted[index];
2869
+ }
2870
+ function isLotusPetal(pixel) {
2871
+ const magentaHue = pixel.h >= 300 || pixel.h <= 18;
2872
+ const pinkHue = pixel.h >= 285 && pixel.h <= 360;
2873
+ const redDominant = pixel.r > pixel.g * 1.12 && pixel.b > pixel.g * 0.58;
2874
+ return (magentaHue || pinkHue) && redDominant && pixel.s > 0.16 && pixel.v > 0.22;
2875
+ }
2876
+ function isLotusStamen(pixel) {
2877
+ return pixel.h >= 32 && pixel.h <= 72 && pixel.s > 0.28 && pixel.v > 0.32;
2878
+ }
2879
+ function isLotusLeaf(pixel) {
2880
+ return pixel.h >= 78 && pixel.h <= 178 && pixel.s > 0.12 && pixel.v > 0.08 && pixel.g >= pixel.r * 0.72;
2881
+ }
2882
+ function isWater(pixel) {
2883
+ return pixel.v < 0.16 && pixel.g >= pixel.r * 0.7;
2884
+ }
2885
+ function estimateStamenCount(stamenPixels, petalPixels) {
2886
+ if (stamenPixels === 0 || petalPixels === 0) {
2887
+ return 58;
2888
+ }
2889
+ const ratio = stamenPixels / Math.max(1, petalPixels);
2890
+ return Math.round(clamp2(42 + ratio * 260, 42, 96));
2891
+ }
2892
+ function estimateConfidence(sampleCount, petalPixels, leafPixels, stamenPixels) {
2893
+ const sampleSignal = clamp01(sampleCount / 3);
2894
+ const petalSignal = clamp01(petalPixels / 300);
2895
+ const contextSignal = clamp01((leafPixels + stamenPixels) / 160);
2896
+ return round4(clamp01(0.2 + sampleSignal * 0.25 + petalSignal * 0.35 + contextSignal * 0.2));
2897
+ }
2898
+ function deriveSubsurfaceRadius(hex) {
2899
+ const pixel = hexToPixel(hex);
2900
+ return [
2901
+ round4(clamp2(0.72 + pixel.r * 0.24, 0.72, 0.96)),
2902
+ round4(clamp2(0.18 + pixel.g * 0.22, 0.18, 0.42)),
2903
+ round4(clamp2(0.46 + pixel.b * 0.32, 0.46, 0.78))
2904
+ ];
2905
+ }
2906
+ function completenessStatus(count, total) {
2907
+ if (count === 0) {
2908
+ return "missing";
2909
+ }
2910
+ return count === total ? "complete" : "partial";
2911
+ }
2912
+ function localGradient(data, width, height, x, y, luma) {
2913
+ const rightX = Math.min(width - 1, x + 1);
2914
+ const downY = Math.min(height - 1, y + 1);
2915
+ const right = lumaAt(data, width, rightX, y);
2916
+ const down = lumaAt(data, width, x, downY);
2917
+ return Math.sqrt((right - luma) ** 2 + (down - luma) ** 2);
2918
+ }
2919
+ function lumaAt(data, width, x, y) {
2920
+ const offset = (y * width + x) * 4;
2921
+ const r = (data[offset] ?? 0) / 255;
2922
+ const g = (data[offset + 1] ?? 0) / 255;
2923
+ const b = (data[offset + 2] ?? 0) / 255;
2924
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
2925
+ }
2926
+ function normalizedEdgeDistance(width, height, x, y) {
2927
+ const dx = Math.abs(x / Math.max(1, width - 1) - 0.5) * 2;
2928
+ const dy = Math.abs(y / Math.max(1, height - 1) - 0.5) * 2;
2929
+ return clamp01(Math.max(dx, dy));
2930
+ }
2931
+ function rgbToHsv(r, g, b) {
2932
+ const max = Math.max(r, g, b);
2933
+ const min = Math.min(r, g, b);
2934
+ const delta = max - min;
2935
+ let h = 0;
2936
+ if (delta > 0) {
2937
+ if (max === r) {
2938
+ h = 60 * ((g - b) / delta % 6);
2939
+ } else if (max === g) {
2940
+ h = 60 * ((b - r) / delta + 2);
2941
+ } else {
2942
+ h = 60 * ((r - g) / delta + 4);
2943
+ }
2944
+ }
2945
+ return {
2946
+ h: h < 0 ? h + 360 : h,
2947
+ s: max === 0 ? 0 : delta / max,
2948
+ v: max
2949
+ };
2950
+ }
2951
+ function pixelToHex(pixel) {
2952
+ const r = byteToHex(pixel.r);
2953
+ const g = byteToHex(pixel.g);
2954
+ const b = byteToHex(pixel.b);
2955
+ return `#${r}${g}${b}`;
2956
+ }
2957
+ function hexToPixel(hex) {
2958
+ const normalized = hex.replace("#", "");
2959
+ const r = Number.parseInt(normalized.slice(0, 2), 16) / 255;
2960
+ const g = Number.parseInt(normalized.slice(2, 4), 16) / 255;
2961
+ const b = Number.parseInt(normalized.slice(4, 6), 16) / 255;
2962
+ const hsv = rgbToHsv(r, g, b);
2963
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
2964
+ return { r, g, b, h: hsv.h, s: hsv.s, v: hsv.v, luma, gradient: 0, edge: 0.5 };
2965
+ }
2966
+ function mixHex(a, b, amountB) {
2967
+ const left = hexToPixel(a);
2968
+ const right = hexToPixel(b);
2969
+ return pixelToHex({
2970
+ r: left.r * (1 - amountB) + right.r * amountB,
2971
+ g: left.g * (1 - amountB) + right.g * amountB,
2972
+ b: left.b * (1 - amountB) + right.b * amountB});
2973
+ }
2974
+ function byteToHex(value) {
2975
+ return Math.round(clamp01(value) * 255).toString(16).padStart(2, "0");
2976
+ }
2977
+ function mean(items, selector, fallback) {
2978
+ if (items.length === 0) {
2979
+ return fallback;
2980
+ }
2981
+ return items.reduce((sum, item) => sum + selector(item), 0) / items.length;
2982
+ }
2983
+ function clamp01(value) {
2984
+ return clamp2(value, 0, 1);
2985
+ }
2986
+ function clamp2(value, min, max) {
2987
+ return Math.min(max, Math.max(min, value));
2988
+ }
2989
+ function round4(value) {
2990
+ return Math.round(value * 1e4) / 1e4;
2991
+ }
2992
+ function normalizeBotanicalMaterialExtraction(extraction) {
2993
+ if (isPixelExtraction(extraction)) {
2994
+ return normalizePixelExtraction(extraction);
2995
+ }
2996
+ return normalizeFixtureExtraction(extraction);
2997
+ }
2998
+ function normalizePixelExtraction(extraction) {
2999
+ const contentHashStatus = extraction.source.content_hash_status;
3000
+ const walletSignatureStatus = extraction.source.wallet_signature_status;
3001
+ return {
3002
+ schema: "holoscript.botanical.material-extract.canonical.v1",
3003
+ source_schema: extraction.schema,
3004
+ source_kind: "pixel-buffer",
3005
+ status: extraction.status === "extracted_from_signed_references" ? "signed" : "pending_anchor",
3006
+ botanical_trait_target: extraction.botanical_trait_target,
3007
+ anchor_ids: extraction.source.anchor_ids,
3008
+ material: normalizeMaterial(extraction.material),
3009
+ colors: extraction.colors,
3010
+ confidence: extraction.diagnostics.confidence,
3011
+ provenance: {
3012
+ content_hash_status: contentHashStatus,
3013
+ wallet_signature_status: walletSignatureStatus,
3014
+ signed_anchor_count: extraction.diagnostics.provenance_signature_count,
3015
+ content_hashed_anchor_count: extraction.diagnostics.provenance_hash_count
3016
+ }
3017
+ };
3018
+ }
3019
+ function normalizeFixtureExtraction(extraction) {
3020
+ return {
3021
+ schema: "holoscript.botanical.material-extract.canonical.v1",
3022
+ source_schema: extraction.schema,
3023
+ source_kind: "photo-fixture",
3024
+ status: normalizeFixtureStatus(extraction.status),
3025
+ botanical_trait_target: extraction.botanical_trait_target,
3026
+ anchor_ids: extraction.source.anchor_ids,
3027
+ material: normalizeMaterial(extraction.material),
3028
+ colors: extraction.colors,
3029
+ confidence: extraction.confidence.overall,
3030
+ provenance: {
3031
+ content_hash_status: completenessStatus2(
3032
+ extraction.source.content_hashed_anchor_count,
3033
+ extraction.source.anchor_ids.length
3034
+ ),
3035
+ wallet_signature_status: completenessStatus2(
3036
+ extraction.source.signed_anchor_count,
3037
+ extraction.source.anchor_ids.length
3038
+ ),
3039
+ signed_anchor_count: extraction.source.signed_anchor_count,
3040
+ content_hashed_anchor_count: extraction.source.content_hashed_anchor_count
3041
+ }
3042
+ };
3043
+ }
3044
+ function normalizeMaterial(material) {
3045
+ return {
3046
+ subsurface_scattering: material.subsurface_scattering,
3047
+ subsurface_radius_rgb: material.subsurface_radius_rgb,
3048
+ petal_translucency_base: material.petal_translucency_base,
3049
+ petal_translucency_edge: material.petal_translucency_edge,
3050
+ roughness: material.roughness,
3051
+ ior: material.ior,
3052
+ vein_normal_intensity: material.vein_normal_intensity,
3053
+ edge_curl_intensity: material.edge_curl_intensity,
3054
+ gravity_sag_outer: material.gravity_sag_outer
3055
+ };
3056
+ }
3057
+ function isPixelExtraction(extraction) {
3058
+ return "diagnostics" in extraction;
3059
+ }
3060
+ function normalizeFixtureStatus(status) {
3061
+ if (status === "extracted-from-signed-anchors") {
3062
+ return "signed";
3063
+ }
3064
+ if (status === "extracted-from-hashed-fixtures") {
3065
+ return "hashed";
3066
+ }
3067
+ return "unsigned";
3068
+ }
3069
+ function completenessStatus2(count, total) {
3070
+ if (count === 0 || total === 0) {
3071
+ return "missing";
3072
+ }
3073
+ return count === total ? "complete" : "partial";
3074
+ }
3075
+
3076
+ exports.BufferManager = BufferManager;
3077
+ exports.CPUReferenceSimulator = CPUReferenceSimulator;
3078
+ exports.DEFAULT_DECODE_PARAMS = DEFAULT_DECODE_PARAMS;
3079
+ exports.DEFAULT_ENCODE_PARAMS = DEFAULT_ENCODE_PARAMS;
3080
+ exports.DEFAULT_LIF_PARAMS = DEFAULT_LIF_PARAMS;
3081
+ exports.DecodingMode = DecodingMode;
3082
+ exports.EncodingMode = EncodingMode;
3083
+ exports.GPUContext = GPUContext;
3084
+ exports.HoloMeshProphecyTransport = HoloMeshProphecyTransport;
3085
+ exports.LIFSimulator = LIFSimulator;
3086
+ exports.LocalProphecyTransport = LocalProphecyTransport;
3087
+ exports.PAPER_2_CANONICAL_CONFIG = PAPER_2_CANONICAL_CONFIG;
3088
+ exports.PipelineFactory = PipelineFactory;
3089
+ exports.ProphecyNotImplementedError = ProphecyNotImplementedError;
3090
+ exports.ProphecyOrchestrator = ProphecyOrchestrator;
3091
+ exports.SNNNetwork = SNNNetwork;
3092
+ exports.SpikeDecoder = SpikeDecoder;
3093
+ exports.SpikeEncoder = SpikeEncoder;
3094
+ exports.TROPICAL_INF = TROPICAL_INF;
3095
+ exports.TropicalActivationTrait = TropicalActivationTrait;
3096
+ exports.TropicalShortestPaths = TropicalShortestPaths;
3097
+ exports.assertGraphShape = assertGraphShape;
3098
+ exports.computeDispatchSize = computeDispatchSize;
3099
+ exports.csrToDense = csrToDense;
3100
+ exports.denseToCSR = denseToCSR;
3101
+ exports.ensureNodeWebGpu = ensureNodeWebGpu;
3102
+ exports.ensureNodeWebGpuSync = ensureNodeWebGpuSync;
3103
+ exports.extractBotanicalLotusMaterial = extractBotanicalLotusMaterial;
3104
+ exports.extractBotanicalMaterialFromPhotoFixtures = extractBotanicalMaterialFromPhotoFixtures;
3105
+ exports.fromEdges = fromEdges;
3106
+ exports.generateSynapticInput = generateSynapticInput;
3107
+ exports.generateWeightMatrix = generateWeightMatrix;
3108
+ exports.normalizeAdjacency = normalizeAdjacency;
3109
+ exports.normalizeBotanicalMaterialExtraction = normalizeBotanicalMaterialExtraction;
3110
+ exports.runLIFDeterminismProbe = runLIFDeterminismProbe;
3111
+ exports.toBotanicalLotusTrait = toBotanicalLotusTrait;
3112
+ //# sourceMappingURL=dist-ACXG6IH5.cjs.map
3113
+ //# sourceMappingURL=dist-ACXG6IH5.cjs.map