creating-intelligence 0.1.0a7__py3-none-win_amd64.whl
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.
- creating_intelligence/__init__.py +41 -0
- creating_intelligence/circuits.py +1945 -0
- creating_intelligence/libmemory.pyd +0 -0
- creating_intelligence/memory_c_ffi.py +170 -0
- creating_intelligence/memory_python.py +143 -0
- creating_intelligence-0.1.0a7.dist-info/METADATA +55 -0
- creating_intelligence-0.1.0a7.dist-info/RECORD +9 -0
- creating_intelligence-0.1.0a7.dist-info/WHEEL +5 -0
- creating_intelligence-0.1.0a7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1945 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026 Peter Overmann
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
This file is part of the "Creating Intelligence" project. It is licensed
|
|
7
|
+
under the MIT License. You may obtain a copy of the License in the LICENSE
|
|
8
|
+
file in the root directory of this repository.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
11
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
12
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
This package employs a standardized plugin mechanism for circuit components,
|
|
19
|
+
encoders (preprocessing), decoders (postprocessing), circuit embedding mechanisms,
|
|
20
|
+
and auto-associative update rules.
|
|
21
|
+
|
|
22
|
+
Plugins are defined via factory functions that take a dictionary as input
|
|
23
|
+
and dispatch a dictionary, using closures encapsulate stateful evaluation functions.
|
|
24
|
+
|
|
25
|
+
The function names match the corresponding value strings in the JSON dataflow description,
|
|
26
|
+
for example {"component": "auto", "plugin": "replacement", ...}
|
|
27
|
+
|
|
28
|
+
Users can patch custom plugins into the circuits namespace via:
|
|
29
|
+
|
|
30
|
+
import creating_intelligence
|
|
31
|
+
from myplugins import mycomponent
|
|
32
|
+
circuits.mycomponent = mycomponent
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Formatting: autopep8 --ignore=E251 -a -a -i circuits.py
|
|
38
|
+
|
|
39
|
+
import itertools
|
|
40
|
+
import math
|
|
41
|
+
import sys
|
|
42
|
+
import os
|
|
43
|
+
import json
|
|
44
|
+
from collections import Counter
|
|
45
|
+
import numpy as np
|
|
46
|
+
import scipy.sparse as sp
|
|
47
|
+
import networkx as nx
|
|
48
|
+
from creating_intelligence import Memory
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Global RNG
|
|
52
|
+
rng = np.random.default_rng()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def multiset(u):
|
|
56
|
+
"""
|
|
57
|
+
Summates excitatory and inhibitory signals.
|
|
58
|
+
Outputs a net multiset. No element will exist as both positive and negative.
|
|
59
|
+
"""
|
|
60
|
+
if not isinstance(u, list):
|
|
61
|
+
u = [u]
|
|
62
|
+
|
|
63
|
+
# Flatten nested structures if necessary
|
|
64
|
+
flat_u = []
|
|
65
|
+
|
|
66
|
+
def _flatten(items):
|
|
67
|
+
for item in items:
|
|
68
|
+
if isinstance(item, list):
|
|
69
|
+
_flatten(item)
|
|
70
|
+
else:
|
|
71
|
+
flat_u.append(item)
|
|
72
|
+
_flatten(u)
|
|
73
|
+
|
|
74
|
+
tally = Counter()
|
|
75
|
+
pos = [x for x in flat_u if x > 0]
|
|
76
|
+
neg = [-x for x in flat_u if x < 0]
|
|
77
|
+
|
|
78
|
+
tally.update(pos)
|
|
79
|
+
tally.subtract(neg)
|
|
80
|
+
|
|
81
|
+
result = []
|
|
82
|
+
for k, v in tally.items():
|
|
83
|
+
if v > 0:
|
|
84
|
+
result.extend([k] * v)
|
|
85
|
+
elif v < 0:
|
|
86
|
+
result.extend([-k] * abs(v))
|
|
87
|
+
|
|
88
|
+
return sorted(result)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def resolve_normal(x):
|
|
92
|
+
"""Applies graded inhibition, drops negatives, squashes excitatory survivors."""
|
|
93
|
+
ae = [i for i in x if i > 0]
|
|
94
|
+
ai = [-i for i in x if i < 0]
|
|
95
|
+
|
|
96
|
+
if not ai: # Fast path for pure boolean
|
|
97
|
+
return sorted(list(set(ae)))
|
|
98
|
+
|
|
99
|
+
counts_e = Counter(ae)
|
|
100
|
+
counts_i = Counter(ai)
|
|
101
|
+
|
|
102
|
+
survivors = [k for k, v in counts_e.items() if v > counts_i.get(k, 0)]
|
|
103
|
+
return sorted(survivors)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolve_graded(x):
|
|
107
|
+
"""Applies graded inhibition, drops negatives, keeps excitatory survivors."""
|
|
108
|
+
ae = [i for i in x if i > 0]
|
|
109
|
+
ai = [-i for i in x if i < 0]
|
|
110
|
+
|
|
111
|
+
if not ai: # Fast path for pure multisets
|
|
112
|
+
return sorted(ae)
|
|
113
|
+
|
|
114
|
+
counts_e = Counter(ae)
|
|
115
|
+
counts_i = Counter(ai)
|
|
116
|
+
|
|
117
|
+
result = []
|
|
118
|
+
for k, v in counts_e.items():
|
|
119
|
+
surviving_count = max(0, v - counts_i.get(k, 0))
|
|
120
|
+
result.extend([k] * surviving_count)
|
|
121
|
+
|
|
122
|
+
return sorted(result)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def multiset_block_join(blocks, dims):
|
|
126
|
+
"""Transparently joins multisets. Preserves duplicates and negative signs."""
|
|
127
|
+
offsets = list(itertools.accumulate([0] + dims[:-1]))
|
|
128
|
+
|
|
129
|
+
result = []
|
|
130
|
+
for sublist, offset in zip(blocks, offsets):
|
|
131
|
+
for val in sublist:
|
|
132
|
+
sign = 1 if val > 0 else -1 if val < 0 else 0
|
|
133
|
+
shifted = sign * (abs(val) + offset)
|
|
134
|
+
result.append(shifted)
|
|
135
|
+
|
|
136
|
+
return sorted(result)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def multiset_block_split(A, dims):
|
|
140
|
+
"""Transparently splits a joined multiset back into its original blocks."""
|
|
141
|
+
limits = list(itertools.accumulate([0] + dims))
|
|
142
|
+
bounds = list(zip(limits[:-1], limits[1:]))
|
|
143
|
+
|
|
144
|
+
split_blocks = []
|
|
145
|
+
for b_start, b_end in bounds:
|
|
146
|
+
chunk = []
|
|
147
|
+
for val in A:
|
|
148
|
+
if b_start < abs(val) <= b_end:
|
|
149
|
+
sign = 1 if val > 0 else -1 if val < 0 else 0
|
|
150
|
+
restored = sign * (abs(val) - b_start)
|
|
151
|
+
chunk.append(restored)
|
|
152
|
+
split_blocks.append(chunk)
|
|
153
|
+
|
|
154
|
+
return split_blocks
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def multiset_block_join_normal(blocks, dims):
|
|
158
|
+
"""
|
|
159
|
+
Join blocks with dimensions into a single list.
|
|
160
|
+
Applies graded inhibition and drops inhibitory elements. Preserves multisets.
|
|
161
|
+
"""
|
|
162
|
+
offsets = list(itertools.accumulate([0] + dims[:-1]))
|
|
163
|
+
|
|
164
|
+
ae = []
|
|
165
|
+
ai = []
|
|
166
|
+
for sublist, offset in zip(blocks, offsets):
|
|
167
|
+
for val in sublist:
|
|
168
|
+
shifted_abs = abs(val) + offset
|
|
169
|
+
if val > 0:
|
|
170
|
+
ae.append(shifted_abs)
|
|
171
|
+
elif val < 0:
|
|
172
|
+
ai.append(shifted_abs)
|
|
173
|
+
|
|
174
|
+
counts_e = Counter(ae)
|
|
175
|
+
counts_i = Counter(ai)
|
|
176
|
+
|
|
177
|
+
net_excitatory = []
|
|
178
|
+
for k, v in counts_e.items():
|
|
179
|
+
surviving_count = max(0, v - counts_i.get(k, 0))
|
|
180
|
+
net_excitatory.extend([k] * surviving_count)
|
|
181
|
+
|
|
182
|
+
return sorted(net_excitatory)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def resolve_block_join_normal(blocks, dims):
|
|
186
|
+
"""
|
|
187
|
+
Applies graded subtraction, drops negatives, and squashes the survivors.
|
|
188
|
+
Outputs a flat, Boolean excitatory set.
|
|
189
|
+
"""
|
|
190
|
+
offsets = list(itertools.accumulate([0] + dims[:-1]))
|
|
191
|
+
|
|
192
|
+
ae = []
|
|
193
|
+
ai = []
|
|
194
|
+
for sublist, offset in zip(blocks, offsets):
|
|
195
|
+
for val in sublist:
|
|
196
|
+
shifted_abs = abs(val) + offset
|
|
197
|
+
if val > 0:
|
|
198
|
+
ae.append(shifted_abs)
|
|
199
|
+
elif val < 0:
|
|
200
|
+
ai.append(shifted_abs)
|
|
201
|
+
|
|
202
|
+
counts_e = Counter(ae)
|
|
203
|
+
counts_i = Counter(ai)
|
|
204
|
+
|
|
205
|
+
survivors = [k for k, v in counts_e.items() if v > counts_i.get(k, 0)]
|
|
206
|
+
return sorted(survivors)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def memory_capacity(A_params, B_params):
|
|
210
|
+
NA, PA = A_params
|
|
211
|
+
NB, PB = B_params
|
|
212
|
+
if NA == NB and PA == PB:
|
|
213
|
+
return round((math.log(2.0) * NB * (NA - 1) * (NA - 2)) /
|
|
214
|
+
(PB * (PA - 1) * (PA - 2)))
|
|
215
|
+
else:
|
|
216
|
+
return round((math.log(2.0) * NB * NA * (NA - 1)) /
|
|
217
|
+
(PB * PA * (PA - 1)))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def matching_threshold(A_params, B_params):
|
|
221
|
+
NA, PA = A_params
|
|
222
|
+
NB, PB = B_params
|
|
223
|
+
|
|
224
|
+
if PA <= 2 or PB <= 2:
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
cap = memory_capacity(A_params, B_params)
|
|
228
|
+
T = 1
|
|
229
|
+
|
|
230
|
+
while T < PA and (
|
|
231
|
+
cap**2 *
|
|
232
|
+
math.comb(
|
|
233
|
+
PA,
|
|
234
|
+
T) *
|
|
235
|
+
math.comb(
|
|
236
|
+
NA -
|
|
237
|
+
PA,
|
|
238
|
+
PA -
|
|
239
|
+
T) /
|
|
240
|
+
math.comb(
|
|
241
|
+
NA,
|
|
242
|
+
PA)) >= 1:
|
|
243
|
+
T += 1
|
|
244
|
+
|
|
245
|
+
return T
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# Encoders and decoders
|
|
249
|
+
|
|
250
|
+
# Shared state structure keyed by (N, P) tuple
|
|
251
|
+
# By design, circuits with identical hyperparameters (N, P) share
|
|
252
|
+
# encoder/decoder instances.
|
|
253
|
+
|
|
254
|
+
circuit_codec_lexicon = {}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def codec(config):
|
|
258
|
+
"""General-purpose SHR encoder/decoder for symbolic expressions."""
|
|
259
|
+
|
|
260
|
+
# Extract hyperparameters from config
|
|
261
|
+
hyperparameters = config.get("hyperparameters", [0, 0])
|
|
262
|
+
n, p = hyperparameters[0], hyperparameters[1]
|
|
263
|
+
lex_key = (n, p)
|
|
264
|
+
|
|
265
|
+
# Initialize global hash table if missing for these dimensions
|
|
266
|
+
if lex_key not in circuit_codec_lexicon:
|
|
267
|
+
circuit_codec_lexicon[lex_key] = {None: []}
|
|
268
|
+
|
|
269
|
+
lex = circuit_codec_lexicon[lex_key]
|
|
270
|
+
|
|
271
|
+
# Use auto-associative pattern matching threshold
|
|
272
|
+
T = matching_threshold((n, p), (n, p))
|
|
273
|
+
|
|
274
|
+
def f(expr):
|
|
275
|
+
# Encoding logic
|
|
276
|
+
if config.get("component") == "input":
|
|
277
|
+
if isinstance(expr, list):
|
|
278
|
+
# Bundle multiple tokens
|
|
279
|
+
bundled = []
|
|
280
|
+
for e in expr:
|
|
281
|
+
res = f(e)
|
|
282
|
+
if res:
|
|
283
|
+
bundled.extend(res)
|
|
284
|
+
return sorted(list(set(bundled)))
|
|
285
|
+
|
|
286
|
+
if expr not in lex:
|
|
287
|
+
# 1-based indexing for native arrays
|
|
288
|
+
sampled = rng.choice(range(1, n + 1), size=p, replace=False)
|
|
289
|
+
lex[expr] = sorted(sampled.tolist())
|
|
290
|
+
|
|
291
|
+
return lex[expr]
|
|
292
|
+
|
|
293
|
+
# Decoding logic
|
|
294
|
+
if not expr:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
overlaps = {k: len(set(v).intersection(expr))
|
|
298
|
+
for k, v in lex.items() if k is not None}
|
|
299
|
+
|
|
300
|
+
result = [k for k, overlap in overlaps.items() if overlap >= T]
|
|
301
|
+
|
|
302
|
+
if len(result) == 0:
|
|
303
|
+
return None
|
|
304
|
+
elif len(result) == 1:
|
|
305
|
+
return result[0]
|
|
306
|
+
else:
|
|
307
|
+
return sorted(result)
|
|
308
|
+
|
|
309
|
+
def clear():
|
|
310
|
+
lex.clear()
|
|
311
|
+
lex[None] = []
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
"label": "SYM",
|
|
315
|
+
"function": f,
|
|
316
|
+
"clear": clear
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Shared state structures keyed by (n, p) tuple
|
|
321
|
+
circuit_category_lexicon = {}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
circuit_category_lexicon = {}
|
|
325
|
+
|
|
326
|
+
def category(config):
|
|
327
|
+
hyperparameters = config.get("hyperparameters", [0, 0])
|
|
328
|
+
n, p = hyperparameters[0], hyperparameters[1]
|
|
329
|
+
|
|
330
|
+
K = config.get("categories", n // p if p > 0 else 0)
|
|
331
|
+
if K * p > n:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"Encoder requires dimension {K * p} or greater.")
|
|
334
|
+
|
|
335
|
+
partitionsize = n // K if K > 0 else 0
|
|
336
|
+
|
|
337
|
+
lex_key = (n, p, K)
|
|
338
|
+
|
|
339
|
+
if lex_key not in circuit_category_lexicon:
|
|
340
|
+
circuit_category_lexicon[lex_key] = {None: []}
|
|
341
|
+
|
|
342
|
+
lex = circuit_category_lexicon[lex_key]
|
|
343
|
+
T = matching_threshold((n, p), (n, p))
|
|
344
|
+
|
|
345
|
+
def f(expr):
|
|
346
|
+
if config.get("component") == "input":
|
|
347
|
+
if not isinstance(expr, int) or expr < 0 or expr >= K:
|
|
348
|
+
return []
|
|
349
|
+
|
|
350
|
+
if expr not in lex:
|
|
351
|
+
# 1-based indexing for native arrays
|
|
352
|
+
start = partitionsize * expr + 1
|
|
353
|
+
end = start + partitionsize
|
|
354
|
+
lex[expr] = list(range(start, end))
|
|
355
|
+
|
|
356
|
+
pool = lex[expr]
|
|
357
|
+
sample_size = min(p, len(pool))
|
|
358
|
+
return sorted(
|
|
359
|
+
rng.choice(
|
|
360
|
+
pool,
|
|
361
|
+
size=sample_size,
|
|
362
|
+
replace=False).tolist())
|
|
363
|
+
|
|
364
|
+
if not expr:
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
overlaps = {k: len(set(v).intersection(expr))
|
|
368
|
+
for k, v in lex.items() if k is not None}
|
|
369
|
+
|
|
370
|
+
result = [k for k, overlap in overlaps.items() if overlap >= T]
|
|
371
|
+
|
|
372
|
+
if len(result) == 0:
|
|
373
|
+
return None
|
|
374
|
+
elif len(result) == 1:
|
|
375
|
+
return result[0]
|
|
376
|
+
else:
|
|
377
|
+
return sorted(result)
|
|
378
|
+
|
|
379
|
+
def clear():
|
|
380
|
+
lex.clear()
|
|
381
|
+
lex[None] = []
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
"label": "CAT",
|
|
385
|
+
"function": f,
|
|
386
|
+
"clear": clear
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
circuit_binning_lexicon = {}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def binning(config):
|
|
394
|
+
hyperparameters = config.get("hyperparameters", [0, 0])
|
|
395
|
+
n, p = hyperparameters[0], hyperparameters[1]
|
|
396
|
+
lex_key = (n, p)
|
|
397
|
+
|
|
398
|
+
if lex_key not in circuit_binning_lexicon:
|
|
399
|
+
circuit_binning_lexicon[lex_key] = {None: []}
|
|
400
|
+
|
|
401
|
+
lex = circuit_binning_lexicon[lex_key]
|
|
402
|
+
T = matching_threshold((n, p), (n, p))
|
|
403
|
+
|
|
404
|
+
if config.get("component") == "input":
|
|
405
|
+
raise ValueError("binning cannot be used as an encoder.")
|
|
406
|
+
|
|
407
|
+
def f(expr):
|
|
408
|
+
if not expr:
|
|
409
|
+
return 0
|
|
410
|
+
|
|
411
|
+
overlaps = {k: len(set(v).intersection(expr))
|
|
412
|
+
for k, v in lex.items() if k is not None}
|
|
413
|
+
|
|
414
|
+
bins = [k for k, overlap in overlaps.items() if overlap >= T]
|
|
415
|
+
|
|
416
|
+
if len(bins) == 0:
|
|
417
|
+
new_bin = len(lex)
|
|
418
|
+
lex[new_bin] = expr
|
|
419
|
+
return new_bin
|
|
420
|
+
elif len(bins) == 1:
|
|
421
|
+
return bins[0]
|
|
422
|
+
else:
|
|
423
|
+
return sorted(bins)
|
|
424
|
+
|
|
425
|
+
def clear():
|
|
426
|
+
lex.clear()
|
|
427
|
+
lex[None] = []
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
"label": "BIN",
|
|
431
|
+
"function": f,
|
|
432
|
+
"clear": clear
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
circuit_vectorencoder_matrices = {}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def vectorencoder(config):
|
|
440
|
+
hyperparameters = config.get("hyperparameters", [0, 0])
|
|
441
|
+
n, p = hyperparameters[0], hyperparameters[1]
|
|
442
|
+
key = (n, p)
|
|
443
|
+
|
|
444
|
+
if key not in circuit_vectorencoder_matrices:
|
|
445
|
+
circuit_vectorencoder_matrices[key] = {}
|
|
446
|
+
|
|
447
|
+
R = circuit_vectorencoder_matrices[key]
|
|
448
|
+
|
|
449
|
+
if config.get("component") == "output":
|
|
450
|
+
raise ValueError("vectorencoder cannot be used as a decoder.")
|
|
451
|
+
|
|
452
|
+
def f(lst):
|
|
453
|
+
if not isinstance(lst, list):
|
|
454
|
+
lst = list(lst)
|
|
455
|
+
d = len(lst)
|
|
456
|
+
if p <= 0 or d == 0:
|
|
457
|
+
return []
|
|
458
|
+
|
|
459
|
+
sparsity = config.get("sparsity", float(1.0 / math.sqrt(d)))
|
|
460
|
+
|
|
461
|
+
if d not in R:
|
|
462
|
+
num_ones = round(n * d * sparsity)
|
|
463
|
+
flat_indices = rng.choice(n * d, size=num_ones, replace=False)
|
|
464
|
+
rows = flat_indices // d
|
|
465
|
+
cols = flat_indices % d
|
|
466
|
+
vals = np.ones(num_ones)
|
|
467
|
+
|
|
468
|
+
R[d] = sp.csr_matrix((vals, (rows, cols)), shape=(n, d))
|
|
469
|
+
|
|
470
|
+
matrix = R[d]
|
|
471
|
+
vec = matrix.dot(np.array(lst))
|
|
472
|
+
|
|
473
|
+
min_val, max_val = np.min(vec), np.max(vec)
|
|
474
|
+
span = max_val - min_val
|
|
475
|
+
scale = max(abs(min_val), abs(max_val))
|
|
476
|
+
tolerance = 1e-10
|
|
477
|
+
|
|
478
|
+
if scale == 0 or span <= tolerance * scale:
|
|
479
|
+
return []
|
|
480
|
+
|
|
481
|
+
k = min(p, len(vec))
|
|
482
|
+
top_k_indices = np.argsort(vec)[-k:]
|
|
483
|
+
return sorted((top_k_indices + 1).tolist())
|
|
484
|
+
|
|
485
|
+
def clear():
|
|
486
|
+
R.clear()
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
"label": "VEC",
|
|
490
|
+
"function": f,
|
|
491
|
+
"clear": clear
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
circuit_flyhash_matrices = {}
|
|
496
|
+
circuit_flyhash_resting = {}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def flyhash(config):
|
|
500
|
+
hyperparameters = config.get("hyperparameters", [0, 0])
|
|
501
|
+
n, p = hyperparameters[0], hyperparameters[1]
|
|
502
|
+
key = (n, p)
|
|
503
|
+
|
|
504
|
+
if key not in circuit_flyhash_matrices:
|
|
505
|
+
circuit_flyhash_matrices[key] = {}
|
|
506
|
+
circuit_flyhash_resting[key] = {}
|
|
507
|
+
|
|
508
|
+
R = circuit_flyhash_matrices[key]
|
|
509
|
+
restingpotential = circuit_flyhash_resting[key]
|
|
510
|
+
|
|
511
|
+
sparsity = config.get("sparsity", 0.1)
|
|
512
|
+
|
|
513
|
+
if config.get("component") == "output":
|
|
514
|
+
raise ValueError("flyhash cannot be used as a decoder.")
|
|
515
|
+
|
|
516
|
+
def f(lst):
|
|
517
|
+
if not isinstance(lst, list):
|
|
518
|
+
lst = list(lst)
|
|
519
|
+
d = len(lst)
|
|
520
|
+
if p <= 0 or d == 0:
|
|
521
|
+
return []
|
|
522
|
+
|
|
523
|
+
arr = np.array(lst)
|
|
524
|
+
centeredlist = arr - np.mean(arr)
|
|
525
|
+
tolerance = 1e-10
|
|
526
|
+
|
|
527
|
+
if d not in R:
|
|
528
|
+
c = max(1, round(d * sparsity))
|
|
529
|
+
|
|
530
|
+
rows = np.repeat(np.arange(n), c)
|
|
531
|
+
cols = np.concatenate(
|
|
532
|
+
[rng.choice(d, size=c, replace=False) for _ in range(n)])
|
|
533
|
+
vals = np.ones(n * c)
|
|
534
|
+
|
|
535
|
+
R[d] = sp.csr_matrix((vals, (rows, cols)), shape=(n, d))
|
|
536
|
+
restingpotential[d] = rng.uniform(0, tolerance * 0.01, size=n)
|
|
537
|
+
|
|
538
|
+
vec = R[d].dot(centeredlist) + restingpotential[d]
|
|
539
|
+
|
|
540
|
+
min_val, max_val = np.min(vec), np.max(vec)
|
|
541
|
+
span = max_val - min_val
|
|
542
|
+
scale = max(abs(min_val), abs(max_val))
|
|
543
|
+
|
|
544
|
+
if scale == 0 or span <= tolerance * scale:
|
|
545
|
+
return []
|
|
546
|
+
|
|
547
|
+
k = min(p, len(vec))
|
|
548
|
+
top_k_indices = np.argsort(vec)[-k:]
|
|
549
|
+
return sorted((top_k_indices + 1).tolist())
|
|
550
|
+
|
|
551
|
+
def clear():
|
|
552
|
+
R.clear()
|
|
553
|
+
restingpotential.clear()
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
"label": "FLY",
|
|
557
|
+
"function": f,
|
|
558
|
+
"clear": clear
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# Circuit Components
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def input(config):
|
|
566
|
+
"""
|
|
567
|
+
At the beginning of each cycle, external input is pushed
|
|
568
|
+
into the input's send slots. Input nodes have no "receive" slots.
|
|
569
|
+
The callback function serves as an optional encoder/preprocessing plugin.
|
|
570
|
+
"""
|
|
571
|
+
pluginconfig = config.copy()
|
|
572
|
+
if "send_blocks" in config and config["send_blocks"]:
|
|
573
|
+
pluginconfig["hyperparameters"] = config["send_blocks"][0]
|
|
574
|
+
|
|
575
|
+
plugin_factory = config.get("plugin")
|
|
576
|
+
|
|
577
|
+
if callable(plugin_factory):
|
|
578
|
+
plugin = plugin_factory(pluginconfig)
|
|
579
|
+
else:
|
|
580
|
+
# Default Identity function
|
|
581
|
+
plugin = {"function": lambda x: x}
|
|
582
|
+
|
|
583
|
+
return plugin | {"size": 10, "checks": ["input", "oneout"]}
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def output(config):
|
|
587
|
+
"""
|
|
588
|
+
At the end of each cycle, the output's input edges are read out.
|
|
589
|
+
Output nodes have no "send" slots.
|
|
590
|
+
The callback function serves as an optional decoder/postprocessing plugin.
|
|
591
|
+
"""
|
|
592
|
+
pluginconfig = config.copy()
|
|
593
|
+
if "receive_blocks" in config and config["receive_blocks"]:
|
|
594
|
+
pluginconfig["hyperparameters"] = config["receive_blocks"][0]
|
|
595
|
+
|
|
596
|
+
plugin_factory = config.get("plugin")
|
|
597
|
+
|
|
598
|
+
if callable(plugin_factory):
|
|
599
|
+
plugin = plugin_factory(pluginconfig)
|
|
600
|
+
else:
|
|
601
|
+
# Default Identity function
|
|
602
|
+
plugin = {"function": lambda x: x}
|
|
603
|
+
|
|
604
|
+
return plugin | {"size": 10, "checks": ["output", "oneinp"]}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def delay(config):
|
|
610
|
+
plugin_factory = config.get("plugin", replacement)
|
|
611
|
+
|
|
612
|
+
if isinstance(plugin_factory, str):
|
|
613
|
+
import sys
|
|
614
|
+
plugin_factory = getattr(
|
|
615
|
+
sys.modules[__name__],
|
|
616
|
+
plugin_factory,
|
|
617
|
+
replacement)
|
|
618
|
+
|
|
619
|
+
plugin = plugin_factory(config)
|
|
620
|
+
|
|
621
|
+
if plugin_factory is replacement:
|
|
622
|
+
plugin.pop("label", None)
|
|
623
|
+
|
|
624
|
+
dims1 = [b[0] for b in config.get("receive_blocks", [])]
|
|
625
|
+
dims2 = [b[0] for b in config.get("send_blocks", [])]
|
|
626
|
+
pop = sum(b[1] for b in config.get("receive_blocks", []))
|
|
627
|
+
|
|
628
|
+
threshold_factor = config.get("threshold", 0.0)
|
|
629
|
+
absthreshold = round(threshold_factor * pop)
|
|
630
|
+
|
|
631
|
+
rate_limit_factor = config.get("rate_limit", float('inf'))
|
|
632
|
+
absratelimit = round(
|
|
633
|
+
rate_limit_factor *
|
|
634
|
+
pop) if rate_limit_factor != float('inf') else float('inf')
|
|
635
|
+
|
|
636
|
+
decay = config.get("decay", 0.0)
|
|
637
|
+
capacity_factor = config.get("capacity", 1.0)
|
|
638
|
+
abscapacity = round(capacity_factor * pop)
|
|
639
|
+
decimation = config.get("decimate", 1.0)
|
|
640
|
+
latch = config.get("latch", False)
|
|
641
|
+
|
|
642
|
+
Xstate = []
|
|
643
|
+
|
|
644
|
+
def f(*blocks):
|
|
645
|
+
nonlocal Xstate
|
|
646
|
+
X = multiset_block_join(list(blocks), dims1)
|
|
647
|
+
|
|
648
|
+
# Enforce minimum population
|
|
649
|
+
if len(X) < absthreshold:
|
|
650
|
+
X = []
|
|
651
|
+
|
|
652
|
+
if len(X) > absratelimit:
|
|
653
|
+
X = sorted(
|
|
654
|
+
rng.choice(
|
|
655
|
+
X,
|
|
656
|
+
size=absratelimit,
|
|
657
|
+
replace=False).tolist())
|
|
658
|
+
|
|
659
|
+
if decay > 0.0:
|
|
660
|
+
keep_size = math.floor((1.0 - decay) * len(Xstate))
|
|
661
|
+
Xstate = sorted(
|
|
662
|
+
rng.choice(
|
|
663
|
+
Xstate,
|
|
664
|
+
size=keep_size,
|
|
665
|
+
replace=False).tolist())
|
|
666
|
+
|
|
667
|
+
if len(Xstate) > abscapacity:
|
|
668
|
+
Xstate = sorted(
|
|
669
|
+
rng.choice(
|
|
670
|
+
Xstate,
|
|
671
|
+
size=abscapacity,
|
|
672
|
+
replace=False).tolist())
|
|
673
|
+
|
|
674
|
+
# Temporal integration via update rules
|
|
675
|
+
if not (latch and len(X) == 0):
|
|
676
|
+
Xstate = plugin["updaterule"](X, Xstate)
|
|
677
|
+
|
|
678
|
+
# Proportional multiset subsampling applied to output
|
|
679
|
+
Xdec = Xstate
|
|
680
|
+
if decimation < 1.0:
|
|
681
|
+
sample_size = math.floor(decimation * len(Xdec))
|
|
682
|
+
Xdec = sorted(
|
|
683
|
+
rng.choice(
|
|
684
|
+
Xdec,
|
|
685
|
+
size=sample_size,
|
|
686
|
+
replace=False).tolist())
|
|
687
|
+
|
|
688
|
+
return tuple(multiset_block_split(Xdec, dims2))
|
|
689
|
+
|
|
690
|
+
result = dict(plugin)
|
|
691
|
+
|
|
692
|
+
result.update({
|
|
693
|
+
"function": f,
|
|
694
|
+
"checks": ["arginp", "argout", "totaldim"],
|
|
695
|
+
"fill": 13
|
|
696
|
+
})
|
|
697
|
+
return result
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# Update rules for auto-associative memory components ("auto")
|
|
701
|
+
# and temporal integration ("delay").
|
|
702
|
+
|
|
703
|
+
def replacement(config):
|
|
704
|
+
return {"updaterule": lambda y, x: y, "label": "▼", "size": 18}
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def residual(config):
|
|
708
|
+
return {"updaterule": lambda y, x: resolve_graded(
|
|
709
|
+
multiset([x, [-i for i in y]])), "label": "▲", "size": 18}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def complement(config):
|
|
713
|
+
return {"updaterule": lambda y, x: resolve_graded(
|
|
714
|
+
multiset([y, [-i for i in x]])), "label": "▽", "size": 20}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def difference(config):
|
|
718
|
+
return {"updaterule": lambda y, x: sorted(
|
|
719
|
+
[abs(i) for i in multiset([y, [-i for i in x]])]), "label": "△", "size": 20}
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def augmentation(config):
|
|
723
|
+
return {"updaterule": lambda y, x: multiset(
|
|
724
|
+
[y, x]), "label": "∪", "size": 14}
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def coincidence(config):
|
|
728
|
+
def multiset_intersection(y, x):
|
|
729
|
+
counts_y = Counter(y)
|
|
730
|
+
counts_x = Counter(x)
|
|
731
|
+
res = []
|
|
732
|
+
for k, v in counts_y.items():
|
|
733
|
+
res.extend([k] * min(v, counts_x.get(k, 0)))
|
|
734
|
+
return sorted(res)
|
|
735
|
+
return {"updaterule": multiset_intersection, "label": "∩", "size": 14}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def permutation(config):
|
|
739
|
+
dims = sum(b[0] for b in config.get("receive_blocks", []))
|
|
740
|
+
dim = dims if dims else 0
|
|
741
|
+
perm = rng.choice(
|
|
742
|
+
range(1, dim + 1),
|
|
743
|
+
size = dim,
|
|
744
|
+
replace=False).tolist() if dim > 0 else []
|
|
745
|
+
|
|
746
|
+
def updaterule(y, x):
|
|
747
|
+
mapped_x = sorted([(1 if i > 0 else -1 if i < 0 else 0)
|
|
748
|
+
* perm[abs(i) - 1] for i in x if i != 0])
|
|
749
|
+
return multiset([y, mapped_x])
|
|
750
|
+
|
|
751
|
+
return {"updaterule": updaterule, "label": "π", "size": 16}
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
"""
|
|
755
|
+
Auto-associative memory component.
|
|
756
|
+
|
|
757
|
+
Note: There is a wide range of possible learning, subsampling, retrieval
|
|
758
|
+
and update rules for auto-associative memory. This prototype captures the
|
|
759
|
+
generic cases -- modify as needed.
|
|
760
|
+
"""
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def auto(config):
|
|
764
|
+
plugin_factory = config.get("plugin", replacement)
|
|
765
|
+
|
|
766
|
+
if isinstance(plugin_factory, str):
|
|
767
|
+
# Resolve string name to function dynamically
|
|
768
|
+
import sys
|
|
769
|
+
plugin_factory = getattr(
|
|
770
|
+
sys.modules[__name__],
|
|
771
|
+
plugin_factory,
|
|
772
|
+
replacement)
|
|
773
|
+
|
|
774
|
+
plugin = plugin_factory(config)
|
|
775
|
+
|
|
776
|
+
receive_blocks = config.get("receive_blocks", [])
|
|
777
|
+
dims = [b[0] for b in receive_blocks]
|
|
778
|
+
|
|
779
|
+
# params = Plus @@ config["receive_blocks"] (Sums Ns and Ps respectively)
|
|
780
|
+
params = [sum(b[0] for b in receive_blocks), sum(b[1]
|
|
781
|
+
for b in receive_blocks)]
|
|
782
|
+
pop = params[1] if len(params) > 1 else 0
|
|
783
|
+
|
|
784
|
+
rate_limit_factor = config.get("rate_limit", float('inf'))
|
|
785
|
+
absratelimit = round(
|
|
786
|
+
rate_limit_factor *
|
|
787
|
+
pop) if rate_limit_factor != float('inf') else float('inf')
|
|
788
|
+
|
|
789
|
+
decimation = config.get("decimate", 1.0)
|
|
790
|
+
|
|
791
|
+
# Learning thresholds
|
|
792
|
+
min_val = pop
|
|
793
|
+
max_val = pop
|
|
794
|
+
learn_val = config.get("learn", False)
|
|
795
|
+
|
|
796
|
+
if isinstance(learn_val, (int, float)) and not isinstance(learn_val, bool):
|
|
797
|
+
min_val = max_val = round(learn_val * pop)
|
|
798
|
+
elif isinstance(learn_val, (list, tuple)) and len(learn_val) == 2:
|
|
799
|
+
min_val = round(learn_val[0] * pop)
|
|
800
|
+
max_val = round(learn_val[1] * pop)
|
|
801
|
+
|
|
802
|
+
# Memory with identical input and output parameters
|
|
803
|
+
m_config = dict(config)
|
|
804
|
+
m_config.update({
|
|
805
|
+
"A_parameters": params,
|
|
806
|
+
"B_parameters": params
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
# Memory backend
|
|
810
|
+
M = Memory(m_config)
|
|
811
|
+
|
|
812
|
+
def f(*blocks):
|
|
813
|
+
# Join partitions and apply inhibition
|
|
814
|
+
A = resolve_block_join_normal(list(blocks), dims)
|
|
815
|
+
X = A
|
|
816
|
+
|
|
817
|
+
# Limit the rate of incoming positives
|
|
818
|
+
if len(X) > absratelimit:
|
|
819
|
+
X = sorted(
|
|
820
|
+
rng.choice(
|
|
821
|
+
X,
|
|
822
|
+
size=absratelimit,
|
|
823
|
+
replace=False).tolist())
|
|
824
|
+
|
|
825
|
+
# Proportional decimation
|
|
826
|
+
if decimation < 1.0:
|
|
827
|
+
sample_size = math.floor(decimation * len(X))
|
|
828
|
+
Xdec = sorted(
|
|
829
|
+
rng.choice(
|
|
830
|
+
X,
|
|
831
|
+
size=sample_size,
|
|
832
|
+
replace=False).tolist())
|
|
833
|
+
else:
|
|
834
|
+
Xdec = X
|
|
835
|
+
|
|
836
|
+
# Always learn if "learn" is True
|
|
837
|
+
if learn_val is True:
|
|
838
|
+
M["store"](A)
|
|
839
|
+
|
|
840
|
+
# Memory retrieval with decimated and rate-limited input state
|
|
841
|
+
Y = M["retrieve"](Xdec)
|
|
842
|
+
|
|
843
|
+
# Learn input A if retrieval fails and it falls within thresholds
|
|
844
|
+
if not Y and min_val <= len(A) <= max_val:
|
|
845
|
+
if learn_val is not True:
|
|
846
|
+
M["store"](A)
|
|
847
|
+
Y = A
|
|
848
|
+
|
|
849
|
+
X_out = plugin["updaterule"](Y, X)
|
|
850
|
+
X_out = sorted(list(set(X_out)))
|
|
851
|
+
|
|
852
|
+
return tuple(multiset_block_split(X_out, dims))
|
|
853
|
+
|
|
854
|
+
# Merge plugin attributes, component defaults, and Memory closures (e.g.,
|
|
855
|
+
# "clear")
|
|
856
|
+
result = dict(plugin)
|
|
857
|
+
result.update({
|
|
858
|
+
"function": f,
|
|
859
|
+
"checks": ["arginp", "argout", "ident"],
|
|
860
|
+
"shape": "square", "fill": 8
|
|
861
|
+
})
|
|
862
|
+
result.update(M)
|
|
863
|
+
|
|
864
|
+
return result
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
"""
|
|
868
|
+
Temporal-associative memory component.
|
|
869
|
+
Learns higher-order sequences on the fly and predicts the next token.
|
|
870
|
+
A hybrid between auto-associative and hetero-associative architectures.
|
|
871
|
+
Uses the same temporal integration parametrization as "delay".
|
|
872
|
+
"""
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def temporal(config):
|
|
876
|
+
plugin_factory = config.get("plugin", replacement)
|
|
877
|
+
|
|
878
|
+
if isinstance(plugin_factory, str):
|
|
879
|
+
import sys
|
|
880
|
+
plugin_factory = getattr(
|
|
881
|
+
sys.modules[__name__],
|
|
882
|
+
plugin_factory,
|
|
883
|
+
replacement)
|
|
884
|
+
|
|
885
|
+
plugin = plugin_factory(config)
|
|
886
|
+
|
|
887
|
+
receive_blocks = config.get("receive_blocks", [])
|
|
888
|
+
dims = [b[0] for b in receive_blocks]
|
|
889
|
+
|
|
890
|
+
params = [sum(b[0] for b in receive_blocks), sum(b[1]
|
|
891
|
+
for b in receive_blocks)]
|
|
892
|
+
pop = params[1] if len(params) > 1 else 0
|
|
893
|
+
|
|
894
|
+
threshold_factor = config.get("threshold", 0.0)
|
|
895
|
+
absthreshold = round(threshold_factor * pop)
|
|
896
|
+
|
|
897
|
+
rate_limit_factor = config.get("rate_limit", float('inf'))
|
|
898
|
+
absratelimit = round(
|
|
899
|
+
rate_limit_factor *
|
|
900
|
+
pop) if rate_limit_factor != float('inf') else float('inf')
|
|
901
|
+
|
|
902
|
+
decay = config.get("decay", 0.0)
|
|
903
|
+
capacity_factor = config.get("capacity", 1.0)
|
|
904
|
+
abscapacity = round(capacity_factor * pop)
|
|
905
|
+
decimation = config.get("decimate", 1.0)
|
|
906
|
+
latch = config.get("latch", False)
|
|
907
|
+
|
|
908
|
+
# Memory with identical input and output parameters
|
|
909
|
+
m_config = dict(config)
|
|
910
|
+
m_config.update({
|
|
911
|
+
"A_parameters": params,
|
|
912
|
+
"B_parameters": params
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
M = Memory(m_config)
|
|
916
|
+
Xstate = []
|
|
917
|
+
prediction = []
|
|
918
|
+
|
|
919
|
+
def f(*blocks):
|
|
920
|
+
nonlocal Xstate, prediction
|
|
921
|
+
X = multiset_block_join(list(blocks), dims)
|
|
922
|
+
|
|
923
|
+
# Enforce minimum population
|
|
924
|
+
if len(X) < absthreshold:
|
|
925
|
+
X = []
|
|
926
|
+
|
|
927
|
+
# Rate limiting equally applies to positive and negative elements
|
|
928
|
+
if len(X) > absratelimit:
|
|
929
|
+
X = sorted(
|
|
930
|
+
rng.choice(
|
|
931
|
+
X,
|
|
932
|
+
size=absratelimit,
|
|
933
|
+
replace=False).tolist())
|
|
934
|
+
|
|
935
|
+
# Pre-integration stochastic decay
|
|
936
|
+
if decay > 0.0:
|
|
937
|
+
keep_size = math.floor((1.0 - decay) * len(Xstate))
|
|
938
|
+
Xstate = sorted(
|
|
939
|
+
rng.choice(
|
|
940
|
+
Xstate,
|
|
941
|
+
size=keep_size,
|
|
942
|
+
replace=False).tolist())
|
|
943
|
+
|
|
944
|
+
# Always learn.
|
|
945
|
+
M["store"](resolve_normal(Xstate), resolve_normal(X))
|
|
946
|
+
|
|
947
|
+
# Multiset subsampling applied to previous state
|
|
948
|
+
if len(Xstate) > abscapacity:
|
|
949
|
+
Xstate = sorted(
|
|
950
|
+
rng.choice(
|
|
951
|
+
Xstate,
|
|
952
|
+
size=abscapacity,
|
|
953
|
+
replace=False).tolist())
|
|
954
|
+
|
|
955
|
+
# Temporal integration via update rules
|
|
956
|
+
if not (latch and len(X) == 0):
|
|
957
|
+
Xstate = plugin["updaterule"](X, Xstate)
|
|
958
|
+
|
|
959
|
+
# Proportional multiset subsampling applied to the integrated state
|
|
960
|
+
Xdec = Xstate
|
|
961
|
+
if decimation < 1.0:
|
|
962
|
+
sample_size = math.floor(decimation * len(Xdec))
|
|
963
|
+
Xdec = sorted(
|
|
964
|
+
rng.choice(
|
|
965
|
+
Xdec,
|
|
966
|
+
size=sample_size,
|
|
967
|
+
replace=False).tolist())
|
|
968
|
+
|
|
969
|
+
# Predict next token
|
|
970
|
+
prediction = M["retrieve"](resolve_normal(Xdec))
|
|
971
|
+
|
|
972
|
+
return tuple(multiset_block_split(prediction, dims))
|
|
973
|
+
|
|
974
|
+
result = dict(plugin)
|
|
975
|
+
result.update({
|
|
976
|
+
"function": f,
|
|
977
|
+
"checks": ["arginp", "argout", "ident"],
|
|
978
|
+
"shape": "square", "fill": 14
|
|
979
|
+
})
|
|
980
|
+
result.update(M)
|
|
981
|
+
|
|
982
|
+
return result
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def associator(config):
|
|
986
|
+
"""
|
|
987
|
+
Vanilla hetero-associative node for supervised learning A -> B.
|
|
988
|
+
B is the first input argument.
|
|
989
|
+
A is given by the rest of the input arguments.
|
|
990
|
+
Can be block-coded. Always learns if B != [].
|
|
991
|
+
"""
|
|
992
|
+
receive_blocks = config.get("receive_blocks", [])
|
|
993
|
+
Adims = [b[0] for b in receive_blocks[1:]]
|
|
994
|
+
|
|
995
|
+
params_A = [sum(b[0] for b in receive_blocks[1:]), sum(b[1]
|
|
996
|
+
for b in receive_blocks[1:])]
|
|
997
|
+
params_B = receive_blocks[0]
|
|
998
|
+
|
|
999
|
+
m_config = dict(config)
|
|
1000
|
+
m_config.update({
|
|
1001
|
+
"A_parameters": params_A,
|
|
1002
|
+
"B_parameters": params_B
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
M = Memory(m_config)
|
|
1006
|
+
|
|
1007
|
+
def f(B, *blocks):
|
|
1008
|
+
X = resolve_block_join_normal(list(blocks), Adims)
|
|
1009
|
+
Y = resolve_normal(B)
|
|
1010
|
+
|
|
1011
|
+
if not Y:
|
|
1012
|
+
return (M["retrieve"](X),)
|
|
1013
|
+
|
|
1014
|
+
M["store"](X, Y)
|
|
1015
|
+
return ([],)
|
|
1016
|
+
|
|
1017
|
+
result = {
|
|
1018
|
+
"function": f,
|
|
1019
|
+
"checks": ["dimfirst", "oneout"],
|
|
1020
|
+
"shape": "square", "fill": 11, "size": 18, "label": "▶●"
|
|
1021
|
+
}
|
|
1022
|
+
result.update(M)
|
|
1023
|
+
return result
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def heteroencoder(config):
|
|
1027
|
+
"""
|
|
1028
|
+
Heteroencoder A -> B. A may be partitioned. Has no input for B.
|
|
1029
|
+
"""
|
|
1030
|
+
receive_blocks = config.get("receive_blocks", [])
|
|
1031
|
+
dims = [b[0] for b in receive_blocks]
|
|
1032
|
+
|
|
1033
|
+
params_A = [sum(b[0] for b in receive_blocks), sum(b[1]
|
|
1034
|
+
for b in receive_blocks)]
|
|
1035
|
+
params_B = config.get("send_blocks", [[0, 0]])[0]
|
|
1036
|
+
|
|
1037
|
+
m_config = dict(config)
|
|
1038
|
+
m_config.update({
|
|
1039
|
+
"A_parameters": params_A,
|
|
1040
|
+
"B_parameters": params_B
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
M = Memory(m_config)
|
|
1044
|
+
|
|
1045
|
+
def f(*blocks):
|
|
1046
|
+
X = resolve_block_join_normal(list(blocks), dims)
|
|
1047
|
+
|
|
1048
|
+
# Empty input: no output, nothing to learn
|
|
1049
|
+
if not X:
|
|
1050
|
+
return ([],)
|
|
1051
|
+
|
|
1052
|
+
Y = M["retrieve"](X)
|
|
1053
|
+
|
|
1054
|
+
if not Y:
|
|
1055
|
+
n, p = params_B
|
|
1056
|
+
Y = sorted(
|
|
1057
|
+
rng.choice(
|
|
1058
|
+
range(1, n + 1),
|
|
1059
|
+
size=p, replace=False).tolist())
|
|
1060
|
+
|
|
1061
|
+
M["store"](X, Y)
|
|
1062
|
+
return (Y,)
|
|
1063
|
+
|
|
1064
|
+
result = {
|
|
1065
|
+
"function": f,
|
|
1066
|
+
"checks": ["arginp", "oneout"],
|
|
1067
|
+
"shape": "square",
|
|
1068
|
+
"fill": 11,
|
|
1069
|
+
"size": 18,
|
|
1070
|
+
"label": "◀▶"
|
|
1071
|
+
}
|
|
1072
|
+
result.update(M)
|
|
1073
|
+
return result
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def predictor(config):
|
|
1077
|
+
"""
|
|
1078
|
+
Generates a prediction based on the current input (slot #1)
|
|
1079
|
+
and context (slots #2,...).
|
|
1080
|
+
Automatically learns the correct prediction in the following cycle.
|
|
1081
|
+
|
|
1082
|
+
The context option sets how the context blocks are taken:
|
|
1083
|
+
"replace" (default) takes every input, so an empty block is empty;
|
|
1084
|
+
"hold" keeps each block at its last non-empty value, for receivers
|
|
1085
|
+
of event-driven senders that emit only on change.
|
|
1086
|
+
"""
|
|
1087
|
+
receive_blocks = config.get("receive_blocks", [])
|
|
1088
|
+
itemconfig = receive_blocks[0]
|
|
1089
|
+
contextconfig = receive_blocks[1:]
|
|
1090
|
+
|
|
1091
|
+
# Extract decimation parameter, defaulting to 1.0
|
|
1092
|
+
decimation = config.get("decimate", 1.0)
|
|
1093
|
+
|
|
1094
|
+
# Context: "replace" (take every input) or "hold" (keep last non-empty)
|
|
1095
|
+
context_mode = config.get("context", "replace")
|
|
1096
|
+
if context_mode not in ("replace", "hold"):
|
|
1097
|
+
raise CircuitError(
|
|
1098
|
+
"predictor: context must be \"replace\" or \"hold\".")
|
|
1099
|
+
hold = context_mode == "hold"
|
|
1100
|
+
|
|
1101
|
+
params_A = [sum(b[0] for b in contextconfig), sum(b[1]
|
|
1102
|
+
for b in contextconfig)]
|
|
1103
|
+
params_B = itemconfig
|
|
1104
|
+
|
|
1105
|
+
m_config = dict(config)
|
|
1106
|
+
m_config.update({
|
|
1107
|
+
"A_parameters": params_A,
|
|
1108
|
+
"B_parameters": params_B
|
|
1109
|
+
})
|
|
1110
|
+
|
|
1111
|
+
M = Memory(m_config)
|
|
1112
|
+
X = []
|
|
1113
|
+
prediction = []
|
|
1114
|
+
held = [[] for _ in contextconfig]
|
|
1115
|
+
|
|
1116
|
+
def f(item, *blocks):
|
|
1117
|
+
nonlocal X, prediction, held
|
|
1118
|
+
Y = resolve_normal(item)
|
|
1119
|
+
|
|
1120
|
+
M["store"](X, Y) # always learn
|
|
1121
|
+
|
|
1122
|
+
# Hold: an empty block keeps its last non-empty value
|
|
1123
|
+
if hold:
|
|
1124
|
+
held = [b if b else h for b, h in zip(blocks, held)]
|
|
1125
|
+
blocks = held
|
|
1126
|
+
|
|
1127
|
+
X = resolve_block_join_normal(
|
|
1128
|
+
list(blocks), [b[0] for b in contextconfig])
|
|
1129
|
+
|
|
1130
|
+
# Apply stochastic subsampling to X before retrieval
|
|
1131
|
+
if decimation < 1.0:
|
|
1132
|
+
sample_size = math.floor(decimation * len(X))
|
|
1133
|
+
Xdec = sorted(
|
|
1134
|
+
rng.choice(
|
|
1135
|
+
X,
|
|
1136
|
+
size=sample_size,
|
|
1137
|
+
replace=False).tolist())
|
|
1138
|
+
else:
|
|
1139
|
+
Xdec = X
|
|
1140
|
+
|
|
1141
|
+
prediction = M["retrieve"](Xdec)
|
|
1142
|
+
|
|
1143
|
+
return (prediction,)
|
|
1144
|
+
|
|
1145
|
+
def clear():
|
|
1146
|
+
nonlocal X, prediction, held
|
|
1147
|
+
M["clear"]()
|
|
1148
|
+
X, prediction = [], []
|
|
1149
|
+
held = [[] for _ in contextconfig]
|
|
1150
|
+
|
|
1151
|
+
result = {
|
|
1152
|
+
"function": f,
|
|
1153
|
+
"checks": ["dimfirst", "oneout"],
|
|
1154
|
+
"shape": "square", "fill": 11, "size": 18, "label": "▶▶"
|
|
1155
|
+
}
|
|
1156
|
+
result.update(M)
|
|
1157
|
+
result["clear"] = clear
|
|
1158
|
+
return result
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def noise(config):
|
|
1163
|
+
n, p = config.get("send_blocks", [[0, 0]])[0]
|
|
1164
|
+
|
|
1165
|
+
def f(*blocks):
|
|
1166
|
+
return (
|
|
1167
|
+
sorted(
|
|
1168
|
+
rng.choice(range(1, n + 1),
|
|
1169
|
+
size=p,
|
|
1170
|
+
replace=False).tolist()),
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
return {
|
|
1174
|
+
"function": f,
|
|
1175
|
+
"checks": ["input", "oneout"],
|
|
1176
|
+
"label": "~", "fill": 13, "size": 18
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def file(config):
|
|
1181
|
+
file_path = config.get("name")
|
|
1182
|
+
if not file_path:
|
|
1183
|
+
raise CircuitError(f"Missing 'name' property in {config}")
|
|
1184
|
+
|
|
1185
|
+
if not file_path.endswith(".json"):
|
|
1186
|
+
file_path += ".json"
|
|
1187
|
+
|
|
1188
|
+
# Search current working directory first, then sys.path
|
|
1189
|
+
search_paths = [""] + sys.path
|
|
1190
|
+
|
|
1191
|
+
full_path = None
|
|
1192
|
+
for base in search_paths:
|
|
1193
|
+
target = os.path.join(base, file_path) if base else file_path
|
|
1194
|
+
if os.path.exists(target):
|
|
1195
|
+
full_path = target
|
|
1196
|
+
break
|
|
1197
|
+
|
|
1198
|
+
if not full_path:
|
|
1199
|
+
raise CircuitError(
|
|
1200
|
+
f"File '{file_path}' not found in current directory or sys.path.")
|
|
1201
|
+
|
|
1202
|
+
with open(full_path, "r") as f:
|
|
1203
|
+
circ_expr = json.load(f)
|
|
1204
|
+
|
|
1205
|
+
# Rescale hyperparameters of embedded circuit
|
|
1206
|
+
scale = config.get("scale")
|
|
1207
|
+
default_hp = circ_expr.get("hyperparameters", {}).get("default")
|
|
1208
|
+
|
|
1209
|
+
if (isinstance(scale, (list, tuple)) and len(scale) == 2 and
|
|
1210
|
+
isinstance(default_hp, (list, tuple)) and len(default_hp) == 2):
|
|
1211
|
+
|
|
1212
|
+
# Calculate separate scaling factors
|
|
1213
|
+
rescale_factor_n = scale[0] / default_hp[0]
|
|
1214
|
+
rescale_factor_p = scale[1] / default_hp[1]
|
|
1215
|
+
|
|
1216
|
+
# Apply scaling to all hyperparameters in the embedded circuit
|
|
1217
|
+
for k, v in circ_expr["hyperparameters"].items():
|
|
1218
|
+
if isinstance(v, (list, tuple)) and len(v) == 2:
|
|
1219
|
+
circ_expr["hyperparameters"][k] = [
|
|
1220
|
+
round(v[0] * rescale_factor_n),
|
|
1221
|
+
round(v[1] * rescale_factor_p)
|
|
1222
|
+
]
|
|
1223
|
+
|
|
1224
|
+
# Compile the imported circuit
|
|
1225
|
+
sub = Circuit(circ_expr)
|
|
1226
|
+
|
|
1227
|
+
# Extract up to 5 characters from the filename for the label
|
|
1228
|
+
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
1229
|
+
sub["label"] = base_name[:5]
|
|
1230
|
+
|
|
1231
|
+
return sub
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
# Registry for compiled embedded circuits
|
|
1235
|
+
circuit_registry = {}
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def shared(config):
|
|
1239
|
+
"""Plug-in for embedding a precompiled circuit."""
|
|
1240
|
+
name = config.get("name")
|
|
1241
|
+
if not name:
|
|
1242
|
+
raise ValueError(
|
|
1243
|
+
f"Missing 'name' property in shared circuit: {config}")
|
|
1244
|
+
|
|
1245
|
+
sub = circuit_registry.get(name)
|
|
1246
|
+
if not sub:
|
|
1247
|
+
raise ValueError(f"Unknown circuit '{name}'.")
|
|
1248
|
+
|
|
1249
|
+
result = dict(sub)
|
|
1250
|
+
result["label"] = name
|
|
1251
|
+
result.pop("clear", None)
|
|
1252
|
+
|
|
1253
|
+
return result
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def circuit(config):
|
|
1257
|
+
"""
|
|
1258
|
+
Embedded circuit component.
|
|
1259
|
+
Multiple circuit components can reference the same embedded instance.
|
|
1260
|
+
"""
|
|
1261
|
+
plugin_factory = config.get("plugin", shared)
|
|
1262
|
+
|
|
1263
|
+
if isinstance(plugin_factory, str):
|
|
1264
|
+
import sys
|
|
1265
|
+
plugin_factory = getattr(sys.modules[__name__], plugin_factory, shared)
|
|
1266
|
+
|
|
1267
|
+
plugin = plugin_factory(config)
|
|
1268
|
+
if not plugin:
|
|
1269
|
+
return {}
|
|
1270
|
+
|
|
1271
|
+
params = [plugin.get("receive_blocks", []), plugin.get("send_blocks", [])]
|
|
1272
|
+
|
|
1273
|
+
if [config.get("receive_blocks", []), config.get(
|
|
1274
|
+
"send_blocks", [])] != params:
|
|
1275
|
+
raise ValueError(
|
|
1276
|
+
f"Hyperparameters of embedded circuit do not match: {params}")
|
|
1277
|
+
|
|
1278
|
+
def f(*blocks):
|
|
1279
|
+
return plugin["function"](*blocks)
|
|
1280
|
+
|
|
1281
|
+
result = dict(plugin)
|
|
1282
|
+
result.update({
|
|
1283
|
+
"function": f,
|
|
1284
|
+
"checks": ["arginp", "argout"],
|
|
1285
|
+
"shape": "square", "size": 9
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
return result
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
# Circuit Error Handling
|
|
1292
|
+
|
|
1293
|
+
class CircuitError(Exception):
|
|
1294
|
+
"""Custom exception for Circuit-related errors."""
|
|
1295
|
+
pass
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
circuit_messages = {
|
|
1299
|
+
"ident": "{0}: Inputs must match outputs.",
|
|
1300
|
+
"identdim": "{0}: Input and output dimensions must be identical.",
|
|
1301
|
+
"totaldim": "{0}: Total input and output dimensions must match.",
|
|
1302
|
+
"dimfirst": "{0}: Output must match first input slot.",
|
|
1303
|
+
"input": "No input slots allowed in {0}.",
|
|
1304
|
+
"oneinp": "Expecting one input slot in {0}.",
|
|
1305
|
+
"arginp": "{0}: Missing input.",
|
|
1306
|
+
"output": "No output slots allowed in {0}.",
|
|
1307
|
+
"oneout": "{0}: Expecting one output slot.",
|
|
1308
|
+
"argout": "{0}: Missing output."
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
circuit_checks = {
|
|
1312
|
+
"ident": lambda r,
|
|
1313
|
+
s: [b[0] for b in r] == [b[0] for b in s],
|
|
1314
|
+
"identdim": lambda r,
|
|
1315
|
+
s: sorted(list(set(b[0] for b in r))) == sorted(list(set(b[0] for b in s))),
|
|
1316
|
+
"totaldim": lambda r,
|
|
1317
|
+
s: sum(b[0] for b in r) == sum(b[0] for b in s),
|
|
1318
|
+
"dimfirst": lambda r,
|
|
1319
|
+
s: len(r) > 0 and len(s) > 0 and r[0] == s[0],
|
|
1320
|
+
"input": lambda r,
|
|
1321
|
+
s: len(r) == 0,
|
|
1322
|
+
"oneinp": lambda r,
|
|
1323
|
+
s: len(r) == 1,
|
|
1324
|
+
"arginp": lambda r,
|
|
1325
|
+
s: len(r) > 0,
|
|
1326
|
+
"output": lambda r,
|
|
1327
|
+
s: len(s) == 0,
|
|
1328
|
+
"oneout": lambda r,
|
|
1329
|
+
s: len(s) == 1,
|
|
1330
|
+
"argout": lambda r,
|
|
1331
|
+
s: len(s) > 0}
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
# Circuit Visualization
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
# 16-color Nord hex palette as defined in the Mathematica source
|
|
1338
|
+
NORD_PALETTE = [
|
|
1339
|
+
"#2E3440", "#3B4252", "#434C5E", "#4C566A",
|
|
1340
|
+
"#D8DEE9", "#E5E9F0", "#ECEFF4", "#8FBCBB",
|
|
1341
|
+
"#88C0D0", "#81A1C1", "#5E81AC", "#BF616A",
|
|
1342
|
+
"#D08770", "#EBCB8B", "#A3BE8C", "#B48EAD"
|
|
1343
|
+
]
|
|
1344
|
+
|
|
1345
|
+
# Tag mapping for edge labels
|
|
1346
|
+
TAG_MAP = {
|
|
1347
|
+
"multiset": "+", "inhibit": "-", "permute": "π",
|
|
1348
|
+
"noise": "~", "rate_limit": "R", "threshold": "T",
|
|
1349
|
+
"veto": "X", "mandatory": "*", "priority": "!",
|
|
1350
|
+
"dependency": "&", "fallback": "|", "barrier": "=",
|
|
1351
|
+
"kwta_excitatory": "K", "kwta_absolute": "k",
|
|
1352
|
+
"log": "?", "show_slot": "#", "show_dimension": "$",
|
|
1353
|
+
"show_population": "%"
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def Circuit(expr):
|
|
1358
|
+
"""
|
|
1359
|
+
Parses a circuit dataflow dictionary or JSON string and returns
|
|
1360
|
+
a compiled circuit interface.
|
|
1361
|
+
Implements data-driven execution flow and state isolation.
|
|
1362
|
+
"""
|
|
1363
|
+
if isinstance(expr, str):
|
|
1364
|
+
expr = json.loads(expr)
|
|
1365
|
+
|
|
1366
|
+
nodes = {}
|
|
1367
|
+
pathways = {}
|
|
1368
|
+
|
|
1369
|
+
preprocess = []
|
|
1370
|
+
postprocess = []
|
|
1371
|
+
inputslots = []
|
|
1372
|
+
outputedges = []
|
|
1373
|
+
|
|
1374
|
+
receiveparams = []
|
|
1375
|
+
sendparams = []
|
|
1376
|
+
permutations = {}
|
|
1377
|
+
|
|
1378
|
+
# Isolated runtime state buffers
|
|
1379
|
+
tick = 0
|
|
1380
|
+
nextstate = {}
|
|
1381
|
+
currentstate = {}
|
|
1382
|
+
|
|
1383
|
+
nodeid = 0
|
|
1384
|
+
edgeid = 0
|
|
1385
|
+
|
|
1386
|
+
def slotparams(slot):
|
|
1387
|
+
hyperparameters = expr.get("hyperparameters", {})
|
|
1388
|
+
default = hyperparameters.get("default", [100, 3])
|
|
1389
|
+
# Accommodate both int and string keys from JSON
|
|
1390
|
+
np_val = hyperparameters.get(
|
|
1391
|
+
slot, hyperparameters.get(
|
|
1392
|
+
str(slot), default))
|
|
1393
|
+
|
|
1394
|
+
if not (isinstance(np_val, list) and len(np_val) == 2):
|
|
1395
|
+
return [100, 3]
|
|
1396
|
+
return np_val
|
|
1397
|
+
|
|
1398
|
+
def fillparams(x):
|
|
1399
|
+
if isinstance(x, list):
|
|
1400
|
+
lists = [fillparams(i) for i in x]
|
|
1401
|
+
dims = [l[0] for l in lists]
|
|
1402
|
+
return [dims[0], min(l[1] for l in lists)]
|
|
1403
|
+
elif isinstance(x, int):
|
|
1404
|
+
return slotparams(x)
|
|
1405
|
+
elif isinstance(x, dict):
|
|
1406
|
+
return slotparams(x["slot"])
|
|
1407
|
+
return None
|
|
1408
|
+
|
|
1409
|
+
def compilepathway(receive):
|
|
1410
|
+
nonlocal edgeid
|
|
1411
|
+
|
|
1412
|
+
if isinstance(receive, list):
|
|
1413
|
+
return [compilepathway(r) for r in receive]
|
|
1414
|
+
|
|
1415
|
+
edgeid += 1
|
|
1416
|
+
current_id = edgeid
|
|
1417
|
+
|
|
1418
|
+
e = {
|
|
1419
|
+
"to_node_id": nodeid,
|
|
1420
|
+
"label": "",
|
|
1421
|
+
"tags": []
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if isinstance(receive, int):
|
|
1425
|
+
e["slot"] = receive
|
|
1426
|
+
elif isinstance(receive, dict):
|
|
1427
|
+
e.update(receive)
|
|
1428
|
+
|
|
1429
|
+
e["hyperparameters"] = slotparams(e.get("slot"))
|
|
1430
|
+
|
|
1431
|
+
if "inhibit" in e["tags"] and "multiset" not in e["tags"]:
|
|
1432
|
+
e["tags"].append("multiset")
|
|
1433
|
+
|
|
1434
|
+
pathways[current_id] = e
|
|
1435
|
+
currentstate[e.get("slot")] = []
|
|
1436
|
+
|
|
1437
|
+
# Initialize 1-based permutation array
|
|
1438
|
+
n = e["hyperparameters"][0]
|
|
1439
|
+
perm = rng.choice(range(1, n + 1), size=n, replace=False).tolist()
|
|
1440
|
+
permutations[current_id] = perm
|
|
1441
|
+
|
|
1442
|
+
return current_id
|
|
1443
|
+
|
|
1444
|
+
def compile_node(node):
|
|
1445
|
+
nonlocal nodeid
|
|
1446
|
+
nodeid += 1
|
|
1447
|
+
current_node_id = nodeid
|
|
1448
|
+
|
|
1449
|
+
rec_paths = [compilepathway(r) for r in node.get("receive", [])]
|
|
1450
|
+
|
|
1451
|
+
# Flatten send array and extract slots
|
|
1452
|
+
raw_sends = node.get("send", [])
|
|
1453
|
+
flat_sends = []
|
|
1454
|
+
|
|
1455
|
+
def _flat(items):
|
|
1456
|
+
for i in items:
|
|
1457
|
+
if isinstance(i, list):
|
|
1458
|
+
_flat(i)
|
|
1459
|
+
else:
|
|
1460
|
+
flat_sends.append(i)
|
|
1461
|
+
_flat(raw_sends)
|
|
1462
|
+
|
|
1463
|
+
send_slots = []
|
|
1464
|
+
for s in flat_sends:
|
|
1465
|
+
if isinstance(s, int):
|
|
1466
|
+
send_slots.append(s)
|
|
1467
|
+
elif isinstance(s, dict) and "slot" in s:
|
|
1468
|
+
send_slots.append(s["slot"])
|
|
1469
|
+
|
|
1470
|
+
# Base configuration dictionary
|
|
1471
|
+
config = dict(node)
|
|
1472
|
+
config.update({
|
|
1473
|
+
"receive_paths": rec_paths,
|
|
1474
|
+
"node_id": current_node_id,
|
|
1475
|
+
"send_slots": send_slots,
|
|
1476
|
+
"scale": expr.get("hyperparameters", {}).get("default"),
|
|
1477
|
+
"receive_blocks": [fillparams(r) for r in node.get("receive", [])],
|
|
1478
|
+
"send_blocks": [fillparams(s) for s in node.get("send", [])]
|
|
1479
|
+
})
|
|
1480
|
+
|
|
1481
|
+
# Resolve plugin dependency dynamically
|
|
1482
|
+
plugin_name = config.get("plugin")
|
|
1483
|
+
if isinstance(plugin_name, str):
|
|
1484
|
+
plugin_func = getattr(sys.modules[__name__], plugin_name, None)
|
|
1485
|
+
if callable(plugin_func):
|
|
1486
|
+
config["plugin"] = plugin_func
|
|
1487
|
+
else:
|
|
1488
|
+
raise ValueError(f"Invalid plugin '{plugin_name}'.")
|
|
1489
|
+
|
|
1490
|
+
comp_name = node.get("component")
|
|
1491
|
+
if not isinstance(comp_name, str):
|
|
1492
|
+
return
|
|
1493
|
+
|
|
1494
|
+
# Dynamic module namespace lookup
|
|
1495
|
+
comp_func = getattr(sys.modules[__name__], comp_name, None)
|
|
1496
|
+
if not callable(comp_func):
|
|
1497
|
+
raise ValueError(f"Invalid circuit component '{comp_name}'.")
|
|
1498
|
+
|
|
1499
|
+
comp_result = comp_func(config)
|
|
1500
|
+
|
|
1501
|
+
# Strict overriding sequence
|
|
1502
|
+
final_config = {
|
|
1503
|
+
"shape": "circle", "label": "", "fill": 6, "color": 0, "size": 12,
|
|
1504
|
+
"checks": [], "function": None
|
|
1505
|
+
}
|
|
1506
|
+
final_config.update(config)
|
|
1507
|
+
final_config.update(comp_result)
|
|
1508
|
+
|
|
1509
|
+
# Static error checking, based on component-supplied "checks"
|
|
1510
|
+
for check in final_config.get("checks", []):
|
|
1511
|
+
if check in circuit_checks:
|
|
1512
|
+
if not circuit_checks[check](
|
|
1513
|
+
final_config["receive_blocks"],
|
|
1514
|
+
final_config["send_blocks"]):
|
|
1515
|
+
error_msg = circuit_messages.get(
|
|
1516
|
+
check, f"Check {check} failed.").format(comp_name)
|
|
1517
|
+
raise CircuitError(error_msg)
|
|
1518
|
+
|
|
1519
|
+
if comp_name == "input":
|
|
1520
|
+
preprocess.append(final_config["function"])
|
|
1521
|
+
inputslots.append(final_config["send_slots"][0])
|
|
1522
|
+
receiveparams.append(final_config["send_blocks"][0])
|
|
1523
|
+
final_config["label"] = final_config["label"].replace(
|
|
1524
|
+
"#", str(len(inputslots)))
|
|
1525
|
+
|
|
1526
|
+
if comp_name == "output":
|
|
1527
|
+
postprocess.append(final_config["function"])
|
|
1528
|
+
outputedges.append(final_config["receive_paths"][0])
|
|
1529
|
+
sendparams.append(final_config["receive_blocks"][0])
|
|
1530
|
+
final_config["label"] = final_config["label"].replace(
|
|
1531
|
+
"#", str(len(outputedges)))
|
|
1532
|
+
|
|
1533
|
+
nodes[current_node_id] = final_config
|
|
1534
|
+
|
|
1535
|
+
# Trigger compilation
|
|
1536
|
+
for node in expr.get("dataflow", []):
|
|
1537
|
+
compile_node(node)
|
|
1538
|
+
|
|
1539
|
+
# Compile link table for from_node_id values
|
|
1540
|
+
links = {}
|
|
1541
|
+
for node in nodes.values():
|
|
1542
|
+
for slot in node.get("send_slots", []):
|
|
1543
|
+
links[slot] = node["node_id"]
|
|
1544
|
+
|
|
1545
|
+
for edge in pathways.values():
|
|
1546
|
+
edge["from_node_id"] = links.get(edge.get("slot"), 0)
|
|
1547
|
+
|
|
1548
|
+
def patheval(id_val):
|
|
1549
|
+
e = pathways[id_val]
|
|
1550
|
+
tags = set(e.get("tags", []))
|
|
1551
|
+
n, p = e["hyperparameters"]
|
|
1552
|
+
|
|
1553
|
+
x = currentstate.get(e.get("slot"), [])
|
|
1554
|
+
|
|
1555
|
+
# Upstream validation of 1-based non-zero data
|
|
1556
|
+
if not isinstance(x, list) or 0 in x:
|
|
1557
|
+
raise ValueError(f"Invalid data in slot {e.get('slot')}: {x}")
|
|
1558
|
+
|
|
1559
|
+
gating = e.get("gating", [1])
|
|
1560
|
+
if gating[(tick - 1) % len(gating)] == 0:
|
|
1561
|
+
return []
|
|
1562
|
+
|
|
1563
|
+
# The ONLY source of inhibition
|
|
1564
|
+
if "inhibit" in tags:
|
|
1565
|
+
x = [-abs(i) for i in x]
|
|
1566
|
+
|
|
1567
|
+
if "multiset" in tags:
|
|
1568
|
+
x = multiset(x)
|
|
1569
|
+
else:
|
|
1570
|
+
x = resolve_normal(x)
|
|
1571
|
+
|
|
1572
|
+
if "permute" in tags:
|
|
1573
|
+
perm = permutations[id_val]
|
|
1574
|
+
x = sorted([(1 if i > 0 else -1) * perm[abs(i) - 1] for i in x])
|
|
1575
|
+
|
|
1576
|
+
if "rate_limit" in tags and len(x) > p:
|
|
1577
|
+
x = sorted(rng.choice(x, size=p, replace=False).tolist())
|
|
1578
|
+
|
|
1579
|
+
if "threshold" in tags and len(x) < p:
|
|
1580
|
+
x = []
|
|
1581
|
+
|
|
1582
|
+
if "noise" in tags:
|
|
1583
|
+
if not x:
|
|
1584
|
+
x = sorted(
|
|
1585
|
+
rng.choice(
|
|
1586
|
+
range(
|
|
1587
|
+
1,
|
|
1588
|
+
n + 1),
|
|
1589
|
+
size=p,
|
|
1590
|
+
replace=False).tolist())
|
|
1591
|
+
else:
|
|
1592
|
+
x = []
|
|
1593
|
+
|
|
1594
|
+
if "log" in tags:
|
|
1595
|
+
print(f"Slot {e.get('slot')}: {x}")
|
|
1596
|
+
|
|
1597
|
+
return x
|
|
1598
|
+
|
|
1599
|
+
def pathmerge(ids):
|
|
1600
|
+
if isinstance(ids, int):
|
|
1601
|
+
ids = [ids]
|
|
1602
|
+
|
|
1603
|
+
x_list = [patheval(i) for i in ids]
|
|
1604
|
+
paths = [pathways[i] for i in ids]
|
|
1605
|
+
|
|
1606
|
+
def has_tag(t):
|
|
1607
|
+
return [t in p.get("tags", []) for p in paths]
|
|
1608
|
+
|
|
1609
|
+
veto_tags = has_tag("veto")
|
|
1610
|
+
if any(x for i, x in enumerate(x_list) if veto_tags[i] and x):
|
|
1611
|
+
return []
|
|
1612
|
+
|
|
1613
|
+
mandatory_tags = has_tag("mandatory")
|
|
1614
|
+
if any(not x for i, x in enumerate(x_list) if mandatory_tags[i]):
|
|
1615
|
+
return []
|
|
1616
|
+
|
|
1617
|
+
priority_tags = has_tag("priority")
|
|
1618
|
+
if any(x for i, x in enumerate(x_list) if priority_tags[i] and x):
|
|
1619
|
+
x_list = [x if priority_tags[i] else []
|
|
1620
|
+
for i, x in enumerate(x_list)]
|
|
1621
|
+
|
|
1622
|
+
dependency_tags = has_tag("dependency")
|
|
1623
|
+
if any(not x for i, x in enumerate(x_list) if not dependency_tags[i]):
|
|
1624
|
+
x_list = [[] if dependency_tags[i]
|
|
1625
|
+
else x for i, x in enumerate(x_list)]
|
|
1626
|
+
|
|
1627
|
+
fallback_tags = has_tag("fallback")
|
|
1628
|
+
if any(x for i, x in enumerate(x_list) if not fallback_tags[i] and x):
|
|
1629
|
+
x_list = [[] if fallback_tags[i]
|
|
1630
|
+
else x for i, x in enumerate(x_list)]
|
|
1631
|
+
|
|
1632
|
+
barrier_tags = has_tag("barrier")
|
|
1633
|
+
if any(barrier_tags) and any(not x for i,
|
|
1634
|
+
x in enumerate(x_list) if barrier_tags[i]):
|
|
1635
|
+
x_list = [[] if barrier_tags[i]
|
|
1636
|
+
else x for i, x in enumerate(x_list)]
|
|
1637
|
+
|
|
1638
|
+
merged = multiset(x_list)
|
|
1639
|
+
|
|
1640
|
+
kwta_exc_tags = has_tag("kwta_excitatory")
|
|
1641
|
+
kwta_abs_tags = has_tag("kwta_absolute")
|
|
1642
|
+
|
|
1643
|
+
# Optional kWTA. Ranks elements by multiplicity across all paths
|
|
1644
|
+
# (within-path counts of multiset paths included). Passes every
|
|
1645
|
+
# element at or above the k-th highest count, ties included. With
|
|
1646
|
+
# k or fewer distinct elements, all pass. Outputs a set.
|
|
1647
|
+
if any(kwta_exc_tags) or any(kwta_abs_tags):
|
|
1648
|
+
k = min(p["hyperparameters"][1] for p in paths)
|
|
1649
|
+
U = [i for i in merged if i > 0] if any(kwta_exc_tags) else merged
|
|
1650
|
+
|
|
1651
|
+
tally = Counter(U)
|
|
1652
|
+
if len(tally) <= k:
|
|
1653
|
+
merged = sorted(tally)
|
|
1654
|
+
else:
|
|
1655
|
+
kth = sorted(tally.values())[-k]
|
|
1656
|
+
merged = sorted(
|
|
1657
|
+
[val for val, count in tally.items() if count >= kth])
|
|
1658
|
+
|
|
1659
|
+
return merged
|
|
1660
|
+
|
|
1661
|
+
def componenteval(config):
|
|
1662
|
+
if config.get("component") in ("input", "output"):
|
|
1663
|
+
return
|
|
1664
|
+
|
|
1665
|
+
inputs = [pathmerge(rp) for rp in config.get("receive_paths", [])]
|
|
1666
|
+
|
|
1667
|
+
func = config.get("function")
|
|
1668
|
+
result = func(*inputs) if func else tuple([]
|
|
1669
|
+
for _ in config.get("send_slots", []))
|
|
1670
|
+
|
|
1671
|
+
if not isinstance(result, tuple):
|
|
1672
|
+
result = (result,)
|
|
1673
|
+
|
|
1674
|
+
for slot, res in zip(config.get("send_slots", []), result):
|
|
1675
|
+
nextstate[slot] = res
|
|
1676
|
+
|
|
1677
|
+
def evaluate(x):
|
|
1678
|
+
nonlocal tick, currentstate, nextstate
|
|
1679
|
+
tick += 1
|
|
1680
|
+
|
|
1681
|
+
for slot, val in zip(inputslots, x):
|
|
1682
|
+
currentstate[slot] = val
|
|
1683
|
+
nextstate[slot] = val
|
|
1684
|
+
|
|
1685
|
+
for config in nodes.values():
|
|
1686
|
+
componenteval(config)
|
|
1687
|
+
|
|
1688
|
+
# Snapshot state, preparing for extraction and next cycle
|
|
1689
|
+
currentstate = dict(nextstate)
|
|
1690
|
+
|
|
1691
|
+
return [pathmerge(edge) for edge in outputedges]
|
|
1692
|
+
|
|
1693
|
+
def multiplex(x):
|
|
1694
|
+
if len(x) != len(inputslots):
|
|
1695
|
+
raise ValueError(
|
|
1696
|
+
f"Function arguments do not match input nodes: {inputslots}")
|
|
1697
|
+
|
|
1698
|
+
# Latch: skip the cycle (clock included) if all inputs are empty
|
|
1699
|
+
if expr.get("options", {}).get("latch", False) is True and \
|
|
1700
|
+
not any(x):
|
|
1701
|
+
return [[] for _ in outputedges]
|
|
1702
|
+
|
|
1703
|
+
multiplex = expr.get("options", {}).get("multiplex", [1])
|
|
1704
|
+
if not all(m in (0, 1) for m in multiplex):
|
|
1705
|
+
multiplex = [1]
|
|
1706
|
+
|
|
1707
|
+
y = []
|
|
1708
|
+
for gate in multiplex:
|
|
1709
|
+
if gate == 1:
|
|
1710
|
+
y = evaluate(x)
|
|
1711
|
+
else:
|
|
1712
|
+
y = evaluate([[] for _ in x])
|
|
1713
|
+
return y
|
|
1714
|
+
|
|
1715
|
+
def f(*blocks):
|
|
1716
|
+
x = list(blocks)
|
|
1717
|
+
if len(x) != len(inputslots):
|
|
1718
|
+
raise ValueError(
|
|
1719
|
+
f"Function arguments do not match input nodes: {inputslots}")
|
|
1720
|
+
|
|
1721
|
+
x_enc = [prep(val) for prep, val in zip(preprocess, x)]
|
|
1722
|
+
y_raw = multiplex(x_enc)
|
|
1723
|
+
y = [post(val) for post, val in zip(postprocess, y_raw)]
|
|
1724
|
+
|
|
1725
|
+
return tuple(y)
|
|
1726
|
+
|
|
1727
|
+
def schematics(filename):
|
|
1728
|
+
"""Generates a static PNG graph visualization of the circuit."""
|
|
1729
|
+
from matplotlib.figure import Figure # imported here: only needed for drawing
|
|
1730
|
+
|
|
1731
|
+
G = nx.MultiDiGraph()
|
|
1732
|
+
|
|
1733
|
+
# Build nodes
|
|
1734
|
+
for nid, node_data in nodes.items():
|
|
1735
|
+
fill_color = NORD_PALETTE[node_data.get("fill", 6)]
|
|
1736
|
+
text_color = NORD_PALETTE[node_data.get("color", 0)]
|
|
1737
|
+
label = node_data.get("label", "")
|
|
1738
|
+
|
|
1739
|
+
# Map node shapes and apply shape-specific area multipliers
|
|
1740
|
+
if node_data.get("shape") == "square":
|
|
1741
|
+
shape = "s"
|
|
1742
|
+
size_multiplier = 100
|
|
1743
|
+
else:
|
|
1744
|
+
shape = "o" # Default Circle
|
|
1745
|
+
size_multiplier = 180 # Scale circles up relative to squares
|
|
1746
|
+
|
|
1747
|
+
G.add_node(
|
|
1748
|
+
nid,
|
|
1749
|
+
label=label,
|
|
1750
|
+
fill_color=fill_color,
|
|
1751
|
+
text_color=text_color,
|
|
1752
|
+
shape=shape,
|
|
1753
|
+
size=node_data.get("size", 12) * size_multiplier
|
|
1754
|
+
)
|
|
1755
|
+
# Build edges
|
|
1756
|
+
for eid, edge_data in pathways.items():
|
|
1757
|
+
u = edge_data.get("from_node_id")
|
|
1758
|
+
v = edge_data.get("to_node_id")
|
|
1759
|
+
|
|
1760
|
+
if not u or not v:
|
|
1761
|
+
continue
|
|
1762
|
+
|
|
1763
|
+
tags = edge_data.get("tags", [])
|
|
1764
|
+
|
|
1765
|
+
# Construct edge label logic
|
|
1766
|
+
tag_symbols = "".join([TAG_MAP.get(t, "") for t in tags])
|
|
1767
|
+
user_label = edge_data.get("label", "")
|
|
1768
|
+
|
|
1769
|
+
gating = edge_data.get("gating")
|
|
1770
|
+
gating_str = "@" + "".join(str(g)
|
|
1771
|
+
for g in gating) if gating else ""
|
|
1772
|
+
|
|
1773
|
+
raw_label = f"{user_label}{tag_symbols}{gating_str}"
|
|
1774
|
+
|
|
1775
|
+
# Substitute dynamic variables
|
|
1776
|
+
hyperparams = edge_data.get("hyperparameters", [0, 0])
|
|
1777
|
+
replacements = {
|
|
1778
|
+
"#": str(edge_data.get("slot", "")),
|
|
1779
|
+
"$": str(hyperparams[0]),
|
|
1780
|
+
"%": str(hyperparams[1])
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
final_label = raw_label
|
|
1784
|
+
for old, new in replacements.items():
|
|
1785
|
+
final_label = final_label.replace(old, new)
|
|
1786
|
+
|
|
1787
|
+
# Clean up default labels from UI display
|
|
1788
|
+
display_label = final_label.replace(
|
|
1789
|
+
"-",
|
|
1790
|
+
"").replace(
|
|
1791
|
+
"+",
|
|
1792
|
+
"").replace(
|
|
1793
|
+
"*",
|
|
1794
|
+
"✱")
|
|
1795
|
+
|
|
1796
|
+
# Edge styling
|
|
1797
|
+
edge_color = NORD_PALETTE[0]
|
|
1798
|
+
edge_width = 1.0
|
|
1799
|
+
|
|
1800
|
+
if "-" in tag_symbols:
|
|
1801
|
+
edge_color = NORD_PALETTE[11] # Thick red for inhibition
|
|
1802
|
+
edge_width = 3.5
|
|
1803
|
+
elif "+" in tag_symbols:
|
|
1804
|
+
edge_color = NORD_PALETTE[8] # Thick blue for excitation
|
|
1805
|
+
edge_width = 3.5
|
|
1806
|
+
|
|
1807
|
+
G.add_edge(
|
|
1808
|
+
u, v,
|
|
1809
|
+
label=display_label,
|
|
1810
|
+
color=edge_color,
|
|
1811
|
+
width=edge_width
|
|
1812
|
+
)
|
|
1813
|
+
|
|
1814
|
+
|
|
1815
|
+
# Create a clean graph for layout calculation
|
|
1816
|
+
G_layout = nx.MultiDiGraph()
|
|
1817
|
+
G_layout.add_nodes_from(G.nodes())
|
|
1818
|
+
G_layout.add_edges_from(G.edges(keys=True))
|
|
1819
|
+
|
|
1820
|
+
# Assign attributes (respected by AGraph, often dropped by PyDot)
|
|
1821
|
+
G_layout.graph['rankdir'] = 'LR'
|
|
1822
|
+
G_layout.graph['ranksep'] = '0.85'
|
|
1823
|
+
G_layout.graph['nodesep'] = '0.85'
|
|
1824
|
+
|
|
1825
|
+
# Left-to-right hierarchical layout using Graphviz (dot)
|
|
1826
|
+
try:
|
|
1827
|
+
from networkx.drawing.nx_agraph import graphviz_layout
|
|
1828
|
+
pos = graphviz_layout(G_layout, prog='dot', args='-Grankdir=LR')
|
|
1829
|
+
except Exception:
|
|
1830
|
+
try:
|
|
1831
|
+
from networkx.drawing.nx_pydot import pydot_layout
|
|
1832
|
+
# PyDot wrapper drops the rankdir graph attribute.
|
|
1833
|
+
# Calculate standard Top-to-Bottom and rotate mathematically to
|
|
1834
|
+
# enforce Left-to-Right.
|
|
1835
|
+
pos_tb = pydot_layout(G_layout, prog='dot')
|
|
1836
|
+
pos = {n: (-y, x) for n, (x, y) in pos_tb.items()}
|
|
1837
|
+
except Exception:
|
|
1838
|
+
print(
|
|
1839
|
+
"Warning: Install 'pygraphviz' or 'pydot' (and OS-level Graphviz) for left-to-right layout.")
|
|
1840
|
+
pos = nx.spring_layout(G_layout, seed=42)
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
# Render graph on a slightly larger canvas
|
|
1844
|
+
fig = Figure(figsize=(11, 7))
|
|
1845
|
+
ax = fig.add_subplot()
|
|
1846
|
+
|
|
1847
|
+
# Draw edges individually to calculate curve routing for bidirectional
|
|
1848
|
+
# and parallel overlaps
|
|
1849
|
+
edges = G.edges(data=True, keys=True)
|
|
1850
|
+
for u, v, key, data in edges:
|
|
1851
|
+
rad = 0.0
|
|
1852
|
+
if G.has_edge(v, u) or G.number_of_edges(u, v) > 1:
|
|
1853
|
+
# Alternate the curvature radius for parallel edges: 0.2, -0.2,
|
|
1854
|
+
# 0.4, -0.4...
|
|
1855
|
+
magnitude = 0.2 + 0.2 * (key // 2)
|
|
1856
|
+
direction = 1 if key % 2 == 0 else -1
|
|
1857
|
+
rad = magnitude * direction
|
|
1858
|
+
|
|
1859
|
+
nx.draw_networkx_edges(
|
|
1860
|
+
G, pos,
|
|
1861
|
+
edgelist=[(u, v)],
|
|
1862
|
+
edge_color=data['color'],
|
|
1863
|
+
width=data['width'],
|
|
1864
|
+
arrows=True,
|
|
1865
|
+
arrowstyle='-|>',
|
|
1866
|
+
arrowsize=18,
|
|
1867
|
+
node_size=G.nodes[v]['size'],
|
|
1868
|
+
# Instructs Matplotlib to calculate intersection for this
|
|
1869
|
+
# specific shape
|
|
1870
|
+
node_shape=G.nodes[v]['shape'],
|
|
1871
|
+
connectionstyle=f'arc3,rad={rad}',
|
|
1872
|
+
ax=ax
|
|
1873
|
+
)
|
|
1874
|
+
|
|
1875
|
+
# Aggregate labels for parallel edges into multiline strings centered
|
|
1876
|
+
# between the curves
|
|
1877
|
+
edge_labels = {}
|
|
1878
|
+
for u, v, key, data in edges:
|
|
1879
|
+
lbl = data.get('label', '')
|
|
1880
|
+
if lbl:
|
|
1881
|
+
current = edge_labels.get((u, v), "")
|
|
1882
|
+
# Prevent duplicating identical labels on perfectly mirrored
|
|
1883
|
+
# parallel edges
|
|
1884
|
+
if lbl not in current:
|
|
1885
|
+
edge_labels[(u,
|
|
1886
|
+
v)] = f"{current}\n{lbl}" if current else lbl
|
|
1887
|
+
|
|
1888
|
+
nx.draw_networkx_edge_labels(
|
|
1889
|
+
G, pos, edge_labels=edge_labels,
|
|
1890
|
+
font_family="Inter Variable",
|
|
1891
|
+
font_color=NORD_PALETTE[0],
|
|
1892
|
+
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="none"),
|
|
1893
|
+
label_pos=0.5,
|
|
1894
|
+
ax=ax
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
# Draw nodes
|
|
1898
|
+
for node, data in G.nodes(data=True):
|
|
1899
|
+
nx.draw_networkx_nodes(
|
|
1900
|
+
G, pos, nodelist=[node],
|
|
1901
|
+
node_color=data['fill_color'],
|
|
1902
|
+
node_shape=data['shape'],
|
|
1903
|
+
node_size=data['size'],
|
|
1904
|
+
edgecolors=NORD_PALETTE[1],
|
|
1905
|
+
linewidths=1.5,
|
|
1906
|
+
ax=ax
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
# Draw node labels
|
|
1910
|
+
node_labels = {node: data['label']
|
|
1911
|
+
for node, data in G.nodes(data=True)}
|
|
1912
|
+
nx.draw_networkx_labels(
|
|
1913
|
+
G, pos, labels=node_labels,
|
|
1914
|
+
font_family="Inter Variable",
|
|
1915
|
+
font_color=NORD_PALETTE[0],
|
|
1916
|
+
font_weight="bold",
|
|
1917
|
+
ax=ax
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
ax.axis('off')
|
|
1921
|
+
fig.tight_layout()
|
|
1922
|
+
fig.savefig(filename, dpi=300, bbox_inches='tight')
|
|
1923
|
+
|
|
1924
|
+
|
|
1925
|
+
def clear():
|
|
1926
|
+
for node in nodes.values():
|
|
1927
|
+
c = node.get("clear")
|
|
1928
|
+
if callable(c):
|
|
1929
|
+
c()
|
|
1930
|
+
|
|
1931
|
+
dispatch = {
|
|
1932
|
+
"function": f,
|
|
1933
|
+
"schematics": schematics,
|
|
1934
|
+
"clear": clear,
|
|
1935
|
+
"receive_blocks": receiveparams,
|
|
1936
|
+
"send_blocks": sendparams,
|
|
1937
|
+
"nodes": lambda: nodes
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
name = expr.get("options", {}).get("name")
|
|
1941
|
+
if name is not None:
|
|
1942
|
+
dispatch["name"] = name
|
|
1943
|
+
circuit_registry[name] = dispatch
|
|
1944
|
+
|
|
1945
|
+
return dispatch
|