CharlesCNorton commited on
Commit
6a6ad5c
·
1 Parent(s): 4bb9078

neural_reflect: universal netlist interpreter with indexed addressing, two banks, and a compiler from family nets

Browse files
Files changed (3) hide show
  1. README.md +53 -41
  2. src/reflect.py +591 -381
  3. variants/neural_reflect.safetensors +2 -2
README.md CHANGED
@@ -53,10 +53,10 @@ product followed by thresholding, exactly what a resistive or photonic
53
  crossbar computes. `neural_subleq8io` hosts a universal constructor: a
54
  program that reads a description of any machine in the family and prints that
55
  machine's weight file byte for byte, self-reproduction being the case where
56
- the description is its own. And `neural_reflect` removes the last fixed part:
57
- its transition is a ternary threshold network whose weights are stored in the
58
- writable state it operates on, so a running program can edit the weights that
59
- define it.
60
 
61
  ---
62
 
@@ -505,48 +505,60 @@ weights and checked to the byte.
505
  ## neural_reflect — an interpreter whose state holds its own weights
506
 
507
  Every machine above has a fixed weight tensor: the weights define the machine,
508
- the state is what it operates on, and the two are disjoint. `neural_reflect`
509
- removes that separation. The state is a flat array of 256 binary signals; a
510
- 192-bit block of them (the NET region) encodes a netlist of six threshold
511
- gates, and a fixed ternary interpreter U evaluates one stored gate per
512
- recurrence. It selects gate `gp`, reads that gate's two ternary input weights,
513
- integer bias, two input addresses, and one output address from the NET bits,
514
- fetches the two addressed signals, computes `out = H(w0·v0 + w1·v1 + b)`,
515
- writes `out` to the addressed signal, and advances `gp`. Six recurrences
516
- evaluate the stored netlist once.
517
-
518
- - **The program shares memory with the data.** Addresses index the whole
519
- signal array, so a gate's output can target the NET region: a stored gate
520
- can overwrite another gate's weight, bias, or wiring. The running program
521
- edits the weights that define it (self-modification), and rewriting the NET
522
- region replaces the interpreted machine with another one, with no external
523
- load step (metamorphosis).
524
- - **U is fixed and ternary.** The interpreter is one threshold circuit built
525
- from the family's cells (address decoders, 2:1 muxes, a ternary
526
- multiply-accumulate over full adders, Heaviside), 2,938 gates compiled to 27
527
- ternary matrices by `matrix8`'s compiler. Its behavior is set entirely by
528
- the netlist bits in the state, not by its own weights.
529
- - **Physical reading.** One recurrence is one matrix-vector product with a
530
- threshold, and the write is a single bit flip in the state. On a memristive
531
- crossbar that bit is a conductance, so a program editing the NET region is
532
- the computation editing the array's own weights as it runs.
 
 
 
 
 
 
 
 
 
 
 
533
 
534
  Verified against a reference interpreter: exhaustive single-gate semantics
535
  (every ternary weight pair, bias, and input assignment), single-step agreement
536
- on 3,000 random full states plus halted fixed points, lockstep interpretation
537
- of a composed XOR built from the family's gates, and a program that rewrites
538
- its own AND gate into an OR gate between sweeps (output `a & b` on the first
539
- sweep, `a | b` on the second). The compiled matrix form is checked equal to
540
- the gate graph bit for bit.
541
 
542
  ```bash
543
- python src/reflect.py build # compile + save variants/neural_reflect.safetensors
544
- python src/reflect.py verify # reference equality, self-modification, matrix == gate
545
  ```
546
 
547
- The constructor prints a machine's weights; this interprets them out of its
548
- own memory and can rewrite them mid-run. The transition and the state it
549
- transforms are the same tensor.
 
550
 
551
  ---
552
 
@@ -736,8 +748,8 @@ src/ the library (run scripts as `python src/<nam
736
  │ exhaustive equality suite and the analog crossbar simulation
737
  ├── constructor8.py the neural_subleq8io host, the recipe codec, and the universal
738
  │ constructor / self-reproduction suite
739
- └── reflect.py neural_reflect: a threshold interpreter whose state holds its
740
- own gate table; reference equality + self-modification suite
741
  tools/ build_all.py (build + quantize + verify every profile),
742
  cpu_programs.py (assembler + CPU program suite), test_cpu.py
743
  (program suite vs a variant), play.py (interactive demo),
 
53
  crossbar computes. `neural_subleq8io` hosts a universal constructor: a
54
  program that reads a description of any machine in the family and prints that
55
  machine's weight file byte for byte, self-reproduction being the case where
56
+ the description is its own. And `neural_reflect` is a universal interpreter
57
+ over ternary threshold netlists: its fixed transition circuit evaluates a
58
+ netlist held in the writable state, so the machine it runs is stored data that
59
+ a program reads, edits, and reproduces.
60
 
61
  ---
62
 
 
505
  ## neural_reflect — an interpreter whose state holds its own weights
506
 
507
  Every machine above has a fixed weight tensor: the weights define the machine,
508
+ the state is what it operates on, and the two are disjoint. `neural_reflect` is
509
+ a universal interpreter that holds the netlist it evaluates in its own writable
510
+ state. The fixed part is a ternary threshold circuit U (12,993 gates, 50
511
+ ternary matrices); everything machine-specific is stored data that the running
512
+ program can read, edit, copy, and replace.
513
+
514
+ The state is a flat array of 1,024 signals: a pointer register, memory-mapped
515
+ device signals (output stream, pointer advance, bank select, halt) of the kind
516
+ `neural_rv32` and `neural_subleq8io` already use, working signals, and two
517
+ netlist banks. Each stored gate record holds two input slots (address, ternary
518
+ weight, index flag), a signed bias, and an output (address, index flag). One
519
+ recurrence evaluates gate `gp` of the active bank:
520
+
521
+ ```
522
+ eff(addr, idx) = (addr + (idx ? PTR : 0)) mod S
523
+ out = H( w0·sig[eff(a0,i0)] + w1·sig[eff(a1,i1)] + bias ) ; sig[eff(oa,oidx)] = out
524
+ ```
525
+
526
+ `G` recurrences sweep the netlist once; `G` sweeps settle any acyclic netlist
527
+ regardless of gate order, so U reproduces the family's combinational evaluation.
528
+
529
+ - **Universality.** `compile_to_reflect` maps any ≤2-input family `Net` to a
530
+ stored netlist. The family's own full-adder cell, compiled this way, is
531
+ interpreted bit-exact, and stays correct with its gates stored in arbitrary
532
+ order.
533
+ - **Self-reference.** Addresses index the whole signal array, so a gate can
534
+ write into the netlist region: a stored gate edits the weights that define
535
+ it. The index flag adds `PTR`, so a fixed-size program walks an address
536
+ range; one such program streams the netlist's own bytes to the output
537
+ device, emitting its own description. Another copies its bank into the
538
+ second bank and sets the `BANK` flag, so the interpreter runs a different
539
+ resident machine without an external load step.
540
+ - **Physical reading.** One recurrence is one matrix-vector product followed by
541
+ a threshold; each write is one bit of the state. On a memristive crossbar
542
+ that bit is a conductance, so a program editing the netlist region is the
543
+ computation editing the array's own weights.
544
 
545
  Verified against a reference interpreter: exhaustive single-gate semantics
546
  (every ternary weight pair, bias, and input assignment), single-step agreement
547
+ on random full states with halted states as fixed points, bit-exact
548
+ interpretation of the full-adder cell under sorted and shuffled gate order, the
549
+ self-reproduction and bank-replacement programs, and equality of the compiled
550
+ matrix form with the gate graph, the 0.5 comparator margin measured under read
551
+ noise and conductance mismatch.
552
 
553
  ```bash
554
+ python src/reflect.py verify # universality, order independence, quine, metamorphosis
555
+ python src/reflect.py analog # matrix == gate + noise / conductance-mismatch margin
556
  ```
557
 
558
+ The fixed interpreter is a substrate, universal over ternary threshold
559
+ netlists; the machine it runs is stored data that reads, rewrites, and
560
+ reproduces itself. The transition and the state it transforms are the same
561
+ tensor.
562
 
563
  ---
564
 
 
748
  │ exhaustive equality suite and the analog crossbar simulation
749
  ├── constructor8.py the neural_subleq8io host, the recipe codec, and the universal
750
  │ constructor / self-reproduction suite
751
+ └── reflect.py neural_reflect: a universal threshold interpreter whose state
752
+ holds its own gate table; universality / quine / matrix suites
753
  tools/ build_all.py (build + quantize + verify every profile),
754
  cpu_programs.py (assembler + CPU program suite), test_cpu.py
755
  (program suite vs a variant), play.py (interactive demo),
src/reflect.py CHANGED
@@ -1,40 +1,50 @@
1
- """neural_reflect — a self-interpreting ternary threshold processor whose
2
- transition is determined by weights stored in its own writable state.
3
-
4
- Every other machine in the family has a fixed weight tensor: the weights are
5
- what the machine is, the state is what it operates on, and the two never
6
- touch. neural_reflect removes that separation. The state is a flat array of S
7
- binary signals; a contiguous block of them (the NET region) encodes a netlist
8
- of G threshold gates, and a fixed ternary interpreter U evaluates one stored
9
- gate per recurrence:
10
-
11
- 1. select gate gp (an interpreter register) from the stored table,
12
- 2. read its two ternary input weights, integer bias, and two input
13
- addresses and one output address (all addresses index the S signals),
14
- 3. fetch the two addressed signal values (indirect read),
15
- 4. compute out = H(w0*v0 + w1*v1 + bias),
16
- 5. write out to the addressed signal (indirect write),
17
- 6. gp = (gp + 1) mod G.
18
-
19
- G recurrences evaluate the whole stored netlist once. Because the addresses
20
- in step 5 may point into the NET region, a stored gate can overwrite the
21
- encoding of another gate: the running program edits the weights that define
22
- it (self-modification), and by rewriting the whole NET region it becomes a
23
- different machine with no external constructor (metamorphosis). U is a fixed
24
- ternary threshold circuit ({-1,0,1} weights, integer biases, Heaviside step),
25
- compiled to a recurrent matrix stack by matrix8's compiler; on a memristive
26
- crossbar the write in step 5 is a conductance change, so a program editing
27
- NET is the computation editing the array's own weights.
28
-
29
- The interpreter is verified against a reference in four ways: exhaustive
30
- single-gate semantics, single-step agreement on random full states, lockstep
31
- interpretation of loaded netlists (a composed XOR built from the family's
32
- gates, and a program that rewrites its own gate table between sweeps), and
33
- gate-graph vs matrix-form equality.
 
 
 
 
 
 
 
 
 
34
 
35
  Usage:
36
- python src/reflect.py build # compile + save variants/neural_reflect.safetensors
37
- python src/reflect.py verify # reference equality + self-modification + matrix==gate
 
38
  python src/reflect.py all
39
  """
40
 
@@ -42,9 +52,12 @@ from __future__ import annotations
42
 
43
  import argparse
44
  import json
 
45
  import os
 
46
  import sys
47
  import time
 
48
  from typing import Dict, List, Tuple
49
 
50
  import torch
@@ -56,467 +69,664 @@ from matrix8 import Net, compile_net
56
  REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
57
  MODEL_PATH = os.path.join(REPO, "variants", "neural_reflect.safetensors")
58
 
59
- # --- configuration (parametric; this instance is the shipped one) ----------
60
- S = 256 # addressable signal bits (the whole RAM)
61
- A = 8 # address width, 2**A == S
62
- G = 6 # stored gates
63
- F = 2 # inputs per gate (the family builds everything from <=2-in gates)
64
- WB = 2 # weight-code bits per input
65
- BB = 4 # bias bits (signed)
66
- R = F * (A + WB) + BB + A # 32 bits per gate record
67
- NET_BASE = S - G * R # 64: records occupy the high signals
68
- HALT_SIG = NET_BASE - 1 # 63: a data bit the program raises to halt
69
- GPW = 3 # gp register width (ceil(log2 G))
70
- STATE_BITS = S + GPW + 1 # signals, gp, halt
71
 
72
- assert (1 << A) == S and NET_BASE == 64 and R == 32
73
 
74
- # record field offsets (MSB-first within the 32-bit record)
75
- OFF_A0, OFF_W0 = 0, 8
76
- OFF_A1, OFF_W1 = 10, 18
77
- OFF_BIAS, OFF_OUT = 20, 24
78
 
79
- WCODE = {1: (0, 1), -1: (1, 0), 0: (0, 0)} # (hi, lo), MSB-first
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
 
82
  # =============================================================================
83
- # bit helpers and netlist encoding
84
  # =============================================================================
85
 
86
- def to_bits(v: int, n: int) -> List[int]:
87
  return [(v >> (n - 1 - i)) & 1 for i in range(n)]
88
 
89
 
90
- def from_bits(bits: List[int]) -> int:
91
  v = 0
92
  for b in bits:
93
  v = (v << 1) | int(b)
94
  return v
95
 
96
 
97
- def signed(bits: List[int]) -> int:
98
  v = from_bits(bits)
99
  return v - (1 << len(bits)) if bits[0] else v
100
 
101
 
102
- def encode_gate(s0: Tuple[int, int], s1: Tuple[int, int], bias: int, out: int) -> List[int]:
103
- """(addr, weight) per input, signed bias, output address -> 32-bit record."""
104
- (a0, w0), (a1, w1) = s0, s1
105
- rec = [0] * R
106
- rec[OFF_A0:OFF_A0 + A] = to_bits(a0, A)
107
- rec[OFF_W0:OFF_W0 + WB] = list(WCODE[w0])
108
- rec[OFF_A1:OFF_A1 + A] = to_bits(a1, A)
109
- rec[OFF_W1:OFF_W1 + WB] = list(WCODE[w1])
110
- rec[OFF_BIAS:OFF_BIAS + BB] = to_bits(bias & ((1 << BB) - 1), BB)
111
- rec[OFF_OUT:OFF_OUT + A] = to_bits(out, A)
 
 
112
  return rec
113
 
114
 
115
- NOOP = ((0, 0), (0, 0), -1, HALT_SIG - 1) # weights 0, bias<0 -> writes 0 to a scratch bit
 
 
 
 
116
 
117
 
118
- def encode_netlist(gates: List[tuple]) -> List[int]:
119
- """Pack up to G gate records into the NET region (returns G*R bits)."""
120
- assert len(gates) <= G
121
- padded = list(gates) + [NOOP] * (G - len(gates))
122
- out: List[int] = []
123
- for (s0, s1, bias, o) in padded:
124
- out += encode_gate(s0, s1, bias, o)
125
  return out
126
 
127
 
128
- def make_state(signals: List[int], gp: int = 0, halt: int = 0,
129
- netlist: List[tuple] | None = None) -> dict:
130
- sig = list(signals) + [0] * (S - len(signals))
131
- if netlist is not None:
132
- sig[NET_BASE:S] = encode_netlist(netlist)
133
- return {"sig": sig[:S], "gp": gp, "halt": int(halt)}
134
-
135
-
136
  # =============================================================================
137
- # reference interpreter (one stored gate per step)
138
  # =============================================================================
139
 
140
- def ref_step(st: dict) -> dict:
141
- sig, gp, halt = list(st["sig"]), st["gp"], st["halt"]
 
 
 
 
 
 
 
 
 
 
 
 
142
  if halt:
143
- return {"sig": sig, "gp": gp, "halt": halt}
144
- base = NET_BASE + gp * R
145
- rec = sig[base:base + R]
146
-
147
- def wval(hilo):
148
- hi, lo = hilo
149
- if (hi, lo) == (0, 1):
150
- return 1
151
- if (hi, lo) == (1, 0):
152
- return -1
153
- return 0
154
-
155
- a0 = from_bits(rec[OFF_A0:OFF_A0 + A]); w0 = wval((rec[OFF_W0], rec[OFF_W0 + 1]))
156
- a1 = from_bits(rec[OFF_A1:OFF_A1 + A]); w1 = wval((rec[OFF_W1], rec[OFF_W1 + 1]))
157
- bias = signed(rec[OFF_BIAS:OFF_BIAS + BB])
158
- oa = from_bits(rec[OFF_OUT:OFF_OUT + A])
159
- acc = w0 * sig[a0] + w1 * sig[a1] + bias
160
  out = 1 if acc >= 0 else 0
161
- h = sig[HALT_SIG] # halt latches from the pre-write value, as U does
162
- sig[oa] = out
163
- ngp = 0 if gp == G - 1 else gp + 1
164
- nhalt = 1 if (halt or h) else 0
165
- return {"sig": sig, "gp": ngp, "halt": nhalt}
 
 
 
 
 
 
 
166
 
167
 
168
  # =============================================================================
169
  # the interpreter as a fixed ternary threshold circuit
170
  # =============================================================================
171
 
172
- def build_reflect_net() -> Tuple[Net, List[str], List[str]]:
173
  net = Net()
174
- sig = [f"s{i}" for i in range(S)]
175
- gp = [f"gp{k}" for k in range(GPW)]
176
  halt = "halt"
177
  nhalt = net.NOT("nhalt", halt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- # gate select (gp one-hot over 0..G-1)
180
- gp_oh = [net.DECODE(f"gpoh{g}", gp, g) for g in range(G)]
181
-
182
- # read the selected record: recbit[r] = OR_g (gp_oh[g] AND NET[g,r])
183
- recbit = []
184
- for r in range(R):
185
- terms = [net.AND(f"rb{r}g{g}", [gp_oh[g], sig[NET_BASE + g * R + r]]) for g in range(G)]
186
- recbit.append(net.OR(f"rb{r}", terms))
187
-
188
- a0 = recbit[OFF_A0:OFF_A0 + A]
189
- w0 = (recbit[OFF_W0], recbit[OFF_W0 + 1])
190
- a1 = recbit[OFF_A1:OFF_A1 + A]
191
- w1 = (recbit[OFF_W1], recbit[OFF_W1 + 1])
192
- biasbits = recbit[OFF_BIAS:OFF_BIAS + BB]
193
- oabits = recbit[OFF_OUT:OFF_OUT + A]
194
-
195
- # indirect reads: val_j = OR_s ( decode(addr_j)==s AND sig[s] )
196
  def read(addrbits, tag):
197
- terms = []
198
- for s in range(S):
199
- oh = net.DECODE(f"rd{tag}oh{s}", addrbits, s)
200
- terms.append(net.AND(f"rd{tag}a{s}", [oh, sig[s]]))
201
- return net.OR(f"rd{tag}", terms)
202
-
203
- v0 = read(a0, "0")
204
- v1 = read(a1, "1")
205
-
206
- # ternary contribution c_j in {-1,0,1}, two's complement (bit0 magnitude, sign)
207
- def contrib(w, v, tag):
208
- hi, lo = w
209
- pos = net.AND(f"pos{tag}", [net.NOT(f"nhi{tag}", hi), lo])
210
- neg = net.AND(f"neg{tag}", [hi, net.NOT(f"nlo{tag}", lo)])
211
- mag = net.OR(f"mag{tag}", [net.AND(f"tp{tag}", [pos, v]),
212
- net.AND(f"tn{tag}", [neg, v])])
213
- sign = net.AND(f"csgn{tag}", [neg, v])
214
- return mag, sign
215
-
216
- m0, sg0 = contrib(w0, v0, "0")
217
- m1, sg1 = contrib(w1, v1, "1")
218
-
219
- # acc = c0 + c1 + bias over 6-bit two's complement (LSB-first), out = acc >= 0
220
- W6 = 6
221
-
222
- def c_lsb(mag, sign):
223
- return [mag] + [sign] * (W6 - 1)
224
-
225
- def bias_lsb():
226
- b = biasbits # MSB-first [b3(sign) b2 b1 b0]
227
- lo = [b[3], b[2], b[1], b[0]] # b0..b3 LSB-first
228
- return lo + [b[0]] * (W6 - BB) # sign-extend b3
229
-
230
- def ripple(pfx, a_lsb, b_lsb):
231
- c = "#0"
232
- out = []
233
- for k in range(W6):
234
- s_, c = net.FA(f"{pfx}.fa{k}", a_lsb[k], b_lsb[k], c)
235
  out.append(s_)
236
  return out
237
 
238
- s01 = ripple("acc0", c_lsb(m0, sg0), c_lsb(m1, sg1))
239
- acc = ripple("acc1", s01, bias_lsb())
240
- out = net.NOT("out", acc[W6 - 1]) # acc >= 0 <=> sign bit 0
 
 
241
 
242
- # indirect write: sig'[s] = (decode(outaddr)==s AND nhalt) ? out : sig[s]
243
  nsig = []
244
- for s in range(S):
245
- oh = net.DECODE(f"wroh{s}", oabits, s)
246
- sel = net.AND(f"wrsel{s}", [oh, nhalt])
247
  nsig.append(net.MUX(f"nsig{s}", sel, out, sig[s]))
248
 
249
- # gp' = nhalt ? ((gp == G-1) ? 0 : gp+1) : gp (3-bit increment)
250
- gp_lsb = [gp[GPW - 1 - k] for k in range(GPW)]
251
  inc, c = [], "#1"
252
- for k in range(GPW):
253
- s_ = net.XOR(f"gpinc.x{k}", gp_lsb[k], c)
254
- c = net.AND(f"gpinc.c{k}", [gp_lsb[k], c])
255
- inc.append(s_)
256
- inc_msb = [inc[GPW - 1 - k] for k in range(GPW)]
257
- wrapped = [net.MUX(f"gpwrap{k}", gp_oh[G - 1], "#0", inc_msb[k]) for k in range(GPW)]
258
- ngp = [net.MUX(f"ngp{k}", nhalt, wrapped[k], gp[k]) for k in range(GPW)]
259
 
260
- nhalt_out = net.OR("halt_next", [halt, sig[HALT_SIG]])
261
 
262
- outputs = nsig + ngp + [nhalt_out]
263
- inputs = sig + gp + [halt]
264
- return net, inputs, outputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
 
267
  # =============================================================================
268
- # gate-graph evaluator (independent of the matrix compile) for matrix==gate
269
  # =============================================================================
270
 
271
- def eval_net(net: Net, inputs: List[str], outputs: List[str],
272
- values: Dict[str, int]) -> List[int]:
273
- v = dict(values)
274
- v["#0"], v["#1"] = 0, 1
275
- indeg = {g: 0 for g in net.gates}
276
  cons: Dict[str, List[str]] = {}
277
- for g, (ins, _) in net.gates.items():
278
  for s, _w in ins:
279
- if s in net.gates:
280
  indeg[g] += 1
281
  cons.setdefault(s, []).append(g)
282
  order = [g for g, d in indeg.items() if d == 0]
283
  i = 0
284
  while i < len(order):
285
  for c in cons.get(order[i], []):
286
- if c in net.gates:
287
  indeg[c] -= 1
288
  if indeg[c] == 0:
289
  order.append(c)
290
  i += 1
291
- for g in order:
292
- ins, b = net.gates[g]
293
- acc = b + sum(w * v[s] for s, w in ins)
294
- v[g] = 1 if acc >= 0 else 0
295
- return [v[o] for o in outputs]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
 
298
  # =============================================================================
299
- # state <-> vector, and the compiled matrix runtime
300
  # =============================================================================
301
 
302
- def state_to_vec(st: dict) -> torch.Tensor:
303
- v = torch.zeros(STATE_BITS)
304
- v[:S] = torch.tensor(st["sig"], dtype=torch.float32)
305
- v[S:S + GPW] = torch.tensor(to_bits(st["gp"], GPW), dtype=torch.float32)
306
- v[S + GPW] = float(st["halt"])
307
- return v
308
 
309
 
310
- def vec_to_state(v: torch.Tensor) -> dict:
311
- b = [int(round(float(x))) for x in v.tolist()]
312
- return {"sig": b[:S], "gp": from_bits(b[S:S + GPW]), "halt": b[S + GPW]}
313
-
314
-
315
- class Reflect:
316
- HALT_IDX = S + GPW
317
-
318
- def __init__(self, layers, device="cpu"):
319
- self.W = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
320
- self.B = [B.to(device=device, dtype=torch.float32) for _, B in layers]
321
- self.device = device
322
-
323
- def step(self, v: torch.Tensor) -> torch.Tensor:
324
- for W, b in zip(self.W, self.B):
325
- v = ((v @ W.T + b) >= 0).float()
326
- return v
327
-
328
- def run_steps(self, st: dict, n: int) -> dict:
329
- v = state_to_vec(st).unsqueeze(0).to(self.device)
330
- for _ in range(n):
331
- v = self.step(v)
332
- return vec_to_state(v[0].cpu())
333
 
334
 
335
  # =============================================================================
336
- # build
337
  # =============================================================================
338
 
339
- def build(save: bool = True):
340
- print("Assembling the interpreter U from the verified cell library...")
 
341
  t0 = time.perf_counter()
342
- net, inputs, outputs = build_reflect_net()
 
 
 
 
343
  layers, info = compile_net(net, inputs, outputs)
344
- for W, B in layers:
345
- assert set(torch.unique(W).tolist()) <= {-1, 0, 1}
346
- print(f" {len(net.gates)} threshold gates -> {info['layers']} ternary matrices "
347
- f"(max width {info['max_width']}, {info['total_weights']:,} weights) "
348
- f"in {time.perf_counter() - t0:.1f}s")
349
- if save:
350
- tensors = {f"matrix.layer{i:03d}.weight": W for i, (W, _) in enumerate(layers)}
351
- tensors.update({f"matrix.layer{i:03d}.bias": B for i, (_, B) in enumerate(layers)})
352
- tensors["manifest.signals"] = torch.tensor([float(S)])
353
- tensors["manifest.gates"] = torch.tensor([float(G)])
354
- tensors["manifest.state_bits"] = torch.tensor([float(STATE_BITS)])
355
- tensors["manifest.layers"] = torch.tensor([float(info["layers"])])
356
- tensors["manifest.version"] = torch.tensor([1.0])
357
- meta = {"machine": "reflect", "weight_quantization": "ternary",
358
- "config": json.dumps({"S": S, "A": A, "G": G, "F": F,
359
- "record_bits": R, "net_base": NET_BASE,
360
- "halt_signal": HALT_SIG})}
361
- save_file(tensors, MODEL_PATH, metadata=meta)
362
- print(f" saved {MODEL_PATH} ({os.path.getsize(MODEL_PATH) / 1e6:.1f} MB)")
363
  return net, inputs, outputs, layers
364
 
365
 
366
- # =============================================================================
367
- # demonstration netlists
368
- # =============================================================================
369
-
370
- def netlist_xor(a=0, b=1, out=4, t0=2, t1=3):
371
- # XOR = AND( OR(a,b), NAND(a,b) ), all from the family's threshold cells.
372
- return [((a, 1), (b, 1), -1, t0), # OR -> t0
373
- ((a, -1), (b, -1), 1, t1), # NAND -> t1
374
- ((t0, 1), (t1, 1), -2, out)] # AND -> out
375
-
376
-
377
- def _bias_lsb_signal(gate_index: int) -> int:
378
- # signal index of the bias LSB (b0) of gate `gate_index`
379
- return NET_BASE + gate_index * R + OFF_BIAS + (BB - 1)
380
-
381
-
382
- def netlist_selfmod(a=0, b=1, out=2):
383
- # g0 computes AND(a,b)->out (bias -2). g1 writes 1 into g0's bias LSB,
384
- # turning bias -2 (AND) into -1 (OR): the program edits the weights that
385
- # define g0, so the interpreted function becomes OR on the next sweep.
386
- return [((a, 1), (b, 1), -2, out), # g0: AND now, OR after edit
387
- ((0, 0), (0, 0), 0, _bias_lsb_signal(0))] # g1: write 1 -> g0.bias LSB
388
-
389
-
390
  # =============================================================================
391
  # verify
392
  # =============================================================================
393
 
394
- def _rand_states(n, gen):
395
- sig = (torch.rand(n, S, generator=gen) < 0.5).long()
396
- gp = torch.randint(0, G, (n,), generator=gen)
397
- return sig, gp
 
398
 
399
 
400
- def verify(refl: Reflect, net, inputs, outputs, device) -> bool:
401
  ok = True
 
 
 
402
 
403
- # 1. exhaustive single-gate semantics: selected gate at gp=0, both inputs
404
- # placed at fixed addresses, sweep the two ternary weights x both bias signs
405
- # x all input assignments; U's write and out bit must match H(w0 v0 + w1 v1 + b).
406
- print("[1/4] Exhaustive single-gate semantics "
407
- "(all weights x biases x input assignments)")
408
- bad = 0
409
- total = 0
410
  for w0 in (-1, 0, 1):
411
  for w1 in (-1, 0, 1):
412
- for bias in range(-(1 << (BB - 1)), (1 << (BB - 1))):
413
- nl = [((10, w0), (11, w1), bias, 20)]
414
  for va in (0, 1):
415
  for vb in (0, 1):
416
- st = make_state([0] * S, gp=0, netlist=nl)
417
- st["sig"][10] = va
418
- st["sig"][11] = vb
419
- got = refl.run_steps(st, 1)
420
- exp = ref_step(st)
421
  total += 1
422
- if got != exp or got["sig"][20] != (1 if w0 * va + w1 * vb + bias >= 0 else 0):
 
423
  bad += 1
424
- print(f" {'OK ' if bad == 0 else 'FAIL'} {total} gate configurations exact")
425
  ok &= bad == 0
426
 
427
- # 2. single step on random full states, plus halted fixed point
428
- print("[2/4] Single step vs reference on random full states")
429
  gen = torch.Generator().manual_seed(0x5EED)
430
- sig, gp = _rand_states(3000, gen)
 
 
431
  bad = 0
432
- for i in range(sig.shape[0]):
433
- st = {"sig": sig[i].tolist(), "gp": int(gp[i]), "halt": 0}
434
- if refl.run_steps(st, 1) != ref_step(st):
 
435
  bad += 1
436
- print(f" {'OK ' if bad == 0 else 'FAIL'} 3,000 random states exact")
437
- ok &= bad == 0
438
- hbad = 0
439
- for i in range(300):
440
- st = {"sig": sig[i].tolist(), "gp": int(gp[i]), "halt": 1}
441
- if refl.run_steps(st, 1) != st:
442
- hbad += 1
443
- print(f" {'OK ' if hbad == 0 else 'FAIL'} 300 halted states are fixed points")
444
- ok &= hbad == 0
445
-
446
- # 3. interpret a composed XOR, and a self-modifying program, in lockstep
447
- print("[3/4] Interpretation lockstep (composed XOR; self-rewriting program)")
448
- xbad = 0
449
- for a in (0, 1):
450
- for b in (0, 1):
451
- st = make_state([0] * S, gp=0, netlist=netlist_xor())
452
- st["sig"][0], st["sig"][1] = a, b
453
- r = dict(st)
454
- for k in range(G): # one sweep
455
- st2 = refl.run_steps(st, 1)
456
- r = ref_step(r)
457
- if st2 != r:
458
- xbad += 1
459
- st = st2
460
- if st["sig"][4] != (a ^ b):
461
- xbad += 1
462
- print(f" {'OK ' if xbad == 0 else 'FAIL'} XOR interpreted for all inputs "
463
- f"(U == reference every step; s4 == a^b)")
464
- ok &= xbad == 0
465
-
466
- smbad = 0
467
- for a in (0, 1):
468
- for b in (0, 1):
469
- st = make_state([0] * S, gp=0, netlist=netlist_selfmod())
470
- st["sig"][0], st["sig"][1] = a, b
471
- r = dict(st)
472
- trace = []
473
- for k in range(2 * G): # two sweeps
474
- st = refl.run_steps(st, 1)
475
- r = ref_step(r)
476
- if st != r:
477
- smbad += 1
478
- if (k + 1) % G == 0:
479
- trace.append(st["sig"][2])
480
- # tick 1 computed AND, tick 2 (after the edit) computed OR
481
- if trace != [a & b, a | b]:
482
- smbad += 1
483
- print(f" {'OK ' if smbad == 0 else 'FAIL'} self-modification: AND rewrites "
484
- f"itself to OR mid-run (out = a&b then a|b), U == reference")
485
- ok &= smbad == 0
486
-
487
- # 4. gate graph == matrix form on random states
488
- print("[4/4] Gate graph == matrix form")
489
- gbad = 0
490
- for i in range(400):
491
- vals = {f"s{j}": int(sig[i, j]) for j in range(S)}
492
- vals.update({f"gp{k}": to_bits(int(gp[i]), GPW)[k] for k in range(GPW)})
493
- vals["halt"] = 0
494
- gate_out = eval_net(net, inputs, outputs, vals)
495
- v = torch.zeros(STATE_BITS)
496
- for j in range(S):
497
- v[j] = int(sig[i, j])
498
- v[S:S + GPW] = torch.tensor(to_bits(int(gp[i]), GPW), dtype=torch.float32)
499
- mat_out = refl.step(v.unsqueeze(0).to(device))[0].cpu().tolist()
500
- if [int(round(x)) for x in mat_out] != gate_out:
501
- gbad += 1
502
- print(f" {'OK ' if gbad == 0 else 'FAIL'} 400 random states: matrix step "
503
- f"== gate-graph step, bit for bit")
504
- ok &= gbad == 0
505
-
506
- print("\nREFLECT (interpreter == reference; matrix == gate):", "PASS" if ok else "FAIL")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  return ok
508
 
509
 
510
- def main() -> int:
511
- ap = argparse.ArgumentParser(description="neural_reflect builder / verifier")
512
- ap.add_argument("cmd", choices=["build", "verify", "all"])
513
  ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
514
  args = ap.parse_args()
515
- net, inputs, outputs, layers = build(save=args.cmd in ("build", "all"))
516
  rc = 0
 
 
517
  if args.cmd in ("verify", "all"):
518
- refl = Reflect(layers, device=args.device)
519
- rc = 0 if verify(refl, net, inputs, outputs, args.device) else 1
 
520
  return rc
521
 
522
 
 
1
+ """neural_reflect — a universal ternary-threshold netlist processor whose
2
+ transition is a fixed, machine-independent interpreter U over a netlist held
3
+ in the writable state.
4
+
5
+ U is the only fixed part: a ternary threshold circuit that evaluates one stored
6
+ gate per recurrence. Everything machine-specific is the stored netlist, which
7
+ the running program can read, edit, copy, and replace. U is universal over
8
+ ternary threshold netlists, so the fixed part is a substrate (like a
9
+ cellular-automaton rule), not a particular machine.
10
+
11
+ State: a flat array of S binary signals (A = log2 S), two netlist banks, and
12
+ memory-mapped device signals of the kind the rest of the family already uses
13
+ (rv32's console, subleq8io's tape):
14
+
15
+ [ PTR | OUT_DATA OUT_STROBE PTR_ADV BANK HALT | work | NET bank0 | NET bank1 ]
16
+
17
+ Each gate record holds F input slots (address, ternary weight, index flag), a
18
+ signed bias, and an output (address, index flag). An index flag adds PTR to the
19
+ address, so a fixed-size program can walk an address range. Writing OUT_STROBE
20
+ emits OUT_DATA; writing PTR_ADV advances PTR; both are cleared by the device.
21
+ These make loaders, a self-reproduction quine, and bank replacement expressible
22
+ as programs.
23
+
24
+ One recurrence, gate gp of the active bank:
25
+ eff(addr, idx) = (addr + (idx ? PTR : 0)) mod S
26
+ out = H( sum_j w_j * sig[eff(a_j, i_j)] + bias )
27
+ sig[eff(oa, oidx)] = out
28
+ G recurrences sweep the netlist once; G sweeps settle any acyclic netlist
29
+ regardless of gate order, so U reproduces the family's combinational evaluation.
30
+
31
+ Capabilities:
32
+ - compile_to_reflect() maps any <=2-input family Net to a stored netlist; the
33
+ family's full-adder cell runs through the interpreter bit-exact.
34
+ - A netlist stored in arbitrary gate order yields the combinational result,
35
+ since G sweeps relax to the fixed point.
36
+ - A program streams the netlist's own bytes to the output device, emitting its
37
+ own description.
38
+ - A program copies its bank into the other bank and sets the BANK flag to run
39
+ a different resident machine, without an external load step.
40
+ - The accumulator width follows from F and the bias width. U compiles to a
41
+ recurrent ternary matrix stack equal to the gate graph, with a measured
42
+ noise and conductance-mismatch margin.
43
 
44
  Usage:
45
+ python src/reflect.py build # save variants/neural_reflect.safetensors
46
+ python src/reflect.py verify # universality, order, quine, metamorphosis
47
+ python src/reflect.py analog # matrix == gate + noise / mismatch margins
48
  python src/reflect.py all
49
  """
50
 
 
52
 
53
  import argparse
54
  import json
55
+ import math
56
  import os
57
+ import random
58
  import sys
59
  import time
60
+ from dataclasses import dataclass, field
61
  from typing import Dict, List, Tuple
62
 
63
  import torch
 
69
  REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
70
  MODEL_PATH = os.path.join(REPO, "variants", "neural_reflect.safetensors")
71
 
72
+ WCODE = {1: (0, 1), -1: (1, 0), 0: (0, 0)}
 
 
 
 
 
 
 
 
 
 
 
73
 
 
74
 
75
+ # =============================================================================
76
+ # configuration
77
+ # =============================================================================
 
78
 
79
+ @dataclass
80
+ class Cfg:
81
+ A: int = 10
82
+ G: int = 11
83
+ F: int = 2
84
+ BB: int = 4
85
+ banks: int = 2
86
+
87
+ def __post_init__(self):
88
+ self.S = 1 << self.A
89
+ self.SLOT = self.A + 3
90
+ self.R = self.F * self.SLOT + self.BB + (self.A + 1)
91
+ span = self.F + (1 << (self.BB - 1))
92
+ self.ACC = int(math.ceil(math.log2(span + 1))) + 2
93
+ self.PTR_BASE = 0
94
+ self.OUT_DATA = self.A
95
+ self.OUT_STROBE = self.A + 1
96
+ self.PTR_ADV = self.A + 2
97
+ self.BANK_SIG = self.A + 3
98
+ self.HALT_SIG = self.A + 4
99
+ self.TRASH = self.A + 5 # no-op sink; no program reads it
100
+ self.WORK_BASE = self.A + 6
101
+ self.NET0 = self.S - self.banks * self.G * self.R
102
+ self.NET1 = self.NET0 + self.G * self.R
103
+ self.GPW = max(1, (self.G - 1).bit_length())
104
+ self.STATE_BITS = self.S + self.GPW + 1
105
+ assert self.NET0 >= self.WORK_BASE + 16, "too little data room; raise A"
106
+
107
+ def bank_base(self, bank):
108
+ return self.NET0 if bank == 0 else self.NET1
109
+
110
+
111
+ def slot_off(cfg, j):
112
+ return j * cfg.SLOT
113
+
114
+
115
+ def field_off(cfg):
116
+ o = cfg.F * cfg.SLOT
117
+ return o, o + cfg.BB
118
 
119
 
120
  # =============================================================================
121
+ # bit helpers + encoding
122
  # =============================================================================
123
 
124
+ def to_bits(v, n):
125
  return [(v >> (n - 1 - i)) & 1 for i in range(n)]
126
 
127
 
128
+ def from_bits(bits):
129
  v = 0
130
  for b in bits:
131
  v = (v << 1) | int(b)
132
  return v
133
 
134
 
135
+ def signed(bits):
136
  v = from_bits(bits)
137
  return v - (1 << len(bits)) if bits[0] else v
138
 
139
 
140
+ def encode_gate(cfg, slots, bias, out):
141
+ rec = [0] * cfg.R
142
+ for j, (a, w, idx) in enumerate(slots):
143
+ b = slot_off(cfg, j)
144
+ rec[b:b + cfg.A] = to_bits(a, cfg.A)
145
+ rec[b + cfg.A:b + cfg.A + 2] = list(WCODE[w])
146
+ rec[b + cfg.A + 2] = idx
147
+ bo, oo = field_off(cfg)
148
+ rec[bo:bo + cfg.BB] = to_bits(bias & ((1 << cfg.BB) - 1), cfg.BB)
149
+ oa, oidx = out
150
+ rec[oo:oo + cfg.A] = to_bits(oa, cfg.A)
151
+ rec[oo + cfg.A] = oidx
152
  return rec
153
 
154
 
155
+ def pad(cfg, slots):
156
+ slots = list(slots)
157
+ while len(slots) < cfg.F:
158
+ slots.append((0, 0, 0))
159
+ return slots[:cfg.F]
160
 
161
 
162
+ def encode_netlist(cfg, gates):
163
+ out = []
164
+ noop = ([(0, 0, 0)] * cfg.F, -1, (cfg.TRASH, 0))
165
+ for g in (gates + [noop] * (cfg.G - len(gates))):
166
+ slots, bias, o = g
167
+ out += encode_gate(cfg, pad(cfg, slots), bias, o)
 
168
  return out
169
 
170
 
 
 
 
 
 
 
 
 
171
  # =============================================================================
172
+ # reference: gate step + memory-mapped device logic
173
  # =============================================================================
174
 
175
+ def apply_device(cfg, sig):
176
+ emit = None
177
+ if sig[cfg.OUT_STROBE] == 1:
178
+ emit = sig[cfg.OUT_DATA]
179
+ sig[cfg.OUT_STROBE] = 0
180
+ if sig[cfg.PTR_ADV] == 1:
181
+ p = (from_bits(sig[cfg.PTR_BASE:cfg.PTR_BASE + cfg.A]) + 1) & (cfg.S - 1)
182
+ sig[cfg.PTR_BASE:cfg.PTR_BASE + cfg.A] = to_bits(p, cfg.A)
183
+ sig[cfg.PTR_ADV] = 0
184
+ return emit
185
+
186
+
187
+ def ref_gate(cfg, st):
188
+ sig = list(st["sig"]); gp = st["gp"]; halt = st["halt"]
189
  if halt:
190
+ return sig, gp, halt
191
+ ptr = from_bits(sig[cfg.PTR_BASE:cfg.PTR_BASE + cfg.A])
192
+ bank = sig[cfg.BANK_SIG]
193
+ rec = sig[cfg.bank_base(bank) + gp * cfg.R:][:cfg.R]
194
+
195
+ def eff(a, idx):
196
+ return (a + (ptr if idx else 0)) & (cfg.S - 1)
197
+
198
+ def wv(hilo):
199
+ return {(0, 1): 1, (1, 0): -1}.get(tuple(hilo), 0)
200
+
201
+ acc = 0
202
+ for j in range(cfg.F):
203
+ b = slot_off(cfg, j)
204
+ acc += wv(rec[b + cfg.A:b + cfg.A + 2]) * sig[eff(from_bits(rec[b:b + cfg.A]), rec[b + cfg.A + 2])]
205
+ bo, oo = field_off(cfg)
206
+ acc += signed(rec[bo:bo + cfg.BB])
207
  out = 1 if acc >= 0 else 0
208
+ dst = eff(from_bits(rec[oo:oo + cfg.A]), rec[oo + cfg.A])
209
+ h = sig[cfg.HALT_SIG]
210
+ sig[dst] = out
211
+ ngp = 0 if gp == cfg.G - 1 else gp + 1
212
+ return sig, ngp, (1 if (halt or h) else 0)
213
+
214
+
215
+ def ref_step(cfg, st):
216
+ halt_in = st["halt"]
217
+ sig, gp, halt = ref_gate(cfg, st)
218
+ emit = apply_device(cfg, sig) if halt_in == 0 else None
219
+ return {"sig": sig, "gp": gp, "halt": halt, "emit": emit}
220
 
221
 
222
  # =============================================================================
223
  # the interpreter as a fixed ternary threshold circuit
224
  # =============================================================================
225
 
226
+ def build_net(cfg):
227
  net = Net()
228
+ sig = [f"s{i}" for i in range(cfg.S)]
229
+ gp = [f"gp{k}" for k in range(cfg.GPW)]
230
  halt = "halt"
231
  nhalt = net.NOT("nhalt", halt)
232
+ bank = sig[cfg.BANK_SIG]
233
+ gp_oh = [net.DECODE(f"gpoh{g}", gp, g) for g in range(cfg.G)]
234
+
235
+ def netbit(g, r):
236
+ b0 = sig[cfg.NET0 + g * cfg.R + r]
237
+ if cfg.banks == 1:
238
+ return b0
239
+ return net.MUX(f"bk{g}_{r}", bank, sig[cfg.NET1 + g * cfg.R + r], b0)
240
+
241
+ recbit = [net.OR(f"rb{r}", [net.AND(f"rb{r}g{g}", [gp_oh[g], netbit(g, r)])
242
+ for g in range(cfg.G)]) for r in range(cfg.R)]
243
+
244
+ ptr = [sig[cfg.PTR_BASE + k] for k in range(cfg.A)]
245
+
246
+ def eff_addr(addrbits, idx, tag):
247
+ m = [net.AND(f"pm{tag}_{k}", [ptr[k], idx]) for k in range(cfg.A)]
248
+ a_lsb = [addrbits[cfg.A - 1 - k] for k in range(cfg.A)]
249
+ m_lsb = [m[cfg.A - 1 - k] for k in range(cfg.A)]
250
+ c = "#0"; s_lsb = []
251
+ for k in range(cfg.A):
252
+ s_, c = net.FA(f"eff{tag}_fa{k}", a_lsb[k], m_lsb[k], c)
253
+ s_lsb.append(s_)
254
+ return [s_lsb[cfg.A - 1 - k] for k in range(cfg.A)]
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  def read(addrbits, tag):
257
+ return net.OR(f"rd{tag}", [net.AND(f"rd{tag}a{s}", [net.DECODE(f"rd{tag}oh{s}", addrbits, s), sig[s]])
258
+ for s in range(cfg.S)])
259
+
260
+ contribs = []
261
+ for j in range(cfg.F):
262
+ b = slot_off(cfg, j)
263
+ addrbits = recbit[b:b + cfg.A]
264
+ hi, lo, idx = recbit[b + cfg.A], recbit[b + cfg.A + 1], recbit[b + cfg.A + 2]
265
+ v = read(eff_addr(addrbits, idx, f"r{j}"), f"{j}")
266
+ pos = net.AND(f"pos{j}", [net.NOT(f"nhi{j}", hi), lo])
267
+ neg = net.AND(f"neg{j}", [hi, net.NOT(f"nlo{j}", lo)])
268
+ mag = net.OR(f"mag{j}", [net.AND(f"tp{j}", [pos, v]), net.AND(f"tn{j}", [neg, v])])
269
+ contribs.append((mag, net.AND(f"sg{j}", [neg, v])))
270
+
271
+ W = cfg.ACC
272
+ bo, oo = field_off(cfg)
273
+ biasbits = recbit[bo:bo + cfg.BB]
274
+
275
+ def clsb(mag, sign):
276
+ return [mag] + [sign] * (W - 1)
277
+
278
+ def blsb():
279
+ lo = [biasbits[cfg.BB - 1 - k] for k in range(cfg.BB)]
280
+ return lo + [biasbits[0]] * (W - cfg.BB)
281
+
282
+ def ripple(pfx, a, b):
283
+ c = "#0"; out = []
284
+ for k in range(W):
285
+ s_, c = net.FA(f"{pfx}_fa{k}", a[k], b[k], c)
 
 
 
 
 
 
 
 
 
286
  out.append(s_)
287
  return out
288
 
289
+ acc = clsb(*contribs[0])
290
+ for j in range(1, cfg.F):
291
+ acc = ripple(f"acc{j}", acc, clsb(*contribs[j]))
292
+ acc = ripple("accb", acc, blsb())
293
+ out = net.NOT("out", acc[W - 1])
294
 
295
+ oea = eff_addr(recbit[oo:oo + cfg.A], recbit[oo + cfg.A], "w")
296
  nsig = []
297
+ for s in range(cfg.S):
298
+ sel = net.AND(f"wrsel{s}", [net.DECODE(f"wroh{s}", oea, s), nhalt])
 
299
  nsig.append(net.MUX(f"nsig{s}", sel, out, sig[s]))
300
 
301
+ gp_lsb = [gp[cfg.GPW - 1 - k] for k in range(cfg.GPW)]
 
302
  inc, c = [], "#1"
303
+ for k in range(cfg.GPW):
304
+ inc.append(net.XOR(f"gpi_x{k}", gp_lsb[k], c))
305
+ c = net.AND(f"gpi_c{k}", [gp_lsb[k], c])
306
+ inc_msb = [inc[cfg.GPW - 1 - k] for k in range(cfg.GPW)]
307
+ wrap = [net.MUX(f"gpw{k}", gp_oh[cfg.G - 1], "#0", inc_msb[k]) for k in range(cfg.GPW)]
308
+ ngp = [net.MUX(f"ngp{k}", nhalt, wrap[k], gp[k]) for k in range(cfg.GPW)]
309
+ nhalt_out = net.OR("halt_next", [halt, sig[cfg.HALT_SIG]])
310
 
311
+ return net, sig + gp + [halt], nsig + ngp + [nhalt_out]
312
 
313
+
314
+ # =============================================================================
315
+ # leveled sparse evaluator (scales past dense matrices)
316
+ # =============================================================================
317
+
318
+ class Leveled:
319
+ def __init__(self, net, inputs, outputs, device="cpu"):
320
+ self.device = device
321
+ self.inputs = inputs
322
+ gates = net.gates
323
+ indeg = {g: 0 for g in gates}
324
+ cons: Dict[str, List[str]] = {}
325
+ for g, (ins, _) in gates.items():
326
+ for s, _w in ins:
327
+ if s in gates:
328
+ indeg[g] += 1
329
+ cons.setdefault(s, []).append(g)
330
+ order = [g for g, d in indeg.items() if d == 0]
331
+ i = 0
332
+ while i < len(order):
333
+ for c in cons.get(order[i], []):
334
+ if c in gates:
335
+ indeg[c] -= 1
336
+ if indeg[c] == 0:
337
+ order.append(c)
338
+ i += 1
339
+ assert len(order) == len(gates), "cycle"
340
+ names = ["#0", "#1"] + inputs + order
341
+ slot = {n: i for i, n in enumerate(names)}
342
+ depth = {n: 0 for n in names if n not in gates}
343
+ for g in order:
344
+ depth[g] = 1 + max((depth[s] for s, _ in gates[g][0]), default=0)
345
+ by_level: Dict[int, List[str]] = {}
346
+ for g in order:
347
+ by_level.setdefault(depth[g], []).append(g)
348
+ self.n_sig = len(names)
349
+ self.plan = []
350
+ for lv in sorted(by_level):
351
+ gs = by_level[lv]
352
+ fan = max(len(gates[g][0]) for g in gs)
353
+ idx = torch.zeros(len(gs), fan, dtype=torch.long)
354
+ w = torch.zeros(len(gs), fan)
355
+ b = torch.zeros(len(gs))
356
+ out = torch.zeros(len(gs), dtype=torch.long)
357
+ for r, g in enumerate(gs):
358
+ ins, bias = gates[g]
359
+ out[r] = slot[g]; b[r] = bias
360
+ for c, (s, wt) in enumerate(ins):
361
+ idx[r, c] = slot[s]; w[r, c] = wt
362
+ self.plan.append((idx.to(device), w.to(device), b.to(device), out.to(device)))
363
+ self.in_slots = torch.tensor([slot[n] for n in inputs], device=device)
364
+ self.out_slots = torch.tensor([slot[n] for n in outputs], device=device)
365
+
366
+ def step(self, invec):
367
+ B = invec.shape[0]
368
+ V = torch.zeros(self.n_sig, B, device=self.device)
369
+ V[1] = 1.0
370
+ V[self.in_slots] = invec.T
371
+ for idx, w, b, out in self.plan:
372
+ g = V[idx]
373
+ V[out] = ((g * w[:, :, None]).sum(1) + b[:, None] >= 0).float()
374
+ return V[self.out_slots].T
375
+
376
+
377
+ def state_to_vec(cfg, st):
378
+ v = torch.zeros(cfg.STATE_BITS)
379
+ v[:cfg.S] = torch.tensor(st["sig"], dtype=torch.float32)
380
+ v[cfg.S:cfg.S + cfg.GPW] = torch.tensor(to_bits(st["gp"], cfg.GPW), dtype=torch.float32)
381
+ v[cfg.S + cfg.GPW] = float(st["halt"])
382
+ return v
383
+
384
+
385
+ def vec_to_state(cfg, v):
386
+ b = [int(round(float(x))) for x in (v.tolist() if hasattr(v, "tolist") else v)]
387
+ return {"sig": b[:cfg.S], "gp": from_bits(b[cfg.S:cfg.S + cfg.GPW]), "halt": b[cfg.S + cfg.GPW]}
388
+
389
+
390
+ class Runner:
391
+ def __init__(self, cfg, lev):
392
+ self.cfg = cfg
393
+ self.lev = lev
394
+
395
+ def step(self, st):
396
+ halt_in = st["halt"]
397
+ v = state_to_vec(self.cfg, st).unsqueeze(0).to(self.lev.device)
398
+ out = self.lev.step(v[:, :len(self.lev.inputs)])[0].cpu()
399
+ ns = vec_to_state(self.cfg, out)
400
+ ns["emit"] = apply_device(self.cfg, ns["sig"]) if halt_in == 0 else None
401
+ return ns
402
+
403
+ def run(self, st, n, collect=False):
404
+ emits = []
405
+ for _ in range(n):
406
+ st = self.step(st)
407
+ if collect and st.get("emit") is not None:
408
+ emits.append(st["emit"])
409
+ return (st, emits) if collect else st
410
 
411
 
412
  # =============================================================================
413
+ # compile a <=2-input family Net into a stored reflect netlist
414
  # =============================================================================
415
 
416
+ def topo(net):
417
+ gates = net.gates
418
+ indeg = {g: 0 for g in gates}
 
 
419
  cons: Dict[str, List[str]] = {}
420
+ for g, (ins, _) in gates.items():
421
  for s, _w in ins:
422
+ if s in gates:
423
  indeg[g] += 1
424
  cons.setdefault(s, []).append(g)
425
  order = [g for g, d in indeg.items() if d == 0]
426
  i = 0
427
  while i < len(order):
428
  for c in cons.get(order[i], []):
429
+ if c in gates:
430
  indeg[c] -= 1
431
  if indeg[c] == 0:
432
  order.append(c)
433
  i += 1
434
+ assert len(order) == len(gates)
435
+ return order
436
+
437
+
438
+ def compile_to_reflect(cfg, net, in_addr, out_names, work_start):
439
+ addr = dict(in_addr)
440
+ w = work_start
441
+ rgates = []
442
+ for g in topo(net):
443
+ ins, bias = net.gates[g]
444
+ assert len(ins) <= cfg.F
445
+ addr[g] = w; w += 1
446
+ slots = [(addr[s], wt, 0) for s, wt in ins]
447
+ rgates.append((slots, bias, (addr[g], 0)))
448
+ assert len(rgates) <= cfg.G, f"{len(rgates)} gates > bank {cfg.G}"
449
+ return rgates, addr
450
+
451
+
452
+ def adder_net(bits):
453
+ """N-bit ripple-carry adder from the family's full-adder cell."""
454
+ net = Net()
455
+ a = [f"a{i}" for i in range(bits)]
456
+ b = [f"b{i}" for i in range(bits)]
457
+ carry = "#0"
458
+ sums = []
459
+ for i in range(bits):
460
+ s_, carry = net.FA(f"fa{i}", a[i], b[i], carry)
461
+ sums.append(s_)
462
+ return net, a, b, sums, carry
463
 
464
 
465
  # =============================================================================
466
+ # programs (stored netlists) for the reflective demos
467
  # =============================================================================
468
 
469
+ def quine_program(cfg):
470
+ # emit sig[NET0 + ptr], strobe, advance ptr
471
+ return [([(cfg.NET0, 1, 1)], -1, (cfg.OUT_DATA, 0)), # OUT_DATA = NET0[ptr]
472
+ ([], 0, (cfg.OUT_STROBE, 0)), # OUT_STROBE = 1
473
+ ([], 0, (cfg.PTR_ADV, 0))] # PTR_ADV = 1
 
474
 
475
 
476
+ def copier_program(cfg):
477
+ # bank1[NET1 + ptr] = bank0[NET0 + ptr]; advance ptr
478
+ return [([(cfg.NET0, 1, 1)], -1, (cfg.WORK_BASE, 0)), # tmp = NET0[ptr]
479
+ ([(cfg.WORK_BASE, 1, 0)], -1, (cfg.NET1, 1)), # NET1[ptr] = tmp
480
+ ([], 0, (cfg.PTR_ADV, 0))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
 
483
  # =============================================================================
484
+ # build + save (compiles U to a recurrent ternary matrix stack)
485
  # =============================================================================
486
 
487
+ def build(cfg, save=True):
488
+ print(f"Interpreter U (S={cfg.S}, G={cfg.G}, F={cfg.F}, banks={cfg.banks}, "
489
+ f"record={cfg.R}b, acc={cfg.ACC}b)")
490
  t0 = time.perf_counter()
491
+ net, inputs, outputs = build_net(cfg)
492
+ print(f" {len(net.gates):,} threshold gates in {time.perf_counter() - t0:.1f}s")
493
+ if not save:
494
+ return net, inputs, outputs, None
495
+ t1 = time.perf_counter()
496
  layers, info = compile_net(net, inputs, outputs)
497
+ for Wt, _ in layers:
498
+ assert set(torch.unique(Wt).tolist()) <= {-1, 0, 1}
499
+ tensors = {f"matrix.layer{i:03d}.weight": Wt for i, (Wt, _) in enumerate(layers)}
500
+ tensors.update({f"matrix.layer{i:03d}.bias": B for i, (_, B) in enumerate(layers)})
501
+ for k, v in (("signals", cfg.S), ("gates", cfg.G), ("banks", cfg.banks),
502
+ ("state_bits", cfg.STATE_BITS), ("layers", info["layers"]), ("version", 2)):
503
+ tensors[f"manifest.{k}"] = torch.tensor([float(v)])
504
+ meta = {"machine": "reflect", "weight_quantization": "ternary",
505
+ "config": json.dumps({"A": cfg.A, "S": cfg.S, "G": cfg.G, "F": cfg.F,
506
+ "BB": cfg.BB, "banks": cfg.banks, "record_bits": cfg.R,
507
+ "acc_bits": cfg.ACC, "net_base": cfg.NET0})}
508
+ save_file(tensors, MODEL_PATH, metadata=meta)
509
+ print(f" compiled to {info['layers']} ternary matrices (max width "
510
+ f"{info['max_width']}, {info['total_weights']:,} weights) in "
511
+ f"{time.perf_counter() - t1:.1f}s; saved {MODEL_PATH} "
512
+ f"({os.path.getsize(MODEL_PATH) / 1e6:.1f} MB)")
 
 
 
513
  return net, inputs, outputs, layers
514
 
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  # =============================================================================
517
  # verify
518
  # =============================================================================
519
 
520
+ def _load(cfg, netlist, bank=0):
521
+ sig = [0] * cfg.S
522
+ base = cfg.bank_base(bank)
523
+ sig[base:base + len(netlist)] = netlist
524
+ return sig
525
 
526
 
527
+ def verify(cfg, device):
528
  ok = True
529
+ net, inputs, outputs = build_net(cfg)
530
+ lev = Leveled(net, inputs, outputs, device=device)
531
+ R = Runner(cfg, lev)
532
 
533
+ print("[1/6] Exhaustive single-gate semantics")
534
+ bad = total = 0
535
+ ra, rb, ro = cfg.WORK_BASE, cfg.WORK_BASE + 1, cfg.WORK_BASE + 2
 
 
 
 
536
  for w0 in (-1, 0, 1):
537
  for w1 in (-1, 0, 1):
538
+ for bias in range(-(1 << (cfg.BB - 1)), 1 << (cfg.BB - 1)):
539
+ nl = encode_netlist(cfg, [([(ra, w0, 0), (rb, w1, 0)], bias, (ro, 0))])
540
  for va in (0, 1):
541
  for vb in (0, 1):
542
+ sig = _load(cfg, nl); sig[ra], sig[rb] = va, vb
543
+ st = {"sig": sig, "gp": 0, "halt": 0}
544
+ g = R.step(st); e = ref_step(cfg, st)
 
 
545
  total += 1
546
+ exp = 1 if w0 * va + w1 * vb + bias >= 0 else 0
547
+ if {k: g[k] for k in ("sig", "gp", "halt")} != {k: e[k] for k in ("sig", "gp", "halt")} or g["sig"][ro] != exp:
548
  bad += 1
549
+ print(f" {'OK ' if bad == 0 else 'FAIL'} {total} configurations exact")
550
  ok &= bad == 0
551
 
552
+ print("[2/6] Single step vs reference on random full states")
 
553
  gen = torch.Generator().manual_seed(0x5EED)
554
+ N = 1500
555
+ sig = (torch.rand(N, cfg.S, generator=gen) < 0.5).long()
556
+ gpv = torch.randint(0, cfg.G, (N,), generator=gen)
557
  bad = 0
558
+ for i in range(N):
559
+ st = {"sig": sig[i].tolist(), "gp": int(gpv[i]), "halt": 0}
560
+ g = R.step(st); e = ref_step(cfg, st)
561
+ if {k: g[k] for k in ("sig", "gp", "halt")} != {k: e[k] for k in ("sig", "gp", "halt")}:
562
  bad += 1
563
+ hbad = sum(1 for i in range(200)
564
+ if R.step({"sig": sig[i].tolist(), "gp": int(gpv[i]), "halt": 1})["sig"] != sig[i].tolist())
565
+ print(f" {'OK ' if bad == 0 else 'FAIL'} {N} random states exact; "
566
+ f"{'OK ' if hbad == 0 else 'FAIL'} halted states are fixed points")
567
+ ok &= bad == 0 and hbad == 0
568
+
569
+ print("[3/6] Universality: interpret a family full-adder cell")
570
+ anet, an, bn, sums, cout = adder_net(1)
571
+ ia = {an[0]: cfg.WORK_BASE, bn[0]: cfg.WORK_BASE + 1}
572
+ rg, addr = compile_to_reflect(cfg, anet, ia, sums + [cout], cfg.WORK_BASE + 2)
573
+ nl = encode_netlist(cfg, rg)
574
+ abad = 0
575
+ for x in (0, 1):
576
+ for y in (0, 1):
577
+ sig = _load(cfg, nl)
578
+ sig[ia[an[0]]] = x; sig[ia[bn[0]]] = y
579
+ st = R.run({"sig": sig, "gp": 0, "halt": 0}, cfg.G * cfg.G)
580
+ s = st["sig"][addr[sums[0]]] | (st["sig"][addr[cout]] << 1)
581
+ if s != x + y:
582
+ abad += 1
583
+ print(f" {'OK ' if abad == 0 else 'FAIL'} full adder computed bit-exact "
584
+ f"through the interpreter ({len(rg)} gates, compiled from the family's Net)")
585
+ ok &= abad == 0
586
+
587
+ print("[4/6] Order independence (adder gates stored in random order)")
588
+ obad = 0
589
+ for seed in range(4):
590
+ perm = list(range(len(rg)))
591
+ random.Random(seed + 1).shuffle(perm)
592
+ nls = encode_netlist(cfg, [rg[p] for p in perm])
593
+ for x in (0, 1):
594
+ for y in (0, 1):
595
+ sig = _load(cfg, nls)
596
+ sig[ia[an[0]]] = x; sig[ia[bn[0]]] = y
597
+ st = R.run({"sig": sig, "gp": 0, "halt": 0}, cfg.G * cfg.G)
598
+ s = st["sig"][addr[sums[0]]] | (st["sig"][addr[cout]] << 1)
599
+ if s != x + y:
600
+ obad += 1
601
+ print(f" {'OK ' if obad == 0 else 'FAIL'} adder correct under 4 random gate "
602
+ f"orders (relaxation reproduces combinational evaluation)")
603
+ ok &= obad == 0
604
+
605
+ print("[5/6] Self-reproduction (quine): program emits its own encoding")
606
+ nlq = encode_netlist(cfg, quine_program(cfg))
607
+ K = 48
608
+ st = {"sig": _load(cfg, nlq), "gp": 0, "halt": 0}
609
+ _, emits = R.run(st, cfg.G * K + cfg.G, collect=True)
610
+ good = emits[:K] == nlq[:K]
611
+ print(f" {'OK ' if good else 'FAIL'} first {K} emitted bits == the program's "
612
+ f"own stored bits (streaming its description out the output device)")
613
+ ok &= good
614
+
615
+ print("[6/6] Metamorphosis: program fills the other bank and switches to it")
616
+ # (a) copier program fills bank1 with a copy of bank0
617
+ nlc = encode_netlist(cfg, copier_program(cfg))
618
+ st = {"sig": _load(cfg, nlc), "gp": 0, "halt": 0}
619
+ st = R.run(st, cfg.G * (cfg.G * cfg.R) + cfg.G)
620
+ filled = st["sig"][cfg.NET1:cfg.NET1 + cfg.G * cfg.R] == nlc
621
+ # (b) a program-set BANK flag switches the interpreter to a different machine
622
+ # preinstalled in bank1 (the full adder), which then runs.
623
+ nl_switch = encode_netlist(cfg, [([], 0, (cfg.BANK_SIG, 0))]) # BANK = 1
624
+ sig = _load(cfg, nl_switch, bank=0)
625
+ sig[cfg.NET1:cfg.NET1 + len(nl)] = nl # bank1 = adder
626
+ st = {"sig": sig, "gp": 0, "halt": 0}
627
+ st = R.run(st, 1) # gp0 sets BANK=1
628
+ st["gp"] = 0; st["sig"][cfg.WORK_BASE] = 1; st["sig"][cfg.WORK_BASE + 1] = 1
629
+ st = R.run(st, cfg.G * cfg.G) # now running bank1
630
+ switched = (st["sig"][cfg.BANK_SIG] == 1
631
+ and (st["sig"][addr[sums[0]]] | (st["sig"][addr[cout]] << 1)) == 2)
632
+ print(f" {'OK ' if filled else 'FAIL'} copier wrote bank0 into bank1; "
633
+ f"{'OK ' if switched else 'FAIL'} program set BANK and ran the bank-1 machine")
634
+ ok &= filled and switched
635
+
636
+ print("\nREFLECT v2 (universality, order, quine, metamorphosis):",
637
+ "PASS" if ok else "FAIL")
638
+ return ok
639
+
640
+
641
+ # =============================================================================
642
+ # analog: matrix == gate + noise / conductance-mismatch margins
643
+ # =============================================================================
644
+
645
+ def analog(cfg, device):
646
+ ok = True
647
+ net, inputs, outputs = build_net(cfg)
648
+ print("Compiling U to a recurrent ternary matrix stack...")
649
+ layers, info = compile_net(net, inputs, outputs)
650
+ print(f" {info['layers']} matrices, max width {info['max_width']}, "
651
+ f"{info['total_weights']:,} weights")
652
+ Wm = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
653
+ Bm = [B.to(device=device, dtype=torch.float32) for _, B in layers]
654
+ lev = Leveled(net, inputs, outputs, device=device)
655
+
656
+ def mstep(v, thresh=0.0, noise=0.0, gen=None, Wover=None):
657
+ for W, b in zip(Wover or Wm, Bm):
658
+ pre = v @ W.T + b
659
+ if noise:
660
+ pre = pre + torch.randn(pre.shape, generator=gen, device=v.device) * noise
661
+ v = (pre >= thresh).float()
662
+ return v
663
+
664
+ gen = torch.Generator().manual_seed(11)
665
+ ns = 300
666
+ V = torch.zeros(ns, cfg.STATE_BITS)
667
+ V[:, :cfg.S] = (torch.rand(ns, cfg.S, generator=gen) < 0.5).float()
668
+ gpv = torch.randint(0, cfg.G, (ns,), generator=gen)
669
+ for i in range(ns):
670
+ V[i, cfg.S:cfg.S + cfg.GPW] = torch.tensor(to_bits(int(gpv[i]), cfg.GPW), dtype=torch.float32)
671
+ V = V.to(device)
672
+ same = bool((mstep(V) == lev.step(V[:, :len(inputs)])).all())
673
+ print(f" {'OK ' if same else 'FAIL'} matrix step == gate-graph step on {ns} random states")
674
+ ok &= same
675
+
676
+ m = float("inf"); v = V.clone()
677
+ for W, b in zip(Wm, Bm):
678
+ pre = v @ W.T + b
679
+ m = min(m, float((pre + 0.5).abs().min()))
680
+ v = (pre >= 0).float()
681
+ print(f" measured minimum |pre-activation - (-0.5)|: {m:.3f} (guarantee 0.5)")
682
+ ok &= abs(m - 0.5) < 1e-6
683
+
684
+ base = state_to_vec(cfg, {"sig": _load(cfg, encode_netlist(cfg, quine_program(cfg))),
685
+ "gp": 0, "halt": 0}).unsqueeze(0).to(device)
686
+
687
+ def sweep(noise=0.0, gen=None, Wover=None):
688
+ v = base.clone()
689
+ for _ in range(cfg.G):
690
+ v = mstep(v, thresh=-0.5, noise=noise, gen=gen, Wover=Wover)
691
+ return v
692
+
693
+ ref = sweep()
694
+ print(" read noise per MVM (20 trials, full sweep bit-exact):")
695
+ for sigma in (0.05, 0.10, 0.20, 0.40):
696
+ good = sum(int(bool((sweep(noise=sigma, gen=torch.Generator(device=device).manual_seed(t)) == ref).all()))
697
+ for t in range(20))
698
+ print(f" sigma={sigma:.2f}: {good}/20 bit-exact")
699
+ if sigma <= 0.05 and good != 20: # deep within the 0.5 margin
700
+ ok = False
701
+
702
+ print(" conductance mismatch (8 instances, full sweep bit-exact):")
703
+ for sg in (0.05, 0.20):
704
+ good = 0
705
+ for t in range(8):
706
+ gg = torch.Generator().manual_seed(7 + t)
707
+ Wp = [W + (torch.randn(W.shape, generator=gg) * sg).to(device) * (W != 0) for W in Wm]
708
+ good += int(bool((sweep(Wover=Wp) == ref).all()))
709
+ print(f" sigma_G={sg:.2f}: {good}/8 bit-exact")
710
+ if sg <= 0.05 and good != 8:
711
+ ok = False
712
+
713
+ print("ANALOG (matrix == gate; bit-exact within the 0.5 margin):", "PASS" if ok else "FAIL")
714
  return ok
715
 
716
 
717
+ def main():
718
+ ap = argparse.ArgumentParser(description="neural_reflect v2")
719
+ ap.add_argument("cmd", choices=["build", "verify", "analog", "all"])
720
  ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
721
  args = ap.parse_args()
722
+ cfg = Cfg()
723
  rc = 0
724
+ if args.cmd in ("build", "all"):
725
+ build(cfg, save=True)
726
  if args.cmd in ("verify", "all"):
727
+ rc |= 0 if verify(cfg, args.device) else 1
728
+ if args.cmd in ("analog", "all"):
729
+ rc |= 0 if analog(cfg, args.device) else 1
730
  return rc
731
 
732
 
variants/neural_reflect.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:677c47e918126104af6c391b9f18eae30ee3759652548df144372b50abd9c81c
3
- size 8166918
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3a9c8eda3eef586ea00b4ff32e511223f54bff3ab6b4e151c69b5318cb106db2
3
+ size 157772226